2024-04-09 22:57:31 +02:00
|
|
|
|
using Android.App;
|
|
|
|
|
using Android.Content;
|
2023-12-21 00:04:21 +01:00
|
|
|
|
using Android.Content.PM;
|
|
|
|
|
using Android.OS;
|
2024-04-09 22:57:31 +02:00
|
|
|
|
using AndroidX.Credentials;
|
2023-12-21 00:04:21 +01:00
|
|
|
|
using AndroidX.Credentials.Provider;
|
|
|
|
|
using AndroidX.Credentials.WebAuthn;
|
2024-04-09 22:57:31 +02:00
|
|
|
|
using Bit.App.Abstractions;
|
2023-12-21 00:04:21 +01:00
|
|
|
|
using Bit.Core.Abstractions;
|
|
|
|
|
using Bit.Core.Utilities;
|
|
|
|
|
using Bit.App.Droid.Utilities;
|
2024-04-09 22:57:31 +02:00
|
|
|
|
using Bit.Core.Resources.Localization;
|
|
|
|
|
using Bit.Core.Utilities.Fido2;
|
|
|
|
|
using Java.Security;
|
|
|
|
|
using Bit.Core.Services;
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
|
|
|
|
namespace Bit.Droid.Autofill
|
|
|
|
|
{
|
|
|
|
|
[Activity(
|
|
|
|
|
NoHistory = true,
|
|
|
|
|
LaunchMode = LaunchMode.SingleTop)]
|
|
|
|
|
public class CredentialProviderSelectionActivity : MauiAppCompatActivity
|
|
|
|
|
{
|
2024-04-09 22:57:31 +02:00
|
|
|
|
private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
|
|
|
|
|
private LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
|
|
|
|
|
private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
|
|
|
|
|
private LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
|
|
|
|
private LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
|
|
|
|
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
|
|
|
|
|
|
2023-12-21 00:04:21 +01:00
|
|
|
|
protected override void OnCreate(Bundle bundle)
|
|
|
|
|
{
|
|
|
|
|
Intent?.Validate();
|
|
|
|
|
base.OnCreate(bundle);
|
|
|
|
|
|
|
|
|
|
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
|
|
|
|
|
if (string.IsNullOrEmpty(cipherId))
|
|
|
|
|
{
|
|
|
|
|
Finish();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
GetCipherAndPerformFido2AuthAsync(cipherId).FireAndForget();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Used to avoid crash on MAUI when doing back
|
|
|
|
|
public override void OnBackPressed()
|
|
|
|
|
{
|
|
|
|
|
Finish();
|
2023-12-21 00:04:21 +01:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
private async Task GetCipherAndPerformFido2AuthAsync(string cipherId)
|
2023-12-21 00:04:21 +01:00
|
|
|
|
{
|
2024-04-09 22:57:31 +02:00
|
|
|
|
string RpId = string.Empty;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
|
|
|
|
|
|
|
|
|
|
var credentialOption = getRequest?.CredentialOptions.FirstOrDefault();
|
|
|
|
|
var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
|
|
|
|
|
|
|
|
|
|
var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson);
|
|
|
|
|
RpId = requestOptions.RpId;
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
|
|
|
|
|
var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);
|
|
|
|
|
var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false);
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo);
|
|
|
|
|
var packageName = getRequest?.CallingAppInfo.PackageName;
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
var userInterface = new Fido2GetAssertionUserInterface(
|
|
|
|
|
cipherId: cipherId,
|
|
|
|
|
userVerified: false,
|
|
|
|
|
ensureUnlockedVaultCallback: EnsureUnlockedVaultAsync,
|
|
|
|
|
hasVaultBeenUnlockedInThisTransaction: () => hasVaultBeenUnlockedInThisTransaction,
|
|
|
|
|
verifyUserCallback: (cipherId, uvPreference) => VerifyUserAsync(cipherId, uvPreference, RpId, hasVaultBeenUnlockedInThisTransaction));
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
var assertParams = new Fido2AuthenticatorGetAssertionParams
|
|
|
|
|
{
|
|
|
|
|
Challenge = requestOptions.GetChallenge(),
|
|
|
|
|
RpId = RpId,
|
|
|
|
|
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(requestOptions.UserVerification),
|
|
|
|
|
Hash = credentialPublic.GetClientDataHash(),
|
|
|
|
|
AllowCredentialDescriptorList = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } },
|
|
|
|
|
Extensions = new object()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var assertResult = await _fido2MediatorService.Value.GetAssertionAsync(assertParams, userInterface);
|
|
|
|
|
|
|
|
|
|
var response = new AuthenticatorAssertionResponse(
|
|
|
|
|
requestOptions,
|
|
|
|
|
assertResult.SelectedCredential.Id,
|
|
|
|
|
androidOrigin,
|
|
|
|
|
false, // These flags have no effect, we set our own within `SetAuthenticatorData`
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
assertResult.SelectedCredential.UserHandle,
|
|
|
|
|
packageName,
|
|
|
|
|
credentialPublic.GetClientDataHash() //clientDataHash
|
|
|
|
|
);
|
|
|
|
|
response.SetAuthenticatorData(assertResult.AuthenticatorData);
|
|
|
|
|
response.SetSignature(assertResult.Signature);
|
|
|
|
|
|
|
|
|
|
var result = new Intent();
|
|
|
|
|
var fidoCredential = new FidoPublicKeyCredential(assertResult.SelectedCredential.Id, response, "platform");
|
|
|
|
|
var cred = new PublicKeyCredential(fidoCredential.Json());
|
|
|
|
|
var credResponse = new GetCredentialResponse(cred);
|
|
|
|
|
PendingIntentHandler.SetGetCredentialResponse(result, credResponse);
|
|
|
|
|
|
|
|
|
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
SetResult(Result.Ok, result);
|
|
|
|
|
Finish();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (NotAllowedError)
|
|
|
|
|
{
|
|
|
|
|
await MainThread.InvokeOnMainThreadAsync(async() =>
|
|
|
|
|
{
|
|
|
|
|
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
|
|
|
|
|
Finish();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
|
|
|
await MainThread.InvokeOnMainThreadAsync(async() =>
|
|
|
|
|
{
|
|
|
|
|
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
|
|
|
|
|
Finish();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
private async Task EnsureUnlockedVaultAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!await _stateService.Value.IsAuthenticatedAsync() || await _vaultTimeoutService.Value.IsLockedAsync())
|
|
|
|
|
{
|
|
|
|
|
// this should never happen but just in case.
|
|
|
|
|
throw new InvalidOperationException("Not authed or vault locked");
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
internal async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var encrypted = await _cipherService.Value.GetAsync(selectedCipherId);
|
|
|
|
|
var cipher = await encrypted.DecryptAsync();
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
var userVerification = await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
|
|
|
|
new Fido2UserVerificationOptions(
|
|
|
|
|
cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password,
|
|
|
|
|
userVerificationPreference,
|
|
|
|
|
vaultUnlockedDuringThisTransaction,
|
|
|
|
|
rpId)
|
|
|
|
|
);
|
|
|
|
|
return !userVerification.IsCancelled && userVerification.Result;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-21 00:04:21 +01:00
|
|
|
|
|
2024-04-09 22:57:31 +02:00
|
|
|
|
private string AppInfoToOrigin(CallingAppInfo info)
|
|
|
|
|
{
|
|
|
|
|
var cert = info.SigningInfo.GetApkContentsSigners()[0].ToByteArray();
|
|
|
|
|
var md = MessageDigest.GetInstance("SHA-256");
|
|
|
|
|
var certHash = md.Digest(cert);
|
|
|
|
|
return $"android:apk-key-hash:${CoreHelpers.Base64UrlEncode(certHash)}";
|
2023-12-21 00:04:21 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|