[PM-1817] Expand biometric integrity checks to the account level (#2498)

* Change bio integrity validation to work at account-level

* biometric state migration

* fix account bio valid key storage location during migration

* comment clarification

* fix for iOS extensions not using custom avatar color
This commit is contained in:
mp-bw 2023-05-01 09:47:00 -04:00 committed by GitHub
parent 4f0238122b
commit 0f417b8434
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 236 additions and 102 deletions

View file

@ -21,6 +21,7 @@ using Bit.App.Utilities;
using Bit.App.Pages; using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement; using Bit.App.Utilities.AccountManagement;
using Bit.App.Controls; using Bit.App.Controls;
using Bit.Core.Enums;
#if !FDROID #if !FDROID
using Android.Gms.Security; using Android.Gms.Security;
#endif #endif
@ -147,7 +148,7 @@ namespace Bit.Droid
var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage); var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage);
var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService); var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService);
var stateMigrationService = var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService); new StateMigrationService(DeviceType.Android, liteDbStorage, preferencesStorage, secureStorageService);
var clipboardService = new ClipboardService(stateService); var clipboardService = new ClipboardService(stateService);
var deviceActionService = new DeviceActionService(stateService, messagingService); var deviceActionService = new DeviceActionService(stateService, messagingService);
var fileService = new FileService(stateService, broadcasterService); var fileService = new FileService(stateService, broadcasterService);
@ -155,7 +156,7 @@ namespace Bit.Droid
messagingService, broadcasterService); messagingService, broadcasterService);
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService, var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService,
platformUtilsService, new LazyResolve<IEventService>()); platformUtilsService, new LazyResolve<IEventService>());
var biometricService = new BiometricService(); var biometricService = new BiometricService(stateService);
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService); var cryptoService = new CryptoService(stateService, cryptoFunctionService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);

View file

@ -4,7 +4,6 @@ using Android.OS;
using Android.Security.Keystore; using Android.Security.Keystore;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Java.Security; using Java.Security;
using Javax.Crypto; using Javax.Crypto;
@ -12,6 +11,8 @@ namespace Bit.Droid.Services
{ {
public class BiometricService : IBiometricService public class BiometricService : IBiometricService
{ {
private readonly IStateService _stateService;
private const string KeyName = "com.8bit.bitwarden.biometric_integrity"; private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
private const string KeyStoreName = "AndroidKeyStore"; private const string KeyStoreName = "AndroidKeyStore";
@ -23,28 +24,28 @@ namespace Bit.Droid.Services
private readonly KeyStore _keystore; private readonly KeyStore _keystore;
public BiometricService() public BiometricService(IStateService stateService)
{ {
_stateService = stateService;
_keystore = KeyStore.GetInstance(KeyStoreName); _keystore = KeyStore.GetInstance(KeyStoreName);
_keystore.Load(null); _keystore.Load(null);
} }
public Task<bool> SetupBiometricAsync(string bioIntegrityKey = null) public async Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null)
{ {
// bioIntegrityKey used in iOS only
if (Build.VERSION.SdkInt >= BuildVersionCodes.M) if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
{ {
CreateKey(); await CreateKeyAsync(bioIntegritySrcKey);
} }
return Task.FromResult(true); return true;
} }
public Task<bool> ValidateIntegrityAsync(string bioIntegrityKey = null) public async Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
{ {
if (Build.VERSION.SdkInt < BuildVersionCodes.M) if (Build.VERSION.SdkInt < BuildVersionCodes.M)
{ {
return Task.FromResult(true); return true;
} }
try try
@ -55,7 +56,7 @@ namespace Bit.Droid.Services
if (key == null || cipher == null) if (key == null || cipher == null)
{ {
return Task.FromResult(true); return true;
} }
cipher.Init(CipherMode.EncryptMode, key); cipher.Init(CipherMode.EncryptMode, key);
@ -63,25 +64,32 @@ namespace Bit.Droid.Services
catch (KeyPermanentlyInvalidatedException e) catch (KeyPermanentlyInvalidatedException e)
{ {
// Biometric has changed // Biometric has changed
return Task.FromResult(false); await ClearStateAsync(bioIntegritySrcKey);
return false;
} }
catch (UnrecoverableKeyException e) catch (UnrecoverableKeyException e)
{ {
// Biometric was disabled and re-enabled // Biometric was disabled and re-enabled
return Task.FromResult(false); await ClearStateAsync(bioIntegritySrcKey);
return false;
} }
catch (InvalidKeyException e) catch (InvalidKeyException e)
{ {
// Fallback for old bitwarden users without a key // Fallback for old bitwarden users without a key
LoggerHelper.LogEvenIfCantBeResolved(e); LoggerHelper.LogEvenIfCantBeResolved(e);
CreateKey(); await CreateKeyAsync(bioIntegritySrcKey);
} }
return Task.FromResult(true); return true;
} }
private void CreateKey() private async Task CreateKeyAsync(string bioIntegritySrcKey = null)
{ {
bioIntegritySrcKey ??= Core.Constants.BiometricIntegritySourceKey;
await _stateService.SetSystemBiometricIntegrityState(bioIntegritySrcKey,
await GetStateAsync(bioIntegritySrcKey));
await _stateService.SetAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
try try
{ {
var keyGen = KeyGenerator.GetInstance(KeyAlgorithm, KeyStoreName); var keyGen = KeyGenerator.GetInstance(KeyAlgorithm, KeyStoreName);
@ -101,5 +109,16 @@ namespace Bit.Droid.Services
LoggerHelper.LogEvenIfCantBeResolved(e); LoggerHelper.LogEvenIfCantBeResolved(e);
} }
} }
private async Task<string> GetStateAsync(string bioIntegritySrcKey)
{
return await _stateService.GetSystemBiometricIntegrityState(bioIntegritySrcKey) ??
Guid.NewGuid().ToString();
}
private async Task ClearStateAsync(string bioIntegritySrcKey)
{
await _stateService.SetSystemBiometricIntegrityState(bioIntegritySrcKey, null);
}
} }
} }

