Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-5153] Android Passkey Implementation #3020

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
482563d
Initial WIP implementation for the app unlock flow when called from P…
dinisvieira Feb 21, 2024
820e731
Added missing IDeviceActionService.cs implementation for iOS to build.
dinisvieira Feb 21, 2024
cddd167
Added Async to ReturnToPasskeyAfterUnlockMethod
dinisvieira Feb 21, 2024
7bef854
minor code change (added comment)
dinisvieira Feb 22, 2024
02380f1
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Feb 25, 2024
5da04a3
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Feb 26, 2024
dada617
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Feb 28, 2024
be804e0
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Feb 29, 2024
75b160a
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 1, 2024
5605dc8
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 4, 2024
dba25bf
Added back the case for loading a specific Window for CredentialProvi…
dinisvieira Mar 4, 2024
9be7091
Added fix for Intent not passing properly to CredentialProviderSelect…
dinisvieira Mar 4, 2024
1a890e0
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 6, 2024
e0ba93a
Added WIP code for Android passkey implementation. Currently returns …
dinisvieira Mar 7, 2024
557e4c0
Fixed conflicts
dinisvieira Mar 7, 2024
d1d4ccd
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 9, 2024
6125d09
Added WIP code for creating passkeys on Android. Still missing unlock…
dinisvieira Mar 10, 2024
11616ca
Started working on logic to adding unlock flow. It's already handling…
dinisvieira Mar 10, 2024
e8ffb5a
Changed "cross-platform" to "platform"
dinisvieira Mar 10, 2024
f7a6d64
Created CredentialHelpers.cs class to share code used for Populating …
dinisvieira Mar 10, 2024
6d40c34
Added Passkey Credential Creation shared code to CredentialHelpers.
dinisvieira Mar 10, 2024
01b6710
Updated code for checking if the CredentialProviderService has been e…
dinisvieira Mar 11, 2024
27c2662
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 11, 2024
c08515a
Replaced the AndroidX.Credential helpers with custom JSON creation to…
dinisvieira Mar 12, 2024
d9a7ec5
minor code cleanup on CredentialProviderSelectionActivity
dinisvieira Mar 12, 2024
177b3ff
added todo comment
dinisvieira Mar 12, 2024
f4cb237
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 12, 2024
2e9fee8
Feature/maui migraton passkeys android unlock fix andreas (#3077)
coroiu Mar 13, 2024
0cb664e
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 13, 2024
b521129
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
fedemkr Mar 14, 2024
9ed98ed
Removed / commented some older Passkey Proof of concept code.
dinisvieira Mar 15, 2024
14994d7
PM-6829 Implemented Fido2...UserInterfaces on Android and necessary l…
fedemkr Mar 19, 2024
34eac4b
Added IFido2MediatorService registrations
dinisvieira Mar 20, 2024
391f3b5
Added navigation to autofillCipher when creating passkey
dinisvieira Mar 20, 2024
7fded63
Updated LockPage to avoid multiple executions of SubmitAsync
dinisvieira Mar 21, 2024
4747c11
Added new flow for creating new passkey on Android with the Cipher pa…
dinisvieira Mar 21, 2024
35ffa08
Changed the Credential Provider Switch to an external link control
dinisvieira Mar 22, 2024
176a726
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
fedemkr Mar 22, 2024
98557ee
Added i18n for Passkey Settings
dinisvieira Mar 22, 2024
61081c5
Merge branch 'feature/maui-migraton-passkeys-android-unlock' of githu…
dinisvieira Mar 22, 2024
6a266ac
Cleanup of older Credentials code used for Android Fido2 POC.
dinisvieira Mar 22, 2024
7acdc1b
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 25, 2024
c4909cc
fixed merge conflict/error and added error check to Fido2 navigation …
dinisvieira Mar 25, 2024
ab5e7e5
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 25, 2024
5c040a6
Removed from MainActivity casts from DeviceActionService
dinisvieira Mar 25, 2024
9483697
Added some error messages. Still need to confirm the Text Resource to…
dinisvieira Mar 27, 2024
1b0e5a1
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Mar 27, 2024
3441f14
Changed some messages to use AppResources
dinisvieira Mar 28, 2024
e30e68b
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Apr 2, 2024
9ae67b3
Cleanup of Credential Android code and added exception result if the …
dinisvieira Apr 3, 2024
0292071
Updated Add new item button text when creating a new passkey
dinisvieira Apr 3, 2024
fc7247e
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Apr 3, 2024
b1166b9
Merge branch 'feature/maui-migration-passkeys' into feature/maui-migr…
dinisvieira Apr 3, 2024
048669e
Added AccountSwitchedException for the Fido Mediator Service
dinisvieira Apr 8, 2024
d146bce
Removed TODO that is no longer needed
dinisvieira Apr 8, 2024
75f416b
Updated some todo messages in Android AutofillHandler
dinisvieira Apr 9, 2024
b9494b3
When authenticating a passkey on Android the "showDialog" callback ca…
dinisvieira Apr 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions src/App/Platforms/Android/Autofill/CredentialHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Credentials;
using AndroidX.Credentials.Exceptions;
using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Bit.Droid;
using Org.Json;
using Activity = Android.App.Activity;
using Drawables = Android.Graphics.Drawables;

namespace Bit.App.Platforms.Android.Autofill
{
public static class CredentialHelpers
{
public static async Task<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
{
var passkeyEntries = new List<CredentialEntry>();
var requestOptions = new PublicKeyCredentialRequestOptions(option.RequestJson);

var authenticator = Bit.Core.Utilities.ServiceContainer.Resolve<IFido2AuthenticatorService>();
var credentials = await authenticator.SilentCredentialDiscoveryAsync(requestOptions.RpId);

passkeyEntries = credentials.Select(credential => MapCredential(credential, option, context, hasVaultBeenUnlockedInThisTransaction) as CredentialEntry).ToList();

return passkeyEntries;
}

private static PublicKeyCredentialEntry MapCredential(Fido2AuthenticatorDiscoverableCredentialMetadata credential, BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
{
var credDataBundle = new Bundle();
credDataBundle.PutByteArray(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialIdIntentExtra, credential.Id);

var intent = new Intent(context, typeof(Bit.Droid.Autofill.CredentialProviderSelectionActivity))
.SetAction(Bit.Droid.Autofill.CredentialProviderService.GetFido2IntentAction).SetPackage(Constants.PACKAGE_NAME);
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialProviderCipherId, credential.CipherId);
intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, hasVaultBeenUnlockedInThisTransaction);
var pendingIntent = PendingIntent.GetActivity(context, Bit.Droid.Autofill.CredentialProviderService.UniqueGetRequestCode, intent,
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent);

return new PublicKeyCredentialEntry.Builder(
context,
credential.UserName ?? "No username",
pendingIntent,
option)
.SetDisplayName(credential.UserName ?? "No username")
.SetIcon(Drawables.Icon.CreateWithResource(context, Microsoft.Maui.Resource.Drawable.icon))
.Build();
}

public static async Task CreateCipherPasskeyAsync(ProviderCreateCredentialRequest getRequest, Activity activity)
{
var callingRequest = getRequest?.CallingRequest as CreatePublicKeyCredentialRequest;
var origin = callingRequest.Origin;
var credentialCreationOptions = new PublicKeyCredentialCreationOptions(callingRequest.RequestJson);

var rp = new Core.Utilities.Fido2.PublicKeyCredentialRpEntity()
{
Id = credentialCreationOptions.Rp.Id,
Name = credentialCreationOptions.Rp.Name
};

var user = new Core.Utilities.Fido2.PublicKeyCredentialUserEntity()
{
Id = credentialCreationOptions.User.GetId(),
Name = credentialCreationOptions.User.Name,
DisplayName = credentialCreationOptions.User.DisplayName
};

var pubKeyCredParams = new List<Core.Utilities.Fido2.PublicKeyCredentialParameters>();
foreach (var pubKeyCredParam in credentialCreationOptions.PubKeyCredParams)
{
pubKeyCredParams.Add(new Core.Utilities.Fido2.PublicKeyCredentialParameters() { Alg = Convert.ToInt32(pubKeyCredParam.Alg), Type = pubKeyCredParam.Type });
}

var excludeCredentials = new List<Core.Utilities.Fido2.PublicKeyCredentialDescriptor>();
foreach (var excludeCred in credentialCreationOptions.ExcludeCredentials)
{
excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor(){ Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() });
}

var authenticatorSelection = new Core.Utilities.Fido2.AuthenticatorSelectionCriteria()
{
UserVerification = credentialCreationOptions.AuthenticatorSelection.UserVerification,
ResidentKey = credentialCreationOptions.AuthenticatorSelection.ResidentKey,
RequireResidentKey = credentialCreationOptions.AuthenticatorSelection.RequireResidentKey
};

var timeout = Convert.ToInt32(credentialCreationOptions.Timeout);

var credentialCreateParams = new Bit.Core.Utilities.Fido2.Fido2ClientCreateCredentialParams()
{
Challenge = credentialCreationOptions.GetChallenge(),
Origin = origin,
PubKeyCredParams = pubKeyCredParams.ToArray(),
Rp = rp,
User = user,
Timeout = timeout,
Attestation = credentialCreationOptions.Attestation,
AuthenticatorSelection = authenticatorSelection,
ExcludeCredentials = excludeCredentials.ToArray(),
//Extensions = // Can be improved later to add support for 'credProps'
SameOriginWithAncestors = true
};

var fido2MediatorService = ServiceContainer.Resolve<IFido2MediatorService>();
var clientCreateCredentialResult = await fido2MediatorService.CreateCredentialAsync(credentialCreateParams);
if (clientCreateCredentialResult == null)
{
var resultErrorIntent = new Intent();
PendingIntentHandler.SetCreateCredentialException(resultErrorIntent, new CreateCredentialUnknownException());
activity.SetResult(Result.Ok, resultErrorIntent);
activity.Finish();
return;
}

var transportsArray = new JSONArray();
if (clientCreateCredentialResult.Transports != null)
{
foreach (var transport in clientCreateCredentialResult.Transports)
{
transportsArray.Put(transport);
}
}

var responseInnerAndroidJson = new JSONObject();
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON));
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData));
responseInnerAndroidJson.Put("attestationObject", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AttestationObject));
responseInnerAndroidJson.Put("transports", transportsArray);
responseInnerAndroidJson.Put("publicKeyAlgorithm", clientCreateCredentialResult.PublicKeyAlgorithm);
responseInnerAndroidJson.Put("publicKey", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.PublicKey));

