23 changed files with 475 additions and 1 deletions
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Droid.Autofill |
||||
{ |
||||
public class CredentialProviderConstants |
||||
{ |
||||
public const string CredentialProviderCipherId = "credentialProviderCipherId"; |
||||
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA"; |
||||
public const string CredentialIdIntentExtra = "credId"; |
||||
} |
||||
} |
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
using System.Threading.Tasks; |
||||
using Android.App; |
||||
using Android.Content.PM; |
||||
using Android.OS; |
||||
using AndroidX.Credentials.Provider; |
||||
using AndroidX.Credentials.WebAuthn; |
||||
using Bit.Core.Abstractions; |
||||
using Bit.Core.Utilities; |
||||
using Bit.App.Droid.Utilities; |
||||
|
||||
namespace Bit.Droid.Autofill |
||||
{ |
||||
[Activity( |
||||
NoHistory = true, |
||||
LaunchMode = LaunchMode.SingleTop)] |
||||
public class CredentialProviderSelectionActivity : MauiAppCompatActivity |
||||
{ |
||||
protected override void OnCreate(Bundle bundle) |
||||
{ |
||||
Intent?.Validate(); |
||||
base.OnCreate(bundle); |
||||
|
||||
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId); |
||||
if (string.IsNullOrEmpty(cipherId)) |
||||
{ |
||||
SetResult(Result.Canceled); |
||||
Finish(); |
||||
return; |
||||
} |
||||
|
||||
GetCipherAndPerformPasskeyAuthAsync(cipherId).FireAndForget(); |
||||
} |
||||
|
||||
private async Task GetCipherAndPerformPasskeyAuthAsync(string cipherId) |
||||
{ |
||||
// TODO this is a work in progress |
||||
// https://developer.android.com/training/sign-in/credential-provider#passkeys-implement |
||||
|
||||
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent); |
||||
// var publicKeyRequest = getRequest?.CredentialOptions as PublicKeyCredentialRequestOptions; |
||||
|
||||
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra); |
||||
var credIdEnc = requestInfo?.GetString(CredentialProviderConstants.CredentialIdIntentExtra); |
||||
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>(); |
||||
var cipher = await cipherService.GetAsync(cipherId); |
||||
var decCipher = await cipher.DecryptAsync(); |
||||
|
||||
var passkey = decCipher.Login.Fido2Credentials.Find(f => f.CredentialId == credIdEnc); |
||||
|
||||
var credId = Convert.FromBase64String(credIdEnc); |
||||
// var privateKey = Convert.FromBase64String(passkey.PrivateKey); |
||||
// var uid = Convert.FromBase64String(passkey.uid); |
||||
|
||||
var origin = getRequest?.CallingAppInfo.Origin; |
||||
var packageName = getRequest?.CallingAppInfo.PackageName; |
||||
|
||||
// --- continue WIP here (save TOTP copy as last step) --- |
||||
|
||||
// Copy TOTP if needed |
||||
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>(); |
||||
autofillHandler.Autofill(decCipher); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,147 @@
@@ -0,0 +1,147 @@
|
||||
using Android; |
||||
using Android.App; |
||||
using Android.Content; |
||||
using Android.Graphics.Drawables; |
||||
using Android.OS; |
||||
using Android.Runtime; |
||||
using AndroidX.Credentials.Provider; |
||||
using Bit.Core.Abstractions; |
||||
using Bit.Core.Utilities; |
||||
using AndroidX.Credentials.Exceptions; |
||||
using AndroidX.Credentials.WebAuthn; |
||||
using Bit.Core.Models.View; |
||||
using Resource = Microsoft.Maui.Resource; |
||||
|
||||
namespace Bit.Droid.Autofill |
||||
{ |
||||
[Service(Permission = Manifest.Permission.BindCredentialProviderService, Label = "Bitwarden", Exported = true)] |
||||
[IntentFilter(new string[] { "android.service.credentials.CredentialProviderService" })] |
||||
[MetaData("android.credentials.provider", Resource = "@xml/provider")] |
||||
[Register("com.x8bit.bitwarden.Autofill.CredentialProviderService")] |
||||
public class CredentialProviderService : AndroidX.Credentials.Provider.CredentialProviderService |
||||
{ |
||||
private const string GetPasskeyIntentAction = "PACKAGE_NAME.GET_PASSKEY"; |
||||
private const int UniqueRequestCode = 94556023; |
||||
|
||||
private ICipherService _cipherService; |
||||
private IUserVerificationService _userVerificationService; |
||||
private IVaultTimeoutService _vaultTimeoutService; |
||||
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger"); |
||||
|
||||
public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request, |
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException(); |
||||
|
||||
public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request, |
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback) |
||||
{ |
||||
try |
||||
{ |
||||
_vaultTimeoutService ??= ServiceContainer.Resolve<IVaultTimeoutService>(); |
||||
|
||||
await _vaultTimeoutService.CheckVaultTimeoutAsync(); |
||||
var locked = await _vaultTimeoutService.IsLockedAsync(); |
||||
if (!locked) |
||||
{ |
||||
var response = await ProcessGetCredentialsRequestAsync(request); |
||||
callback.OnResult(response); |
||||
} |
||||
// TODO handle auth/unlock account flow |
||||
} |
||||
catch (GetCredentialException e) |
||||
{ |
||||
_logger.Value.Exception(e); |
||||
callback.OnError(e.ErrorMessage ?? "Error getting credentials"); |
||||
} |
||||
catch (Exception e) |
||||
{ |
||||
_logger.Value.Exception(e); |
||||
throw; |
||||
} |
||||
} |
||||
|
||||
private async Task<BeginGetCredentialResponse> ProcessGetCredentialsRequestAsync( |
||||
BeginGetCredentialRequest request) |
||||
{ |
||||
IList<CredentialEntry> credentialEntries = null; |
||||
|
||||
foreach (var option in request.BeginGetCredentialOptions) |
||||
{ |
||||
var credentialOption = option as BeginGetPublicKeyCredentialOption; |
||||
if (credentialOption != null) |
||||
{ |
||||
credentialEntries ??= new List<CredentialEntry>(); |
||||
((List<CredentialEntry>)credentialEntries).AddRange( |
||||
await PopulatePasskeyDataAsync(request.CallingAppInfo, credentialOption)); |
||||
} |
||||
} |
||||
|
||||
if (credentialEntries == null) |
||||
{ |
||||
return new BeginGetCredentialResponse(); |
||||
} |
||||
|
||||
return new BeginGetCredentialResponse.Builder() |
||||
.SetCredentialEntries(credentialEntries) |
||||
.Build(); |
||||
} |
||||
|
||||
private async Task<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo, |
||||
BeginGetPublicKeyCredentialOption option) |
||||
{ |
||||
var packageName = callingAppInfo.PackageName; |
||||
var origin = callingAppInfo.Origin; |
||||
var signingInfo = callingAppInfo.SigningInfo; |
||||
|
||||
var request = new PublicKeyCredentialRequestOptions(option.RequestJson); |
||||
|
||||
var passkeyEntries = new List<CredentialEntry>(); |
||||
|
||||
_cipherService ??= 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 PublicKeyCredentialEntry GetPasskey(CipherView cipher, BeginGetPublicKeyCredentialOption option) |
||||
{ |
||||
var credDataBundle = new Bundle(); |
||||
credDataBundle.PutString(CredentialProviderConstants.CredentialIdIntentExtra, |
||||
cipher.Login.MainFido2Credential.CredentialId); |
||||
|
||||
var intent = new Intent(ApplicationContext, typeof(CredentialProviderSelectionActivity)) |
||||
.SetAction(GetPasskeyIntentAction).SetPackage(Constants.PACKAGE_NAME); |
||||
intent.PutExtra(CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle); |
||||
intent.PutExtra(CredentialProviderConstants.CredentialProviderCipherId, cipher.Id); |
||||
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueRequestCode, intent, |
||||
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent); |
||||
|
||||
return new PublicKeyCredentialEntry.Builder( |
||||
ApplicationContext, |
||||
cipher.Login.Username ?? "No username", |
||||
pendingIntent, |
||||
option) |
||||
.SetDisplayName(cipher.Name) |
||||
.SetIcon(Icon.CreateWithResource(ApplicationContext, Resource.Drawable.icon)) |
||||
.Build(); |
||||
} |
||||
|
||||
public override void OnClearCredentialStateRequest(ProviderClearCredentialStateRequest request, |
||||
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException(); |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<capabilities> |
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" /> |
||||
</capabilities> |
||||
</credential-provider> |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
Additions allow you to add arbitrary C# to the generated classes |
||||
before they are compiled. This can be helpful for providing convenience |
||||
methods or adding pure C# classes. |
||||
|
||||
== Adding Methods to Generated Classes == |
||||
|
||||
Let's say the library being bound has a Rectangle class with a constructor |
||||
that takes an x and y position, and a width and length size. It will look like |
||||
this: |
||||
|
||||
public partial class Rectangle |
||||
{ |
||||
public Rectangle (int x, int y, int width, int height) |
||||
{ |
||||
// JNI bindings |
||||
} |
||||
} |
||||
|
||||
Imagine we want to add a constructor to this class that takes a Point and |
||||
Size structure instead of 4 ints. We can add a new file called Rectangle.cs |
||||
with a partial class containing our new method: |
||||
|
||||
public partial class Rectangle |
||||
{ |
||||
public Rectangle (Point location, Size size) : |
||||
this (location.X, location.Y, size.Width, size.Height) |
||||
{ |
||||
} |
||||
} |
||||
|
||||
At compile time, the additions class will be added to the generated class |
||||
and the final assembly will a Rectangle class with both constructors. |
||||
|
||||
|
||||
== Adding C# Classes == |
||||
|
||||
Another thing that can be done is adding fully C# managed classes to the |
||||
generated library. In the above example, let's assume that there isn't a |
||||
Point class available in Java or our library. The one we create doesn't need |
||||
to interact with Java, so we'll create it like a normal class in C#. |
||||
|
||||
By adding a Point.cs file with this class, it will end up in the binding library: |
||||
|
||||
public class Point |
||||
{ |
||||
public int X { get; set; } |
||||
public int Y { get; set; } |
||||
} |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
<enum-field-mappings> |
||||
<!-- |
||||
This example converts the constants Fragment_id, Fragment_name, |
||||
and Fragment_tag from android.support.v4.app.FragmentActivity.FragmentTag |
||||
to an enum called Android.Support.V4.App.FragmentTagType with values |
||||
Id, Name, and Tag. |
||||
|
||||
<mapping jni-class="android/support/v4/app/FragmentActivity$FragmentTag" clr-enum-type="Android.Support.V4.App.FragmentTagType"> |
||||
<field jni-name="Fragment_name" clr-name="Name" value="0" /> |
||||
<field jni-name="Fragment_id" clr-name="Id" value="1" /> |
||||
<field jni-name="Fragment_tag" clr-name="Tag" value="2" /> |
||||
</mapping> |
||||
--> |
||||
</enum-field-mappings> |
||||
|
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
<enum-method-mappings> |
||||
<!-- |
||||
This example changes the Java method: |
||||
android.support.v4.app.Fragment.SavedState.writeToParcel (int flags) |
||||
to be: |
||||
android.support.v4.app.Fragment.SavedState.writeToParcel (Android.OS.ParcelableWriteFlags flags) |
||||
when bound in C#. |
||||
|
||||
<mapping jni-class="android/support/v4/app/Fragment.SavedState"> |
||||
<method jni-name="writeToParcel" parameter="flags" clr-enum-type="Android.OS.ParcelableWriteFlags" /> |
||||
</mapping> |
||||
--> |
||||
</enum-method-mappings> |
||||
|
||||
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
<metadata> |
||||
<attr path="/api/package[@name='androidx.credentials']" name="managedName">AndroidX.Credentials</attr> |
||||
<attr path="/api/package[@name='androidx.credentials.provider']" name="managedName">AndroidX.Credentials.Provider</attr> |
||||
<attr path="/api/package[@name='androidx.credentials.exceptions']" name="managedName">AndroidX.Credentials.Exceptions</attr> |
||||
<attr path="/api/package[@name='androidx.credentials.webauthn']" name="managedName">AndroidX.Credentials.WebAuthn</attr> |
||||
|
||||
<!-- fix companions --> |
||||
<attr path="/api/package/class[substring(@name,string-length(@name)-9)='.Companion']" name="managedName">CompanionStatic</attr> |
||||
<remove-node path="/api/package/class[substring(@name,string-length(@name)-9)='.Companion' and count(method)=0 and count(field)=0]" /> |
||||
<attr path="/api/package/class[substring(@name,string-length(@name)-7)='.Default']" name="managedName">DefaultStatic</attr> |
||||
<remove-node path="/api/package/class[substring(@name,string-length(@name)-7)='.Default' and count(method)=0 and count(field)=0]" /> |
||||
</metadata> |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
<PropertyGroup> |
||||
<TargetFramework>net8.0-android</TargetFramework> |
||||
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion> |
||||
|
||||
|
||||
<!--<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion> |
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>--> |
||||
|
||||
<Nullable>enable</Nullable> |
||||
<ImplicitUsings>enable</ImplicitUsings> |
||||
<RootNamespace>XamarinBinding.AndroidX.Credentials</RootNamespace> |
||||
</PropertyGroup> |
||||
<ItemGroup> |
||||
|
||||
<PackageReference Include="Xamarin.Kotlin.StdLib" Version="1.9.10.1" /> |
||||
</ItemGroup> |
||||
</Project> |
||||
Binary file not shown.
Loading…
Reference in new issue