mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
051e15215d | ||
|
|
fee8f58c0a | ||
|
|
cc036cf3c5 | ||
|
|
4e51517ddb | ||
|
|
3b7454961d | ||
|
|
88009e1a63 | ||
|
|
0afca29b0c | ||
|
|
46a75a2944 | ||
|
|
c099f82752 | ||
|
|
1da94bd9c8 | ||
|
|
96ce8165e9 | ||
|
|
f9b617339d | ||
|
|
58084810f3 | ||
|
|
429e62e6b5 | ||
|
|
b0b7f2afdf | ||
|
|
55f160d125 | ||
|
|
f6352f5392 | ||
|
|
ac7e90c0aa | ||
|
|
88fccfd6cd | ||
|
|
5fdf8e6045 | ||
|
|
d9907cdbeb | ||
|
|
d308f1ca3b | ||
|
|
0cdc138ba3 | ||
|
|
59d5314164 | ||
|
|
9c08a37772 | ||
|
|
b13f5356fe | ||
|
|
5f0c9725ce | ||
|
|
f951fea555 | ||
|
|
4b989b01e9 | ||
|
|
aed3ec5474 | ||
|
|
b354986199 | ||
|
|
e1983a7d66 | ||
|
|
0400d79f43 | ||
|
|
c911484632 | ||
|
|
713e441d2e | ||
|
|
d4b577732b | ||
|
|
440a410d7f | ||
|
|
37a536b138 | ||
|
|
a0aca3e837 | ||
|
|
b58c29111a | ||
|
|
b0f86ea161 | ||
|
|
93b59a75a4 | ||
|
|
54fcabaea6 | ||
|
|
0e966c0304 | ||
|
|
a363712127 | ||
|
|
4d8c665917 | ||
|
|
53bdd92e72 | ||
|
|
e51aa39ede | ||
|
|
33c82129ff | ||
|
|
7c5b8c0e9f | ||
|
|
9dc01bca1c | ||
|
|
3c7920b84c | ||
|
|
b92f3abbaf | ||
|
|
b6747a63ed | ||
|
|
41a44548d2 | ||
|
|
a79d3a0d7c | ||
|
|
f3a17709e5 | ||
|
|
ced9d33d2e | ||
|
|
23b1373f80 | ||
|
|
a80eb1f533 | ||
|
|
f657edf195 | ||
|
|
d34279dca5 | ||
|
|
954aa1112a | ||
|
|
b35a3339cb | ||
|
|
b59433debd | ||
|
|
fb2db9c652 | ||
|
|
2507f3301b | ||
|
|
bdad5e4f0a | ||
|
|
b5dcdc74d7 | ||
|
|
e2d1da02d3 | ||
|
|
8253f18312 | ||
|
|
f4a98a2031 | ||
|
|
224845cfd3 | ||
|
|
fc8c2ad67a | ||
|
|
c9d6f58563 | ||
|
|
325b557506 | ||
|
|
ce751cfc87 | ||
|
|
0f451fd4b9 |
@@ -1,5 +1,5 @@
|
||||
image:
|
||||
- Visual Studio 2017
|
||||
- Visual Studio 2019 Preview
|
||||
- Ubuntu1804
|
||||
|
||||
branches:
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bitwarden-fdroid",
|
||||
"name": "bitwarden-mobile",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
@@ -196,9 +196,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.11",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
|
||||
@@ -63,6 +63,11 @@ namespace Bit.Droid.Accessibility
|
||||
new Browser("com.kiwibrowser.browser", "url_bar"),
|
||||
new Browser("com.ecosia.android", "url_bar"),
|
||||
new Browser("com.qwant.liberty", "url_bar_title"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir", "url_text"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir_black", "url_text"),
|
||||
new Browser("jp.co.fenrir.android.sleipnir_test", "url_text"),
|
||||
new Browser("com.vivaldi.browser", "url_bar"),
|
||||
new Browser("com.feedback.browser.wjbrowser", "addressbar_url"),
|
||||
}.ToDictionary(n => n.PackageName);
|
||||
|
||||
// Known packages to skip
|
||||
@@ -82,13 +87,18 @@ namespace Bit.Droid.Accessibility
|
||||
"com.teslacoilsw.launcher.prime",
|
||||
"is.shortcut",
|
||||
"me.craftsapp.nlauncher",
|
||||
"com.ss.squarehome2"
|
||||
"com.ss.squarehome2",
|
||||
"com.treydev.pns"
|
||||
};
|
||||
|
||||
public static void PrintTestData(AccessibilityNodeInfo root, AccessibilityEvent e)
|
||||
{
|
||||
var testNodes = GetWindowNodes(root, e, n => n.ViewIdResourceName != null && n.Text != null, false);
|
||||
var testNodesData = testNodes.Select(n => new { id = n.ViewIdResourceName, text = n.Text });
|
||||
foreach(var node in testNodesData)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("Node: {0} = {1}", node.id, node.text);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetUri(AccessibilityNodeInfo root)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<MonoAndroidResourcePrefix>Resources</MonoAndroidResourcePrefix>
|
||||
<MonoAndroidAssetsPrefix>Assets</MonoAndroidAssetsPrefix>
|
||||
<AndroidUseLatestPlatformSdk>false</AndroidUseLatestPlatformSdk>
|
||||
<TargetFrameworkVersion>v9.0</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v10.0</TargetFrameworkVersion>
|
||||
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
@@ -82,19 +82,19 @@
|
||||
<Version>2.1.0.4</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Portable.BouncyCastle">
|
||||
<Version>1.8.5</Version>
|
||||
<Version>1.8.5.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Essentials">
|
||||
<Version>1.1.0</Version>
|
||||
<Version>1.3.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Firebase.Messaging">
|
||||
<Version>60.1142.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xamarin.Android.Support.Design" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.AppCompat" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v4" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.CardView" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.MediaRouter" Version="28.0.0.1" />
|
||||
<PackageReference Include="Xamarin.Android.Support.Design" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.AppCompat" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v4" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.CardView" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.Android.Support.v7.MediaRouter" Version="28.0.0.3" />
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet">
|
||||
<Version>60.1142.1</Version>
|
||||
</PackageReference>
|
||||
@@ -116,7 +116,6 @@
|
||||
<Compile Include="Effects\FixedSizeEffect.cs" />
|
||||
<Compile Include="Effects\SelectableLabelEffect.cs" />
|
||||
<Compile Include="Effects\TabBarEffect.cs" />
|
||||
<Compile Include="Migration\AndroidKeyStoreStorageService.cs" />
|
||||
<Compile Include="Push\FirebaseInstanceIdService.cs" />
|
||||
<Compile Include="Push\FirebaseMessagingService.cs" />
|
||||
<Compile Include="Receivers\ClearClipboardAlarmReceiver.cs" />
|
||||
|
||||
@@ -60,12 +60,15 @@ namespace Bit.Droid.Autofill
|
||||
"org.mozilla.fenix.nightly",
|
||||
"org.mozilla.reference.browser",
|
||||
"org.mozilla.rocket",
|
||||
"org.torproject.torbrowser",
|
||||
"com.vivaldi.browser",
|
||||
};
|
||||
|
||||
// The URLs are blacklisted from autofilling
|
||||
public static HashSet<string> BlacklistedUris = new HashSet<string>
|
||||
{
|
||||
"androidapp://android",
|
||||
"androidapp://com.android.settings",
|
||||
"androidapp://com.x8bit.bitwarden",
|
||||
"androidapp://com.oneplus.applocker",
|
||||
};
|
||||
|
||||
@@ -40,12 +40,8 @@ namespace Bit.Droid
|
||||
if(ServiceContainer.RegisteredServices.Count == 0)
|
||||
{
|
||||
RegisterLocalServices();
|
||||
ServiceContainer.Init();
|
||||
if(App.Migration.MigrationHelpers.NeedsMigration())
|
||||
{
|
||||
var task = App.Migration.MigrationHelpers.PerformMigrationAsync();
|
||||
Task.Delay(2000).Wait();
|
||||
}
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
ServiceContainer.Init(deviceActionService.DeviceUserAgent);
|
||||
}
|
||||
#if !FDROID
|
||||
if(Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||
@@ -73,12 +69,6 @@ namespace Bit.Droid
|
||||
private void RegisterLocalServices()
|
||||
{
|
||||
ServiceContainer.Register<ILogService>("logService", new AndroidLogService());
|
||||
ServiceContainer.Register("settingsShim", new App.Migration.SettingsShim());
|
||||
if(App.Migration.MigrationHelpers.NeedsMigration())
|
||||
{
|
||||
ServiceContainer.Register<App.Migration.Abstractions.IOldSecureStorageService>(
|
||||
"oldSecureStorageService", new Migration.AndroidKeyStoreStorageService());
|
||||
}
|
||||
|
||||
Refractored.FabControl.Droid.FloatingActionButtonViewRenderer.Init();
|
||||
// Note: This might cause a race condition. Investigate more.
|
||||
@@ -155,4 +145,4 @@ namespace Bit.Droid
|
||||
await ServiceContainer.Resolve<IEnvironmentService>("environmentService").SetUrlsFromStorageAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
using Android.OS;
|
||||
using Bit.App.Abstractions;
|
||||
using System;
|
||||
using Android.Security;
|
||||
using Javax.Security.Auth.X500;
|
||||
using Java.Math;
|
||||
using Android.Security.Keystore;
|
||||
using Android.App;
|
||||
using Java.Util;
|
||||
using Javax.Crypto.Spec;
|
||||
using Android.Preferences;
|
||||
using Bit.App.Migration;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.App.Migration.Abstractions;
|
||||
|
||||
namespace Bit.Droid.Migration
|
||||
{
|
||||
public class AndroidKeyStoreStorageService : IOldSecureStorageService
|
||||
{
|
||||
private const string AndroidKeyStore = "AndroidKeyStore";
|
||||
private const string AesMode = "AES/GCM/NoPadding";
|
||||
|
||||
private const string KeyAlias = "bitwardenKey2";
|
||||
private const string KeyAliasV1 = "bitwardenKey";
|
||||
|
||||
private const string SettingsFormat = "ksSecured2:{0}";
|
||||
private const string SettingsFormatV1 = "ksSecured:{0}";
|
||||
|
||||
private const string AesKey = "ksSecured2:aesKeyForService";
|
||||
private const string AesKeyV1 = "ksSecured:aesKeyForService";
|
||||
|
||||
private readonly string _rsaMode;
|
||||
private readonly bool _oldAndroid;
|
||||
private readonly SettingsShim _settings;
|
||||
private readonly KeyStore _keyStore;
|
||||
|
||||
public AndroidKeyStoreStorageService()
|
||||
{
|
||||
_oldAndroid = Build.VERSION.SdkInt < BuildVersionCodes.M;
|
||||
_rsaMode = _oldAndroid ? "RSA/ECB/PKCS1Padding" : "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
_settings = ServiceContainer.Resolve<SettingsShim>("settingsShim");
|
||||
|
||||
_keyStore = KeyStore.GetInstance(AndroidKeyStore);
|
||||
_keyStore.Load(null);
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
GenerateStoreKey(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
GenerateStoreKey(false);
|
||||
}
|
||||
|
||||
GenerateAesKey();
|
||||
*/
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return _settings.Contains(string.Format(SettingsFormat, key)) ||
|
||||
_settings.Contains(string.Format(SettingsFormatV1, key));
|
||||
}
|
||||
|
||||
public void Delete(string key)
|
||||
{
|
||||
CleanupOld(key);
|
||||
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(_settings.Contains(formattedKey))
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Retrieve(string key)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
if(!_settings.Contains(formattedKey))
|
||||
{
|
||||
return TryGetAndMigrate(key);
|
||||
}
|
||||
|
||||
var cs = _settings.GetValueOrDefault(formattedKey, null);
|
||||
if(string.IsNullOrWhiteSpace(cs))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return App.Migration.Crypto.AesCbcDecrypt(new App.Migration.Models.CipherString(cs), aesKey);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt from secure storage.");
|
||||
_settings.Remove(formattedKey);
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Store(string key, byte[] dataBytes)
|
||||
{
|
||||
var formattedKey = string.Format(SettingsFormat, key);
|
||||
CleanupOld(key);
|
||||
if(dataBytes == null)
|
||||
{
|
||||
_settings.Remove(formattedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
var aesKey = GetAesKey();
|
||||
if(aesKey == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var cipherString = App.Migration.Crypto.AesCbcEncrypt(dataBytes, aesKey);
|
||||
_settings.AddOrUpdateValue(formattedKey, cipherString.EncryptedString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to encrypt to secure storage.");
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateStoreKey(bool withDate)
|
||||
{
|
||||
if(_keyStore.ContainsAlias(KeyAlias))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearSettings();
|
||||
|
||||
var end = Calendar.Instance;
|
||||
end.Add(CalendarField.Year, 99);
|
||||
|
||||
if(_oldAndroid)
|
||||
{
|
||||
var subject = new X500Principal($"CN={KeyAlias}");
|
||||
|
||||
var builder = new KeyPairGeneratorSpec.Builder(Application.Context)
|
||||
.SetAlias(KeyAlias)
|
||||
.SetSubject(subject)
|
||||
.SetSerialNumber(BigInteger.Ten);
|
||||
|
||||
if(withDate)
|
||||
{
|
||||
builder.SetStartDate(new Date(0)).SetEndDate(end.Time);
|
||||
}
|
||||
|
||||
var spec = builder.Build();
|
||||
var gen = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, AndroidKeyStore);
|
||||
gen.Initialize(spec);
|
||||
gen.GenerateKeyPair();
|
||||
}
|
||||
else
|
||||
{
|
||||
var builder = new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Decrypt | KeyStorePurpose.Encrypt)
|
||||
.SetBlockModes(KeyProperties.BlockModeGcm)
|
||||
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone);
|
||||
|
||||
if(withDate)
|
||||
{
|
||||
builder.SetKeyValidityStart(new Date(0)).SetKeyValidityEnd(end.Time);
|
||||
}
|
||||
|
||||
var spec = builder.Build();
|
||||
var gen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, AndroidKeyStore);
|
||||
gen.Init(spec);
|
||||
gen.GenerateKey();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyStore.PrivateKeyEntry GetRsaKeyEntry(string alias)
|
||||
{
|
||||
return _keyStore.GetEntry(alias, null) as KeyStore.PrivateKeyEntry;
|
||||
}
|
||||
|
||||
private void GenerateAesKey()
|
||||
{
|
||||
if(_settings.Contains(AesKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = App.Migration.Crypto.RandomBytes(512 / 8);
|
||||
var encKey = _oldAndroid ? RsaEncrypt(key) : AesEncrypt(key);
|
||||
_settings.AddOrUpdateValue(AesKey, encKey);
|
||||
}
|
||||
|
||||
private App.Migration.Models.SymmetricCryptoKey GetAesKey(bool v1 = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var aesKey = v1 ? AesKeyV1 : AesKey;
|
||||
if(!_settings.Contains(aesKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var encKey = _settings.GetValueOrDefault(aesKey, null);
|
||||
if(string.IsNullOrWhiteSpace(encKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if(_oldAndroid || v1)
|
||||
{
|
||||
var encKeyBytes = Convert.FromBase64String(encKey);
|
||||
var key = RsaDecrypt(encKeyBytes, v1);
|
||||
return new App.Migration.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = encKey.Split('|');
|
||||
if(parts.Length < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ivBytes = Convert.FromBase64String(parts[0]);
|
||||
var encKeyBytes = Convert.FromBase64String(parts[1]);
|
||||
var key = AesDecrypt(ivBytes, encKeyBytes);
|
||||
return new App.Migration.Models.SymmetricCryptoKey(key);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Cannot get AesKey.");
|
||||
_keyStore.DeleteEntry(KeyAlias);
|
||||
_settings.Remove(AesKey);
|
||||
if(!v1)
|
||||
{
|
||||
//Utilities.SendCrashEmail(e);
|
||||
//Utilities.SaveCrashFile(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string AesEncrypt(byte[] input)
|
||||
{
|
||||
using(var entry = _keyStore.GetKey(KeyAlias, null))
|
||||
using(var cipher = Cipher.GetInstance(AesMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry);
|
||||
var encBytes = cipher.DoFinal(input);
|
||||
var ivBytes = cipher.GetIV();
|
||||
return $"{Convert.ToBase64String(ivBytes)}|{Convert.ToBase64String(encBytes)}";
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] AesDecrypt(byte[] iv, byte[] encData)
|
||||
{
|
||||
using(var entry = _keyStore.GetKey(KeyAlias, null))
|
||||
using(var cipher = Cipher.GetInstance(AesMode))
|
||||
{
|
||||
var spec = new GCMParameterSpec(128, iv);
|
||||
cipher.Init(CipherMode.DecryptMode, entry, spec);
|
||||
var decBytes = cipher.DoFinal(encData);
|
||||
return decBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private string RsaEncrypt(byte[] data)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry(KeyAlias))
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, entry.Certificate.PublicKey);
|
||||
var cipherText = cipher.DoFinal(data);
|
||||
return Convert.ToBase64String(cipherText);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] RsaDecrypt(byte[] encData, bool v1)
|
||||
{
|
||||
using(var entry = GetRsaKeyEntry(v1 ? KeyAliasV1 : KeyAlias))
|
||||
using(var cipher = Cipher.GetInstance(_rsaMode))
|
||||
{
|
||||
if(_oldAndroid)
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
cipher.Init(CipherMode.DecryptMode, entry.PrivateKey, OAEPParameterSpec.Default);
|
||||
}
|
||||
|
||||
var plainText = cipher.DoFinal(encData);
|
||||
return plainText;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] TryGetAndMigrate(string key)
|
||||
{
|
||||
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
|
||||
if(_settings.Contains(formattedKeyV1))
|
||||
{
|
||||
var aesKeyV1 = GetAesKey(true);
|
||||
if(aesKeyV1 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = _settings.GetValueOrDefault(formattedKeyV1, null);
|
||||
var value = App.Migration.Crypto.AesCbcDecrypt(new App.Migration.Models.CipherString(cs), aesKeyV1);
|
||||
Store(key, value);
|
||||
return value;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.WriteLine("Failed to decrypt v1 from secure storage.");
|
||||
}
|
||||
}
|
||||
|
||||
_settings.Remove(formattedKeyV1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CleanupOld(string key)
|
||||
{
|
||||
var formattedKeyV1 = string.Format(SettingsFormatV1, key);
|
||||
if(_settings.Contains(formattedKeyV1))
|
||||
{
|
||||
_settings.Remove(formattedKeyV1);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSettings(string format = SettingsFormat)
|
||||
{
|
||||
var prefix = string.Format(format, string.Empty);
|
||||
|
||||
using(var sharedPreferences = PreferenceManager.GetDefaultSharedPreferences(Application.Context))
|
||||
using(var sharedPreferencesEditor = sharedPreferences.Edit())
|
||||
{
|
||||
var removed = false;
|
||||
foreach(var pref in sharedPreferences.All)
|
||||
{
|
||||
if(pref.Key.StartsWith(prefix))
|
||||
{
|
||||
removed = true;
|
||||
sharedPreferencesEditor.Remove(pref.Key);
|
||||
}
|
||||
}
|
||||
|
||||
if(removed)
|
||||
{
|
||||
sharedPreferencesEditor.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,17 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:versionCode="1"
|
||||
android:versionName="2.2.0"
|
||||
android:versionName="2.2.8"
|
||||
package="com.x8bit.bitwarden">
|
||||
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="28" />
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="29" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
|
||||
|
||||
|
||||
13313
src/Android/Resources/Resource.designer.cs
generated
13313
src/Android/Resources/Resource.designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -90,4 +90,7 @@
|
||||
<compatibility-package
|
||||
android:name="com.ecosia.android"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
<compatibility-package
|
||||
android:name="com.vivaldi.browser"
|
||||
android:maxLongVersionCode="10000000000"/>
|
||||
</autofill-service>
|
||||
|
||||
@@ -8,9 +8,12 @@ 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.Support.V4.App;
|
||||
using Android.Support.V4.Content;
|
||||
using Android.Text;
|
||||
@@ -28,6 +31,7 @@ using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Droid.Autofill;
|
||||
using Plugin.CurrentActivity;
|
||||
using Plugin.Fingerprint;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
@@ -40,6 +44,7 @@ namespace Bit.Droid.Services
|
||||
private ProgressDialog _progressDialog;
|
||||
private bool _cameraPermissionsDenied;
|
||||
private Toast _toast;
|
||||
private string _userAgent;
|
||||
|
||||
public DeviceActionService(
|
||||
IStorageService storageService,
|
||||
@@ -61,6 +66,19 @@ namespace Bit.Droid.Services
|
||||
});
|
||||
}
|
||||
|
||||
public string DeviceUserAgent
|
||||
{
|
||||
get
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(_userAgent))
|
||||
{
|
||||
_userAgent = $"Bitwarden_Mobile/{Xamarin.Essentials.AppInfo.VersionString} " +
|
||||
$"(Android {Build.VERSION.Release}; SDK {Build.VERSION.Sdk}; Model {Build.Model})";
|
||||
}
|
||||
return _userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
public DeviceType DeviceType => DeviceType.Android;
|
||||
|
||||
public void Toast(string text, bool longDuration = false)
|
||||
@@ -114,38 +132,14 @@ namespace Bit.Droid.Services
|
||||
|
||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||
{
|
||||
if(!CanOpenFile(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var cachePath = activity.CacheDir;
|
||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var file = new Java.IO.File(cachePath, fileName);
|
||||
if(!file.IsFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
intent.SetDataAndType(uri, mimeType);
|
||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = BuildOpenFileIntent(fileData, fileName);
|
||||
if(intent == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
activity.StartActivity(intent);
|
||||
return true;
|
||||
}
|
||||
@@ -154,22 +148,57 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
|
||||
public bool CanOpenFile(string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
||||
if(intent == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
||||
PackageInfoFlags.MatchDefaultOnly);
|
||||
return (activities?.Count ?? 0) > 0;
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
||||
{
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
intent.SetType(mimeType);
|
||||
var activities = activity.PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
|
||||
return (activities?.Count ?? 0) > 0;
|
||||
var cachePath = activity.CacheDir;
|
||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var file = new Java.IO.File(cachePath, fileName);
|
||||
if(!file.IsFile)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
intent.SetDataAndType(uri, mimeType);
|
||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||
return intent;
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task ClearCacheAsync()
|
||||
@@ -185,7 +214,8 @@ namespace Bit.Droid.Services
|
||||
public Task SelectFileAsync()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var hasStorageWritePermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||
var hasStorageWritePermission = !_cameraPermissionsDenied &&
|
||||
HasPermission(Manifest.Permission.WriteExternalStorage);
|
||||
var additionalIntents = new List<IParcelable>();
|
||||
if(activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
|
||||
{
|
||||
@@ -309,11 +339,72 @@ namespace Bit.Droid.Services
|
||||
Application.Context.PackageName, 0).VersionCode.ToString();
|
||||
}
|
||||
|
||||
public bool SupportsFaceId()
|
||||
public bool SupportsFaceBiometric()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> SupportsFaceBiometricAsync()
|
||||
{
|
||||
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),
|
||||
Failed = () => result.TrySetResult(false),
|
||||
Help = (helpCode, helpString) => { }
|
||||
});
|
||||
return result.Task;
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsNfc()
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
@@ -543,7 +634,8 @@ namespace Bit.Droid.Services
|
||||
try
|
||||
{
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var afm = (AutofillManager)activity.GetSystemService(Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||
var afm = (AutofillManager)activity.GetSystemService(
|
||||
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
||||
}
|
||||
catch
|
||||
@@ -585,6 +677,11 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
}
|
||||
|
||||
public bool UsingDarkTheme()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool DeleteDir(Java.IO.File dir)
|
||||
{
|
||||
if(dir != null && dir.IsDirectory)
|
||||
@@ -683,5 +780,41 @@ namespace Bit.Droid.Services
|
||||
Context.ClipboardService) as Android.Content.ClipboardManager;
|
||||
clipboardManager.Text = text;
|
||||
}
|
||||
|
||||
private class BiometricAuthenticationCallback : BiometricPrompt.AuthenticationCallback
|
||||
{
|
||||
public Action<BiometricPrompt.AuthenticationResult> Success { 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 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Bit.App.Abstractions
|
||||
{
|
||||
public interface IDeviceActionService
|
||||
{
|
||||
string DeviceUserAgent { get; }
|
||||
DeviceType DeviceType { get; }
|
||||
void Toast(string text, bool longDuration = false);
|
||||
bool LaunchApp(string appName);
|
||||
@@ -19,7 +20,11 @@ namespace Bit.App.Abstractions
|
||||
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||
bool autofocus = true);
|
||||
void RateApp();
|
||||
bool SupportsFaceId();
|
||||
bool SupportsFaceBiometric();
|
||||
Task<bool> SupportsFaceBiometricAsync();
|
||||
Task<bool> BiometricAvailableAsync();
|
||||
bool UseNativeBiometric();
|
||||
Task<bool> AuthenticateBiometricAsync(string text = null);
|
||||
bool SupportsNfc();
|
||||
bool SupportsCamera();
|
||||
bool SupportsAutofillService();
|
||||
@@ -34,5 +39,6 @@ namespace Bit.App.Abstractions
|
||||
string GetBuildNumber();
|
||||
void OpenAccessibilitySettings();
|
||||
void OpenAutofillSettings();
|
||||
bool UsingDarkTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<PackageReference Include="HockeySDK.Xamarin" Version="5.2.0" />
|
||||
<PackageReference Include="Plugin.Fingerprint" Version="1.4.9" />
|
||||
<PackageReference Include="Refractored.FloatingActionButtonForms" Version="2.1.0" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.1.0" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.3.1" />
|
||||
<PackageReference Include="Xamarin.FFImageLoading.Forms" Version="2.4.11.982" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="3.6.0.344457" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="4.4.0.991265" />
|
||||
<PackageReference Include="ZXing.Net.Mobile.Forms" Version="2.1.47" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -97,11 +97,8 @@ namespace Bit.App
|
||||
}
|
||||
else if(message.Command == "logout")
|
||||
{
|
||||
if(Migration.MigrationHelpers.Migrating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Device.BeginInvokeOnMainThread(async () => await LogOutAsync(false));
|
||||
Device.BeginInvokeOnMainThread(async () =>
|
||||
await LogOutAsync((message.Data as bool?).GetValueOrDefault()));
|
||||
}
|
||||
else if(message.Command == "loggedOut")
|
||||
{
|
||||
@@ -212,6 +209,13 @@ namespace Bit.App
|
||||
SyncIfNeeded();
|
||||
if(Current.MainPage is NavigationPage navPage && navPage.CurrentPage is LockPage lockPage)
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.Android)
|
||||
{
|
||||
// Workaround for https://github.com/xamarin/Xamarin.Forms/issues/7478
|
||||
await Task.Delay(100);
|
||||
Current.MainPage = new NavigationPage(lockPage);
|
||||
// End workaround
|
||||
}
|
||||
await lockPage.PromptFingerprintAfterResumeAsync();
|
||||
}
|
||||
}
|
||||
@@ -239,12 +243,15 @@ namespace Bit.App
|
||||
_passwordGenerationService.ClearAsync(),
|
||||
_lockService.ClearAsync(),
|
||||
_stateService.PurgeAsync());
|
||||
_lockService.PinLocked = false;
|
||||
_lockService.FingerprintLocked = true;
|
||||
_searchService.ClearIndex();
|
||||
_authService.LogOut(() =>
|
||||
{
|
||||
Current.MainPage = new HomePage();
|
||||
if(expired)
|
||||
{
|
||||
_platformUtilsService.ShowToast("warning", null, AppResources.LoginExpired);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -356,10 +363,6 @@ namespace Bit.App
|
||||
|
||||
private void SyncIfNeeded()
|
||||
{
|
||||
if(Migration.MigrationHelpers.Migrating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
return;
|
||||
@@ -367,8 +370,9 @@ namespace Bit.App
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var lastSync = await _syncService.GetLastSyncAsync();
|
||||
if(DateTime.UtcNow - lastSync > TimeSpan.FromMinutes(30))
|
||||
if(lastSync == null || ((DateTime.UtcNow - lastSync) > TimeSpan.FromMinutes(30)))
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
await _syncService.FullSyncAsync(false);
|
||||
}
|
||||
});
|
||||
@@ -399,6 +403,11 @@ namespace Bit.App
|
||||
autoPromptFingerprint = false;
|
||||
}
|
||||
}
|
||||
else if(autoPromptFingerprint && Device.RuntimePlatform == Device.Android &&
|
||||
_deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
autoPromptFingerprint = false;
|
||||
}
|
||||
PreviousPageInfo lastPageBeforeLock = null;
|
||||
if(Current.MainPage is TabbedPage tabbedPage && tabbedPage.Navigation.ModalStack.Count > 0)
|
||||
{
|
||||
|
||||
21
src/App/Controls/ExtendedSearchBar.cs
Normal file
21
src/App/Controls/ExtendedSearchBar.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Controls
|
||||
{
|
||||
public class ExtendedSearchBar : SearchBar
|
||||
{
|
||||
public ExtendedSearchBar()
|
||||
{
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService", true);
|
||||
if(!deviceActionService?.UsingDarkTheme() ?? false)
|
||||
{
|
||||
TextColor = Color.Black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Bit.App.Migration.Abstractions
|
||||
{
|
||||
public interface IOldSecureStorageService
|
||||
{
|
||||
bool Contains(string key);
|
||||
void Delete(string key);
|
||||
byte[] Retrieve(string key);
|
||||
void Store(string key, byte[] dataBytes);
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
using Bit.App.Migration.Models;
|
||||
using Bit.Core.Enums;
|
||||
using PCLCrypto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public static class Crypto
|
||||
{
|
||||
public static CipherString AesCbcEncrypt(byte[] plainBytes, SymmetricCryptoKey key)
|
||||
{
|
||||
var parts = AesCbcEncryptToParts(plainBytes, key);
|
||||
return new CipherString(parts.Item1, Convert.ToBase64String(parts.Item2),
|
||||
Convert.ToBase64String(parts.Item4), parts.Item3 != null ? Convert.ToBase64String(parts.Item3) : null);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcEncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key)
|
||||
{
|
||||
var parts = AesCbcEncryptToParts(plainBytes, key);
|
||||
var macLength = parts.Item3?.Length ?? 0;
|
||||
|
||||
var encBytes = new byte[1 + parts.Item2.Length + macLength + parts.Item4.Length];
|
||||
encBytes[0] = (byte)parts.Item1;
|
||||
parts.Item2.CopyTo(encBytes, 1);
|
||||
if(parts.Item3 != null)
|
||||
{
|
||||
parts.Item3.CopyTo(encBytes, 1 + parts.Item2.Length);
|
||||
}
|
||||
parts.Item4.CopyTo(encBytes, 1 + parts.Item2.Length + macLength);
|
||||
return encBytes;
|
||||
}
|
||||
|
||||
private static Tuple<EncryptionType, byte[], byte[], byte[]> AesCbcEncryptToParts(byte[] plainBytes,
|
||||
SymmetricCryptoKey key)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(plainBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(plainBytes));
|
||||
}
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var iv = RandomBytes(provider.BlockLength);
|
||||
var ct = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plainBytes, iv);
|
||||
var mac = key.MacKey != null ? ComputeMac(ct, iv, key.MacKey) : null;
|
||||
|
||||
return new Tuple<EncryptionType, byte[], byte[], byte[]>(key.EncryptionType, iv, mac, ct);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcDecrypt(CipherString encyptedValue, SymmetricCryptoKey key)
|
||||
{
|
||||
if(encyptedValue == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encyptedValue));
|
||||
}
|
||||
|
||||
return AesCbcDecrypt(encyptedValue.EncryptionType, encyptedValue.CipherTextBytes,
|
||||
encyptedValue.InitializationVectorBytes, encyptedValue.MacBytes, key);
|
||||
}
|
||||
|
||||
public static byte[] AesCbcDecrypt(EncryptionType type, byte[] ct, byte[] iv, byte[] mac,
|
||||
SymmetricCryptoKey key)
|
||||
{
|
||||
if(key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if(ct == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ct));
|
||||
}
|
||||
|
||||
if(iv == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(iv));
|
||||
}
|
||||
|
||||
if(key.MacKey != null && mac == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mac));
|
||||
}
|
||||
|
||||
if(key.EncryptionType != type)
|
||||
{
|
||||
throw new InvalidOperationException(nameof(type));
|
||||
}
|
||||
|
||||
if(key.MacKey != null && mac != null)
|
||||
{
|
||||
var computedMacBytes = ComputeMac(ct, iv, key.MacKey);
|
||||
if(!MacsEqual(computedMacBytes, mac))
|
||||
{
|
||||
throw new InvalidOperationException("MAC failed.");
|
||||
}
|
||||
}
|
||||
|
||||
var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7);
|
||||
var cryptoKey = provider.CreateSymmetricKey(key.EncKey);
|
||||
var decryptedBytes = WinRTCrypto.CryptographicEngine.Decrypt(cryptoKey, ct, iv);
|
||||
return decryptedBytes;
|
||||
}
|
||||
|
||||
public static byte[] RandomBytes(int length)
|
||||
{
|
||||
return WinRTCrypto.CryptographicBuffer.GenerateRandom(length);
|
||||
}
|
||||
|
||||
public static byte[] ComputeMac(byte[] ctBytes, byte[] ivBytes, byte[] macKey)
|
||||
{
|
||||
if(ctBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ctBytes));
|
||||
}
|
||||
|
||||
if(ivBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(ivBytes));
|
||||
}
|
||||
|
||||
return ComputeMac(ivBytes.Concat(ctBytes), macKey);
|
||||
}
|
||||
|
||||
public static byte[] ComputeMac(IEnumerable<byte> dataBytes, byte[] macKey)
|
||||
{
|
||||
if(macKey == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(macKey));
|
||||
}
|
||||
|
||||
if(dataBytes == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataBytes));
|
||||
}
|
||||
|
||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256);
|
||||
var hasher = algorithm.CreateHash(macKey);
|
||||
hasher.Append(dataBytes.ToArray());
|
||||
var mac = hasher.GetValueAndReset();
|
||||
return mac;
|
||||
}
|
||||
|
||||
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).
|
||||
// ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/
|
||||
// ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy
|
||||
public static bool MacsEqual(byte[] mac1, byte[] mac2)
|
||||
{
|
||||
var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256);
|
||||
var hasher = algorithm.CreateHash(RandomBytes(32));
|
||||
|
||||
hasher.Append(mac1);
|
||||
mac1 = hasher.GetValueAndReset();
|
||||
|
||||
hasher.Append(mac2);
|
||||
mac2 = hasher.GetValueAndReset();
|
||||
|
||||
if(mac1.Length != mac2.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for(int i = 0; i < mac2.Length; i++)
|
||||
{
|
||||
if(mac1[i] != mac2[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
public static byte[] HkdfExpand(byte[] prk, byte[] info, int size)
|
||||
{
|
||||
var hashLen = 32; // sha256
|
||||
var okm = new byte[size];
|
||||
var previousT = new byte[0];
|
||||
var n = (int)Math.Ceiling((double)size / hashLen);
|
||||
for(int i = 0; i < n; i++)
|
||||
{
|
||||
var t = new byte[previousT.Length + info.Length + 1];
|
||||
previousT.CopyTo(t, 0);
|
||||
info.CopyTo(t, previousT.Length);
|
||||
t[t.Length - 1] = (byte)(i + 1);
|
||||
previousT = ComputeMac(t, prk);
|
||||
previousT.CopyTo(okm, i * hashLen);
|
||||
}
|
||||
return okm;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public static class MigrationHelpers
|
||||
{
|
||||
public static bool Migrating = false;
|
||||
|
||||
public static bool NeedsMigration()
|
||||
{
|
||||
return ServiceContainer.Resolve<SettingsShim>("settingsShim")
|
||||
.GetValueOrDefault(Constants.OldUserIdKey, null) != null; ;
|
||||
}
|
||||
|
||||
public static async Task<bool> PerformMigrationAsync()
|
||||
{
|
||||
if(!NeedsMigration() || Migrating)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Migrating = true;
|
||||
var settingsShim = ServiceContainer.Resolve<SettingsShim>("settingsShim");
|
||||
var oldSecureStorageService = ServiceContainer.Resolve<Abstractions.IOldSecureStorageService>(
|
||||
"oldSecureStorageService");
|
||||
|
||||
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
var cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
var tokenService = ServiceContainer.Resolve<ITokenService>("tokenService");
|
||||
var userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
var environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
var passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
|
||||
"passwordGenerationService");
|
||||
var syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
var lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
||||
|
||||
// Get old data
|
||||
|
||||
var oldTokenBytes = oldSecureStorageService.Retrieve("accessToken");
|
||||
var oldToken = oldTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldTokenBytes, 0, oldTokenBytes.Length);
|
||||
var oldKeyBytes = oldSecureStorageService.Retrieve("key");
|
||||
var oldKey = oldKeyBytes == null ? null : new Models.SymmetricCryptoKey(oldKeyBytes);
|
||||
var oldUserId = settingsShim.GetValueOrDefault("userId", null);
|
||||
|
||||
var isAuthenticated = oldKey != null && !string.IsNullOrWhiteSpace(oldToken) &&
|
||||
!string.IsNullOrWhiteSpace(oldUserId);
|
||||
if(!isAuthenticated)
|
||||
{
|
||||
Migrating = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var oldRefreshTokenBytes = oldSecureStorageService.Retrieve("refreshToken");
|
||||
var oldRefreshToken = oldRefreshTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldRefreshTokenBytes, 0, oldRefreshTokenBytes.Length);
|
||||
var oldPinBytes = oldSecureStorageService.Retrieve("pin");
|
||||
var oldPin = oldPinBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldPinBytes, 0, oldPinBytes.Length);
|
||||
|
||||
var oldEncKey = settingsShim.GetValueOrDefault("encKey", null);
|
||||
var oldEncPrivateKey = settingsShim.GetValueOrDefault("encPrivateKey", null);
|
||||
var oldEmail = settingsShim.GetValueOrDefault("email", null);
|
||||
var oldKdf = (KdfType)settingsShim.GetValueOrDefault("kdf", (int)KdfType.PBKDF2_SHA256);
|
||||
var oldKdfIterations = settingsShim.GetValueOrDefault("kdfIterations", 5000);
|
||||
|
||||
var oldTwoFactorTokenBytes = oldSecureStorageService.Retrieve(
|
||||
string.Format("twoFactorToken_{0}", Convert.ToBase64String(Encoding.UTF8.GetBytes(oldEmail))));
|
||||
var oldTwoFactorToken = oldTwoFactorTokenBytes == null ? null : Encoding.UTF8.GetString(
|
||||
oldTwoFactorTokenBytes, 0, oldTwoFactorTokenBytes.Length);
|
||||
|
||||
var oldAppIdBytes = oldSecureStorageService.Retrieve("appId");
|
||||
var oldAppId = oldAppIdBytes == null ? null : new Guid(oldAppIdBytes).ToString();
|
||||
var oldAnonAppIdBytes = oldSecureStorageService.Retrieve("anonymousAppId");
|
||||
var oldAnonAppId = oldAnonAppIdBytes == null ? null : new Guid(oldAnonAppIdBytes).ToString();
|
||||
var oldFingerprint = settingsShim.GetValueOrDefault("setting:fingerprintUnlockOn", false);
|
||||
|
||||
// Save settings
|
||||
|
||||
await storageService.SaveAsync(Constants.AccessibilityAutofillPersistNotificationKey,
|
||||
settingsShim.GetValueOrDefault("setting:persistNotification", false));
|
||||
await storageService.SaveAsync(Constants.AccessibilityAutofillPasswordFieldKey,
|
||||
settingsShim.GetValueOrDefault("setting:autofillPasswordField", false));
|
||||
await storageService.SaveAsync(Constants.DisableAutoTotpCopyKey,
|
||||
settingsShim.GetValueOrDefault("setting:disableAutoCopyTotp", false));
|
||||
await storageService.SaveAsync(Constants.DisableFaviconKey,
|
||||
settingsShim.GetValueOrDefault("setting:disableWebsiteIcons", false));
|
||||
await storageService.SaveAsync(Constants.AddSitePromptShownKey,
|
||||
settingsShim.GetValueOrDefault("addedSiteAlert", false));
|
||||
await storageService.SaveAsync(Constants.PushInitialPromptShownKey,
|
||||
settingsShim.GetValueOrDefault("push:initialPromptShown", false));
|
||||
await storageService.SaveAsync(Constants.PushCurrentTokenKey,
|
||||
settingsShim.GetValueOrDefault("push:currentToken", null));
|
||||
await storageService.SaveAsync(Constants.PushRegisteredTokenKey,
|
||||
settingsShim.GetValueOrDefault("push:registeredToken", null));
|
||||
// For some reason "push:lastRegistrationDate" isn't getting pulled from settingsShim correctly.
|
||||
// We don't really need it anyways.
|
||||
// var lastReg = settingsShim.GetValueOrDefault("push:lastRegistrationDate", DateTime.MinValue);
|
||||
// await storageService.SaveAsync(Constants.PushLastRegistrationDateKey, lastReg);
|
||||
await storageService.SaveAsync("rememberedEmail",
|
||||
settingsShim.GetValueOrDefault("other:lastLoginEmail", null));
|
||||
await storageService.SaveAsync("appExtensionStarted",
|
||||
settingsShim.GetValueOrDefault("extension:started", false));
|
||||
await storageService.SaveAsync("appExtensionActivated",
|
||||
settingsShim.GetValueOrDefault("extension:activated", false));
|
||||
|
||||
await environmentService.SetUrlsAsync(new Core.Models.Data.EnvironmentUrlData
|
||||
{
|
||||
Base = settingsShim.GetValueOrDefault("other:baseUrl", null),
|
||||
Api = settingsShim.GetValueOrDefault("other:apiUrl", null),
|
||||
WebVault = settingsShim.GetValueOrDefault("other:webVaultUrl", null),
|
||||
Identity = settingsShim.GetValueOrDefault("other:identityUrl", null),
|
||||
Icons = settingsShim.GetValueOrDefault("other:iconsUrl", null)
|
||||
});
|
||||
|
||||
await passwordGenerationService.SaveOptionsAsync(new Core.Models.Domain.PasswordGenerationOptions
|
||||
{
|
||||
Ambiguous = settingsShim.GetValueOrDefault("pwGenerator:ambiguous", false),
|
||||
Length = settingsShim.GetValueOrDefault("pwGenerator:length", 15),
|
||||
Uppercase = settingsShim.GetValueOrDefault("pwGenerator:uppercase", true),
|
||||
Lowercase = settingsShim.GetValueOrDefault("pwGenerator:lowercase", true),
|
||||
Number = settingsShim.GetValueOrDefault("pwGenerator:numbers", true),
|
||||
MinNumber = settingsShim.GetValueOrDefault("pwGenerator:minNumbers", 0),
|
||||
Special = settingsShim.GetValueOrDefault("pwGenerator:special", true),
|
||||
MinSpecial = settingsShim.GetValueOrDefault("pwGenerator:minSpecial", 0),
|
||||
WordSeparator = "-",
|
||||
NumWords = 3
|
||||
});
|
||||
|
||||
// Save lock options
|
||||
|
||||
int? lockOptionsSeconds = settingsShim.GetValueOrDefault("setting:lockSeconds", -10);
|
||||
if(lockOptionsSeconds == -10)
|
||||
{
|
||||
lockOptionsSeconds = 60 * 15;
|
||||
}
|
||||
else if(lockOptionsSeconds == -1)
|
||||
{
|
||||
lockOptionsSeconds = null;
|
||||
}
|
||||
await storageService.SaveAsync(Constants.LockOptionKey,
|
||||
lockOptionsSeconds == null ? (int?)null : lockOptionsSeconds.Value / 60);
|
||||
|
||||
// Save app ids
|
||||
|
||||
await storageService.SaveAsync("appId", oldAppId);
|
||||
await storageService.SaveAsync("anonymousAppId", oldAnonAppId);
|
||||
|
||||
// Save new authed data
|
||||
|
||||
await tokenService.SetTwoFactorTokenAsync(oldTwoFactorToken, oldEmail);
|
||||
await tokenService.SetTokensAsync(oldToken, oldRefreshToken);
|
||||
await userService.SetInformationAsync(oldUserId, oldEmail, oldKdf, oldKdfIterations);
|
||||
|
||||
var newKey = new Core.Models.Domain.SymmetricCryptoKey(oldKey.Key);
|
||||
await cryptoService.SetKeyAsync(newKey);
|
||||
// Key hash is unavailable in old version, store old key until we can move it to key hash
|
||||
await secureStorageService.SaveAsync("oldKey", newKey.KeyB64);
|
||||
await cryptoService.SetEncKeyAsync(oldEncKey);
|
||||
await cryptoService.SetEncPrivateKeyAsync(oldEncPrivateKey);
|
||||
|
||||
// Save fingerprint/pin
|
||||
|
||||
if(oldFingerprint)
|
||||
{
|
||||
await storageService.SaveAsync(Constants.FingerprintUnlockKey, true);
|
||||
}
|
||||
else if(!string.IsNullOrWhiteSpace(oldPin))
|
||||
{
|
||||
var pinKey = await cryptoService.MakePinKeyAysnc(oldPin, oldEmail, oldKdf, oldKdfIterations);
|
||||
var pinProtectedKey = await cryptoService.EncryptAsync(oldKeyBytes, pinKey);
|
||||
await storageService.SaveAsync(Constants.PinProtectedKey, pinProtectedKey.EncryptedString);
|
||||
}
|
||||
|
||||
// Post migration tasks
|
||||
await cryptoService.ToggleKeyAsync();
|
||||
await storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow.AddYears(-1));
|
||||
await lockService.CheckLockAsync();
|
||||
|
||||
// Remove "needs migration" flag
|
||||
settingsShim.Remove(Constants.OldUserIdKey);
|
||||
await storageService.SaveAsync(Constants.MigratedFromV1, true);
|
||||
Migrating = false;
|
||||
messagingService.Send("migrated");
|
||||
if(Xamarin.Essentials.Connectivity.NetworkAccess != Xamarin.Essentials.NetworkAccess.None)
|
||||
{
|
||||
var task = Task.Run(() => syncService.FullSyncAsync(true));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Migration.Models
|
||||
{
|
||||
public class CipherString
|
||||
{
|
||||
private string _decryptedValue;
|
||||
|
||||
public CipherString(string encryptedString)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(encryptedString))
|
||||
{
|
||||
throw new ArgumentException(nameof(encryptedString));
|
||||
}
|
||||
|
||||
var headerPieces = encryptedString.Split('.');
|
||||
string[] encPieces;
|
||||
|
||||
EncryptionType encType;
|
||||
if(headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out encType))
|
||||
{
|
||||
EncryptionType = encType;
|
||||
encPieces = headerPieces[1].Split('|');
|
||||
}
|
||||
else if(headerPieces.Length == 1)
|
||||
{
|
||||
encPieces = headerPieces[0].Split('|');
|
||||
EncryptionType = encPieces.Length == 3 ? EncryptionType.AesCbc128_HmacSha256_B64 :
|
||||
EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Malformed header.");
|
||||
}
|
||||
|
||||
switch(EncryptionType)
|
||||
{
|
||||
case EncryptionType.AesCbc256_B64:
|
||||
if(encPieces.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
InitializationVector = encPieces[0];
|
||||
CipherText = encPieces[1];
|
||||
break;
|
||||
case EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if(encPieces.Length != 3)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
InitializationVector = encPieces[0];
|
||||
CipherText = encPieces[1];
|
||||
Mac = encPieces[2];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
if(encPieces.Length != 1)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
break;
|
||||
case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
if(encPieces.Length != 2)
|
||||
{
|
||||
throw new ArgumentException("Malformed encPieces.");
|
||||
}
|
||||
CipherText = encPieces[0];
|
||||
Mac = encPieces[1];
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown encType.");
|
||||
}
|
||||
|
||||
EncryptedString = encryptedString;
|
||||
}
|
||||
|
||||
public CipherString(EncryptionType encryptionType, string initializationVector, string cipherText,
|
||||
string mac = null)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(initializationVector))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(initializationVector));
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(cipherText))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cipherText));
|
||||
}
|
||||
|
||||
EncryptionType = encryptionType;
|
||||
EncryptedString = string.Format("{0}.{1}|{2}", (byte)EncryptionType, initializationVector, cipherText);
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(mac))
|
||||
{
|
||||
EncryptedString = string.Format("{0}|{1}", EncryptedString, mac);
|
||||
}
|
||||
|
||||
CipherText = cipherText;
|
||||
InitializationVector = initializationVector;
|
||||
Mac = mac;
|
||||
}
|
||||
|
||||
public EncryptionType EncryptionType { get; private set; }
|
||||
public string EncryptedString { get; private set; }
|
||||
public string InitializationVector { get; private set; }
|
||||
public string CipherText { get; private set; }
|
||||
public string Mac { get; private set; }
|
||||
public byte[] InitializationVectorBytes => string.IsNullOrWhiteSpace(InitializationVector) ?
|
||||
null : Convert.FromBase64String(InitializationVector);
|
||||
public byte[] CipherTextBytes => Convert.FromBase64String(CipherText);
|
||||
public byte[] MacBytes => Mac == null ? null : Convert.FromBase64String(Mac);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.App.Migration.Models
|
||||
{
|
||||
public class SymmetricCryptoKey
|
||||
{
|
||||
public SymmetricCryptoKey(byte[] rawBytes, EncryptionType? encType = null)
|
||||
{
|
||||
if(rawBytes == null || rawBytes.Length == 0)
|
||||
{
|
||||
throw new Exception("Must provide keyBytes.");
|
||||
}
|
||||
|
||||
if(encType == null)
|
||||
{
|
||||
if(rawBytes.Length == 32)
|
||||
{
|
||||
encType = EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
else if(rawBytes.Length == 64)
|
||||
{
|
||||
encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unable to determine encType.");
|
||||
}
|
||||
}
|
||||
|
||||
EncryptionType = encType.Value;
|
||||
Key = rawBytes;
|
||||
|
||||
if(EncryptionType == EncryptionType.AesCbc256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key;
|
||||
MacKey = null;
|
||||
}
|
||||
else if(EncryptionType == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32)
|
||||
{
|
||||
EncKey = Key.Take(16).ToArray();
|
||||
MacKey = Key.Skip(16).Take(16).ToArray();
|
||||
}
|
||||
else if(EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64)
|
||||
{
|
||||
EncKey = Key.Take(32).ToArray();
|
||||
MacKey = Key.Skip(32).Take(32).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unsupported encType/key length.");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Key { get; set; }
|
||||
public string B64Key => Convert.ToBase64String(Key);
|
||||
public byte[] EncKey { get; set; }
|
||||
public byte[] MacKey { get; set; }
|
||||
public EncryptionType EncryptionType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.App.Migration
|
||||
{
|
||||
public class SettingsShim
|
||||
{
|
||||
private readonly string _sharedName;
|
||||
|
||||
public SettingsShim(string sharedName = null)
|
||||
{
|
||||
_sharedName = sharedName;
|
||||
}
|
||||
|
||||
public bool Contains(string key)
|
||||
{
|
||||
return _sharedName != null ? Xamarin.Essentials.Preferences.ContainsKey(key, _sharedName) :
|
||||
Xamarin.Essentials.Preferences.ContainsKey(key);
|
||||
}
|
||||
|
||||
public string GetValueOrDefault(string key, string defaultValue)
|
||||
{
|
||||
return _sharedName != null ? Xamarin.Essentials.Preferences.Get(key, defaultValue, _sharedName) :
|
||||
Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public DateTime GetValueOrDefault(string key, DateTime defaultValue)
|
||||
{
|
||||
return _sharedName != null ? Xamarin.Essentials.Preferences.Get(key, defaultValue, _sharedName) :
|
||||
Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public bool GetValueOrDefault(string key, bool defaultValue)
|
||||
{
|
||||
return _sharedName != null ? Xamarin.Essentials.Preferences.Get(key, defaultValue, _sharedName) :
|
||||
Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public int GetValueOrDefault(string key, int defaultValue)
|
||||
{
|
||||
return _sharedName != null ? Xamarin.Essentials.Preferences.Get(key, defaultValue, _sharedName) :
|
||||
Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public long GetValueOrDefault(string key, long defaultValue)
|
||||
{
|
||||
return _sharedName != null ? Xamarin.Essentials.Preferences.Get(key, defaultValue, _sharedName) :
|
||||
Xamarin.Essentials.Preferences.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, string value)
|
||||
{
|
||||
if(_sharedName != null)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value, _sharedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, DateTime value)
|
||||
{
|
||||
if(_sharedName != null)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value, _sharedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, bool value)
|
||||
{
|
||||
if(_sharedName != null)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value, _sharedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, long value)
|
||||
{
|
||||
if(_sharedName != null)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value, _sharedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddOrUpdateValue(string key, int value)
|
||||
{
|
||||
if(_sharedName != null)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value, _sharedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
if(_sharedName != null)
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Remove(key, _sharedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Xamarin.Essentials.Preferences.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ namespace Bit.App.Pages
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
_messagingService.Send("showStatusBar", false);
|
||||
InitializeComponent();
|
||||
var theme = ThemeManager.GetTheme(Device.RuntimePlatform == Device.Android);
|
||||
var darkbasedTheme = theme == "dark" || theme == "black" || theme == "nord";
|
||||
_logo.Source = darkbasedTheme ? "logo_white.png" : "logo.png";
|
||||
_logo.Source = !ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png";
|
||||
}
|
||||
|
||||
public async Task DismissRegisterPageAndLogInAsync(string email)
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace Bit.App.Pages
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
private bool _hasKey;
|
||||
private string _email;
|
||||
private bool _showPassword;
|
||||
private bool _pinLock;
|
||||
@@ -104,8 +103,7 @@ namespace Bit.App.Pages
|
||||
public async Task InitAsync(bool autoPromptFingerprint)
|
||||
{
|
||||
_pinSet = await _lockService.IsPinLockSetAsync();
|
||||
_hasKey = await _cryptoService.HasKeyAsync();
|
||||
PinLock = (_pinSet.Item1 && _hasKey) || _pinSet.Item2;
|
||||
PinLock = (_pinSet.Item1 && _lockService.PinProtectedKey != null) || _pinSet.Item2;
|
||||
FingerprintLock = await _lockService.IsFingerprintLockSetAsync();
|
||||
_email = await _userService.GetEmailAsync();
|
||||
var webVault = _environmentService.GetWebVaultUrl();
|
||||
@@ -128,8 +126,19 @@ namespace Bit.App.Pages
|
||||
|
||||
if(FingerprintLock)
|
||||
{
|
||||
FingerprintButtonText = _deviceActionService.SupportsFaceId() ? AppResources.UseFaceIDToUnlock :
|
||||
AppResources.UseFingerprintToUnlock;
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
if(Device.RuntimePlatform == Device.iOS && supportsFace)
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseFaceIDToUnlock;
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseBiometricsToUnlock;
|
||||
}
|
||||
else
|
||||
{
|
||||
FingerprintButtonText = AppResources.UseFingerprintToUnlock;
|
||||
}
|
||||
if(autoPromptFingerprint)
|
||||
{
|
||||
var tasks = Task.Run(async () =>
|
||||
@@ -169,14 +178,17 @@ namespace Bit.App.Pages
|
||||
{
|
||||
if(_pinSet.Item1)
|
||||
{
|
||||
var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000),
|
||||
_lockService.PinProtectedKey);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var protectedPin = await _storageService.GetAsync<string>(Constants.ProtectedPin);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin));
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
failed = decPin != Pin;
|
||||
_lockService.PinLocked = failed;
|
||||
if(!failed)
|
||||
{
|
||||
Pin = string.Empty;
|
||||
await DoContinueAsync();
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -221,6 +233,15 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash)
|
||||
{
|
||||
if(_pinSet.Item1)
|
||||
{
|
||||
var protectedPin = await _storageService.GetAsync<string>(Constants.ProtectedPin);
|
||||
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
||||
var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin), encKey);
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email,
|
||||
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
||||
_lockService.PinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
}
|
||||
MasterPassword = string.Empty;
|
||||
await SetKeyAndContinueAsync(key);
|
||||
}
|
||||
@@ -256,7 +277,7 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
var success = await _platformUtilsService.AuthenticateFingerprintAsync(null,
|
||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
PinLock ? AppResources.PIN : AppResources.MasterPassword, () =>
|
||||
{
|
||||
var page = Page as LockPage;
|
||||
@@ -278,7 +299,8 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key)
|
||||
{
|
||||
if(!_hasKey)
|
||||
var hasKey = await _cryptoService.HasKeyAsync();
|
||||
if(!hasKey)
|
||||
{
|
||||
await _cryptoService.SetKeyAsync(key);
|
||||
}
|
||||
@@ -287,6 +309,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task DoContinueAsync()
|
||||
{
|
||||
_lockService.FingerprintLocked = false;
|
||||
var disableFavicon = await _storageService.GetAsync<bool?>(Constants.DisableFaviconKey);
|
||||
await _stateService.SaveAsync(Constants.DisableFaviconKey, disableFavicon.GetValueOrDefault());
|
||||
_messagingService.Send("unlocked");
|
||||
|
||||
@@ -135,7 +135,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,7 +135,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -213,8 +213,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.PlatformConfiguration;
|
||||
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
@@ -14,6 +16,14 @@ namespace Bit.App.Pages
|
||||
protected int ShowModalAnimationDelay = 400;
|
||||
protected int ShowPageAnimationDelay = 100;
|
||||
|
||||
public BaseContentPage()
|
||||
{
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.FullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? LastPageAction { get; set; }
|
||||
|
||||
protected override void OnAppearing()
|
||||
|
||||
@@ -93,7 +93,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -128,7 +132,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -142,8 +142,12 @@ namespace Bit.App.Pages
|
||||
var fingerprintName = AppResources.Fingerprint;
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
fingerprintName = _deviceActionService.SupportsFaceId() ?
|
||||
AppResources.FaceID : AppResources.TouchID;
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
fingerprintName = supportsFace ? AppResources.FaceID : AppResources.TouchID;
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
fingerprintName = AppResources.Biometrics;
|
||||
}
|
||||
if(item.Name == string.Format(AppResources.UnlockWith, fingerprintName))
|
||||
{
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
_supportsFingerprint = await _platformUtilsService.SupportsFingerprintAsync();
|
||||
_supportsFingerprint = await _platformUtilsService.SupportsBiometricAsync();
|
||||
var lastSync = await _syncService.GetLastSyncAsync();
|
||||
if(lastSync != null)
|
||||
{
|
||||
@@ -214,21 +214,24 @@ namespace Bit.App.Pages
|
||||
var masterPassOnRestart = await _platformUtilsService.ShowDialogAsync(
|
||||
AppResources.PINRequireMasterPasswordRestart, AppResources.UnlockWithPIN,
|
||||
AppResources.Yes, AppResources.No);
|
||||
|
||||
var kdf = await _userService.GetKdfAsync();
|
||||
var kdfIterations = await _userService.GetKdfIterationsAsync();
|
||||
var email = await _userService.GetEmailAsync();
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email,
|
||||
kdf.GetValueOrDefault(Core.Enums.KdfType.PBKDF2_SHA256),
|
||||
kdfIterations.GetValueOrDefault(5000));
|
||||
var key = await _cryptoService.GetKeyAsync();
|
||||
var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
|
||||
if(masterPassOnRestart)
|
||||
{
|
||||
var encPin = await _cryptoService.EncryptAsync(pin);
|
||||
await _storageService.SaveAsync(Constants.ProtectedPin, encPin.EncryptedString);
|
||||
_lockService.PinProtectedKey = pinProtectedKey;
|
||||
}
|
||||
else
|
||||
{
|
||||
var kdf = await _userService.GetKdfAsync();
|
||||
var kdfIterations = await _userService.GetKdfIterationsAsync();
|
||||
var email = await _userService.GetEmailAsync();
|
||||
var pinKey = await _cryptoService.MakePinKeyAysnc(pin, email,
|
||||
kdf.GetValueOrDefault(Core.Enums.KdfType.PBKDF2_SHA256),
|
||||
kdfIterations.GetValueOrDefault(5000));
|
||||
var key = await _cryptoService.GetKeyAsync();
|
||||
var pinProtectedKey = await _cryptoService.EncryptAsync(key.Key, pinKey);
|
||||
await _storageService.SaveAsync(Constants.PinProtectedKey, pinProtectedKey.EncryptedString);
|
||||
}
|
||||
}
|
||||
@@ -239,8 +242,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if(!_pin)
|
||||
{
|
||||
await _storageService.RemoveAsync(Constants.PinProtectedKey);
|
||||
await _storageService.RemoveAsync(Constants.ProtectedPin);
|
||||
await _cryptoService.ClearPinProtectedKeyAsync();
|
||||
await _lockService.ClearAsync();
|
||||
}
|
||||
BuildList();
|
||||
}
|
||||
@@ -252,9 +255,9 @@ namespace Bit.App.Pages
|
||||
{
|
||||
_fingerprint = false;
|
||||
}
|
||||
else if(await _platformUtilsService.SupportsFingerprintAsync())
|
||||
else if(await _platformUtilsService.SupportsBiometricAsync())
|
||||
{
|
||||
_fingerprint = await _platformUtilsService.AuthenticateFingerprintAsync(null,
|
||||
_fingerprint = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||
_deviceActionService.DeviceType == Core.Enums.DeviceType.Android ? "." : null);
|
||||
}
|
||||
if(_fingerprint == current)
|
||||
@@ -325,8 +328,12 @@ namespace Bit.App.Pages
|
||||
var fingerprintName = AppResources.Fingerprint;
|
||||
if(Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
fingerprintName = _deviceActionService.SupportsFaceId() ?
|
||||
AppResources.FaceID : AppResources.TouchID;
|
||||
fingerprintName = _deviceActionService.SupportsFaceBiometric() ? AppResources.FaceID :
|
||||
AppResources.TouchID;
|
||||
}
|
||||
else if(Device.RuntimePlatform == Device.Android && _deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
fingerprintName = AppResources.Biometrics;
|
||||
}
|
||||
var item = new SettingsPageListItem
|
||||
{
|
||||
|
||||
@@ -70,7 +70,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,7 +563,15 @@
|
||||
IsVisible="{Binding IsHiddenType}"
|
||||
IsPassword="{Binding ShowHiddenValue, Converter={StaticResource inverseBool}}"
|
||||
IsSpellCheckEnabled="False"
|
||||
IsTextPredictionEnabled="False" />
|
||||
IsTextPredictionEnabled="False">
|
||||
<Entry.Keyboard>
|
||||
<Keyboard x:FactoryMethod="Create">
|
||||
<x:Arguments>
|
||||
<KeyboardFlags>None</KeyboardFlags>
|
||||
</x:Arguments>
|
||||
</Keyboard>
|
||||
</Entry.Keyboard>
|
||||
</controls:MonoEntry>
|
||||
<Switch
|
||||
IsToggled="{Binding BooleanValue}"
|
||||
Grid.Row="0"
|
||||
|
||||
@@ -73,6 +73,8 @@ namespace Bit.App.Pages
|
||||
_folderPicker.ItemDisplayBinding = new Binding("Key");
|
||||
_ownershipPicker.ItemDisplayBinding = new Binding("Key");
|
||||
|
||||
_loginPasswordEntry.Keyboard = Keyboard.Create(KeyboardFlags.None);
|
||||
|
||||
_nameEntry.ReturnType = ReturnType.Next;
|
||||
_nameEntry.ReturnCommand = new Command(() =>
|
||||
{
|
||||
|
||||
@@ -401,12 +401,13 @@ namespace Bit.App.Pages
|
||||
return false;
|
||||
}
|
||||
|
||||
Cipher.Fields = Fields != null && Fields.Any() ? Fields.Select(f => f.Field).ToList() : null;
|
||||
Cipher.Fields = Fields != null && Fields.Any() ?
|
||||
Fields.Where(f => f != null).Select(f => f.Field).ToList() : null;
|
||||
if(Cipher.Login != null)
|
||||
{
|
||||
Cipher.Login.Uris = Uris?.ToList();
|
||||
if(!EditMode && Cipher.Type == CipherType.Login && (Cipher.Login.Uris?.Count ?? 0) == 1 &&
|
||||
string.IsNullOrWhiteSpace(Cipher.Login.Uris.First().Uri))
|
||||
if(!EditMode && Cipher.Type == CipherType.Login && Cipher.Login.Uris != null &&
|
||||
Cipher.Login.Uris.Count == 1 && string.IsNullOrWhiteSpace(Cipher.Login.Uris[0].Uri))
|
||||
{
|
||||
Cipher.Login.Uris = null;
|
||||
}
|
||||
@@ -414,7 +415,7 @@ namespace Bit.App.Pages
|
||||
|
||||
if(!EditMode && Cipher.OrganizationId != null)
|
||||
{
|
||||
if(!Collections?.Any(c => c.Checked) ?? true)
|
||||
if(Collections == null || !Collections.Any(c => c != null && c.Checked))
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection,
|
||||
AppResources.Ok);
|
||||
@@ -422,7 +423,8 @@ namespace Bit.App.Pages
|
||||
}
|
||||
|
||||
Cipher.CollectionIds = Collections.Any() ?
|
||||
new HashSet<string>(Collections.Where(c => c.Checked).Select(c => c.Collection.Id)) : null;
|
||||
new HashSet<string>(Collections.Where(c => c != null && c.Checked && c.Collection?.Id != null)
|
||||
.Select(c => c.Collection.Id)) : null;
|
||||
}
|
||||
|
||||
var cipher = await _cipherService.EncryptAsync(Cipher);
|
||||
@@ -454,7 +456,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -485,7 +491,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -164,7 +168,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
catch(Exception e) when(e.Message.Contains("No key."))
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
await Task.Delay(1000);
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}, _mainContent);
|
||||
|
||||
@@ -157,8 +157,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(),
|
||||
AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
VerticalOptions="CenterAndExpand"
|
||||
Clicked="BackButton_Clicked"
|
||||
x:Name="_backButton" />
|
||||
<SearchBar
|
||||
<controls:ExtendedSearchBar
|
||||
x:Name="_searchBar"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
TextChanged="SearchBar_TextChanged"
|
||||
|
||||
@@ -168,8 +168,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(),
|
||||
AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(_deviceActionService.SystemMajorVersion() < 21)
|
||||
|
||||
@@ -85,7 +85,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</controls:FaLabel>
|
||||
<Label Text="{Binding Name, Mode=OneWay}"
|
||||
LineBreakMode="TailTruncation"
|
||||
HorizontalOptions="StartAndExpand"
|
||||
HorizontalOptions="FillAndExpand"
|
||||
VerticalOptions="CenterAndExpand"
|
||||
StyleClass="list-title"/>
|
||||
<Label Text="{Binding ItemCount, Mode=OneWay}"
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
@@ -19,6 +20,7 @@ namespace Bit.App.Pages
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly ILockService _lockService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly GroupingsPageViewModel _vm;
|
||||
private readonly string _pageName;
|
||||
@@ -37,6 +39,7 @@ namespace Bit.App.Pages
|
||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_vm = BindingContext as GroupingsPageViewModel;
|
||||
_vm.Page = this;
|
||||
@@ -97,7 +100,7 @@ namespace Bit.App.Pages
|
||||
var migratedFromV1 = await _storageService.GetAsync<bool?>(Constants.MigratedFromV1);
|
||||
await LoadOnAppearedAsync(_mainLayout, false, async () =>
|
||||
{
|
||||
if(!_syncService.SyncInProgress)
|
||||
if(!_syncService.SyncInProgress || (await _cipherService.GetAllAsync()).Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -105,7 +108,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
catch(Exception e) when(e.Message.Contains("No key."))
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
await Task.Delay(1000);
|
||||
await _vm.LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace Bit.App.Pages
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly ICollectionService _collectionService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ILockService _lockService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
@@ -48,6 +50,8 @@ namespace Bit.App.Pages
|
||||
_folderService = ServiceContainer.Resolve<IFolderService>("folderService");
|
||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
_lockService = ServiceContainer.Resolve<ILockService>("lockService");
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
@@ -134,6 +138,15 @@ namespace Bit.App.Pages
|
||||
{
|
||||
return;
|
||||
}
|
||||
var authed = await _userService.IsAuthenticatedAsync();
|
||||
if(!authed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(await _lockService.IsLockedAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
_doingLoad = true;
|
||||
LoadedOnce = true;
|
||||
ShowNoData = false;
|
||||
@@ -305,9 +318,17 @@ namespace Bit.App.Pages
|
||||
return;
|
||||
}
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Syncing);
|
||||
await _syncService.FullSyncAsync(false);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||
try
|
||||
{
|
||||
await _syncService.FullSyncAsync(false, true);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.SyncingComplete);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("error", null, AppResources.SyncingFailed);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
|
||||
@@ -116,12 +116,19 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
catch(System.Exception e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Message, AppResources.Ok);
|
||||
if(e.Message != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Message, AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -298,7 +298,11 @@ namespace Bit.App.Pages
|
||||
catch(ApiException e)
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
|
||||
if(e?.Error != null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
||||
AppResources.AnErrorHasOccurred);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
36
src/App/Resources/AppResources.Designer.cs
generated
36
src/App/Resources/AppResources.Designer.cs
generated
@@ -537,6 +537,24 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Biometrics.
|
||||
/// </summary>
|
||||
public static string Biometrics {
|
||||
get {
|
||||
return ResourceManager.GetString("Biometrics", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use biometrics to verify..
|
||||
/// </summary>
|
||||
public static string BiometricsDirection {
|
||||
get {
|
||||
return ResourceManager.GetString("BiometricsDirection", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bitwarden.
|
||||
/// </summary>
|
||||
@@ -2274,6 +2292,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your login session has expired..
|
||||
/// </summary>
|
||||
public static string LoginExpired {
|
||||
get {
|
||||
return ResourceManager.GetString("LoginExpired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Login.
|
||||
/// </summary>
|
||||
@@ -3813,6 +3840,15 @@ namespace Bit.App.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use Biometrics To Unlock.
|
||||
/// </summary>
|
||||
public static string UseBiometricsToUnlock {
|
||||
get {
|
||||
return ResourceManager.GetString("UseBiometricsToUnlock", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use Face ID To Unlock.
|
||||
/// </summary>
|
||||
|
||||
@@ -1575,4 +1575,16 @@
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1575,4 +1575,16 @@
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1305,33 +1305,33 @@
|
||||
<value>The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Settings" screen.</value>
|
||||
</data>
|
||||
<data name="InvalidEmail" xml:space="preserve">
|
||||
<value>Invalid email address.</value>
|
||||
<value>Alamat email tidak valid.</value>
|
||||
</data>
|
||||
<data name="Cards" xml:space="preserve">
|
||||
<value>Cards</value>
|
||||
<value>Kartu</value>
|
||||
</data>
|
||||
<data name="Identities" xml:space="preserve">
|
||||
<value>Identities</value>
|
||||
<value>Identitas</value>
|
||||
</data>
|
||||
<data name="Logins" xml:space="preserve">
|
||||
<value>Logins</value>
|
||||
<value>Info Masuk</value>
|
||||
</data>
|
||||
<data name="SecureNotes" xml:space="preserve">
|
||||
<value>Secure Notes</value>
|
||||
<value>Catatan Aman</value>
|
||||
</data>
|
||||
<data name="AllItems" xml:space="preserve">
|
||||
<value>All Items</value>
|
||||
<value>Semua Item</value>
|
||||
</data>
|
||||
<data name="URIs" xml:space="preserve">
|
||||
<value>URIs</value>
|
||||
<value>URI</value>
|
||||
<comment>Plural form of a URI</comment>
|
||||
</data>
|
||||
<data name="CheckingPassword" xml:space="preserve">
|
||||
<value>Checking password...</value>
|
||||
<value>Memeriksa kata sandi...</value>
|
||||
<comment>A loading message when doing an exposed password check.</comment>
|
||||
</data>
|
||||
<data name="CheckPassword" xml:space="preserve">
|
||||
<value>Check if password has been exposed.</value>
|
||||
<value>Periksalah jika kata sandi telah terpapar.</value>
|
||||
</data>
|
||||
<data name="PasswordExposed" xml:space="preserve">
|
||||
<value>This password has been exposed {0} time(s) in data breaches. You should change it.</value>
|
||||
@@ -1449,10 +1449,10 @@
|
||||
<value>Unlock</value>
|
||||
</data>
|
||||
<data name="LockOption30Minutes" xml:space="preserve">
|
||||
<value>30 minutes</value>
|
||||
<value>30 menit</value>
|
||||
</data>
|
||||
<data name="LockOption5Minutes" xml:space="preserve">
|
||||
<value>5 minutes</value>
|
||||
<value>5 menit</value>
|
||||
</data>
|
||||
<data name="SetPINDescription" xml:space="preserve">
|
||||
<value>Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application.</value>
|
||||
@@ -1476,22 +1476,22 @@
|
||||
<comment>A light color</comment>
|
||||
</data>
|
||||
<data name="FiveMinutes" xml:space="preserve">
|
||||
<value>5 minutes</value>
|
||||
<value>5 menit</value>
|
||||
</data>
|
||||
<data name="OneMinute" xml:space="preserve">
|
||||
<value>1 minute</value>
|
||||
<value>1 menit</value>
|
||||
</data>
|
||||
<data name="TenSeconds" xml:space="preserve">
|
||||
<value>10 seconds</value>
|
||||
<value>10 detik</value>
|
||||
</data>
|
||||
<data name="ThirtySeconds" xml:space="preserve">
|
||||
<value>30 seconds</value>
|
||||
<value>30 detik</value>
|
||||
</data>
|
||||
<data name="TwentySeconds" xml:space="preserve">
|
||||
<value>20 seconds</value>
|
||||
<value>20 detik</value>
|
||||
</data>
|
||||
<data name="TwoMinutes" xml:space="preserve">
|
||||
<value>2 minutes</value>
|
||||
<value>2 menit</value>
|
||||
</data>
|
||||
<data name="ClearClipboard" xml:space="preserve">
|
||||
<value>Clear Clipboard</value>
|
||||
@@ -1509,70 +1509,82 @@
|
||||
<value>Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill.</value>
|
||||
</data>
|
||||
<data name="Theme" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
<value>Tema</value>
|
||||
<comment>Color theme</comment>
|
||||
</data>
|
||||
<data name="ThemeDescription" xml:space="preserve">
|
||||
<value>Change the application's color theme.</value>
|
||||
<value>Ubah warna tema aplikasi.</value>
|
||||
</data>
|
||||
<data name="RestartIsRequired" xml:space="preserve">
|
||||
<value>Restart is required.</value>
|
||||
<value>Dibutuhkan pemulaian ulang.</value>
|
||||
<comment>Referring to restarting the application.</comment>
|
||||
</data>
|
||||
<data name="Restarting" xml:space="preserve">
|
||||
<value>Restarting...</value>
|
||||
<value>Memulai ulang...</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Copy Notes</value>
|
||||
<value>Catatan Salinan</value>
|
||||
</data>
|
||||
<data name="Exit" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
<value>Keluar</value>
|
||||
</data>
|
||||
<data name="ExitConfirmation" xml:space="preserve">
|
||||
<value>Are you sure you want to exit Bitwarden?</value>
|
||||
<value>Apakah anda yakin ingin keluar dari Bitwarden?</value>
|
||||
</data>
|
||||
<data name="PINRequireMasterPasswordRestart" xml:space="preserve">
|
||||
<value>You you want to require unlocking with your master password when the application is restarted?</value>
|
||||
</data>
|
||||
<data name="Black" xml:space="preserve">
|
||||
<value>Black</value>
|
||||
<value>Hitam</value>
|
||||
<comment>The color black</comment>
|
||||
</data>
|
||||
<data name="BlacklistedUris" xml:space="preserve">
|
||||
<value>Blacklisted URIs</value>
|
||||
<value>Daftar hitam URI</value>
|
||||
</data>
|
||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
||||
<value>URIs that are blacklisted will not offer auto-fill. The list of apps should be comma separated. Ex: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||
</data>
|
||||
<data name="DisableSavePrompt" xml:space="preserve">
|
||||
<value>Disable Save Prompt</value>
|
||||
<value>Nonaktifkan Saran Simpan</value>
|
||||
</data>
|
||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
||||
<value>The "Save Prompt" automatically prompts you to save new items to your vault whenever you enter them for the first time.</value>
|
||||
<value>"Saran Simpan" secara otomatis meminta anda untuk menyimpan item baru ke brankas anda setiap kali anda memasukkannya untuk pertama kali.</value>
|
||||
</data>
|
||||
<data name="LockOptionOnRestart" xml:space="preserve">
|
||||
<value>On App Restart</value>
|
||||
<value>Saat Aplikasi Memulai Ulang</value>
|
||||
</data>
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>Auto-fill makes it easy to securely access your Bitwarden vault from other websites and apps. It looks like you have not enabled an auto-fill service for Bitwarden. Enable auto-fill for Bitwarden from the "Settings" screen.</value>
|
||||
<value>Isi-otomatis memudahkan untuk mengakses brankas Bitwarden anda dengan aman dari situs web dan aplikasi lain. Sepertinya anda belum mengaktifkan layanan pengisian-otomatis untuk Bitwarden. Aktifkan pengisian-otomatis untuk Bitwarden dari layar "Pengaturan".</value>
|
||||
</data>
|
||||
<data name="ThemeAppliedOnRestart" xml:space="preserve">
|
||||
<value>Your theme changes will apply when the app is restarted.</value>
|
||||
<value>Perubahan tema anda akan diterapkan setelah aplikasi dimulai ulang.</value>
|
||||
</data>
|
||||
<data name="Capitalize" xml:space="preserve">
|
||||
<value>Capitalize</value>
|
||||
<value>Jadikan Huruf Besar</value>
|
||||
<comment>ex. Uppercase the first character of a word.</comment>
|
||||
</data>
|
||||
<data name="IncludeNumber" xml:space="preserve">
|
||||
<value>Include Number</value>
|
||||
<value>Sertakan Digit</value>
|
||||
</data>
|
||||
<data name="Download" xml:space="preserve">
|
||||
<value>Download</value>
|
||||
<value>Unduh</value>
|
||||
</data>
|
||||
<data name="Shared" xml:space="preserve">
|
||||
<value>Shared</value>
|
||||
<value>Dibagikan</value>
|
||||
</data>
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
<value>Alihkan Visibilitas</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1308,22 +1308,22 @@
|
||||
<value>잘못된 이메일 주소입니다.</value>
|
||||
</data>
|
||||
<data name="Cards" xml:space="preserve">
|
||||
<value>Cards</value>
|
||||
<value>카드</value>
|
||||
</data>
|
||||
<data name="Identities" xml:space="preserve">
|
||||
<value>Identities</value>
|
||||
<value>신원</value>
|
||||
</data>
|
||||
<data name="Logins" xml:space="preserve">
|
||||
<value>Logins</value>
|
||||
<value>로그인</value>
|
||||
</data>
|
||||
<data name="SecureNotes" xml:space="preserve">
|
||||
<value>Secure Notes</value>
|
||||
<value>보안 메모</value>
|
||||
</data>
|
||||
<data name="AllItems" xml:space="preserve">
|
||||
<value>All Items</value>
|
||||
<value>모든 항목</value>
|
||||
</data>
|
||||
<data name="URIs" xml:space="preserve">
|
||||
<value>URIs</value>
|
||||
<value>URI</value>
|
||||
<comment>Plural form of a URI</comment>
|
||||
</data>
|
||||
<data name="CheckingPassword" xml:space="preserve">
|
||||
@@ -1334,25 +1334,25 @@
|
||||
<value>비밀번호가 노출되었는지 확인합니다.</value>
|
||||
</data>
|
||||
<data name="PasswordExposed" xml:space="preserve">
|
||||
<value>This password has been exposed {0} time(s) in data breaches. You should change it.</value>
|
||||
<value>이 비밀번호는 데이터 유출에 {0}회 노출되었습니다. 비밀번호를 변경하는 것이 좋습니다.</value>
|
||||
</data>
|
||||
<data name="PasswordSafe" xml:space="preserve">
|
||||
<value>This password was not found in any known data breaches. It should be safe to use.</value>
|
||||
<value>이 비밀번호는 데이터 유출 목록에 없습니다. 사용하기에 안전한 비밀번호입니다.</value>
|
||||
</data>
|
||||
<data name="IdentityName" xml:space="preserve">
|
||||
<value>Identity Name</value>
|
||||
<value>ID 이름</value>
|
||||
</data>
|
||||
<data name="Value" xml:space="preserve">
|
||||
<value>값</value>
|
||||
</data>
|
||||
<data name="PasswordHistory" xml:space="preserve">
|
||||
<value>Password History</value>
|
||||
<value>비밀번호 변경 기록</value>
|
||||
</data>
|
||||
<data name="Types" xml:space="preserve">
|
||||
<value>유형</value>
|
||||
</data>
|
||||
<data name="NoPasswordsToList" xml:space="preserve">
|
||||
<value>No passwords to list.</value>
|
||||
<value>비밀번호가 없습니다.</value>
|
||||
</data>
|
||||
<data name="NoItemsToList" xml:space="preserve">
|
||||
<value>항목이 없습니다.</value>
|
||||
@@ -1364,66 +1364,66 @@
|
||||
<value>폴더 검색</value>
|
||||
</data>
|
||||
<data name="SearchType" xml:space="preserve">
|
||||
<value>Search type</value>
|
||||
<value>검색 유형</value>
|
||||
</data>
|
||||
<data name="Type" xml:space="preserve">
|
||||
<value>유형</value>
|
||||
</data>
|
||||
<data name="MoveDown" xml:space="preserve">
|
||||
<value>Move Down</value>
|
||||
<value>아래로 옮기기</value>
|
||||
</data>
|
||||
<data name="MoveUp" xml:space="preserve">
|
||||
<value>Move Up</value>
|
||||
<value>위로 옮기기</value>
|
||||
</data>
|
||||
<data name="Miscellaneous" xml:space="preserve">
|
||||
<value>Miscellaneous</value>
|
||||
<value>기타</value>
|
||||
</data>
|
||||
<data name="Ownership" xml:space="preserve">
|
||||
<value>Ownership</value>
|
||||
<value>소유자</value>
|
||||
</data>
|
||||
<data name="WhoOwnsThisItem" xml:space="preserve">
|
||||
<value>Who owns this item?</value>
|
||||
<value>이 항목의 소유자는 누구입니까?</value>
|
||||
</data>
|
||||
<data name="NoCollectionsToList" xml:space="preserve">
|
||||
<value>There are no collections to list.</value>
|
||||
<value>컬렉션이 없습니다.</value>
|
||||
</data>
|
||||
<data name="ItemShared" xml:space="preserve">
|
||||
<value>Item has been shared.</value>
|
||||
<value>항목이 공유되었습니다.</value>
|
||||
</data>
|
||||
<data name="SelectOneCollection" xml:space="preserve">
|
||||
<value>You must select at least one collection.</value>
|
||||
</data>
|
||||
<data name="Share" xml:space="preserve">
|
||||
<value>Share</value>
|
||||
<value>공유</value>
|
||||
</data>
|
||||
<data name="ShareItem" xml:space="preserve">
|
||||
<value>Share Item</value>
|
||||
<value>항목 공유</value>
|
||||
</data>
|
||||
<data name="NoOrgsToList" xml:space="preserve">
|
||||
<value>No organizations to list.</value>
|
||||
<value>조직이 없습니다.</value>
|
||||
</data>
|
||||
<data name="ShareDesc" xml:space="preserve">
|
||||
<value>Choose an organization that you wish to share this item with. Sharing transfers ownership of the item to the organization. You will no longer be the direct owner of this item once it has been shared.</value>
|
||||
<value>이 항목을 공유할 조직을 선택하세요. 항목을 공유하면 소유권이 조직으로 이전됩니다. 일단 항목을 공유하면, 항목의 직접적인 소유자가 아니게 됩니다.</value>
|
||||
</data>
|
||||
<data name="NumberOfWords" xml:space="preserve">
|
||||
<value>Number of Words</value>
|
||||
<value>단어 수</value>
|
||||
</data>
|
||||
<data name="Passphrase" xml:space="preserve">
|
||||
<value>Passphrase</value>
|
||||
<value>패스프레이즈</value>
|
||||
</data>
|
||||
<data name="WordSeparator" xml:space="preserve">
|
||||
<value>Word Separator</value>
|
||||
<value>구분 기호</value>
|
||||
</data>
|
||||
<data name="Clear" xml:space="preserve">
|
||||
<value>Clear</value>
|
||||
<value>삭제</value>
|
||||
<comment>To clear something out. example: To clear browser history.</comment>
|
||||
</data>
|
||||
<data name="Generator" xml:space="preserve">
|
||||
<value>Generator</value>
|
||||
<value>생성기</value>
|
||||
<comment>Short for "Password Generator"</comment>
|
||||
</data>
|
||||
<data name="NoFoldersToList" xml:space="preserve">
|
||||
<value>There are no folders to list.</value>
|
||||
<value>폴더가 없습니다.</value>
|
||||
</data>
|
||||
<data name="FingerprintPhrase" xml:space="preserve">
|
||||
<value>Fingerprint Phrase</value>
|
||||
@@ -1437,22 +1437,22 @@
|
||||
<value>Bitwarden allows you to share your vault with others by using an organization account. Would you like to visit the bitwarden.com website to learn more?</value>
|
||||
</data>
|
||||
<data name="ExportVault" xml:space="preserve">
|
||||
<value>Export Vault</value>
|
||||
<value>보관함 내보내기</value>
|
||||
</data>
|
||||
<data name="LockNow" xml:space="preserve">
|
||||
<value>Lock Now</value>
|
||||
<value>지금 잠그기</value>
|
||||
</data>
|
||||
<data name="PIN" xml:space="preserve">
|
||||
<value>PIN</value>
|
||||
</data>
|
||||
<data name="Unlock" xml:space="preserve">
|
||||
<value>Unlock</value>
|
||||
<value>잠금 해제</value>
|
||||
</data>
|
||||
<data name="LockOption30Minutes" xml:space="preserve">
|
||||
<value>30 minutes</value>
|
||||
<value>30분</value>
|
||||
</data>
|
||||
<data name="LockOption5Minutes" xml:space="preserve">
|
||||
<value>5 minutes</value>
|
||||
<value>5분</value>
|
||||
</data>
|
||||
<data name="SetPINDescription" xml:space="preserve">
|
||||
<value>Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if you ever fully log out of the application.</value>
|
||||
@@ -1468,52 +1468,52 @@
|
||||
<value>Your vault is locked. Verify your PIN code to continue.</value>
|
||||
</data>
|
||||
<data name="Dark" xml:space="preserve">
|
||||
<value>Dark</value>
|
||||
<value>어두운 테마</value>
|
||||
<comment>A dark color</comment>
|
||||
</data>
|
||||
<data name="Light" xml:space="preserve">
|
||||
<value>Light</value>
|
||||
<value>밝은 테마</value>
|
||||
<comment>A light color</comment>
|
||||
</data>
|
||||
<data name="FiveMinutes" xml:space="preserve">
|
||||
<value>5 minutes</value>
|
||||
<value>5분</value>
|
||||
</data>
|
||||
<data name="OneMinute" xml:space="preserve">
|
||||
<value>1 minute</value>
|
||||
<value>1분</value>
|
||||
</data>
|
||||
<data name="TenSeconds" xml:space="preserve">
|
||||
<value>10 seconds</value>
|
||||
<value>10초</value>
|
||||
</data>
|
||||
<data name="ThirtySeconds" xml:space="preserve">
|
||||
<value>30 seconds</value>
|
||||
<value>30초</value>
|
||||
</data>
|
||||
<data name="TwentySeconds" xml:space="preserve">
|
||||
<value>20 seconds</value>
|
||||
<value>20초</value>
|
||||
</data>
|
||||
<data name="TwoMinutes" xml:space="preserve">
|
||||
<value>2 minutes</value>
|
||||
<value>2분</value>
|
||||
</data>
|
||||
<data name="ClearClipboard" xml:space="preserve">
|
||||
<value>Clear Clipboard</value>
|
||||
<value>클립보드 비우기</value>
|
||||
<comment>Clipboard is the operating system thing where you copy/paste data to on your device.</comment>
|
||||
</data>
|
||||
<data name="ClearClipboardDescription" xml:space="preserve">
|
||||
<value>Automatically clear copied values from your clipboard.</value>
|
||||
<value>클립보드에 복사된 값을 자동으로 제거합니다.</value>
|
||||
<comment>Clipboard is the operating system thing where you copy/paste data to on your device.</comment>
|
||||
</data>
|
||||
<data name="DefaultUriMatchDetection" xml:space="preserve">
|
||||
<value>Default URI Match Detection</value>
|
||||
<value>기본 URI 일치 인식</value>
|
||||
<comment>Default URI match detection for auto-fill.</comment>
|
||||
</data>
|
||||
<data name="DefaultUriMatchDetectionDescription" xml:space="preserve">
|
||||
<value>Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill.</value>
|
||||
</data>
|
||||
<data name="Theme" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
<value>테마</value>
|
||||
<comment>Color theme</comment>
|
||||
</data>
|
||||
<data name="ThemeDescription" xml:space="preserve">
|
||||
<value>Change the application's color theme.</value>
|
||||
<value>애플리케이션의 색상 테마를 변경합니다.</value>
|
||||
</data>
|
||||
<data name="RestartIsRequired" xml:space="preserve">
|
||||
<value>Restart is required.</value>
|
||||
@@ -1523,10 +1523,10 @@
|
||||
<value>Restarting...</value>
|
||||
</data>
|
||||
<data name="CopyNotes" xml:space="preserve">
|
||||
<value>Copy Notes</value>
|
||||
<value>메모 복사</value>
|
||||
</data>
|
||||
<data name="Exit" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
<value>끝내기</value>
|
||||
</data>
|
||||
<data name="ExitConfirmation" xml:space="preserve">
|
||||
<value>Are you sure you want to exit Bitwarden?</value>
|
||||
@@ -1535,7 +1535,7 @@
|
||||
<value>You you want to require unlocking with your master password when the application is restarted?</value>
|
||||
</data>
|
||||
<data name="Black" xml:space="preserve">
|
||||
<value>Black</value>
|
||||
<value>검은 테마</value>
|
||||
<comment>The color black</comment>
|
||||
</data>
|
||||
<data name="BlacklistedUris" xml:space="preserve">
|
||||
@@ -1575,4 +1575,16 @@
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1557,22 +1557,34 @@
|
||||
<value>Auto-utfylling gjør det lett å få sikker tilgang til Bitwarden-hvelvet ditt fra andre nettsteder og apper. Det ser ut til at du ikke har skrudd på noen auto-utfyllingstjenester for Bitwarden. Du kan skru på auto-utfylling for Bitwarden i «Innstillinger»-menyen.</value>
|
||||
</data>
|
||||
<data name="ThemeAppliedOnRestart" xml:space="preserve">
|
||||
<value>Your theme changes will apply when the app is restarted.</value>
|
||||
<value>Temaendringene dine vil gjelde når appen startes på nytt.</value>
|
||||
</data>
|
||||
<data name="Capitalize" xml:space="preserve">
|
||||
<value>Capitalize</value>
|
||||
<value>Stor forbokstav</value>
|
||||
<comment>ex. Uppercase the first character of a word.</comment>
|
||||
</data>
|
||||
<data name="IncludeNumber" xml:space="preserve">
|
||||
<value>Include Number</value>
|
||||
<value>Inkluder nummer</value>
|
||||
</data>
|
||||
<data name="Download" xml:space="preserve">
|
||||
<value>Download</value>
|
||||
<value>Last ned</value>
|
||||
</data>
|
||||
<data name="Shared" xml:space="preserve">
|
||||
<value>Shared</value>
|
||||
<value>Delt</value>
|
||||
</data>
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1575,4 +1575,16 @@
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visibility</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -412,7 +412,7 @@
|
||||
<value>bitwarden rozšírenie aplikácie</value>
|
||||
</data>
|
||||
<data name="BitwardenAppExtensionAlert2" xml:space="preserve">
|
||||
<value>The easiest way to add new logins to your vault is from the Bitwarden App Extension. Learn more about using the Bitwarden App Extension by navigating to the "Settings" screen.</value>
|
||||
<value>Najjednoduchší spôsob, ako pridať nové prihlasovacie údaje do trezora, je z Bitwarden rozšírenia aplikácie. Ďalšie informácie o používaní Bitwarden rozšírenia aplikácie získate prechodom do obrazovky Nastavení.</value>
|
||||
</data>
|
||||
<data name="BitwardenAppExtensionDescription" xml:space="preserve">
|
||||
<value>Použiť bitwarden v Safari a iných aplikáciach pre automatické vypĺňanie prihlasovacích údajov.</value>
|
||||
@@ -753,7 +753,7 @@
|
||||
<value>Stav</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillServiceAlert2" xml:space="preserve">
|
||||
<value>The easiest way to add new logins to your vault is from the Bitwarden Auto-fill Service. Learn more about using the Bitwarden Auto-fill Service by navigating to the "Settings" screen.</value>
|
||||
<value>Najjednoduchší spôsob, ako pridať nové prihlasovacie údaje do trezora, je z bitwarden služby automatického vypĺňania. Ďalšie informácie o používaní bitwarden automatického vypĺňania získate prechodom do obrazovky Nastavení.</value>
|
||||
</data>
|
||||
<data name="Autofill" xml:space="preserve">
|
||||
<value>Automatické dopĺňanie</value>
|
||||
@@ -1302,7 +1302,7 @@
|
||||
<value>Automatické vypĺňanie hesiel</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillAlert2" xml:space="preserve">
|
||||
<value>The easiest way to add new logins to your vault is by using the Bitwarden Password AutoFill extension. Learn more about using the Bitwarden Password AutoFill extension by navigating to the "Settings" screen.</value>
|
||||
<value>Najjednoduchší spôsob, ako pridať nové prihlasovacie údaje do trezora, je z Bitwarden rozšírenia pre automatické vypĺňanie. Ďalšie informácie o používaní rozšírenia pre automatické dopĺňanie získate prechodom na obrazovku Nastavení.</value>
|
||||
</data>
|
||||
<data name="InvalidEmail" xml:space="preserve">
|
||||
<value>Neplatná emailová adresa.</value>
|
||||
@@ -1352,7 +1352,7 @@
|
||||
<value>Typy</value>
|
||||
</data>
|
||||
<data name="NoPasswordsToList" xml:space="preserve">
|
||||
<value>No passwords to list.</value>
|
||||
<value>Nenašli sa žiadne heslá.</value>
|
||||
</data>
|
||||
<data name="NoItemsToList" xml:space="preserve">
|
||||
<value>Neexistujú žiadne položky na zobrazenie.</value>
|
||||
@@ -1388,7 +1388,7 @@
|
||||
<value>Neexistujú žiadne zbierky na zobrazenie.</value>
|
||||
</data>
|
||||
<data name="ItemShared" xml:space="preserve">
|
||||
<value>Item has been shared.</value>
|
||||
<value>Položka bola zdieľaná.</value>
|
||||
</data>
|
||||
<data name="SelectOneCollection" xml:space="preserve">
|
||||
<value>Musíte vybrať aspoň jednu zbierku.</value>
|
||||
@@ -1400,7 +1400,7 @@
|
||||
<value>Zdieľať položku</value>
|
||||
</data>
|
||||
<data name="NoOrgsToList" xml:space="preserve">
|
||||
<value>No organizations to list.</value>
|
||||
<value>Nenašli sa žiadne organizácie.</value>
|
||||
</data>
|
||||
<data name="ShareDesc" xml:space="preserve">
|
||||
<value>Vyberte organizáciu s ktorou chcete zdieľať túto položku. Zdieľanie presunie vlastníctvo položky na organizáciu. Po zdieľaní už nebudete priamy vlastník položky.</value>
|
||||
@@ -1458,7 +1458,7 @@
|
||||
<value>Nastaviť kód PIN na odomknutie Bitwarden. Nastavenie PIN sa vynuluje, ak úplne odhlásite z aplikácie.</value>
|
||||
</data>
|
||||
<data name="LoggedInAsOn" xml:space="preserve">
|
||||
<value>Logged in as {0} on {1}.</value>
|
||||
<value>Prihlásený ako {0} na {1}.</value>
|
||||
<comment>ex: Logged in as user@example.com on bitwarden.com.</comment>
|
||||
</data>
|
||||
<data name="VaultLockedMasterPassword" xml:space="preserve">
|
||||
@@ -1539,40 +1539,52 @@
|
||||
<comment>The color black</comment>
|
||||
</data>
|
||||
<data name="BlacklistedUris" xml:space="preserve">
|
||||
<value>Blacklisted URIs</value>
|
||||
<value>Zakázané URI</value>
|
||||
</data>
|
||||
<data name="BlacklistedUrisDescription" xml:space="preserve">
|
||||
<value>URIs that are blacklisted will not offer auto-fill. The list of apps should be comma separated. Ex: "https://twitter.com, androidapp://com.twitter.android".</value>
|
||||
</data>
|
||||
<data name="DisableSavePrompt" xml:space="preserve">
|
||||
<value>Disable Save Prompt</value>
|
||||
<value>Nepýtať sa na ukládanie</value>
|
||||
</data>
|
||||
<data name="DisableSavePromptDescription" xml:space="preserve">
|
||||
<value>The "Save Prompt" automatically prompts you to save new items to your vault whenever you enter them for the first time.</value>
|
||||
<value>"Notifikácia Pridať prihlasovacie údaje" vás automaticky upozorní na uloženie nových údajov do vášho trezoru vždy, keď sa s nimi prihlasujete po prvý krát.</value>
|
||||
</data>
|
||||
<data name="LockOptionOnRestart" xml:space="preserve">
|
||||
<value>On App Restart</value>
|
||||
<value>Po reštarte aplikácie</value>
|
||||
</data>
|
||||
<data name="AutofillServiceNotEnabled" xml:space="preserve">
|
||||
<value>Auto-fill makes it easy to securely access your Bitwarden vault from other websites and apps. It looks like you have not enabled an auto-fill service for Bitwarden. Enable auto-fill for Bitwarden from the "Settings" screen.</value>
|
||||
<value>Automatické dopĺňanie umožňuje jednoducho pristupovať k vašemu trezoru z iných stránok a plaikácii. Vyzerá že ste službu automatického dopĺňania pomocou Bitwarden nezapli. Zapnúť ju môžete na obrazove "Nastavenia".</value>
|
||||
</data>
|
||||
<data name="ThemeAppliedOnRestart" xml:space="preserve">
|
||||
<value>Your theme changes will apply when the app is restarted.</value>
|
||||
<value>Zmena nastavení vzhľadu sa prejaví po opätovnom štarte aplikácie.</value>
|
||||
</data>
|
||||
<data name="Capitalize" xml:space="preserve">
|
||||
<value>Capitalize</value>
|
||||
<value>Prvé písmeno veľkým</value>
|
||||
<comment>ex. Uppercase the first character of a word.</comment>
|
||||
</data>
|
||||
<data name="IncludeNumber" xml:space="preserve">
|
||||
<value>Include Number</value>
|
||||
<value>Zahrnúť číslo</value>
|
||||
</data>
|
||||
<data name="Download" xml:space="preserve">
|
||||
<value>Download</value>
|
||||
<value>Stiahnuť</value>
|
||||
</data>
|
||||
<data name="Shared" xml:space="preserve">
|
||||
<value>Shared</value>
|
||||
<value>Zdieľané</value>
|
||||
</data>
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1575,4 +1575,16 @@
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1575,4 +1575,16 @@
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1575,4 +1575,16 @@
|
||||
<data name="ToggleVisibility" xml:space="preserve">
|
||||
<value>Toggle Visiblity</value>
|
||||
</data>
|
||||
<data name="LoginExpired" xml:space="preserve">
|
||||
<value>Your login session has expired.</value>
|
||||
</data>
|
||||
<data name="BiometricsDirection" xml:space="preserve">
|
||||
<value>Use biometrics to verify.</value>
|
||||
</data>
|
||||
<data name="Biometrics" xml:space="preserve">
|
||||
<value>Biometrics</value>
|
||||
</data>
|
||||
<data name="UseBiometricsToUnlock" xml:space="preserve">
|
||||
<value>Use Biometrics To Unlock</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
using Bit.App.Resources;
|
||||
using Bit.Core.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
@@ -18,6 +19,7 @@ namespace Bit.App.Services
|
||||
private readonly CultureInfo _defaultCulture = new CultureInfo("en-US");
|
||||
private bool _inited;
|
||||
private StringComparer _stringComparer;
|
||||
private Dictionary<string, string> _localeNames;
|
||||
|
||||
public MobileI18nService(CultureInfo systemCulture)
|
||||
{
|
||||
@@ -36,6 +38,57 @@ namespace Bit.App.Services
|
||||
return _stringComparer;
|
||||
}
|
||||
}
|
||||
public Dictionary<string, string> LocaleNames
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_localeNames == null)
|
||||
{
|
||||
_localeNames = new Dictionary<string, string>
|
||||
{
|
||||
["af"] = "Afrikaans",
|
||||
["bg"] = "български",
|
||||
["ca"] = "català",
|
||||
["cs"] = "čeština",
|
||||
["da"] = "dansk",
|
||||
["de"] = "Deutsch",
|
||||
["el"] = "Ελληνικά",
|
||||
["en"] = "English",
|
||||
["en-GB"] = "English (British)",
|
||||
["eo"] = "Esperanto",
|
||||
["es"] = "español",
|
||||
["et"] = "eesti",
|
||||
["fa"] = "فارسی",
|
||||
["fi"] = "suomi",
|
||||
["fr"] = "français",
|
||||
["he"] = "עברית",
|
||||
["hi"] = "हिन्दी",
|
||||
["hr"] = "hrvatski",
|
||||
["hu"] = "magyar",
|
||||
["id"] = "Bahasa Indonesia",
|
||||
["it"] = "italiano",
|
||||
["ja"] = "日本語",
|
||||
["ko"] = "한국어",
|
||||
["nb"] = "norsk (bokmål)",
|
||||
["nl"] = "Nederlands",
|
||||
["pl"] = "polski",
|
||||
["pt-BR"] = "português do Brasil",
|
||||
["pt-PT"] = "português",
|
||||
["ro"] = "română",
|
||||
["ru"] = "русский",
|
||||
["sk"] = "slovenčina",
|
||||
["sv"] = "svenska",
|
||||
["th"] = "ไทย",
|
||||
["tr"] = "Türkçe",
|
||||
["uk"] = "українська",
|
||||
["vi"] = "Tiếng Việt",
|
||||
["zh-CN"] = "中文(中国大陆)",
|
||||
["zh-TW"] = "中文(台灣)"
|
||||
};
|
||||
}
|
||||
return _localeNames;
|
||||
}
|
||||
}
|
||||
|
||||
public void Init(CultureInfo culture = null)
|
||||
{
|
||||
|
||||
@@ -197,45 +197,45 @@ namespace Bit.App.Services
|
||||
return await Clipboard.GetTextAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> SupportsFingerprintAsync()
|
||||
public async Task<bool> SupportsBiometricAsync()
|
||||
{
|
||||
try
|
||||
return await _deviceActionService.BiometricAvailableAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
|
||||
Action fallback = null)
|
||||
{
|
||||
if(_deviceActionService.UseNativeBiometric())
|
||||
{
|
||||
return await CrossFingerprint.Current.IsAvailableAsync();
|
||||
return await _deviceActionService.AuthenticateBiometricAsync(text);
|
||||
}
|
||||
catch
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if(text == null)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
|
||||
}
|
||||
var fingerprintRequest = new AuthenticationRequestConfiguration(text)
|
||||
{
|
||||
CancelTitle = AppResources.Cancel,
|
||||
FallbackTitle = fallbackText
|
||||
};
|
||||
var result = await CrossFingerprint.Current.AuthenticateAsync(fingerprintRequest);
|
||||
if(result.Authenticated)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
|
||||
{
|
||||
fallback?.Invoke();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AuthenticateFingerprintAsync(string text = null, string fallbackText = null,
|
||||
Action fallback = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(text == null)
|
||||
{
|
||||
text = _deviceActionService.SupportsFaceId() ? AppResources.FaceIDDirection :
|
||||
AppResources.FingerprintDirection;
|
||||
}
|
||||
var fingerprintRequest = new AuthenticationRequestConfiguration(text)
|
||||
{
|
||||
CancelTitle = AppResources.Cancel,
|
||||
FallbackTitle = fallbackText
|
||||
};
|
||||
var result = await CrossFingerprint.Current.AuthenticateAsync(fingerprintRequest);
|
||||
if(result.Authenticated)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
|
||||
{
|
||||
fallback?.Invoke();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
<Setter Property="Margin"
|
||||
Value="-4, 0, -4, -4" />
|
||||
</Style>
|
||||
<Style TargetType="SearchBar">
|
||||
<Style TargetType="SearchBar"
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="Transparent" />
|
||||
<Setter Property="TextColor"
|
||||
@@ -111,7 +112,7 @@
|
||||
Class="list-icon-platform"
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="FontSize"
|
||||
Value="10" />
|
||||
Value="23" />
|
||||
</Style>
|
||||
<Style TargetType="Button"
|
||||
ApplyToDerivedTypes="True"
|
||||
@@ -138,6 +139,8 @@
|
||||
Class="box-row-button-platform">
|
||||
<Setter Property="WidthRequest"
|
||||
Value="37" />
|
||||
<Setter Property="HeightRequest"
|
||||
Value="45" />
|
||||
<Setter Property="FontSize"
|
||||
Value="25" />
|
||||
</Style>
|
||||
|
||||
@@ -126,9 +126,9 @@
|
||||
<Style TargetType="Label"
|
||||
Class="list-header">
|
||||
<Setter Property="HorizontalOptions"
|
||||
Value="StartAndExpand" />
|
||||
Value="FillAndExpand" />
|
||||
<Setter Property="VerticalOptions"
|
||||
Value="CenterAndExpand" />
|
||||
Value="FillAndExpand" />
|
||||
<Setter Property="VerticalTextAlignment"
|
||||
Value="Center" />
|
||||
</Style>
|
||||
|
||||
@@ -35,11 +35,12 @@
|
||||
<Setter Property="OnColor"
|
||||
Value="{StaticResource SwitchOnColor}" />
|
||||
</Style>
|
||||
<Style TargetType="SearchBar">
|
||||
<Style TargetType="SearchBar"
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="BackgroundColor"
|
||||
Value="{StaticResource ListHeaderBackgroundColor}" />
|
||||
<Setter Property="TextColor"
|
||||
Value="Black" />
|
||||
Value="{StaticResource TextColor}" />
|
||||
<Setter Property="CancelButtonColor"
|
||||
Value="{StaticResource PrimaryColor}" />
|
||||
<Setter Property="PlaceholderColor"
|
||||
|
||||
@@ -124,23 +124,20 @@ namespace Bit.App.Utilities
|
||||
{
|
||||
var currentBuild = deviceActionService.GetBuildNumber();
|
||||
var lastBuild = await storageService.GetAsync<string>(Constants.LastBuildKey);
|
||||
if(!Migration.MigrationHelpers.NeedsMigration())
|
||||
if(lastBuild == null)
|
||||
{
|
||||
if(lastBuild == null)
|
||||
// Installed
|
||||
var currentLock = await storageService.GetAsync<int?>(Constants.LockOptionKey);
|
||||
if(currentLock == null)
|
||||
{
|
||||
// Installed
|
||||
var currentLock = await storageService.GetAsync<int?>(Constants.LockOptionKey);
|
||||
if(currentLock == null)
|
||||
{
|
||||
await storageService.SaveAsync(Constants.LockOptionKey, 15);
|
||||
}
|
||||
}
|
||||
else if(lastBuild != currentBuild)
|
||||
{
|
||||
// Updated
|
||||
var tasks = Task.Run(() => syncService.FullSyncAsync(true));
|
||||
await storageService.SaveAsync(Constants.LockOptionKey, 15);
|
||||
}
|
||||
}
|
||||
else if(lastBuild != currentBuild)
|
||||
{
|
||||
// Updated
|
||||
var tasks = Task.Run(() => syncService.FullSyncAsync(true));
|
||||
}
|
||||
if(lastBuild != currentBuild)
|
||||
{
|
||||
await storageService.SaveAsync(Constants.LastBuildKey, currentBuild);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
using Bit.App.Services;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Services;
|
||||
using Bit.App.Styles;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Utilities;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Utilities
|
||||
{
|
||||
public static class ThemeManager
|
||||
{
|
||||
public static bool UsingLightTheme = true;
|
||||
|
||||
public static void SetThemeStyle(string name)
|
||||
{
|
||||
// Reset styles
|
||||
@@ -20,18 +24,36 @@ namespace Bit.App.Utilities
|
||||
if(name == "dark")
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new Dark());
|
||||
UsingLightTheme = false;
|
||||
}
|
||||
else if(name == "black")
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new Black());
|
||||
UsingLightTheme = false;
|
||||
}
|
||||
else if(name == "nord")
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new Nord());
|
||||
UsingLightTheme = false;
|
||||
}
|
||||
else if(name == "light")
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new Light());
|
||||
UsingLightTheme = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new Light());
|
||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService", true);
|
||||
if(deviceActionService?.UsingDarkTheme() ?? false)
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new Dark());
|
||||
UsingLightTheme = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(new Light());
|
||||
UsingLightTheme = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Base styles
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Bit.Core.Abstractions
|
||||
Task<CipherString> EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null);
|
||||
Task<CipherString> EncryptAsync(string plainValue, SymmetricCryptoKey key = null);
|
||||
Task<byte[]> EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null);
|
||||
Task<SymmetricCryptoKey> GetEncKeyAsync();
|
||||
Task<SymmetricCryptoKey> GetEncKeyAsync(SymmetricCryptoKey key = null);
|
||||
Task<List<string>> GetFingerprintAsync(string userId, byte[] publicKey = null);
|
||||
Task<SymmetricCryptoKey> GetKeyAsync();
|
||||
Task<string> GetKeyHashAsync();
|
||||
@@ -35,7 +35,8 @@ namespace Bit.Core.Abstractions
|
||||
Task<bool> HasKeyAsync();
|
||||
Task<Tuple<SymmetricCryptoKey, CipherString>> MakeEncKeyAsync(SymmetricCryptoKey key);
|
||||
Task<SymmetricCryptoKey> MakeKeyAsync(string password, string salt, KdfType? kdf, int? kdfIterations);
|
||||
Task<SymmetricCryptoKey> MakeKeyFromPinAsync(string pin, string salt, KdfType kdf, int kdfIterations);
|
||||
Task<SymmetricCryptoKey> MakeKeyFromPinAsync(string pin, string salt, KdfType kdf, int kdfIterations,
|
||||
CipherString protectedKeyCs = null);
|
||||
Task<Tuple<string, CipherString>> MakeKeyPairAsync(SymmetricCryptoKey key = null);
|
||||
Task<SymmetricCryptoKey> MakePinKeyAysnc(string pin, string salt, KdfType kdf, int kdfIterations);
|
||||
Task<Tuple<CipherString, SymmetricCryptoKey>> MakeShareKeyAsync();
|
||||
@@ -49,4 +50,4 @@ namespace Bit.Core.Abstractions
|
||||
Task SetOrgKeysAsync(IEnumerable<ProfileOrganizationResponse> orgs);
|
||||
Task ToggleKeyAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
@@ -7,7 +8,8 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
CultureInfo Culture { get; set; }
|
||||
StringComparer StringComparer { get; }
|
||||
Dictionary<string, string> LocaleNames { get; }
|
||||
string T(string id, string p1 = null, string p2 = null, string p3 = null);
|
||||
string Translate(string id, string p1 = null, string p2 = null, string p3 = null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface ILockService
|
||||
{
|
||||
bool PinLocked { get; set; }
|
||||
CipherString PinProtectedKey { get; set; }
|
||||
bool FingerprintLocked { get; set; }
|
||||
|
||||
Task CheckLockAsync();
|
||||
@@ -16,4 +17,4 @@ namespace Bit.Core.Abstractions
|
||||
Task LockAsync(bool allowSoftLock = false, bool userInitiated = false);
|
||||
Task SetLockOptionAsync(int? lockOption);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Bit.Core.Abstractions
|
||||
void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null);
|
||||
bool SupportsU2f();
|
||||
bool SupportsDuo();
|
||||
Task<bool> SupportsFingerprintAsync();
|
||||
Task<bool> AuthenticateFingerprintAsync(string text = null, string fallbackText = null, Action fallback = null);
|
||||
Task<bool> SupportsBiometricAsync();
|
||||
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
bool SyncInProgress { get; set; }
|
||||
|
||||
Task<bool> FullSyncAsync(bool forceSync);
|
||||
Task<bool> FullSyncAsync(bool forceSync, bool allowThrowOnError = false);
|
||||
Task<DateTime?> GetLastSyncAsync();
|
||||
Task SetLastSyncAsync(DateTime date);
|
||||
Task<bool> SyncDeleteCipherAsync(SyncCipherNotification notification);
|
||||
@@ -16,4 +16,4 @@ namespace Bit.Core.Abstractions
|
||||
Task<bool> SyncUpsertCipherAsync(SyncCipherNotification notification, bool isEdit);
|
||||
Task<bool> SyncUpsertFolderAsync(SyncFolderNotification notification, bool isEdit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LiteDB" Version="4.1.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="PCLCrypto" Version="2.0.147" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace Bit.Core.Models.View
|
||||
_subTitle = Brand;
|
||||
if(Number != null && Number.Length >= 4)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(_subTitle))
|
||||
if(!string.IsNullOrWhiteSpace(_subTitle))
|
||||
{
|
||||
_subTitle += ", ";
|
||||
}
|
||||
|
||||
@@ -25,18 +25,22 @@ namespace Bit.Core.Services
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly Func<bool, Task> _logoutCallbackAsync;
|
||||
private string _deviceType;
|
||||
|
||||
public ApiService(
|
||||
ITokenService tokenService,
|
||||
IPlatformUtilsService platformUtilsService,
|
||||
Func<bool, Task> logoutCallbackAsync)
|
||||
Func<bool, Task> logoutCallbackAsync,
|
||||
string customUserAgent = null)
|
||||
{
|
||||
_tokenService = tokenService;
|
||||
_platformUtilsService = platformUtilsService;
|
||||
_logoutCallbackAsync = logoutCallbackAsync;
|
||||
var device = _platformUtilsService.GetDevice();
|
||||
_deviceType = device.ToString();
|
||||
var device = (int)_platformUtilsService.GetDevice();
|
||||
_httpClient.DefaultRequestHeaders.Add("Device-Type", device.ToString());
|
||||
if(!string.IsNullOrWhiteSpace(customUserAgent))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(customUserAgent);
|
||||
}
|
||||
}
|
||||
|
||||
public bool UrlsSet { get; private set; }
|
||||
@@ -81,21 +85,21 @@ namespace Bit.Core.Services
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Version = new Version(1, 0),
|
||||
RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")),
|
||||
Method = HttpMethod.Post,
|
||||
Content = new FormUrlEncodedContent(request.ToIdentityToken(_platformUtilsService.IdentityClientId))
|
||||
};
|
||||
requestMessage.Headers.Add("Accept", "application/json");
|
||||
requestMessage.Headers.Add("Device-Type", _deviceType);
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ApiException(HandleWebError());
|
||||
throw new ApiException(HandleWebError(e));
|
||||
}
|
||||
JObject responseJObject = null;
|
||||
if(IsJsonResponse(response))
|
||||
@@ -302,9 +306,9 @@ namespace Bit.Core.Services
|
||||
{
|
||||
using(var requestMessage = new HttpRequestMessage())
|
||||
{
|
||||
requestMessage.Version = new Version(1, 0);
|
||||
requestMessage.Method = HttpMethod.Post;
|
||||
requestMessage.RequestUri = new Uri(string.Concat(EventsBaseUrl, "/collect"));
|
||||
requestMessage.Headers.Add("Device-Type", _deviceType);
|
||||
var authHeader = await GetActiveBearerTokenAsync();
|
||||
requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader));
|
||||
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(request, _jsonSettings),
|
||||
@@ -314,9 +318,9 @@ namespace Bit.Core.Services
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ApiException(HandleWebError());
|
||||
throw new ApiException(HandleWebError(e));
|
||||
}
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -356,6 +360,7 @@ namespace Bit.Core.Services
|
||||
{
|
||||
using(var requestMessage = new HttpRequestMessage())
|
||||
{
|
||||
requestMessage.Version = new Version(1, 0);
|
||||
requestMessage.Method = method;
|
||||
requestMessage.RequestUri = new Uri(string.Concat(ApiBaseUrl, path));
|
||||
if(body != null)
|
||||
@@ -377,7 +382,6 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
requestMessage.Headers.Add("Device-Type", _deviceType);
|
||||
if(authed)
|
||||
{
|
||||
var authHeader = await GetActiveBearerTokenAsync();
|
||||
@@ -393,9 +397,9 @@ namespace Bit.Core.Services
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ApiException(HandleWebError());
|
||||
throw new ApiException(HandleWebError(e));
|
||||
}
|
||||
if(hasResponse && response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -422,6 +426,7 @@ namespace Bit.Core.Services
|
||||
var decodedToken = _tokenService.DecodeToken();
|
||||
var requestMessage = new HttpRequestMessage
|
||||
{
|
||||
Version = new Version(1, 0),
|
||||
RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")),
|
||||
Method = HttpMethod.Post,
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
@@ -432,16 +437,15 @@ namespace Bit.Core.Services
|
||||
})
|
||||
};
|
||||
requestMessage.Headers.Add("Accept", "application/json");
|
||||
requestMessage.Headers.Add("Device-Type", _deviceType);
|
||||
|
||||
HttpResponseMessage response;
|
||||
try
|
||||
{
|
||||
response = await _httpClient.SendAsync(requestMessage);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ApiException(HandleWebError());
|
||||
throw new ApiException(HandleWebError(e));
|
||||
}
|
||||
if(response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -457,12 +461,12 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
private ErrorResponse HandleWebError()
|
||||
private ErrorResponse HandleWebError(Exception e)
|
||||
{
|
||||
return new ErrorResponse
|
||||
{
|
||||
StatusCode = HttpStatusCode.BadGateway,
|
||||
Message = "There is a problem connecting to the server."
|
||||
Message = "Exception message: " + e.Message
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Bit.Core.Services
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly ILockService _lockService;
|
||||
private readonly bool _setCryptoKeys;
|
||||
|
||||
private SymmetricCryptoKey _key;
|
||||
@@ -34,6 +35,7 @@ namespace Bit.Core.Services
|
||||
II18nService i18nService,
|
||||
IPlatformUtilsService platformUtilsService,
|
||||
IMessagingService messagingService,
|
||||
ILockService lockService,
|
||||
bool setCryptoKeys = true)
|
||||
{
|
||||
_cryptoService = cryptoService;
|
||||
@@ -44,6 +46,7 @@ namespace Bit.Core.Services
|
||||
_i18nService = i18nService;
|
||||
_platformUtilsService = platformUtilsService;
|
||||
_messagingService = messagingService;
|
||||
_lockService = lockService;
|
||||
_setCryptoKeys = setCryptoKeys;
|
||||
|
||||
TwoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>();
|
||||
@@ -312,6 +315,7 @@ namespace Bit.Core.Services
|
||||
await _cryptoService.SetEncPrivateKeyAsync(tokenResponse.PrivateKey);
|
||||
}
|
||||
|
||||
_lockService.FingerprintLocked = false;
|
||||
_messagingService.Send("loggedIn");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ namespace Bit.Core.Services
|
||||
return _keyHash;
|
||||
}
|
||||
|
||||
public Task<SymmetricCryptoKey> GetEncKeyAsync()
|
||||
public Task<SymmetricCryptoKey> GetEncKeyAsync(SymmetricCryptoKey key = null)
|
||||
{
|
||||
if(_encKey != null)
|
||||
{
|
||||
@@ -138,7 +138,10 @@ namespace Bit.Core.Services
|
||||
return null;
|
||||
}
|
||||
|
||||
var key = await GetKeyAsync();
|
||||
if(key == null)
|
||||
{
|
||||
key = await GetKeyAsync();
|
||||
}
|
||||
if(key == null)
|
||||
{
|
||||
return null;
|
||||
@@ -386,14 +389,17 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
public async Task<SymmetricCryptoKey> MakeKeyFromPinAsync(string pin, string salt,
|
||||
KdfType kdf, int kdfIterations)
|
||||
KdfType kdf, int kdfIterations, CipherString protectedKeyCs = null)
|
||||
{
|
||||
var pinProtectedKey = await _storageService.GetAsync<string>(Constants.PinProtectedKey);
|
||||
if(pinProtectedKey == null)
|
||||
if(protectedKeyCs == null)
|
||||
{
|
||||
throw new Exception("No PIN protected key found.");
|
||||
var pinProtectedKey = await _storageService.GetAsync<string>(Constants.PinProtectedKey);
|
||||
if(pinProtectedKey == null)
|
||||
{
|
||||
throw new Exception("No PIN protected key found.");
|
||||
}
|
||||
protectedKeyCs = new CipherString(pinProtectedKey);
|
||||
}
|
||||
var protectedKeyCs = new CipherString(pinProtectedKey);
|
||||
var pinKey = await MakePinKeyAysnc(pin, salt, kdf, kdfIterations);
|
||||
var decKey = await DecryptToBytesAsync(protectedKeyCs, pinKey);
|
||||
return new SymmetricCryptoKey(decKey);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user