This commit is contained in:
Federico Maccaroni 2024-05-17 20:52:45 +02:00 committed by GitHub
commit 94901085a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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.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<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
@ -31,6 +36,8 @@ namespace Bit.Droid.Autofill
private LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
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<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.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<bool> hasVaultBeenUnlockedInThisTransaction,
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
@ -19,6 +25,8 @@ namespace Bit.App.Platforms.Android.Autofill
private readonly ICipherService _cipherService;
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
private TaskCompletionSource<bool> _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<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)
{
try

View File

@ -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<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)
{
await ExecuteFido2CreateCredentialAsync();

View File

@ -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";