View file

@ -137,7 +137,7 @@
</StackLayout> </StackLayout>
<StackLayout Padding="10, 0"> <StackLayout Padding="10, 0">
<Label <Label
Text="{u:I18n BiometricInvalidated}" Text="{u:I18n AccountBiometricInvalidated}"
StyleClass="box-footer-label,text-danger,text-bold" StyleClass="box-footer-label,text-danger,text-bold"
IsVisible="{Binding BiometricIntegrityValid, Converter={StaticResource inverseBool}}" /> IsVisible="{Binding BiometricIntegrityValid, Converter={StaticResource inverseBool}}" />
<Button Text="{Binding BiometricButtonText}" Clicked="Biometric_Clicked" <Button Text="{Binding BiometricButtonText}" Clicked="Biometric_Clicked"

View file

@ -209,7 +209,7 @@ namespace Bit.App.Pages
if (BiometricLock) if (BiometricLock)
{ {
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync(); BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
if (!_biometricIntegrityValid) if (!_biometricIntegrityValid)
{ {
BiometricButtonVisible = false; BiometricButtonVisible = false;
@ -431,7 +431,8 @@ namespace Bit.App.Pages
public async Task PromptBiometricAsync() public async Task PromptBiometricAsync()
{ {
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync(); BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync();
BiometricButtonVisible = BiometricIntegrityValid;
if (!BiometricLock || !BiometricIntegrityValid) if (!BiometricLock || !BiometricIntegrityValid)
{ {
return; return;

View file

@ -970,18 +970,18 @@ namespace Bit.App.Resources {
/// <summary> /// <summary>
/// Looks up a localized string similar to Biometric unlock disabled pending verification of master password.. /// Looks up a localized string similar to Biometric unlock disabled pending verification of master password..
/// </summary> /// </summary>
public static string BiometricInvalidated { public static string AccountBiometricInvalidated {
get { get {
return ResourceManager.GetString("BiometricInvalidated", resourceCulture); return ResourceManager.GetString("AccountBiometricInvalidated", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Biometric unlock for autofill disabled pending verification of master password.. /// Looks up a localized string similar to Biometric unlock for autofill disabled pending verification of master password..
/// </summary> /// </summary>
public static string BiometricInvalidatedExtension { public static string AccountBiometricInvalidatedExtension {
get { get {
return ResourceManager.GetString("BiometricInvalidatedExtension", resourceCulture); return ResourceManager.GetString("AccountBiometricInvalidatedExtension", resourceCulture);
} }
} }

View file

@ -1752,11 +1752,11 @@ Scanning will happen automatically.</value>
<value>Do you really want to send to the trash?</value> <value>Do you really want to send to the trash?</value>
<comment>Confirmation alert message when soft-deleting a cipher.</comment> <comment>Confirmation alert message when soft-deleting a cipher.</comment>
</data> </data>
<data name="BiometricInvalidated" xml:space="preserve"> <data name="AccountBiometricInvalidated" xml:space="preserve">
<value>Biometric unlock disabled pending verification of master password.</value> <value>Biometric unlock for this account is disabled pending verification of master password.</value>
</data> </data>
<data name="BiometricInvalidatedExtension" xml:space="preserve"> <data name="AccountBiometricInvalidatedExtension" xml:space="preserve">
<value>Biometric unlock for autofill disabled pending verification of master password.</value> <value>Autofill biometric unlock for this account is disabled pending verification of master password.</value>
</data> </data>
<data name="EnableSyncOnRefresh" xml:space="preserve"> <data name="EnableSyncOnRefresh" xml:space="preserve">
<value>Allow sync on refresh</value> <value>Allow sync on refresh</value>

View file

@ -6,6 +6,7 @@ using Bit.App.Models;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities;
using Plugin.Fingerprint; using Plugin.Fingerprint;
using Plugin.Fingerprint.Abstractions; using Plugin.Fingerprint.Abstractions;
using Xamarin.Essentials; using Xamarin.Essentials;
@ -226,6 +227,20 @@ namespace Bit.App.Services
} }
} }
public async Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
{
bioIntegritySrcKey ??= Core.Constants.BiometricIntegritySourceKey;
var biometricService = ServiceContainer.Resolve<IBiometricService>();
if (!await biometricService.IsSystemBiometricIntegrityValidAsync(bioIntegritySrcKey))
{
return false;
}
var stateService = ServiceContainer.Resolve<IStateService>();
return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
}
public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
Action fallback = null) Action fallback = null)
{ {

View file

@ -22,14 +22,14 @@ namespace Bit.App.Services
Constants.PushRegisteredTokenKey, Constants.PushRegisteredTokenKey,
Constants.LastBuildKey, Constants.LastBuildKey,
Constants.ClearCiphersCacheKey, Constants.ClearCiphersCacheKey,
Constants.BiometricIntegrityKey, Constants.BiometricIntegritySourceKey,
Constants.iOSExtensionActiveUserIdKey, Constants.iOSExtensionActiveUserIdKey,
Constants.iOSAutoFillClearCiphersCacheKey, Constants.iOSAutoFillClearCiphersCacheKey,
Constants.iOSAutoFillBiometricIntegrityKey, Constants.iOSAutoFillBiometricIntegritySourceKey,
Constants.iOSExtensionClearCiphersCacheKey, Constants.iOSExtensionClearCiphersCacheKey,
Constants.iOSExtensionBiometricIntegrityKey, Constants.iOSExtensionBiometricIntegritySourceKey,
Constants.iOSShareExtensionClearCiphersCacheKey, Constants.iOSShareExtensionClearCiphersCacheKey,
Constants.iOSShareExtensionBiometricIntegrityKey, Constants.iOSShareExtensionBiometricIntegritySourceKey,
Constants.RememberedEmailKey, Constants.RememberedEmailKey,
Constants.RememberedOrgIdentifierKey Constants.RememberedOrgIdentifierKey
}; };

View file

@ -4,7 +4,7 @@ namespace Bit.Core.Abstractions
{ {
public interface IBiometricService public interface IBiometricService
{ {
Task<bool> SetupBiometricAsync(string bioIntegrityKey = null); Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null);
Task<bool> ValidateIntegrityAsync(string bioIntegrityKey = null); Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
} }
} }

View file

@ -28,6 +28,7 @@ namespace Bit.Core.Abstractions
bool SupportsFido2(); bool SupportsFido2();
bool SupportsDuo(); bool SupportsDuo();
Task<bool> SupportsBiometricAsync(); Task<bool> SupportsBiometricAsync();
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null); Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null);
long GetActiveTime(); long GetActiveTime();
} }

View file

@ -31,6 +31,10 @@ namespace Bit.Core.Abstractions
Task SetBiometricUnlockAsync(bool? value, string userId = null); Task SetBiometricUnlockAsync(bool? value, string userId = null);
Task<bool> GetBiometricLockedAsync(string userId = null); Task<bool> GetBiometricLockedAsync(string userId = null);
Task SetBiometricLockedAsync(bool value, string userId = null); Task SetBiometricLockedAsync(bool value, string userId = null);
Task<string> GetSystemBiometricIntegrityState(string bioIntegritySrcKey);
Task SetSystemBiometricIntegrityState(string bioIntegritySrcKey, string systemBioIntegrityState);
Task<bool> IsAccountBiometricIntegrityValidAsync(string bioIntegritySrcKey, string userId = null);
Task SetAccountBiometricIntegrityValidAsync(string bioIntegritySrcKey, string userId = null);
Task<bool> CanAccessPremiumAsync(string userId = null); Task<bool> CanAccessPremiumAsync(string userId = null);
Task SetPersonalPremiumAsync(bool value, string userId = null); Task SetPersonalPremiumAsync(bool value, string userId = null);
Task<string> GetProtectedPinAsync(string userId = null); Task<string> GetProtectedPinAsync(string userId = null);

View file

@ -18,13 +18,13 @@
public const string LastBuildKey = "lastBuild"; public const string LastBuildKey = "lastBuild";
public const string AddSitePromptShownKey = "addSitePromptShown"; public const string AddSitePromptShownKey = "addSitePromptShown";
public const string ClearCiphersCacheKey = "clearCiphersCache"; public const string ClearCiphersCacheKey = "clearCiphersCache";
public const string BiometricIntegrityKey = "biometricIntegrityState"; public const string BiometricIntegritySourceKey = "biometricIntegritySource";
public const string iOSAutoFillClearCiphersCacheKey = "iOSAutoFillClearCiphersCache"; public const string iOSAutoFillClearCiphersCacheKey = "iOSAutoFillClearCiphersCache";
public const string iOSAutoFillBiometricIntegrityKey = "iOSAutoFillBiometricIntegrityState"; public const string iOSAutoFillBiometricIntegritySourceKey = "iOSAutoFillBiometricIntegritySource";
public const string iOSExtensionClearCiphersCacheKey = "iOSExtensionClearCiphersCache"; public const string iOSExtensionClearCiphersCacheKey = "iOSExtensionClearCiphersCache";
public const string iOSExtensionBiometricIntegrityKey = "iOSExtensionBiometricIntegrityState"; public const string iOSExtensionBiometricIntegritySourceKey = "iOSExtensionBiometricIntegritySource";
public const string iOSShareExtensionClearCiphersCacheKey = "iOSShareExtensionClearCiphersCache"; public const string iOSShareExtensionClearCiphersCacheKey = "iOSShareExtensionClearCiphersCache";
public const string iOSShareExtensionBiometricIntegrityKey = "iOSShareExtensionBiometricIntegrityState"; public const string iOSShareExtensionBiometricIntegritySourceKey = "iOSShareExtensionBiometricIntegritySource";
public const string iOSExtensionActiveUserIdKey = "iOSExtensionActiveUserId"; public const string iOSExtensionActiveUserIdKey = "iOSExtensionActiveUserId";
public const string EventCollectionKey = "eventCollection"; public const string EventCollectionKey = "eventCollection";
public const string RememberedEmailKey = "rememberedEmail"; public const string RememberedEmailKey = "rememberedEmail";
@ -110,6 +110,8 @@
public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}"; public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}";
public static string LastSyncKey(string userId) => $"lastSync_{userId}"; public static string LastSyncKey(string userId) => $"lastSync_{userId}";
public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}"; public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}";
public static string AccountBiometricIntegrityValidKey(string userId, string systemBioIntegrityState) =>
$"accountBiometricIntegrityValid_{userId}_{systemBioIntegrityState}";
public static string ApprovePasswordlessLoginsKey(string userId) => $"approvePasswordlessLogins_{userId}"; public static string ApprovePasswordlessLoginsKey(string userId) => $"approvePasswordlessLogins_{userId}";
public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}"; public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}";
public static string PushLastRegistrationDateKey(string userId) => $"pushLastRegistrationDate_{userId}"; public static string PushLastRegistrationDateKey(string userId) => $"pushLastRegistrationDate_{userId}";

