Skip to content

Commit

Permalink
Initial WIP implementation for the app unlock flow when called from P…
Browse files Browse the repository at this point in the history
…asskey. Still needs code organization and to be finished.

Also added a new Window workaround in App.xaml.cs to allow CredentialProviderSelectionActivity to launch separately.
  • Loading branch information
dinisvieira committed Feb 21, 2024
1 parent 16e1b60 commit 482563d
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
public class CredentialProviderConstants
{
public const string PasskeyFramework = "passkeyFramework";
public const string CredentialProviderCipherId = "credentialProviderCipherId";
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA";
public const string CredentialIdIntentExtra = "credId";
Expand Down
17 changes: 16 additions & 1 deletion src/App/Platforms/Android/Autofill/CredentialProviderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Bit.Core.Utilities;
using AndroidX.Credentials.Exceptions;
using AndroidX.Credentials.WebAuthn;
using Bit.App.Droid.Utilities;
using Bit.Core.Models.View;
using Resource = Microsoft.Maui.Resource;

Expand Down Expand Up @@ -44,8 +45,22 @@ public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest
{
var response = await ProcessGetCredentialsRequestAsync(request);
callback.OnResult(response);
return;
}
// TODO handle auth/unlock account flow

var intent = new Intent(ApplicationContext, typeof(MainActivity));
intent.PutExtra(CredentialProviderConstants.PasskeyFramework, true);
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueRequestCode, intent,
AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));

var i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
var unlockText = i18nService.T("Unlock");
var unlockAction = new AuthenticationAction(unlockText, pendingIntent);

var unlockResponse = new BeginGetCredentialResponse.Builder()
.SetAuthenticationActions(new List<AuthenticationAction>() { unlockAction } )
.Build();
callback.OnResult(unlockResponse);
}
catch (GetCredentialException e)
{
Expand Down
1 change: 1 addition & 0 deletions src/App/Platforms/Android/MainActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ private AppOptions GetOptions()
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
FromPasskeyFramework = Intent.GetBooleanExtra(CredentialProviderConstants.PasskeyFramework, false),
CreateSend = GetCreateSendRequest(Intent)
};
var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
Expand Down
97 changes: 97 additions & 0 deletions src/App/Platforms/Android/Services/DeviceActionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Resource = Bit.Core.Resource;
using Application = Android.App.Application;
using static Android.Content.Res.Resources;

namespace Bit.Droid.Services
{
Expand All @@ -35,6 +36,10 @@ public class DeviceActionService : IDeviceActionService
private Toast _toast;
private string _userAgent;

//TODO: These consts need to be moved somewhere else where they can be shared with the code in CredentialProviderService.cs
private const string GetPasskeyIntentAction = "PACKAGE_NAME.GET_PASSKEY";
private const int UniqueRequestCode = 94556023;

public DeviceActionService(
IStateService stateService,
IMessagingService messagingService)
Expand Down Expand Up @@ -553,6 +558,98 @@ public long GetActiveTime()
// ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()
return SystemClock.ElapsedRealtime();
}

public async Task ReturnToPasskeyAfterUnlock()
{
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null)
{
return;
}

var request = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveBeginGetCredentialRequest(activity.Intent);
var response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse();;
IList<AndroidX.Credentials.Provider.CredentialEntry> credentialEntries = null;
foreach (var option in request.BeginGetCredentialOptions)
{
var credentialOption = option as AndroidX.Credentials.Provider.BeginGetPublicKeyCredentialOption;
if (credentialOption != null)
{
credentialEntries ??= new List<AndroidX.Credentials.Provider.CredentialEntry>();
((List<AndroidX.Credentials.Provider.CredentialEntry>)credentialEntries).AddRange(
await PopulatePasskeyDataAsync(request.CallingAppInfo, credentialOption));
}
}

if (credentialEntries != null)
{
response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse.Builder()
.SetCredentialEntries(credentialEntries)
.Build();
}

var result = new Android.Content.Intent();
AndroidX.Credentials.Provider.PendingIntentHandler.SetBeginGetCredentialResponse(result, response);

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

#region CODE_THAT_NEEDS_TO_BE_MOVED_ELSEWHERE
//TODO: This region needs to be moved somewhere else where it can be shared with the code in CredentialProviderService.cs
private async Task<List<AndroidX.Credentials.Provider.CredentialEntry>> PopulatePasskeyDataAsync(AndroidX.Credentials.Provider.CallingAppInfo callingAppInfo,
AndroidX.Credentials.Provider.BeginGetPublicKeyCredentialOption option)
{
var origin = callingAppInfo.Origin;
var passkeyEntries = new List<AndroidX.Credentials.Provider.CredentialEntry>();

var cipherService = Bit.Core.Utilities.ServiceContainer.Resolve<ICipherService>();
var ciphers = await cipherService.GetAllDecryptedForUrlAsync(origin);
if (ciphers == null)
{
return passkeyEntries;
}

var passkeyCiphers = ciphers.Where(cipher => cipher.HasFido2Credential).ToList();
if (!passkeyCiphers.Any())
{
return passkeyEntries;
}

foreach (var cipher in passkeyCiphers)
{
var passkeyEntry = GetPasskey(cipher, option);
passkeyEntries.Add(passkeyEntry);
}

return passkeyEntries;
}

