Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52261f99d7 | ||
|
|
692dc154ef | ||
|
|
22101d8f4a | ||
|
|
f68db90b1f | ||
|
|
5e680531da | ||
|
|
93cd31018e | ||
|
|
277c570723 | ||
|
|
f1419a75f6 | ||
|
|
3af08a4727 | ||
|
|
a535cea85f | ||
|
|
29e443ed76 | ||
|
|
f95cddd05a | ||
|
|
ae28de4159 | ||
|
|
39de2c1d25 | ||
|
|
22570e08aa | ||
|
|
3b4ef4d238 | ||
|
|
c5a71c4304 | ||
|
|
4f37c2cb73 | ||
|
|
c1ec97055c | ||
|
|
086c71126f | ||
|
|
10a78c1c94 | ||
|
|
cf6021d898 | ||
|
|
ff322cd2dd | ||
|
|
1a96d3c38e | ||
|
|
cfe84963fa | ||
|
|
d908a599b1 | ||
|
|
9b3ddb8da3 | ||
|
|
56d994a69d | ||
|
|
67cd17c604 | ||
|
|
f4fb7eb8b7 | ||
|
|
b9021e4331 | ||
|
|
3583836d3e | ||
|
|
278815119f | ||
|
|
1bb678e455 | ||
|
|
b9e5fc604b | ||
|
|
b65e8c48ce | ||
|
|
e59bc1a08e | ||
|
|
ff994629de | ||
|
|
a458b9bc88 | ||
|
|
2834e25151 | ||
|
|
c2582fe055 | ||
|
|
0980219c8d | ||
|
|
c5f158f1cf | ||
|
|
62afc023c8 | ||
|
|
52ca84946b | ||
|
|
e64fb39c32 | ||
|
|
1066598150 | ||
|
|
caed8c2cf0 | ||
|
|
663be2402d | ||
|
|
6fd24c842f | ||
|
|
302631c4fa | ||
|
|
8a94623b2b | ||
|
|
016dfdb455 | ||
|
|
097415385e | ||
|
|
8e6c6e04a3 | ||
|
|
81a30e580e | ||
|
|
676efe7253 | ||
|
|
39e0e77824 | ||
|
|
363f5be8ff | ||
|
|
df15fa2f0e | ||
|
|
e8c1fbb86f | ||
|
|
df986b9ecf | ||
|
|
daabf4bab9 | ||
|
|
3095948024 | ||
|
|
fad289305f | ||
|
|
97d8f07e0d | ||
|
|
dc374c7ce9 | ||
|
|
1584475bc3 | ||
|
|
8f1db25c5c | ||
|
|
092b536009 | ||
|
|
f95bbaa0f7 | ||
|
|
bc1f6464d3 | ||
|
|
b828cd5975 | ||
|
|
766b4f7971 | ||
|
|
45e2ffd71e | ||
|
|
98757c3f11 | ||
|
|
9aed6d350b | ||
|
|
ca6ce6db32 | ||
|
|
1c9a6a02af | ||
|
|
ba9bafcb5f | ||
|
|
0628394122 | ||
|
|
99b2cd2ad0 | ||
|
|
707a6ecbaa | ||
|
|
aa2bc40f03 | ||
|
|
5e00e76c4b | ||
|
|
0dba992dd4 | ||
|
|
b6f61cac9b | ||
|
|
0e41945a8a | ||
|
|
a05e037308 | ||
|
|
f40576c39d | ||
|
|
9fc810182a | ||
|
|
dd7a52ba08 | ||
|
|
fa6d2a3080 | ||
|
|
36efc0c877 | ||
|
|
e9efcf1b92 | ||
|
|
f4ad1ec8e7 | ||
|
|
0027c21630 | ||
|
|
6173cab99f | ||
|
|
80c8097a71 | ||
|
|
ba3d577125 | ||
|
|
8ce4ebb16e | ||
|
|
4358ff2338 | ||
|
|
53f9eb083d | ||
|
|
0a3a982cb9 | ||
|
|
c30239b3a8 | ||
|
|
183834689d | ||
|
|
5da2f3279b | ||
|
|
ec7d87e757 | ||
|
|
4155f69e3c | ||
|
|
473e93ea16 | ||
|
|
fd1941cc3e | ||
|
|
dbb51b58db | ||
|
|
e7d00cfe54 | ||
|
|
63453fa962 | ||
|
|
689eb7f87b | ||
|
|
67a4646a50 | ||
|
|
a4e0535464 | ||
|
|
61495cd428 | ||
|
|
9e7b7415a5 | ||
|
|
06a462d48c |
17
.github/resources/export-options-ad-hoc.plist
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>ad-hoc</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>com.8bit.bitwarden</key>
|
||||
<string>Ad hoc: Bitwarden 2020</string>
|
||||
<key>com.8bit.bitwarden.autofill</key>
|
||||
<string>Ad hoc: Autofill 2020</string>
|
||||
<key>com.8bit.bitwarden.find-login-action-extension</key>
|
||||
<string>Ad hoc: Extension 2020</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
17
.github/resources/export-options-app-store.plist
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>app-store</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>com.8bit.bitwarden</key>
|
||||
<string>Dist: Bitwarden 2020</string>
|
||||
<key>com.8bit.bitwarden.autofill</key>
|
||||
<string>Dist: Autofill 2020</string>
|
||||
<key>com.8bit.bitwarden.find-login-action-extension</key>
|
||||
<string>Dist: Extension 2020</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
13
.github/scripts/android/build.ps1
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string] $configuration
|
||||
)
|
||||
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$androidPath = $($rootPath + "/src/Android/Android.csproj");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Build $configuration Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/p:Configuration=$configuration"
|
||||
75
.github/scripts/android/clean-fdroid.ps1
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
|
||||
$androidPath = $($rootPath + "/src/Android/Android.csproj");
|
||||
$appPath = $($rootPath + "/src/App/App.csproj");
|
||||
|
||||
$androidManifest = $($rootPath + "/src/Android/Properties/AndroidManifest.xml");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Clean Android and App"
|
||||
Write-Output "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:Clean" "/p:Configuration=FDroid"
|
||||
msbuild "$($appPath)" "/t:Clean" "/p:Configuration=FDroid"
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Backup project files"
|
||||
Write-Output "########################################"
|
||||
|
||||
Copy-Item $androidManifest $($androidManifest + ".original");
|
||||
Copy-Item $androidPath $($androidPath + ".original");
|
||||
Copy-Item $appPath $($appPath + ".original");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Cleanup Android Manifest"
|
||||
Write-Output "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidManifest);
|
||||
|
||||
$nsAndroid=New-Object System.Xml.XmlNamespaceManager($xml.NameTable);
|
||||
$nsAndroid.AddNamespace("android", "http://schemas.android.com/apk/res/android");
|
||||
|
||||
$firebaseReceiver1=$xml.SelectSingleNode(`
|
||||
"/manifest/application/receiver[@android:name='com.google.firebase.iid.FirebaseInstanceIdInternalReceiver']", `
|
||||
$nsAndroid);
|
||||
$firebaseReceiver1.ParentNode.RemoveChild($firebaseReceiver1);
|
||||
|
||||
$firebaseReceiver2=$xml.SelectSingleNode(`
|
||||
"/manifest/application/receiver[@android:name='com.google.firebase.iid.FirebaseInstanceIdReceiver']", `
|
||||
$nsAndroid);
|
||||
$firebaseReceiver2.ParentNode.RemoveChild($firebaseReceiver2);
|
||||
|
||||
$xml.Save($androidManifest);
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Uninstall from Android.csproj"
|
||||
Write-Output "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidPath);
|
||||
|
||||
$ns=New-Object System.Xml.XmlNamespaceManager($xml.NameTable);
|
||||
$ns.AddNamespace("ns", $xml.DocumentElement.NamespaceURI);
|
||||
|
||||
$firebaseNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.Firebase.Messaging']", $ns);
|
||||
$firebaseNode.ParentNode.RemoveChild($firebaseNode);
|
||||
|
||||
$safetyNetNode=$xml.SelectSingleNode(`
|
||||
"/ns:Project/ns:ItemGroup/ns:PackageReference[@Include='Xamarin.GooglePlayServices.SafetyNet']", $ns);
|
||||
$safetyNetNode.ParentNode.RemoveChild($safetyNetNode);
|
||||
|
||||
$xml.Save($androidPath);
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Uninstall from App.csproj"
|
||||
Write-Output "########################################"
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($appPath);
|
||||
|
||||
$appCenterNode=$xml.SelectSingleNode("/Project/ItemGroup/PackageReference[@Include='Microsoft.AppCenter.Crashes']");
|
||||
$appCenterNode.ParentNode.RemoveChild($appCenterNode);
|
||||
|
||||
$xml.Save($appPath);
|
||||
24
.github/scripts/android/compile-fdroid.sh
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd $GITHUB_WORKSPACE
|
||||
mkdir dist
|
||||
cp CNAME ./dist
|
||||
cd store
|
||||
chmod 600 fdroid/config.py fdroid/keystore.jks
|
||||
mkdir -p temp/fdroid
|
||||
TEMP_DIR="$GITHUB_WORKSPACE/store/temp/fdroid"
|
||||
cd fdroid
|
||||
echo "keypass=\"$FDROID_STORE_KEYSTORE_PASSWORD\"" >>config.py
|
||||
echo "keystorepass=\"$FDROID_STORE_KEYSTORE_PASSWORD\"" >>config.py
|
||||
echo "local_copy_dir=\"$TEMP_DIR\"" >>config.py
|
||||
mkdir -p repo
|
||||
curl -Lo repo/com.x8bit.bitwarden-fdroid.apk \
|
||||
https://github.com/bitwarden/mobile/releases/download/$RELEASE_TAG_NAME/com.x8bit.bitwarden-fdroid.apk
|
||||
fdroid update
|
||||
fdroid server update
|
||||
cd ..
|
||||
rm -rf temp/fdroid/archive
|
||||
mv -v temp/fdroid ../dist
|
||||
cd fdroid
|
||||
cp index.html btn.png qr.png ../../dist/fdroid
|
||||
cd $GITHUB_WORKSPACE
|
||||
22
.github/scripts/android/decrypt-secrets.ps1
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
|
||||
$decryptSecretPath = $($rootPath + "/.github/scripts/decrypt-secret.ps1");
|
||||
|
||||
$appKeystorePlayFilename = "app_play-keystore.jks";
|
||||
$appKeystorePlayPath = $($rootPath + "/src/Android/$appKeystorePlayFilename");
|
||||
$appKeystoreUploadFilename = "app_upload-keystore.jks";
|
||||
$appKeystoreUploadPath = $($rootPath + "/src/Android/$appKeystoreUploadFilename");
|
||||
$appKeystoreFdroidFilename = "app_fdroid-keystore.jks";
|
||||
$appKeystoreFdroidPath = $($rootPath + "/src/Android/$appKeystoreFdroidFilename");
|
||||
$googleServicesFilename = "google-services.json";
|
||||
$googleServicesPath = $($rootPath + "/src/Android/$googleServicesFilename");
|
||||
|
||||
Invoke-Expression `
|
||||
"& `"$decryptSecretPath`" -filename $($appKeystorePlayFilename + ".gpg") -output $($appKeystorePlayPath)"
|
||||
Invoke-Expression `
|
||||
"& `"$decryptSecretPath`" -filename $($appKeystoreUploadFilename + ".gpg") -output $($appKeystoreUploadPath)"
|
||||
Invoke-Expression `
|
||||
"& `"$decryptSecretPath`" -filename $($appKeystoreFdroidFilename + ".gpg") -output $($appKeystoreFdroidPath)"
|
||||
Invoke-Expression `
|
||||
"& `"$decryptSecretPath`" -filename $($googleServicesFilename + ".gpg") -output $($googleServicesPath)"
|
||||
Invoke-Expression "& `"$decryptSecretPath`" -filename play_creds.json.gpg"
|
||||
9
.github/scripts/android/deploy-play.ps1
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$homePath = Resolve-Path "~" | Select-Object -ExpandProperty Path;
|
||||
|
||||
$publisherPath = $($rootPath + "/store/google/Publisher/bin/Release/netcoreapp2.0/Publisher.dll");
|
||||
$credsPath = $($homePath + "/secrets/play_creds.json");
|
||||
$aabPath = $($rootPath + "/com.x8bit.bitwarden.aab");
|
||||
$track = "internal";
|
||||
|
||||
dotnet $publisherPath $credsPath $aabPath $track
|
||||
16
.github/scripts/android/increment-version.ps1
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$buildNumber = 3000 + [int]$env:GITHUB_RUN_NUMBER;
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Setting Version Code $buildNumber"
|
||||
Write-Output "########################################"
|
||||
|
||||
$androidManifest = $($rootPath + "/src/Android/Properties/AndroidManifest.xml");
|
||||
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($androidManifest);
|
||||
|
||||
$node=$xml.SelectNodes("/manifest");
|
||||
$node.SetAttribute("android:versionCode", [string]$buildNumber);
|
||||
|
||||
$xml.Save($androidManifest);
|
||||
23
.github/scripts/android/sign-fdroid.ps1
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
|
||||
$androidPath = $($rootPath + "/src/Android/Android.csproj");
|
||||
|
||||
$appKeystoreFdroidFilename = "app_fdroid-keystore.jks";
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Sign FDroid Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=FDroid" "/p:AndroidKeyStore=true" `
|
||||
"/p:AndroidSigningKeyAlias=bitwarden" "/p:AndroidSigningKeyPass=$($env:FDROID_KEYSTORE_PASSWORD)" `
|
||||
"/p:AndroidSigningKeyStore=$($appKeystoreFdroidFilename)" `
|
||||
"/p:AndroidSigningStorePass=$($env:FDROID_KEYSTORE_PASSWORD)" "/v:quiet"
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy FDroid apk to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedApkPath = $($rootPath + "/src/Android/bin/FDroid/com.x8bit.bitwarden-Signed.apk");
|
||||
$signedApkDestPath = $($rootPath + "/com.x8bit.bitwarden-fdroid.apk");
|
||||
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
42
.github/scripts/android/sign-play.ps1
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
|
||||
$androidPath = $($rootPath + "/src/Android/Android.csproj");
|
||||
|
||||
$appKeystorePlayFilename = "app_play-keystore.jks";
|
||||
$appKeystoreUploadFilename = "app_upload-keystore.jks";
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Sign Google Play Bundle Release Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=Release" "/p:AndroidKeyStore=true" `
|
||||
"/p:AndroidSigningKeyAlias=upload" "/p:AndroidSigningKeyPass=$($env:UPLOAD_KEYSTORE_PASSWORD)" `
|
||||
"/p:AndroidSigningKeyStore=$($appKeystoreUploadFilename)" `
|
||||
"/p:AndroidSigningStorePass=$($env:UPLOAD_KEYSTORE_PASSWORD)" "/p:AndroidPackageFormat=aab" "/v:quiet"
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy Google Play Bundle to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedAabPath = $($rootPath + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.aab");
|
||||
$signedAabDestPath = $($rootPath + "/com.x8bit.bitwarden.aab");
|
||||
|
||||
Copy-Item $signedAabPath $signedAabDestPath
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Sign APK Release Configuration"
|
||||
Write-Output "########################################"
|
||||
|
||||
msbuild "$($androidPath)" "/t:SignAndroidPackage" "/p:Configuration=Release" "/p:AndroidKeyStore=true" `
|
||||
"/p:AndroidSigningKeyAlias=bitwarden" "/p:AndroidSigningKeyPass=$($env:PLAY_KEYSTORE_PASSWORD)" `
|
||||
"/p:AndroidSigningKeyStore=$($appKeystorePlayFilename)" `
|
||||
"/p:AndroidSigningStorePass=$($env:PLAY_KEYSTORE_PASSWORD)" "/v:quiet"
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Copy Release APK to project root"
|
||||
Write-Output "########################################"
|
||||
|
||||
$signedApkPath = $($rootPath + "/src/Android/bin/Release/com.x8bit.bitwarden-Signed.apk");
|
||||
$signedApkDestPath = $($rootPath + "/com.x8bit.bitwarden.apk");
|
||||
|
||||
Copy-Item $signedApkPath $signedApkDestPath
|
||||
29
.github/scripts/decrypt-secret.ps1
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string] $filename,
|
||||
[string] $output
|
||||
)
|
||||
|
||||
$homePath = Resolve-Path "~" | Select-Object -ExpandProperty Path
|
||||
$rootPath = $env:GITHUB_WORKSPACE
|
||||
|
||||
$secretInputPath = $rootPath + "/.github/secrets"
|
||||
$input = $secretInputPath + "/" + $filename
|
||||
|
||||
$passphrase = $env:DECRYPT_FILE_PASSWORD
|
||||
$secretOutputPath = $homePath + "/secrets"
|
||||
|
||||
if ([string]::IsNullOrEmpty($output)) {
|
||||
if ($filename.EndsWith(".gpg")) {
|
||||
$output = $secretOutputPath + "/" + $filename.TrimEnd(".gpg")
|
||||
} else {
|
||||
$output = $secretOutputPath + "/" + $filename + ".plaintext"
|
||||
}
|
||||
}
|
||||
|
||||
if (!(Test-Path -Path $secretOutputPath))
|
||||
{
|
||||
New-Item -ItemType Directory -Path $secretOutputPath
|
||||
}
|
||||
|
||||
gpg --quiet --batch --yes --decrypt --passphrase="$passphrase" --output $output $input
|
||||
29
.github/scripts/ios/build.ps1
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string] $configuration,
|
||||
[string] $platform = "iPhone",
|
||||
[switch] $archive
|
||||
)
|
||||
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$iosPath = $($rootPath + "/src/iOS/iOS.csproj");
|
||||
|
||||
if ($archive)
|
||||
{
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Archive $configuration Configuration for $platform Platform"
|
||||
Write-Output "########################################"
|
||||
msbuild "$($iosPath)" "/p:Platform=$platform" "/p:Configuration=$configuration" `
|
||||
"/p:ArchiveOnBuild=true" "/t:`"Build`""
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Done"
|
||||
Write-Output "########################################"
|
||||
ls ~/Library/Developer/Xcode/Archives
|
||||
} else
|
||||
{
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Build $configuration Configuration for $platform Platform"
|
||||
Write-Output "########################################"
|
||||
msbuild "$($iosPath)" "/p:Platform=$platform" "/p:Configuration=$configuration" "/t:`"Build`""
|
||||
}
|
||||
9
.github/scripts/ios/decrypt-secrets.ps1
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
|
||||
$decryptSecretPath = $($rootPath + "/.github/scripts/decrypt-secret.ps1");
|
||||
|
||||
Invoke-Expression "& `"$decryptSecretPath`" -filename bitwarden-mobile-key.p12.gpg"
|
||||
Invoke-Expression "& `"$decryptSecretPath`" -filename iphone-distribution-cert.p12.gpg"
|
||||
Invoke-Expression "& `"$decryptSecretPath`" -filename dist_autofill.mobileprovision.gpg"
|
||||
Invoke-Expression "& `"$decryptSecretPath`" -filename dist_bitwarden.mobileprovision.gpg"
|
||||
Invoke-Expression "& `"$decryptSecretPath`" -filename dist_extension.mobileprovision.gpg"
|
||||
5
.github/scripts/ios/deploy-app-store.ps1
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$ipaPath = "$rootPath/bitwarden-export/Bitwarden.ipa"
|
||||
|
||||
xcrun altool --upload-app --type ios --file "$ipaPath" `
|
||||
--username "$env:APPLE_ID_USERNAME" --password "$env:APPLE_ID_PASSWORD"
|
||||
13
.github/scripts/ios/export-ipa.ps1
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string] $method
|
||||
)
|
||||
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$homePath = Resolve-Path "~" | Select-Object -ExpandProperty Path
|
||||
|
||||
$exportOptionsPath = "$rootPath/.github/resources/export-options-$method.plist";
|
||||
$archivePath = "$homePath/Library/Developer/Xcode/Archives/*/*.xcarchive";
|
||||
$exportPath = "$rootPath/bitwarden-export";
|
||||
|
||||
xcodebuild -exportArchive -archivePath $archivePath -exportPath $exportPath -exportOptionsPlist $exportOptionsPath
|
||||
26
.github/scripts/ios/increment-version.ps1
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
$rootPath = $env:GITHUB_WORKSPACE;
|
||||
$buildNumber = 100 + [int]$env:GITHUB_RUN_NUMBER;
|
||||
|
||||
$bitwardenInfo = $($rootPath + "/src/iOS/Info.plist");
|
||||
$extensionInfo = $($rootPath + "/src/iOS.Extension/Info.plist");
|
||||
$autofillInfo = $($rootPath + "/src/iOS.Autofill/Info.plist");
|
||||
|
||||
Write-Output "########################################"
|
||||
Write-Output "##### Setting CFBundleVersion $buildNumber"
|
||||
Write-Output "########################################"
|
||||
|
||||
function Update-Version($file) {
|
||||
$xml=New-Object XML;
|
||||
$xml.Load($file);
|
||||
|
||||
Select-Xml -xml $xml -XPath "//dict/key[. = 'CFBundleVersion']/following-sibling::string[1]" |
|
||||
%{
|
||||
$_.Node.InnerXml = $buildNumber
|
||||
}
|
||||
|
||||
$xml.Save($file);
|
||||
}
|
||||
|
||||
Update-Version $bitwardenInfo
|
||||
Update-Version $extensionInfo
|
||||
Update-Version $autofillInfo
|
||||
13
.github/scripts/ios/setup-keychain.ps1
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
$homePath = Resolve-Path "~" | Select-Object -ExpandProperty Path;
|
||||
$secretsPath = $homePath + "/secrets"
|
||||
|
||||
$mobileKeyPath = $($secretsPath + "/bitwarden-mobile-key.p12");
|
||||
$distCertPath = $($secretsPath + "/iphone-distribution-cert.p12");
|
||||
|
||||
security create-keychain -p $env:KEYCHAIN_PASSWORD build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p $env:KEYCHAIN_PASSWORD build.keychain
|
||||
security set-keychain-settings -lut 1200 build.keychain
|
||||
security import $mobileKeyPath -k build.keychain -P $env:MOBILE_KEY_PASSWORD -T /usr/bin/codesign -T /usr/bin/security
|
||||
security import $distCertPath -k build.keychain -P $env:DIST_CERT_PASSWORD -T /usr/bin/codesign -T /usr/bin/security
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $env:KEYCHAIN_PASSWORD build.keychain
|
||||
21
.github/scripts/ios/setup-profiles.ps1
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
$homePath = Resolve-Path "~" | Select-Object -ExpandProperty Path;
|
||||
$secretsPath = $homePath + "/secrets"
|
||||
|
||||
$autofillProfilePath = $($secretsPath + "/dist_autofill.mobileprovision");
|
||||
$bitwardenProfilePath = $($secretsPath + "/dist_bitwarden.mobileprovision");
|
||||
$extensionProfilePath = $($secretsPath + "/dist_extension.mobileprovision");
|
||||
$profilesDirPath = "~/Library/MobileDevice/Provisioning Profiles"
|
||||
|
||||
if (!(Test-Path -Path $profilesDirPath))
|
||||
{
|
||||
New-Item -ItemType Directory -Path $profilesDirPath
|
||||
}
|
||||
|
||||
$autofill_uuid = grep UUID -A1 -a $autofillProfilePath | grep -io "[-A-F0-9]\{36\}"
|
||||
Copy-Item $autofillProfilePath -destination "$profilesDirPath/$autofill_uuid.mobileprovision"
|
||||
|
||||
$bitwarden_uuid = grep UUID -A1 -a $bitwardenProfilePath | grep -io "[-A-F0-9]\{36\}"
|
||||
Copy-Item $bitwardenProfilePath -destination "$profilesDirPath/$bitwarden_uuid.mobileprovision"
|
||||
|
||||
$extension_uuid = grep UUID -A1 -a $extensionProfilePath | grep -io "[-A-F0-9]\{36\}"
|
||||
Copy-Item $extensionProfilePath -destination "$profilesDirPath/$extension_uuid.mobileprovision"
|
||||
BIN
.github/secrets/app_fdroid-keystore.jks.gpg
vendored
Normal file
BIN
.github/secrets/app_play-keystore.jks.gpg
vendored
Normal file
BIN
.github/secrets/app_upload-keystore.jks.gpg
vendored
Normal file
BIN
.github/secrets/bitwarden-mobile-key.p12.gpg
vendored
Normal file
BIN
.github/secrets/dist_autofill.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_bitwarden.mobileprovision.gpg
vendored
Normal file
BIN
.github/secrets/dist_extension.mobileprovision.gpg
vendored
Normal file
3
.github/secrets/google-services.json.gpg
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
Œ
|
||||
KØY#æ(˜õÒéEIÖ<49>ß„T?)lÿ´¬—üƒ"=Ä|¯'e•Àm…/~åè'F±É>Üæ÷‚lŒb¶[ý+RöÌiL‘Ÿ"”ñê~V:£³pÜañÚµel%8t„â튖Ôy<¶nÓâÌaU‡wêJD¡ª•ìWe¦9ýÑôž<x8d»O®j\€×“ëûóVq¤¹Ö‹
|
||||
Ǻ½-Î#ªîƒó]$›(íl,×Br†«¬d¾·®Â•a-±¥…: Ù:œ„9b,!EmµŽÐQf®DçgˆÑ©x(PìÈ¡~ÊÍ¹êµ –š[§‡!:×;fÜý
|
||||
BIN
.github/secrets/iphone-distribution-cert.p12.gpg
vendored
Normal file
BIN
.github/secrets/play_creds.json.gpg
vendored
Normal file
BIN
.github/secrets/store_fdroid-keystore.jks.gpg
vendored
Normal file
299
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,299 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
- 'gh-pages'
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
|
||||
cloc:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up cloc
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install cloc
|
||||
|
||||
- name: Print lines of code
|
||||
run: cloc --vcs git --exclude-dir Resources,store,test,Properties --include-lang C#,XAML
|
||||
|
||||
android:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Set up NuGet
|
||||
uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-version: 'latest'
|
||||
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@v1.0.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
nuget help
|
||||
msbuild -version
|
||||
dotnet --info
|
||||
Write-Output "GitHub ref: $env:GITHUB_REF"
|
||||
Write-Output "GitHub event: $env:GITHUB_EVENT"
|
||||
shell: pwsh
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT: ${{ github.event_name }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Decrypt secrets
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/android/decrypt-secrets.ps1
|
||||
shell: pwsh
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
|
||||
- name: Increment version
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/android/increment-version.ps1
|
||||
shell: pwsh
|
||||
|
||||
- name: Restore packages
|
||||
run: nuget restore
|
||||
|
||||
- name: Build Play Store publisher
|
||||
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release
|
||||
|
||||
- name: Build for Play Store
|
||||
run: ./.github/scripts/android/build.ps1 -configuration Release
|
||||
shell: pwsh
|
||||
|
||||
- name: Sign for Play Store
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/android/sign-play.ps1
|
||||
shell: pwsh
|
||||
env:
|
||||
PLAY_KEYSTORE_PASSWORD: ${{ secrets.PLAY_KEYSTORE_PASSWORD }}
|
||||
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
|
||||
|
||||
- name: Upload Play Store .aab artifact
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: com.x8bit.bitwarden.aab
|
||||
path: ./com.x8bit.bitwarden.aab
|
||||
|
||||
- name: Upload Play Store .apk artifact
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: com.x8bit.bitwarden.apk
|
||||
path: ./com.x8bit.bitwarden.apk
|
||||
|
||||
- name: Clean for F-Droid
|
||||
run: ./.github/scripts/android/clean-fdroid.ps1
|
||||
shell: pwsh
|
||||
|
||||
- name: Restore packages
|
||||
run: nuget restore
|
||||
|
||||
- name: Build for F-Droid
|
||||
run: ./.github/scripts/android/build.ps1 -configuration FDroid
|
||||
shell: pwsh
|
||||
|
||||
- name: Sign for F-Droid
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/android/sign-fdroid.ps1
|
||||
shell: pwsh
|
||||
env:
|
||||
FDROID_KEYSTORE_PASSWORD: ${{ secrets.FDROID_KEYSTORE_PASSWORD }}
|
||||
|
||||
- name: Upload F-Droid .apk artifact
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: com.x8bit.bitwarden-fdroid.apk
|
||||
path: ./com.x8bit.bitwarden-fdroid.apk
|
||||
|
||||
- name: Deploy to Play Store
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/android/deploy-play.ps1
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload release assets
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
hub release edit `
|
||||
-a ./com.x8bit.bitwarden.aab `
|
||||
-a ./com.x8bit.bitwarden.apk `
|
||||
-a ./com.x8bit.bitwarden-fdroid.apk `
|
||||
-m "Version $($env:RELEASE_TAG_NAME.TrimStart('v'))" `
|
||||
$env:RELEASE_TAG_NAME
|
||||
shell: pwsh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
|
||||
android-ubuntu:
|
||||
runs-on: ubuntu-latest
|
||||
needs: android
|
||||
|
||||
steps:
|
||||
- name: Set up Node
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '10.x'
|
||||
|
||||
- name: Set up F-Droid server
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get -qqy install --no-install-recommends fdroidserver wget
|
||||
|
||||
- name: Set up git credentials
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
run: |
|
||||
git config --global credential.helper store
|
||||
echo "https://${ACCESS_TOKEN}:x-oauth-basic@github.com" >> ~/.git-credentials
|
||||
git config --global user.email "ci@bitwarden.com"
|
||||
git config --global user.name "Bitwarden CI"
|
||||
|
||||
- name: Print environment
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
git --version
|
||||
Write-Output "GitHub ref: $env:GITHUB_REF"
|
||||
Write-Output "GitHub event: $env:GITHUB_EVENT"
|
||||
shell: pwsh
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT: ${{ github.event_name }}
|
||||
|
||||
- name: Checkout repo
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Node dependencies
|
||||
if: github.event_name == 'release'
|
||||
run: npm install
|
||||
|
||||
- name: Decrypt secrets
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
./.github/scripts/decrypt-secret.ps1 -filename store_fdroid-keystore.jks.gpg `
|
||||
-output ./store/fdroid/keystore.jks
|
||||
shell: pwsh
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
|
||||
- name: Compile for F-Droid Store
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
sudo chmod +x ./.github/scripts/android/compile-fdroid.sh
|
||||
./.github/scripts/android/compile-fdroid.sh
|
||||
env:
|
||||
FDROID_STORE_KEYSTORE_PASSWORD: ${{ secrets.FDROID_STORE_KEYSTORE_PASSWORD }}
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Deploy to gh-pages
|
||||
if: github.event_name == 'release'
|
||||
run: npm run deploy
|
||||
|
||||
ios:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Print environment
|
||||
run: |
|
||||
nuget help
|
||||
msbuild -version
|
||||
dotnet --info
|
||||
Write-Output "GitHub ref: $env:GITHUB_REF"
|
||||
Write-Output "GitHub event: $env:GITHUB_EVENT"
|
||||
shell: pwsh
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
GITHUB_EVENT: ${{ github.event_name }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Decrypt secrets
|
||||
run: ./.github/scripts/ios/decrypt-secrets.ps1
|
||||
shell: pwsh
|
||||
env:
|
||||
DECRYPT_FILE_PASSWORD: ${{ secrets.DECRYPT_FILE_PASSWORD }}
|
||||
|
||||
- name: Increment version
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/ios/increment-version.ps1
|
||||
shell: pwsh
|
||||
|
||||
- name: Set up keychain
|
||||
run: ./.github/scripts/ios/setup-keychain.ps1
|
||||
shell: pwsh
|
||||
env:
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
|
||||
MOBILE_KEY_PASSWORD: ${{ secrets.IOS_KEY_PASSWORD }}
|
||||
DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
|
||||
|
||||
- name: Set up provisioning profiles
|
||||
run: ./.github/scripts/ios/setup-profiles.ps1
|
||||
shell: pwsh
|
||||
|
||||
- name: Restore packages
|
||||
run: nuget restore
|
||||
|
||||
- name: Archive Build for App Store
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/ios/build.ps1 -configuration AppStore -platform iPhone -archive
|
||||
shell: pwsh
|
||||
|
||||
- name: Build for App Store
|
||||
if: github.ref != 'refs/heads/master'
|
||||
run: ./.github/scripts/ios/build.ps1 -configuration AppStore -platform iPhone
|
||||
shell: pwsh
|
||||
|
||||
- name: Export .ipa for App Store
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/ios/export-ipa.ps1 -method app-store
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload App Store .ipa artifact
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Bitwarden.ipa
|
||||
path: ./bitwarden-export/Bitwarden.ipa
|
||||
|
||||
- name: Deploy to App Store
|
||||
if: github.ref == 'refs/heads/master' || github.event_name == 'release'
|
||||
run: ./.github/scripts/ios/deploy-app-store.ps1
|
||||
shell: pwsh
|
||||
env:
|
||||
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
|
||||
- name: Upload release assets
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
hub release edit `
|
||||
-a ./bitwarden-export/Bitwarden.ipa `
|
||||
-m "Version $($env:RELEASE_TAG_NAME.TrimStart('v'))" `
|
||||
$env:RELEASE_TAG_NAME
|
||||
shell: pwsh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
@@ -24,6 +24,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
.editorconfig = .editorconfig
|
||||
.gitignore = .gitignore
|
||||
appveyor.yml = appveyor.yml
|
||||
.github\workflows\build.yml = .github\workflows\build.yml
|
||||
CONTRIBUTING.md = CONTRIBUTING.md
|
||||
crowdin.yml = crowdin.yml
|
||||
README.md = README.md
|
||||
@@ -238,22 +239,22 @@ Global
|
||||
{256F9E44-0AF5-4D97-A2F9-DA26080C0A5D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|Any CPU.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.Build.0 = AppStore|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.ActiveCfg = AppStore|iPhone
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhone.Build.0 = AppStore|iPhone
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhone.ActiveCfg = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhone.Build.0 = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhone.ActiveCfg = Debug|iPhone
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhone.Build.0 = Debug|iPhone
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.FDroid|Any CPU.ActiveCfg = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.FDroid|Any CPU.Build.0 = FDroid|Any CPU
|
||||
{E71F3053-056C-4381-9638-048ED73BDFF6}.FDroid|iPhone.ActiveCfg = FDroid|Any CPU
|
||||
|
||||
@@ -70,6 +70,7 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("org.adblockplus.browser", "url_bar,url_bar_title"), // 2nd = Legacy (before v2)
|
||||
new Browser("org.adblockplus.browser.beta", "url_bar,url_bar_title"), // 2nd = Legacy (before v2)
|
||||
new Browser("org.bromite.bromite", "url_bar"),
|
||||
new Browser("org.bromite.chromium", "url_bar"),
|
||||
new Browser("org.chromium.chrome", "url_bar"),
|
||||
new Browser("org.codeaurora.swe.browser", "url_bar"),
|
||||
new Browser("org.gnu.icecat", "url_bar_title,mozac_browser_toolbar_url_view"), // 2nd = Anticipation
|
||||
@@ -85,6 +86,7 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("org.mozilla.rocket", "display_url"),
|
||||
new Browser("org.torproject.torbrowser", "url_bar_title,mozac_browser_toolbar_url_view"), // 2nd = Anticipation
|
||||
new Browser("org.torproject.torbrowser_alpha", "url_bar_title,mozac_browser_toolbar_url_view"), // 2nd = Anticipation
|
||||
new Browser("org.ungoogled.chromium", "url_bar"),
|
||||
|
||||
// [Section B] Entries only present here
|
||||
//
|
||||
@@ -127,16 +129,196 @@ namespace Bit.Droid.Accessibility
|
||||
"com.ss.squarehome2",
|
||||
"com.treydev.pns"
|
||||
};
|
||||
|
||||
// Be sure to keep these entries sorted alphabetically
|
||||
|
||||
// Be sure to keep these sections sorted alphabetically
|
||||
public static Dictionary<string, KnownUsernameField> KnownUsernameFields => new List<KnownUsernameField>
|
||||
{
|
||||
new KnownUsernameField("accounts.google.com", "ServiceLogin", "Email"),
|
||||
new KnownUsernameField("amazon.com", "signin", "ap_email_login"),
|
||||
new KnownUsernameField("github.com", "", "user[login]-footer"),
|
||||
new KnownUsernameField("paypal.com", "signin", "email"),
|
||||
new KnownUsernameField("signin.aws.amazon.com", "signin", "resolving_input"),
|
||||
new KnownUsernameField("signin.ebay.com", "eBayISAPI.dll", "userid"),
|
||||
/**************************************************************************************
|
||||
* SECTION A ——— World-renowned web sites/applications
|
||||
*************************************************************************************/
|
||||
|
||||
// REM.: For this type of web sites/applications, the Top 100 (SimilarWeb, 2019)
|
||||
// and the Top 50 (Alexa Internet, 2020) are covered. National variants
|
||||
// have been added when available. Mobile and desktop versions supported.
|
||||
//
|
||||
// A few other popular web sites/applications have also been added.
|
||||
//
|
||||
// Could not be added, however:
|
||||
// web sites/applications that don't use an "id" attribute for their login field.
|
||||
|
||||
// NOTE: The case of OAuth compatible web sites/applications that also provide
|
||||
// a "user ID only" login page in this situation
|
||||
// was taken into account in the tests as well.
|
||||
|
||||
/*
|
||||
* A
|
||||
*/
|
||||
|
||||
// Amazon ——— ap_email_login = mobile / ap_email = desktop (amazon.co.jp currently uses ap_email in both cases, as of July 2020).
|
||||
new KnownUsernameField("amazon.ae", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.ca", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.cn", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.co.jp", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.co.uk", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.com", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.com.au", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.com.br", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.com.mx", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.com.tr", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.de", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.es", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.fr", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.in", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.it", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.nl", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.sa", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
new KnownUsernameField("amazon.sg", new (string, string)[] { ("contains:/ap/signin", "ap_email_login,ap_email") }),
|
||||
|
||||
// Amazon Web Services
|
||||
new KnownUsernameField("signin.aws.amazon.com", new (string, string)[] { ("signin", "resolving_input") }),
|
||||
|
||||
// Atlassian
|
||||
new KnownUsernameField("id.atlassian.com", new (string, string)[] { ("login", "username") }),
|
||||
|
||||
/*
|
||||
* B
|
||||
*/
|
||||
|
||||
// Bitly ——— enterprise users.
|
||||
new KnownUsernameField("bitly.com", new (string, string)[] { ("/sso/url_slug", "url_slug") }),
|
||||
|
||||
/*
|
||||
* E
|
||||
*/
|
||||
|
||||
// eBay ——— 1st = traditional access / 2nd = direct access (i.e. https://signin.ebay.tld/).
|
||||
new KnownUsernameField("signin.befr.ebay.be", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.benl.ebay.be", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.cafr.ebay.ca", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.at", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.be", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.ca", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.ch", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.co.uk", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.com", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.com.au", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.com.hk", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.com.my", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.com.sg", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.de", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.es", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.fr", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.ie", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.in", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.it", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.nl", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.ph", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
new KnownUsernameField("signin.ebay.pl", new (string, string)[] { ("iendswith:eBayISAPI.dll", "userid"), ("icontains:/signin/", "userid") }),
|
||||
|
||||
/*
|
||||
* G
|
||||
*/
|
||||
|
||||
// Google ——— 1st = used in most cases (v2) / 2nd = used in some cases (v1).
|
||||
new KnownUsernameField("accounts.google.com", new (string, string)[] { ("identifier", "identifierId"), ("ServiceLogin", "Email") }),
|
||||
|
||||
/*
|
||||
* P
|
||||
*/
|
||||
|
||||
// PayPal ——— 1st = traditional access / 2nd = access using OAuth.
|
||||
new KnownUsernameField("paypal.com", new (string, string)[] { ("signin", "email"), ("contains:/connect/", "email") }),
|
||||
|
||||
/*
|
||||
* T
|
||||
*/
|
||||
|
||||
// Tumblr ——— despite "signup" in its ID, it's the login field (the website offers registration if the account doesn't exist).
|
||||
new KnownUsernameField("tumblr.com", new (string, string)[] { ("login", "signup_determine_email") }),
|
||||
|
||||
/*
|
||||
* Y
|
||||
*/
|
||||
|
||||
// Yandex
|
||||
new KnownUsernameField("passport.yandex.az", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.by", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.co.il", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.com", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.com.am", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.com.ge", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.com.tr", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.ee", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.fi", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.fr", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.kg", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.kz", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.lt", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.lv", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.md", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.pl", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.ru", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.tj", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.tm", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.ua", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
new KnownUsernameField("passport.yandex.uz", new (string, string)[] { ("auth", "passp-field-login") }),
|
||||
|
||||
/**************************************************************************************
|
||||
* SECTION B ——— Top 100 worldwide
|
||||
*************************************************************************************/
|
||||
|
||||
// As of July 2020, all entries that needed to be added from
|
||||
// Top 100 (SimilarWeb, 2019) and Top 50 (Alexa Internet, 2020)
|
||||
// matched section A.
|
||||
//
|
||||
// Therefore, no entry currently.
|
||||
|
||||
/**************************************************************************************
|
||||
* SECTION C ——— Top 20 for selected countries
|
||||
*************************************************************************************/
|
||||
|
||||
// REM.: For these selected countries, the Top 20 (SimilarWeb, 2020)
|
||||
// and the Top 20 (Alexa Internet, 2020) are covered.
|
||||
// Mobile and desktop versions supported.
|
||||
//
|
||||
// Could not be added, however:
|
||||
// web sites/applications that don't use an "id" attribute for their login field.
|
||||
|
||||
/*
|
||||
* Japan
|
||||
*/
|
||||
|
||||
// NTT DOCOMO ——— mainly used for "My docomo".
|
||||
new KnownUsernameField("cfg.smt.docomo.ne.jp", new (string, string)[] { ("contains:/auth/", "Di_Uid") }),
|
||||
|
||||
/**************************************************************************************
|
||||
* SECTION D ——— Miscellaneous
|
||||
*************************************************************************************/
|
||||
|
||||
/*
|
||||
* Various entries ——— Following user requests, etc.
|
||||
*/
|
||||
|
||||
// No entry, currently.
|
||||
|
||||
/**************************************************************************************
|
||||
* SECTION Z ——— Special forms
|
||||
*
|
||||
* Despite "user ID + password" fields both visible, detection rules required.
|
||||
*************************************************************************************/
|
||||
|
||||
/*
|
||||
* Main
|
||||
*/
|
||||
|
||||
// No entry, currently.
|
||||
|
||||
/*
|
||||
* Test/example purposes only
|
||||
*/
|
||||
|
||||
// GitHub ——— VERY special case (signup form, just to test the proper functioning of special forms).
|
||||
new KnownUsernameField("github.com", new (string, string)[] { ("", "user[login]-footer") }),
|
||||
}.ToDictionary(n => n.UriAuthority);
|
||||
|
||||
public static void PrintTestData(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
@@ -306,11 +488,13 @@ namespace Bit.Droid.Accessibility
|
||||
public static AccessibilityNodeInfo GetUsernameEditText(string uriString,
|
||||
IEnumerable<AccessibilityNodeInfo> allEditTexts)
|
||||
{
|
||||
string uriAuthority = null;
|
||||
string uriKey = null;
|
||||
string uriLocalPath = null;
|
||||
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
|
||||
{
|
||||
uriKey = uri.Authority;
|
||||
uriAuthority = uri.Authority;
|
||||
uriKey = uriAuthority.StartsWith("www.", StringComparison.Ordinal) ? uriAuthority.Substring(4) : uriAuthority;
|
||||
uriLocalPath = uri.LocalPath;
|
||||
}
|
||||
|
||||
@@ -326,15 +510,64 @@ namespace Bit.Droid.Accessibility
|
||||
if (KnownUsernameFields.ContainsKey(uriKey))
|
||||
{
|
||||
var usernameField = KnownUsernameFields[uriKey];
|
||||
if (uriLocalPath.EndsWith(usernameField.UriPathEnd))
|
||||
(string UriPathWanted, string UsernameViewId)[] accessOptions = usernameField.AccessOptions;
|
||||
|
||||
for (int i = 0; i < accessOptions.Length; i++)
|
||||
{
|
||||
foreach (var editText in allEditTexts)
|
||||
string curUriPathWanted = accessOptions[i].UriPathWanted;
|
||||
string curUsernameViewId = accessOptions[i].UsernameViewId;
|
||||
bool uriLocalPathMatches = false;
|
||||
|
||||
// Case-sensitive comparison
|
||||
if (curUriPathWanted.StartsWith("startswith:", StringComparison.Ordinal))
|
||||
{
|
||||
foreach (var usernameViewId in usernameField.UsernameViewId.Split(","))
|
||||
curUriPathWanted = curUriPathWanted.Substring(11);
|
||||
uriLocalPathMatches = uriLocalPath.StartsWith(curUriPathWanted, StringComparison.Ordinal);
|
||||
}
|
||||
else if (curUriPathWanted.StartsWith("contains:", StringComparison.Ordinal))
|
||||
{
|
||||
curUriPathWanted = curUriPathWanted.Substring(9);
|
||||
uriLocalPathMatches = uriLocalPath.Contains(curUriPathWanted, StringComparison.Ordinal);
|
||||
}
|
||||
else if (curUriPathWanted.StartsWith("endswith:", StringComparison.Ordinal))
|
||||
{
|
||||
curUriPathWanted = curUriPathWanted.Substring(9);
|
||||
uriLocalPathMatches = uriLocalPath.EndsWith(curUriPathWanted, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// Case-insensitive comparison
|
||||
else if (curUriPathWanted.StartsWith("istartswith:", StringComparison.Ordinal))
|
||||
{
|
||||
curUriPathWanted = curUriPathWanted.Substring(12);
|
||||
uriLocalPathMatches = uriLocalPath.StartsWith(curUriPathWanted, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (curUriPathWanted.StartsWith("icontains:", StringComparison.Ordinal))
|
||||
{
|
||||
curUriPathWanted = curUriPathWanted.Substring(10);
|
||||
uriLocalPathMatches = uriLocalPath.Contains(curUriPathWanted, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (curUriPathWanted.StartsWith("iendswith:", StringComparison.Ordinal))
|
||||
{
|
||||
curUriPathWanted = curUriPathWanted.Substring(10);
|
||||
uriLocalPathMatches = uriLocalPath.EndsWith(curUriPathWanted, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Default type of comparison
|
||||
else
|
||||
{
|
||||
uriLocalPathMatches = uriLocalPath.EndsWith(curUriPathWanted, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (uriLocalPathMatches)
|
||||
{
|
||||
foreach (var editText in allEditTexts)
|
||||
{
|
||||
if (usernameViewId == editText.ViewIdResourceName)
|
||||
foreach (var usernameViewId in curUsernameViewId.Split(","))
|
||||
{
|
||||
return editText;
|
||||
if (usernameViewId == editText.ViewIdResourceName)
|
||||
{
|
||||
return editText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -600,7 +833,12 @@ namespace Bit.Droid.Accessibility
|
||||
|
||||
public static bool IsAutofillServicePromptVisible(IEnumerable<AccessibilityWindowInfo> windows)
|
||||
{
|
||||
return windows?.Any(w => w.Title?.ToLower().Contains("autofill") ?? false) ?? false;
|
||||
// Autofill framework not available until API 26
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
|
||||
{
|
||||
return windows?.Any(w => w.Title?.ToLower().Contains("autofill") ?? false) ?? false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int GetNodeHeight(AccessibilityNodeInfo node)
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
{
|
||||
public class KnownUsernameField
|
||||
{
|
||||
public KnownUsernameField(string uriAuthority, string uriPathEnd, string usernameViewId)
|
||||
public KnownUsernameField(string uriAuthority, (string UriPathWanted, string UsernameViewId)[] accessOptions)
|
||||
{
|
||||
UriAuthority = uriAuthority;
|
||||
UriPathEnd = uriPathEnd;
|
||||
UsernameViewId = usernameViewId;
|
||||
AccessOptions = accessOptions;
|
||||
}
|
||||
|
||||
public string UriAuthority { get; set; }
|
||||
public string UriPathEnd { get; set; }
|
||||
public string UsernameViewId { get; set; }
|
||||
public (string UriPathWanted, string UsernameViewId)[] AccessOptions { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<Version>1.8.6.7</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Essentials">
|
||||
<Version>1.5.3.1</Version>
|
||||
<Version>1.5.3.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>71.1740.0</Version>
|
||||
@@ -133,6 +133,7 @@
|
||||
<Compile Include="MainActivity.cs" />
|
||||
<Compile Include="Resources\Resource.designer.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\BiometricService.cs" />
|
||||
<Compile Include="Services\CryptoPrimitiveService.cs" />
|
||||
<Compile Include="Services\DeviceActionService.cs" />
|
||||
<Compile Include="Services\LocalizeService.cs" />
|
||||
@@ -141,6 +142,7 @@
|
||||
<Compile Include="Tiles\MyVaultTileService.cs" />
|
||||
<Compile Include="Utilities\AndroidHelpers.cs" />
|
||||
<Compile Include="Utilities\AppCenterHelper.cs" />
|
||||
<Compile Include="WebAuthCallbackActivity.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidAsset Include="Assets\FontAwesome.ttf" />
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace Bit.Droid.Autofill
|
||||
"org.adblockplus.browser",
|
||||
"org.adblockplus.browser.beta",
|
||||
"org.bromite.bromite",
|
||||
"org.bromite.chromium",
|
||||
"org.chromium.chrome",
|
||||
"org.codeaurora.swe.browser",
|
||||
"org.gnu.icecat",
|
||||
@@ -88,6 +89,7 @@ namespace Bit.Droid.Autofill
|
||||
"org.mozilla.rocket",
|
||||
"org.torproject.torbrowser",
|
||||
"org.torproject.torbrowser_alpha",
|
||||
"org.ungoogled.chromium",
|
||||
};
|
||||
|
||||
// The URLs are blacklisted from autofilling
|
||||
|
||||
@@ -18,6 +18,7 @@ using Android.Nfc;
|
||||
using Bit.App.Utilities;
|
||||
using System.Threading.Tasks;
|
||||
using AndroidX.Core.Content;
|
||||
using ZXing.Net.Mobile.Android;
|
||||
|
||||
namespace Bit.Droid
|
||||
{
|
||||
@@ -143,6 +144,7 @@ namespace Bit.Droid
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
Xamarin.Essentials.Platform.OnResume();
|
||||
if (_deviceActionService.SupportsNfc())
|
||||
{
|
||||
try
|
||||
@@ -193,8 +195,7 @@ namespace Bit.Droid
|
||||
else
|
||||
{
|
||||
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
ZXing.Net.Mobile.Forms.Android.PermissionsHandler.OnRequestPermissionsResult(
|
||||
requestCode, permissions, grantResults);
|
||||
PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ namespace Bit.Droid
|
||||
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
var biometricService = new BiometricService();
|
||||
|
||||
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
|
||||
ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
|
||||
@@ -108,6 +109,7 @@ namespace Bit.Droid
|
||||
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
|
||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||
|
||||
// Push
|
||||
#if FDROID
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="1"
|
||||
android:versionName="2.4.2"
|
||||
android:versionName="2.6.0"
|
||||
android:installLocation="internalOnly"
|
||||
package="com.x8bit.bitwarden">
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29" />
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using Android.Content;
|
||||
using System.ComponentModel;
|
||||
using Android.Content;
|
||||
using Android.Graphics;
|
||||
using Android.Text;
|
||||
using Android.Views.InputMethods;
|
||||
using Android.Widget;
|
||||
using Bit.Droid.Renderers;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
@@ -24,5 +28,46 @@ namespace Bit.Droid.Renderers
|
||||
(ImeAction)ImeFlags.NoExtractUi;
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for failure to disable text prediction on non-password fields
|
||||
// see https://github.com/xamarin/Xamarin.Forms/issues/10857
|
||||
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnElementPropertyChanged(sender, e);
|
||||
|
||||
// Check if changed property is "IsPassword", otherwise ignore
|
||||
if (e.PropertyName == Entry.IsPasswordProperty.PropertyName)
|
||||
{
|
||||
// Check if field type is text, otherwise ignore (numeric passwords, etc.)
|
||||
EditText.InputType = Element.Keyboard.ToInputType();
|
||||
bool isText = (EditText.InputType & InputTypes.ClassText) == InputTypes.ClassText,
|
||||
isNumber = (EditText.InputType & InputTypes.ClassNumber) == InputTypes.ClassNumber;
|
||||
if (isText || isNumber)
|
||||
{
|
||||
if (Element.IsPassword)
|
||||
{
|
||||
// Element is a password field, set inputType to TextVariationPassword which disables
|
||||
// predictive text by default
|
||||
EditText.InputType = EditText.InputType |
|
||||
(isText ? InputTypes.TextVariationPassword : InputTypes.NumberVariationPassword);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Element is not a password field, set inputType to TextVariationVisiblePassword to
|
||||
// disable predictive text while still displaying the content.
|
||||
EditText.InputType = EditText.InputType |
|
||||
(isText ? InputTypes.TextVariationVisiblePassword : InputTypes.NumberVariationNormal);
|
||||
}
|
||||
|
||||
// The workaround above forces a reset of the style properties, so we need to re-apply the font.
|
||||
// see https://xamarin.github.io/bugzilla-archives/33/33666/bug.html
|
||||
var typeface = Typeface.CreateFromAsset(Context.Assets, "RobotoMono_Regular.ttf");
|
||||
if (Control is TextView label)
|
||||
{
|
||||
label.Typeface = typeface;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="1024.4897"
|
||||
android:viewportHeight="1024.4897">
|
||||
<group android:translateX="300.2449"
|
||||
android:translateY="261.2449">
|
||||
<path
|
||||
android:pathData="M393.4,52.2C389.7,48.5 385.3,46.6 380.3,46.6L43.7,46.6C38.6,46.6 34.3,48.5 30.6,52.2C26.9,55.9 25,60.2 25,65.3L25,289.7C25,306.4 28.3,323.1 34.8,339.5C41.3,356 49.4,370.6 59.1,383.3C68.7,396.1 80.2,408.5 93.6,420.5C106.9,432.6 119.3,442.6 130.6,450.6C141.9,458.6 153.7,466.1 166,473.2C178.3,480.3 187,485.1 192.2,487.7C197.4,490.2 201.5,492.2 204.6,493.5C206.9,494.7 209.5,495.3 212.2,495.3C214.9,495.3 217.5,494.7 219.8,493.5C222.9,492.1 227.1,490.2 232.2,487.7C237.4,485.2 246.1,480.3 258.4,473.2C270.7,466.1 282.5,458.5 293.8,450.6C305.1,442.6 317.4,432.6 330.8,420.5C344.1,408.4 355.6,396 365.3,383.3C374.9,370.5 383,355.9 389.5,339.5C396,323 399.3,306.4 399.3,289.7L399.3,65.3C399,60.2 397.1,55.9 393.4,52.2ZM350,291.8C350,373 212,443 212,443L212,94.6L350,94.6L350,291.8Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="nonZero"/>
|
||||
</group>
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<group
|
||||
android:scaleX="0.099"
|
||||
android:scaleY="0.099"
|
||||
android:translateX="24.3"
|
||||
android:translateY="24.3">
|
||||
<path android:pathData="M482,103l418,418l-235,434l-495,-495l130,75l175,-175z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="573.9931"
|
||||
android:endY="815.5847"
|
||||
android:startX="57.80515"
|
||||
android:startY="213.12973"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#3F000000"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M481.4,102.2c-3.7,-3.7 -8.1,-5.6 -13.1,-5.6L131.7,96.6c-5.1,0 -9.4,1.9 -13.1,5.6C114.9,105.9 113,110.2 113,115.3v224.4c0,16.7 3.3,33.4 9.8,49.8c6.5,16.5 14.6,31.1 24.3,43.8c9.6,12.8 21.1,25.2 34.5,37.2c13.3,12.1 25.7,22.1 37,30.1c11.3,8 23.1,15.5 35.4,22.6c12.3,7.1 21,11.9 26.2,14.5c5.2,2.5 9.3,4.5 12.4,5.8c2.3,1.2 4.9,1.8 7.6,1.8c2.7,0 5.3,-0.6 7.6,-1.8c3.1,-1.4 7.3,-3.3 12.4,-5.8c5.2,-2.5 13.9,-7.4 26.2,-14.5c12.3,-7.1 24.1,-14.7 35.4,-22.6c11.3,-8 23.6,-18 37,-30.1c13.3,-12.1 24.8,-24.5 34.5,-37.2c9.6,-12.8 17.7,-27.4 24.2,-43.8c6.5,-16.5 9.8,-33.1 9.8,-49.8L487.3,115.3C487,110.2 485.1,105.9 481.4,102.2zM438,341.8C438,423 300,493 300,493L300,144.6h138C438,144.6 438,260.6 438,341.8z" />
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="1024dp"
|
||||
android:height="1024dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M1024,864c0,88.4 -71.6,160 -160,160H160C71.6,1024 0,952.4 0,864V160C0,71.6 71.6,0 160,0h704c88.4,0 160,71.6 160,160V864z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M829.8,128.6c-6.5,-6.5 -14.2,-9.7 -23,-9.7H217.2c-8.9,0 -16.5,3.2 -23,9.7c-6.5,6.5 -9.7,14.2 -9.7,23v393.1c0,29.3 5.7,58.4 17.1,87.3c11.4,28.8 25.6,54.4 42.5,76.8c16.9,22.3 37,44.1 60.4,65.3c23.4,21.2 45,38.7 64.7,52.7c19.8,14 40.4,27.2 61.9,39.7c21.5,12.5 36.8,20.9 45.8,25.3c9,4.4 16.3,7.9 21.7,10.2c4.1,2 8.5,3.1 13.3,3.1c4.8,0 9.2,-1 13.3,-3.1c5.5,-2.4 12.7,-5.8 21.8,-10.2c9,-4.4 24.3,-12.9 45.8,-25.3c21.5,-12.5 42.1,-25.7 61.9,-39.7c19.8,-14 41.4,-31.6 64.8,-52.7c23.4,-21.2 43.5,-42.9 60.4,-65.3c16.9,-22.4 31,-47.9 42.5,-76.8c11.4,-28.8 17.1,-57.9 17.1,-87.3V151.7C839.6,142.8 836.3,135.1 829.8,128.6zM753.8,548.4c0,142.3 -241.8,264.9 -241.8,264.9V203.1h241.8C753.8,203.1 753.8,406.1 753.8,548.4z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
<path
|
||||
android:fillColor="#175DDC"
|
||||
android:pathData="M1024,864c0,88.4 -71.6,160 -160,160H160C71.6,1024 0,952.4 0,864V160C0,71.6 71.6,0 160,0h704c88.4,0 160,71.6 160,160V864z" />
|
||||
<path android:pathData="M803.2,195.8l668.8,668.8l-376,694.4l-792,-792l208,120l280,-280z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="922.23486"
|
||||
android:endY="1353.2649"
|
||||
android:startX="96.33417"
|
||||
android:startY="389.33688"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#3F000000"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M802.24,194.52c-5.92,-5.92 -12.96,-8.96 -20.96,-8.96L242.72,185.56c-8.16,0 -15.04,3.04 -20.96,8.96C215.84,200.44 212.8,207.32 212.8,215.48l0,359.04c0,26.72 5.28,53.44 15.68,79.68c10.4,26.4 23.36,49.76 38.88,70.08c15.36,20.48 33.76,40.32 55.2,59.52c21.28,19.36 41.12,35.36 59.2,48.16c18.08,12.8 36.96,24.8 56.64,36.16c19.68,11.36 33.6,19.04 41.92,23.2c8.32,4 14.88,7.2 19.84,9.28c3.68,1.92 7.84,2.88 12.16,2.88c4.32,0 8.48,-0.96 12.16,-2.88c4.96,-2.24 11.68,-5.28 19.84,-9.28c8.32,-4 22.24,-11.84 41.92,-23.2c19.68,-11.36 38.56,-23.52 56.64,-36.16c18.08,-12.8 37.76,-28.8 59.2,-48.16c21.28,-19.36 39.68,-39.2 55.2,-59.52c15.36,-20.48 28.32,-43.84 38.72,-70.08c10.4,-26.4 15.68,-52.96 15.68,-79.68L811.68,215.48C811.2,207.32 808.16,200.44 802.24,194.52zM732.8,577.88C732.8,707.8 512,819.8 512,819.8L512,262.36l220.8,0C732.8,262.36 732.8,447.96 732.8,577.88z" />
|
||||
</vector>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 968 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 11 KiB |
@@ -121,6 +121,9 @@
|
||||
<compatibility-package
|
||||
android:name="org.bromite.bromite"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="org.bromite.chromium"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="org.chromium.chrome"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
@@ -160,4 +163,7 @@
|
||||
<compatibility-package
|
||||
android:name="org.torproject.torbrowser_alpha"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="org.ungoogled.chromium"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
</autofill-service>
|
||||
|
||||
90
src/Android/Services/BiometricService.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Android.OS;
|
||||
using Android.Security.Keystore;
|
||||
using Bit.Core.Abstractions;
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
public class BiometricService : IBiometricService
|
||||
{
|
||||
private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
|
||||
|
||||
private const string KeyStoreName = "AndroidKeyStore";
|
||||
|
||||
private const string KeyAlgorithm = KeyProperties.KeyAlgorithmAes;
|
||||
private const string BlockMode = KeyProperties.BlockModeCbc;
|
||||
private const string EncryptionPadding = KeyProperties.EncryptionPaddingPkcs7;
|
||||
private const string Transformation = KeyAlgorithm + "/" + BlockMode + "/" + EncryptionPadding;
|
||||
|
||||
private readonly KeyStore _keystore;
|
||||
|
||||
public BiometricService()
|
||||
{
|
||||
_keystore = KeyStore.GetInstance(KeyStoreName);
|
||||
_keystore.Load(null);
|
||||
}
|
||||
|
||||
public Task<bool> SetupBiometricAsync()
|
||||
{
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
|
||||
{
|
||||
CreateKey();
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<bool> ValidateIntegrityAsync()
|
||||
{
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.M)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
_keystore.Load(null);
|
||||
IKey key = _keystore.GetKey(KeyName, null);
|
||||
Cipher cipher = Cipher.GetInstance(Transformation);
|
||||
|
||||
try
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, key);
|
||||
}
|
||||
catch (KeyPermanentlyInvalidatedException e)
|
||||
{
|
||||
// Biometric has changed
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
catch (UnrecoverableKeyException e)
|
||||
{
|
||||
// Biometric was disabled and re-enabled
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
catch (InvalidKeyException e)
|
||||
{
|
||||
// Fallback for old bitwarden users without a key
|
||||
CreateKey();
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private void CreateKey()
|
||||
{
|
||||
KeyGenerator keyGen = KeyGenerator.GetInstance(KeyAlgorithm, KeyStoreName);
|
||||
KeyGenParameterSpec keyGenSpec =
|
||||
new KeyGenParameterSpec.Builder(KeyName, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
|
||||
.SetBlockModes(BlockMode)
|
||||
.SetEncryptionPaddings(EncryptionPadding)
|
||||
.SetUserAuthenticationRequired(true)
|
||||
.Build();
|
||||
keyGen.Init(keyGenSpec);
|
||||
keyGen.GenerateKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,9 @@ using Android.App;
|
||||
using Android.App.Assist;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Hardware.Biometrics;
|
||||
using Android.Hardware.Fingerprints;
|
||||
using Android.Nfc;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Runtime;
|
||||
using Android.Text;
|
||||
using Android.Text.Method;
|
||||
using Android.Views.Autofill;
|
||||
@@ -31,7 +28,6 @@ using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Autofill;
|
||||
using Plugin.CurrentActivity;
|
||||
using Plugin.Fingerprint;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
@@ -389,71 +385,16 @@ namespace Bit.Droid.Services
|
||||
|
||||
public bool SupportsFaceBiometric()
|
||||
{
|
||||
// only used by iOS
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> SupportsFaceBiometricAsync()
|
||||
{
|
||||
// only used by iOS
|
||||
return Task.FromResult(SupportsFaceBiometric());
|
||||
}
|
||||
|
||||
public async Task<bool> BiometricAvailableAsync()
|
||||
{
|
||||
if (UseNativeBiometric())
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var manager = activity.GetSystemService(Context.BiometricService) as BiometricManager;
|
||||
return manager.CanAuthenticate() == BiometricCode.Success;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CrossFingerprint.Current.IsAvailableAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseNativeBiometric()
|
||||
{
|
||||
return (int)Build.VERSION.SdkInt >= 29;
|
||||
}
|
||||
|
||||
public Task<bool> AuthenticateBiometricAsync(string text = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
text = AppResources.BiometricsDirection;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
using (var builder = new BiometricPrompt.Builder(activity))
|
||||
{
|
||||
builder.SetTitle(text);
|
||||
builder.SetConfirmationRequired(false);
|
||||
builder.SetNegativeButton(AppResources.Cancel, activity.MainExecutor,
|
||||
new DialogInterfaceOnClickListener
|
||||
{
|
||||
Clicked = () => { }
|
||||
});
|
||||
var prompt = builder.Build();
|
||||
var result = new TaskCompletionSource<bool>();
|
||||
prompt.Authenticate(new CancellationSignal(), activity.MainExecutor,
|
||||
new BiometricAuthenticationCallback
|
||||
{
|
||||
Success = authResult => result.TrySetResult(true),
|
||||
Error = () => result.TrySetResult(false),
|
||||
Failed = () => { },
|
||||
Help = (helpCode, helpString) => { }
|
||||
});
|
||||
return result.Task;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsNfc()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
@@ -870,48 +811,5 @@ namespace Bit.Droid.Services
|
||||
Context.ClipboardService) as Android.Content.ClipboardManager;
|
||||
clipboardManager.PrimaryClip = ClipData.NewPlainText("bitwarden", text);
|
||||
}
|
||||
|
||||
private class BiometricAuthenticationCallback : BiometricPrompt.AuthenticationCallback
|
||||
{
|
||||
public Action<BiometricPrompt.AuthenticationResult> Success { get; set; }
|
||||
public Action Error { get; set; }
|
||||
public Action Failed { get; set; }
|
||||
public Action<BiometricAcquiredStatus, Java.Lang.ICharSequence> Help { get; set; }
|
||||
|
||||
public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult authResult)
|
||||
{
|
||||
base.OnAuthenticationSucceeded(authResult);
|
||||
Success?.Invoke(authResult);
|
||||
}
|
||||
|
||||
public override void OnAuthenticationError([GeneratedEnum] BiometricErrorCode errorCode, Java.Lang.ICharSequence errString)
|
||||
{
|
||||
base.OnAuthenticationError(errorCode, errString);
|
||||
Error?.Invoke();
|
||||
}
|
||||
|
||||
public override void OnAuthenticationFailed()
|
||||
{
|
||||
base.OnAuthenticationFailed();
|
||||
Failed?.Invoke();
|
||||
}
|
||||
|
||||
public override void OnAuthenticationHelp([GeneratedEnum] BiometricAcquiredStatus helpCode,
|
||||
Java.Lang.ICharSequence helpString)
|
||||
{
|
||||
base.OnAuthenticationHelp(helpCode, helpString);
|
||||
Help?.Invoke(helpCode, helpString);
|
||||
}
|
||||
}
|
||||
|
||||
private class DialogInterfaceOnClickListener : Java.Lang.Object, IDialogInterfaceOnClickListener
|
||||
{
|
||||
public Action Clicked { get; set; }
|
||||
|
||||
public void OnClick(IDialogInterface dialog, int which)
|
||||
{
|
||||
Clicked?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
src/Android/WebAuthCallbackActivity.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
|
||||
namespace Bit.Droid
|
||||
{
|
||||
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
|
||||
[IntentFilter(new[] { Android.Content.Intent.ActionView },
|
||||
Categories = new[] { Android.Content.Intent.CategoryDefault, Android.Content.Intent.CategoryBrowsable },
|
||||
DataScheme = "bitwarden")]
|
||||
public class WebAuthCallbackActivity : Xamarin.Essentials.WebAuthenticatorCallbackActivity { }
|
||||
}
|
||||
@@ -23,9 +23,6 @@ namespace Bit.App.Abstractions
|
||||
void RateApp();
|
||||
bool SupportsFaceBiometric();
|
||||
Task<bool> SupportsFaceBiometricAsync();
|
||||
Task<bool> BiometricAvailableAsync();
|
||||
bool UseNativeBiometric();
|
||||
Task<bool> AuthenticateBiometricAsync(string text = null);
|
||||
bool SupportsNfc();
|
||||
bool SupportsCamera();
|
||||
bool SupportsAutofillService();
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="3.2.1" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="2.1.1" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.1" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.5.3.2" />
|
||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="4.5.0.725" />
|
||||
<PackageReference Include="ZXing.Net.Mobile" Version="2.4.1" />
|
||||
@@ -111,6 +111,14 @@
|
||||
<Compile Update="Pages\Vault\GroupingsPage\GroupingsPage.xaml.cs">
|
||||
<DependentUpon>GroupingsPage.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Accounts\LoginSsoPage.xaml.cs">
|
||||
<DependentUpon>LoginSsoPage.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Pages\Accounts\SetPasswordPage.xaml.cs">
|
||||
<DependentUpon>ResetMasterPasswordPage.xaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -219,7 +219,7 @@ namespace Bit.App
|
||||
SyncIfNeeded();
|
||||
if (Current.MainPage is NavigationPage navPage && navPage.CurrentPage is LockPage lockPage)
|
||||
{
|
||||
await lockPage.PromptFingerprintAfterResumeAsync();
|
||||
await lockPage.PromptBiometricAfterResumeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ namespace Bit.App
|
||||
_passwordGenerationService.ClearAsync(),
|
||||
_vaultTimeoutService.ClearAsync(),
|
||||
_stateService.PurgeAsync());
|
||||
_vaultTimeoutService.FingerprintLocked = true;
|
||||
_vaultTimeoutService.BiometricLocked = true;
|
||||
_searchService.ClearIndex();
|
||||
_authService.LogOut(() =>
|
||||
{
|
||||
@@ -404,21 +404,20 @@ namespace Bit.App
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LockedAsync(bool autoPromptFingerprint)
|
||||
private async Task LockedAsync(bool autoPromptBiometric)
|
||||
{
|
||||
await _stateService.PurgeAsync();
|
||||
if (autoPromptFingerprint && Device.RuntimePlatform == Device.iOS)
|
||||
if (autoPromptBiometric && Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var vaultTimeout = await _storageService.GetAsync<int?>(Constants.VaultTimeoutKey);
|
||||
if (vaultTimeout == 0)
|
||||
{
|
||||
autoPromptFingerprint = false;
|
||||
autoPromptBiometric = false;
|
||||
}
|
||||
}
|
||||
else if (autoPromptFingerprint && Device.RuntimePlatform == Device.Android &&
|
||||
_deviceActionService.UseNativeBiometric())
|
||||
else if (autoPromptBiometric && Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
autoPromptFingerprint = false;
|
||||
autoPromptBiometric = false;
|
||||
}
|
||||
PreviousPageInfo lastPageBeforeLock = null;
|
||||
if (Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
|
||||
@@ -445,7 +444,7 @@ namespace Bit.App
|
||||
}
|
||||
}
|
||||
await _storageService.SaveAsync(Constants.PreviousPageKey, lastPageBeforeLock);
|
||||
var lockPage = new LockPage(Options, autoPromptFingerprint);
|
||||
var lockPage = new LockPage(Options, autoPromptBiometric);
|
||||
Device.BeginInvokeOnMainThread(() => Current.MainPage = new NavigationPage(lockPage));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,28 @@ namespace Bit.App.Models
|
||||
public string SaveCardExpYear { get; set; }
|
||||
public string SaveCardCode { get; set; }
|
||||
public bool IosExtension { get; set; }
|
||||
|
||||
public void SetAllFrom(AppOptions o)
|
||||
{
|
||||
if (o == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
MyVaultTile = o.MyVaultTile;
|
||||
GeneratorTile = o.GeneratorTile;
|
||||
FromAutofillFramework = o.FromAutofillFramework;
|
||||
FillType = o.FillType;
|
||||
Uri = o.Uri;
|
||||
SaveType = o.SaveType;
|
||||
SaveName = o.SaveName;
|
||||
SaveUsername = o.SaveUsername;
|
||||
SavePassword = o.SavePassword;
|
||||
SaveCardName = o.SaveCardName;
|
||||
SaveCardNumber = o.SaveCardNumber;
|
||||
SaveCardExpMonth = o.SaveCardExpMonth;
|
||||
SaveCardExpYear = o.SaveCardExpYear;
|
||||
SaveCardCode = o.SaveCardCode;
|
||||
IosExtension = o.IosExtension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class EnvironmentPage : BaseContentPage
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly EnvironmentPageViewModel _vm;
|
||||
|
||||
public EnvironmentPage()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
InitializeComponent();
|
||||
@@ -28,6 +32,12 @@ namespace Bit.App.Pages
|
||||
_apiEntry.ReturnCommand = new Command(() => _identityEntry.Focus());
|
||||
_identityEntry.ReturnType = ReturnType.Next;
|
||||
_identityEntry.ReturnCommand = new Command(() => _iconsEntry.Focus());
|
||||
_vm.SubmitSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SubmitSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
@@ -38,12 +48,17 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
private async Task SubmitSuccessAsync()
|
||||
{
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||
await Navigation.PopModalAsync();
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -8,12 +9,10 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public class EnvironmentPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
|
||||
public EnvironmentPageViewModel()
|
||||
{
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
|
||||
PageTitle = AppResources.Settings;
|
||||
@@ -33,6 +32,8 @@ namespace Bit.App.Pages
|
||||
public string WebVaultUrl { get; set; }
|
||||
public string IconsUrl { get; set; }
|
||||
public string NotificationsUrls { get; set; }
|
||||
public Action SubmitSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
@@ -54,8 +55,7 @@ namespace Bit.App.Pages
|
||||
IconsUrl = resUrls.Icons;
|
||||
NotificationsUrls = resUrls.Notifications;
|
||||
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.EnvironmentSaved);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
SubmitSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,16 @@
|
||||
<ContentPage.BindingContext>
|
||||
<pages:HomeViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<StackLayout Spacing="0" Padding="10, 5">
|
||||
<controls:FaButton Text=""
|
||||
StyleClass="btn-muted, btn-icon, btn-icon-platform"
|
||||
HorizontalOptions="Start"
|
||||
Clicked="Settings_Clicked"
|
||||
Clicked="Environment_Clicked"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n Options}">
|
||||
<controls:FaButton.Margin>
|
||||
@@ -39,6 +43,8 @@
|
||||
Clicked="LogIn_Clicked"></Button>
|
||||
<Button Text="{u:I18n CreateAccount}"
|
||||
Clicked="Register_Clicked"></Button>
|
||||
<Button Text="{u:I18n LogInSso}"
|
||||
Clicked="LogInSso_Clicked"></Button>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class HomePage : BaseContentPage
|
||||
{
|
||||
private readonly HomeViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
private IMessagingService _messagingService;
|
||||
|
||||
@@ -19,6 +20,12 @@ namespace Bit.App.Pages
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as HomeViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.StartLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartLoginAsync());
|
||||
_vm.StartRegisterAction = () => Device.BeginInvokeOnMainThread(async () => await StartRegisterAsync());
|
||||
_vm.StartSsoLoginAction = () => Device.BeginInvokeOnMainThread(async () => await StartSsoLoginAsync());
|
||||
_vm.StartEnvironmentAction = () => Device.BeginInvokeOnMainThread(async () => await StartEnvironmentAsync());
|
||||
_logo.Source = !ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png";
|
||||
}
|
||||
|
||||
@@ -33,29 +40,69 @@ namespace Bit.App.Pages
|
||||
base.OnAppearing();
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PushModalAsync(new NavigationPage(new LoginPage(null, _appOptions)));
|
||||
_vm.StartLoginAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartLoginAsync()
|
||||
{
|
||||
var page = new LoginPage(null, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void Register_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PushModalAsync(new NavigationPage(new RegisterPage(this)));
|
||||
_vm.StartRegisterAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartRegisterAsync()
|
||||
{
|
||||
var page = new RegisterPage(this);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void Settings_Clicked(object sender, EventArgs e)
|
||||
private void LogInSso_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
Navigation.PushModalAsync(new NavigationPage(new EnvironmentPage()));
|
||||
_vm.StartSsoLoginAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartSsoLoginAsync()
|
||||
{
|
||||
var page = new LoginSsoPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private void Environment_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.StartEnvironmentAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartEnvironmentAsync()
|
||||
{
|
||||
var page = new EnvironmentPage();
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Bit.App.Resources;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -6,7 +7,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public HomeViewModel()
|
||||
{
|
||||
PageTitle = "Home Page";
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
}
|
||||
|
||||
public Action StartLoginAction { get; set; }
|
||||
public Action StartRegisterAction { get; set; }
|
||||
public Action StartSsoLoginAction { get; set; }
|
||||
public Action StartEnvironmentAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,8 +106,12 @@
|
||||
Margin="0, 10, 0, 0" />
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
<Button Text="{Binding FingerprintButtonText}" Clicked="Fingerprint_Clicked"
|
||||
IsVisible="{Binding FingerprintLock}"></Button>
|
||||
<Label
|
||||
Text="{u:I18n BiometricInvalidated}"
|
||||
StyleClass="box-footer-label,text-danger,text-bold"
|
||||
IsVisible="{Binding BiometricIntegrityValid, Converter={StaticResource inverseBool}}" />
|
||||
<Button Text="{Binding BiometricButtonText}" Clicked="Biometric_Clicked"
|
||||
IsVisible="{Binding BiometricLock}" IsEnabled="{Binding BiometricIntegrityValid}"></Button>
|
||||
<Button Text="{u:I18n LogOut}" Clicked="LogOut_Clicked"></Button>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -12,17 +12,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly AppOptions _appOptions;
|
||||
private readonly bool _autoPromptFingerprint;
|
||||
private readonly bool _autoPromptBiometric;
|
||||
private readonly LockPageViewModel _vm;
|
||||
|
||||
private bool _promptedAfterResume;
|
||||
private bool _appeared;
|
||||
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptFingerprint = true)
|
||||
public LockPage(AppOptions appOptions = null, bool autoPromptBiometric = true)
|
||||
{
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_appOptions = appOptions;
|
||||
_autoPromptFingerprint = autoPromptFingerprint;
|
||||
_autoPromptBiometric = autoPromptBiometric;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LockPageViewModel;
|
||||
_vm.Page = this;
|
||||
@@ -34,15 +34,15 @@ namespace Bit.App.Pages
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry PinEntry { get; set; }
|
||||
|
||||
public async Task PromptFingerprintAfterResumeAsync()
|
||||
public async Task PromptBiometricAfterResumeAsync()
|
||||
{
|
||||
if (_vm.FingerprintLock)
|
||||
if (_vm.BiometricLock)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
if (!_promptedAfterResume)
|
||||
{
|
||||
_promptedAfterResume = true;
|
||||
await _vm?.PromptFingerprintAsync();
|
||||
await _vm?.PromptBiometricAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,8 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
_appeared = true;
|
||||
await _vm.InitAsync(_autoPromptFingerprint);
|
||||
if (!_vm.FingerprintLock)
|
||||
await _vm.InitAsync(_autoPromptBiometric);
|
||||
if (!_vm.BiometricLock)
|
||||
{
|
||||
if (_vm.PinLock)
|
||||
{
|
||||
@@ -89,34 +89,21 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Fingerprint_Clicked(object sender, EventArgs e)
|
||||
private async void Biometric_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.PromptFingerprintAsync();
|
||||
await _vm.PromptBiometricAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnlockedAsync()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
if (_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if (previousPage != null)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Request;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LockPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
@@ -24,12 +26,14 @@ namespace Bit.App.Pages
|
||||
private readonly IStorageService _secureStorageService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IBiometricService _biometricService;
|
||||
|
||||
private string _email;
|
||||
private bool _showPassword;
|
||||
private bool _pinLock;
|
||||
private bool _fingerprintLock;
|
||||
private string _fingerprintButtonText;
|
||||
private bool _biometricLock;
|
||||
private bool _biometricIntegrityValid = true;
|
||||
private string _biometricButtonText;
|
||||
private string _loggedInAsText;
|
||||
private string _lockedVerifyText;
|
||||
private int _invalidPinAttempts = 0;
|
||||
@@ -37,6 +41,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public LockPageViewModel()
|
||||
{
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
@@ -47,6 +52,7 @@ namespace Bit.App.Pages
|
||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@@ -69,16 +75,22 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _pinLock, value);
|
||||
}
|
||||
|
||||
public bool FingerprintLock
|
||||
public bool BiometricLock
|
||||
{
|
||||
get => _fingerprintLock;
|
||||
set => SetProperty(ref _fingerprintLock, value);
|
||||
get => _biometricLock;
|
||||
set => SetProperty(ref _biometricLock, value);
|
||||
}
|
||||
|
||||
public string FingerprintButtonText
|
||||
public bool BiometricIntegrityValid
|
||||
{
|
||||
get => _fingerprintButtonText;
|
||||
set => SetProperty(ref _fingerprintButtonText, value);
|
||||
get => _biometricIntegrityValid;
|
||||
set => SetProperty(ref _biometricIntegrityValid, value);
|
||||
}
|
||||
|
||||
public string BiometricButtonText
|
||||
{
|
||||
get => _biometricButtonText;
|
||||
set => SetProperty(ref _biometricButtonText, value);
|
||||
}
|
||||
|
||||
public string LoggedInAsText
|
||||
@@ -100,11 +112,11 @@ namespace Bit.App.Pages
|
||||
public string Pin { get; set; }
|
||||
public Action UnlockedAction { get; set; }
|
||||
|
||||
public async Task InitAsync(bool autoPromptFingerprint)
|
||||
public async Task InitAsync(bool autoPromptBiometric)
|
||||
{
|
||||
_pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
|
||||
PinLock = (_pinSet.Item1 && _vaultTimeoutService.PinProtectedKey != null) || _pinSet.Item2;
|
||||
FingerprintLock = await _vaultTimeoutService.IsFingerprintLockSetAsync();
|
||||
BiometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
||||
_email = await _userService.GetEmailAsync();
|
||||
var webVault = _environmentService.GetWebVaultUrl();
|
||||
if (string.IsNullOrWhiteSpace(webVault))
|
||||
@@ -124,27 +136,22 @@ namespace Bit.App.Pages
|
||||
LockedVerifyText = AppResources.VaultLockedMasterPassword;
|
||||
}
|
||||
|
||||
if (FingerprintLock)
|
||||
if (BiometricLock)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
if (Device.RuntimePlatform == Device.iOS && supportsFace)
|
||||
BiometricButtonText = AppResources.UseBiometricsToUnlock;
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseFaceIDToUnlock;
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
||||
AppResources.UseFingerprintToUnlock;
|
||||
}
|
||||
else if (Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseBiometricsToUnlock;
|
||||
}
|
||||
else
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseFingerprintToUnlock;
|
||||
}
|
||||
if (autoPromptFingerprint)
|
||||
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
|
||||
if (autoPromptBiometric & _biometricIntegrityValid)
|
||||
{
|
||||
var tasks = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(500);
|
||||
Device.BeginInvokeOnMainThread(async () => await PromptFingerprintAsync());
|
||||
Device.BeginInvokeOnMainThread(async () => await PromptBiometricAsync());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -220,18 +227,33 @@ namespace Bit.App.Pages
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdf, kdfIterations);
|
||||
var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
if (storedKeyHash == null)
|
||||
var passwordValid = false;
|
||||
if (keyHash != null)
|
||||
{
|
||||
var oldKey = await _secureStorageService.GetAsync<string>("oldKey");
|
||||
if (key.KeyB64 == oldKey)
|
||||
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
||||
if (storedKeyHash != null)
|
||||
{
|
||||
await _secureStorageService.RemoveAsync("oldKey");
|
||||
await _cryptoService.SetKeyHashAsync(keyHash);
|
||||
storedKeyHash = keyHash;
|
||||
passwordValid = storedKeyHash == keyHash;
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||
var request = new PasswordVerificationRequest();
|
||||
request.MasterPasswordHash = keyHash;
|
||||
try
|
||||
{
|
||||
await _apiService.PostAccountVerifyPasswordAsync(request);
|
||||
passwordValid = true;
|
||||
await _cryptoService.SetKeyHashAsync(keyHash);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
}
|
||||
if (storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
|
||||
if (passwordValid)
|
||||
{
|
||||
if (_pinSet.Item1)
|
||||
{
|
||||
@@ -244,6 +266,12 @@ namespace Bit.App.Pages
|
||||
}
|
||||
MasterPassword = string.Empty;
|
||||
await SetKeyAndContinueAsync(key);
|
||||
|
||||
// Re-enable biometrics
|
||||
if (BiometricLock & !BiometricIntegrityValid)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -271,9 +299,10 @@ namespace Bit.App.Pages
|
||||
entry.Focus();
|
||||
}
|
||||
|
||||
public async Task PromptFingerprintAsync()
|
||||
public async Task PromptBiometricAsync()
|
||||
{
|
||||
if (!FingerprintLock)
|
||||
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
|
||||
if (!BiometricLock || !BiometricIntegrityValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -290,7 +319,7 @@ namespace Bit.App.Pages
|
||||
page.MasterPasswordEntry.Focus();
|
||||
}
|
||||
});
|
||||
_vaultTimeoutService.FingerprintLocked = !success;
|
||||
_vaultTimeoutService.BiometricLocked = !success;
|
||||
if (success)
|
||||
{
|
||||
await DoContinueAsync();
|
||||
@@ -309,7 +338,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task DoContinueAsync()
|
||||
{
|
||||
_vaultTimeoutService.FingerprintLocked = false;
|
||||
_vaultTimeoutService.BiometricLocked = false;
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
_messagingService.Send("unlocked");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -25,7 +25,7 @@ namespace Bit.App.Pages
|
||||
_vm = BindingContext as LoginPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.LoggedInAction = () => Device.BeginInvokeOnMainThread(async () => await LoggedInAsync());
|
||||
_vm.LogInSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await LogInSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
@@ -74,7 +74,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
@@ -84,30 +84,17 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task StartTwoFactorAsync()
|
||||
{
|
||||
var page = new TwoFactorPage();
|
||||
var page = new TwoFactorPage(false, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task LoggedInAsync()
|
||||
private async Task LogInSuccessAsync()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
if (_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
}
|
||||
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if (previousPage != null)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Bit.App.Pages
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public bool RememberEmail { get; set; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action LoggedInAction { get; set; }
|
||||
public Action LogInSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public bool HideHintButton
|
||||
@@ -142,7 +142,7 @@ namespace Bit.App.Pages
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
LoggedInAction?.Invoke();
|
||||
LogInSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
|
||||
42
src/App/Pages/Accounts/LoginSsoPage.xaml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.LoginSsoPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:LoginSsoPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:LoginSsoPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n LogIn}" Clicked="LogIn_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20" Margin="0,10,0,0">
|
||||
<StackLayout StyleClass="box">
|
||||
<Label Text="{u:I18n LogInSsoSummary}"
|
||||
StyleClass="text-md"
|
||||
HorizontalTextAlignment="Start"></Label>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n OrgIdentifier}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_orgIdentifier"
|
||||
Text="{Binding OrgIdentifier}"
|
||||
Keyboard="Default"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding LogInCommand}" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
114
src/App/Pages/Accounts/LoginSsoPage.xaml.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class LoginSsoPage : BaseContentPage
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly LoginSsoPageViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private AppOptions _appOptionsCopy;
|
||||
|
||||
public LoginSsoPage(AppOptions appOptions = null)
|
||||
{
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as LoginSsoPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.StartTwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await StartTwoFactorAsync());
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.SsoAuthSuccessAction = () => Device.BeginInvokeOnMainThread(async () => await SsoAuthSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
if (string.IsNullOrWhiteSpace(_vm.OrgIdentifier))
|
||||
{
|
||||
RequestFocus(_orgIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyAppOptions()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
// create an object copy of _appOptions to persist values when app is exited during web auth flow
|
||||
_appOptionsCopy = new AppOptions();
|
||||
_appOptionsCopy.SetAllFrom(_appOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreAppOptionsFromCopy()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
{
|
||||
// restore values to original readonly _appOptions object from copy
|
||||
_appOptions.SetAllFrom(_appOptionsCopy);
|
||||
_appOptionsCopy = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void LogIn_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
CopyAppOptions();
|
||||
await _vm.LogInAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartTwoFactorAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
var page = new TwoFactorPage(true, _appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task StartSetPasswordAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
var page = new SetPasswordPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task SsoAuthSuccessAsync()
|
||||
{
|
||||
RestoreAppOptionsFromCopy();
|
||||
await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
218
src/App/Pages/Accounts/LoginSsoPageViewModel.cs
Normal file
@@ -0,0 +1,218 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class LoginSsoPageViewModel : BaseViewModel
|
||||
{
|
||||
private const string Keys_RememberedOrgIdentifier = "rememberedOrgIdentifier";
|
||||
private const string Keys_RememberOrgIdentifier = "rememberOrgIdentifier";
|
||||
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IAuthService _authService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private string _orgIdentifier;
|
||||
|
||||
public LoginSsoPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_passwordGenerationService =
|
||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_cryptoFunctionService = ServiceContainer.Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
|
||||
PageTitle = AppResources.Bitwarden;
|
||||
LogInCommand = new Command(async () => await LogInAsync());
|
||||
}
|
||||
|
||||
public string OrgIdentifier
|
||||
{
|
||||
get => _orgIdentifier;
|
||||
set => SetProperty(ref _orgIdentifier, value);
|
||||
}
|
||||
|
||||
public Command LogInCommand { get; }
|
||||
public bool RememberOrgIdentifier { get; set; }
|
||||
public Action StartTwoFactorAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action SsoAuthSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(OrgIdentifier))
|
||||
{
|
||||
OrgIdentifier = await _storageService.GetAsync<string>(Keys_RememberedOrgIdentifier);
|
||||
}
|
||||
var rememberOrgIdentifier = await _storageService.GetAsync<bool?>(Keys_RememberOrgIdentifier);
|
||||
RememberOrgIdentifier = rememberOrgIdentifier.GetValueOrDefault(true);
|
||||
}
|
||||
|
||||
public async Task LogInAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(OrgIdentifier))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier),
|
||||
AppResources.AnErrorHasOccurred,
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn);
|
||||
|
||||
try
|
||||
{
|
||||
await _apiService.PreValidateSso(OrgIdentifier);
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(
|
||||
(e?.Error != null ? e.Error.GetSingleMessage() : AppResources.LoginSsoError),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
return;
|
||||
}
|
||||
|
||||
var passwordOptions = new PasswordGenerationOptions(true);
|
||||
passwordOptions.Length = 64;
|
||||
|
||||
var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||
var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256);
|
||||
var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash);
|
||||
|
||||
var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions);
|
||||
|
||||
var redirectUri = "bitwarden://sso-callback";
|
||||
|
||||
var url = _apiService.IdentityBaseUrl + "/connect/authorize?" +
|
||||
"client_id=" + _platformUtilsService.IdentityClientId + "&" +
|
||||
"redirect_uri=" + Uri.EscapeDataString(redirectUri) + "&" +
|
||||
"response_type=code&scope=api%20offline_access&" +
|
||||
"state=" + state + "&code_challenge=" + codeChallenge + "&" +
|
||||
"code_challenge_method=S256&response_mode=query&" +
|
||||
"domain_hint=" + Uri.EscapeDataString(OrgIdentifier);
|
||||
|
||||
WebAuthenticatorResult authResult = null;
|
||||
bool cancelled = false;
|
||||
try
|
||||
{
|
||||
authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url),
|
||||
new Uri(redirectUri));
|
||||
}
|
||||
catch (TaskCanceledException taskCanceledException)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
cancelled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// WebAuthenticator throws NSErrorException if iOS flow is cancelled - by setting cancelled to true
|
||||
// here we maintain the appearance of a clean cancellation (we don't want to do this across the board
|
||||
// because we still want to present legitimate errors). If/when this is fixed, we can remove this
|
||||
// particular catch block (catching taskCanceledException above must remain)
|
||||
// https://github.com/xamarin/Essentials/issues/1240
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
cancelled = true;
|
||||
}
|
||||
}
|
||||
if (!cancelled)
|
||||
{
|
||||
var code = GetResultCode(authResult, state);
|
||||
if (!string.IsNullOrEmpty(code))
|
||||
{
|
||||
await LogIn(code, codeVerifier, redirectUri);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetResultCode(WebAuthenticatorResult authResult, string state)
|
||||
{
|
||||
string code = null;
|
||||
if (authResult != null)
|
||||
{
|
||||
authResult.Properties.TryGetValue("state", out var resultState);
|
||||
if (resultState == state)
|
||||
{
|
||||
authResult.Properties.TryGetValue("code", out var resultCode);
|
||||
code = resultCode;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private async Task LogIn(string code, string codeVerifier, string redirectUri)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _authService.LogInSsoAsync(code, codeVerifier, redirectUri);
|
||||
if (RememberOrgIdentifier)
|
||||
{
|
||||
await _storageService.SaveAsync(Keys_RememberedOrgIdentifier, OrgIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _storageService.RemoveAsync(Keys_RememberedOrgIdentifier);
|
||||
}
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (response.TwoFactor)
|
||||
{
|
||||
StartTwoFactorAction?.Invoke();
|
||||
}
|
||||
else if (response.ResetMasterPassword)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
var task = Task.Run(async () => await _syncService.FullSyncAsync(true));
|
||||
SsoAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError,
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -17,12 +18,11 @@ namespace Bit.App.Pages
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as RegisterPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.RegistrationSuccess = async () =>
|
||||
_vm.RegistrationSuccess = () => Device.BeginInvokeOnMainThread(async () => await RegistrationSuccessAsync(homePage));
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
if (homePage != null)
|
||||
{
|
||||
await homePage.DismissRegisterPageAndLogInAsync(_vm.Email);
|
||||
}
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
ConfirmMasterPasswordEntry = _confirmMasterPassword;
|
||||
@@ -55,13 +55,20 @@ namespace Bit.App.Pages
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegistrationSuccessAsync(HomePage homePage)
|
||||
{
|
||||
if (homePage != null)
|
||||
{
|
||||
await homePage.DismissRegisterPageAndLogInAsync(_vm.Email);
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
private void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ namespace Bit.App.Pages
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public Action RegistrationSuccess { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
|
||||
146
src/App/Pages/Accounts/SetPasswordPage.xaml
Normal file
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BaseContentPage
|
||||
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.SetPasswordPage"
|
||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
x:DataType="pages:SetPasswordPageViewModel"
|
||||
Title="{Binding PageTitle}">
|
||||
|
||||
<ContentPage.BindingContext>
|
||||
<pages:SetPasswordPageViewModel />
|
||||
</ContentPage.BindingContext>
|
||||
|
||||
<ContentPage.Resources>
|
||||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
<ToolbarItem Text="{u:I18n Submit}" Clicked="Submit_Clicked" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label Text="{u:I18n SetMasterPasswordSummary}"
|
||||
StyleClass="text-md"
|
||||
HorizontalTextAlignment="Start"></Label>
|
||||
</StackLayout>
|
||||
<Grid IsVisible="{Binding IsPolicyInEffect}"
|
||||
RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Frame Padding="10"
|
||||
Margin="0"
|
||||
HasShadow="False"
|
||||
BackgroundColor="Transparent"
|
||||
BorderColor="Accent">
|
||||
<Label
|
||||
Text="{Binding PolicySummary}"
|
||||
StyleClass="text-muted, text-sm, text-bold"
|
||||
HorizontalTextAlignment="Start" />
|
||||
</Frame>
|
||||
</Grid>
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n MasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_masterPassword"
|
||||
Text="{Binding MasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding TogglePasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<Grid StyleClass="box-row">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Label
|
||||
Text="{u:I18n RetypeMasterPassword}"
|
||||
StyleClass="box-label"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0" />
|
||||
<controls:MonoEntry
|
||||
x:Name="_confirmMasterPassword"
|
||||
Text="{Binding ConfirmMasterPassword}"
|
||||
StyleClass="box-value"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
Command="{Binding ToggleConfirmPasswordCommand}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
</Grid>
|
||||
<StackLayout StyleClass="box-row">
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHint}"
|
||||
StyleClass="box-label" />
|
||||
<Entry
|
||||
x:Name="_hint"
|
||||
Text="{Binding Hint}"
|
||||
StyleClass="box-value"
|
||||
ReturnType="Go"
|
||||
ReturnCommand="{Binding SubmitCommand}" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n MasterPasswordHintDescription}"
|
||||
StyleClass="box-footer-label" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
82
src/App/Pages/Accounts/SetPasswordPage.xaml.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class SetPasswordPage : BaseContentPage
|
||||
{
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly SetPasswordPageViewModel _vm;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
public SetPasswordPage(AppOptions appOptions = null)
|
||||
{
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", true);
|
||||
_appOptions = appOptions;
|
||||
InitializeComponent();
|
||||
_vm = BindingContext as SetPasswordPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.SetPasswordSuccessAction =
|
||||
() => Device.BeginInvokeOnMainThread(async () => await SetPasswordSuccessAsync());
|
||||
_vm.CloseAction = async () =>
|
||||
{
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
await Navigation.PopModalAsync();
|
||||
};
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
ToolbarItems.RemoveAt(0);
|
||||
}
|
||||
|
||||
MasterPasswordEntry = _masterPassword;
|
||||
ConfirmMasterPasswordEntry = _confirmMasterPassword;
|
||||
|
||||
_masterPassword.ReturnType = ReturnType.Next;
|
||||
_masterPassword.ReturnCommand = new Command(() => _confirmMasterPassword.Focus());
|
||||
_confirmMasterPassword.ReturnType = ReturnType.Next;
|
||||
_confirmMasterPassword.ReturnCommand = new Command(() => _hint.Focus());
|
||||
}
|
||||
|
||||
public Entry MasterPasswordEntry { get; set; }
|
||||
public Entry ConfirmMasterPasswordEntry { get; set; }
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.InitAsync();
|
||||
RequestFocus(_masterPassword);
|
||||
}
|
||||
|
||||
private async void Submit_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
_vm.CloseAction();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetPasswordSuccessAsync()
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
259
src/App/Pages/Accounts/SetPasswordPageViewModel.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public class SetPasswordPageViewModel : BaseViewModel
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IApiService _apiService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IPasswordGenerationService _passwordGenerationService;
|
||||
private readonly II18nService _i18nService;
|
||||
|
||||
private bool _showPassword;
|
||||
private bool _isPolicyInEffect;
|
||||
private string _policySummary;
|
||||
private MasterPasswordPolicyOptions _policy;
|
||||
|
||||
public SetPasswordPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||
_passwordGenerationService =
|
||||
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
|
||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||
|
||||
PageTitle = AppResources.SetMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
ToggleConfirmPasswordCommand = new Command(ToggleConfirmPassword);
|
||||
SubmitCommand = new Command(async () => await SubmitAsync());
|
||||
}
|
||||
|
||||
public bool ShowPassword
|
||||
{
|
||||
get => _showPassword;
|
||||
set => SetProperty(ref _showPassword, value,
|
||||
additionalPropertyNames: new[] { nameof(ShowPasswordIcon) });
|
||||
}
|
||||
|
||||
public bool IsPolicyInEffect
|
||||
{
|
||||
get => _isPolicyInEffect;
|
||||
set => SetProperty(ref _isPolicyInEffect, value);
|
||||
}
|
||||
|
||||
public string PolicySummary
|
||||
{
|
||||
get => _policySummary;
|
||||
set => SetProperty(ref _policySummary, value);
|
||||
}
|
||||
|
||||
public MasterPasswordPolicyOptions Policy
|
||||
{
|
||||
get => _policy;
|
||||
set => SetProperty(ref _policy, value);
|
||||
}
|
||||
|
||||
public Command SubmitCommand { get; }
|
||||
public Command TogglePasswordCommand { get; }
|
||||
public Command ToggleConfirmPasswordCommand { get; }
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string MasterPassword { get; set; }
|
||||
public string ConfirmMasterPassword { get; set; }
|
||||
public string Hint { get; set; }
|
||||
public Action SetPasswordSuccessAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await CheckPasswordPolicy();
|
||||
}
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
if (Connectivity.NetworkAccess == NetworkAccess.None)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
||||
AppResources.InternetConnectionRequiredTitle);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(MasterPassword))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword),
|
||||
AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
if (IsPolicyInEffect)
|
||||
{
|
||||
var userInput = await GetPasswordStrengthUserInput();
|
||||
var passwordStrength = _passwordGenerationService.PasswordStrength(MasterPassword, userInput);
|
||||
if (!await _policyService.EvaluateMasterPassword(passwordStrength.Score, MasterPassword, Policy))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
|
||||
AppResources.MasterPasswordPolicyValidationMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (MasterPassword.Length < 8)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.MasterPasswordPolicyValidationTitle,
|
||||
AppResources.MasterPasswordLengthValMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (MasterPassword != ConfirmMasterPassword)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred,
|
||||
AppResources.MasterPasswordConfirmationValMessage, AppResources.Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
var kdf = KdfType.PBKDF2_SHA256;
|
||||
var kdfIterations = 100000;
|
||||
var email = await _userService.GetEmailAsync();
|
||||
var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations);
|
||||
var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key);
|
||||
|
||||
Tuple<SymmetricCryptoKey, CipherString> encKey;
|
||||
var existingEncKey = await _cryptoService.GetEncKeyAsync();
|
||||
if (existingEncKey == null)
|
||||
{
|
||||
encKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
encKey = await _cryptoService.RemakeEncKeyAsync(key);
|
||||
}
|
||||
|
||||
var keys = await _cryptoService.MakeKeyPairAsync(encKey.Item1);
|
||||
var request = new SetPasswordRequest
|
||||
{
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Key = encKey.Item2.EncryptedString,
|
||||
MasterPasswordHint = Hint,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations,
|
||||
Keys = new KeysRequest
|
||||
{
|
||||
PublicKey = keys.Item1,
|
||||
EncryptedPrivateKey = keys.Item2.EncryptedString
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
|
||||
await _apiService.SetPasswordAsync(request);
|
||||
await _userService.SetInformationAsync(await _userService.GetUserIdAsync(),
|
||||
await _userService.GetEmailAsync(), kdf, kdfIterations);
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
await _cryptoService.SetKeyHashAsync(masterPasswordHash);
|
||||
await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString);
|
||||
await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
|
||||
SetPasswordSuccessAction?.Invoke();
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if (e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TogglePassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as SetPasswordPage).MasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
public void ToggleConfirmPassword()
|
||||
{
|
||||
ShowPassword = !ShowPassword;
|
||||
(Page as SetPasswordPage).ConfirmMasterPasswordEntry.Focus();
|
||||
}
|
||||
|
||||
private async Task CheckPasswordPolicy()
|
||||
{
|
||||
Policy = await _policyService.GetMasterPasswordPolicyOptions();
|
||||
IsPolicyInEffect = Policy?.InEffect() ?? false;
|
||||
if (!IsPolicyInEffect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bullet = "\n" + "".PadLeft(4) + "\u2022 ";
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(_i18nService.T("MasterPasswordPolicyInEffect"));
|
||||
if (Policy.MinComplexity > 0)
|
||||
{
|
||||
sb.Append(bullet)
|
||||
.Append(string.Format(_i18nService.T("PolicyInEffectMinComplexity"), Policy.MinComplexity));
|
||||
}
|
||||
if (Policy.MinLength > 0)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectMinLength"), Policy.MinLength));
|
||||
}
|
||||
if (Policy.RequireUpper)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectUppercase"));
|
||||
}
|
||||
if (Policy.RequireLower)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectLowercase"));
|
||||
}
|
||||
if (Policy.RequireNumbers)
|
||||
{
|
||||
sb.Append(bullet).Append(_i18nService.T("PolicyInEffectNumbers"));
|
||||
}
|
||||
if (Policy.RequireSpecial)
|
||||
{
|
||||
sb.Append(bullet).Append(string.Format(_i18nService.T("PolicyInEffectSpecial"), "!@#$%^&*"));
|
||||
}
|
||||
PolicySummary = sb.ToString();
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetPasswordStrengthUserInput()
|
||||
{
|
||||
var email = await _userService.GetEmailAsync();
|
||||
List<string> userInput = null;
|
||||
var atPosition = email.IndexOf('@');
|
||||
if (atPosition > -1)
|
||||
{
|
||||
var rx = new Regex("/[^A-Za-z0-9]/", RegexOptions.Compiled);
|
||||
var data = rx.Split(email.Substring(0, atPosition).Trim().ToLower());
|
||||
userInput = new List<string>(data);
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using Bit.App.Controls;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
@@ -14,22 +14,29 @@ namespace Bit.App.Pages
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly AppOptions _appOptions;
|
||||
|
||||
private TwoFactorPageViewModel _vm;
|
||||
private bool _inited;
|
||||
private bool _authingWithSso;
|
||||
|
||||
public TwoFactorPage(AppOptions appOptions = null)
|
||||
public TwoFactorPage(bool? authingWithSso = false, AppOptions appOptions = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
SetActivityIndicator();
|
||||
_authingWithSso = authingWithSso ?? false;
|
||||
_appOptions = appOptions;
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_vm = BindingContext as TwoFactorPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.TwoFactorAction = () => Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthAsync());
|
||||
_vm.StartSetPasswordAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await StartSetPasswordAsync());
|
||||
_vm.TwoFactorAuthSuccessAction = () =>
|
||||
Device.BeginInvokeOnMainThread(async () => await TwoFactorAuthSuccessAsync());
|
||||
_vm.CloseAction = async () => await Navigation.PopModalAsync();
|
||||
DuoWebView = _duoWebView;
|
||||
if (Device.RuntimePlatform == Device.Android)
|
||||
@@ -141,7 +148,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||
private void Close_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
@@ -159,28 +166,29 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthAsync()
|
||||
|
||||
private async Task StartSetPasswordAsync()
|
||||
{
|
||||
if (_appOptions != null)
|
||||
_vm.CloseAction();
|
||||
var page = new SetPasswordPage(_appOptions);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
|
||||
private async Task TwoFactorAuthSuccessAsync()
|
||||
{
|
||||
if (_authingWithSso)
|
||||
{
|
||||
if (_appOptions.FromAutofillFramework && _appOptions.SaveType.HasValue)
|
||||
Application.Current.MainPage = new NavigationPage(new LockPage(_appOptions));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (AppHelpers.SetAlternateMainPage(_appOptions))
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AddEditPage(appOptions: _appOptions));
|
||||
return;
|
||||
}
|
||||
if (_appOptions.Uri != null)
|
||||
{
|
||||
Application.Current.MainPage = new NavigationPage(new AutofillCiphersPage(_appOptions));
|
||||
return;
|
||||
}
|
||||
var previousPage = await AppHelpers.ClearPreviousPage();
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
var previousPage = await _storageService.GetAsync<PreviousPageInfo>(Constants.PreviousPageKey);
|
||||
if (previousPage != null)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PreviousPageKey);
|
||||
}
|
||||
Application.Current.MainPage = new TabsPage(_appOptions, previousPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Bit.App.Pages
|
||||
private TwoFactorProviderType? _selectedProviderType;
|
||||
private string _totpInstruction;
|
||||
private string _webVaultUrl = "https://vault.bitwarden.com";
|
||||
private bool _authingWithSso = false;
|
||||
|
||||
public TwoFactorPageViewModel()
|
||||
{
|
||||
@@ -89,19 +90,21 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
public Command SubmitCommand { get; }
|
||||
public Action TwoFactorAction { get; set; }
|
||||
public Action TwoFactorAuthSuccessAction { get; set; }
|
||||
public Action StartSetPasswordAction { get; set; }
|
||||
public Action CloseAction { get; set; }
|
||||
|
||||
public void Init()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_authService.Email) ||
|
||||
string.IsNullOrWhiteSpace(_authService.MasterPasswordHash) ||
|
||||
if ((!_authService.AuthingWithSso() && !_authService.AuthingWithPassword()) ||
|
||||
_authService.TwoFactorProvidersData == null)
|
||||
{
|
||||
// TODO: dismiss modal?
|
||||
return;
|
||||
}
|
||||
|
||||
_authingWithSso = _authService.AuthingWithSso();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl))
|
||||
{
|
||||
_webVaultUrl = _environmentService.BaseUrl;
|
||||
@@ -204,14 +207,21 @@ namespace Bit.App.Pages
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Validating);
|
||||
await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember);
|
||||
var task = Task.Run(() => _syncService.FullSyncAsync(true));
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_messagingService.Send("listenYubiKeyOTP", false);
|
||||
_broadcasterService.Unsubscribe(nameof(TwoFactorPage));
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
TwoFactorAction?.Invoke();
|
||||
if (_authingWithSso && result.ResetMasterPassword)
|
||||
{
|
||||
StartSetPasswordAction?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
TwoFactorAuthSuccessAction?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (ApiException e)
|
||||
{
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
Text="{Binding Date, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
||||
<controls:FaButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding BindingContext.CopyCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
|
||||
@@ -286,6 +286,10 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task SliderInputAsync()
|
||||
{
|
||||
if (!_doneIniting)
|
||||
{
|
||||
return;
|
||||
}
|
||||
SetOptions();
|
||||
_passwordGenerationService.NormalizeOptions(_options, _enforcedPolicyOptions);
|
||||
Password = await _passwordGenerationService.GeneratePasswordAsync(_options);
|
||||
|
||||
@@ -143,19 +143,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
else
|
||||
{
|
||||
var fingerprintName = AppResources.Fingerprint;
|
||||
var biometricName = AppResources.Biometrics;
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
fingerprintName = supportsFace ? AppResources.FaceID : AppResources.TouchID;
|
||||
biometricName = supportsFace ? AppResources.FaceID : AppResources.TouchID;
|
||||
}
|
||||
else if (Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
if (item.Name == string.Format(AppResources.UnlockWith, biometricName))
|
||||
{
|
||||
fingerprintName = AppResources.Biometrics;
|
||||
}
|
||||
if (item.Name == string.Format(AppResources.UnlockWith, fingerprintName))
|
||||
{
|
||||
await _vm.UpdateFingerprintAsync();
|
||||
await _vm.UpdateBiometricAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,10 +22,11 @@ namespace Bit.App.Pages
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IBiometricService _biometricService;
|
||||
|
||||
private bool _supportsFingerprint;
|
||||
private bool _supportsBiometric;
|
||||
private bool _pin;
|
||||
private bool _fingerprint;
|
||||
private bool _biometric;
|
||||
private string _lastSyncDate;
|
||||
private string _vaultTimeoutDisplayValue;
|
||||
private string _vaultTimeoutActionDisplayValue;
|
||||
@@ -60,6 +61,7 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
|
||||
GroupedItems = new ExtendedObservableCollection<SettingsPageListGroup>();
|
||||
PageTitle = AppResources.Settings;
|
||||
@@ -69,7 +71,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_supportsFingerprint = await _platformUtilsService.SupportsBiometricAsync();
|
||||
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
|
||||
var lastSync = await _syncService.GetLastSyncAsync();
|
||||
if (lastSync != null)
|
||||
{
|
||||
@@ -83,7 +85,7 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutActionDisplayValue = _vaultTimeoutActions.FirstOrDefault(o => o.Value == action).Key;
|
||||
var pinSet = await _vaultTimeoutService.IsPinLockSetAsync();
|
||||
_pin = pinSet.Item1 || pinSet.Item2;
|
||||
_fingerprint = await _vaultTimeoutService.IsFingerprintLockSetAsync();
|
||||
_biometric = await _vaultTimeoutService.IsBiometricLockSetAsync();
|
||||
BuildList();
|
||||
}
|
||||
|
||||
@@ -288,31 +290,32 @@ namespace Bit.App.Pages
|
||||
BuildList();
|
||||
}
|
||||
|
||||
public async Task UpdateFingerprintAsync()
|
||||
public async Task UpdateBiometricAsync()
|
||||
{
|
||||
var current = _fingerprint;
|
||||
if (_fingerprint)
|
||||
var current = _biometric;
|
||||
if (_biometric)
|
||||
{
|
||||
_fingerprint = false;
|
||||
_biometric = false;
|
||||
}
|
||||
else if (await _platformUtilsService.SupportsBiometricAsync())
|
||||
{
|
||||
_fingerprint = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
_biometric = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
_deviceActionService.DeviceType == Core.Enums.DeviceType.Android ? "." : null);
|
||||
}
|
||||
if (_fingerprint == current)
|
||||
if (_biometric == current)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_fingerprint)
|
||||
if (_biometric)
|
||||
{
|
||||
await _storageService.SaveAsync(Constants.FingerprintUnlockKey, true);
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
await _storageService.SaveAsync(Constants.BiometricUnlockKey, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.FingerprintUnlockKey);
|
||||
await _storageService.RemoveAsync(Constants.BiometricUnlockKey);
|
||||
}
|
||||
_vaultTimeoutService.FingerprintLocked = false;
|
||||
_vaultTimeoutService.BiometricLocked = false;
|
||||
await _cryptoService.ToggleKeyAsync();
|
||||
BuildList();
|
||||
}
|
||||
@@ -371,22 +374,18 @@ namespace Bit.App.Pages
|
||||
new SettingsPageListItem { Name = AppResources.LockNow },
|
||||
new SettingsPageListItem { Name = AppResources.TwoStepLogin }
|
||||
};
|
||||
if (_supportsFingerprint || _fingerprint)
|
||||
if (_supportsBiometric || _biometric)
|
||||
{
|
||||
var fingerprintName = AppResources.Fingerprint;
|
||||
var biometricName = AppResources.Biometrics;
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
fingerprintName = _deviceActionService.SupportsFaceBiometric() ? AppResources.FaceID :
|
||||
biometricName = _deviceActionService.SupportsFaceBiometric() ? AppResources.FaceID :
|
||||
AppResources.TouchID;
|
||||
}
|
||||
else if (Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
fingerprintName = AppResources.Biometrics;
|
||||
}
|
||||
var item = new SettingsPageListItem
|
||||
{
|
||||
Name = string.Format(AppResources.UnlockWith, fingerprintName),
|
||||
SubLabel = _fingerprint ? AppResources.Enabled : AppResources.Disabled
|
||||
Name = string.Format(AppResources.UnlockWith, biometricName),
|
||||
SubLabel = _biometric ? AppResources.Enabled : AppResources.Disabled
|
||||
};
|
||||
securityItems.Insert(2, item);
|
||||
}
|
||||
|
||||
@@ -16,23 +16,36 @@
|
||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="10"
|
||||
Padding="10, 5"
|
||||
VerticalOptions="Center"
|
||||
HorizontalOptions="FillAndExpand">
|
||||
<Button Text="{u:I18n SyncVaultNow}"
|
||||
Clicked="Sync_Clicked"></Button>
|
||||
<Label StyleClass="text-muted, text-sm" HorizontalTextAlignment="Center">
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
<Span Text="{u:I18n LastSync}" />
|
||||
<Span Text=" " />
|
||||
<Span Text="{Binding LastSync}" />
|
||||
</FormattedString>
|
||||
</Label.FormattedText>
|
||||
</Label>
|
||||
<ScrollView Padding="0, 0, 0, 20">
|
||||
<StackLayout Padding="0" Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<Label
|
||||
Text="{u:I18n EnableSyncOnRefresh}"
|
||||
StyleClass="box-label, box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding EnableSyncOnRefresh}"
|
||||
StyleClass="box-value"
|
||||
HorizontalOptions="End" />
|
||||
</StackLayout>
|
||||
<Label
|
||||
Text="{u:I18n EnableSyncOnRefreshDescription}"
|
||||
StyleClass="box-footer-label, box-footer-label-switch" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box">
|
||||
<Button Text="{u:I18n SyncVaultNow}" Clicked="Sync_Clicked"></Button>
|
||||
<Label StyleClass="text-muted, text-sm" HorizontalTextAlignment="Center">
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
<Span Text="{u:I18n LastSync}" />
|
||||
<Span Text=" " />
|
||||
<Span Text="{Binding LastSync}" />
|
||||
</FormattedString>
|
||||
</Label.FormattedText>
|
||||
</Label>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
</pages:BaseContentPage>
|
||||
@@ -21,7 +21,7 @@ namespace Bit.App.Pages
|
||||
protected async override void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _vm.SetLastSyncAsync();
|
||||
await _vm.InitAsync();
|
||||
}
|
||||
|
||||
private async void Sync_Clicked(object sender, EventArgs e)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -11,25 +12,56 @@ namespace Bit.App.Pages
|
||||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly ISyncService _syncService;
|
||||
|
||||
private string _lastSync = "--";
|
||||
private bool _inited;
|
||||
private bool _syncOnRefresh;
|
||||
|
||||
public SyncPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
|
||||
PageTitle = AppResources.Sync;
|
||||
}
|
||||
|
||||
public bool EnableSyncOnRefresh
|
||||
{
|
||||
get => _syncOnRefresh;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _syncOnRefresh, value))
|
||||
{
|
||||
var task = UpdateSyncOnRefreshAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LastSync
|
||||
{
|
||||
get => _lastSync;
|
||||
set => SetProperty(ref _lastSync, value);
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
await SetLastSyncAsync();
|
||||
EnableSyncOnRefresh = await _storageService.GetAsync<bool>(Constants.SyncOnRefreshKey);
|
||||
_inited = true;
|
||||
}
|
||||
|
||||
public async Task UpdateSyncOnRefreshAsync()
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
await _storageService.SaveAsync(Constants.SyncOnRefreshKey, _syncOnRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetLastSyncAsync()
|
||||
{
|
||||
var last = await _syncService.GetLastSyncAsync();
|
||||
|
||||
@@ -112,9 +112,11 @@
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="{Binding PasswordFieldColSpan}"
|
||||
IsPassword="{Binding ShowPassword, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False" />
|
||||
IsTextPredictionEnabled="False"
|
||||
IsEnabled="{Binding Cipher.ViewPassword}"/>
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
@@ -123,7 +125,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}" />
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -132,7 +135,8 @@
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
@@ -141,7 +145,8 @@
|
||||
Grid.Column="3"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n GeneratePassword}" />
|
||||
AutomationProperties.Name="{u:I18n GeneratePassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
</Grid>
|
||||
|
||||
<Grid StyleClass="box-row, box-row-input">
|
||||
@@ -163,9 +168,12 @@
|
||||
Text="{Binding Cipher.Login.Totp}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False"
|
||||
IsPassword="{Binding Cipher.ViewPassword, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding Cipher.ViewPassword}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="{Binding TotpColumnSpan}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
@@ -173,6 +181,7 @@
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
IsVisible="{Binding Cipher.ViewPassword}"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ScanQrTitle}" />
|
||||
</Grid>
|
||||
@@ -562,6 +571,7 @@
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsHiddenType}"
|
||||
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
||||
IsEnabled="{Binding ShowViewHidden}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False">
|
||||
<Entry.Keyboard>
|
||||
@@ -582,7 +592,7 @@
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowHiddenValueIcon}"
|
||||
Command="{Binding ToggleHiddenValueCommand}"
|
||||
IsVisible="{Binding IsHiddenType}"
|
||||
IsVisible="{Binding ShowViewHidden}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Resources;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Essentials;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
@@ -243,6 +245,12 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if (DoOnce())
|
||||
{
|
||||
var cameraPermission = await PermissionManager.CheckAndRequestPermissionAsync(new Permissions.Camera());
|
||||
if (cameraPermission != PermissionStatus.Granted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var page = new ScanPage(key =>
|
||||
{
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
|
||||
@@ -274,6 +274,8 @@ namespace Bit.App.Pages
|
||||
public bool ShowAttachments => Cipher.HasAttachments;
|
||||
public string ShowPasswordIcon => ShowPassword ? "" : "";
|
||||
public string ShowCardCodeIcon => ShowCardCode ? "" : "";
|
||||
public int PasswordFieldColSpan => Cipher.ViewPassword ? 1 : 4;
|
||||
public int TotpColumnSpan => Cipher.ViewPassword ? 1 : 2;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
@@ -631,7 +633,8 @@ namespace Bit.App.Pages
|
||||
Fields.Add(new AddEditPageFieldViewModel(Cipher, new FieldView
|
||||
{
|
||||
Type = type,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name
|
||||
Name = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
NewField = true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -818,6 +821,7 @@ namespace Bit.App.Pages
|
||||
public bool IsTextType => _field.Type == FieldType.Text;
|
||||
public bool IsBooleanType => _field.Type == FieldType.Boolean;
|
||||
public bool IsHiddenType => _field.Type == FieldType.Hidden;
|
||||
public bool ShowViewHidden => IsHiddenType && (_cipher.ViewPassword || _field.NewField);
|
||||
|
||||
public void ToggleHiddenValue()
|
||||
{
|
||||
|
||||
@@ -194,6 +194,7 @@ namespace Bit.App.Pages
|
||||
base.OnDisappearing();
|
||||
IsBusy = false;
|
||||
_broadcasterService.Unsubscribe(_pageName);
|
||||
_vm.DisableRefreshing();
|
||||
}
|
||||
|
||||
private async void RowSelected(object sender, SelectedItemChangedEventArgs e)
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Bit.App.Pages
|
||||
private bool _showNoData;
|
||||
private bool _showList;
|
||||
private bool _websiteIconsEnabled;
|
||||
private bool _syncRefreshing;
|
||||
private string _noDataText;
|
||||
private List<CipherView> _allCiphers;
|
||||
private Dictionary<string, int> _folderCounts = new Dictionary<string, int>();
|
||||
@@ -44,6 +45,7 @@ namespace Bit.App.Pages
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IStorageService _storageService;
|
||||
|
||||
public GroupingsPageViewModel()
|
||||
{
|
||||
@@ -57,6 +59,7 @@ namespace Bit.App.Pages
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
|
||||
Loading = true;
|
||||
PageTitle = AppResources.MyVault;
|
||||
@@ -94,6 +97,11 @@ namespace Bit.App.Pages
|
||||
get => _refreshing;
|
||||
set => SetProperty(ref _refreshing, value);
|
||||
}
|
||||
public bool SyncRefreshing
|
||||
{
|
||||
get => _syncRefreshing;
|
||||
set => SetProperty(ref _syncRefreshing, value);
|
||||
}
|
||||
public bool Loading
|
||||
{
|
||||
get => _loading;
|
||||
@@ -149,6 +157,13 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (await _storageService.GetAsync<bool>(Constants.SyncOnRefreshKey) && Refreshing && !SyncRefreshing)
|
||||
{
|
||||
SyncRefreshing = true;
|
||||
await _syncService.FullSyncAsync(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_doingLoad = true;
|
||||
LoadedOnce = true;
|
||||
ShowNoData = false;
|
||||
@@ -266,12 +281,18 @@ namespace Bit.App.Pages
|
||||
_doingLoad = false;
|
||||
Loaded = true;
|
||||
Loading = false;
|
||||
Refreshing = false;
|
||||
ShowNoData = (MainPage && !HasCiphers) || !groupedItems.Any();
|
||||
ShowList = !ShowNoData;
|
||||
DisableRefreshing();
|
||||
}
|
||||
}
|
||||
|
||||
public void DisableRefreshing()
|
||||
{
|
||||
Refreshing = false;
|
||||
SyncRefreshing = false;
|
||||
}
|
||||
|
||||
public async Task SelectCipherAsync(CipherView cipher)
|
||||
{
|
||||
var page = new ViewPage(cipher.Id);
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
Text="{Binding LastUsedDate, Mode=OneWay, Converter={StaticResource dateTime}}" />
|
||||
<controls:FaButton
|
||||
StyleClass="list-row-button, list-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding BindingContext.CopyCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
Grid.Column="0" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="LoginUsername"
|
||||
Grid.Row="0"
|
||||
@@ -134,7 +134,8 @@
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}" />
|
||||
AutomationProperties.Name="{u:I18n CheckPassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowPasswordIcon}"
|
||||
@@ -143,17 +144,19 @@
|
||||
Grid.Column="2"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="LoginPassword"
|
||||
Grid.Row="0"
|
||||
Grid.Column="3"
|
||||
Grid.RowSpan="2"
|
||||
AutomationProperties.IsInAccessibleTree="True"
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}" />
|
||||
AutomationProperties.Name="{u:I18n CopyPassword}"
|
||||
IsVisible="{Binding Cipher.ViewPassword}" />
|
||||
</Grid>
|
||||
<BoxView StyleClass="box-row-separator"
|
||||
IsVisible="{Binding Cipher.Login.Password, Converter={StaticResource stringHasValue}}" />
|
||||
@@ -189,7 +192,7 @@
|
||||
VerticalOptions="CenterAndExpand" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="LoginTotp"
|
||||
Grid.Row="0"
|
||||
@@ -234,7 +237,7 @@
|
||||
Grid.Column="0" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="CardNumber"
|
||||
Grid.Row="0"
|
||||
@@ -306,7 +309,7 @@
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding CopyCommand}"
|
||||
CommandParameter="CardCode"
|
||||
Grid.Row="0"
|
||||
@@ -467,7 +470,7 @@
|
||||
Grid.Column="0"
|
||||
IsVisible="{Binding IsWebsite, Mode=OneWay}" />
|
||||
<Label
|
||||
Text="{Binding HostnameOrUri, Mode=OneWay}"
|
||||
Text="{Binding HostOrUri, Mode=OneWay}"
|
||||
StyleClass="box-value"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0" />
|
||||
@@ -484,7 +487,7 @@
|
||||
AutomationProperties.Name="{u:I18n Launch}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding BindingContext.CopyUriCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
Grid.Row="0"
|
||||
@@ -570,7 +573,7 @@
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text="{Binding ShowHiddenValueIcon}"
|
||||
Command="{Binding ToggleHiddenValueCommand}"
|
||||
IsVisible="{Binding IsHiddenType}"
|
||||
IsVisible="{Binding ShowViewHidden}"
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Grid.RowSpan="2"
|
||||
@@ -578,7 +581,7 @@
|
||||
AutomationProperties.Name="{u:I18n ToggleVisibility}" />
|
||||
<controls:FaButton
|
||||
StyleClass="box-row-button, box-row-button-platform"
|
||||
Text=""
|
||||
Text=""
|
||||
Command="{Binding BindingContext.CopyFieldCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding Field}"
|
||||
IsVisible="{Binding ShowCopyButton}"
|
||||
@@ -678,4 +681,4 @@
|
||||
</Button>
|
||||
</AbsoluteLayout>
|
||||
|
||||
</pages:BaseContentPage>
|
||||
</pages:BaseContentPage>
|
||||
|
||||
@@ -684,8 +684,9 @@ namespace Bit.App.Pages
|
||||
public bool IsTextType => _field.Type == Core.Enums.FieldType.Text;
|
||||
public bool IsBooleanType => _field.Type == Core.Enums.FieldType.Boolean;
|
||||
public bool IsHiddenType => _field.Type == Core.Enums.FieldType.Hidden;
|
||||
public bool ShowViewHidden => IsHiddenType && _cipher.ViewPassword;
|
||||
public bool ShowCopyButton => _field.Type != Core.Enums.FieldType.Boolean &&
|
||||
!string.IsNullOrWhiteSpace(_field.Value);
|
||||
!string.IsNullOrWhiteSpace(_field.Value) && !(IsHiddenType && !_cipher.ViewPassword);
|
||||
|
||||
public void ToggleHiddenValue()
|
||||
{
|
||||
|
||||
115
src/App/Resources/AppResources.Designer.cs
generated
@@ -9,7 +9,6 @@
|
||||
|
||||
namespace Bit.App.Resources {
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
@@ -2937,5 +2936,119 @@ namespace Bit.App.Resources {
|
||||
return ResourceManager.GetString("DoYouReallyWantToSoftDeleteCipher", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string BiometricInvalidated {
|
||||
get {
|
||||
return ResourceManager.GetString("BiometricInvalidated", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string EnableSyncOnRefresh {
|
||||
get {
|
||||
return ResourceManager.GetString("EnableSyncOnRefresh", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string EnableSyncOnRefreshDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("EnableSyncOnRefreshDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string LogInSso {
|
||||
get {
|
||||
return ResourceManager.GetString("LogInSso", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string LogInSsoSummary {
|
||||
get {
|
||||
return ResourceManager.GetString("LogInSsoSummary", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string OrgIdentifier {
|
||||
get {
|
||||
return ResourceManager.GetString("OrgIdentifier", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string LoginSsoError {
|
||||
get {
|
||||
return ResourceManager.GetString("LoginSsoError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SetMasterPassword {
|
||||
get {
|
||||
return ResourceManager.GetString("SetMasterPassword", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string SetMasterPasswordSummary {
|
||||
get {
|
||||
return ResourceManager.GetString("SetMasterPasswordSummary", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string MasterPasswordPolicyInEffect {
|
||||
get {
|
||||
return ResourceManager.GetString("MasterPasswordPolicyInEffect", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PolicyInEffectMinComplexity {
|
||||
get {
|
||||
return ResourceManager.GetString("PolicyInEffectMinComplexity", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PolicyInEffectMinLength {
|
||||
get {
|
||||
return ResourceManager.GetString("PolicyInEffectMinLength", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PolicyInEffectUppercase {
|
||||
get {
|
||||
return ResourceManager.GetString("PolicyInEffectUppercase", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PolicyInEffectLowercase {
|
||||
get {
|
||||
return ResourceManager.GetString("PolicyInEffectLowercase", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PolicyInEffectNumbers {
|
||||
get {
|
||||
return ResourceManager.GetString("PolicyInEffectNumbers", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string PolicyInEffectSpecial {
|
||||
get {
|
||||
return ResourceManager.GetString("PolicyInEffectSpecial", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string MasterPasswordPolicyValidationTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("MasterPasswordPolicyValidationTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string MasterPasswordPolicyValidationMessage {
|
||||
get {
|
||||
return ResourceManager.GetString("MasterPasswordPolicyValidationMessage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Loading {
|
||||
get {
|
||||
return ResourceManager.GetString("Loading", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1564,7 +1564,7 @@
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
<value>Biometric Verification</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
@@ -1674,4 +1674,61 @@
|
||||
<value>Do you really want to send to the trash?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
<data name="BiometricInvalidated" xml:space="preserve">
|
||||
<value>Biometric change detected, login using Master Password to enable again.</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefresh" xml:space="preserve">
|
||||
<value>Enable sync on refresh</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefreshDescription" xml:space="preserve">
|
||||
<value>Syncing vault with pull down gesture.</value>
|
||||
</data>
|
||||
<data name="LogInSso" xml:space="preserve">
|
||||
<value>Enterprise Single Sign-On</value>
|
||||
</data>
|
||||
<data name="LogInSsoSummary" xml:space="preserve">
|
||||
<value>Quickly log in using your organization's single sign-on portal. Please enter your organization's identifier to begin.</value>
|
||||
</data>
|
||||
<data name="OrgIdentifier" xml:space="preserve">
|
||||
<value>Organization Identifier</value>
|
||||
</data>
|
||||
<data name="LoginSsoError" xml:space="preserve">
|
||||
<value>Currently unable to login with SSO</value>
|
||||
</data>
|
||||
<data name="SetMasterPassword" xml:space="preserve">
|
||||
<value>Set Master Password</value>
|
||||
</data>
|
||||
<data name="SetMasterPasswordSummary" xml:space="preserve">
|
||||
<value>In order to complete logging in with SSO, please set a master password to access and protect your vault.</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyInEffect" xml:space="preserve">
|
||||
<value>One or more organization policies require your master password to meet the following requirements:</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinComplexity" xml:space="preserve">
|
||||
<value>Minimum complexity score of {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinLength" xml:space="preserve">
|
||||
<value>Minimum length of {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectUppercase" xml:space="preserve">
|
||||
<value>Contain one or more uppercase characters</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectLowercase" xml:space="preserve">
|
||||
<value>Contain one or more lowercase characters</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectNumbers" xml:space="preserve">
|
||||
<value>Contain one or more numbers</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectSpecial" xml:space="preserve">
|
||||
<value>Contain one or more of the following special characters: {0}</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationTitle" xml:space="preserve">
|
||||
<value>Invalid Password</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationMessage" xml:space="preserve">
|
||||
<value>Password does not meet organization requirements. Please check the policy information and try again.</value>
|
||||
</data>
|
||||
<data name="Loading" xml:space="preserve">
|
||||
<value>Loading</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -519,28 +519,28 @@
|
||||
<value>Заблакіраваць</value>
|
||||
</data>
|
||||
<data name="FifteenMinutes" xml:space="preserve">
|
||||
<value>15 minutes</value>
|
||||
<value>15 хвілін</value>
|
||||
</data>
|
||||
<data name="OneHour" xml:space="preserve">
|
||||
<value>1 hour</value>
|
||||
<value>1 гадзіна</value>
|
||||
</data>
|
||||
<data name="OneMinute" xml:space="preserve">
|
||||
<value>1 хвіліна</value>
|
||||
</data>
|
||||
<data name="FourHours" xml:space="preserve">
|
||||
<value>4 hours</value>
|
||||
<value>4 гадзіны</value>
|
||||
</data>
|
||||
<data name="Immediately" xml:space="preserve">
|
||||
<value>Immediately</value>
|
||||
<value>Адразу</value>
|
||||
</data>
|
||||
<data name="VaultTimeout" xml:space="preserve">
|
||||
<value>Vault Timeout</value>
|
||||
<value>Таймаўт сховішча</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutAction" xml:space="preserve">
|
||||
<value>Vault Timeout Action</value>
|
||||
<value>Дзеянне пры таймаўце</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutLogOutConfirmation" xml:space="preserve">
|
||||
<value>Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?</value>
|
||||
<value>Выхад з сістэмы выдаліць доступ да сховішча і патрабуе анлайн-аутэнтыфікацію па заканчэнні перыяду чакання. Вы сапраўды жадаеце ўключыць гэтую наладу?</value>
|
||||
</data>
|
||||
<data name="LoggingIn" xml:space="preserve">
|
||||
<value>Уваход...</value>
|
||||
@@ -1440,7 +1440,7 @@
|
||||
<value>Разблакіраваць</value>
|
||||
</data>
|
||||
<data name="ThirtyMinutes" xml:space="preserve">
|
||||
<value>30 minutes</value>
|
||||
<value>30 хвілін</value>
|
||||
</data>
|
||||
<data name="SetPINDescription" xml:space="preserve">
|
||||
<value>Задайце PIN-код для разблакіроўкі Bitwarden. Налады PIN-кода будуць скінуты, калі вы калі-небудзь цалкам выйдзеце з праграмы.</value>
|
||||
@@ -1536,7 +1536,7 @@
|
||||
<value>Апавяшчэнне аб захаванні аўтаматычна прапануе вам захаваць новыя элементы ў сховішчы пасля іх дадання.</value>
|
||||
</data>
|
||||
<data name="OnRestart" xml:space="preserve">
|
||||
<value>On App Restart</value>
|
||||
<value>Пры перазапуску</value>
|
||||
</data>
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>Аўтазапаўненне спрашчае бяспечны доступ да вашага сховішча Bitwarden з другіх вэб-сайтаў і праграм. Здаецца, вы не уключылі службу аўтазапаўнення Bitwarden. Гэта можна зрабіць у наладах.</value>
|
||||
@@ -1674,4 +1674,61 @@
|
||||
<value>Вы сапраўды хочаце адправіць элемент у сметніцу?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
<data name="BiometricInvalidated" xml:space="preserve">
|
||||
<value>Biometric change detected, login using Master Password to enable again.</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefresh" xml:space="preserve">
|
||||
<value>Enable sync on refresh</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefreshDescription" xml:space="preserve">
|
||||
<value>Syncing vault with pull down gesture.</value>
|
||||
</data>
|
||||
<data name="LogInSso" xml:space="preserve">
|
||||
<value>Enterprise Single Sign-On</value>
|
||||
</data>
|
||||
<data name="LogInSsoSummary" xml:space="preserve">
|
||||
<value>Quickly log in using your organization's single sign-on portal. Please enter your organization's identifier to begin.</value>
|
||||
</data>
|
||||
<data name="OrgIdentifier" xml:space="preserve">
|
||||
<value>Organization Identifier</value>
|
||||
</data>
|
||||
<data name="LoginSsoError" xml:space="preserve">
|
||||
<value>Currently unable to login with SSO</value>
|
||||
</data>
|
||||
<data name="SetMasterPassword" xml:space="preserve">
|
||||
<value>Set Master Password</value>
|
||||
</data>
|
||||
<data name="SetMasterPasswordSummary" xml:space="preserve">
|
||||
<value>In order to complete logging in with SSO, please set a master password to access and protect your vault.</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyInEffect" xml:space="preserve">
|
||||
<value>One or more organization policies require your master password to meet the following requirements:</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinComplexity" xml:space="preserve">
|
||||
<value>Minimum complexity score of {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinLength" xml:space="preserve">
|
||||
<value>Minimum length of {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectUppercase" xml:space="preserve">
|
||||
<value>Contain one or more uppercase characters</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectLowercase" xml:space="preserve">
|
||||
<value>Contain one or more lowercase characters</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectNumbers" xml:space="preserve">
|
||||
<value>Contain one or more numbers</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectSpecial" xml:space="preserve">
|
||||
<value>Contain one or more of the following special characters: {0}</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationTitle" xml:space="preserve">
|
||||
<value>Invalid Password</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationMessage" xml:space="preserve">
|
||||
<value>Password does not meet organization requirements. Please check the policy information and try again.</value>
|
||||
</data>
|
||||
<data name="Loading" xml:space="preserve">
|
||||
<value>Loading</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -519,28 +519,28 @@
|
||||
<value>Заключване</value>
|
||||
</data>
|
||||
<data name="FifteenMinutes" xml:space="preserve">
|
||||
<value>15 minutes</value>
|
||||
<value>15 минути</value>
|
||||
</data>
|
||||
<data name="OneHour" xml:space="preserve">
|
||||
<value>1 hour</value>
|
||||
<value>1 час</value>
|
||||
</data>
|
||||
<data name="OneMinute" xml:space="preserve">
|
||||
<value>1 минута</value>
|
||||
</data>
|
||||
<data name="FourHours" xml:space="preserve">
|
||||
<value>4 hours</value>
|
||||
<value>4 часа</value>
|
||||
</data>
|
||||
<data name="Immediately" xml:space="preserve">
|
||||
<value>Immediately</value>
|
||||
<value>Незабавно</value>
|
||||
</data>
|
||||
<data name="VaultTimeout" xml:space="preserve">
|
||||
<value>Vault Timeout</value>
|
||||
<value>Време за достъп</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutAction" xml:space="preserve">
|
||||
<value>Vault Timeout Action</value>
|
||||
<value>Действие при изтичане на времето</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutLogOutConfirmation" xml:space="preserve">
|
||||
<value>Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?</value>
|
||||
<value>Излизането от трезора изцяло спира достъпа до него след изтичане на времето. Ще ви се наложи отново да се идентифицирате, за да го достъпите. Сигурни ли сте, че искате това действие?</value>
|
||||
</data>
|
||||
<data name="LoggingIn" xml:space="preserve">
|
||||
<value>Вписване…</value>
|
||||
@@ -1440,7 +1440,7 @@
|
||||
<value>Отключване</value>
|
||||
</data>
|
||||
<data name="ThirtyMinutes" xml:space="preserve">
|
||||
<value>30 minutes</value>
|
||||
<value>30 минути</value>
|
||||
</data>
|
||||
<data name="SetPINDescription" xml:space="preserve">
|
||||
<value>Задайте ПИН за отключване на Битуорден. Настройките за ПИН се изчистват при всяко пълно излизане от програмата.</value>
|
||||
@@ -1536,7 +1536,7 @@
|
||||
<value>Дали да бъдете автоматично подканени да запазите в трезора данни за вписване, които въвеждате за първи път.</value>
|
||||
</data>
|
||||
<data name="OnRestart" xml:space="preserve">
|
||||
<value>On App Restart</value>
|
||||
<value>При рестартиране на приложението</value>
|
||||
</data>
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>Автоматичното дописване позволява сигурен достъп до трезора от сайтове и приложения. Изглежда не сте включили тази възможност в Битуорден, можете да го направите от екрана за настройките.</value>
|
||||
@@ -1674,4 +1674,61 @@
|
||||
<value>Сигурни ли сте, че искате да преместите записа в кошчето?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
<data name="BiometricInvalidated" xml:space="preserve">
|
||||
<value>Засечена е промяна в биометричните данни. Впишете се с главната парола, за да я включите отново биометричната идентификация.</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefresh" xml:space="preserve">
|
||||
<value>Синхронизация при опресняване</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefreshDescription" xml:space="preserve">
|
||||
<value>Синхронизиране на трезора с жест — провлачване надолу.</value>
|
||||
</data>
|
||||
<data name="LogInSso" xml:space="preserve">
|
||||
<value>Еднократна идентификация (SSO)</value>
|
||||
</data>
|
||||
<data name="LogInSsoSummary" xml:space="preserve">
|
||||
<value>Вписване чрез портала на организацията ви за еднократна идентификация. За да продължите, въведете идентификатора на организацията.</value>
|
||||
</data>
|
||||
<data name="OrgIdentifier" xml:space="preserve">
|
||||
<value>Идентификатор на организация</value>
|
||||
</data>
|
||||
<data name="LoginSsoError" xml:space="preserve">
|
||||
<value>В момента не може да се впишете с еднократна идентификация</value>
|
||||
</data>
|
||||
<data name="SetMasterPassword" xml:space="preserve">
|
||||
<value>Задаване на главна парола</value>
|
||||
</data>
|
||||
<data name="SetMasterPasswordSummary" xml:space="preserve">
|
||||
<value>За да завършите настройките за еднократна идентификация, трябва да зададете главна парола за трезора.</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyInEffect" xml:space="preserve">
|
||||
<value>Поне една политика на организация има следните изисквания към главната ви парола:</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinComplexity" xml:space="preserve">
|
||||
<value>Минимална сложност от {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinLength" xml:space="preserve">
|
||||
<value>Минимална дължина от {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectUppercase" xml:space="preserve">
|
||||
<value>Поне една главна буква</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectLowercase" xml:space="preserve">
|
||||
<value>Поне една малка буква</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectNumbers" xml:space="preserve">
|
||||
<value>Поне една цифра</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectSpecial" xml:space="preserve">
|
||||
<value>Поне един от следните специални знаци: {0}</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationTitle" xml:space="preserve">
|
||||
<value>Грешна парола</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationMessage" xml:space="preserve">
|
||||
<value>Паролата не покрива изискванията на организацията. Проверете политиките и пробвайте отново.</value>
|
||||
</data>
|
||||
<data name="Loading" xml:space="preserve">
|
||||
<value>Зареждане</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1674,4 +1674,61 @@
|
||||
<value>Esteu segur que voleu enviar-ho a la paperera?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
<data name="BiometricInvalidated" xml:space="preserve">
|
||||
<value>S'ha detectat un canvi biomètric. Inicieu la sessió mitjançant la contrasenya principal per tornar a activar-la.</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefresh" xml:space="preserve">
|
||||
<value>Activa la sincronització en actualitzar-se</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefreshDescription" xml:space="preserve">
|
||||
<value>Sincronització de la caixa forta amb un gest cap avall.</value>
|
||||
</data>
|
||||
<data name="LogInSso" xml:space="preserve">
|
||||
<value>Inici de sessió únic d'empresa</value>
|
||||
</data>
|
||||
<data name="LogInSsoSummary" xml:space="preserve">
|
||||
<value>Inicieu la sessió ràpidament mitjançant el portal d'inici de sessió únic de la vostra organització. Introduïu l'identificador de la vostra organització per començar.</value>
|
||||
</data>
|
||||
<data name="OrgIdentifier" xml:space="preserve">
|
||||
<value>Identificador d’organització</value>
|
||||
</data>
|
||||
<data name="LoginSsoError" xml:space="preserve">
|
||||
<value>Actualment no es pot iniciar sessió amb SSO</value>
|
||||
</data>
|
||||
<data name="SetMasterPassword" xml:space="preserve">
|
||||
<value>Estableix la contrasenya mestra</value>
|
||||
</data>
|
||||
<data name="SetMasterPasswordSummary" xml:space="preserve">
|
||||
<value>Per completar la sessió amb SSO, configureu una contrasenya mestra per accedir i protegir la vostra caixa forta.</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyInEffect" xml:space="preserve">
|
||||
<value>Una o més polítiques d’organització requereixen que la vostra contrasenya mestra complisca els requisits següents:</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinComplexity" xml:space="preserve">
|
||||
<value>Puntuació mínima de complexitat de {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinLength" xml:space="preserve">
|
||||
<value>Longitud mínima de {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectUppercase" xml:space="preserve">
|
||||
<value>Conté un o més caràcters en majúscula</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectLowercase" xml:space="preserve">
|
||||
<value>Conté un o més caràcters en minúscula</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectNumbers" xml:space="preserve">
|
||||
<value>Conté un o més números</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectSpecial" xml:space="preserve">
|
||||
<value>Conté un o més dels següents caràcters especials: {0}</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationTitle" xml:space="preserve">
|
||||
<value>Contrasenya no vàlida</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationMessage" xml:space="preserve">
|
||||
<value>La contrasenya no compleix els requisits de l'organització. Comproveu la informació de la política i torneu-ho a provar.</value>
|
||||
</data>
|
||||
<data name="Loading" xml:space="preserve">
|
||||
<value>S'està carregant</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -519,28 +519,28 @@
|
||||
<value>Zamknout</value>
|
||||
</data>
|
||||
<data name="FifteenMinutes" xml:space="preserve">
|
||||
<value>15 minutes</value>
|
||||
<value>15 minut</value>
|
||||
</data>
|
||||
<data name="OneHour" xml:space="preserve">
|
||||
<value>1 hour</value>
|
||||
<value>1 hodina</value>
|
||||
</data>
|
||||
<data name="OneMinute" xml:space="preserve">
|
||||
<value>1 minuta</value>
|
||||
</data>
|
||||
<data name="FourHours" xml:space="preserve">
|
||||
<value>4 hours</value>
|
||||
<value>4 hodiny</value>
|
||||
</data>
|
||||
<data name="Immediately" xml:space="preserve">
|
||||
<value>Immediately</value>
|
||||
<value>Okamžitě</value>
|
||||
</data>
|
||||
<data name="VaultTimeout" xml:space="preserve">
|
||||
<value>Vault Timeout</value>
|
||||
<value>Časový limit trezoru</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutAction" xml:space="preserve">
|
||||
<value>Vault Timeout Action</value>
|
||||
<value>Akce při vypršení časového limitu trezoru</value>
|
||||
</data>
|
||||
<data name="VaultTimeoutLogOutConfirmation" xml:space="preserve">
|
||||
<value>Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?</value>
|
||||
<value>Po vypršení časového limitu dojde k odhlášení. Přístup k trezoru bude odebrán a pro opětovné přihlášení bude vyžadováno online ověření. Opravdu chcete použít toto nastavení?</value>
|
||||
</data>
|
||||
<data name="LoggingIn" xml:space="preserve">
|
||||
<value>Přihlašování…</value>
|
||||
@@ -1440,7 +1440,7 @@
|
||||
<value>Odemknout</value>
|
||||
</data>
|
||||
<data name="ThirtyMinutes" xml:space="preserve">
|
||||
<value>30 minutes</value>
|
||||
<value>30 minut</value>
|
||||
</data>
|
||||
<data name="SetPINDescription" xml:space="preserve">
|
||||
<value>Nastavte svůj PIN kód pro odemknutí trezoru. Pokud se zcela odhlásíte z aplikace bude váš současný PIN bude resetován.</value>
|
||||
@@ -1536,7 +1536,7 @@
|
||||
<value>„Výzva k uložení“ vás automaticky vyzve k uložení nových položek do trezoru, kdykoli je poprvé zadáte.</value>
|
||||
</data>
|
||||
<data name="OnRestart" xml:space="preserve">
|
||||
<value>On App Restart</value>
|
||||
<value>Při restartu aplikace</value>
|
||||
</data>
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>Automatické vyplňování usnadňuje bezpečný přístup k trezoru Bitwarden z jiných webových stránek a aplikací. Vypadá to, že jste službu Bitwarden nepovolili. Povolte automatické vyplnění pro Bitwarden z obrazovky „Nastavení“.</value>
|
||||
@@ -1674,4 +1674,61 @@
|
||||
<value>Opravdu chcete položku přesunout do koše?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
<data name="BiometricInvalidated" xml:space="preserve">
|
||||
<value>Biometric change detected, login using Master Password to enable again.</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefresh" xml:space="preserve">
|
||||
<value>Enable sync on refresh</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefreshDescription" xml:space="preserve">
|
||||
<value>Syncing vault with pull down gesture.</value>
|
||||
</data>
|
||||
<data name="LogInSso" xml:space="preserve">
|
||||
<value>Enterprise Single Sign-On</value>
|
||||
</data>
|
||||
<data name="LogInSsoSummary" xml:space="preserve">
|
||||
<value>Quickly log in using your organization's single sign-on portal. Please enter your organization's identifier to begin.</value>
|
||||
</data>
|
||||
<data name="OrgIdentifier" xml:space="preserve">
|
||||
<value>Organization Identifier</value>
|
||||
</data>
|
||||
<data name="LoginSsoError" xml:space="preserve">
|
||||
<value>Currently unable to login with SSO</value>
|
||||
</data>
|
||||
<data name="SetMasterPassword" xml:space="preserve">
|
||||
<value>Set Master Password</value>
|
||||
</data>
|
||||
<data name="SetMasterPasswordSummary" xml:space="preserve">
|
||||
<value>In order to complete logging in with SSO, please set a master password to access and protect your vault.</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyInEffect" xml:space="preserve">
|
||||
<value>One or more organization policies require your master password to meet the following requirements:</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinComplexity" xml:space="preserve">
|
||||
<value>Minimum complexity score of {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectMinLength" xml:space="preserve">
|
||||
<value>Minimum length of {0}</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectUppercase" xml:space="preserve">
|
||||
<value>Contain one or more uppercase characters</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectLowercase" xml:space="preserve">
|
||||
<value>Contain one or more lowercase characters</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectNumbers" xml:space="preserve">
|
||||
<value>Contain one or more numbers</value>
|
||||
</data>
|
||||
<data name="PolicyInEffectSpecial" xml:space="preserve">
|
||||
<value>Contain one or more of the following special characters: {0}</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationTitle" xml:space="preserve">
|
||||
<value>Invalid Password</value>
|
||||
</data>
|
||||
<data name="MasterPasswordPolicyValidationMessage" xml:space="preserve">
|
||||
<value>Password does not meet organization requirements. Please check the policy information and try again.</value>
|
||||
</data>
|
||||
<data name="Loading" xml:space="preserve">
|
||||
<value>Loading</value>
|
||||
</data>
|
||||
</root>
|
||||