View file

@ -13,8 +13,9 @@ namespace Bit.Core.Services
{ {
public class StateMigrationService : IStateMigrationService public class StateMigrationService : IStateMigrationService
{ {
private const int StateVersion = 4; private const int StateVersion = 5;
private readonly DeviceType _deviceType;
private readonly IStorageService _preferencesStorageService; private readonly IStorageService _preferencesStorageService;
private readonly IStorageService _liteDbStorageService; private readonly IStorageService _liteDbStorageService;
private readonly IStorageService _secureStorageService; private readonly IStorageService _secureStorageService;
@ -27,9 +28,10 @@ namespace Bit.Core.Services
Secure, Secure,
} }
public StateMigrationService(IStorageService liteDbStorageService, IStorageService preferenceStorageService, public StateMigrationService(DeviceType deviceType, IStorageService liteDbStorageService,
IStorageService secureStorageService) IStorageService preferenceStorageService, IStorageService secureStorageService)
{ {
_deviceType = deviceType;
_liteDbStorageService = liteDbStorageService; _liteDbStorageService = liteDbStorageService;
_preferencesStorageService = preferenceStorageService; _preferencesStorageService = preferenceStorageService;
_secureStorageService = secureStorageService; _secureStorageService = secureStorageService;
@ -79,6 +81,9 @@ namespace Bit.Core.Services
case 3: case 3:
await MigrateFrom3To4Async(); await MigrateFrom3To4Async();
break; break;
case 4:
await MigrateFrom4To5Async();
break;
} }
} }
@ -442,6 +447,10 @@ namespace Bit.Core.Services
// Removal of old state data will happen organically as state is rebuilt in app // Removal of old state data will happen organically as state is rebuilt in app
} }
#endregion
#region v4 to v5 Migration
private class V4Keys private class V4Keys
{ {
internal static string VaultTimeoutKey(string userId) => $"vaultTimeout_{userId}"; internal static string VaultTimeoutKey(string userId) => $"vaultTimeout_{userId}";
@ -450,6 +459,87 @@ namespace Bit.Core.Services
internal const string ThemeKey = "theme"; internal const string ThemeKey = "theme";
internal const string AutoDarkThemeKey = "autoDarkTheme"; internal const string AutoDarkThemeKey = "autoDarkTheme";
internal const string DisableFaviconKey = "disableFavicon"; internal const string DisableFaviconKey = "disableFavicon";
internal const string BiometricIntegrityKey = "biometricIntegrityState";
internal const string iOSAutoFillBiometricIntegrityKey = "iOSAutoFillBiometricIntegrityState";
internal const string iOSExtensionBiometricIntegrityKey = "iOSExtensionBiometricIntegrityState";
internal const string iOSShareExtensionBiometricIntegrityKey = "iOSShareExtensionBiometricIntegrityState";
}
private async Task MigrateFrom4To5Async()
{
var bioIntegrityState = await GetValueAsync<string>(Storage.Prefs, V4Keys.BiometricIntegrityKey);
var iOSAutofillBioIntegrityState =
await GetValueAsync<string>(Storage.Prefs, V4Keys.iOSAutoFillBiometricIntegrityKey);
var iOSExtensionBioIntegrityState =
await GetValueAsync<string>(Storage.Prefs, V4Keys.iOSExtensionBiometricIntegrityKey);
var iOSShareExtensionBioIntegrityState =
await GetValueAsync<string>(Storage.Prefs, V4Keys.iOSShareExtensionBiometricIntegrityKey);
if (_deviceType == DeviceType.Android && string.IsNullOrWhiteSpace(bioIntegrityState))
{
bioIntegrityState = Guid.NewGuid().ToString();
}
await SetValueAsync(Storage.Prefs, V5Keys.BiometricIntegritySourceKey, bioIntegrityState);
if (_deviceType == DeviceType.iOS)
{
await SetValueAsync(Storage.Prefs, V5Keys.iOSAutoFillBiometricIntegritySourceKey,
iOSAutofillBioIntegrityState);
await SetValueAsync(Storage.Prefs, V5Keys.iOSExtensionBiometricIntegritySourceKey,
iOSExtensionBioIntegrityState);
await SetValueAsync(Storage.Prefs, V5Keys.iOSShareExtensionBiometricIntegritySourceKey,
iOSShareExtensionBioIntegrityState);
}
var state = await GetValueAsync<State>(Storage.LiteDb, Constants.StateKey);
if (state?.Accounts is null)
{
// No accounts available, update stored version and exit
await SetLastStateVersionAsync(5);
return;
}
// build integrity keys for existing users
foreach (var account in state.Accounts.Where(a => a.Value?.Profile?.UserId != null))
{
var userId = account.Value.Profile.UserId;
await SetValueAsync(Storage.LiteDb,
V5Keys.AccountBiometricIntegrityValidKey(userId, bioIntegrityState), true);
if (_deviceType == DeviceType.iOS)
{
await SetValueAsync(Storage.LiteDb,
V5Keys.AccountBiometricIntegrityValidKey(userId, iOSAutofillBioIntegrityState), true);
await SetValueAsync(Storage.LiteDb,
V5Keys.AccountBiometricIntegrityValidKey(userId, iOSExtensionBioIntegrityState), true);
await SetValueAsync(Storage.LiteDb,
V5Keys.AccountBiometricIntegrityValidKey(userId, iOSShareExtensionBioIntegrityState), true);
}
}
// Update stored version
await SetLastStateVersionAsync(5);
// Remove old data
await RemoveValueAsync(Storage.Prefs, V4Keys.BiometricIntegrityKey);
await RemoveValueAsync(Storage.Prefs, V4Keys.iOSAutoFillBiometricIntegrityKey);
await RemoveValueAsync(Storage.Prefs, V4Keys.iOSExtensionBiometricIntegrityKey);
await RemoveValueAsync(Storage.Prefs, V4Keys.iOSShareExtensionBiometricIntegrityKey);
}
private class V5Keys
{
internal const string BiometricIntegritySourceKey = "biometricIntegritySource";
internal const string iOSAutoFillBiometricIntegritySourceKey = "iOSAutoFillBiometricIntegritySource";
internal const string iOSExtensionBiometricIntegritySourceKey = "iOSExtensionBiometricIntegritySource";
internal const string iOSShareExtensionBiometricIntegritySourceKey =
"iOSShareExtensionBiometricIntegritySource";
internal static string AccountBiometricIntegrityValidKey(string userId, string systemBioIntegrityState) =>
$"accountBiometricIntegrityValid_{userId}_{systemBioIntegrityState}";
} }
#endregion #endregion