private AndroidX.Credentials.Provider.PublicKeyCredentialEntry GetPasskey(Bit.Core.Models.View.CipherView cipher, AndroidX.Credentials.Provider.BeginGetPublicKeyCredentialOption option)
{
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;

var credDataBundle = new Bundle();
credDataBundle.PutString(Bit.Droid.Autofill.CredentialProviderConstants.CredentialIdIntentExtra,
cipher.Login.MainFido2Credential.CredentialId);

var intent = new Intent(activity.ApplicationContext, typeof(Bit.Droid.Autofill.CredentialProviderSelectionActivity))
.SetAction(GetPasskeyIntentAction).SetPackage(Constants.PACKAGE_NAME);
intent.PutExtra(Bit.Droid.Autofill.CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
intent.PutExtra(Bit.Droid.Autofill.CredentialProviderConstants.CredentialProviderCipherId, cipher.Id);
var pendingIntent = PendingIntent.GetActivity(activity.ApplicationContext, UniqueRequestCode, intent,
PendingIntentFlags.Immutable | PendingIntentFlags.UpdateCurrent);

return new AndroidX.Credentials.Provider.PublicKeyCredentialEntry.Builder(
activity.ApplicationContext,
cipher.Login.Username ?? "No username",
pendingIntent,
option)
.SetDisplayName(cipher.Name)
.SetIcon(Android.Graphics.Drawables.Icon.CreateWithResource(activity.ApplicationContext, Microsoft.Maui.Resource.Drawable.icon))
.Build();
}
#endregion

public void CloseMainApp()
{
Expand Down
1 change: 1 addition & 0 deletions src/Core/Abstractions/IDeviceActionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Task<string> DisplayPromptAync(string title = null, string description = null, s
void OpenCredentialProviderSettings();
void OpenAutofillSettings();
long GetActiveTime();
Task ReturnToPasskeyAfterUnlock();
void CloseMainApp();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
Expand Down
12 changes: 11 additions & 1 deletion src/Core/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public void SetAndroidOptions(AppOptions appOptions)
Options.MyVaultTile = appOptions.MyVaultTile;
Options.GeneratorTile = appOptions.GeneratorTile;
Options.FromAutofillFramework = appOptions.FromAutofillFramework;
Options.FromPasskeyFramework = appOptions.FromPasskeyFramework;
Options.CreateSend = appOptions.CreateSend;
}
}
Expand All @@ -120,8 +121,17 @@ protected override Window CreateWindow(IActivationState activationState)
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
}

//When executing from CredentialProviderSelectionActivity we don't have "Options" so we need to filter "manually"
//In the CredentialProviderSelectionActivity we don't need to show any Page, so we just create a "dummy" Window with a NavigationPage to avoid crashing.
if (activationState != null
&& activationState.State.ContainsKey("CREDENTIAL_DATA")
&& activationState.State.ContainsKey("credentialProviderCipherId"))
{
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
}

//"Internal" Autofill and Uri/Otp/CreateSend. This is where we create the autofill specific Window
if (Options != null && (Options.FromAutofillFramework || Options.Uri != null || Options.OtpData != null || Options.CreateSend != null))
if (Options != null && (Options.FromAutofillFramework || Options.Uri != null || Options.OtpData != null || Options.CreateSend != null || Options.FromPasskeyFramework))
{
_isResumed = true; //Specifically for the Autofill scenario we need to manually set the _isResumed here
_hasNavigatedToAutofillWindow = true;
Expand Down
1 change: 1 addition & 0 deletions src/Core/Models/AppOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class AppOptions
public bool MyVaultTile { get; set; }
public bool GeneratorTile { get; set; }
public bool FromAutofillFramework { get; set; }
public bool FromPasskeyFramework { get; set; }
public CipherType? FillType { get; set; }
public string Uri { get; set; }
public CipherType? SaveType { get; set; }
Expand Down
7 changes: 7 additions & 0 deletions src/Core/Utilities/AppHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,13 @@ public static bool SetAlternateMainPage(AppOptions appOptions)
App.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: appOptions));
return true;
}
if (appOptions.FromPasskeyFramework)
{
appOptions.FromPasskeyFramework = false;
var deviceActionService = Bit.Core.Utilities.ServiceContainer.Resolve<IDeviceActionService>();
deviceActionService.ReturnToPasskeyAfterUnlock().FireAndForget();
return true;
}
if (appOptions.Uri != null
||
appOptions.OtpData != null)
Expand Down

0 comments on commit 482563d

Please sign in to comment.