var rootAndroidJson = new JSONObject();
rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
rootAndroidJson.Put("authenticatorAttachment", "platform");
rootAndroidJson.Put("type", "public-key");
rootAndroidJson.Put("clientExtensionResults", new JSONObject());
rootAndroidJson.Put("response", responseInnerAndroidJson);

var responseAndroidJson = rootAndroidJson.ToString();

System.Diagnostics.Debug.WriteLine(responseAndroidJson);

var result = new Intent();
var publicKeyResponse = new CreatePublicKeyCredentialResponse(responseAndroidJson);
PendingIntentHandler.SetCreateCredentialResponse(result, publicKeyResponse);

activity.SetResult(Result.Ok, result);
activity.Finish();
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using System.Threading.Tasks;
using Android.App;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using AndroidX.Credentials;
using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn;
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Bit.App.Droid.Utilities;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities.Fido2;
using Java.Security;
using Bit.Core.Services;

namespace Bit.Droid.Autofill
{
Expand All @@ -15,6 +21,13 @@ namespace Bit.Droid.Autofill
LaunchMode = LaunchMode.SingleTop)]
public class CredentialProviderSelectionActivity : MauiAppCompatActivity
{
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>();

protected override void OnCreate(Bundle bundle)
{
Intent?.Validate();
Expand All @@ -23,43 +36,142 @@ protected override void OnCreate(Bundle bundle)
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
if (string.IsNullOrEmpty(cipherId))
{
SetResult(Result.Canceled);
Finish();
return;
}

GetCipherAndPerformPasskeyAuthAsync(cipherId).FireAndForget();
GetCipherAndPerformFido2AuthAsync(cipherId).FireAndForget();
}

//Used to avoid crash on MAUI when doing back
public override void OnBackPressed()
{
Finish();
}

private async Task GetCipherAndPerformPasskeyAuthAsync(string cipherId)
private async Task GetCipherAndPerformFido2AuthAsync(string cipherId)
{
// TODO this is a work in progress
// https://developer.android.com/training/sign-in/credential-provider#passkeys-implement
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;

var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
// var publicKeyRequest = getRequest?.CredentialOptions as PublicKeyCredentialRequestOptions;
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);
var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false);