View file

@ -272,6 +272,36 @@ namespace Bit.Core.Services
await SaveAccountAsync(account, reconciledOptions); await SaveAccountAsync(account, reconciledOptions);
} }
public async Task<string> GetSystemBiometricIntegrityState(string bioIntegritySrcKey)
{
return await GetValueAsync<string>(bioIntegritySrcKey, await GetDefaultStorageOptionsAsync());
}
public async Task SetSystemBiometricIntegrityState(string bioIntegritySrcKey, string systemBioIntegrityState)
{
await SetValueAsync(bioIntegritySrcKey, systemBioIntegrityState, await GetDefaultStorageOptionsAsync());
}
public async Task<bool> IsAccountBiometricIntegrityValidAsync(string bioIntegritySrcKey, string userId = null)
{
var systemBioIntegrityState = await GetSystemBiometricIntegrityState(bioIntegritySrcKey);
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
await GetDefaultStorageOptionsAsync());
return await GetValueAsync<bool?>(
Constants.AccountBiometricIntegrityValidKey(reconciledOptions.UserId, systemBioIntegrityState),
reconciledOptions) ?? false;
}
public async Task SetAccountBiometricIntegrityValidAsync(string bioIntegritySrcKey, string userId = null)
{
var systemBioIntegrityState = await GetSystemBiometricIntegrityState(bioIntegritySrcKey);
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
await GetDefaultStorageOptionsAsync());
await SetValueAsync(
Constants.AccountBiometricIntegrityValidKey(reconciledOptions.UserId, systemBioIntegrityState),
true, reconciledOptions);
}
public async Task<bool> CanAccessPremiumAsync(string userId = null) public async Task<bool> CanAccessPremiumAsync(string userId = null)
{ {
if (userId == null) if (userId == null)

View file

@ -15,7 +15,7 @@ namespace Bit.iOS.Autofill
public LockPasswordViewController(IntPtr handle) public LockPasswordViewController(IntPtr handle)
: base(handle) : base(handle)
{ {
BiometricIntegrityKey = Bit.Core.Constants.iOSAutoFillBiometricIntegrityKey; BiometricIntegritySourceKey = Bit.Core.Constants.iOSAutoFillBiometricIntegritySourceKey;
DismissModalAction = Cancel; DismissModalAction = Cancel;
autofillExtension = true; autofillExtension = true;
} }

View file

@ -56,7 +56,7 @@ namespace Bit.iOS.Core.Controllers
public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell( public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell(
AppResources.MasterPassword, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One); AppResources.MasterPassword, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One);
public string BiometricIntegrityKey { get; set; } public string BiometricIntegritySourceKey { get; set; }
public UITableViewCell BiometricCell public UITableViewCell BiometricCell
{ {
@ -77,7 +77,7 @@ namespace Bit.iOS.Core.Controllers
cell.TextLabel.Font = ThemeHelpers.GetDangerFont(); cell.TextLabel.Font = ThemeHelpers.GetDangerFont();
cell.TextLabel.Lines = 0; cell.TextLabel.Lines = 0;
cell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap; cell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap;
cell.TextLabel.Text = AppResources.BiometricInvalidatedExtension; cell.TextLabel.Text = AppResources.AccountBiometricInvalidatedExtension;
} }
return cell; return cell;
} }
@ -114,7 +114,8 @@ namespace Bit.iOS.Core.Controllers
_isPinProtectedWithKey; _isPinProtectedWithKey;
_biometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && _biometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() &&
await _cryptoService.HasKeyAsync(); await _cryptoService.HasKeyAsync();
_biometricIntegrityValid = await _biometricService.ValidateIntegrityAsync(BiometricIntegrityKey); _biometricIntegrityValid =
await _platformUtilsService.IsBiometricIntegrityValidAsync(BiometricIntegritySourceKey);
_usesKeyConnector = await _keyConnectorService.GetUsesKeyConnector(); _usesKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
_biometricUnlockOnly = _usesKeyConnector && _biometricLock && !_pinLock; _biometricUnlockOnly = _usesKeyConnector && _biometricLock && !_pinLock;
} }
@ -371,7 +372,7 @@ namespace Bit.iOS.Core.Controllers
// Re-enable biometrics if initial use // Re-enable biometrics if initial use
if (_biometricLock & !_biometricIntegrityValid) if (_biometricLock & !_biometricIntegrityValid)
{ {
await _biometricService.SetupBiometricAsync(BiometricIntegrityKey); await _biometricService.SetupBiometricAsync(BiometricIntegritySourceKey);
} }
} }

