mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
[PM-6428] Implement Legacy Secure Storage (#3027)
This commit is contained in:
parent
d6c2ebe4c2
commit
04e7cfe06d
9 changed files with 555 additions and 9 deletions
|
@ -5,7 +5,8 @@ namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
public enum AwaiterPrecondition
|
public enum AwaiterPrecondition
|
||||||
{
|
{
|
||||||
EnvironmentUrlsInited
|
EnvironmentUrlsInited,
|
||||||
|
AndroidWindowCreated
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IConditionedAwaiterManager
|
public interface IConditionedAwaiterManager
|
||||||
|
|
|
@ -12,12 +12,14 @@ namespace Bit.App.Pages
|
||||||
private readonly HomeViewModel _vm;
|
private readonly HomeViewModel _vm;
|
||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
private IBroadcasterService _broadcasterService;
|
private IBroadcasterService _broadcasterService;
|
||||||
|
private IConditionedAwaiterManager _conditionedAwaiterManager;
|
||||||
|
|
||||||
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||||
|
|
||||||
public HomePage(AppOptions appOptions = null)
|
public HomePage(AppOptions appOptions = null)
|
||||||
{
|
{
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>();
|
||||||
|
_conditionedAwaiterManager = ServiceContainer.Resolve<IConditionedAwaiterManager>();
|
||||||
_appOptions = appOptions;
|
_appOptions = appOptions;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as HomeViewModel;
|
_vm = BindingContext as HomeViewModel;
|
||||||
|
@ -56,6 +58,8 @@ namespace Bit.App.Pages
|
||||||
PerformNavigationOnAccountChangedOnLoad = false;
|
PerformNavigationOnAccountChangedOnLoad = false;
|
||||||
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Pages;
|
namespace Bit.Core.Pages;
|
||||||
|
@ -6,15 +7,18 @@ namespace Bit.Core.Pages;
|
||||||
public partial class AndroidNavigationRedirectPage : ContentPage
|
public partial class AndroidNavigationRedirectPage : ContentPage
|
||||||
{
|
{
|
||||||
private readonly IAccountsManager _accountsManager;
|
private readonly IAccountsManager _accountsManager;
|
||||||
|
private readonly IConditionedAwaiterManager _conditionedAwaiterManager;
|
||||||
|
|
||||||
public AndroidNavigationRedirectPage()
|
public AndroidNavigationRedirectPage()
|
||||||
{
|
{
|
||||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||||
|
_conditionedAwaiterManager = ServiceContainer.Resolve<IConditionedAwaiterManager>();
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AndroidNavigationRedirectPage_OnLoaded(object sender, EventArgs e)
|
private void AndroidNavigationRedirectPage_OnLoaded(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
_accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
|
||||||
|
_conditionedAwaiterManager.SetAsCompleted(AwaiterPrecondition.AndroidWindowCreated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@ namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>> _preconditionsTasks = new ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>>
|
private readonly ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>> _preconditionsTasks = new ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>>
|
||||||
{
|
{
|
||||||
[AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource<bool>()
|
[AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource<bool>(),
|
||||||
|
[AwaiterPrecondition.AndroidWindowCreated] = new TaskCompletionSource<bool>()
|
||||||
};
|
};
|
||||||
|
|
||||||
public Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition)
|
public Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition)
|
||||||
|
|
295
src/Core/Services/LegacySecureStorage/AndroidKeyStore.cs
Normal file
295
src/Core/Services/LegacySecureStorage/AndroidKeyStore.cs
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
#if ANDROID
|
||||||
|
|
||||||
|
using Android.Content;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Runtime;
|
||||||
|
using Android.Security;
|
||||||
|
using Android.Security.Keystore;
|
||||||
|
using Java.Security;
|
||||||
|
using Javax.Crypto;
|
||||||
|
using Javax.Crypto.Spec;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
class AndroidKeyStore
|
||||||
|
{
|
||||||
|
const string androidKeyStore = "AndroidKeyStore"; // this is an Android const value
|
||||||
|
const string aesAlgorithm = "AES";
|
||||||
|
const string cipherTransformationAsymmetric = "RSA/ECB/PKCS1Padding";
|
||||||
|
const string cipherTransformationSymmetric = "AES/GCM/NoPadding";
|
||||||
|
const string prefsMasterKey = "SecureStorageKey";
|
||||||
|
const int initializationVectorLen = 12; // Android supports an IV of 12 for AES/GCM
|
||||||
|
|
||||||
|
internal AndroidKeyStore(Context context, string keystoreAlias, bool alwaysUseAsymmetricKeyStorage)
|
||||||
|
{
|
||||||
|
alwaysUseAsymmetricKey = alwaysUseAsymmetricKeyStorage;
|
||||||
|
appContext = context;
|
||||||
|
alias = keystoreAlias;
|
||||||
|
|
||||||
|
keyStore = KeyStore.GetInstance(androidKeyStore);
|
||||||
|
keyStore.Load(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly Context appContext;
|
||||||
|
readonly string alias;
|
||||||
|
readonly bool alwaysUseAsymmetricKey;
|
||||||
|
readonly string useSymmetricPreferenceKey = "essentials_use_symmetric";
|
||||||
|
|
||||||
|
KeyStore keyStore;
|
||||||
|
bool useSymmetric = false;
|
||||||
|
|
||||||
|
ISecretKey GetKey()
|
||||||
|
{
|
||||||
|
// check to see if we need to get our key from past-versions or newer versions.
|
||||||
|
// we want to use symmetric if we are >= 23 or we didn't set it previously.
|
||||||
|
var hasApiLevel = Build.VERSION.SdkInt >= BuildVersionCodes.M;
|
||||||
|
|
||||||
|
useSymmetric = Preferences.Get(useSymmetricPreferenceKey, hasApiLevel, alias);
|
||||||
|
|
||||||
|
// If >= API 23 we can use the KeyStore's symmetric key
|
||||||
|
if (useSymmetric && !alwaysUseAsymmetricKey)
|
||||||
|
return GetSymmetricKey();
|
||||||
|
|
||||||
|
// NOTE: KeyStore in < API 23 can only store asymmetric keys
|
||||||
|
// specifically, only RSA/ECB/PKCS1Padding
|
||||||
|
// So we will wrap our symmetric AES key we just generated
|
||||||
|
// with this and save the encrypted/wrapped key out to
|
||||||
|
// preferences for future use.
|
||||||
|
// ECB should be fine in this case as the AES key should be
|
||||||
|
// contained in one block.
|
||||||
|
|
||||||
|
// Get the asymmetric key pair
|
||||||
|
var keyPair = GetAsymmetricKeyPair();
|
||||||
|
|
||||||
|
var existingKeyStr = Preferences.Get(prefsMasterKey, null, alias);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(existingKeyStr))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wrappedKey = Convert.FromBase64String(existingKeyStr);
|
||||||
|
|
||||||
|
var unwrappedKey = UnwrapKey(wrappedKey, keyPair.Private);
|
||||||
|
var kp = unwrappedKey.JavaCast<ISecretKey>();
|
||||||
|
|
||||||
|
return kp;
|
||||||
|
}
|
||||||
|
catch (InvalidKeyException ikEx)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine(
|
||||||
|
$"Unable to unwrap key: Invalid Key. This may be caused by system backup or upgrades. All secure storage items will now be removed. {ikEx.Message}");
|
||||||
|
}
|
||||||
|
catch (IllegalBlockSizeException ibsEx)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine(
|
||||||
|
$"Unable to unwrap key: Illegal Block Size. This may be caused by system backup or upgrades. All secure storage items will now be removed. {ibsEx.Message}");
|
||||||
|
}
|
||||||
|
catch (BadPaddingException paddingEx)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine(
|
||||||
|
$"Unable to unwrap key: Bad Padding. This may be caused by system backup or upgrades. All secure storage items will now be removed. {paddingEx.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacySecureStorage.RemoveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyGenerator = KeyGenerator.GetInstance(aesAlgorithm);
|
||||||
|
var defSymmetricKey = keyGenerator.GenerateKey();
|
||||||
|
|
||||||
|
var newWrappedKey = WrapKey(defSymmetricKey, keyPair.Public);
|
||||||
|
|
||||||
|
Preferences.Set(prefsMasterKey, Convert.ToBase64String(newWrappedKey), alias);
|
||||||
|
|
||||||
|
return defSymmetricKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 23+ Only
|
||||||
|
#pragma warning disable CA1416
|
||||||
|
ISecretKey GetSymmetricKey()
|
||||||
|
{
|
||||||
|
Preferences.Set(useSymmetricPreferenceKey, true, alias);
|
||||||
|
|
||||||
|
var existingKey = keyStore.GetKey(alias, null);
|
||||||
|
|
||||||
|
if (existingKey != null)
|
||||||
|
{
|
||||||
|
var existingSecretKey = existingKey.JavaCast<ISecretKey>();
|
||||||
|
return existingSecretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyGenerator = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, androidKeyStore);
|
||||||
|
var builder = new KeyGenParameterSpec.Builder(alias, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
|
||||||
|
.SetBlockModes(KeyProperties.BlockModeGcm)
|
||||||
|
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone)
|
||||||
|
.SetRandomizedEncryptionRequired(false);
|
||||||
|
|
||||||
|
keyGenerator.Init(builder.Build());
|
||||||
|
|
||||||
|
return keyGenerator.GenerateKey();
|
||||||
|
}
|
||||||
|
#pragma warning restore CA1416
|
||||||
|
|
||||||
|
KeyPair GetAsymmetricKeyPair()
|
||||||
|
{
|
||||||
|
// set that we generated keys on pre-m device.
|
||||||
|
Preferences.Set(useSymmetricPreferenceKey, false, alias);
|
||||||
|
|
||||||
|
var asymmetricAlias = $"{alias}.asymmetric";
|
||||||
|
|
||||||
|
var privateKey = keyStore.GetKey(asymmetricAlias, null)?.JavaCast<IPrivateKey>();
|
||||||
|
var publicKey = keyStore.GetCertificate(asymmetricAlias)?.PublicKey;
|
||||||
|
|
||||||
|
// Return the existing key if found
|
||||||
|
if (privateKey != null && publicKey != null)
|
||||||
|
return new KeyPair(publicKey, privateKey);
|
||||||
|
|
||||||
|
var originalLocale = Java.Util.Locale.Default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Force to english for known bug in date parsing:
|
||||||
|
// https://issuetracker.google.com/issues/37095309
|
||||||
|
SetLocale(Java.Util.Locale.English);
|
||||||
|
|
||||||
|
// Otherwise we create a new key
|
||||||
|
#pragma warning disable CA1416
|
||||||
|
var generator = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, androidKeyStore);
|
||||||
|
#pragma warning restore CA1416
|
||||||
|
|
||||||
|
var end = DateTime.UtcNow.AddYears(20);
|
||||||
|
var startDate = new Java.Util.Date();
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
var endDate = new Java.Util.Date(end.Year, end.Month, end.Day);
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
|
#pragma warning disable CS0618
|
||||||
|
var builder = new KeyPairGeneratorSpec.Builder(Platform.AppContext)
|
||||||
|
.SetAlias(asymmetricAlias)
|
||||||
|
.SetSerialNumber(Java.Math.BigInteger.One)
|
||||||
|
.SetSubject(new Javax.Security.Auth.X500.X500Principal($"CN={asymmetricAlias} CA Certificate"))
|
||||||
|
.SetStartDate(startDate)
|
||||||
|
.SetEndDate(endDate);
|
||||||
|
|
||||||
|
generator.Initialize(builder.Build());
|
||||||
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
|
return generator.GenerateKeyPair();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetLocale(originalLocale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] WrapKey(IKey keyToWrap, IKey withKey)
|
||||||
|
{
|
||||||
|
var cipher = Cipher.GetInstance(cipherTransformationAsymmetric);
|
||||||
|
cipher.Init(CipherMode.WrapMode, withKey);
|
||||||
|
return cipher.Wrap(keyToWrap);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1416
|
||||||
|
IKey UnwrapKey(byte[] wrappedData, IKey withKey)
|
||||||
|
{
|
||||||
|
var cipher = Cipher.GetInstance(cipherTransformationAsymmetric);
|
||||||
|
cipher.Init(CipherMode.UnwrapMode, withKey);
|
||||||
|
var unwrapped = cipher.Unwrap(wrappedData, KeyProperties.KeyAlgorithmAes, KeyType.SecretKey);
|
||||||
|
return unwrapped;
|
||||||
|
}
|
||||||
|
#pragma warning restore CA1416
|
||||||
|
|
||||||
|
internal byte[] Encrypt(string data)
|
||||||
|
{
|
||||||
|
var key = GetKey();
|
||||||
|
|
||||||
|
// Generate initialization vector
|
||||||
|
var iv = new byte[initializationVectorLen];
|
||||||
|
|
||||||
|
var sr = new SecureRandom();
|
||||||
|
sr.NextBytes(iv);
|
||||||
|
|
||||||
|
Cipher cipher;
|
||||||
|
|
||||||
|
// Attempt to use GCMParameterSpec by default
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cipher = Cipher.GetInstance(cipherTransformationSymmetric);
|
||||||
|
cipher.Init(CipherMode.EncryptMode, key, new GCMParameterSpec(128, iv));
|
||||||
|
}
|
||||||
|
catch (InvalidAlgorithmParameterException)
|
||||||
|
{
|
||||||
|
// If we encounter this error, it's likely an old bouncycastle provider version
|
||||||
|
// is being used which does not recognize GCMParameterSpec, but should work
|
||||||
|
// with IvParameterSpec, however we only do this as a last effort since other
|
||||||
|
// implementations will error if you use IvParameterSpec when GCMParameterSpec
|
||||||
|
// is recognized and expected.
|
||||||
|
cipher = Cipher.GetInstance(cipherTransformationSymmetric);
|
||||||
|
cipher.Init(CipherMode.EncryptMode, key, new IvParameterSpec(iv));
|
||||||
|
}
|
||||||
|
|
||||||
|
var decryptedData = Encoding.UTF8.GetBytes(data);
|
||||||
|
var encryptedBytes = cipher.DoFinal(decryptedData);
|
||||||
|
|
||||||
|
// Combine the IV and the encrypted data into one array
|
||||||
|
var r = new byte[iv.Length + encryptedBytes.Length];
|
||||||
|
Buffer.BlockCopy(iv, 0, r, 0, iv.Length);
|
||||||
|
Buffer.BlockCopy(encryptedBytes, 0, r, iv.Length, encryptedBytes.Length);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal string Decrypt(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length < initializationVectorLen)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var key = GetKey();
|
||||||
|
|
||||||
|
// IV will be the first 16 bytes of the encrypted data
|
||||||
|
var iv = new byte[initializationVectorLen];
|
||||||
|
Buffer.BlockCopy(data, 0, iv, 0, initializationVectorLen);
|
||||||
|
|
||||||
|
Cipher cipher;
|
||||||
|
|
||||||
|
// Attempt to use GCMParameterSpec by default
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cipher = Cipher.GetInstance(cipherTransformationSymmetric);
|
||||||
|
cipher.Init(CipherMode.DecryptMode, key, new GCMParameterSpec(128, iv));
|
||||||
|
}
|
||||||
|
catch (InvalidAlgorithmParameterException)
|
||||||
|
{
|
||||||
|
// If we encounter this error, it's likely an old bouncycastle provider version
|
||||||
|
// is being used which does not recognize GCMParameterSpec, but should work
|
||||||
|
// with IvParameterSpec, however we only do this as a last effort since other
|
||||||
|
// implementations will error if you use IvParameterSpec when GCMParameterSpec
|
||||||
|
// is recognized and expected.
|
||||||
|
cipher = Cipher.GetInstance(cipherTransformationSymmetric);
|
||||||
|
cipher.Init(CipherMode.DecryptMode, key, new IvParameterSpec(iv));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt starting after the first 16 bytes from the IV
|
||||||
|
var decryptedData = cipher.DoFinal(data, initializationVectorLen, data.Length - initializationVectorLen);
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(decryptedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetLocale(Java.Util.Locale locale)
|
||||||
|
{
|
||||||
|
Java.Util.Locale.Default = locale;
|
||||||
|
var resources = appContext.Resources;
|
||||||
|
var config = resources.Configuration;
|
||||||
|
|
||||||
|
if (Build.VERSION.SdkInt >= BuildVersionCodes.N)
|
||||||
|
config.SetLocale(locale);
|
||||||
|
else
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
config.Locale = locale;
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
resources.UpdateConfiguration(config, resources.DisplayMetrics);
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
128
src/Core/Services/LegacySecureStorage/KeyChain.cs
Normal file
128
src/Core/Services/LegacySecureStorage/KeyChain.cs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
#if IOS
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Foundation;
|
||||||
|
using Security;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
internal class KeyChain
|
||||||
|
{
|
||||||
|
SecAccessible accessible;
|
||||||
|
|
||||||
|
internal KeyChain(SecAccessible accessible) =>
|
||||||
|
this.accessible = accessible;
|
||||||
|
|
||||||
|
SecRecord ExistingRecordForKey(string key, string service)
|
||||||
|
{
|
||||||
|
return new SecRecord(SecKind.GenericPassword)
|
||||||
|
{
|
||||||
|
Account = key,
|
||||||
|
Service = service
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal string ValueForKey(string key, string service)
|
||||||
|
{
|
||||||
|
using (var record = ExistingRecordForKey(key, service))
|
||||||
|
using (var match = SecKeyChain.QueryAsRecord(record, out var resultCode))
|
||||||
|
{
|
||||||
|
if (resultCode == SecStatusCode.Success)
|
||||||
|
return NSString.FromData(match.ValueData, NSStringEncoding.UTF8);
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetValueForKey(string value, string key, string service)
|
||||||
|
{
|
||||||
|
using (var record = ExistingRecordForKey(key, service))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(ValueForKey(key, service)))
|
||||||
|
RemoveRecord(record);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the key already exists, remove it
|
||||||
|
if (!string.IsNullOrEmpty(ValueForKey(key, service)))
|
||||||
|
RemoveRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var newRecord = CreateRecordForNewKeyValue(key, value, service))
|
||||||
|
{
|
||||||
|
var result = SecKeyChain.Add(newRecord);
|
||||||
|
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case SecStatusCode.DuplicateItem:
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Duplicate item found. Attempting to remove and add again.");
|
||||||
|
|
||||||
|
// try to remove and add again
|
||||||
|
if (Remove(key, service))
|
||||||
|
{
|
||||||
|
result = SecKeyChain.Add(newRecord);
|
||||||
|
if (result != SecStatusCode.Success)
|
||||||
|
throw new Exception($"Error adding record: {result}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Unable to remove key.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SecStatusCode.Success:
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new Exception($"Error adding record: {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Remove(string key, string service)
|
||||||
|
{
|
||||||
|
using (var record = ExistingRecordForKey(key, service))
|
||||||
|
using (var match = SecKeyChain.QueryAsRecord(record, out var resultCode))
|
||||||
|
{
|
||||||
|
if (resultCode == SecStatusCode.Success)
|
||||||
|
{
|
||||||
|
RemoveRecord(record);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RemoveAll(string service)
|
||||||
|
{
|
||||||
|
using (var query = new SecRecord(SecKind.GenericPassword) { Service = service })
|
||||||
|
{
|
||||||
|
SecKeyChain.Remove(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SecRecord CreateRecordForNewKeyValue(string key, string value, string service)
|
||||||
|
{
|
||||||
|
return new SecRecord(SecKind.GenericPassword)
|
||||||
|
{
|
||||||
|
Account = key,
|
||||||
|
Service = service,
|
||||||
|
Label = key,
|
||||||
|
Accessible = accessible,
|
||||||
|
ValueData = NSData.FromString(value, NSStringEncoding.UTF8),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RemoveRecord(SecRecord record)
|
||||||
|
{
|
||||||
|
var result = SecKeyChain.Remove(record);
|
||||||
|
if (result != SecStatusCode.Success && result != SecStatusCode.ItemNotFound)
|
||||||
|
throw new Exception($"Error removing record: {result}");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
108
src/Core/Services/LegacySecureStorage/LegacySecureStorage.cs
Normal file
108
src/Core/Services/LegacySecureStorage/LegacySecureStorage.cs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
#if IOS
|
||||||
|
using Security;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if ANDROID
|
||||||
|
using Javax.Crypto;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class LegacySecureStorage
|
||||||
|
{
|
||||||
|
internal static readonly string Alias = $"{AppInfo.PackageName}.xamarinessentials";
|
||||||
|
|
||||||
|
#if IOS
|
||||||
|
private static SecAccessible DefaultAccessible { get; set; } = SecAccessible.AfterFirstUnlock;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
public static Task<string?> GetAsync(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
|
||||||
|
#if ANDROID
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
object locker = new object();
|
||||||
|
string? encVal = Preferences.Get(key, null, Alias);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(encVal))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] encData = Convert.FromBase64String(encVal);
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
AndroidKeyStore keyStore = new AndroidKeyStore(Platform.AppContext, Alias, false);
|
||||||
|
return keyStore.Decrypt(encData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (AEADBadTagException)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Unable to decrypt key, {key}, which is likely due to an app uninstall. Removing old key and returning null.");
|
||||||
|
Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
#elif IOS
|
||||||
|
var keyChain = new KeyChain(DefaultAccessible);
|
||||||
|
return Task.FromResult<string?>(keyChain.ValueForKey(key, Alias));
|
||||||
|
#else
|
||||||
|
return Task.FromResult((string?)null);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task SetAsync(string key, string value)
|
||||||
|
{
|
||||||
|
#if ANDROID
|
||||||
|
return Task.Run(() =>
|
||||||
|
{
|
||||||
|
var context = Platform.AppContext;
|
||||||
|
|
||||||
|
byte[] encryptedData = null;
|
||||||
|
object locker = new object();
|
||||||
|
lock (locker)
|
||||||
|
{
|
||||||
|
AndroidKeyStore keyStore = new AndroidKeyStore(Platform.AppContext, Alias, false);
|
||||||
|
encryptedData = keyStore.Encrypt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var encStr = Convert.ToBase64String(encryptedData);
|
||||||
|
Preferences.Set(key, encStr, Alias);
|
||||||
|
});
|
||||||
|
#elif IOS
|
||||||
|
KeyChain keyChain = new KeyChain(DefaultAccessible);
|
||||||
|
keyChain.SetValueForKey(value, key, Alias);
|
||||||
|
#endif
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Remove(string key)
|
||||||
|
{
|
||||||
|
#if ANDROID
|
||||||
|
Preferences.Remove(key, Alias);
|
||||||
|
return true;
|
||||||
|
#elif IOS
|
||||||
|
var keyChain = new KeyChain(DefaultAccessible);
|
||||||
|
return keyChain.Remove(key, Alias);
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RemoveAll()
|
||||||
|
{
|
||||||
|
#if ANDROID
|
||||||
|
Preferences.Clear(Alias);
|
||||||
|
#elif IOS
|
||||||
|
var keyChain = new KeyChain(DefaultAccessible);
|
||||||
|
keyChain.RemoveAll(Alias);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
using System.Threading.Tasks;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Services;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ namespace Bit.App.Services
|
||||||
public async Task<T> GetAsync<T>(string key)
|
public async Task<T> GetAsync<T>(string key)
|
||||||
{
|
{
|
||||||
var formattedKey = string.Format(_keyFormat, key);
|
var formattedKey = string.Format(_keyFormat, key);
|
||||||
var val = await Microsoft.Maui.Storage.SecureStorage.GetAsync(formattedKey);
|
var val = await LegacySecureStorage.GetAsync(formattedKey);
|
||||||
if (typeof(T) == typeof(string))
|
if (typeof(T) == typeof(string))
|
||||||
{
|
{
|
||||||
return (T)(object)val;
|
return (T)(object)val;
|
||||||
|
@ -37,11 +37,11 @@ namespace Bit.App.Services
|
||||||
var formattedKey = string.Format(_keyFormat, key);
|
var formattedKey = string.Format(_keyFormat, key);
|
||||||
if (typeof(T) == typeof(string))
|
if (typeof(T) == typeof(string))
|
||||||
{
|
{
|
||||||
await Microsoft.Maui.Storage.SecureStorage.SetAsync(formattedKey, obj as string);
|
await LegacySecureStorage.SetAsync(formattedKey, obj as string);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Microsoft.Maui.Storage.SecureStorage.SetAsync(formattedKey,
|
await LegacySecureStorage.SetAsync(formattedKey,
|
||||||
JsonConvert.SerializeObject(obj, _jsonSettings));
|
JsonConvert.SerializeObject(obj, _jsonSettings));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ namespace Bit.App.Services
|
||||||
public Task RemoveAsync(string key)
|
public Task RemoveAsync(string key)
|
||||||
{
|
{
|
||||||
var formattedKey = string.Format(_keyFormat, key);
|
var formattedKey = string.Format(_keyFormat, key);
|
||||||
Microsoft.Maui.Storage.SecureStorage.Remove(formattedKey);
|
LegacySecureStorage.Remove(formattedKey);
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,9 @@ namespace Bit.App.Utilities.AccountManagement
|
||||||
public async Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction)
|
public async Task StartDefaultNavigationFlowAsync(Action<AppOptions> appOptionsAction)
|
||||||
{
|
{
|
||||||
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
|
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
|
||||||
|
#if ANDROID
|
||||||
|
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.AndroidWindowCreated);
|
||||||
|
#endif
|
||||||
appOptionsAction(Options);
|
appOptionsAction(Options);
|
||||||
|
|
||||||
await NavigateOnAccountChangeAsync();
|
await NavigateOnAccountChangeAsync();
|
||||||
|
@ -69,6 +71,9 @@ namespace Bit.App.Utilities.AccountManagement
|
||||||
public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null)
|
public async Task NavigateOnAccountChangeAsync(bool? isAuthed = null)
|
||||||
{
|
{
|
||||||
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
|
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.EnvironmentUrlsInited);
|
||||||
|
#if ANDROID
|
||||||
|
await _conditionedAwaiterManager.GetAwaiterForPrecondition(AwaiterPrecondition.AndroidWindowCreated);
|
||||||
|
#endif
|
||||||
|
|
||||||
// TODO: this could be improved by doing chain of responsability pattern
|
// TODO: this could be improved by doing chain of responsability pattern
|
||||||
// but for now it may be an overkill, if logic gets more complex consider refactoring it
|
// but for now it may be an overkill, if logic gets more complex consider refactoring it
|
||||||
|
|
Loading…
Reference in a new issue