From 2cdb5a9c7465e4a4a248d042151e6372cab1e8f7 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Fri, 10 May 2024 17:54:56 -0300 Subject: [PATCH] PM-7963 Fix vault timeout immediately on Android Fido2 autofill --- .../CredentialProviderSelectionActivity.cs | 58 +++++++++++++++---- .../Fido2GetAssertionUserInterface.cs | 39 ++++++++++++- .../Android/Services/DeviceActionService.cs | 12 ++++ .../Fido2/CredentialProviderConstants.cs | 1 + 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs b/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs index 5ea4213c0..f786d5507 100644 --- a/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs +++ b/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs @@ -1,26 +1,31 @@ -using Android.App; +using System.Diagnostics; +using Android.App; using Android.Content; using Android.Content.PM; using Android.OS; +using Android.Runtime; +using AndroidX.Activity.Result; +using AndroidX.Activity.Result.Contract; using AndroidX.Credentials; +using AndroidX.Credentials.Exceptions; using AndroidX.Credentials.Provider; using AndroidX.Credentials.WebAuthn; -using Bit.App.Droid.Utilities; using Bit.App.Abstractions; -using Bit.Core.Abstractions; -using Bit.Core.Utilities; -using Bit.Core.Resources.Localization; -using Bit.Core.Utilities.Fido2; -using Bit.Core.Services; +using Bit.App.Droid.Utilities; 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; namespace Bit.Droid.Autofill { [Activity( - NoHistory = true, - LaunchMode = LaunchMode.SingleTop)] + NoHistory = false, + LaunchMode = LaunchMode.SingleInstance)] + [Register("com.x8bit.bitwarden.CredentialProviderSelectionActivity")] public class CredentialProviderSelectionActivity : MauiAppCompatActivity { private LazyResolve _fido2MediatorService = new LazyResolve(); @@ -31,6 +36,8 @@ namespace Bit.Droid.Autofill private LazyResolve _userVerificationMediatorService = new LazyResolve(); private LazyResolve _deviceActionService = new LazyResolve(); + private ActivityResultLauncher _activityResultLauncher; + protected override void OnCreate(Bundle bundle) { Intent?.Validate(); @@ -100,8 +107,14 @@ namespace Bit.Droid.Autofill cipherId, false, () => hasVaultBeenUnlockedInThisTransaction, - RpId - ); + RpId + ); + + _activityResultLauncher = RegisterForActivityResult(new ActivityResultContracts.StartActivityForResult(), + new ActivityResultCallback(result => + { + _fido2GetAssertionUserInterface.Value.ConfirmVaultUnlocked(result.ResultCode == (int)Result.Ok); + })); 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() { var result = new Intent(); @@ -180,4 +206,12 @@ namespace Bit.Droid.Autofill Finish(); } } + + public class ActivityResultCallback : Java.Lang.Object, IActivityResultCallback + { + readonly Action _callback; + public ActivityResultCallback(Action callback) => _callback = callback; + public ActivityResultCallback(TaskCompletionSource tcs) => _callback = tcs.SetResult; + public void OnActivityResult(Java.Lang.Object p0) => _callback((ActivityResult)p0); + } } diff --git a/src/App/Platforms/Android/Autofill/Fido2GetAssertionUserInterface.cs b/src/App/Platforms/Android/Autofill/Fido2GetAssertionUserInterface.cs index 9d1e503ec..48113e384 100644 --- a/src/App/Platforms/Android/Autofill/Fido2GetAssertionUserInterface.cs +++ b/src/App/Platforms/Android/Autofill/Fido2GetAssertionUserInterface.cs @@ -1,6 +1,7 @@ using Bit.Core.Abstractions; using Bit.Core.Services; using Bit.Core.Utilities.Fido2; +using Bit.Droid.Autofill; namespace Bit.App.Platforms.Android.Autofill { @@ -10,6 +11,11 @@ namespace Bit.App.Platforms.Android.Autofill bool userVerified, Func hasVaultBeenUnlockedInThisTransaction, string rpId); + + /// + /// Call this after the vault was unlocked so that Fido2 credential autofill can proceed. + /// + void ConfirmVaultUnlocked(bool unlocked); } public class Fido2GetAssertionUserInterface : Core.Utilities.Fido2.Fido2GetAssertionUserInterface, IFido2AndroidGetAssertionUserInterface @@ -19,6 +25,8 @@ namespace Bit.App.Platforms.Android.Autofill private readonly ICipherService _cipherService; private readonly IUserVerificationMediatorService _userVerificationMediatorService; + private TaskCompletionSource _unlockVaultTcs; + public Fido2GetAssertionUserInterface(IStateService stateService, IVaultTimeoutService vaultTimeoutService, ICipherService cipherService, @@ -46,11 +54,38 @@ namespace Bit.App.Platforms.Android.Autofill { if (!await _stateService.IsAuthenticatedAsync() || await _vaultTimeoutService.IsLockedAsync()) { - // this should never happen but just in case. - throw new InvalidOperationException("Not authed or vault locked"); + if (await _stateService.GetVaultTimeoutAsync() != 0) + { + // 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 NavigateAndWaitForUnlockAsync() + { + var credentialProviderSelectionActivity = Platform.CurrentActivity as CredentialProviderSelectionActivity; + if (credentialProviderSelectionActivity == null) + { + throw new InvalidOperationException("Can't get current activity"); + } + + _unlockVaultTcs?.TrySetCanceled(); + _unlockVaultTcs = new TaskCompletionSource(); + + credentialProviderSelectionActivity.LaunchToUnlock(); + + return await _unlockVaultTcs.Task; + } + private async Task VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction) { try diff --git a/src/App/Platforms/Android/Services/DeviceActionService.cs b/src/App/Platforms/Android/Services/DeviceActionService.cs index 82174220f..52b38a329 100644 --- a/src/App/Platforms/Android/Services/DeviceActionService.cs +++ b/src/App/Platforms/Android/Services/DeviceActionService.cs @@ -23,11 +23,14 @@ using Resource = Bit.Core.Resource; using Application = Android.App.Application; using Bit.Core.Services; using Bit.Core.Utilities.Fido2; +using Bit.Core.Utilities; namespace Bit.Droid.Services { public class DeviceActionService : IDeviceActionService { + public const int DELAY_LOCK_LOGOUT_FOR_FIDO2_AUTOFILL_ON_IMMEDIATE_TIMEOUT_MS = 15000; + private readonly IStateService _stateService; private readonly IMessagingService _messagingService; private AlertDialog _progressDialog; @@ -578,6 +581,15 @@ namespace Bit.Droid.Services { await ExecuteFido2GetCredentialAsync(appOptions); } + else if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialNeedsUnlockingAgainBecauseImmediateTimeout + && + ServiceContainer.TryResolve(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) { await ExecuteFido2CreateCredentialAsync(); diff --git a/src/Core/Utilities/Fido2/CredentialProviderConstants.cs b/src/Core/Utilities/Fido2/CredentialProviderConstants.cs index 6a4fc0948..0bb359f22 100644 --- a/src/Core/Utilities/Fido2/CredentialProviderConstants.cs +++ b/src/Core/Utilities/Fido2/CredentialProviderConstants.cs @@ -5,6 +5,7 @@ public const string Fido2CredentialCreate = "fido2CredentialCreate"; public const string Fido2CredentialGet = "fido2CredentialGet"; public const string Fido2CredentialAction = "fido2CredentialAction"; + public const string Fido2CredentialNeedsUnlockingAgainBecauseImmediateTimeout = "fido2CredentialNeedsUnlockingAgainBecauseImmediateTimeout"; public const string CredentialProviderCipherId = "credentialProviderCipherId"; public const string CredentialDataIntentExtra = "CREDENTIAL_DATA"; public const string CredentialIdIntentExtra = "credId";