View file

@ -53,7 +53,7 @@ namespace Bit.iOS.Core.Controllers
public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell( public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell(
AppResources.MasterPassword, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One); AppResources.MasterPassword, buttonsConfig: FormEntryTableViewCell.ButtonsConfig.One);
public string BiometricIntegrityKey { get; set; } public string BiometricIntegritySourceKey { get; set; }
public UITableViewCell BiometricCell public UITableViewCell BiometricCell
{ {
@ -74,7 +74,7 @@ namespace Bit.iOS.Core.Controllers
cell.TextLabel.Font = ThemeHelpers.GetDangerFont(); cell.TextLabel.Font = ThemeHelpers.GetDangerFont();
cell.TextLabel.Lines = 0; cell.TextLabel.Lines = 0;
cell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap; cell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap;
cell.TextLabel.Text = AppResources.BiometricInvalidatedExtension; cell.TextLabel.Text = AppResources.AccountBiometricInvalidatedExtension;
} }
return cell; return cell;
} }
@ -108,7 +108,8 @@ namespace Bit.iOS.Core.Controllers
_isPinProtectedWithKey; _isPinProtectedWithKey;
_biometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() && _biometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() &&
await _cryptoService.HasKeyAsync(); await _cryptoService.HasKeyAsync();
_biometricIntegrityValid = await _biometricService.ValidateIntegrityAsync(BiometricIntegrityKey); _biometricIntegrityValid =
await _platformUtilsService.IsBiometricIntegrityValidAsync(BiometricIntegritySourceKey);
_usesKeyConnector = await _keyConnectorService.GetUsesKeyConnector(); _usesKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
_biometricUnlockOnly = _usesKeyConnector && _biometricLock && !_pinLock; _biometricUnlockOnly = _usesKeyConnector && _biometricLock && !_pinLock;
} }
@ -361,7 +362,7 @@ namespace Bit.iOS.Core.Controllers
// Re-enable biometrics if initial use // Re-enable biometrics if initial use
if (_biometricLock & !_biometricIntegrityValid) if (_biometricLock & !_biometricIntegrityValid)
{ {
await _biometricService.SetupBiometricAsync(BiometricIntegrityKey); await _biometricService.SetupBiometricAsync(BiometricIntegritySourceKey);
} }
} }

