1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-12-02 13:13:31 +01:00

PM-7963 Fix vault timeout immediately on Android Fido2 autofill

This commit is contained in:
Federico Maccaroni 2024-05-10 17:54:56 -03:00
parent 477b1cca44
commit 2cdb5a9c74
No known key found for this signature in database
GPG Key ID: 5D233F8F2B034536
4 changed files with 96 additions and 14 deletions

View File

@ -1,26 +1,31 @@
using Android.App; using System.Diagnostics;
using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.OS; using Android.OS;
using Android.Runtime;
using AndroidX.Activity.Result;
using AndroidX.Activity.Result.Contract;
using AndroidX.Credentials; using AndroidX.Credentials;
using AndroidX.Credentials.Exceptions;
using AndroidX.Credentials.Provider; using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn; using AndroidX.Credentials.WebAuthn;
using Bit.App.Droid.Utilities;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.Core.Abstractions; using Bit.App.Droid.Utilities;
using Bit.Core.Utilities;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities.Fido2;
using Bit.Core.Services;
using Bit.App.Platforms.Android.Autofill; using Bit.App.Platforms.Android.Autofill;
using AndroidX.Credentials.Exceptions; using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
using Org.Json; using Org.Json;
namespace Bit.Droid.Autofill namespace Bit.Droid.Autofill
{ {
[Activity( [Activity(
NoHistory = true, NoHistory = false,
LaunchMode = LaunchMode.SingleTop)] LaunchMode = LaunchMode.SingleInstance)]
[Register("com.x8bit.bitwarden.CredentialProviderSelectionActivity")]
public class CredentialProviderSelectionActivity : MauiAppCompatActivity public class CredentialProviderSelectionActivity : MauiAppCompatActivity
{ {
private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>(); private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
@ -31,6 +36,8 @@ namespace Bit.Droid.Autofill
private LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>(); private LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>(); private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
private ActivityResultLauncher _activityResultLauncher;
protected override void OnCreate(Bundle bundle) protected override void OnCreate(Bundle bundle)
{ {
Intent?.Validate(); Intent?.Validate();
@ -100,8 +107,14 @@ namespace Bit.Droid.Autofill
cipherId, cipherId,
false, false,
() => hasVaultBeenUnlockedInThisTransaction, () => hasVaultBeenUnlockedInThisTransaction,
RpId RpId
); );
_activityResultLauncher = RegisterForActivityResult(new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback(result =>
{
_fido2GetAssertionUserInterface.Value.ConfirmVaultUnlocked(result.ResultCode == (int)Result.Ok);
}));
var clientAssertParams = new Fido2ClientAssertCredentialParams var clientAssertParams = new Fido2ClientAssertCredentialParams
{ {
@ -171,6 +184,19 @@ namespace Bit.Droid.Autofill
} }
} }
public void LaunchToUnlock()
{
if (_activityResultLauncher is null)
{
throw new InvalidOperationException("There is no activity result launcher available");
}
var intent = new Intent(this, typeof(MainActivity));
intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialNeedsUnlockingAgainBecauseImmediateTimeout);
_activityResultLauncher.Launch(intent);
}
private void FailAndFinish() private void FailAndFinish()
{ {
var result = new Intent(); var result = new Intent();
@ -180,4 +206,12 @@ namespace Bit.Droid.Autofill
Finish(); Finish();
} }
} }
public class ActivityResultCallback : Java.Lang.Object, IActivityResultCallback
{
readonly Action<ActivityResult> _callback;
public ActivityResultCallback(Action<ActivityResult> callback) => _callback = callback;
public ActivityResultCallback(TaskCompletionSource<ActivityResult> tcs) => _callback = tcs.SetResult;
public void OnActivityResult(Java.Lang.Object p0) => _callback((ActivityResult)p0);
}
} }

View File

