Skip to content

Commit

Permalink
Invalidate biometric on change (#1026)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Hinton and kspearrin committed Aug 9, 2020
1 parent 39de2c1 commit ae28de4
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 20 deletions.
1 change: 1 addition & 0 deletions src/Android/Android.csproj
Expand Up @@ -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" />
Expand Down
2 changes: 2 additions & 0 deletions src/Android/MainApplication.cs
Expand Up @@ -98,6 +98,7 @@ private void RegisterLocalServices()
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);
Expand All @@ -108,6 +109,7 @@ private void RegisterLocalServices()
ServiceContainer.Register<IStorageService>("secureStorageService", secureStorageService);
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);

// Push
#if FDROID
Expand Down
90 changes: 90 additions & 0 deletions src/Android/Services/BiometricService.cs
@@ -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();
}
}
}
6 changes: 5 additions & 1 deletion src/App/Pages/Accounts/LockPage.xaml
Expand Up @@ -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>
Expand Down
21 changes: 19 additions & 2 deletions src/App/Pages/Accounts/LockPageViewModel.cs
Expand Up @@ -24,11 +24,13 @@ public class LockPageViewModel : BaseViewModel
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;
Expand All @@ -47,6 +49,7 @@ public LockPageViewModel()
_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);
Expand Down Expand Up @@ -75,6 +78,12 @@ public bool BiometricLock
set => SetProperty(ref _biometricLock, value);
}

public bool BiometricIntegrityValid
{
get => _biometricIntegrityValid;
set => SetProperty(ref _biometricIntegrityValid, value);
}

public string BiometricButtonText
{
get => _biometricButtonText;
Expand Down Expand Up @@ -133,7 +142,8 @@ public async Task InitAsync(bool autoPromptBiometric)
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
AppResources.UseFingerprintToUnlock;
}
if (autoPromptBiometric)
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
if (autoPromptBiometric & _biometricIntegrityValid)
{
var tasks = Task.Run(async () =>
{
Expand Down Expand Up @@ -238,6 +248,12 @@ public async Task SubmitAsync()
}
MasterPassword = string.Empty;
await SetKeyAndContinueAsync(key);

// Re-enable biometrics
if (BiometricLock & !BiometricIntegrityValid)
{
await _biometricService.SetupBiometricAsync();
}
}
else
{
Expand Down Expand Up @@ -267,7 +283,8 @@ public void TogglePassword()

public async Task PromptBiometricAsync()
{
if (!BiometricLock)
BiometricIntegrityValid = await _biometricService.ValidateIntegrityAsync();
if (!BiometricLock || !BiometricIntegrityValid)
{
return;
}
Expand Down
3 changes: 3 additions & 0 deletions src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
Expand Up @@ -22,6 +22,7 @@ public class SettingsPageViewModel : BaseViewModel
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IStorageService _storageService;
private readonly ISyncService _syncService;
private readonly IBiometricService _biometricService;

private bool _supportsBiometric;
private bool _pin;
Expand Down Expand Up @@ -60,6 +61,7 @@ public SettingsPageViewModel()
_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;
Expand Down Expand Up @@ -306,6 +308,7 @@ public async Task UpdateBiometricAsync()
}
if (_biometric)
{
await _biometricService.SetupBiometricAsync();
await _storageService.SaveAsync(Constants.BiometricUnlockKey, true);
}
else
Expand Down
37 changes: 20 additions & 17 deletions src/App/Resources/AppResources.en-GB.resx
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
3 changes: 3 additions & 0 deletions src/App/Resources/AppResources.resx
Expand Up @@ -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>
Expand Down
10 changes: 10 additions & 0 deletions src/Core/Abstractions/IBiometricService.cs
@@ -0,0 +1,10 @@
using System.Threading.Tasks;

namespace Bit.Core.Abstractions
{
public interface IBiometricService
{
Task<bool> SetupBiometricAsync();
Task<bool> ValidateIntegrityAsync();
}
}
62 changes: 62 additions & 0 deletions src/iOS.Core/Services/BiometricService.cs
@@ -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);
}
}
}
2 changes: 2 additions & 0 deletions src/iOS.Core/Utilities/iOSCoreHelpers.cs
Expand Up @@ -55,6 +55,7 @@ public static void RegisterLocalServices()
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);
Expand All @@ -65,6 +66,7 @@ public static void RegisterLocalServices()
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)
Expand Down

0 comments on commit ae28de4

Please sign in to comment.