View file

@ -7,29 +7,30 @@ namespace Bit.iOS.Core.Services
{ {
public class BiometricService : IBiometricService public class BiometricService : IBiometricService
{ {
private IStorageService _storageService; private IStateService _stateService;
public BiometricService(IStorageService storageService) public BiometricService(IStateService stateService)
{ {
_storageService = storageService; _stateService = stateService;
} }
public async Task<bool> SetupBiometricAsync(string bioIntegrityKey = null) public async Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null)
{ {
if (bioIntegrityKey == null) if (bioIntegritySrcKey == null)
{ {
bioIntegrityKey = Bit.Core.Constants.BiometricIntegrityKey; bioIntegritySrcKey = Bit.Core.Constants.BiometricIntegritySourceKey;
} }
var state = GetState(); var state = GetState();
if (state != null) if (state != null)
{ {
await _storageService.SaveAsync(bioIntegrityKey, ToBase64(state)); await _stateService.SetSystemBiometricIntegrityState(bioIntegritySrcKey, ToBase64(state));
await _stateService.SetAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
} }
return true; return true;
} }
public async Task<bool> ValidateIntegrityAsync(string bioIntegrityKey = null) public async Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
{ {
var state = GetState(); var state = GetState();
if (state == null) if (state == null)
@ -38,18 +39,14 @@ namespace Bit.iOS.Core.Services
return true; return true;
} }
if (bioIntegrityKey == null) if (bioIntegritySrcKey == null)
{ {
bioIntegrityKey = Bit.Core.Constants.BiometricIntegrityKey; bioIntegritySrcKey = Bit.Core.Constants.BiometricIntegritySourceKey;
} }
var oldState = await _storageService.GetAsync<string>(bioIntegrityKey); var savedState = await _stateService.GetSystemBiometricIntegrityState(bioIntegritySrcKey);
if (oldState == null) if (savedState != null)
{ {
oldState = await GetMigratedIntegrityState(bioIntegrityKey); return FromBase64(savedState).Equals(state);
}
if (oldState != null)
{
return FromBase64(oldState).Equals(state);
} }
return false; return false;
} }
@ -72,35 +69,5 @@ namespace Bit.iOS.Core.Services
var bytes = System.Convert.FromBase64String(data); var bytes = System.Convert.FromBase64String(data);
return NSData.FromArray(bytes); return NSData.FromArray(bytes);
} }
private async Task<string> GetMigratedIntegrityState(string bioIntegrityKey)
{
var legacyKey = "biometricState";
if (bioIntegrityKey == Bit.Core.Constants.iOSAutoFillBiometricIntegrityKey)
{
legacyKey = "autofillBiometricState";
}
else if (bioIntegrityKey == Bit.Core.Constants.iOSExtensionBiometricIntegrityKey)
{
legacyKey = "extensionBiometricState";
}
// Original values are pulled from DB since the legacy keys were never defined in _preferenceStorageKeys
var integrityState = await _storageService.GetAsync<string>(legacyKey);
if (integrityState != null)
{
// Save original value to pref storage with new key
await _storageService.SaveAsync(bioIntegrityKey, integrityState);
// Remove value from DB storage with legacy key
await _storageService.RemoveAsync(legacyKey);
// Return value as if it was always in pref storage
return integrityState;
}
// Return null since the state was never set
return null;
}
} }
} }