var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
var credIdEnc = requestInfo?.GetString(CredentialProviderConstants.CredentialIdIntentExtra);
var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo);
var packageName = getRequest?.CallingAppInfo.PackageName;

var cipherService = ServiceContainer.Resolve<ICipherService>();
var cipher = await cipherService.GetAsync(cipherId);
var decCipher = await cipher.DecryptAsync();
var userInterface = new Fido2GetAssertionUserInterface(
cipherId: cipherId,
userVerified: false,
ensureUnlockedVaultCallback: EnsureUnlockedVaultAsync,
hasVaultBeenUnlockedInThisTransaction: () => hasVaultBeenUnlockedInThisTransaction,
verifyUserCallback: (cipherId, uvPreference) => VerifyUserAsync(cipherId, uvPreference, RpId, hasVaultBeenUnlockedInThisTransaction));

var passkey = decCipher.Login.Fido2Credentials.Find(f => f.CredentialId == credIdEnc);
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();
});
}
}

var credId = Convert.FromBase64String(credIdEnc);
// var privateKey = Convert.FromBase64String(passkey.PrivateKey);
// var uid = Convert.FromBase64String(passkey.uid);
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");
}
}

var origin = getRequest?.CallingAppInfo.Origin;
var packageName = getRequest?.CallingAppInfo.PackageName;
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();

// --- continue WIP here (save TOTP copy as last step) ---
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;
}
}

// Copy TOTP if needed
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
autofillHandler.Autofill(decCipher);
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)}";
}
}
}
Loading
Loading