mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
289 Commits
bugfix/hel
...
temp-passk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3568a5bb7 | ||
|
|
25f63ec4e0 | ||
|
|
c11df272be | ||
|
|
a4adcdcfd4 | ||
|
|
c801b2fc3a | ||
|
|
41161864db | ||
|
|
a1c9ebf01f | ||
|
|
7381d5278a | ||
|
|
8b5a7b257d | ||
|
|
62e0626648 | ||
|
|
d339514d9a | ||
|
|
75ec96f282 | ||
|
|
3c848a3dcc | ||
|
|
18beb4e5f4 | ||
|
|
e9d1792dd7 | ||
|
|
8f8a5795d3 | ||
|
|
4631a9e62c | ||
|
|
51ee6a84b5 | ||
|
|
b8c5ef501a | ||
|
|
2075f8168c | ||
|
|
0f5df0f6b0 | ||
|
|
ad8faec200 | ||
|
|
3223ceb9a8 | ||
|
|
8d5006c0bd | ||
|
|
37208571fe | ||
|
|
7469dad35a | ||
|
|
759627b3c7 | ||
|
|
eae84ded42 | ||
|
|
563210a74e | ||
|
|
08fac4752f | ||
|
|
70db27b750 | ||
|
|
00cff182b4 | ||
|
|
dc5e90436b | ||
|
|
9307e7e0d8 | ||
|
|
b1a0801f9b | ||
|
|
04cc53b934 | ||
|
|
c138658a31 | ||
|
|
f1854f2c04 | ||
|
|
e4056d9ee6 | ||
|
|
eb95a54db2 | ||
|
|
7ddea4c70b | ||
|
|
3804e86995 | ||
|
|
b787a6c840 | ||
|
|
a6c4bc9273 | ||
|
|
b23bed182f | ||
|
|
f8e421871b | ||
|
|
4988dbea72 | ||
|
|
ca250c53ad | ||
|
|
6bb724ff06 | ||
|
|
f3c64a89eb | ||
|
|
aa71ebc634 | ||
|
|
d0bb7f0a54 | ||
|
|
d0103496b9 | ||
|
|
cd8952221e | ||
|
|
155c7539bd | ||
|
|
5f43681fb1 | ||
|
|
d2965e6e10 | ||
|
|
ec1ade7761 | ||
|
|
f35bef0d7b | ||
|
|
5d5d113369 | ||
|
|
7ca9e61e93 | ||
|
|
da7326b0cc | ||
|
|
138d37cf5e | ||
|
|
c87728027e | ||
|
|
fc2fed079f | ||
|
|
9c441a98f4 | ||
|
|
e1908d8eef | ||
|
|
8be604feac | ||
|
|
c90ed74faa | ||
|
|
1491872b62 | ||
|
|
c74636ffa5 | ||
|
|
05677f93c5 | ||
|
|
0aef241df6 | ||
|
|
32c43afae2 | ||
|
|
44b2443554 | ||
|
|
e0b58461b5 | ||
|
|
cd33c7f608 | ||
|
|
f0dde7eb82 | ||
|
|
19639b61c3 | ||
|
|
9d29af36e5 | ||
|
|
4472d7f9a8 | ||
|
|
999579915c | ||
|
|
63904fd303 | ||
|
|
ce550fee74 | ||
|
|
f0841eb8b2 | ||
|
|
b23d58c0b1 | ||
|
|
2cb6872e4e | ||
|
|
f539bf051d | ||
|
|
14f845d623 | ||
|
|
133a80acef | ||
|
|
e8f6c37c06 | ||
|
|
b43790de9a | ||
|
|
d0e0f0ecdb | ||
|
|
0bdd63df06 | ||
|
|
c6544b49e9 | ||
|
|
8e1a8b5f0e | ||
|
|
4717f5e230 | ||
|
|
ad80defa40 | ||
|
|
0dc281edc1 | ||
|
|
378551e2d5 | ||
|
|
dbe4110027 | ||
|
|
a08466d220 | ||
|
|
66a01e30d3 | ||
|
|
01ee1ff845 | ||
|
|
75b4655f38 | ||
|
|
9b2f596d15 | ||
|
|
cc89b6a5d5 | ||
|
|
32c2f2aac4 | ||
|
|
f9b4e30b0b | ||
|
|
55fb71744d | ||
|
|
ee252be634 | ||
|
|
66f0471f2e | ||
|
|
6b9eeba88d | ||
|
|
0a1fbfafb5 | ||
|
|
0a5d772886 | ||
|
|
70c8a264d2 | ||
|
|
b5fbb2cade | ||
|
|
9027755b71 | ||
|
|
6d625f285b | ||
|
|
822ad7564e | ||
|
|
1949a450fd | ||
|
|
27fa79e0bd | ||
|
|
1e29eacc61 | ||
|
|
b81d26d589 | ||
|
|
cd107b6161 | ||
|
|
7ac3646fb0 | ||
|
|
d1e4e8645a | ||
|
|
36a648e53e | ||
|
|
6c04ac67b1 | ||
|
|
dfb7a0621f | ||
|
|
1eb9e5f8ea | ||
|
|
b149e7549c | ||
|
|
e3877cc589 | ||
|
|
275ae76761 | ||
|
|
a1e4f0aaa2 | ||
|
|
adaef0d15b | ||
|
|
fa4a2247e3 | ||
|
|
5d2fc4530f | ||
|
|
9b64af3423 | ||
|
|
b6ff6e34f6 | ||
|
|
6d4c706026 | ||
|
|
14fd026ea0 | ||
|
|
a4392a8730 | ||
|
|
1b885ea438 | ||
|
|
fa022a1a4f | ||
|
|
6011b63958 | ||
|
|
7d79b98bf2 | ||
|
|
d4e75e9de8 | ||
|
|
c3370b58ec | ||
|
|
3de13325c9 | ||
|
|
c253c110c1 | ||
|
|
bf35d1f2dc | ||
|
|
05b6aa90b6 | ||
|
|
e39898bba6 | ||
|
|
da0866cc85 | ||
|
|
b3140381ab | ||
|
|
c01a8f8d93 | ||
|
|
8484b4af30 | ||
|
|
6b9faed45f | ||
|
|
770a1c5dfe | ||
|
|
3c87d4db1c | ||
|
|
8b3c6ab35f | ||
|
|
90912977c4 | ||
|
|
740b368b8c | ||
|
|
3a40a4cda8 | ||
|
|
9bcd2e51f7 | ||
|
|
741214a1cc | ||
|
|
aad87dfdce | ||
|
|
8fc1e9a3b9 | ||
|
|
2a8e15146e | ||
|
|
8559d5908e | ||
|
|
f60c4d94fe | ||
|
|
05858bea48 | ||
|
|
5cbef47fd4 | ||
|
|
4bf695d18c | ||
|
|
c24e0dfa28 | ||
|
|
9ccd0834ff | ||
|
|
a806f17d3b | ||
|
|
436a162df2 | ||
|
|
f2c298607e | ||
|
|
b5dbb9ae5e | ||
|
|
7a5f7c0274 | ||
|
|
17acb57732 | ||
|
|
5803635f44 | ||
|
|
19c393842f | ||
|
|
15a306490d | ||
|
|
a4a3d31c19 | ||
|
|
922dc683af | ||
|
|
bae1b3e891 | ||
|
|
a5888827c9 | ||
|
|
0348940a12 | ||
|
|
4c2998337d | ||
|
|
7ea86380f4 | ||
|
|
406f4425c8 | ||
|
|
95ca911444 | ||
|
|
fa62510e09 | ||
|
|
65dc73495d | ||
|
|
02a2e41118 | ||
|
|
bd6f8295e7 | ||
|
|
0a0cb7093b | ||
|
|
465e5eff76 | ||
|
|
5b756aaf7a | ||
|
|
d168a7b750 | ||
|
|
7f4bbafe3c | ||
|
|
a5804df6a3 | ||
|
|
bfa2a51608 | ||
|
|
32be08daae | ||
|
|
0a628cc8a8 | ||
|
|
80c424ed03 | ||
|
|
99fb5463cf | ||
|
|
c5d941e1df | ||
|
|
3edfef6169 | ||
|
|
1c8742511a | ||
|
|
8e424d6c05 | ||
|
|
390c303b90 | ||
|
|
443f7282b8 | ||
|
|
50109ee70b | ||
|
|
9ffdfd51cc | ||
|
|
04e409f3c6 | ||
|
|
6c143bad57 | ||
|
|
286e18059a | ||
|
|
553bf9ed0a | ||
|
|
ddb27b52d3 | ||
|
|
6c504aa710 | ||
|
|
62254aef8d | ||
|
|
06a0195a6d | ||
|
|
df2b0b21d5 | ||
|
|
e6b1bab860 | ||
|
|
ce41eb0578 | ||
|
|
1a0b52d644 | ||
|
|
16ada4993c | ||
|
|
3795f3aa17 | ||
|
|
eceb506c77 | ||
|
|
2c7870d660 | ||
|
|
f02b3415a3 | ||
|
|
beda4e9ff8 | ||
|
|
df4d89cd52 | ||
|
|
5f12bb9747 | ||
|
|
5712639492 | ||
|
|
e0a3c301fb | ||
|
|
27306fe353 | ||
|
|
a31f15559f | ||
|
|
0e75f3f5c8 | ||
|
|
363da063fa | ||
|
|
974a571455 | ||
|
|
e0c721098c | ||
|
|
a86f6e3034 | ||
|
|
fe17288b99 | ||
|
|
7324da9d47 | ||
|
|
69aa6fc044 | ||
|
|
e840dc2e30 | ||
|
|
eb25ee5d1b | ||
|
|
840f24dbe5 | ||
|
|
c6309173ba | ||
|
|
946c465f0c | ||
|
|
e90409d842 | ||
|
|
484b5a5160 | ||
|
|
2688209752 | ||
|
|
53e0e55915 | ||
|
|
ca57948d9f | ||
|
|
aaf082faba | ||
|
|
e7aeb08cae | ||
|
|
f177968958 | ||
|
|
f1d59210f9 | ||
|
|
62213c0aaf | ||
|
|
8be8abb8fe | ||
|
|
174acbc558 | ||
|
|
4bcc7c0d71 | ||
|
|
14b2960f30 | ||
|
|
455c3a257c | ||
|
|
8c623a2067 | ||
|
|
3cdf1c2f0e | ||
|
|
ce9503fa0c | ||
|
|
2e4da1b87d | ||
|
|
d63a219272 | ||
|
|
c92cd90a97 | ||
|
|
1dcd3a3daa | ||
|
|
efb8763d3c | ||
|
|
90649d1c8b | ||
|
|
828055791f | ||
|
|
87eebda55f | ||
|
|
7542d1ae1c | ||
|
|
990de4ea4e | ||
|
|
0dbc23f734 | ||
|
|
9f6c8601d3 | ||
|
|
8b7f9b9fb3 | ||
|
|
d17789d5ee | ||
|
|
b8f0747dd4 | ||
|
|
8ef9443b1e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -148,6 +148,7 @@ publish/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
!**/Xamarin.AndroidX.Credentials.1.0.0.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<MauiVersion>8.0.4-nightly.*</MauiVersion>
|
||||
<MauiVersion>8.0.7-nightly.*</MauiVersion>
|
||||
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
|
||||
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
|
||||
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>Xamarin.AndroidX.Credentials</name>
|
||||
</assembly>
|
||||
<members>
|
||||
</members>
|
||||
</doc>
|
||||
Binary file not shown.
@@ -2,5 +2,6 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="MAUI Nightly builds" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/maui-nightly/nuget/v3/index.json" />
|
||||
<add key="Local AndroidX Credentials" value="lib/android/Xamarin.AndroidX.Credentials" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@@ -121,6 +121,7 @@
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
||||
|
||||
@@ -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 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/App/Platforms/Android/Autofill/CredentialProviderService.cs
Normal file
147
src/App/Platforms/Android/Autofill/CredentialProviderService.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
6
src/App/Platforms/Android/Resources/xml/provider.xml
Normal file
6
src/App/Platforms/Android/Resources/xml/provider.xml
Normal file
@@ -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>
|
||||
@@ -37,6 +37,23 @@ namespace Bit.Droid.Services
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
public bool CredentialProviderServiceEnabled()
|
||||
{
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.UpsideDownCake)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
// TODO - find a way to programmatically check if the credential provider service is enabled
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutofillServiceEnabled()
|
||||
{
|
||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||
@@ -163,7 +180,14 @@ namespace Bit.Droid.Services
|
||||
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
||||
}
|
||||
|
||||
|
||||
public void DisableCredentialProviderService()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO - find a way to programmatically disable the provider service, or take the user to the settings page where they can do it
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public void DisableAutofillService()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ using Android.Text.Method;
|
||||
using Android.Views;
|
||||
using Android.Views.InputMethods;
|
||||
using Android.Widget;
|
||||
using AndroidX.Credentials;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
@@ -490,6 +491,27 @@ namespace Bit.Droid.Services
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenCredentialProviderSettings()
|
||||
{
|
||||
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
|
||||
try
|
||||
{
|
||||
var pendingIntent = CredentialManager.Create(activity).CreateSettingsPendingIntent();
|
||||
pendingIntent.Send();
|
||||
}
|
||||
catch (ActivityNotFoundException)
|
||||
{
|
||||
var alertBuilder = new AlertDialog.Builder(activity);
|
||||
alertBuilder.SetMessage(AppResources.BitwardenCredentialProviderGoToSettings);
|
||||
alertBuilder.SetCancelable(true);
|
||||
alertBuilder.SetPositiveButton(AppResources.Ok, (sender, args) =>
|
||||
{
|
||||
(sender as AlertDialog)?.Cancel();
|
||||
});
|
||||
alertBuilder.Create().Show();
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenAccessibilitySettings()
|
||||
{
|
||||
try
|
||||
@@ -548,6 +570,8 @@ namespace Bit.Droid.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SupportsCredentialProviderService() => Build.VERSION.SdkInt >= BuildVersionCodes.UpsideDownCake;
|
||||
|
||||
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
|
||||
|
||||
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Bit.iOS
|
||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||
if (needsAutofillReplacement.GetValueOrDefault())
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else if (message.Command == "showAppExtension")
|
||||
@@ -102,7 +102,7 @@ namespace Bit.iOS
|
||||
var success = value as bool?;
|
||||
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,22 +114,21 @@ namespace Bit.iOS
|
||||
return;
|
||||
}
|
||||
|
||||
if (await ASHelpers.IdentitiesCanIncremental())
|
||||
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||
{
|
||||
var cipherId = message.Data as string;
|
||||
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
||||
{
|
||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherId);
|
||||
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
|
||||
if (identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync(
|
||||
new ASPasswordCredentialIdentity[] { identity });
|
||||
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
||||
{
|
||||
@@ -138,28 +137,27 @@ namespace Bit.iOS
|
||||
return;
|
||||
}
|
||||
|
||||
if (await ASHelpers.IdentitiesCanIncremental())
|
||||
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||
{
|
||||
var identity = ASHelpers.ToCredentialIdentity(
|
||||
var identity = ASHelpers.ToPasswordCredentialIdentity(
|
||||
message.Data as Bit.Core.Models.View.CipherView);
|
||||
if (identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync(
|
||||
new ASPasswordCredentialIdentity[] { identity });
|
||||
await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
|
||||
return;
|
||||
}
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
||||
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
||||
{
|
||||
@@ -168,12 +166,12 @@ namespace Bit.iOS
|
||||
{
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IAutofillHandler
|
||||
{
|
||||
bool CredentialProviderServiceEnabled();
|
||||
bool AutofillServicesEnabled();
|
||||
bool SupportsAutofillService();
|
||||
void Autofill(CipherView cipher);
|
||||
@@ -11,6 +12,7 @@ namespace Bit.Core.Abstractions
|
||||
bool AutofillAccessibilityServiceRunning();
|
||||
bool AutofillAccessibilityOverlayPermitted();
|
||||
bool AutofillServiceEnabled();
|
||||
void DisableCredentialProviderService();
|
||||
void DisableAutofillService();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.View;
|
||||
@@ -37,5 +34,6 @@ namespace Bit.Core.Abstractions
|
||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
||||
Task SoftDeleteWithServerAsync(string id);
|
||||
Task RestoreWithServerAsync(string id);
|
||||
Task<string> CreateNewLoginForPasskeyAsync(string rpId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Bit.App.Abstractions
|
||||
bool SupportsNfc();
|
||||
bool SupportsCamera();
|
||||
bool SupportsFido2();
|
||||
bool SupportsCredentialProviderService();
|
||||
bool SupportsAutofillServices();
|
||||
bool SupportsInlineAutofill();
|
||||
bool SupportsDrawOver();
|
||||
@@ -36,6 +37,7 @@ namespace Bit.App.Abstractions
|
||||
void RateApp();
|
||||
void OpenAccessibilitySettings();
|
||||
void OpenAccessibilityOverlayPermissionSettings();
|
||||
void OpenCredentialProviderSettings();
|
||||
void OpenAutofillSettings();
|
||||
long GetActiveTime();
|
||||
void CloseMainApp();
|
||||
|
||||
9
src/Core/Abstractions/IFido2AuthenticationService.cs
Normal file
9
src/Core/Abstractions/IFido2AuthenticationService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IFido2AuthenticationService
|
||||
{
|
||||
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams);
|
||||
}
|
||||
}
|
||||
13
src/Core/Abstractions/IFido2AuthenticatorService.cs
Normal file
13
src/Core/Abstractions/IFido2AuthenticatorService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IFido2AuthenticatorService
|
||||
{
|
||||
void Init(IFido2UserInterface userInterface);
|
||||
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams);
|
||||
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams);
|
||||
// TODO: Should this return a List? Or maybe IEnumerable?
|
||||
Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId);
|
||||
}
|
||||
}
|
||||
35
src/Core/Abstractions/IFido2ClientService.cs
Normal file
35
src/Core/Abstractions/IFido2ClientService.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents an abstraction of the WebAuthn Client as described by W3C:
|
||||
/// https://www.w3.org/TR/webauthn-3/#webauthn-client
|
||||
///
|
||||
/// The WebAuthn Client is an intermediary entity typically implemented in the user agent
|
||||
/// (in whole, or in part). Conceptually, it underlies the Web Authentication API and embodies
|
||||
/// the implementation of the Web Authentication API's operations.
|
||||
///
|
||||
/// It is responsible for both marshalling the inputs for the underlying authenticator operations,
|
||||
/// and for returning the results of the latter operations to the Web Authentication API's callers.
|
||||
/// </summary>
|
||||
public interface IFido2ClientService
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
|
||||
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential
|
||||
/// </summary>
|
||||
/// <param name="createCredentialParams">The parameters for the credential creation operation</param>
|
||||
/// <returns>The new credential</returns>
|
||||
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams);
|
||||
|
||||
/// <summary>
|
||||
/// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the user’s consent.
|
||||
/// Relying Party script can optionally specify some criteria to indicate what credential sources are acceptable to it.
|
||||
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-getAssertion
|
||||
/// </summary>
|
||||
/// <param name="assertCredentialParams">The parameters for the credential assertion operation</param>
|
||||
/// <returns>The asserted credential</returns>
|
||||
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams);
|
||||
}
|
||||
}
|
||||
100
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
100
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters used to ask the user to pick a credential from a list of existing credentials.
|
||||
/// </summary>
|
||||
public struct Fido2PickCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The IDs of the credentials that the user can pick from.
|
||||
/// </summary>
|
||||
public string[] CipherIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the user must be verified before completing the operation.
|
||||
/// </summary>
|
||||
public bool UserVerification { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result of asking the user to pick a credential from a list of existing credentials.
|
||||
/// </summary>
|
||||
public struct Fido2PickCredentialResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the cipher that contains the credentials the user picked.
|
||||
/// </summary>
|
||||
public string CipherId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the user was verified before completing the operation.
|
||||
/// </summary>
|
||||
public bool UserVerified { get; set; }
|
||||
}
|
||||
|
||||
public struct Fido2ConfirmNewCredentialParams
|
||||
{
|
||||
///<summary>
|
||||
/// The name of the credential.
|
||||
///</summary>
|
||||
public string CredentialName { get; set; }
|
||||
|
||||
///<summary>
|
||||
/// The name of the user.
|
||||
///</summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the user must be verified before completing the operation.
|
||||
/// </summary>
|
||||
public bool UserVerification { get; set; }
|
||||
|
||||
public string RpId { get; set; }
|
||||
}
|
||||
|
||||
public struct Fido2ConfirmNewCredentialResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the user.
|
||||
/// </summary>
|
||||
public string CipherId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the user was verified.
|
||||
/// </summary>
|
||||
public bool UserVerified { get; set; }
|
||||
}
|
||||
|
||||
public interface IFido2UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Ask the user to pick a credential from a list of existing credentials.
|
||||
/// </summary>
|
||||
/// <param name="pickCredentialParams">The parameters to use when asking the user to pick a credential.</param>
|
||||
/// <returns>The ID of the cipher that contains the credentials the user picked.</returns>
|
||||
Task<Fido2PickCredentialResult> PickCredentialAsync(Fido2PickCredentialParams pickCredentialParams);
|
||||
|
||||
/// <summary>
|
||||
/// Inform the user that the operation was cancelled because their vault contains excluded credentials.
|
||||
/// </summary>
|
||||
/// <param name="existingCipherIds">The IDs of the excluded credentials.</param>
|
||||
/// <returns>When user has confirmed the message</returns>
|
||||
Task InformExcludedCredential(string[] existingCipherIds);
|
||||
|
||||
/// <summary>
|
||||
/// Ask the user to confirm the creation of a new credential.
|
||||
/// </summary>
|
||||
/// <param name="confirmNewCredentialParams">The parameters to use when asking the user to confirm the creation of a new credential.</param>
|
||||
/// <returns>The ID of the cipher where the new credential should be saved.</returns>
|
||||
Task<Fido2ConfirmNewCredentialResult> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams);
|
||||
|
||||
/// <summary>
|
||||
/// Make sure that the vault is unlocked.
|
||||
/// This should open a window and ask the user to login or unlock the vault if necessary.
|
||||
/// </summary>
|
||||
/// <returns>When vault has been unlocked.</returns>
|
||||
Task EnsureUnlockedVaultAsync();
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||
<PackageReference Include="PCLCrypto" Version="2.1.40-alpha" />
|
||||
<PackageReference Include="System.Formats.Cbor" Version="8.0.0" />
|
||||
<PackageReference Include="zxcvbn-core" Version="7.0.92" />
|
||||
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -52,6 +53,7 @@
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Activity.Ktx" Version="1.7.2.1" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
||||
@@ -75,6 +77,7 @@
|
||||
<Folder Include="Utilities\Automation\" />
|
||||
<Folder Include="Utilities\Prompts\" />
|
||||
<Folder Include="Resources\Localization\" />
|
||||
<Folder Include="Utilities\Fido2\" />
|
||||
<Folder Include="Controls\Picker\" />
|
||||
<Folder Include="Controls\Avatar\" />
|
||||
</ItemGroup>
|
||||
@@ -105,6 +108,7 @@
|
||||
</MauiXaml>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Utilities\Fido2\" />
|
||||
<None Remove="Controls\Picker\" />
|
||||
<None Remove="Controls\Avatar\" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.Api
|
||||
{
|
||||
@@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api
|
||||
RpName = fido2Key.RpName?.EncryptedString;
|
||||
UserHandle = fido2Key.UserHandle?.EncryptedString;
|
||||
UserName = fido2Key.UserName?.EncryptedString;
|
||||
UserDisplayName = fido2Key.UserDisplayName?.EncryptedString;
|
||||
Counter = fido2Key.Counter?.EncryptedString;
|
||||
CreationDate = fido2Key.CreationDate;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
|
||||
public string RpName { get; set; }
|
||||
public string UserHandle { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserDisplayName { get; set; }
|
||||
public string Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data
|
||||
RpName = apiData.RpName;
|
||||
UserHandle = apiData.UserHandle;
|
||||
UserName = apiData.UserName;
|
||||
UserDisplayName = apiData.UserDisplayName;
|
||||
Counter = apiData.Counter;
|
||||
CreationDate = apiData.CreationDate;
|
||||
}
|
||||
@@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
|
||||
public string RpName { get; set; }
|
||||
public string UserHandle { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserDisplayName { get; set; }
|
||||
public string Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
@@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain
|
||||
nameof(RpName),
|
||||
nameof(UserHandle),
|
||||
nameof(UserName),
|
||||
nameof(UserDisplayName),
|
||||
nameof(Counter)
|
||||
};
|
||||
|
||||
@@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain
|
||||
public EncString RpName { get; set; }
|
||||
public EncString UserHandle { get; set; }
|
||||
public EncString UserName { get; set; }
|
||||
public EncString UserDisplayName { get; set; }
|
||||
public EncString Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
{
|
||||
@@ -26,13 +26,40 @@ namespace Bit.Core.Models.View
|
||||
public string RpName { get; set; }
|
||||
public string UserHandle { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string UserDisplayName { get; set; }
|
||||
public string Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public int CounterValue {
|
||||
get => int.TryParse(Counter, out var counter) ? counter : 0;
|
||||
set => Counter = value.ToString();
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public byte[] UserHandleValue {
|
||||
get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle);
|
||||
set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public byte[] KeyBytes {
|
||||
get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue);
|
||||
set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool DiscoverableValue {
|
||||
get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
|
||||
set => Discoverable = value.ToString().ToLower(); // must be lowercase so it can be parsed in the current version of clients
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override string SubTitle => UserName;
|
||||
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
|
||||
public bool IsDiscoverable => !string.IsNullOrWhiteSpace(Discoverable);
|
||||
[JsonIgnore]
|
||||
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
|
||||
[JsonIgnore]
|
||||
public string LaunchUri => $"https://{RpId}";
|
||||
|
||||
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
<ScrollView>
|
||||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="MAUI APP"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
</StackLayout>
|
||||
<StackLayout StyleClass="box-row-header">
|
||||
<Label Text="{u:I18n SelfHostedEnvironment, Header=True}"
|
||||
StyleClass="box-header, box-header-platform" />
|
||||
|
||||
@@ -68,7 +68,8 @@ namespace Bit.App.Pages
|
||||
{
|
||||
get
|
||||
{
|
||||
var appInfo = string.Format("{0}: {1} ({2})",
|
||||
// TODO: REMOVE WHEN MERGED INTO MAIN BRANCH
|
||||
var appInfo = string.Format("MAUI {0}: {1} ({2})",
|
||||
AppResources.Version,
|
||||
_platformUtilsService.GetApplicationVersion(),
|
||||
_deviceActionService.GetBuildNumber());
|
||||
|
||||
@@ -19,6 +19,15 @@
|
||||
Text="{u:I18n Autofill}"
|
||||
StyleClass="settings-header" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n CredentialProviderService}"
|
||||
Subtitle="{u:I18n CredentialProviderServiceExplanationLong}"
|
||||
IsVisible="{Binding SupportsCredentialProviderService}"
|
||||
IsToggled="{Binding UseCredentialProviderService}"
|
||||
AutomationId="CredentialProviderServiceSwitch"
|
||||
StyleClass="settings-item-view"
|
||||
HorizontalOptions="FillAndExpand" />
|
||||
|
||||
<controls:SwitchItemView
|
||||
Title="{u:I18n AutofillServices}"
|
||||
Subtitle="{u:I18n AutofillServicesExplanationLong}"
|
||||
|
||||
@@ -6,12 +6,27 @@ namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AutofillSettingsPageViewModel
|
||||
{
|
||||
private bool _useCredentialProviderService;
|
||||
private bool _useAutofillServices;
|
||||
private bool _useInlineAutofill;
|
||||
private bool _useAccessibility;
|
||||
private bool _useDrawOver;
|
||||
private bool _askToAddLogin;
|
||||
|
||||
public bool SupportsCredentialProviderService => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsCredentialProviderService();
|
||||
|
||||
public bool UseCredentialProviderService
|
||||
{
|
||||
get => _useCredentialProviderService;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _useCredentialProviderService, value))
|
||||
{
|
||||
((ICommand)ToggleUseCredentialProviderServiceCommand).Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsAndroidAutofillServices => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsAutofillServices();
|
||||
|
||||
public bool UseAutofillServices
|
||||
@@ -84,6 +99,7 @@ namespace Bit.App.Pages
|
||||
}
|
||||
}
|
||||
|
||||
public AsyncRelayCommand ToggleUseCredentialProviderServiceCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseAutofillServicesCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseInlineAutofillCommand { get; private set; }
|
||||
public AsyncRelayCommand ToggleUseAccessibilityCommand { get; private set; }
|
||||
@@ -93,6 +109,7 @@ namespace Bit.App.Pages
|
||||
|
||||
private void InitAndroidCommands()
|
||||
{
|
||||
ToggleUseCredentialProviderServiceCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseCredentialProviderService()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseAutofillServicesCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseInlineAutofillCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited, allowsMultipleExecutions: false);
|
||||
ToggleUseAccessibilityCommand = CreateDefaultAsyncRelayCommand(ToggleUseAccessibilityAsync, () => _inited, allowsMultipleExecutions: false);
|
||||
@@ -115,6 +132,9 @@ namespace Bit.App.Pages
|
||||
|
||||
private async Task UpdateAndroidAutofillSettingsAsync()
|
||||
{
|
||||
// TODO - uncomment once _autofillHandler.CredentialProviderServiceEnabled() returns a real value
|
||||
// _useCredentialProviderService =
|
||||
// SupportsCredentialProviderService && _autofillHandler.CredentialProviderServiceEnabled();
|
||||
_useAutofillServices =
|
||||
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
||||
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
|
||||
@@ -123,6 +143,7 @@ namespace Bit.App.Pages
|
||||
|
||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||
{
|
||||
TriggerPropertyChanged(nameof(UseCredentialProviderService));
|
||||
TriggerPropertyChanged(nameof(UseAutofillServices));
|
||||
TriggerPropertyChanged(nameof(UseAccessibility));
|
||||
TriggerPropertyChanged(nameof(UseDrawOver));
|
||||
@@ -130,6 +151,18 @@ namespace Bit.App.Pages
|
||||
});
|
||||
}
|
||||
|
||||
private void ToggleUseCredentialProviderService()
|
||||
{
|
||||
if (UseCredentialProviderService)
|
||||
{
|
||||
_deviceActionService.OpenCredentialProviderSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
_autofillHandler.DisableCredentialProviderService();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleUseAutofillServices()
|
||||
{
|
||||
if (UseAutofillServices)
|
||||
|
||||
@@ -1318,6 +1318,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to We were unable to automatically open the Android credential provider settings menu for you. You can navigate to the credential provider settings menu manually from Android Settings > System > Passwords & accounts > Passwords, passkeys and data services..
|
||||
/// </summary>
|
||||
public static string BitwardenCredentialProviderGoToSettings {
|
||||
get {
|
||||
return ResourceManager.GetString("BitwardenCredentialProviderGoToSettings", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bitwarden Help Center.
|
||||
/// </summary>
|
||||
@@ -1534,6 +1543,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Choose a login to save this passkey to.
|
||||
/// </summary>
|
||||
public static string ChooseALoginToSaveThisPasskeyTo {
|
||||
get {
|
||||
return ResourceManager.GetString("ChooseALoginToSaveThisPasskeyTo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Choose file.
|
||||
/// </summary>
|
||||
@@ -1894,6 +1912,24 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Credential Provider service.
|
||||
/// </summary>
|
||||
public static string CredentialProviderService {
|
||||
get {
|
||||
return ResourceManager.GetString("CredentialProviderService", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device..
|
||||
/// </summary>
|
||||
public static string CredentialProviderServiceExplanationLong {
|
||||
get {
|
||||
return ResourceManager.GetString("CredentialProviderServiceExplanationLong", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Credits.
|
||||
/// </summary>
|
||||
@@ -5101,6 +5137,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Overwrite passkey?.
|
||||
/// </summary>
|
||||
public static string OverwritePasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("OverwritePasskey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Ownership.
|
||||
/// </summary>
|
||||
@@ -5128,6 +5173,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passkeys for {0}.
|
||||
/// </summary>
|
||||
public static string PasskeysForX {
|
||||
get {
|
||||
return ResourceManager.GetString("PasskeysForX", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passkey will not be copied.
|
||||
/// </summary>
|
||||
@@ -5317,6 +5371,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passwords for {0}.
|
||||
/// </summary>
|
||||
public static string PasswordsForX {
|
||||
get {
|
||||
return ResourceManager.GetString("PasswordsForX", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Password type.
|
||||
/// </summary>
|
||||
@@ -5822,6 +5885,24 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save passkey.
|
||||
/// </summary>
|
||||
public static string SavePasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("SavePasskey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save passkey as new login.
|
||||
/// </summary>
|
||||
public static string SavePasskeyAsNewLogin {
|
||||
get {
|
||||
return ResourceManager.GetString("SavePasskeyAsNewLogin", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Saving....
|
||||
/// </summary>
|
||||
@@ -6695,6 +6776,15 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This item already contains a passkey. Are you sure you want to overwrite the current passkey?.
|
||||
/// </summary>
|
||||
public static string ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to This request is no longer valid.
|
||||
/// </summary>
|
||||
|
||||
@@ -1191,6 +1191,9 @@ Scanning will happen automatically.</value>
|
||||
<data name="WindowsHello" xml:space="preserve">
|
||||
<value>Windows Hello</value>
|
||||
</data>
|
||||
<data name="BitwardenCredentialProviderGoToSettings" xml:space="preserve">
|
||||
<value>We were unable to automatically open the Android credential provider settings menu for you. You can navigate to the credential provider settings menu manually from Android Settings > System > Passwords & accounts > Passwords, passkeys and data services.</value>
|
||||
</data>
|
||||
<data name="BitwardenAutofillGoToSettings" xml:space="preserve">
|
||||
<value>We were unable to automatically open the Android autofill settings menu for you. You can navigate to the autofill settings menu manually from Android Settings > System > Languages and input > Advanced > Autofill service.</value>
|
||||
</data>
|
||||
@@ -1816,6 +1819,9 @@ Scanning will happen automatically.</value>
|
||||
<data name="AccessibilityDrawOverPermissionAlert" xml:space="preserve">
|
||||
<value>Bitwarden needs attention - Turn on "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value>
|
||||
</data>
|
||||
<data name="CredentialProviderService" xml:space="preserve">
|
||||
<value>Credential Provider service</value>
|
||||
</data>
|
||||
<data name="AutofillServices" xml:space="preserve">
|
||||
<value>Auto-fill services</value>
|
||||
</data>
|
||||
@@ -2799,6 +2805,9 @@ Do you want to switch to this account?</value>
|
||||
<data name="XHours" xml:space="preserve">
|
||||
<value>{0} hours</value>
|
||||
</data>
|
||||
<data name="CredentialProviderServiceExplanationLong" xml:space="preserve">
|
||||
<value>The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device.</value>
|
||||
</data>
|
||||
<data name="AutofillServicesExplanationLong" xml:space="preserve">
|
||||
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||
</data>
|
||||
@@ -2877,4 +2886,25 @@ Do you want to switch to this account?</value>
|
||||
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
||||
<value>Set up an unlock option to change your vault timeout action.</value>
|
||||
</data>
|
||||
<data name="ChooseALoginToSaveThisPasskeyTo" xml:space="preserve">
|
||||
<value>Choose a login to save this passkey to</value>
|
||||
</data>
|
||||
<data name="SavePasskeyAsNewLogin" xml:space="preserve">
|
||||
<value>Save passkey as new login</value>
|
||||
</data>
|
||||
<data name="SavePasskey" xml:space="preserve">
|
||||
<value>Save passkey</value>
|
||||
</data>
|
||||
<data name="PasskeysForX" xml:space="preserve">
|
||||
<value>Passkeys for {0}</value>
|
||||
</data>
|
||||
<data name="PasswordsForX" xml:space="preserve">
|
||||
<value>Passwords for {0}</value>
|
||||
</data>
|
||||
<data name="OverwritePasskey" xml:space="preserve">
|
||||
<value>Overwrite passkey?</value>
|
||||
</data>
|
||||
<data name="ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey" xml:space="preserve">
|
||||
<value>This item already contains a passkey. Are you sure you want to overwrite the current passkey?</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1286,6 +1286,34 @@ namespace Bit.Core.Services
|
||||
cipher.PasswordHistory = encPhs;
|
||||
}
|
||||
|
||||
public async Task<string> CreateNewLoginForPasskeyAsync(string rpId)
|
||||
{
|
||||
var newCipher = new CipherView
|
||||
{
|
||||
Name = rpId,
|
||||
Type = CipherType.Login,
|
||||
Login = new LoginView
|
||||
{
|
||||
Uris = new List<LoginUriView>
|
||||
{
|
||||
new LoginUriView { Uri = rpId }
|
||||
}
|
||||
},
|
||||
Card = new CardView(),
|
||||
Identity = new IdentityView(),
|
||||
SecureNote = new SecureNoteView
|
||||
{
|
||||
Type = SecureNoteType.Generic
|
||||
},
|
||||
Reprompt = CipherRepromptType.None
|
||||
};
|
||||
|
||||
var encryptedCipher = await EncryptAsync(newCipher);
|
||||
await SaveWithServerAsync(encryptedCipher);
|
||||
|
||||
return encryptedCipher.Id;
|
||||
}
|
||||
|
||||
private class CipherLocaleComparer : IComparer<CipherView>
|
||||
{
|
||||
private readonly II18nService _i18nService;
|
||||
|
||||
18
src/Core/Services/Fido2AuthenticationService.cs
Normal file
18
src/Core/Services/Fido2AuthenticationService.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class Fido2AuthenticationService : IFido2AuthenticationService
|
||||
{
|
||||
public Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
|
||||
{
|
||||
// TODO: IMPLEMENT this
|
||||
return Task.FromResult(new Fido2AuthenticatorGetAssertionResult
|
||||
{
|
||||
AuthenticatorData = new byte[32],
|
||||
Signature = new byte[8]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
574
src/Core/Services/Fido2AuthenticatorService.cs
Normal file
574
src/Core/Services/Fido2AuthenticatorService.cs
Normal file
@@ -0,0 +1,574 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Formats.Cbor;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class Fido2AuthenticatorService: IFido2AuthenticatorService
|
||||
{
|
||||
// AAGUID: d548826e-79b4-db40-a3d8-11116f7e8349
|
||||
public static readonly byte[] AAGUID = new byte[] { 0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49 };
|
||||
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private IFido2UserInterface _userInterface;
|
||||
|
||||
public Fido2AuthenticatorService(ICipherService cipherService, ISyncService syncService, ICryptoFunctionService cryptoFunctionService)
|
||||
{
|
||||
_cipherService = cipherService;
|
||||
_syncService = syncService;
|
||||
_cryptoFunctionService = cryptoFunctionService;
|
||||
}
|
||||
|
||||
public void Init(IFido2UserInterface userInterface)
|
||||
{
|
||||
_userInterface = userInterface;
|
||||
}
|
||||
|
||||
public async Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams)
|
||||
{
|
||||
if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Alg != (int) Fido2AlgorithmIdentifier.ES256))
|
||||
{
|
||||
// var requestedAlgorithms = string.Join(", ", makeCredentialParams.CredTypesAndPubKeyAlgs.Select((p) => p.Algorithm).ToArray());
|
||||
// _logService.Warning(
|
||||
// $"[Fido2Authenticator] No compatible algorithms found, RP requested: {requestedAlgorithms}"
|
||||
// );
|
||||
ClipLogger.Log("[Fido2Authenticator] No compatible algorithms found, RP requested: {requestedAlgorithms}");
|
||||
throw new NotSupportedError();
|
||||
}
|
||||
|
||||
await _userInterface.EnsureUnlockedVaultAsync();
|
||||
await _syncService.FullSyncAsync(false);
|
||||
|
||||
var existingCipherIds = await FindExcludedCredentialsAsync(
|
||||
makeCredentialParams.ExcludeCredentialDescriptorList
|
||||
);
|
||||
if (existingCipherIds.Length > 0) {
|
||||
// _logService.Info(
|
||||
// "[Fido2Authenticator] Aborting due to excluded credential found in vault."
|
||||
// );
|
||||
ClipLogger.Log("[Fido2Authenticator] Aborting due to excluded credential found in vault");
|
||||
await _userInterface.InformExcludedCredential(existingCipherIds);
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
var response = await _userInterface.ConfirmNewCredentialAsync(new Fido2ConfirmNewCredentialParams {
|
||||
CredentialName = makeCredentialParams.RpEntity.Name,
|
||||
UserName = makeCredentialParams.UserEntity.Name,
|
||||
UserVerification = makeCredentialParams.RequireUserVerification,
|
||||
RpId = makeCredentialParams.RpEntity.Id
|
||||
});
|
||||
|
||||
var cipherId = response.CipherId;
|
||||
var userVerified = response.UserVerified;
|
||||
string credentialId;
|
||||
if (cipherId == null) {
|
||||
// _logService.Info(
|
||||
// "[Fido2Authenticator] Aborting because user confirmation was not recieved."
|
||||
// );
|
||||
ClipLogger.Log("[Fido2Authenticator] Aborting because user confirmation was not recieved");
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
try {
|
||||
var keyPair = GenerateKeyPair();
|
||||
var fido2Credential = CreateCredentialView(makeCredentialParams, keyPair.privateKey);
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] IsDiscoverable {fido2Credential.Discoverable} - {fido2Credential.DiscoverableValue}");
|
||||
|
||||
var encrypted = await _cipherService.GetAsync(cipherId);
|
||||
var cipher = await encrypted.DecryptAsync();
|
||||
|
||||
if (!userVerified && (makeCredentialParams.RequireUserVerification || cipher.Reprompt != CipherRepromptType.None)) {
|
||||
// _logService.Info(
|
||||
// "[Fido2Authenticator] Aborting because user verification was unsuccessful."
|
||||
// );
|
||||
ClipLogger.Log("[Fido2Authenticator] Aborting because user verification was unsuccessful");
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
cipher.Login.Fido2Credentials = new List<Fido2CredentialView> { fido2Credential };
|
||||
var reencrypted = await _cipherService.EncryptAsync(cipher);
|
||||
await _cipherService.SaveWithServerAsync(reencrypted);
|
||||
credentialId = fido2Credential.CredentialId;
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] IsDiscoverable {cipher.Login.MainFido2Credential.Discoverable} - {cipher.Login.MainFido2Credential.DiscoverableValue}");
|
||||
|
||||
var authData = await GenerateAuthDataAsync(
|
||||
rpId: makeCredentialParams.RpEntity.Id,
|
||||
counter: fido2Credential.CounterValue,
|
||||
userPresence: true,
|
||||
userVerification: userVerified,
|
||||
credentialId: credentialId.GuidToRawFormat(),
|
||||
publicKey: keyPair.publicKey
|
||||
);
|
||||
|
||||
return new Fido2AuthenticatorMakeCredentialResult
|
||||
{
|
||||
CredentialId = credentialId.GuidToRawFormat(),
|
||||
AttestationObject = EncodeAttestationObject(authData),
|
||||
AuthData = authData,
|
||||
PublicKey = keyPair.publicKey.ExportDer(),
|
||||
PublicKeyAlgorithm = (int) Fido2AlgorithmIdentifier.ES256,
|
||||
};
|
||||
} catch (NotAllowedError) {
|
||||
throw;
|
||||
} catch (Exception e) {
|
||||
// _logService.Error(
|
||||
// $"[Fido2Authenticator] Unknown error occured during attestation: {e.Message}"
|
||||
// );
|
||||
ClipLogger.Log("[Fido2Authenticator] Unknown error occured during attestation: {e.Message}");
|
||||
|
||||
throw new UnknownError();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
|
||||
{
|
||||
List<CipherView> cipherOptions;
|
||||
|
||||
await _userInterface.EnsureUnlockedVaultAsync();
|
||||
await _syncService.FullSyncAsync(false);
|
||||
|
||||
if (assertionParams.AllowCredentialDescriptorList?.Length > 0) {
|
||||
|
||||
ClipLogger.Log("[Fido2Authenticator] Finding credentials with credential descriptor list");
|
||||
|
||||
cipherOptions = await FindCredentialsByIdAsync(
|
||||
assertionParams.AllowCredentialDescriptorList,
|
||||
assertionParams.RpId
|
||||
);
|
||||
} else
|
||||
{
|
||||
ClipLogger.Log("[Fido2Authenticator] Finding credentials with RP");
|
||||
cipherOptions = await FindCredentialsByRpAsync(assertionParams.RpId);
|
||||
}
|
||||
|
||||
if (cipherOptions.Count == 0) {
|
||||
// _logService.Info(
|
||||
// "[Fido2Authenticator] Aborting because no matching credentials were found in the vault."
|
||||
// );
|
||||
ClipLogger.Log("[Fido2Authenticator] Aborting because no matching credentials were found in the vault");
|
||||
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
string selectedCipherId;
|
||||
bool userVerified;
|
||||
bool userPresence;
|
||||
// TODO: We might want reconsider allowing user presence to be optional
|
||||
if (assertionParams.AllowCredentialDescriptorList?.Length == 1 && assertionParams.RequireUserPresence == false)
|
||||
{
|
||||
ClipLogger.Log("[Fido2Authenticator] AllowCredentialDescriptorList + RequireUserPresence false");
|
||||
selectedCipherId = cipherOptions[0].Id;
|
||||
userVerified = false;
|
||||
userPresence = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ClipLogger.Log("[Fido2Authenticator] PickCredentialAsync");
|
||||
|
||||
var response = await _userInterface.PickCredentialAsync(new Fido2PickCredentialParams {
|
||||
CipherIds = cipherOptions.Select((cipher) => cipher.Id).ToArray(),
|
||||
UserVerification = assertionParams.RequireUserVerification
|
||||
});
|
||||
selectedCipherId = response.CipherId;
|
||||
userVerified = response.UserVerified;
|
||||
userPresence = true;
|
||||
}
|
||||
|
||||
var selectedCipher = cipherOptions.FirstOrDefault((c) => c.Id == selectedCipherId);
|
||||
if (selectedCipher == null) {
|
||||
// _logService.Info(
|
||||
// "[Fido2Authenticator] Aborting because the selected credential could not be found."
|
||||
// );
|
||||
ClipLogger.Log("[Fido2Authenticator] Aborting because the selected credential could not be found");
|
||||
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
if (!userPresence && assertionParams.RequireUserPresence) {
|
||||
// _logService.Info(
|
||||
// "[Fido2Authenticator] Aborting because user presence was required but not detected."
|
||||
// );
|
||||
|
||||
ClipLogger.Log("[Fido2Authenticator] Aborting because user presence was required but not detected");
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
// TODO: Remove this hardcoding
|
||||
userVerified = true;
|
||||
|
||||
if (!userVerified && (assertionParams.RequireUserVerification || selectedCipher.Reprompt != CipherRepromptType.None)) {
|
||||
// _logService.Info(
|
||||
// "[Fido2Authenticator] Aborting because user verification was unsuccessful."
|
||||
// );
|
||||
ClipLogger.Log("[Fido2Authenticator] Aborting because user verification was unsuccessful");
|
||||
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var selectedFido2Credential = selectedCipher.Login.MainFido2Credential;
|
||||
var selectedCredentialId = selectedFido2Credential.CredentialId;
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] Selected fido2 cred {selectedFido2Credential.CredentialId}");
|
||||
|
||||
if (selectedFido2Credential.CounterValue != 0) {
|
||||
++selectedFido2Credential.CounterValue;
|
||||
}
|
||||
|
||||
await _cipherService.UpdateLastUsedDateAsync(selectedCipher.Id);
|
||||
var encrypted = await _cipherService.EncryptAsync(selectedCipher);
|
||||
await _cipherService.SaveWithServerAsync(encrypted);
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] Selected fido2 cred RPID {selectedFido2Credential.RpId}");
|
||||
ClipLogger.Log($"[Fido2Authenticator] param RpId {assertionParams.RpId}");
|
||||
|
||||
var authenticatorData = await GenerateAuthDataAsync(
|
||||
rpId: selectedFido2Credential.RpId,
|
||||
userPresence: true,
|
||||
userVerification: true,
|
||||
counter: selectedFido2Credential.CounterValue
|
||||
);
|
||||
|
||||
|
||||
ClipLogger.Log($"authenticatorData base64 from bytes: {Convert.ToBase64String(authenticatorData, Base64FormattingOptions.None)}");
|
||||
ClipLogger.Log($"ClientDataHash base64 from bytes: {Convert.ToBase64String(assertionParams.Hash, Base64FormattingOptions.None)}");
|
||||
ClipLogger.Log($"selectedFido2Credential.KeyBytes base64 from bytes: {Convert.ToBase64String(selectedFido2Credential.KeyBytes, Base64FormattingOptions.None)}");
|
||||
|
||||
var signature = GenerateSignature(
|
||||
authData: authenticatorData,
|
||||
clientDataHash: assertionParams.Hash,
|
||||
privateKey: selectedFido2Credential.KeyBytes
|
||||
);
|
||||
|
||||
ClipLogger.Log($"signature base64 from bytes: {Convert.ToBase64String(signature, Base64FormattingOptions.None)}");
|
||||
|
||||
return new Fido2AuthenticatorGetAssertionResult
|
||||
{
|
||||
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential
|
||||
{
|
||||
Id = selectedCredentialId.GuidToRawFormat(),
|
||||
UserHandle = selectedFido2Credential.UserHandleValue
|
||||
},
|
||||
AuthenticatorData = authenticatorData,
|
||||
Signature = signature
|
||||
};
|
||||
} catch (Exception e) {
|
||||
// _logService.Error(
|
||||
// $"[Fido2Authenticator] Unknown error occured during assertion: {e.Message}"
|
||||
// );
|
||||
ClipLogger.Log($"[Fido2Authenticator] Unknown error occured during assertion: {e.Message}");
|
||||
|
||||
throw new UnknownError();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId)
|
||||
{
|
||||
var credentials = (await FindCredentialsByRpAsync(rpId)).Select(cipher => new Fido2AuthenticatorDiscoverableCredentialMetadata {
|
||||
Type = "public-key",
|
||||
Id = cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat(),
|
||||
RpId = cipher.Login.MainFido2Credential.RpId,
|
||||
UserHandle = cipher.Login.MainFido2Credential.UserHandleValue,
|
||||
UserName = cipher.Login.MainFido2Credential.UserName
|
||||
}).ToArray();
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds existing crendetials and returns the `CipherId` for each one
|
||||
/// </summary>
|
||||
private async Task<string[]> FindExcludedCredentialsAsync(
|
||||
PublicKeyCredentialDescriptor[] credentials
|
||||
) {
|
||||
if (credentials == null || credentials.Length == 0) {
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var ids = new List<string>();
|
||||
|
||||
foreach (var credential in credentials)
|
||||
{
|
||||
try
|
||||
{
|
||||
ids.Add(credential.Id.GuidToStandardFormat());
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (ids.Count == 0) {
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var ciphers = await _cipherService.GetAllDecryptedAsync();
|
||||
return ciphers
|
||||
.FindAll(
|
||||
(cipher) =>
|
||||
!cipher.IsDeleted &&
|
||||
cipher.OrganizationId == null &&
|
||||
cipher.Type == CipherType.Login &&
|
||||
cipher.Login.HasFido2Credentials &&
|
||||
ids.Contains(cipher.Login.MainFido2Credential.CredentialId)
|
||||
)
|
||||
.Select((cipher) => cipher.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private async Task<List<CipherView>> FindCredentialsByIdAsync(PublicKeyCredentialDescriptor[] credentials, string rpId)
|
||||
{
|
||||
var ids = new List<string>();
|
||||
|
||||
foreach (var credential in credentials)
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> Converting Guid byte length: {credential.Id.Length}");
|
||||
ids.Add(credential.Id.GuidToStandardFormat());
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> Converting Guid ex {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> {credentials.Length} vs {ids.Count}");
|
||||
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
return new List<CipherView>();
|
||||
}
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> {ids[0]}");
|
||||
|
||||
var ciphers = await _cipherService.GetAllDecryptedAsync();
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> ciphers count: {ciphers?.Count}");
|
||||
|
||||
return ciphers.FindAll((cipher) =>
|
||||
!cipher.IsDeleted &&
|
||||
cipher.Type == CipherType.Login &&
|
||||
cipher.Login.HasFido2Credentials &&
|
||||
cipher.Login.MainFido2Credential.RpId == rpId &&
|
||||
ids.Contains(cipher.Login.MainFido2Credential.CredentialId)
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<List<CipherView>> FindCredentialsByRpAsync(string rpId)
|
||||
{
|
||||
var ciphers = await _cipherService.GetAllDecryptedAsync();
|
||||
return ciphers.FindAll((cipher) =>
|
||||
!cipher.IsDeleted &&
|
||||
cipher.Type == CipherType.Login &&
|
||||
cipher.Login.HasFido2Credentials &&
|
||||
cipher.Login.MainFido2Credential.RpId == rpId &&
|
||||
cipher.Login.MainFido2Credential.DiscoverableValue
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Move this to a separate service
|
||||
private (PublicKey publicKey, byte[] privateKey) GenerateKeyPair()
|
||||
{
|
||||
var dsa = ECDsa.Create();
|
||||
dsa.GenerateKey(ECCurve.NamedCurves.nistP256);
|
||||
var privateKey = dsa.ExportPkcs8PrivateKey();
|
||||
|
||||
return (new PublicKey(dsa), privateKey);
|
||||
}
|
||||
|
||||
private Fido2CredentialView CreateCredentialView(Fido2AuthenticatorMakeCredentialParams makeCredentialsParams, byte[] privateKey)
|
||||
{
|
||||
return new Fido2CredentialView {
|
||||
CredentialId = Guid.NewGuid().ToString(),
|
||||
KeyType = Constants.DefaultFido2CredentialType,
|
||||
KeyAlgorithm = Constants.DefaultFido2CredentialAlgorithm,
|
||||
KeyCurve = Constants.DefaultFido2CredentialCurve,
|
||||
KeyValue = CoreHelpers.Base64UrlEncode(privateKey),
|
||||
RpId = makeCredentialsParams.RpEntity.Id,
|
||||
UserHandle = CoreHelpers.Base64UrlEncode(makeCredentialsParams.UserEntity.Id),
|
||||
UserName = makeCredentialsParams.UserEntity.Name,
|
||||
CounterValue = 0,
|
||||
RpName = makeCredentialsParams.RpEntity.Name,
|
||||
UserDisplayName = makeCredentialsParams.UserEntity.DisplayName,
|
||||
DiscoverableValue = makeCredentialsParams.RequireResidentKey,
|
||||
CreationDate = DateTime.Now
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<byte[]> GenerateAuthDataAsync(
|
||||
string rpId,
|
||||
bool userVerification,
|
||||
bool userPresence,
|
||||
int counter,
|
||||
byte[] credentialId = null,
|
||||
PublicKey publicKey = null
|
||||
) {
|
||||
var isAttestation = credentialId != null && publicKey != null;
|
||||
|
||||
List<byte> authData = new List<byte>();
|
||||
|
||||
var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256);
|
||||
authData.AddRange(rpIdHash);
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> rpIdHash: {rpIdHash}");
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> ad: {isAttestation} - uv: {userVerification} - up: {userPresence}");
|
||||
|
||||
var flags = AuthDataFlags(
|
||||
extensionData: false,
|
||||
attestationData: isAttestation,
|
||||
userVerification: userVerification,
|
||||
userPresence: userPresence
|
||||
);
|
||||
authData.Add(flags);
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> flags: {flags}");
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> counter: {counter}");
|
||||
|
||||
authData.AddRange(new List<byte> {
|
||||
(byte)(counter >> 24),
|
||||
(byte)(counter >> 16),
|
||||
(byte)(counter >> 8),
|
||||
(byte)counter
|
||||
});
|
||||
|
||||
if (isAttestation)
|
||||
{
|
||||
var attestedCredentialData = new List<byte>();
|
||||
|
||||
attestedCredentialData.AddRange(AAGUID);
|
||||
|
||||
// credentialIdLength (2 bytes) and credential Id
|
||||
var credentialIdLength = new byte[] {
|
||||
(byte)((credentialId.Length - (credentialId.Length & 0xff)) / 256),
|
||||
(byte)(credentialId.Length & 0xff)
|
||||
};
|
||||
attestedCredentialData.AddRange(credentialIdLength);
|
||||
attestedCredentialData.AddRange(credentialId);
|
||||
attestedCredentialData.AddRange(publicKey.ExportCose());
|
||||
|
||||
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> adding attestedCD: {attestedCredentialData}");
|
||||
authData.AddRange(attestedCredentialData);
|
||||
}
|
||||
|
||||
return authData.ToArray();
|
||||
}
|
||||
|
||||
private byte AuthDataFlags(bool extensionData, bool attestationData, bool userVerification, bool userPresence, bool backupEligibility = true, bool backupState = true) {
|
||||
byte flags = 0;
|
||||
|
||||
if (extensionData) {
|
||||
flags |= 0b1000000;
|
||||
}
|
||||
|
||||
if (attestationData) {
|
||||
flags |= 0b01000000;
|
||||
}
|
||||
|
||||
if (backupEligibility)
|
||||
{
|
||||
flags |= 0b00001000;
|
||||
}
|
||||
|
||||
if (backupState)
|
||||
{
|
||||
flags |= 0b00010000;
|
||||
}
|
||||
|
||||
if (userVerification) {
|
||||
flags |= 0b00000100;
|
||||
}
|
||||
|
||||
if (userPresence) {
|
||||
flags |= 0b00000001;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
private byte[] EncodeAttestationObject(byte[] authData) {
|
||||
var attestationObject = new CborWriter(CborConformanceMode.Ctap2Canonical);
|
||||
attestationObject.WriteStartMap(3);
|
||||
attestationObject.WriteTextString("fmt");
|
||||
attestationObject.WriteTextString("none");
|
||||
attestationObject.WriteTextString("attStmt");
|
||||
attestationObject.WriteStartMap(0);
|
||||
attestationObject.WriteEndMap();
|
||||
attestationObject.WriteTextString("authData");
|
||||
attestationObject.WriteByteString(authData);
|
||||
attestationObject.WriteEndMap();
|
||||
|
||||
return attestationObject.Encode();
|
||||
}
|
||||
|
||||
// TODO: Move this to a separate service
|
||||
private byte[] GenerateSignature(byte[] authData, byte[] clientDataHash, byte[] privateKey)
|
||||
{
|
||||
var sigBase = authData.Concat(clientDataHash).ToArray();
|
||||
var dsa = ECDsa.Create();
|
||||
dsa.ImportPkcs8PrivateKey(privateKey, out var bytesRead);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new Exception("Failed to import private key");
|
||||
}
|
||||
|
||||
return dsa.SignData(sigBase, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
||||
}
|
||||
|
||||
private class PublicKey
|
||||
{
|
||||
private readonly ECDsa _dsa;
|
||||
|
||||
public PublicKey(ECDsa dsa) {
|
||||
_dsa = dsa;
|
||||
}
|
||||
|
||||
public byte[] X => _dsa.ExportParameters(false).Q.X;
|
||||
public byte[] Y => _dsa.ExportParameters(false).Q.Y;
|
||||
|
||||
public byte[] ExportDer()
|
||||
{
|
||||
return _dsa.ExportSubjectPublicKeyInfo();
|
||||
}
|
||||
|
||||
public byte[] ExportCose()
|
||||
{
|
||||
var result = new CborWriter(CborConformanceMode.Ctap2Canonical);
|
||||
result.WriteStartMap(5);
|
||||
|
||||
// kty = EC2
|
||||
result.WriteInt32(1);
|
||||
result.WriteInt32(2);
|
||||
|
||||
// alg = ES256
|
||||
result.WriteInt32(3);
|
||||
result.WriteInt32(-7);
|
||||
|
||||
// crv = P-256
|
||||
result.WriteInt32(-1);
|
||||
result.WriteInt32(1);
|
||||
|
||||
// x
|
||||
result.WriteInt32(-2);
|
||||
result.WriteByteString(X);
|
||||
|
||||
// y
|
||||
result.WriteInt32(-3);
|
||||
result.WriteByteString(Y);
|
||||
|
||||
result.WriteEndMap();
|
||||
|
||||
return result.Encode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
248
src/Core/Services/Fido2ClientService.cs
Normal file
248
src/Core/Services/Fido2ClientService.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class Fido2ClientService : IFido2ClientService
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly IFido2AuthenticatorService _fido2AuthenticatorService;
|
||||
|
||||
public Fido2ClientService(
|
||||
IStateService stateService,
|
||||
IEnvironmentService environmentService,
|
||||
ICryptoFunctionService cryptoFunctionService,
|
||||
IFido2AuthenticatorService fido2AuthenticatorService)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_environmentService = environmentService;
|
||||
_cryptoFunctionService = cryptoFunctionService;
|
||||
_fido2AuthenticatorService = fido2AuthenticatorService;
|
||||
}
|
||||
|
||||
public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
|
||||
{
|
||||
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
|
||||
if (blockedUris.Contains(domain))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.UriBlockedError,
|
||||
"Origin is blocked by the user");
|
||||
}
|
||||
|
||||
if (!await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.InvalidStateError,
|
||||
"No user is logged in");
|
||||
}
|
||||
|
||||
if (createCredentialParams.Origin == _environmentService.GetWebVaultUrl())
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.NotAllowedError,
|
||||
"Saving Bitwarden credentials in a Bitwarden vault is not allowed");
|
||||
}
|
||||
|
||||
if (!createCredentialParams.SameOriginWithAncestors)
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.NotAllowedError,
|
||||
"Credential creation is now allowed from embedded contexts with different origins");
|
||||
}
|
||||
|
||||
if (createCredentialParams.User.Id.Length < 1 || createCredentialParams.User.Id.Length > 64)
|
||||
{
|
||||
// TODO: Should we use ArgumentException here instead?
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.TypeError,
|
||||
"The length of user.id is not between 1 and 64 bytes (inclusive)");
|
||||
}
|
||||
|
||||
if (!createCredentialParams.Origin.StartsWith("https://"))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.SecurityError,
|
||||
"Origin is not a valid https origin");
|
||||
}
|
||||
|
||||
if (!Fido2DomainUtils.IsValidRpId(createCredentialParams.Rp.Id, createCredentialParams.Origin))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.SecurityError,
|
||||
"RP ID cannot be used with this origin");
|
||||
}
|
||||
|
||||
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs;
|
||||
if (createCredentialParams.PubKeyCredParams?.Length > 0)
|
||||
{
|
||||
// Filter out all unsupported algorithms
|
||||
credTypesAndPubKeyAlgs = createCredentialParams.PubKeyCredParams
|
||||
.Where(kp => kp.Alg == -7 && kp.Type == "public-key")
|
||||
.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assign default algorithms
|
||||
credTypesAndPubKeyAlgs = [
|
||||
new PublicKeyCredentialParameters { Alg = -7, Type = "public-key" },
|
||||
new PublicKeyCredentialParameters { Alg = -257, Type = "public-key" }
|
||||
];
|
||||
}
|
||||
|
||||
if (credTypesAndPubKeyAlgs.Length == 0)
|
||||
{
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found");
|
||||
}
|
||||
|
||||
var clientDataJSON = JsonSerializer.Serialize(new {
|
||||
type = "webauthn.create",
|
||||
challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge),
|
||||
origin = createCredentialParams.Origin,
|
||||
crossOrigin = !createCredentialParams.SameOriginWithAncestors,
|
||||
// tokenBinding: {} // Not supported
|
||||
});
|
||||
var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON);
|
||||
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
|
||||
var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
|
||||
|
||||
try {
|
||||
var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams);
|
||||
|
||||
return new Fido2ClientCreateCredentialResult {
|
||||
CredentialId = makeCredentialResult.CredentialId,
|
||||
AttestationObject = makeCredentialResult.AttestationObject,
|
||||
AuthData = makeCredentialResult.AuthData,
|
||||
ClientDataJSON = clientDataJSONBytes,
|
||||
PublicKey = makeCredentialResult.PublicKey,
|
||||
PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm,
|
||||
Transports = createCredentialParams.Rp.Id == "google.com" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side
|
||||
};
|
||||
} catch (InvalidStateError) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
||||
} catch (Exception) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams)
|
||||
{
|
||||
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin);
|
||||
if (blockedUris.Contains(domain))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.UriBlockedError,
|
||||
"Origin is blocked by the user");
|
||||
}
|
||||
|
||||
if (!await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.InvalidStateError,
|
||||
"No user is logged in");
|
||||
}
|
||||
|
||||
if (assertCredentialParams.Origin == _environmentService.GetWebVaultUrl())
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.NotAllowedError,
|
||||
"Saving Bitwarden credentials in a Bitwarden vault is not allowed");
|
||||
}
|
||||
|
||||
if (!assertCredentialParams.Origin.StartsWith("https://"))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.SecurityError,
|
||||
"Origin is not a valid https origin");
|
||||
}
|
||||
|
||||
if (!Fido2DomainUtils.IsValidRpId(assertCredentialParams.RpId, assertCredentialParams.Origin))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.SecurityError,
|
||||
"RP ID cannot be used with this origin");
|
||||
}
|
||||
|
||||
var clientDataJSON = JsonSerializer.Serialize(new {
|
||||
type = "webauthn.get",
|
||||
challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge),
|
||||
origin = assertCredentialParams.Origin,
|
||||
crossOrigin = !assertCredentialParams.SameOriginWithAncestors,
|
||||
});
|
||||
var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON);
|
||||
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
|
||||
var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);
|
||||
|
||||
try {
|
||||
var getAssertionResult = await _fido2AuthenticatorService.GetAssertionAsync(getAssertionParams);
|
||||
|
||||
return new Fido2ClientAssertCredentialResult {
|
||||
AuthenticatorData = getAssertionResult.AuthenticatorData,
|
||||
ClientDataJSON = clientDataJSONBytes,
|
||||
Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id),
|
||||
RawId = getAssertionResult.SelectedCredential.Id,
|
||||
Signature = getAssertionResult.Signature,
|
||||
UserHandle = getAssertionResult.SelectedCredential.UserHandle
|
||||
};
|
||||
} catch (InvalidStateError) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
||||
} catch (Exception) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private Fido2AuthenticatorMakeCredentialParams MapToMakeCredentialParams(
|
||||
Fido2ClientCreateCredentialParams createCredentialParams,
|
||||
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs,
|
||||
byte[] clientDataHash)
|
||||
{
|
||||
var requireResidentKey = createCredentialParams.AuthenticatorSelection?.ResidentKey == "required" ||
|
||||
createCredentialParams.AuthenticatorSelection?.ResidentKey == "preferred" ||
|
||||
(createCredentialParams.AuthenticatorSelection?.ResidentKey == null &&
|
||||
createCredentialParams.AuthenticatorSelection?.RequireResidentKey == true);
|
||||
|
||||
var requireUserVerification = createCredentialParams.AuthenticatorSelection?.UserVerification == "required" ||
|
||||
createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" ||
|
||||
createCredentialParams.AuthenticatorSelection?.UserVerification == null;
|
||||
|
||||
return new Fido2AuthenticatorMakeCredentialParams {
|
||||
RequireResidentKey = requireResidentKey,
|
||||
RequireUserVerification = requireUserVerification,
|
||||
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
|
||||
CredTypesAndPubKeyAlgs = credTypesAndPubKeyAlgs,
|
||||
Hash = clientDataHash,
|
||||
RpEntity = createCredentialParams.Rp,
|
||||
UserEntity = createCredentialParams.User,
|
||||
Extensions = createCredentialParams.Extensions
|
||||
};
|
||||
}
|
||||
|
||||
private Fido2AuthenticatorGetAssertionParams MapToGetAssertionParams(
|
||||
Fido2ClientAssertCredentialParams assertCredentialParams,
|
||||
byte[] cliendDataHash)
|
||||
{
|
||||
var requireUserVerification = assertCredentialParams.UserVerification == "required" ||
|
||||
assertCredentialParams.UserVerification == "preferred" ||
|
||||
assertCredentialParams.UserVerification == null;
|
||||
|
||||
return new Fido2AuthenticatorGetAssertionParams {
|
||||
RpId = assertCredentialParams.RpId,
|
||||
Challenge = assertCredentialParams.Challenge,
|
||||
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,
|
||||
RequireUserPresence = true,
|
||||
RequireUserVerification = requireUserVerification,
|
||||
Hash = cliendDataHash
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/Core/Services/Logging/ClipLogger.cs
Normal file
61
src/Core/Services/Logging/ClipLogger.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
#if IOS
|
||||
using UIKit;
|
||||
#endif
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class ClipLogger : ILogger
|
||||
{
|
||||
private static readonly StringBuilder _currentBreadcrumbs = new StringBuilder();
|
||||
|
||||
static ILogger _instance;
|
||||
public static ILogger Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance is null)
|
||||
{
|
||||
_instance = new ClipLogger();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
protected ClipLogger()
|
||||
{
|
||||
}
|
||||
|
||||
public static void Log(string breadcrumb)
|
||||
{
|
||||
_currentBreadcrumbs.AppendLine($"{DateTime.Now.ToShortTimeString()}: {breadcrumb}");
|
||||
#if IOS
|
||||
UIPasteboard.General.String = _currentBreadcrumbs.ToString();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Error(string message, IDictionary<string, string> extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}";
|
||||
var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}";
|
||||
var properties = new Dictionary<string, string>
|
||||
{
|
||||
["File"] = filePathAndLineNumber,
|
||||
["Method"] = memberName
|
||||
};
|
||||
|
||||
Log(message ?? $"Error found in: {classAndMethod}, {filePathAndLineNumber}");
|
||||
}
|
||||
|
||||
public void Exception(Exception ex) => Log(ex?.ToString());
|
||||
|
||||
public Task InitAsync() => Task.CompletedTask;
|
||||
|
||||
public Task<bool> IsEnabled() => Task.FromResult(true);
|
||||
|
||||
public Task SetEnabled(bool value) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
@@ -22,10 +22,23 @@ namespace Bit.Core.Services
|
||||
#if !FDROID
|
||||
// just in case the caller throws the exception in a moment where the logger can't be resolved
|
||||
// we need to track the error as well
|
||||
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
|
||||
//Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
|
||||
ClipLogger.Log(ex?.ToString());
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogBreadcrumb(string breadcrumb)
|
||||
{
|
||||
if (ServiceContainer.Resolve<ILogger>("logger", true) is ILogger logger)
|
||||
{
|
||||
logger.Error(breadcrumb);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClipLogger.Log(breadcrumb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using PCLCrypto;
|
||||
using static PCLCrypto.WinRTCrypto;
|
||||
|
||||
|
||||
@@ -38,12 +38,38 @@ namespace Bit.Core.Utilities
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the host (and not port) of the given uri.
|
||||
/// Does not support plain hostnames without a protocol.
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => login.bitwarden.com</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => sub.login.bitwarden.com</para>
|
||||
/// <para>https://localhost:8080 => localhost</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetHostname(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
return string.IsNullOrEmpty(uri?.Host) ? null : uri.Host;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the host and port of the given uri.
|
||||
/// Does not support plain hostnames without
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => login.bitwarden.com:1337</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => sub.login.bitwarden.com:1337</para>
|
||||
/// <para>https://localhost:8080 => localhost:8080</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetHost(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
@@ -61,6 +87,19 @@ namespace Bit.Core.Utilities
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the second and top level domain of the given uri.
|
||||
/// Does not support plain hostnames without
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => bitwarden.com</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => bitwarden.com</para>
|
||||
/// <para>https://localhost:8080 => localhost</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetDomain(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
|
||||
18
src/Core/Utilities/Fido2/AuthenticatorSelectionCriteria.cs
Normal file
18
src/Core/Utilities/Fido2/AuthenticatorSelectionCriteria.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements of the authenticator used in the creation of the credential.
|
||||
/// </summary>
|
||||
public class AuthenticatorSelectionCriteria
|
||||
{
|
||||
public bool? RequireResidentKey { get; set; }
|
||||
public string? ResidentKey { get; set; }
|
||||
public string UserVerification { get; set; } = "preferred";
|
||||
|
||||
/// <summary>
|
||||
/// This member is intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the create() operation.
|
||||
/// </summary>
|
||||
// public AuthenticatorAttachment? AuthenticatorAttachment { get; set; } // not used
|
||||
}
|
||||
}
|
||||
6
src/Core/Utilities/Fido2/Fido2AlgorithmIdentifier.cs
Normal file
6
src/Core/Utilities/Fido2/Fido2AlgorithmIdentifier.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Utilities.Fido2 {
|
||||
public enum Fido2AlgorithmIdentifier : int {
|
||||
ES256 = -7,
|
||||
RS256 = -257,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/// <summary>
|
||||
/// Represents the metadata of a discoverable credential for a FIDO2 authenticator.
|
||||
/// See: https://www.w3.org/TR/webauthn-3/#sctn-op-silent-discovery
|
||||
/// </summary>
|
||||
public class Fido2AuthenticatorDiscoverableCredentialMetadata
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
||||
public byte[] Id { get; set; }
|
||||
|
||||
public string RpId { get; set; }
|
||||
|
||||
public byte[] UserHandle { get; set; }
|
||||
|
||||
public string UserName { get; set; }
|
||||
}
|
||||
37
src/Core/Utilities/Fido2/Fido2AuthenticatorException.cs
Normal file
37
src/Core/Utilities/Fido2/Fido2AuthenticatorException.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorException : Exception
|
||||
{
|
||||
public Fido2AuthenticatorException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class NotAllowedError : Fido2AuthenticatorException
|
||||
{
|
||||
public NotAllowedError() : base("NotAllowedError")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class NotSupportedError : Fido2AuthenticatorException
|
||||
{
|
||||
public NotSupportedError() : base("NotSupportedError")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidStateError : Fido2AuthenticatorException
|
||||
{
|
||||
public InvalidStateError() : base("InvalidStateError")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class UnknownError : Fido2AuthenticatorException
|
||||
{
|
||||
public UnknownError() : base("UnknownError")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
/** The caller’s RP ID, as determined by the user agent and the client. */
|
||||
public string RpId { get; set; }
|
||||
|
||||
/** The hash of the serialized client data, provided by the client. */
|
||||
public byte[] Hash { get; set; }
|
||||
|
||||
public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the authenticator to require a user-verifying gesture in order to complete the request. Examples of such gestures are fingerprint scan or a PIN.
|
||||
/// </summary>
|
||||
public bool RequireUserVerification { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the authenticator to require user consent to complete the operation.
|
||||
/// </summary>
|
||||
public bool RequireUserPresence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The challenge to be signed by the authenticator.
|
||||
/// </summary>
|
||||
public byte[] Challenge { get; set; }
|
||||
|
||||
public object Extensions { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorGetAssertionResult
|
||||
{
|
||||
public byte[] AuthenticatorData { get; set; }
|
||||
|
||||
public byte[] Signature { get; set; }
|
||||
|
||||
public Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"AD: {AuthenticatorData.Length}; Sig: {Signature.Length}; SC: {SelectedCredential?.Id?.Length}; {SelectedCredential?.UserHandle?.Length}";
|
||||
}
|
||||
}
|
||||
|
||||
public class Fido2AuthenticatorGetAssertionSelectedCredential {
|
||||
public byte[] Id { get; set; }
|
||||
|
||||
#nullable enable
|
||||
public byte[]? UserHandle { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorMakeCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The Relying Party's PublicKeyCredentialRpEntity.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialRpEntity RpEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's PublicKeyCredentialRpEntity.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialUserEntity UserEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The hash of the serialized client data, provided by the client.
|
||||
/// </summary>
|
||||
public byte[] Hash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A sequence of pairs of PublicKeyCredentialType and public key algorithms (COSEAlgorithmIdentifier) requested by the Relying Party. This sequence is ordered from most preferred to least preferred. The authenticator makes a best-effort to create the most preferred credential that it can.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialParameters[] CredTypesAndPubKeyAlgs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[] ExcludeCredentialDescriptorList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The effective resident key requirement for credential creation, a Boolean value determined by the client. Resident is synonymous with discoverable. */
|
||||
/// </summary>
|
||||
public bool RequireResidentKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The effective user verification requirement for assertion, a Boolean value provided by the client.
|
||||
/// </summary>
|
||||
public bool RequireUserVerification { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CTAP2 authenticators support setting this to false, but we only support the WebAuthn authenticator model which does not have that option.
|
||||
/// </summary>
|
||||
// public bool RequireUserPresence { get; set; } // Always required
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator's attestation preference, a string provided by the client. This is a hint that the client gives to the authenticator about what kind of attestation statement it would like. The authenticator makes a best-effort to satisfy the preference.
|
||||
/// Note: Attestation statements are not supported at this time.
|
||||
/// </summary>
|
||||
// public string AttestationPreference { get; set; }
|
||||
|
||||
public object Extensions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorMakeCredentialResult
|
||||
{
|
||||
public byte[] CredentialId { get; set; }
|
||||
|
||||
public byte[] AttestationObject { get; set; }
|
||||
|
||||
public byte[] AuthData { get; set; }
|
||||
|
||||
public byte[] PublicKey { get; set; }
|
||||
|
||||
public int PublicKeyAlgorithm { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for asserting a credential.
|
||||
///
|
||||
/// This class is an extended version of the WebAuthn struct:
|
||||
/// https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialrequestoptions
|
||||
/// </summary>
|
||||
public class Fido2ClientAssertCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// A value which is true if and only if the caller’s environment settings object is same-origin with its ancestors.
|
||||
/// It is false if caller is cross-origin.
|
||||
/// </summary>
|
||||
public bool SameOriginWithAncestors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The challenge that the selected authenticator signs, along with other data, when producing an authentication
|
||||
/// assertion.
|
||||
/// </summary>
|
||||
public required byte[] Challenge { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The relying party identifier claimed by the caller. If omitted, its value will be the CredentialsContainer
|
||||
/// object's relevant settings object's origin's effective domain.
|
||||
/// </summary>
|
||||
public string RpId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's origin (e.g., "https://example.com").
|
||||
/// </summary>
|
||||
public string Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of PublicKeyCredentialDescriptor objects representing public key credentials acceptable to the caller,
|
||||
/// in descending order of the caller’s preference (the first item in the list is the most preferred credential,
|
||||
/// and so on down the list).
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[] AllowCredentials { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements regarding user verification for the get() operation.
|
||||
/// </summary>
|
||||
public string UserVerification { get; set; } = "preferred";
|
||||
|
||||
/// <summary>
|
||||
/// This time, in milliseconds, that the caller is willing to wait for the call to complete.
|
||||
/// This is treated as a hint, and MAY be overridden by the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not currently supported.
|
||||
/// </remarks>
|
||||
public int? Timeout { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of asserting a credential.
|
||||
///
|
||||
/// See: https://www.w3.org/TR/webauthn-2/#publickeycredential
|
||||
/// </summary>
|
||||
public class Fido2ClientAssertCredentialResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64url encoding of the credential identifer.
|
||||
/// </summary>
|
||||
public required string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The credential identifier.
|
||||
/// </summary>
|
||||
public required byte[] RawId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The JSON-compatible serialization of client datapassed to the authenticator by the client in
|
||||
/// order to generate this assertion.
|
||||
/// </summary>
|
||||
public required byte[] ClientDataJSON { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator data returned by the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] AuthenticatorData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw signature returned from the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user handle returned from the authenticator, or null if the authenticator did not
|
||||
/// return a user handle.
|
||||
/// </summary>
|
||||
public byte[]? UserHandle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents an authenticator's response to a client's request for generation of a
|
||||
/// new authentication assertion given the WebAuthn Relying Party's challenge.
|
||||
/// This response contains a cryptographic signature proving possession of the credential private key,
|
||||
/// and optionally evidence of user consent to a specific transaction.
|
||||
///
|
||||
/// See: https://www.w3.org/TR/webauthn-2/#iface-authenticatorassertionresponse
|
||||
/// </summary>
|
||||
public class Fido2ClientAuthenticatorAssertionResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The JSON-compatible serialization of client data passed to the authenticator by the client
|
||||
/// in order to generate this assertion. The exact JSON serialization MUST be preserved, as the
|
||||
/// hash of the serialized client data has been computed over it.
|
||||
/// </summary>
|
||||
public required byte[] ClientDataJSON { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator data returned by the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] AuthenticatorData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw signature returned from the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user handle returned from the authenticator, or null if the authenticator did not return a user handle.
|
||||
/// </summary>
|
||||
public byte[] UserHandle { get; set; } = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for creating a new credential.
|
||||
/// </summary>
|
||||
public class Fido2ClientCreateCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The Relaying Parties origin, see: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin
|
||||
/// </summary>
|
||||
public required string Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A value which is true if and only if the caller’s environment settings object is same-origin with its ancestors.
|
||||
/// It is false if caller is cross-origin.
|
||||
/// </summary>
|
||||
public bool SameOriginWithAncestors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's preference for attestation conveyance
|
||||
/// </summary>
|
||||
public string? Attestation { get; set; } = "none";
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements of the authenticator used in the creation of the credential.
|
||||
/// </summary>
|
||||
public AuthenticatorSelectionCriteria? AuthenticatorSelection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Challenge intended to be used for generating the newly created credential's attestation object.
|
||||
/// </summary>
|
||||
public required byte[] Challenge { get; set; } // base64url encoded
|
||||
|
||||
/// <summary>
|
||||
/// This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for
|
||||
/// the same account on a single authenticator. The client is requested to return an error if the new credential would
|
||||
/// be created on an authenticator that also contains one of the credentials enumerated in this parameter.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[]? ExcludeCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member contains additional parameters requesting additional processing by the client and authenticator.
|
||||
/// Not currently supported.
|
||||
/// </summary>
|
||||
public object? Extensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member contains information about the desired properties of the credential to be created.
|
||||
/// The sequence is ordered from most preferred to least preferred.
|
||||
/// The client makes a best-effort to create the most preferred credential that it can.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialParameters[] PubKeyCredParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data about the Relying Party responsible for the request.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialRpEntity Rp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data about the user account for which the Relying Party is requesting attestation.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialUserEntity User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member specifies a time, in milliseconds, that the caller is willing to wait for the call to complete.
|
||||
/// This is treated as a hint, and MAY be overridden by the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not currently supported.
|
||||
/// </remarks>
|
||||
public int? Timeout { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of creating a new credential.
|
||||
///
|
||||
/// This class is an extended version of the WebAuthn struct:
|
||||
/// https://www.w3.org/TR/webauthn-3/#credentialcreationdata-attestationobjectresult
|
||||
/// </summary>
|
||||
public class Fido2ClientCreateCredentialResult
|
||||
{
|
||||
public byte[] CredentialId { get; set; }
|
||||
public byte[] ClientDataJSON { get; set; }
|
||||
public byte[] AttestationObject { get; set; }
|
||||
public byte[] AuthData { get; set; }
|
||||
public byte[] PublicKey { get; set; }
|
||||
public int PublicKeyAlgorithm { get; set; }
|
||||
public string[] Transports { get; set; }
|
||||
}
|
||||
}
|
||||
25
src/Core/Utilities/Fido2/Fido2ClientException.cs
Normal file
25
src/Core/Utilities/Fido2/Fido2ClientException.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2ClientException : Exception
|
||||
{
|
||||
public enum ErrorCode
|
||||
{
|
||||
NotAllowedError,
|
||||
TypeError,
|
||||
SecurityError,
|
||||
UriBlockedError,
|
||||
NotSupportedError,
|
||||
InvalidStateError,
|
||||
UnknownError
|
||||
}
|
||||
|
||||
public readonly ErrorCode Code;
|
||||
public readonly string Reason;
|
||||
|
||||
public Fido2ClientException(ErrorCode code, string reason) : base($"{code} ({reason})")
|
||||
{
|
||||
Code = code;
|
||||
Reason = reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Core/Utilities/Fido2/Fido2DomainUtils.cs
Normal file
37
src/Core/Utilities/Fido2/Fido2DomainUtils.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2DomainUtils
|
||||
{
|
||||
// TODO: This is a basic implementation of the domain validation logic, and is probably not correct.
|
||||
// It doesn't support IP-adresses, and it doesn't follow the algorithm in the spec:
|
||||
// https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to
|
||||
public static bool IsValidRpId(string rpId, string origin)
|
||||
{
|
||||
if (rpId == null || origin == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: DomainName doesn't like it when we give it a URL with a protocol or port
|
||||
// So we remove the protocol and port here, while still supporting ipv6 shortform
|
||||
// https is enforced in the client, so we don't need to worry about that here
|
||||
var originWithoutProtocolOrPort = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?", "$2$4");
|
||||
|
||||
if (rpId == originWithoutProtocolOrPort)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolOrPort, out var parsedOrgin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return parsedOrgin.Tld == parsedRpId.Tld &&
|
||||
parsedOrgin.Domain == parsedRpId.Domain &&
|
||||
(parsedOrgin.SubDomain == parsedRpId.SubDomain || parsedOrgin.SubDomain.EndsWith(parsedRpId.SubDomain));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class PublicKeyCredentialAlgorithmDescriptor {
|
||||
public byte[] Id {get; set;}
|
||||
public string[] Transports;
|
||||
public string Type;
|
||||
public int Algorithm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class PublicKeyCredentialDescriptor {
|
||||
public byte[] Id { get; set; }
|
||||
public string[] Transports { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
15
src/Core/Utilities/Fido2/PublicKeyCredentialParameters.cs
Normal file
15
src/Core/Utilities/Fido2/PublicKeyCredentialParameters.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// A description of a key type and algorithm.
|
||||
///</example>
|
||||
public class PublicKeyCredentialParameters
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cose algorithm identifier, e.g. -7 for ES256.
|
||||
/// </summary>
|
||||
public int Alg { get; set; }
|
||||
}
|
||||
}
|
||||
9
src/Core/Utilities/Fido2/PublicKeyCredentialRpEntity.cs
Normal file
9
src/Core/Utilities/Fido2/PublicKeyCredentialRpEntity.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class PublicKeyCredentialRpEntity
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class PublicKeyCredentialUserEntity {
|
||||
public byte[] Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public string Icon { get; set; }
|
||||
}
|
||||
}
|
||||
70
src/Core/Utilities/GuidExtensions.cs
Normal file
70
src/Core/Utilities/GuidExtensions.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for converting between standard and raw GUID formats.
|
||||
///
|
||||
/// Note: Not optimized for performance. Don't use in performance-critical code.
|
||||
/// </summary>
|
||||
public static class GuidExtensions
|
||||
{
|
||||
public static byte[] GuidToRawFormat(this string guidString)
|
||||
{
|
||||
if (guidString == null)
|
||||
{
|
||||
throw new ArgumentException("GUID parameter is null", nameof(guidString));
|
||||
}
|
||||
|
||||
if (!IsValidGuid(guidString)) {
|
||||
throw new FormatException("GUID parameter is invalid");
|
||||
}
|
||||
|
||||
var arr = new byte[16];
|
||||
|
||||
arr[0] = byte.Parse(guidString.Substring(0, 2), NumberStyles.HexNumber); // Parse ##......-....-....-....-............
|
||||
arr[1] = byte.Parse(guidString.Substring(2, 2), NumberStyles.HexNumber); // Parse ..##....-....-....-....-............
|
||||
arr[2] = byte.Parse(guidString.Substring(4, 2), NumberStyles.HexNumber); // Parse ....##..-....-....-....-............
|
||||
arr[3] = byte.Parse(guidString.Substring(6, 2), NumberStyles.HexNumber); // Parse ......##-....-....-....-............
|
||||
|
||||
arr[4] = byte.Parse(guidString.Substring(9, 2), NumberStyles.HexNumber); // Parse ........-##..-....-....-............
|
||||
arr[5] = byte.Parse(guidString.Substring(11, 2), NumberStyles.HexNumber); // Parse ........-..##-....-....-............
|
||||
|
||||
arr[6] = byte.Parse(guidString.Substring(14, 2), NumberStyles.HexNumber); // Parse ........-....-##..-....-............
|
||||
arr[7] = byte.Parse(guidString.Substring(16, 2), NumberStyles.HexNumber); // Parse ........-....-..##-....-............
|
||||
|
||||
arr[8] = byte.Parse(guidString.Substring(19, 2), NumberStyles.HexNumber); // Parse ........-....-....-##..-............
|
||||
arr[9] = byte.Parse(guidString.Substring(21, 2), NumberStyles.HexNumber); // Parse ........-....-....-..##-............
|
||||
|
||||
arr[10] = byte.Parse(guidString.Substring(24, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-##..........
|
||||
arr[11] = byte.Parse(guidString.Substring(26, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-..##........
|
||||
arr[12] = byte.Parse(guidString.Substring(28, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-....##......
|
||||
arr[13] = byte.Parse(guidString.Substring(30, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-......##....
|
||||
arr[14] = byte.Parse(guidString.Substring(32, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-........##..
|
||||
arr[15] = byte.Parse(guidString.Substring(34, 2), NumberStyles.HexNumber); // Parse ........-....-....-....-..........##
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static string GuidToStandardFormat(this byte[] guidBytes)
|
||||
{
|
||||
if (guidBytes == null)
|
||||
{
|
||||
throw new ArgumentException("GUID parameter is null", nameof(guidBytes));
|
||||
}
|
||||
|
||||
if (guidBytes.Length != 16)
|
||||
{
|
||||
throw new ArgumentException("Invalid raw GUID format", nameof(guidBytes));
|
||||
}
|
||||
|
||||
return Convert.ToHexString(guidBytes).ToLower().Insert(8, "-").Insert(13, "-").Insert(18, "-").Insert(23, "-" );
|
||||
}
|
||||
|
||||
public static bool IsValidGuid(string guid)
|
||||
{
|
||||
return Regex.IsMatch(guid, @"^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$", RegexOptions.ECMAScript);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
|
||||
@@ -118,6 +115,7 @@ namespace Bit.Core.Utilities
|
||||
Register<IUsernameGenerationService>(usernameGenerationService);
|
||||
Register<IDeviceTrustCryptoService>(deviceTrustCryptoService);
|
||||
Register<IPasswordResetEnrollmentService>(passwordResetEnrollmentService);
|
||||
Register<IFido2AuthenticationService>(new Fido2AuthenticationService());
|
||||
}
|
||||
|
||||
public static void Register<T>(string serviceName, T obj)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
15
src/Xamarin.AndroidX.Credentials/Transforms/EnumFields.xml
Normal file
15
src/Xamarin.AndroidX.Credentials/Transforms/EnumFields.xml
Normal file
@@ -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>
|
||||
|
||||
14
src/Xamarin.AndroidX.Credentials/Transforms/EnumMethods.xml
Normal file
14
src/Xamarin.AndroidX.Credentials/Transforms/EnumMethods.xml
Normal file
@@ -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>
|
||||
|
||||
12
src/Xamarin.AndroidX.Credentials/Transforms/Metadata.xml
Normal file
12
src/Xamarin.AndroidX.Credentials/Transforms/Metadata.xml
Normal file
@@ -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 @@
|
||||
<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>
|
||||
BIN
src/Xamarin.AndroidX.Credentials/credentials-1.2.0.aar
Normal file
BIN
src/Xamarin.AndroidX.Credentials/credentials-1.2.0.aar
Normal file
Binary file not shown.
8
src/iOS.Autofill/ColorConstants.cs
Normal file
8
src/iOS.Autofill/ColorConstants.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public static class ColorConstants
|
||||
{
|
||||
public const string LIGHT_SECONDARY_300 = "LightSecondary300";
|
||||
public const string LIGHT_TEXT_MUTED = "LightTextMuted";
|
||||
}
|
||||
}
|
||||
356
src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs
Normal file
356
src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AuthenticationServices;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Foundation;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost, IFido2UserInterface
|
||||
{
|
||||
private IFido2AuthenticatorService _fido2AuthService;
|
||||
private IFido2AuthenticatorService Fido2AuthService
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_fido2AuthService is null)
|
||||
{
|
||||
_fido2AuthService = ServiceContainer.Resolve<IFido2AuthenticatorService>();
|
||||
_fido2AuthService.Init(this);
|
||||
}
|
||||
return _fido2AuthService;
|
||||
}
|
||||
}
|
||||
|
||||
public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest)
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClipLogger.Log($"PIFPR(IASC)");
|
||||
|
||||
try
|
||||
{
|
||||
switch (registrationRequest?.Type)
|
||||
{
|
||||
case ASCredentialRequestType.PasskeyAssertion:
|
||||
ClipLogger.Log($"PIFPR(IASC) -> Passkey");
|
||||
var passkeyRegistrationRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(registrationRequest.GetHandle());
|
||||
await PrepareInterfaceForPasskeyRegistrationAsync(passkeyRegistrationRequest);
|
||||
break;
|
||||
default:
|
||||
ClipLogger.Log($"PIFPR(IASC) -> Type not PA");
|
||||
CancelRequest(ASExtensionErrorCode.Failed);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PrepareInterfaceForPasskeyRegistrationAsync(ASPasskeyCredentialRequest passkeyRegistrationRequest)
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0) || passkeyRegistrationRequest?.CredentialIdentity is null)
|
||||
{
|
||||
ClipLogger.Log($"PIFPR Not iOS 17 or null passkey request/identity");
|
||||
return;
|
||||
}
|
||||
|
||||
InitAppIfNeeded();
|
||||
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
ClipLogger.Log($"PIFPR Not Authed");
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
|
||||
_context.PasskeyCredentialRequest = passkeyRegistrationRequest;
|
||||
_context.IsCreatingPasskey = true;
|
||||
|
||||
var credIdentity = Runtime.GetNSObject<ASPasskeyCredentialIdentity>(passkeyRegistrationRequest.CredentialIdentity.GetHandle());
|
||||
|
||||
_context.UrlString = credIdentity?.RelyingPartyIdentifier;
|
||||
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync");
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync RpID: {credIdentity.RelyingPartyIdentifier}");
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync UserName: {credIdentity.UserName}");
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync UVP: {passkeyRegistrationRequest.UserVerificationPreference}");
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync SA: {passkeyRegistrationRequest.SupportedAlgorithms?.Select(a => (int)a)}");
|
||||
ClipLogger.Log($"PIFPR MakeCredentialAsync UH: {credIdentity.UserHandle.GetBase64EncodedString(NSDataBase64EncodingOptions.None)}");
|
||||
|
||||
var result = await Fido2AuthService.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams
|
||||
{
|
||||
Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(),
|
||||
CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms),
|
||||
RequireUserVerification = passkeyRegistrationRequest.UserVerificationPreference == "required",
|
||||
RequireResidentKey = true,
|
||||
RpEntity = new PublicKeyCredentialRpEntity
|
||||
{
|
||||
Id = credIdentity.RelyingPartyIdentifier,
|
||||
Name = credIdentity.RelyingPartyIdentifier
|
||||
},
|
||||
UserEntity = new PublicKeyCredentialUserEntity
|
||||
{
|
||||
Id = credIdentity.UserHandle.ToArray(),
|
||||
Name = credIdentity.UserName,
|
||||
DisplayName = credIdentity.UserName
|
||||
}
|
||||
});
|
||||
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
|
||||
ClipLogger.Log($"PIFPR Completing");
|
||||
ClipLogger.Log($"PIFPR Completing - RpId: {credIdentity.RelyingPartyIdentifier}");
|
||||
ClipLogger.Log($"PIFPR Completing - CDH: {passkeyRegistrationRequest.ClientDataHash.GetBase64EncodedString(NSDataBase64EncodingOptions.None)}");
|
||||
ClipLogger.Log($"PIFPR Completing - CID: {Convert.ToBase64String(result.CredentialId, Base64FormattingOptions.None)}");
|
||||
ClipLogger.Log($"PIFPR Completing - AO: {Convert.ToBase64String(result.AttestationObject, Base64FormattingOptions.None)}");
|
||||
|
||||
var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential(
|
||||
credIdentity.RelyingPartyIdentifier,
|
||||
passkeyRegistrationRequest.ClientDataHash,
|
||||
NSData.FromArray(result.CredentialId),
|
||||
NSData.FromArray(result.AttestationObject)));
|
||||
|
||||
ClipLogger.Log($"CompleteRegistrationRequestAsync: {expired}");
|
||||
}
|
||||
|
||||
private PublicKeyCredentialParameters[] GetCredTypesAndPubKeyAlgs(NSNumber[] supportedAlgorithms)
|
||||
{
|
||||
if (supportedAlgorithms?.Any() != true)
|
||||
{
|
||||
return new PublicKeyCredentialParameters[]
|
||||
{
|
||||
new PublicKeyCredentialParameters
|
||||
{
|
||||
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
||||
Alg = (int)Fido2AlgorithmIdentifier.ES256
|
||||
},
|
||||
new PublicKeyCredentialParameters
|
||||
{
|
||||
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
||||
Alg = (int)Fido2AlgorithmIdentifier.RS256
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return supportedAlgorithms
|
||||
.Where(alg => (int)alg == (int)Fido2AlgorithmIdentifier.ES256)
|
||||
.Select(alg => new PublicKeyCredentialParameters
|
||||
{
|
||||
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
||||
Alg = (int)alg
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest)
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||
return;
|
||||
}
|
||||
_context.PasskeyCredentialRequest = passkeyCredentialRequest;
|
||||
await ProvideCredentialAsync(false);
|
||||
}
|
||||
|
||||
public async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId)
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request before iOS 17"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_context.PasskeyCredentialRequest is null)
|
||||
{
|
||||
OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request without a PasskeyCredentialRequest"));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ClipLogger.Log($"ClientDataHash: {_context.PasskeyCredentialRequest.ClientDataHash}");
|
||||
ClipLogger.Log($"ClientDataHash BA: {_context.PasskeyCredentialRequest.ClientDataHash.ToByteArray()}");
|
||||
ClipLogger.Log($"ClientDataHash base64: {_context.PasskeyCredentialRequest.ClientDataHash.GetBase64EncodedString(NSDataBase64EncodingOptions.None)}");
|
||||
ClipLogger.Log($"ClientDataHash base64 from bytes: {Convert.ToBase64String(_context.PasskeyCredentialRequest.ClientDataHash.ToByteArray(), Base64FormattingOptions.None)}");
|
||||
|
||||
var fido2AssertionResult = await Fido2AuthService.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
RpId = rpId,
|
||||
Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(),
|
||||
RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required",
|
||||
RequireUserPresence = false,
|
||||
AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor
|
||||
{
|
||||
Id = credentialIdData.ToArray()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ClipLogger.Log("fido2AssertionResult:" + fido2AssertionResult);
|
||||
|
||||
var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null
|
||||
? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle)
|
||||
: (NSData)userHandleData;
|
||||
|
||||
|
||||
ClipLogger.Log("selectedUserHandleData:" + selectedUserHandleData);
|
||||
|
||||
var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null
|
||||
? NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
|
||||
: credentialIdData;
|
||||
|
||||
ClipLogger.Log("selectedCredentialIdData:" + selectedCredentialIdData);
|
||||
|
||||
await CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
||||
selectedUserHandleData,
|
||||
rpId,
|
||||
NSData.FromArray(fido2AssertionResult.Signature),
|
||||
_context.PasskeyCredentialRequest.ClientDataHash,
|
||||
NSData.FromArray(fido2AssertionResult.AuthenticatorData),
|
||||
selectedCredentialIdData
|
||||
));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
ClipLogger.Log("CompleteAssertionRequestAsync -> InvalidOperationException NoOp");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential");
|
||||
if (assertionCredential is null)
|
||||
{
|
||||
ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> assertionCredential is null");
|
||||
ServiceContainer.Reset();
|
||||
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||
return;
|
||||
}
|
||||
|
||||
//NSRunLoop.Main.BeginInvokeOnMainThread(() =>
|
||||
//{
|
||||
ServiceContainer.Reset();
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> completing");
|
||||
var expired = await ExtensionContext.CompleteAssertionRequestAsync(assertionCredential);
|
||||
//ExtensionContext.CompleteAssertionRequest(assertionCredential, expired =>
|
||||
//{
|
||||
// ClipLogger.Log($"ASExtensionContext?.CompleteAssertionRequest: {expired}");
|
||||
//});
|
||||
|
||||
ClipLogger.Log($"CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> Completed {expired}");
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
//});
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ClipLogger.Log($"CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> failed {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView)
|
||||
{
|
||||
return _context.PasskeyCredentialRequest != null && !cipherView.Login.HasFido2Credentials;
|
||||
}
|
||||
|
||||
// IFido2UserInterface
|
||||
|
||||
public Task<Fido2PickCredentialResult> PickCredentialAsync(Fido2PickCredentialParams pickCredentialParams)
|
||||
{
|
||||
return Task.FromResult(new Fido2PickCredentialResult());
|
||||
}
|
||||
|
||||
public Task InformExcludedCredential(string[] existingCipherIds)
|
||||
{
|
||||
// iOS doesn't seem to provide the ExcludeCredentialDescriptorList so nothing to do here currently.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<Fido2ConfirmNewCredentialResult> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
|
||||
{
|
||||
ClipLogger.Log($"ConfirmNewCredentialAsync");
|
||||
_context.ConfirmNewCredentialTcs?.SetCanceled();
|
||||
_context.ConfirmNewCredentialTcs = new TaskCompletionSource<Fido2ConfirmNewCredentialResult>();
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
PerformSegue(SegueConstants.LOGIN_LIST, this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
});
|
||||
|
||||
return await _context.ConfirmNewCredentialTcs.Task;
|
||||
}
|
||||
|
||||
public async Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
if (_context.IsCreatingPasskey)
|
||||
{
|
||||
ClipLogger.Log($"EnsureUnlockedVaultAsync creating passkey");
|
||||
if (!await IsLocked())
|
||||
{
|
||||
ClipLogger.Log($"EnsureUnlockedVaultAsync not locked");
|
||||
return;
|
||||
}
|
||||
|
||||
_context.UnlockVaultTcs?.SetCanceled();
|
||||
_context.UnlockVaultTcs = new TaskCompletionSource<bool>();
|
||||
MainThread.BeginInvokeOnMainThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log($"EnsureUnlockedVaultAsync performing lock segue");
|
||||
PerformSegue("lockPasswordSegue", this);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ClipLogger.Log($"EnsureUnlockedVaultAsync {ex}");
|
||||
}
|
||||
});
|
||||
|
||||
ClipLogger.Log($"EnsureUnlockedVaultAsync awaiting for unlock");
|
||||
await _context.UnlockVaultTcs.Task;
|
||||
return;
|
||||
}
|
||||
|
||||
ClipLogger.Log($"EnsureUnlockedVaultAsync Passkey selection");
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||
throw new InvalidOperationException("Not authed or locked");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ using Foundation;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform;
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
using static CoreFoundation.DispatchSource;
|
||||
using static Microsoft.Maui.ApplicationModel.Permissions;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
@@ -45,8 +48,12 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("ViewDidLoad");
|
||||
|
||||
InitAppIfNeeded();
|
||||
|
||||
ClipLogger.Log("Inited");
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
|
||||
@@ -56,11 +63,12 @@ namespace Bit.iOS.Autofill
|
||||
ExtContext = ExtensionContext
|
||||
};
|
||||
|
||||
ClipLogger.Log("ViewDidLoad completed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
ClipLogger.Log($"ViewDidLoad ex: {ex}");
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +76,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers");
|
||||
InitAppIfNeeded();
|
||||
_context.ServiceIdentifiers = serviceIdentifiers;
|
||||
if (serviceIdentifiers.Length > 0)
|
||||
@@ -101,59 +110,201 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
|
||||
[Export("prepareCredentialListForServiceIdentifiers:requestParameters:")]
|
||||
public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters)
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters");
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
_context.ServiceIdentifiers = serviceIdentifiers;
|
||||
if (serviceIdentifiers.Length > 0)
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext.CancelRequest(err);
|
||||
return;
|
||||
var uri = serviceIdentifiers[0].Identifier;
|
||||
if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain)
|
||||
{
|
||||
uri = string.Concat("https://", uri);
|
||||
}
|
||||
_context.UrlString = uri;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
await ProvideCredentialAsync(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
_context.CredentialIdentity = credentialIdentity;
|
||||
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
||||
else if (await IsLocked())
|
||||
{
|
||||
PerformSegue("lockPasswordSegue", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
||||
{
|
||||
PerformSegue("loginSearchSegue", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
PerformSegue("loginListSegue", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[Export("provideCredentialWithoutUserInteractionForRequest:")]
|
||||
public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest)
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest");
|
||||
|
||||
//ClipLogger.Log($"PCWUI(IASC) -> R: {credentialRequest?.GetType().FullName}");
|
||||
//ClipLogger.Log($"PCWUI(IASC) -> I: {credentialRequest?.CredentialIdentity?.GetType().FullName}");
|
||||
|
||||
//ClipLogger.Log($"PCWUI(IASC) -> R k: {asPasskeyCredentialRequest?.GetType().FullName}");
|
||||
//ClipLogger.Log($"PCWUI(IASC) -> I k: {asPasskeyCredentialRequest?.CredentialIdentity?.GetType().FullName}");
|
||||
|
||||
//var crType = asPasskeyCredentialRequest.GetType();
|
||||
//foreach (var item in crType.GetProperties())
|
||||
//{
|
||||
// ClipLogger.Log($"PCWUI(IASC) -> R -> {item.Name} -- {item.PropertyType}");
|
||||
//}
|
||||
|
||||
//var ciType = asPasskeyCredentialRequest.CredentialIdentity.GetType();
|
||||
//foreach (var item in ciType.GetProperties())
|
||||
//{
|
||||
// ClipLogger.Log($"PCWUI(IASC) -> I -> {item.Name} -- {item.PropertyType}");
|
||||
//}
|
||||
|
||||
|
||||
switch (credentialRequest?.Type)
|
||||
{
|
||||
case ASCredentialRequestType.Password:
|
||||
var passwordCredentialIdentity = Runtime.GetNSObject<ASPasswordCredentialIdentity>(credentialRequest.CredentialIdentity.GetHandle());
|
||||
ClipLogger.Log($"PCWUI(IASC) -> Type P {passwordCredentialIdentity}");
|
||||
await ProvideCredentialWithoutUserInteractionAsync(passwordCredentialIdentity);
|
||||
break;
|
||||
case ASCredentialRequestType.PasskeyAssertion:
|
||||
var asPasskeyCredentialRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(credentialRequest.GetHandle());
|
||||
await ProvideCredentialWithoutUserInteractionAsync(asPasskeyCredentialRequest);
|
||||
break;
|
||||
default:
|
||||
ClipLogger.Log($"PCWUI(IASC) -> Type not P nor PA");
|
||||
CancelRequest(ASExtensionErrorCode.Failed);
|
||||
break;
|
||||
}
|
||||
|
||||
//switch (credentialRequest)
|
||||
//{
|
||||
// case ASPasswordCredentialRequest passwordRequest:
|
||||
// await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
||||
// break;
|
||||
// case ASPasskeyCredentialRequest passkeyRequest:
|
||||
// await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest);
|
||||
// break;
|
||||
// default:
|
||||
// CancelRequest(ASExtensionErrorCode.Failed);
|
||||
// break;
|
||||
//}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
//public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// ClipLogger.Log("ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity");
|
||||
// await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// OnProvidingCredentialException(ex);
|
||||
// }
|
||||
//}
|
||||
|
||||
[Export("prepareInterfaceToProvideCredentialForRequest:")]
|
||||
public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest)
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest");
|
||||
|
||||
switch (credentialRequest?.Type)
|
||||
{
|
||||
case ASCredentialRequestType.Password:
|
||||
var passwordCredentialIdentity = Runtime.GetNSObject<ASPasswordCredentialIdentity>(credentialRequest.CredentialIdentity.GetHandle());
|
||||
ClipLogger.Log($"PITPC(IASCR) -> Type P {credentialRequest.CredentialIdentity}");
|
||||
await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = passwordCredentialIdentity);
|
||||
break;
|
||||
case ASCredentialRequestType.PasskeyAssertion:
|
||||
var asPasskeyCredentialRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(credentialRequest.GetHandle());
|
||||
await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = asPasskeyCredentialRequest);
|
||||
break;
|
||||
default:
|
||||
ClipLogger.Log($"PITPC(IASCR) -> Type not P nor PA");
|
||||
CancelRequest(ASExtensionErrorCode.Failed);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
//switch (credentialRequest)
|
||||
//{
|
||||
// case ASPasswordCredentialRequest passwordRequest:
|
||||
// //PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
||||
// await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
||||
// break;
|
||||
// case ASPasskeyCredentialRequest passkeyRequest:
|
||||
// await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = passkeyRequest);
|
||||
// break;
|
||||
// default:
|
||||
// CancelRequest(ASExtensionErrorCode.Failed);
|
||||
// break;
|
||||
//}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
//public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// ClipLogger.Log("PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity");
|
||||
// await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// OnProvidingCredentialException(ex);
|
||||
// }
|
||||
//}
|
||||
|
||||
public override async void PrepareInterfaceForExtensionConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("PrepareInterfaceForExtensionConfiguration");
|
||||
InitAppIfNeeded();
|
||||
_context.Configuring = true;
|
||||
if (!await IsAuthed())
|
||||
@@ -165,14 +316,44 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity)
|
||||
{
|
||||
ClipLogger.Log("ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity");
|
||||
InitAppIfNeeded();
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext.CancelRequest(err);
|
||||
return;
|
||||
}
|
||||
_context.PasswordCredentialIdentity = credentialIdentity;
|
||||
await ProvideCredentialAsync(false);
|
||||
}
|
||||
|
||||
private async Task PrepareInterfaceToProvideCredentialAsync(Action<Context> updateContext)
|
||||
{
|
||||
ClipLogger.Log("PrepareInterfaceToProvideCredentialAsync(Action<Context> updateContext");
|
||||
InitAppIfNeeded();
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||
return;
|
||||
}
|
||||
updateContext(_context);
|
||||
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
||||
}
|
||||
|
||||
public void CompleteRequest(string id = null, string username = null,
|
||||
string password = null, string totp = null)
|
||||
{
|
||||
ClipLogger.Log("CompleteRequest");
|
||||
if ((_context?.Configuring ?? true) && string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
ServiceContainer.Reset();
|
||||
@@ -207,10 +388,32 @@ namespace Bit.iOS.Autofill
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProvidingCredentialException(Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
CancelRequest(ASExtensionErrorCode.Failed);
|
||||
}
|
||||
|
||||
public void CancelRequest(ASExtensionErrorCode code)
|
||||
{
|
||||
ClipLogger.Log("CancelRequest" + code);
|
||||
|
||||
if (_context?.IsPasskey == true)
|
||||
{
|
||||
_context.ConfirmNewCredentialTcs?.TrySetCanceled();
|
||||
_context.UnlockVaultTcs?.TrySetCanceled();
|
||||
}
|
||||
|
||||
//var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null);
|
||||
var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
}
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
{
|
||||
try
|
||||
{
|
||||
ClipLogger.Log("Preparing for Segue");
|
||||
if (segue.DestinationViewController is UINavigationController navController)
|
||||
{
|
||||
if (navController.TopViewController is LoginListViewController listLoginController)
|
||||
@@ -245,18 +448,19 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async void DismissLockAndContinue()
|
||||
public void DismissLockAndContinue()
|
||||
{
|
||||
ClipLogger.Log("DismissLockAndContinue");
|
||||
DismissViewController(false, async () => await OnLockDismissedAsync());
|
||||
}
|
||||
|
||||
private void NavigateToPage(ContentPage page)
|
||||
{
|
||||
ClipLogger.Log("NavigateToPage" + page.GetType().FullName);
|
||||
var navigationPage = new NavigationPage(page);
|
||||
var uiController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext);
|
||||
uiController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
||||
@@ -268,30 +472,42 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_context.CredentialIdentity != null)
|
||||
ClipLogger.Log("OnLockDismissedAsync");
|
||||
|
||||
if (_context.IsCreatingPasskey)
|
||||
{
|
||||
ClipLogger.Log("OnLockDismissedAsync -> IsCreatingPasskey");
|
||||
_context.UnlockVaultTcs.SetResult(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_context.PasswordCredentialIdentity != null || _context.IsPasskey)
|
||||
{
|
||||
ClipLogger.Log("OnLockDismissedAsync -> ProvideCredentialAsync");
|
||||
await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync());
|
||||
return;
|
||||
}
|
||||
if (_context.Configuring)
|
||||
{
|
||||
ClipLogger.Log("OnLockDismissedAsync -> Configuring");
|
||||
await MainThread.InvokeOnMainThreadAsync(() => PerformSegue("setupSegue", this));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
|
||||
{
|
||||
ClipLogger.Log("OnLockDismissedAsync -> loginSearchSegue");
|
||||
await MainThread.InvokeOnMainThreadAsync(() => PerformSegue("loginSearchSegue", this));
|
||||
}
|
||||
else
|
||||
{
|
||||
ClipLogger.Log("OnLockDismissedAsync -> loginListSegue");
|
||||
await MainThread.InvokeOnMainThreadAsync(() => PerformSegue("loginListSegue", this));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,59 +515,86 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true);
|
||||
Bit.Core.Models.Domain.Cipher cipher = null;
|
||||
var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
|
||||
if (!cancel)
|
||||
ClipLogger.Log("ProvideCredentialAsync");
|
||||
|
||||
if (_context.IsPasskey && UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier);
|
||||
cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null;
|
||||
if (_context.PasskeyCredentialIdentity is null)
|
||||
{
|
||||
ClipLogger.Log("ProvideCredentialAsync -> IsPasskey failed");
|
||||
CancelRequest(ASExtensionErrorCode.Failed);
|
||||
}
|
||||
|
||||
ClipLogger.Log("ProvideCredentialAsync -> IsPasskey");
|
||||
ClipLogger.Log($"ProvideCredentialAsync -> IsPasskey - RP: {_context.PasskeyCredentialIdentity.RelyingPartyIdentifier}");
|
||||
ClipLogger.Log($"ProvideCredentialAsync -> IsPasskey - UH: {_context.PasskeyCredentialIdentity.UserHandle}");
|
||||
ClipLogger.Log($"ProvideCredentialAsync -> IsPasskey - CID: {_context.PasskeyCredentialIdentity.CredentialId}");
|
||||
ClipLogger.Log($"ProvideCredentialAsync -> IsPasskey - RI: {_context.RecordIdentifier}");
|
||||
|
||||
await CompleteAssertionRequestAsync(_context.PasskeyCredentialIdentity.RelyingPartyIdentifier,
|
||||
_context.PasskeyCredentialIdentity.UserHandle,
|
||||
_context.PasskeyCredentialIdentity.CredentialId,
|
||||
_context.RecordIdentifier);
|
||||
return;
|
||||
}
|
||||
if (cancel)
|
||||
|
||||
if (!ServiceContainer.TryResolve<ICipherService>(out var cipherService)
|
||||
||
|
||||
_context.RecordIdentifier == null)
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
ClipLogger.Log("ProvideCredentialAsync -> CredentialIdentityNotFound");
|
||||
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
ClipLogger.Log("ProvideCredentialAsync -> IsPassword");
|
||||
var cipher = await cipherService.GetAsync(_context.RecordIdentifier);
|
||||
if (cipher?.Login is null || cipher.Type != CipherType.Login)
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
var decCipher = await cipher.DecryptAsync();
|
||||
if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None)
|
||||
|
||||
if (!CanProvideCredentialOnPasskeyRequest(decCipher))
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
if (decCipher.Reprompt != CipherRepromptType.None)
|
||||
{
|
||||
// Prompt for password using either the lock screen or dialog unless
|
||||
// already verified the password.
|
||||
if (!userInteraction)
|
||||
{
|
||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||
return;
|
||||
}
|
||||
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
|
||||
|
||||
if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
|
||||
{
|
||||
// Add a timeout to resolve keyboard not always showing up.
|
||||
await Task.Delay(250);
|
||||
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>();
|
||||
if (!await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync())
|
||||
{
|
||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
||||
Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null);
|
||||
ExtensionContext?.CancelRequest(err);
|
||||
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string totpCode = null;
|
||||
var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync();
|
||||
if (!disableTotpCopy.GetValueOrDefault(false))
|
||||
if (await _stateService.Value.GetDisableAutoTotpCopyAsync() != true)
|
||||
{
|
||||
var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync();
|
||||
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) &&
|
||||
(canAccessPremiumAsync || cipher.OrganizationUseTotp))
|
||||
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp)
|
||||
&&
|
||||
(cipher.OrganizationUseTotp || await _stateService.Value.CanAccessPremiumAsync()))
|
||||
{
|
||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp);
|
||||
totpCode = await ServiceContainer.Resolve<ITotpService>().GetCodeAsync(decCipher.Login.Totp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,13 +602,13 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
throw;
|
||||
OnProvidingCredentialException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckLockAsync(Action notLockedAction)
|
||||
{
|
||||
ClipLogger.Log("CheckLockAsync");
|
||||
if (await IsLocked() || await _stateService.Value.GetPasswordRepromptAutofillAsync())
|
||||
{
|
||||
DispatchQueue.MainQueue.DispatchAsync(() => PerformSegue("lockPasswordSegue", this));
|
||||
@@ -389,6 +632,7 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
private void LogoutIfAuthed()
|
||||
{
|
||||
ClipLogger.Log("LogoutIfAuthed");
|
||||
NSRunLoop.Main.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
try
|
||||
@@ -411,12 +655,14 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
private void InitApp()
|
||||
{
|
||||
ClipLogger.Log("InitApp");
|
||||
iOSCoreHelpers.InitApp(this, Bit.Core.Constants.iOSAutoFillClearCiphersCacheKey,
|
||||
_nfcSession, out _nfcDelegate, out _accountsManager);
|
||||
}
|
||||
|
||||
private void InitAppIfNeeded()
|
||||
{
|
||||
ClipLogger.Log("InitAppIfNeeded");
|
||||
if (ServiceContainer.RegisteredServices == null || ServiceContainer.RegisteredServices.Count == 0)
|
||||
{
|
||||
InitApp();
|
||||
@@ -614,6 +860,7 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
public void Navigate(NavigationTarget navTarget, INavigationParams navParams = null)
|
||||
{
|
||||
ClipLogger.Log("Navigate" + navTarget);
|
||||
switch (navTarget)
|
||||
{
|
||||
case NavigationTarget.HomeLogin:
|
||||
|
||||
10
src/iOS.Autofill/ILoginListViewController.cs
Normal file
10
src/iOS.Autofill/ILoginListViewController.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Bit.iOS.Autofill.Models;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public interface ILoginListViewController
|
||||
{
|
||||
Context Context { get; }
|
||||
CredentialProviderViewController CPViewController { get; }
|
||||
}
|
||||
}
|
||||
@@ -93,8 +93,15 @@
|
||||
<string>com.apple.authentication-services-credential-provider-ui</string>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
|
||||
<true/>
|
||||
<key>ASCredentialProviderExtensionCapabilities</key>
|
||||
<dict>
|
||||
<key>ProvidesPasskeys</key>
|
||||
<true/>
|
||||
<key>ProvidesPasswords</key>
|
||||
<true/>
|
||||
<key>ShowsConfigurationUI</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
59
src/iOS.Autofill/ListItems/HeaderItemView.cs
Normal file
59
src/iOS.Autofill/ListItems/HeaderItemView.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Bit.Core.Services;
|
||||
using Foundation;
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill.ListItems
|
||||
{
|
||||
public class HeaderItemView : UITableViewHeaderFooterView
|
||||
{
|
||||
private readonly UILabel _header = new UILabel();
|
||||
private readonly UIView _separator = new UIView();
|
||||
|
||||
public HeaderItemView(NSString reuseIdentifier)
|
||||
: base(reuseIdentifier)
|
||||
{
|
||||
Setup();
|
||||
}
|
||||
|
||||
protected internal HeaderItemView(NativeHandle handle) : base(handle)
|
||||
{
|
||||
Setup();
|
||||
}
|
||||
|
||||
public void SetHeaderText(string text) => _header.Text = text;
|
||||
|
||||
private void Setup()
|
||||
{
|
||||
try
|
||||
{
|
||||
_header.TextColor = UIColor.FromName(ColorConstants.LIGHT_TEXT_MUTED);
|
||||
_header.Font = UIFont.SystemFontOfSize(15);
|
||||
_separator.BackgroundColor = UIColor.FromName(ColorConstants.LIGHT_SECONDARY_300);
|
||||
|
||||
_header.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||
_separator.TranslatesAutoresizingMaskIntoConstraints = false;
|
||||
|
||||
ContentView.AddSubview(_header);
|
||||
ContentView.AddSubview(_separator);
|
||||
|
||||
NSLayoutConstraint.ActivateConstraints(new NSLayoutConstraint[]
|
||||
{
|
||||
_header.LeadingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.LeadingAnchor, 9),
|
||||
_header.TrailingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TrailingAnchor, 9),
|
||||
_header.TopAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TopAnchor, 3),
|
||||
|
||||
_separator.HeightAnchor.ConstraintEqualTo(2),
|
||||
_separator.TopAnchor.ConstraintEqualTo(_header.BottomAnchor, 8),
|
||||
_separator.LeadingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.LeadingAnchor, 5),
|
||||
_separator.TrailingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TrailingAnchor, 5),
|
||||
_separator.BottomAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.BottomAnchor, 2)
|
||||
});
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,28 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Controls;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Autofill.ListItems;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
using Bit.iOS.Autofill.Utilities;
|
||||
using Bit.iOS.Core.Controllers;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.iOS.Core.Views;
|
||||
using CoreFoundation;
|
||||
using CoreGraphics;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public partial class LoginListViewController : ExtendedUIViewController
|
||||
public partial class LoginListViewController : ExtendedUIViewController, ILoginListViewController
|
||||
{
|
||||
internal const string HEADER_SECTION_IDENTIFIER = "headerSectionId";
|
||||
|
||||
UIBarButtonItem _cancelButton;
|
||||
UIControl _accountSwitchButton;
|
||||
|
||||
@@ -24,59 +30,92 @@ namespace Bit.iOS.Autofill
|
||||
: base(handle)
|
||||
{
|
||||
DismissModalAction = Cancel;
|
||||
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
}
|
||||
|
||||
public Context Context { get; set; }
|
||||
public CredentialProviderViewController CPViewController { get; set; }
|
||||
public IPasswordRepromptService PasswordRepromptService { get; private set; }
|
||||
|
||||
AccountSwitchingOverlayView _accountSwitchingOverlayView;
|
||||
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper;
|
||||
|
||||
LazyResolve<IBroadcasterService> _broadcasterService = new LazyResolve<IBroadcasterService>("broadcasterService");
|
||||
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
||||
LazyResolve<IBroadcasterService> _broadcasterService = new LazyResolve<IBroadcasterService>();
|
||||
LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
bool _alreadyLoadItemsOnce = false;
|
||||
|
||||
public async override void ViewDidLoad()
|
||||
{
|
||||
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
SubscribeSyncCompleted();
|
||||
|
||||
NavItem.Title = AppResources.Items;
|
||||
_cancelButton.Title = AppResources.Cancel;
|
||||
|
||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||
TableView.EstimatedRowHeight = 44;
|
||||
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
||||
TableView.Source = new TableSource(this);
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
||||
|
||||
_alreadyLoadItemsOnce = true;
|
||||
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var needsAutofillReplacement = await storageService.GetAsync<bool?>(
|
||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||
if (needsAutofillReplacement.GetValueOrDefault())
|
||||
try
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
|
||||
|
||||
base.ViewDidLoad();
|
||||
|
||||
SubscribeSyncCompleted();
|
||||
|
||||
NavItem.Title = Context.IsCreatingPasskey ? AppResources.SavePasskey : AppResources.Items;
|
||||
_cancelButton.Title = AppResources.Cancel;
|
||||
|
||||
TableView.RowHeight = UITableView.AutomaticDimension;
|
||||
TableView.EstimatedRowHeight = 44;
|
||||
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
|
||||
TableView.Source = new TableSource(this);
|
||||
if (Context.IsCreatingPasskey)
|
||||
{
|
||||
TableView.SectionHeaderHeight = 55;
|
||||
TableView.RegisterClassForHeaderFooterViewReuse(typeof(HeaderItemView), HEADER_SECTION_IDENTIFIER);
|
||||
}
|
||||
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(15, 0))
|
||||
{
|
||||
TableView.SectionHeaderTopPadding = 0;
|
||||
}
|
||||
|
||||
await ((TableSource)TableView.Source).LoadAsync();
|
||||
|
||||
if (Context.IsCreatingPasskey)
|
||||
{
|
||||
_headerLabel.Text = AppResources.ChooseALoginToSaveThisPasskeyTo;
|
||||
_emptyViewLabel.Text = string.Format(AppResources.NoItemsForUri, Context.UrlString);
|
||||
|
||||
_emptyViewButton.SetTitle(AppResources.SavePasskeyAsNewLogin, UIControlState.Normal);
|
||||
_emptyViewButton.Layer.BorderWidth = 2;
|
||||
_emptyViewButton.Layer.BorderColor = UIColor.FromName(ColorConstants.LIGHT_TEXT_MUTED).CGColor;
|
||||
_emptyViewButton.Layer.CornerRadius = 10;
|
||||
_emptyViewButton.ClipsToBounds = true;
|
||||
|
||||
_headerView.Hidden = false;
|
||||
}
|
||||
|
||||
_alreadyLoadItemsOnce = true;
|
||||
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var needsAutofillReplacement = await storageService.GetAsync<bool?>(
|
||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||
if (needsAutofillReplacement.GetValueOrDefault())
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
|
||||
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
||||
|
||||
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
|
||||
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
|
||||
|
||||
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
|
||||
{
|
||||
_cancelButton,
|
||||
new UIBarButtonItem(_accountSwitchButton)
|
||||
}, false);
|
||||
|
||||
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
||||
}
|
||||
|
||||
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
||||
|
||||
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
|
||||
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
|
||||
|
||||
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
|
||||
catch (Exception ex)
|
||||
{
|
||||
_cancelButton,
|
||||
new UIBarButtonItem(_accountSwitchButton)
|
||||
}, false);
|
||||
|
||||
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelButton_TouchUpInside(object sender, EventArgs e)
|
||||
@@ -91,17 +130,41 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
CPViewController.CompleteRequest();
|
||||
CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled);
|
||||
}
|
||||
|
||||
partial void AddBarButton_Activated(UIBarButtonItem sender)
|
||||
{
|
||||
PerformSegue("loginAddSegue", this);
|
||||
PerformSegue(SegueConstants.ADD_LOGIN, this);
|
||||
}
|
||||
|
||||
partial void SearchBarButton_Activated(UIBarButtonItem sender)
|
||||
{
|
||||
PerformSegue("loginSearchFromListSegue", this);
|
||||
PerformSegue(SegueConstants.LOGIN_SEARCH_FROM_LIST, this);
|
||||
}
|
||||
|
||||
partial void EmptyButton_Activated(UIButton sender)
|
||||
{
|
||||
SavePasskeyAsNewLoginAsync().FireAndForget(ex =>
|
||||
{
|
||||
_platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred).FireAndForget();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task SavePasskeyAsNewLoginAsync()
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
return;
|
||||
}
|
||||
|
||||
var cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCredentialIdentity.RelyingPartyIdentifier);
|
||||
Context.ConfirmNewCredentialTcs.TrySetResult(new Fido2ConfirmNewCredentialResult
|
||||
{
|
||||
CipherId = cipherId,
|
||||
UserVerified = true
|
||||
});
|
||||
}
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
@@ -136,7 +199,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
||||
await ((TableSource)TableView.Source).LoadAsync();
|
||||
TableView.ReloadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -148,6 +211,13 @@ namespace Bit.iOS.Autofill
|
||||
});
|
||||
}
|
||||
|
||||
public void OnEmptyList()
|
||||
{
|
||||
_emptyView.Hidden = false;
|
||||
_headerView.Hidden = false;
|
||||
TableView.Hidden = true;
|
||||
}
|
||||
|
||||
public override void ViewDidUnload()
|
||||
{
|
||||
base.ViewDidUnload();
|
||||
@@ -159,8 +229,15 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
DismissViewController(true, async () =>
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync();
|
||||
TableView.ReloadData();
|
||||
try
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadAsync();
|
||||
TableView.ReloadData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,20 +256,61 @@ namespace Bit.iOS.Autofill
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
public class TableSource : ExtensionTableSource
|
||||
public class TableSource : BaseLoginListTableSource<LoginListViewController>
|
||||
{
|
||||
private LoginListViewController _controller;
|
||||
|
||||
public TableSource(LoginListViewController controller)
|
||||
: base(controller.Context, controller)
|
||||
: base(controller)
|
||||
{
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
protected override string LoginAddSegue => SegueConstants.ADD_LOGIN;
|
||||
|
||||
public override async Task LoadAsync(bool urlFilter = true, string searchFilter = null)
|
||||
{
|
||||
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
|
||||
_controller.CPViewController, _controller, _controller.PasswordRepromptService, "loginAddSegue");
|
||||
try
|
||||
{
|
||||
await base.LoadAsync(urlFilter, searchFilter);
|
||||
|
||||
if (Context.IsCreatingPasskey && !Items.Any())
|
||||
{
|
||||
Controller?.OnEmptyList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override UIView GetViewForHeader(UITableView tableView, nint section)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Context.IsCreatingPasskey
|
||||
&&
|
||||
tableView.DequeueReusableHeaderFooterView(LoginListViewController.HEADER_SECTION_IDENTIFIER) is HeaderItemView headerItemView)
|
||||
{
|
||||
headerItemView.SetHeaderText(AppResources.ChooseALoginToSaveThisPasskeyTo);
|
||||
return headerItemView;
|
||||
}
|
||||
|
||||
return new UIView(CGRect.Empty);// base.GetViewForHeader(tableView, section);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return new UIView();
|
||||
}
|
||||
}
|
||||
|
||||
public override nint RowsInSection(UITableView tableview, nint section)
|
||||
{
|
||||
if (Context.IsCreatingPasskey)
|
||||
{
|
||||
return Items?.Count() ?? 0;
|
||||
}
|
||||
|
||||
return base.RowsInSection(tableview, section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
src/iOS.Autofill/LoginListViewController.designer.cs
generated
51
src/iOS.Autofill/LoginListViewController.designer.cs
generated
@@ -12,6 +12,24 @@ namespace Bit.iOS.Autofill
|
||||
[Register ("LoginListViewController")]
|
||||
partial class LoginListViewController
|
||||
{
|
||||
[Outlet]
|
||||
UIKit.UIView _emptyView { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UIButton _emptyViewButton { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UIImageView _emptyViewImage { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UILabel _emptyViewLabel { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UILabel _headerLabel { get; set; }
|
||||
|
||||
[Outlet]
|
||||
UIKit.UIView _headerView { get; set; }
|
||||
|
||||
[Outlet]
|
||||
[GeneratedCode ("iOS Designer", "1.0")]
|
||||
UIKit.UIBarButtonItem AddBarButton { get; set; }
|
||||
@@ -32,11 +50,44 @@ namespace Bit.iOS.Autofill
|
||||
[Action ("AddBarButton_Activated:")]
|
||||
partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender);
|
||||
|
||||
[Action ("EmptyButton_Activated:")]
|
||||
partial void EmptyButton_Activated (UIKit.UIButton sender);
|
||||
|
||||
[Action ("SearchBarButton_Activated:")]
|
||||
partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender);
|
||||
|
||||
void ReleaseDesignerOutlets ()
|
||||
{
|
||||
if (_emptyView != null) {
|
||||
_emptyView.Dispose ();
|
||||
_emptyView = null;
|
||||
}
|
||||
|
||||
if (_emptyViewButton != null) {
|
||||
_emptyViewButton.Dispose ();
|
||||
_emptyViewButton = null;
|
||||
}
|
||||
|
||||
if (_emptyViewImage != null) {
|
||||
_emptyViewImage.Dispose ();
|
||||
_emptyViewImage = null;
|
||||
}
|
||||
|
||||
if (_emptyViewLabel != null) {
|
||||
_emptyViewLabel.Dispose ();
|
||||
_emptyViewLabel = null;
|
||||
}
|
||||
|
||||
if (_headerLabel != null) {
|
||||
_headerLabel.Dispose ();
|
||||
_headerLabel = null;
|
||||
}
|
||||
|
||||
if (_headerView != null) {
|
||||
_headerView.Dispose ();
|
||||
_headerView = null;
|
||||
}
|
||||
|
||||
if (AddBarButton != null) {
|
||||
AddBarButton.Dispose ();
|
||||
AddBarButton = null;
|
||||
|
||||
@@ -12,19 +12,17 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public partial class LoginSearchViewController : ExtendedUITableViewController
|
||||
public partial class LoginSearchViewController : ExtendedUITableViewController, ILoginListViewController
|
||||
{
|
||||
public LoginSearchViewController(IntPtr handle)
|
||||
: base(handle)
|
||||
{
|
||||
DismissModalAction = Cancel;
|
||||
PasswordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||
}
|
||||
|
||||
public Context Context { get; set; }
|
||||
public CredentialProviderViewController CPViewController { get; set; }
|
||||
public bool FromList { get; set; }
|
||||
public IPasswordRepromptService PasswordRepromptService { get; private set; }
|
||||
|
||||
public async override void ViewDidLoad()
|
||||
{
|
||||
@@ -39,7 +37,7 @@ namespace Bit.iOS.Autofill
|
||||
TableView.EstimatedRowHeight = 44;
|
||||
TableView.Source = new TableSource(this);
|
||||
SearchBar.Delegate = new ExtensionSearchDelegate(TableView);
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text);
|
||||
await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
|
||||
}
|
||||
|
||||
public override void ViewDidAppear(bool animated)
|
||||
@@ -61,13 +59,13 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
else
|
||||
{
|
||||
CPViewController.CompleteRequest();
|
||||
CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled);
|
||||
}
|
||||
}
|
||||
|
||||
partial void AddBarButton_Activated(UIBarButtonItem sender)
|
||||
{
|
||||
PerformSegue("loginAddFromSearchSegue", this);
|
||||
PerformSegue(SegueConstants.ADD_LOGIN_FROM_SEARCH, this);
|
||||
}
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
@@ -88,29 +86,19 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
DismissViewController(true, async () =>
|
||||
{
|
||||
await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text);
|
||||
await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text);
|
||||
TableView.ReloadData();
|
||||
});
|
||||
}
|
||||
|
||||
public class TableSource : ExtensionTableSource
|
||||
public class TableSource : BaseLoginListTableSource<LoginSearchViewController>
|
||||
{
|
||||
private Context _context;
|
||||
private LoginSearchViewController _controller;
|
||||
|
||||
public TableSource(LoginSearchViewController controller)
|
||||
: base(controller.Context, controller)
|
||||
: base(controller)
|
||||
{
|
||||
_context = controller.Context;
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
|
||||
_controller.CPViewController, _controller, _controller.PasswordRepromptService,
|
||||
"loginAddFromSearchSegue");
|
||||
}
|
||||
protected override string LoginAddSegue => SegueConstants.ADD_LOGIN_FROM_SEARCH;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -131,7 +132,75 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="830"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="2305">
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bAz-MO-Wzd" userLabel="HeaderView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="39.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ngG-eh-mSP" userLabel="HeaderLabel">
|
||||
<rect key="frame" x="18" y="12" width="378" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="V7p-E8-7fp" userLabel="SeparatorView">
|
||||
<rect key="frame" x="9" y="37.5" width="396" height="2"/>
|
||||
<color key="backgroundColor" name="LightSecondary300"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="2" id="5pj-pp-Crd"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="V7p-E8-7fp" secondAttribute="trailing" constant="9" id="5vq-UE-Ebp"/>
|
||||
<constraint firstAttribute="bottom" secondItem="V7p-E8-7fp" secondAttribute="bottom" id="8Jj-Cy-WcG"/>
|
||||
<constraint firstItem="V7p-E8-7fp" firstAttribute="leading" secondItem="bAz-MO-Wzd" secondAttribute="leading" constant="9" id="cmb-sZ-Oar"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ngG-eh-mSP" secondAttribute="trailing" constant="18" id="f14-Hv-ajq"/>
|
||||
<constraint firstItem="ngG-eh-mSP" firstAttribute="leading" secondItem="bAz-MO-Wzd" secondAttribute="leading" constant="18" id="htJ-47-GLg"/>
|
||||
<constraint firstItem="V7p-E8-7fp" firstAttribute="top" secondItem="ngG-eh-mSP" secondAttribute="bottom" constant="5" id="rdq-1s-mfF"/>
|
||||
<constraint firstItem="ngG-eh-mSP" firstAttribute="top" secondItem="bAz-MO-Wzd" secondAttribute="top" constant="12" id="sCw-FM-uEg"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wNm-Sy-bJv" userLabel="EmptyView">
|
||||
<rect key="frame" x="0.0" y="139.5" width="414" height="228"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="empty_items_state" translatesAutoresizingMaskIntoConstraints="NO" id="FDN-Dp-jl3">
|
||||
<rect key="frame" x="128.5" y="0.0" width="157" height="110.5"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ThereAreNoItemsInYourVaultForX" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tEp-qe-xvE">
|
||||
<rect key="frame" x="19" y="125.5" width="376" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" name="LightTextMuted"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Gv5-Xt-G9l" userLabel="EmptyButton">
|
||||
<rect key="frame" x="19" y="186" width="376" height="42"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="42" id="9gk-Kj-BzZ"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<buttonConfiguration key="configuration" style="plain" title="Action">
|
||||
<fontDescription key="titleFontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="16"/>
|
||||
<color key="baseForegroundColor" name="LightTextMuted"/>
|
||||
</buttonConfiguration>
|
||||
<connections>
|
||||
<action selector="EmptyButton_Activated:" destination="2304" eventType="touchUpInside" id="AvC-7H-cda"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="Gv5-Xt-G9l" secondAttribute="trailing" constant="19" id="CAs-sg-RdA"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tEp-qe-xvE" secondAttribute="trailing" constant="19" id="FcV-36-N23"/>
|
||||
<constraint firstItem="tEp-qe-xvE" firstAttribute="leading" secondItem="wNm-Sy-bJv" secondAttribute="leading" constant="19" id="FuK-Iy-WB7"/>
|
||||
<constraint firstItem="FDN-Dp-jl3" firstAttribute="centerX" secondItem="wNm-Sy-bJv" secondAttribute="centerX" id="QY2-iP-E6Z"/>
|
||||
<constraint firstItem="Gv5-Xt-G9l" firstAttribute="leading" secondItem="wNm-Sy-bJv" secondAttribute="leading" constant="19" id="X1L-jb-zcg"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Gv5-Xt-G9l" secondAttribute="bottom" id="Z0q-gg-lmn"/>
|
||||
<constraint firstItem="tEp-qe-xvE" firstAttribute="top" secondItem="FDN-Dp-jl3" secondAttribute="bottom" constant="15" id="iwh-e6-yhe"/>
|
||||
<constraint firstItem="Gv5-Xt-G9l" firstAttribute="top" secondItem="tEp-qe-xvE" secondAttribute="bottom" constant="40" id="jkX-tr-T2A"/>
|
||||
<constraint firstItem="FDN-Dp-jl3" firstAttribute="top" secondItem="wNm-Sy-bJv" secondAttribute="top" id="kKX-UE-JzG"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<tableView opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" translatesAutoresizingMaskIntoConstraints="NO" id="2305">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="781"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<prototypes>
|
||||
@@ -173,13 +242,20 @@
|
||||
<viewLayoutGuide key="safeArea" id="BQW-dG-XMM"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="wNm-Sy-bJv" secondAttribute="bottom" constant="200" id="42A-4V-UIl"/>
|
||||
<constraint firstItem="Tq0-Ep-tHr" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="4wL-FF-CVk"/>
|
||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="Tq0-Ep-tHr" secondAttribute="trailing" id="5BV-0y-vU1"/>
|
||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="2305" secondAttribute="bottom" id="6EB-rh-lLS"/>
|
||||
<constraint firstItem="wNm-Sy-bJv" firstAttribute="top" secondItem="bAz-MO-Wzd" secondAttribute="bottom" constant="100" id="CWX-uT-sfH"/>
|
||||
<constraint firstItem="bAz-MO-Wzd" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="SBv-yF-WW2"/>
|
||||
<constraint firstItem="wNm-Sy-bJv" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="Ytw-kT-KUB"/>
|
||||
<constraint firstItem="Tq0-Ep-tHr" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="eT6-Bv-JaR"/>
|
||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="2305" secondAttribute="trailing" id="ofJ-fL-adF"/>
|
||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="bottom" secondItem="Tq0-Ep-tHr" secondAttribute="bottom" id="pBa-o1-Mtx"/>
|
||||
<constraint firstItem="2305" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="pGe-1e-B4s"/>
|
||||
<constraint firstItem="bAz-MO-Wzd" firstAttribute="top" secondItem="BQW-dG-XMM" secondAttribute="top" id="uiV-Kh-8Iz"/>
|
||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="wNm-Sy-bJv" secondAttribute="trailing" id="v0x-aS-ymc"/>
|
||||
<constraint firstItem="BQW-dG-XMM" firstAttribute="trailing" secondItem="bAz-MO-Wzd" secondAttribute="trailing" id="vC6-AI-wVU"/>
|
||||
<constraint firstItem="2305" firstAttribute="leading" secondItem="BQW-dG-XMM" secondAttribute="leading" id="xfQ-VQ-yWe"/>
|
||||
</constraints>
|
||||
</view>
|
||||
@@ -207,6 +283,12 @@
|
||||
<outlet property="NavItem" destination="3734" id="name-outlet-3734"/>
|
||||
<outlet property="OverlayView" destination="Tq0-Ep-tHr" id="igj-R2-gXJ"/>
|
||||
<outlet property="TableView" destination="2305" id="aUe-Uz-iIb"/>
|
||||
<outlet property="_emptyView" destination="wNm-Sy-bJv" id="Whk-C5-rjW"/>
|
||||
<outlet property="_emptyViewButton" destination="Gv5-Xt-G9l" id="JHd-sV-VJC"/>
|
||||
<outlet property="_emptyViewImage" destination="FDN-Dp-jl3" id="Dzb-p3-tv0"/>
|
||||
<outlet property="_emptyViewLabel" destination="tEp-qe-xvE" id="CPZ-it-kVY"/>
|
||||
<outlet property="_headerLabel" destination="ngG-eh-mSP" id="1bj-Ii-8OY"/>
|
||||
<outlet property="_headerView" destination="bAz-MO-Wzd" id="85r-XO-e5h"/>
|
||||
<segue destination="1845" kind="presentation" identifier="loginAddSegue" modalPresentationStyle="fullScreen" modalTransitionStyle="coverVertical" id="3731"/>
|
||||
<segue destination="11552" kind="show" identifier="loginSearchFromListSegue" id="12574"/>
|
||||
</connections>
|
||||
@@ -575,7 +657,14 @@
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="check.png" width="90" height="90"/>
|
||||
<image name="empty_items_state" width="157" height="111"/>
|
||||
<image name="logo.png" width="282" height="44"/>
|
||||
<namedColor name="LightSecondary300">
|
||||
<color red="0.80800002813339233" green="0.83099997043609619" blue="0.86299997568130493" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</namedColor>
|
||||
<namedColor name="LightTextMuted">
|
||||
<color red="0.42699998617172241" green="0.45899999141693115" blue="0.49399998784065247" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
<systemColor name="darkTextColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using AuthenticationServices;
|
||||
using System.Threading.Tasks;
|
||||
using AuthenticationServices;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.iOS.Core.Models;
|
||||
using Foundation;
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill.Models
|
||||
{
|
||||
@@ -8,7 +12,43 @@ namespace Bit.iOS.Autofill.Models
|
||||
{
|
||||
public NSExtensionContext ExtContext { get; set; }
|
||||
public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; }
|
||||
public ASPasswordCredentialIdentity CredentialIdentity { get; set; }
|
||||
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
||||
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
|
||||
public bool Configuring { get; set; }
|
||||
public bool IsCreatingPasskey { get; set; }
|
||||
public TaskCompletionSource<bool> UnlockVaultTcs { get; set; }
|
||||
public TaskCompletionSource<Fido2ConfirmNewCredentialResult> ConfirmNewCredentialTcs { get; set; }
|
||||
|
||||
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PasskeyCredentialRequest != null && UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return Runtime.GetNSObject<ASPasskeyCredentialIdentity>(PasskeyCredentialRequest.CredentialIdentity.GetHandle());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string? RecordIdentifier
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PasswordCredentialIdentity?.RecordIdentifier is string id)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return PasskeyCredentialIdentity?.RecordIdentifier;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPasskey => PasskeyCredentialRequest != null;
|
||||
}
|
||||
}
|
||||
|
||||
6
src/iOS.Autofill/Resources/Assets.xcassets/Contents.json
Executable file
6
src/iOS.Autofill/Resources/Assets.xcassets/Contents.json
Executable file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "display-p3",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.863",
|
||||
"green" : "0.831",
|
||||
"red" : "0.808"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.924",
|
||||
"green" : "0.879",
|
||||
"red" : "0.854"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.494",
|
||||
"green" : "0.459",
|
||||
"red" : "0.427"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.783",
|
||||
"green" : "0.718",
|
||||
"red" : "0.671"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
25
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
vendored
Executable file
25
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json
vendored
Executable file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Empty-items-state.pdf",
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "Empty-items-state-dark.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
BIN
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf
vendored
Executable file
BIN
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf
vendored
Executable file
Binary file not shown.
BIN
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf
vendored
Executable file
BIN
src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf
vendored
Executable file
Binary file not shown.
13
src/iOS.Autofill/SegueConstants.cs
Normal file
13
src/iOS.Autofill/SegueConstants.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
public static class SegueConstants
|
||||
{
|
||||
public const string LOGIN_LIST = "loginListSegue";
|
||||
public const string LOCK = "lockPasswordSegue";
|
||||
public const string LOGIN_SEARCH = "loginSearchSegue";
|
||||
public const string SETUP = "setupSegue";
|
||||
public const string ADD_LOGIN = "loginAddSegue";
|
||||
public const string LOGIN_SEARCH_FROM_LIST = "loginSearchFromListSegue";
|
||||
public const string ADD_LOGIN_FROM_SEARCH = "loginAddFromSearchSegue";
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
BackButton.Title = AppResources.Back;
|
||||
base.ViewDidLoad();
|
||||
var task = ASHelpers.ReplaceAllIdentities();
|
||||
var task = ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
|
||||
partial void BackButton_Activated(UIBarButtonItem sender)
|
||||
|
||||
91
src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
Normal file
91
src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
using Bit.iOS.Core.Views;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Autofill.Utilities
|
||||
{
|
||||
public abstract class BaseLoginListTableSource<T> : ExtensionTableSource
|
||||
where T : UIViewController, ILoginListViewController
|
||||
{
|
||||
private IPasswordRepromptService _passwordRepromptService;
|
||||
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||
|
||||
public BaseLoginListTableSource(T controller)
|
||||
: base(controller.Context, controller)
|
||||
{
|
||||
_controller = controller;
|
||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>();
|
||||
}
|
||||
|
||||
protected Context Context => (Context)_context;
|
||||
protected T Controller => (T)_controller;
|
||||
|
||||
protected abstract string LoginAddSegue { get; }
|
||||
|
||||
public async override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Context.IsCreatingPasskey)
|
||||
{
|
||||
await SelectRowForPasskeyCreationAsync(tableView, indexPath);
|
||||
return;
|
||||
}
|
||||
|
||||
await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this,
|
||||
Controller.CPViewController, Controller, _passwordRepromptService, LoginAddSegue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectRowForPasskeyCreationAsync(UITableView tableView, NSIndexPath indexPath)
|
||||
{
|
||||
tableView.DeselectRow(indexPath, true);
|
||||
tableView.EndEditing(true);
|
||||
|
||||
var item = Items.ElementAt(indexPath.Row);
|
||||
if (item is null)
|
||||
{
|
||||
await _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.CipherView.Login.HasFido2Credentials
|
||||
&&
|
||||
!await _platformUtilsService.Value.ShowDialogAsync(
|
||||
AppResources.ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey,
|
||||
AppResources.OverwritePasskey,
|
||||
AppResources.Yes,
|
||||
AppResources.No))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await _passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(item.Reprompt))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Check user verification
|
||||
|
||||
Context.ConfirmNewCredentialTcs.SetResult(new Fido2ConfirmNewCredentialResult
|
||||
{
|
||||
CipherId = item.Id,
|
||||
UserVerified = true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
<ItemGroup>
|
||||
<TrimmerRootAssembly Include="System.Security.Cryptography" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="ListItems\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="CredentialProviderViewController.cs" />
|
||||
<Compile Include="CredentialProviderViewController.designer.cs">
|
||||
@@ -81,6 +84,12 @@
|
||||
<Compile Include="Models\Context.cs" />
|
||||
<BundleResource Include="Resources\MaterialIcons_Regular.ttf" />
|
||||
<BundleResource Include="Resources\bwi-font.ttf" />
|
||||
<Compile Include="CredentialProviderViewController.Passkeys.cs" />
|
||||
<Compile Include="SegueConstants.cs" />
|
||||
<Compile Include="ColorConstants.cs" />
|
||||
<Compile Include="ListItems\HeaderItemView.cs" />
|
||||
<Compile Include="Utilities\BaseLoginListTableSource.cs" />
|
||||
<Compile Include="ILoginListViewController.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BundleResource Include="Resources\check.png" />
|
||||
@@ -179,4 +188,7 @@
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\iOS.Core\iOS.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="ListItems\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -188,18 +188,17 @@ namespace Bit.iOS.Core.Controllers
|
||||
await _cipherService.SaveWithServerAsync(cipherDomain);
|
||||
await loadingAlert.DismissViewControllerAsync(true);
|
||||
await _storageService.SaveAsync(Bit.Core.Constants.ClearCiphersCacheKey, true);
|
||||
if (await ASHelpers.IdentitiesCanIncremental())
|
||||
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||
{
|
||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherDomain.Id);
|
||||
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherDomain.Id);
|
||||
if (identity != null)
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync(
|
||||
new ASPasswordCredentialIdentity[] { identity });
|
||||
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
Success(cipherDomain.Id);
|
||||
}
|
||||
@@ -229,7 +228,7 @@ namespace Bit.iOS.Core.Controllers
|
||||
var appOptions = new AppOptions { IosExtension = true };
|
||||
var app = new App.App(appOptions);
|
||||
|
||||
var generatorPage = new GeneratorPage(false, selectAction: async (username) =>
|
||||
var generatorPage = new GeneratorPage(false, selectAction: (username) =>
|
||||
{
|
||||
UsernameCell.TextField.Text = username;
|
||||
DismissViewController(false, null);
|
||||
|
||||
@@ -11,9 +11,11 @@ namespace Bit.iOS.Core.Services
|
||||
{
|
||||
public bool SupportsAutofillService() => false;
|
||||
public bool AutofillServiceEnabled() => false;
|
||||
public void DisableCredentialProviderService() => throw new NotImplementedException();
|
||||
public void Autofill(CipherView cipher) => throw new NotImplementedException();
|
||||
public bool AutofillAccessibilityOverlayPermitted() => false;
|
||||
public bool AutofillAccessibilityServiceRunning() => false;
|
||||
public bool CredentialProviderServiceEnabled() => throw new NotImplementedException();
|
||||
public bool AutofillServicesEnabled() => false;
|
||||
public void CloseAutofill() => throw new NotImplementedException();
|
||||
public void DisableAutofillService() => throw new NotImplementedException();
|
||||
|
||||
@@ -301,6 +301,8 @@ namespace Bit.iOS.Core.Services
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void OpenCredentialProviderSettings() => throw new NotImplementedException();
|
||||
|
||||
public void OpenAutofillSettings()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
@@ -339,6 +341,8 @@ namespace Bit.iOS.Core.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool SupportsCredentialProviderService() => throw new NotImplementedException();
|
||||
|
||||
public bool SupportsAutofillServices() => UIDevice.CurrentDevice.CheckSystemVersion(12, 0);
|
||||
public bool SupportsInlineAutofill() => false;
|
||||
public bool SupportsDrawOver() => false;
|
||||
@@ -375,7 +379,7 @@ namespace Bit.iOS.Core.Services
|
||||
|
||||
public async Task OnAccountSwitchCompleteAsync()
|
||||
{
|
||||
await ASHelpers.ReplaceAllIdentities();
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
}
|
||||
|
||||
public Task SetScreenCaptureAllowedAsync()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using AuthenticationServices;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Core.Utilities
|
||||
{
|
||||
public static class ASCredentialIdentityStoreExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves password credential identities to the shared store of <see cref="ASCredentialIdentityStore"/>
|
||||
/// Note: This is added to provide the proper method depending on the OS version.
|
||||
/// </summary>
|
||||
/// <param name="identities">Password identities to save</param>
|
||||
public static Task<Tuple<bool, NSError>> SaveCredentialIdentitiesAsync(params ASPasswordCredentialIdentity[] identities)
|
||||
{
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return ASCredentialIdentityStore.SharedStore.SaveCredentialIdentityEntriesAsync(identities);
|
||||
}
|
||||
|
||||
return ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync(identities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes password credential identities of the shared store of <see cref="ASCredentialIdentityStore"/>
|
||||
/// Note: This is added to provide the proper method depending on the OS version.
|
||||
/// </summary>
|
||||
/// <param name="identities">Password identities to remove</param>
|
||||
public static Task<Tuple<bool, NSError>> RemoveCredentialIdentitiesAsync(params ASPasswordCredentialIdentity[] identities)
|
||||
{
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
return ASCredentialIdentityStore.SharedStore.RemoveCredentialIdentityEntriesAsync(identities);
|
||||
}
|
||||
|
||||
return ASCredentialIdentityStore.SharedStore.RemoveCredentialIdentitiesAsync(identities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +1,125 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AuthenticationServices;
|
||||
using AuthenticationServices;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Core.Utilities
|
||||
{
|
||||
public static class ASHelpers
|
||||
{
|
||||
public static async Task ReplaceAllIdentities()
|
||||
public static async Task ReplaceAllIdentitiesAsync()
|
||||
{
|
||||
if (await AutofillEnabled())
|
||||
if (!await IsAutofillEnabledAsync())
|
||||
{
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||
if (await vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true);
|
||||
return;
|
||||
}
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
var identities = new List<ASPasswordCredentialIdentity>();
|
||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||
foreach (var cipher in ciphers.Where(x => !x.IsDeleted))
|
||||
{
|
||||
var identity = ToCredentialIdentity(cipher);
|
||||
if (identity != null)
|
||||
{
|
||||
identities.Add(identity);
|
||||
}
|
||||
}
|
||||
if (identities.Any())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore?.ReplaceCredentialIdentitiesAsync(identities.ToArray());
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
||||
return;
|
||||
}
|
||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||
var stateService = ServiceContainer.Resolve<IStateService>();
|
||||
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||
if (await vaultTimeoutService.IsLockedAsync())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true);
|
||||
return;
|
||||
}
|
||||
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
await ReplaceAllIdentitiesIOS17Async(cipherService, storageService);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplaceAllIdentitiesIOS12Async(cipherService, storageService);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> IdentitiesCanIncremental()
|
||||
private static async Task ReplaceAllIdentitiesIOS12Async(ICipherService cipherService, IStorageService storageService)
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||
var identities = ciphers.Where(c => !c.IsDeleted)
|
||||
.Select(ToPasswordCredentialIdentity)
|
||||
.Where(i => i != null)
|
||||
.Cast<ASPasswordCredentialIdentity>()
|
||||
.ToList();
|
||||
if (!identities.Any())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
#pragma warning disable CA1422 // Validate platform compatibility
|
||||
await ASCredentialIdentityStore.SharedStore.ReplaceCredentialIdentitiesAsync(identities.ToArray());
|
||||
#pragma warning restore CA1422 // Validate platform compatibility
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
||||
}
|
||||
|
||||
private static async Task ReplaceAllIdentitiesIOS17Async(ICipherService cipherService, IStorageService storageService)
|
||||
{
|
||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||
var identities = ciphers.Where(c => !c.IsDeleted)
|
||||
.Select(ToCredentialIdentity)
|
||||
.Where(i => i != null)
|
||||
.Cast<IASCredentialIdentity>()
|
||||
.ToList();
|
||||
if (!identities.Any())
|
||||
{
|
||||
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await ASCredentialIdentityStore.SharedStore.ReplaceCredentialIdentityEntriesAsync(identities.ToArray());
|
||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
||||
}
|
||||
|
||||
public static async Task<bool> IdentitiesSupportIncrementalAsync()
|
||||
{
|
||||
var stateService = ServiceContainer.Resolve<IStateService>();
|
||||
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync();
|
||||
var state = await ASCredentialIdentityStore.SharedStore.GetCredentialIdentityStoreStateAsync();
|
||||
return state != null && state.Enabled && state.SupportsIncrementalUpdates;
|
||||
}
|
||||
|
||||
public static async Task<bool> AutofillEnabled()
|
||||
public static async Task<bool> IsAutofillEnabledAsync()
|
||||
{
|
||||
var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync();
|
||||
var state = await ASCredentialIdentityStore.SharedStore.GetCredentialIdentityStoreStateAsync();
|
||||
return state != null && state.Enabled;
|
||||
}
|
||||
|
||||
public static async Task<ASPasswordCredentialIdentity> GetCipherIdentityAsync(string cipherId)
|
||||
public static async Task<ASPasswordCredentialIdentity?> GetCipherPasswordIdentityAsync(string cipherId)
|
||||
{
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
var cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||
var cipher = await cipherService.GetAsync(cipherId);
|
||||
if (cipher == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var cipherView = await cipher.DecryptAsync();
|
||||
return ToCredentialIdentity(cipherView);
|
||||
return ToPasswordCredentialIdentity(cipherView);
|
||||
}
|
||||
|
||||
public static ASPasswordCredentialIdentity ToCredentialIdentity(CipherView cipher)
|
||||
public static ASPasswordCredentialIdentity? ToPasswordCredentialIdentity(CipherView cipher)
|
||||
{
|
||||
if (!cipher?.Login?.Uris?.Any() ?? true)
|
||||
if (cipher?.Login?.Uris?.Any() != true)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var uri = cipher.Login.Uris.FirstOrDefault(u => u.Match != Bit.Core.Enums.UriMatchType.Never)?.Uri;
|
||||
var uri = cipher.Login.Uris.FirstOrDefault(u => u.Match != UriMatchType.Never)?.Uri;
|
||||
if (string.IsNullOrWhiteSpace(uri))
|
||||
{
|
||||
return null;
|
||||
@@ -100,5 +132,24 @@ namespace Bit.iOS.Core.Utilities
|
||||
var serviceId = new ASCredentialServiceIdentifier(uri, ASCredentialServiceIdentifierType.Url);
|
||||
return new ASPasswordCredentialIdentity(serviceId, username, cipher.Id);
|
||||
}
|
||||
|
||||
public static IASCredentialIdentity? ToCredentialIdentity(CipherView cipher)
|
||||
{
|
||||
if (!cipher.HasFido2Credential)
|
||||
{
|
||||
return ToPasswordCredentialIdentity(cipher);
|
||||
}
|
||||
|
||||
if (!cipher.Login.MainFido2Credential.DiscoverableValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ASPasskeyCredentialIdentity(cipher.Login.MainFido2Credential.RpId,
|
||||
cipher.Login.MainFido2Credential.UserName,
|
||||
NSData.FromArray(cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat()),
|
||||
cipher.Login.MainFido2Credential.UserHandle,
|
||||
cipher.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/iOS.Core/Utilities/NSDataExtensions.cs
Normal file
15
src/iOS.Core/Utilities/NSDataExtensions.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Foundation;
|
||||
|
||||
namespace Bit.iOS.Core.Utilities
|
||||
{
|
||||
public static class NSDataExtensions
|
||||
{
|
||||
public static byte[] ToByteArray(this NSData data)
|
||||
{
|
||||
var bytes = new byte[data.Length];
|
||||
Marshal.Copy(data.Bytes, bytes, 0, Convert.ToInt32(data.Length));
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ namespace Bit.iOS.Core.Utilities
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
logger = DebugLogger.Instance;
|
||||
logger = ClipLogger.Instance;
|
||||
#else
|
||||
logger = Logger.Instance;
|
||||
#endif
|
||||
@@ -138,6 +138,7 @@ namespace Bit.iOS.Core.Utilities
|
||||
logger!.Exception(nreAppGroupContainer);
|
||||
throw nreAppGroupContainer;
|
||||
}
|
||||
|
||||
var liteDbStorage = new LiteDbStorageService(
|
||||
Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db"));
|
||||
var localizeService = new LocalizeService();
|
||||
@@ -189,6 +190,11 @@ namespace Bit.iOS.Core.Utilities
|
||||
|
||||
public static void RegisterFinallyBeforeBootstrap()
|
||||
{
|
||||
ServiceContainer.Register<IFido2AuthenticatorService>(new Fido2AuthenticatorService(
|
||||
ServiceContainer.Resolve<ICipherService>(),
|
||||
ServiceContainer.Resolve<ISyncService>(),
|
||||
ServiceContainer.Resolve<ICryptoFunctionService>()));
|
||||
|
||||
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
|
||||
ServiceContainer.Resolve<IEnvironmentService>(),
|
||||
ServiceContainer.Resolve<IStateService>(),
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using System.Diagnostics;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Core.Controllers;
|
||||
using Bit.iOS.Core.Models;
|
||||
@@ -25,8 +20,8 @@ namespace Bit.iOS.Core.Views
|
||||
protected ITotpService _totpService;
|
||||
protected IStateService _stateService;
|
||||
protected ISearchService _searchService;
|
||||
private AppExtensionContext _context;
|
||||
private UIViewController _controller;
|
||||
protected AppExtensionContext _context;
|
||||
protected UIViewController _controller;
|
||||
|
||||
public ExtensionTableSource(AppExtensionContext context, UIViewController controller)
|
||||
{
|
||||
@@ -36,11 +31,19 @@ namespace Bit.iOS.Core.Views
|
||||
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||
_context = context;
|
||||
_controller = controller;
|
||||
|
||||
Items = new List<CipherViewModel>();
|
||||
}
|
||||
|
||||
public IEnumerable<CipherViewModel> Items { get; private set; }
|
||||
|
||||
public async Task LoadItemsAsync(bool urlFilter = true, string searchFilter = null)
|
||||
public virtual async Task LoadAsync(bool urlFilter = true, string searchFilter = null)
|
||||
{
|
||||
_allItems = await LoadItemsAsync(urlFilter, searchFilter);
|
||||
FilterResults(searchFilter, new CancellationToken());
|
||||
}
|
||||
|
||||
protected virtual async Task<IEnumerable<CipherViewModel>> LoadItemsAsync(bool urlFilter = true, string? searchFilter = null)
|
||||
{
|
||||
var combinedLogins = new List<CipherView>();
|
||||
|
||||
@@ -62,11 +65,10 @@ namespace Bit.iOS.Core.Views
|
||||
combinedLogins.AddRange(logins);
|
||||
}
|
||||
|
||||
_allItems = combinedLogins
|
||||
return combinedLogins
|
||||
.Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted)
|
||||
.Select(s => new CipherViewModel(s))
|
||||
.ToList() ?? new List<CipherViewModel>();
|
||||
FilterResults(searchFilter, new CancellationToken());
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void FilterResults(string searchFilter, CancellationToken ct)
|
||||
@@ -87,7 +89,7 @@ namespace Bit.iOS.Core.Views
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<CipherViewModel> TableItems { get; set; }
|
||||
//public IEnumerable<CipherViewModel> TableItems { get; set; }
|
||||
|
||||
public override nint RowsInSection(UITableView tableview, nint section)
|
||||
{
|
||||
@@ -135,9 +137,9 @@ namespace Bit.iOS.Core.Views
|
||||
cell.DetailTextLabel.Text = item.Username;
|
||||
}
|
||||
|
||||
public async Task<string> GetTotpAsync(CipherViewModel item)
|
||||
public async Task<string?> GetTotpAsync(CipherViewModel item)
|
||||
{
|
||||
string totp = null;
|
||||
string? totp = null;
|
||||
var accessPremium = await _stateService.CanAccessPremiumAsync();
|
||||
if (accessPremium || (item?.CipherView.OrganizationUseTotp ?? false))
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user