View file

@ -34,7 +34,8 @@ namespace Bit.iOS.Core.Utilities
} }
var avatarImageSource = new AvatarImageSource(await _stateService.GetActiveUserIdAsync(), var avatarImageSource = new AvatarImageSource(await _stateService.GetActiveUserIdAsync(),
await _stateService.GetNameAsync(), await _stateService.GetEmailAsync()); await _stateService.GetNameAsync(), await _stateService.GetEmailAsync(),
await _stateService.GetAvatarColorAsync());
using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync()) using (var avatarUIImage = await avatarImageSource.GetNativeImageAsync())
{ {
return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE); return avatarUIImage?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) ?? UIImage.GetSystemImage(DEFAULT_SYSTEM_AVATAR_IMAGE);

View file

@ -10,6 +10,7 @@ using Bit.App.Services;
using Bit.App.Utilities; using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement; using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.iOS.Core.Services; using Bit.iOS.Core.Services;
@ -105,13 +106,13 @@ namespace Bit.iOS.Core.Utilities
var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage); var storageMediatorService = new StorageMediatorService(mobileStorageService, secureStorageService, preferencesStorage);
var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService); var stateService = new StateService(mobileStorageService, secureStorageService, storageMediatorService, messagingService);
var stateMigrationService = var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService); new StateMigrationService(DeviceType.iOS, liteDbStorage, preferencesStorage, secureStorageService);
var deviceActionService = new DeviceActionService(); var deviceActionService = new DeviceActionService();
var fileService = new FileService(stateService, messagingService); var fileService = new FileService(stateService, messagingService);
var clipboardService = new ClipboardService(stateService); var clipboardService = new ClipboardService(stateService);
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService, var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
messagingService, broadcasterService); messagingService, broadcasterService);
var biometricService = new BiometricService(mobileStorageService); var biometricService = new BiometricService(stateService);
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService); var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService); var cryptoService = new CryptoService(stateService, cryptoFunctionService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService); var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService);

