mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-04 08:50:18 +01:00
Invalidate biometric on change (#1026)
* Initial working version for Android * Add a fallback for when upgrading from older app version. * Ensure biometric validity is re-checked on focus * Only setup biometric integrity key if biometric is turned on. * Fix styling according to comments * Fallback for Android 5. * Improve comment * Add boilerplate for iOS * Change BiometricService to public * Untested iOS implementation. * Convert IBiometricService to async. Fix code style for iOS. * Base64 NSData. * Review comments for Android BiometricService. * Rename methods in BiometricService to append Async * Ensure we wait for async SetupBiometricAsync. * Update BiometricService.cs Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
This commit is contained in:
parent
39de2c1d25
commit
ae28de4159
@ -133,6 +133,7 @@
|
||||
<Compile Include="MainActivity.cs" />
|
||||
<Compile Include="Resources\Resource.designer.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\BiometricService.cs" />
|
||||
<Compile Include="Services\CryptoPrimitiveService.cs" />
|
||||
<Compile Include="Services\DeviceActionService.cs" />
|
||||
<Compile Include="Services\LocalizeService.cs" />
|
||||
|
@ -98,6 +98,7 @@ namespace Bit.Droid
|
||||
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
var biometricService = new BiometricService();
|
||||
|
||||
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
|
||||
ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
|
||||
@ -108,6 +109,7 @@ namespace Bit.Droid
|
||||
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
|
||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||
|
||||
// Push
|
||||
#if FDROID
|
||||
|
90
src/Android/Services/BiometricService.cs
Normal file
90
src/Android/Services/BiometricService.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Android.OS;
|
||||
using Android.Security.Keystore;
|
||||
using Bit.Core.Abstractions;
|
||||
using Java.Security;
|
||||
using Javax.Crypto;
|
||||
|
||||
namespace Bit.Droid.Services
|
||||
{
|
||||
public class BiometricService : IBiometricService
|
||||
{
|
||||
private const string KeyName = "com.8bit.bitwarden.biometric_integrity";
|
||||
|
||||
private const string KeyStoreName = "AndroidKeyStore";
|
||||
|
||||
private const string KeyAlgorithm = KeyProperties.KeyAlgorithmAes;
|
||||
private const string BlockMode = KeyProperties.BlockModeCbc;
|
||||
private const string EncryptionPadding = KeyProperties.EncryptionPaddingPkcs7;
|
||||
private const string Transformation = KeyAlgorithm + "/" + BlockMode + "/" + EncryptionPadding;
|
||||
|
||||
private readonly KeyStore _keystore;
|
||||
|
||||
public BiometricService()
|
||||
{
|
||||
_keystore = KeyStore.GetInstance(KeyStoreName);
|
||||
_keystore.Load(null);
|
||||
}
|
||||
|
||||
public Task<bool> SetupBiometricAsync()
|
||||
{
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.M)
|
||||
{
|
||||
CreateKey();
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task<bool> ValidateIntegrityAsync()
|
||||
{
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.M)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
_keystore.Load(null);
|
||||
IKey key = _keystore.GetKey(KeyName, null);
|
||||
Cipher cipher = Cipher.GetInstance(Transformation);
|
||||
|
||||
try
|
||||
{
|
||||
cipher.Init(CipherMode.EncryptMode, key);
|
||||
}
|
||||
catch (KeyPermanentlyInvalidatedException e)
|
||||
{
|
||||
// Biometric has changed
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
catch (UnrecoverableKeyException e)
|
||||
{
|
||||
// Biometric was disabled and re-enabled
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
catch (InvalidKeyException e)
|
||||
{
|
||||
// Fallback for old bitwarden users without a key
|
||||
CreateKey();
|
||||
}
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
private void CreateKey()
|
||||
{
|
||||
KeyGenerator keyGen = KeyGenerator.GetInstance(KeyAlgorithm, KeyStoreName);
|
||||
KeyGenParameterSpec keyGenSpec =
|
||||
new KeyGenParameterSpec.Builder(KeyName, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
|
||||
.SetBlockModes(BlockMode)
|
||||
.SetEncryptionPaddings(EncryptionPadding)
|
||||
.SetUserAuthenticationRequired(true)
|
||||
.Build();
|
||||
keyGen.Init(keyGenSpec);
|
||||
keyGen.GenerateKey();
|
||||
}
|
||||
}
|
||||
}
|
@ -106,8 +106,12 @@
|
||||
Margin="0, 10, 0, 0" />
|
||||
</StackLayout>
|
||||
<StackLayout Padding="10, 0">
|
||||
<Label
|
||||
Text="{u:I18n BiometricInvalidated}"
|
||||
StyleClass="box-footer-label,text-danger,text-bold"
|
||||
IsVisible="{Binding BiometricIntegrityValid, Converter={StaticResource inverseBool}}" />
|
||||
<Button Text="{Binding BiometricButtonText}" Clicked="Biometric_Clicked"
|
||||
IsVisible="{Binding BiometricLock}"></Button>
|
||||
IsVisible="{Binding BiometricLock}" IsEnabled="{Binding BiometricIntegrityValid}"></Button>
|
||||
<Button Text="{u:I18n LogOut}" Clicked="LogOut_Clicked"></Button>
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
|
@ -24,11 +24,13 @@ namespace Bit.App.Pages
|
||||
private readonly IStorageService _secureStorageService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IBiometricService _biometricService;
|
||||
|
||||
private string _email;
|
||||
private bool _showPassword;
|
||||
private bool _pinLock;
|
||||
private bool _biometricLock;
|
||||
private bool _biometricIntegrityValid = true;
|
||||
private string _biometricButtonText;
|
||||
private string _loggedInAsText;
|
||||
private string _lockedVerifyText;
|
||||
@ -47,6 +49,7 @@ namespace Bit.App.Pages
|
||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
|
||||
PageTitle = AppResources.VerifyMasterPassword;
|
||||
TogglePasswordCommand = new Command(TogglePassword);
|
||||
@ -75,6 +78,12 @@ namespace Bit.App.Pages
|
||||
set => SetProperty(ref _biometricLock, value);
|
||||
}
|
||||
|
||||
public bool BiometricIntegrityValid
|
||||
{
|
||||
get => _biometricIntegrityValid;
|
||||
set => SetProperty(ref _biometricIntegrityValid, value);
|
||||
}
|
||||
|
||||
public string BiometricButtonText
|
||||
{
|
||||
get => _biometricButtonText;
|
||||
@ -133,7 +142,8 @@ namespace Bit.App.Pages
|
||||
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
|
||||
AppResources.UseFingerprintToUnlock;
|
||||
}
|
||||
if (autoPromptBiometric)
|
||||
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
|
||||
if (autoPromptBiometric & _biometricIntegrityValid)
|
||||
{
|
||||
var tasks = Task.Run(async () =>
|
||||
{
|
||||
@ -238,6 +248,12 @@ namespace Bit.App.Pages
|
||||
}
|
||||
MasterPassword = string.Empty;
|
||||
await SetKeyAndContinueAsync(key);
|
||||
|
||||
// Re-enable biometrics
|
||||
if (BiometricLock & !BiometricIntegrityValid)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -267,7 +283,8 @@ namespace Bit.App.Pages
|
||||
|
||||
public async Task PromptBiometricAsync()
|
||||
{
|
||||
if (!BiometricLock)
|
||||
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
|
||||
if (!BiometricLock || !BiometricIntegrityValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ namespace Bit.App.Pages
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
private readonly IStorageService _storageService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly IBiometricService _biometricService;
|
||||
|
||||
private bool _supportsBiometric;
|
||||
private bool _pin;
|
||||
@ -60,6 +61,7 @@ namespace Bit.App.Pages
|
||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
_storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
||||
|
||||
GroupedItems = new ExtendedObservableCollection<SettingsPageListGroup>();
|
||||
PageTitle = AppResources.Settings;
|
||||
@ -306,6 +308,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
if (_biometric)
|
||||
{
|
||||
await _biometricService.SetupBiometricAsync();
|
||||
await _storageService.SaveAsync(Constants.BiometricUnlockKey, true);
|
||||
}
|
||||
else
|
||||
|
@ -59,46 +59,46 @@
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string"/>
|
||||
<xsd:attribute name="type" type="xsd:string"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string"/>
|
||||
<xsd:attribute name="name" type="xsd:string"/>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
|
||||
<xsd:attribute ref="xml:space"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required"/>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
@ -1674,4 +1674,7 @@
|
||||
<value>Do you really want to send to the bin?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
<data name="BiometricInvalidated" xml:space="preserve">
|
||||
<value>Biometric change detected, login using Master Password to enable again.</value>
|
||||
</data>
|
||||
</root>
|
@ -1674,6 +1674,9 @@
|
||||
<value>Do you really want to send to the trash?</value>
|
||||
<comment>Confirmation alert message when soft-deleting a cipher.</comment>
|
||||
</data>
|
||||
<data name="BiometricInvalidated" xml:space="preserve">
|
||||
<value>Biometric change detected, login using Master Password to enable again.</value>
|
||||
</data>
|
||||
<data name="EnableSyncOnRefresh" xml:space="preserve">
|
||||
<value>Enable sync on refresh</value>
|
||||
</data>
|
||||
|
10
src/Core/Abstractions/IBiometricService.cs
Normal file
10
src/Core/Abstractions/IBiometricService.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IBiometricService
|
||||
{
|
||||
Task<bool> SetupBiometricAsync();
|
||||
Task<bool> ValidateIntegrityAsync();
|
||||
}
|
||||
}
|
62
src/iOS.Core/Services/BiometricService.cs
Normal file
62
src/iOS.Core/Services/BiometricService.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Foundation;
|
||||
using LocalAuthentication;
|
||||
|
||||
namespace Bit.iOS.Core.Services
|
||||
{
|
||||
public class BiometricService : IBiometricService
|
||||
{
|
||||
private IStorageService _storageService;
|
||||
|
||||
public BiometricService(IStorageService storageService)
|
||||
{
|
||||
_storageService = storageService;
|
||||
}
|
||||
|
||||
public async Task<bool> SetupBiometricAsync()
|
||||
{
|
||||
var state = GetState();
|
||||
await _storageService.SaveAsync("biometricState", ToBase64(state));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateIntegrityAsync()
|
||||
{
|
||||
var oldState = await _storageService.GetAsync<string>("biometricState");
|
||||
if (oldState == null)
|
||||
{
|
||||
// Fallback for upgraded devices
|
||||
await SetupBiometricAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var state = GetState();
|
||||
|
||||
return FromBase64(oldState) == state;
|
||||
}
|
||||
}
|
||||
|
||||
private NSData GetState()
|
||||
{
|
||||
var context = new LAContext();
|
||||
context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out _);
|
||||
|
||||
return context.EvaluatedPolicyDomainState;
|
||||
}
|
||||
|
||||
private string ToBase64(NSData data)
|
||||
{
|
||||
return System.Convert.ToBase64String(data.ToArray());
|
||||
}
|
||||
|
||||
private NSData FromBase64(string data)
|
||||
{
|
||||
var bytes = System.Convert.FromBase64String(data);
|
||||
return NSData.FromArray(bytes);
|
||||
}
|
||||
}
|
||||
}
|
@ -55,6 +55,7 @@ namespace Bit.iOS.Core.Utilities
|
||||
var deviceActionService = new DeviceActionService(mobileStorageService, messagingService);
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
var biometricService = new BiometricService(mobileStorageService);
|
||||
|
||||
ServiceContainer.Register<IBroadcasterService>("broadcasterService", broadcasterService);
|
||||
ServiceContainer.Register<IMessagingService>("messagingService", messagingService);
|
||||
@ -65,6 +66,7 @@ namespace Bit.iOS.Core.Utilities
|
||||
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
|
||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||
}
|
||||
|
||||
public static void Bootstrap(Func<Task> postBootstrapFunc = null)
|
||||
|
@ -163,6 +163,7 @@
|
||||
<Compile Include="Renderers\CustomTabbedRenderer.cs" />
|
||||
<Compile Include="Renderers\CustomViewCellRenderer.cs" />
|
||||
<Compile Include="Renderers\HybridWebViewRenderer.cs" />
|
||||
<Compile Include="Services\BiometricService.cs" />
|
||||
<Compile Include="Services\DeviceActionService.cs" />
|
||||
<Compile Include="Utilities\ASHelpers.cs" />
|
||||
<Compile Include="Utilities\Dialogs.cs" />
|
||||
|
Loading…
Reference in New Issue
Block a user