@ -1,6 +1,7 @@
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities.Fido2; using Bit.Core.Utilities.Fido2;
using Bit.Droid.Autofill;
namespace Bit.App.Platforms.Android.Autofill namespace Bit.App.Platforms.Android.Autofill
{ {
@ -10,6 +11,11 @@ namespace Bit.App.Platforms.Android.Autofill
bool userVerified, bool userVerified,
Func<bool> hasVaultBeenUnlockedInThisTransaction, Func<bool> hasVaultBeenUnlockedInThisTransaction,
string rpId); string rpId);
/// <summary>
/// Call this after the vault was unlocked so that Fido2 credential autofill can proceed.
/// </summary>
void ConfirmVaultUnlocked(bool unlocked);
} }
public class Fido2GetAssertionUserInterface : Core.Utilities.Fido2.Fido2GetAssertionUserInterface, IFido2AndroidGetAssertionUserInterface public class Fido2GetAssertionUserInterface : Core.Utilities.Fido2.Fido2GetAssertionUserInterface, IFido2AndroidGetAssertionUserInterface
@ -19,6 +25,8 @@ namespace Bit.App.Platforms.Android.Autofill
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly IUserVerificationMediatorService _userVerificationMediatorService; private readonly IUserVerificationMediatorService _userVerificationMediatorService;
private TaskCompletionSource<bool> _unlockVaultTcs;
public Fido2GetAssertionUserInterface(IStateService stateService, public Fido2GetAssertionUserInterface(IStateService stateService,
IVaultTimeoutService vaultTimeoutService, IVaultTimeoutService vaultTimeoutService,
ICipherService cipherService, ICipherService cipherService,
@ -46,11 +54,38 @@ namespace Bit.App.Platforms.Android.Autofill
{ {
if (!await _stateService.IsAuthenticatedAsync() || await _vaultTimeoutService.IsLockedAsync()) if (!await _stateService.IsAuthenticatedAsync() || await _vaultTimeoutService.IsLockedAsync())
{ {
// this should never happen but just in case. if (await _stateService.GetVaultTimeoutAsync() != 0)
throw new InvalidOperationException("Not authed or vault locked"); {
// this should never happen but just in case.
throw new InvalidOperationException("Not authed or vault locked");
}
// if vault timeout is immediate, then we need to unlock the vault
if (!await NavigateAndWaitForUnlockAsync())
{
throw new InvalidOperationException("Couldn't unlock with immediate timeout");
}
} }
} }
public void ConfirmVaultUnlocked(bool unlocked) => _unlockVaultTcs?.TrySetResult(unlocked);
private async Task<bool> NavigateAndWaitForUnlockAsync()
{
var credentialProviderSelectionActivity = Platform.CurrentActivity as CredentialProviderSelectionActivity;
if (credentialProviderSelectionActivity == null)
{
throw new InvalidOperationException("Can't get current activity");
}
_unlockVaultTcs?.TrySetCanceled();
_unlockVaultTcs = new TaskCompletionSource<bool>();
credentialProviderSelectionActivity.LaunchToUnlock();
return await _unlockVaultTcs.Task;
}
private async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction) private async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction)
{ {
try try

View File

@ -23,11 +23,14 @@ using Resource = Bit.Core.Resource;
using Application = Android.App.Application; using Application = Android.App.Application;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities.Fido2; using Bit.Core.Utilities.Fido2;
using Bit.Core.Utilities;
namespace Bit.Droid.Services namespace Bit.Droid.Services
{ {
public class DeviceActionService : IDeviceActionService public class DeviceActionService : IDeviceActionService
{ {
public const int DELAY_LOCK_LOGOUT_FOR_FIDO2_AUTOFILL_ON_IMMEDIATE_TIMEOUT_MS = 15000;
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IMessagingService _messagingService; private readonly IMessagingService _messagingService;
private AlertDialog _progressDialog; private AlertDialog _progressDialog;
@ -578,6 +581,15 @@ namespace Bit.Droid.Services
{ {
await ExecuteFido2GetCredentialAsync(appOptions); await ExecuteFido2GetCredentialAsync(appOptions);
} }
else if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialNeedsUnlockingAgainBecauseImmediateTimeout
&&
ServiceContainer.TryResolve<IVaultTimeoutService>(out var vaultTimeoutService))
{
vaultTimeoutService.DelayLockAndLogoutMs = DELAY_LOCK_LOGOUT_FOR_FIDO2_AUTOFILL_ON_IMMEDIATE_TIMEOUT_MS;
activity.SetResult(Result.Ok);
activity.Finish();
}
else if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate) else if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
{ {
await ExecuteFido2CreateCredentialAsync(); await ExecuteFido2CreateCredentialAsync();

View File

@ -5,6 +5,7 @@
public const string Fido2CredentialCreate = "fido2CredentialCreate"; public const string Fido2CredentialCreate = "fido2CredentialCreate";
public const string Fido2CredentialGet = "fido2CredentialGet"; public const string Fido2CredentialGet = "fido2CredentialGet";
public const string Fido2CredentialAction = "fido2CredentialAction"; public const string Fido2CredentialAction = "fido2CredentialAction";
public const string Fido2CredentialNeedsUnlockingAgainBecauseImmediateTimeout = "fido2CredentialNeedsUnlockingAgainBecauseImmediateTimeout";
public const string CredentialProviderCipherId = "credentialProviderCipherId"; public const string CredentialProviderCipherId = "credentialProviderCipherId";
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA"; public const string CredentialDataIntentExtra = "CREDENTIAL_DATA";
public const string CredentialIdIntentExtra = "credId"; public const string CredentialIdIntentExtra = "credId";