View file

@ -9,7 +9,7 @@ namespace Bit.iOS.Extension
public LockPasswordViewController(IntPtr handle) public LockPasswordViewController(IntPtr handle)
: base(handle) : base(handle)
{ {
BiometricIntegrityKey = Bit.Core.Constants.iOSExtensionBiometricIntegrityKey; BiometricIntegritySourceKey = Bit.Core.Constants.iOSExtensionBiometricIntegritySourceKey;
DismissModalAction = Cancel; DismissModalAction = Cancel;
} }

View file

@ -13,14 +13,14 @@ namespace Bit.iOS.ShareExtension
public LockPasswordViewController() public LockPasswordViewController()
{ {
BiometricIntegrityKey = Bit.Core.Constants.iOSShareExtensionBiometricIntegrityKey; BiometricIntegritySourceKey = Bit.Core.Constants.iOSShareExtensionBiometricIntegritySourceKey;
DismissModalAction = Cancel; DismissModalAction = Cancel;
} }
public LockPasswordViewController(IntPtr handle) public LockPasswordViewController(IntPtr handle)
: base(handle) : base(handle)
{ {
BiometricIntegrityKey = Bit.Core.Constants.iOSShareExtensionBiometricIntegrityKey; BiometricIntegritySourceKey = Bit.Core.Constants.iOSShareExtensionBiometricIntegritySourceKey;
DismissModalAction = Cancel; DismissModalAction = Cancel;
} }