mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
Compare commits
268 Commits
v2024.7.0
...
vault/pm-7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f312e8c4d2 | ||
|
|
1b3d5e5eb2 | ||
|
|
81fbb91c76 | ||
|
|
45641aadfe | ||
|
|
27380abd89 | ||
|
|
1fd7dd462e | ||
|
|
ff49d041be | ||
|
|
b931263662 | ||
|
|
3a10e09469 | ||
|
|
ebc068d820 | ||
|
|
6bec0ede05 | ||
|
|
39da2a82c6 | ||
|
|
970d3c2621 | ||
|
|
faa515b415 | ||
|
|
74085689d3 | ||
|
|
144fc7c727 | ||
|
|
53aedea93a | ||
|
|
dd997aaa47 | ||
|
|
46c1d72b3c | ||
|
|
01fe329f3b | ||
|
|
67f7b3156e | ||
|
|
39187732c0 | ||
|
|
4292542155 | ||
|
|
e41abf5003 | ||
|
|
4c2932f4d0 | ||
|
|
a10481603d | ||
|
|
b8ff0e0244 | ||
|
|
85755902e1 | ||
|
|
38d3a7ed41 | ||
|
|
18fae7ddd8 | ||
|
|
b83473ce3a | ||
|
|
e34a58e875 | ||
|
|
9f92fdeb29 | ||
|
|
c31444dc8b | ||
|
|
16e1b60a4d | ||
|
|
71de3bedf4 | ||
|
|
d339514d9a | ||
|
|
75ec96f282 | ||
|
|
8f8a5795d3 | ||
|
|
4631a9e62c | ||
|
|
51ee6a84b5 | ||
|
|
8d5006c0bd | ||
|
|
37208571fe | ||
|
|
759627b3c7 | ||
|
|
08fac4752f | ||
|
|
9307e7e0d8 | ||
|
|
b1a0801f9b | ||
|
|
04cc53b934 | ||
|
|
c138658a31 | ||
|
|
f1854f2c04 | ||
|
|
e4056d9ee6 | ||
|
|
eb95a54db2 | ||
|
|
7ddea4c70b | ||
|
|
3804e86995 | ||
|
|
b23bed182f | ||
|
|
f8e421871b | ||
|
|
d0103496b9 | ||
|
|
cd8952221e | ||
|
|
155c7539bd | ||
|
|
5f43681fb1 | ||
|
|
d2965e6e10 | ||
|
|
ec1ade7761 | ||
|
|
f35bef0d7b | ||
|
|
138d37cf5e | ||
|
|
fc2fed079f | ||
|
|
9c441a98f4 | ||
|
|
1491872b62 | ||
|
|
c74636ffa5 | ||
|
|
05677f93c5 | ||
|
|
0aef241df6 | ||
|
|
e0b58461b5 | ||
|
|
cd33c7f608 | ||
|
|
9d29af36e5 | ||
|
|
4472d7f9a8 | ||
|
|
999579915c | ||
|
|
63904fd303 | ||
|
|
2cb6872e4e | ||
|
|
f539bf051d | ||
|
|
14f845d623 | ||
|
|
133a80acef | ||
|
|
b43790de9a | ||
|
|
0bdd63df06 | ||
|
|
c6544b49e9 | ||
|
|
8e1a8b5f0e | ||
|
|
4717f5e230 | ||
|
|
01ee1ff845 | ||
|
|
75b4655f38 | ||
|
|
9b2f596d15 | ||
|
|
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
|
# NuGet Packages
|
||||||
*.nupkg
|
*.nupkg
|
||||||
|
!**/Xamarin.AndroidX.Credentials.1.0.0.nupkg
|
||||||
# The packages folder can be ignored because of Package Restore
|
# The packages folder can be ignored because of Package Restore
|
||||||
**/packages/*
|
**/packages/*
|
||||||
# except build/, which is used as an MSBuild target.
|
# except build/, which is used as an MSBuild target.
|
||||||
|
|||||||
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>
|
<configuration>
|
||||||
<packageSources>
|
<packageSources>
|
||||||
<add key="MAUI Nightly builds" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/maui-nightly/nuget/v3/index.json" />
|
<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>
|
</packageSources>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -121,6 +121,7 @@
|
|||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
<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.Activity.Ktx" Version="1.7.2.1" />
|
||||||
|
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
||||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,12 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Resolve<IWatchDeviceService>(),
|
ServiceContainer.Resolve<IWatchDeviceService>(),
|
||||||
ServiceContainer.Resolve<IConditionedAwaiterManager>());
|
ServiceContainer.Resolve<IConditionedAwaiterManager>());
|
||||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||||
|
|
||||||
|
var userPinService = new UserPinService(
|
||||||
|
ServiceContainer.Resolve<IStateService>(),
|
||||||
|
ServiceContainer.Resolve<ICryptoService>(),
|
||||||
|
ServiceContainer.Resolve<IVaultTimeoutService>());
|
||||||
|
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||||
}
|
}
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||||
@@ -160,7 +166,6 @@ namespace Bit.Droid
|
|||||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
|
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
|
||||||
var biometricService = new BiometricService(stateService, cryptoService);
|
var biometricService = new BiometricService(stateService, cryptoService);
|
||||||
var userPinService = new UserPinService(stateService, cryptoService);
|
|
||||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
||||||
|
|
||||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||||
@@ -184,7 +189,6 @@ namespace Bit.Droid
|
|||||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||||
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
|
||||||
|
|
||||||
// Push
|
// Push
|
||||||
#if FDROID
|
#if FDROID
|
||||||
|
|||||||
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;
|
_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()
|
public bool AutofillServiceEnabled()
|
||||||
{
|
{
|
||||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
@@ -163,7 +180,14 @@ namespace Bit.Droid.Services
|
|||||||
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
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()
|
public void DisableAutofillService()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Android.Text.Method;
|
|||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Android.Views.InputMethods;
|
using Android.Views.InputMethods;
|
||||||
using Android.Widget;
|
using Android.Widget;
|
||||||
|
using AndroidX.Credentials;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
@@ -501,6 +502,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()
|
public void OpenAccessibilitySettings()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -559,6 +581,8 @@ namespace Bit.Droid.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool SupportsCredentialProviderService() => Build.VERSION.SdkInt >= BuildVersionCodes.UpsideDownCake;
|
||||||
|
|
||||||
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
|
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
|
||||||
|
|
||||||
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
|
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ namespace Bit.iOS
|
|||||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||||
if (needsAutofillReplacement.GetValueOrDefault())
|
if (needsAutofillReplacement.GetValueOrDefault())
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (message.Command == "showAppExtension")
|
else if (message.Command == "showAppExtension")
|
||||||
@@ -102,7 +102,7 @@ namespace Bit.iOS
|
|||||||
var success = value as bool?;
|
var success = value as bool?;
|
||||||
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,22 +114,21 @@ namespace Bit.iOS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await ASHelpers.IdentitiesCanIncremental())
|
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||||
{
|
{
|
||||||
var cipherId = message.Data as string;
|
var cipherId = message.Data as string;
|
||||||
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
||||||
{
|
{
|
||||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherId);
|
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
|
||||||
if (identity == null)
|
if (identity == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync(
|
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||||
new ASPasswordCredentialIdentity[] { identity });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
||||||
{
|
{
|
||||||
@@ -138,28 +137,27 @@ namespace Bit.iOS
|
|||||||
return;
|
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);
|
message.Data as Bit.Core.Models.View.CipherView);
|
||||||
if (identity == null)
|
if (identity == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync(
|
await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
|
||||||
new ASPasswordCredentialIdentity[] { identity });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
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")
|
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
||||||
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
||||||
{
|
{
|
||||||
@@ -168,12 +166,12 @@ namespace Bit.iOS
|
|||||||
{
|
{
|
||||||
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||||
{
|
{
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace Bit.Core.Abstractions
|
|||||||
{
|
{
|
||||||
public interface IAutofillHandler
|
public interface IAutofillHandler
|
||||||
{
|
{
|
||||||
|
bool CredentialProviderServiceEnabled();
|
||||||
bool AutofillServicesEnabled();
|
bool AutofillServicesEnabled();
|
||||||
bool SupportsAutofillService();
|
bool SupportsAutofillService();
|
||||||
void Autofill(CipherView cipher);
|
void Autofill(CipherView cipher);
|
||||||
@@ -11,6 +12,7 @@ namespace Bit.Core.Abstractions
|
|||||||
bool AutofillAccessibilityServiceRunning();
|
bool AutofillAccessibilityServiceRunning();
|
||||||
bool AutofillAccessibilityOverlayPermitted();
|
bool AutofillAccessibilityOverlayPermitted();
|
||||||
bool AutofillServiceEnabled();
|
bool AutofillServiceEnabled();
|
||||||
|
void DisableCredentialProviderService();
|
||||||
void DisableAutofillService();
|
void DisableAutofillService();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
@@ -37,5 +34,7 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
|
||||||
Task SoftDeleteWithServerAsync(string id);
|
Task SoftDeleteWithServerAsync(string id);
|
||||||
Task RestoreWithServerAsync(string id);
|
Task RestoreWithServerAsync(string id);
|
||||||
|
Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams);
|
||||||
|
Task CopyTotpCodeIfNeededAsync(CipherView cipher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using System;
|
namespace Bit.Core.Abstractions
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
|
||||||
{
|
{
|
||||||
public enum AwaiterPrecondition
|
public enum AwaiterPrecondition
|
||||||
{
|
{
|
||||||
EnvironmentUrlsInited,
|
EnvironmentUrlsInited,
|
||||||
AndroidWindowCreated
|
AndroidWindowCreated,
|
||||||
|
AutofillIOSExtensionViewDidAppear
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IConditionedAwaiterManager
|
public interface IConditionedAwaiterManager
|
||||||
@@ -14,5 +12,6 @@ namespace Bit.Core.Abstractions
|
|||||||
Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition);
|
Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition);
|
||||||
void SetAsCompleted(AwaiterPrecondition awaiterPrecondition);
|
void SetAsCompleted(AwaiterPrecondition awaiterPrecondition);
|
||||||
void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex);
|
void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex);
|
||||||
|
void Recreate(AwaiterPrecondition awaiterPrecondition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace Bit.App.Abstractions
|
|||||||
bool SupportsNfc();
|
bool SupportsNfc();
|
||||||
bool SupportsCamera();
|
bool SupportsCamera();
|
||||||
bool SupportsFido2();
|
bool SupportsFido2();
|
||||||
|
bool SupportsCredentialProviderService();
|
||||||
bool SupportsAutofillServices();
|
bool SupportsAutofillServices();
|
||||||
bool SupportsInlineAutofill();
|
bool SupportsInlineAutofill();
|
||||||
bool SupportsDrawOver();
|
bool SupportsDrawOver();
|
||||||
@@ -36,6 +37,7 @@ namespace Bit.App.Abstractions
|
|||||||
void RateApp();
|
void RateApp();
|
||||||
void OpenAccessibilitySettings();
|
void OpenAccessibilitySettings();
|
||||||
void OpenAccessibilityOverlayPermissionSettings();
|
void OpenAccessibilityOverlayPermissionSettings();
|
||||||
|
void OpenCredentialProviderSettings();
|
||||||
void OpenAutofillSettings();
|
void OpenAutofillSettings();
|
||||||
long GetActiveTime();
|
long GetActiveTime();
|
||||||
void CloseMainApp();
|
void CloseMainApp();
|
||||||
|
|||||||
12
src/Core/Abstractions/IFido2AuthenticatorService.cs
Normal file
12
src/Core/Abstractions/IFido2AuthenticatorService.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public interface IFido2AuthenticatorService
|
||||||
|
{
|
||||||
|
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
|
||||||
|
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Core/Abstractions/IFido2GetAssertionUserInterface.cs
Normal file
20
src/Core/Abstractions/IFido2GetAssertionUserInterface.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public struct Fido2GetAssertionUserInterfaceCredential
|
||||||
|
{
|
||||||
|
public string CipherId { get; set; }
|
||||||
|
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IFido2GetAssertionUserInterface : IFido2UserInterface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ask the user to pick a credential from a list of existing credentials.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials">The credentials that the user can pick from, and if the user must be verified before completing the operation</param>
|
||||||
|
/// <returns>The ID of the cipher that contains the credentials the user picked, and if the user was verified before completing the operation</returns>
|
||||||
|
Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs
Normal file
44
src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
/// The preference to whether or not the user must be verified before completing the operation.
|
||||||
|
/// </summary>
|
||||||
|
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The relying party identifier
|
||||||
|
/// </summary>
|
||||||
|
public string RpId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IFido2MakeCredentialUserInterface : IFido2UserInterface
|
||||||
|
{
|
||||||
|
/// <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 InformExcludedCredentialAsync(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, and if the user was verified before completing the operation</returns>
|
||||||
|
Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/Core/Abstractions/IFido2MediatorService.cs
Normal file
14
src/Core/Abstractions/IFido2MediatorService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public interface IFido2MediatorService
|
||||||
|
{
|
||||||
|
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams);
|
||||||
|
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams);
|
||||||
|
|
||||||
|
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
|
||||||
|
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
|
||||||
|
Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
17
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public interface IFido2UserInterface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the vault has been unlocked during this transaction
|
||||||
|
/// </summary>
|
||||||
|
bool HasVaultBeenUnlockedInThisTransaction { get; }
|
||||||
|
|
||||||
|
/// <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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Threading.Tasks;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
@@ -10,5 +9,7 @@ namespace Bit.App.Abstractions
|
|||||||
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
|
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
|
||||||
|
|
||||||
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
||||||
|
|
||||||
|
Task<bool> ShouldByPassMasterPasswordRepromptAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
@@ -29,7 +26,7 @@ namespace Bit.Core.Abstractions
|
|||||||
bool SupportsDuo();
|
bool SupportsDuo();
|
||||||
Task<bool> SupportsBiometricAsync();
|
Task<bool> SupportsBiometricAsync();
|
||||||
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
|
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
|
||||||
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false);
|
Task<bool?> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false);
|
||||||
long GetActiveTime();
|
long GetActiveTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ namespace Bit.Core.Abstractions
|
|||||||
Task<BwRegion?> GetActiveUserRegionAsync();
|
Task<BwRegion?> GetActiveUserRegionAsync();
|
||||||
Task<BwRegion?> GetPreAuthRegionAsync();
|
Task<BwRegion?> GetPreAuthRegionAsync();
|
||||||
Task SetPreAuthRegionAsync(BwRegion value);
|
Task SetPreAuthRegionAsync(BwRegion value);
|
||||||
|
Task ReloadStateAsync();
|
||||||
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
||||||
Task<string> GetPinProtectedAsync(string userId = null);
|
Task<string> GetPinProtectedAsync(string userId = null);
|
||||||
[Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
[Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using System.Threading.Tasks;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
public interface IUserPinService
|
public interface IUserPinService
|
||||||
{
|
{
|
||||||
|
Task<bool> IsPinLockEnabledAsync();
|
||||||
Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart);
|
Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart);
|
||||||
|
Task<bool> VerifyPinAsync(string inputPin);
|
||||||
|
Task<bool> VerifyPinAsync(string inputPin, string email, KdfConfig kdfConfig, PinLockType pinLockType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/Core/Abstractions/IUserVerificationMediatorService.cs
Normal file
28
src/Core/Abstractions/IUserVerificationMediatorService.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public interface IUserVerificationMediatorService
|
||||||
|
{
|
||||||
|
Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
||||||
|
Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options);
|
||||||
|
Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options);
|
||||||
|
Task<bool> ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options);
|
||||||
|
Task<CancellableResult<UVResult>> PerformOSUnlockAsync();
|
||||||
|
Task<CancellableResult<UVResult>> VerifyPinCodeAsync();
|
||||||
|
Task<CancellableResult<UVResult>> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt);
|
||||||
|
|
||||||
|
public struct UVResult
|
||||||
|
{
|
||||||
|
public UVResult(bool canPerform, bool isVerified)
|
||||||
|
{
|
||||||
|
CanPerform = canPerform;
|
||||||
|
IsVerified = isVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanPerform { get; set; }
|
||||||
|
public bool IsVerified { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using System.Threading.Tasks;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
public interface IUserVerificationService
|
public interface IUserVerificationService
|
||||||
{
|
{
|
||||||
Task<bool> VerifyUser(string secret, VerificationType verificationType);
|
Task<bool> VerifyUser(string secret, VerificationType verificationType);
|
||||||
|
Task<bool> VerifyMasterPasswordAsync(string masterPassword);
|
||||||
Task<bool> HasMasterPasswordAsync(bool checkMasterKeyHash = false);
|
Task<bool> HasMasterPasswordAsync(bool checkMasterKeyHash = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||||
<PackageReference Include="PCLCrypto" Version="2.1.40-alpha" />
|
<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="zxcvbn-core" Version="7.0.92" />
|
||||||
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||||
<PackageReference Include="Xamarin.AndroidX.AutoFill" Version="1.1.0.18" />
|
<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.Activity.Ktx" Version="1.7.2.1" />
|
||||||
|
<PackageReference Include="Xamarin.AndroidX.Credentials" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android' AND !$(DefineConstants.Contains(FDROID))">
|
||||||
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
<PackageReference Include="Xamarin.GooglePlayServices.SafetyNet" Version="118.0.1.5" />
|
||||||
@@ -75,8 +77,10 @@
|
|||||||
<Folder Include="Utilities\Automation\" />
|
<Folder Include="Utilities\Automation\" />
|
||||||
<Folder Include="Utilities\Prompts\" />
|
<Folder Include="Utilities\Prompts\" />
|
||||||
<Folder Include="Resources\Localization\" />
|
<Folder Include="Resources\Localization\" />
|
||||||
|
<Folder Include="Utilities\Fido2\" />
|
||||||
<Folder Include="Controls\Picker\" />
|
<Folder Include="Controls\Picker\" />
|
||||||
<Folder Include="Controls\Avatar\" />
|
<Folder Include="Controls\Avatar\" />
|
||||||
|
<Folder Include="Services\UserVerification\" />
|
||||||
<Folder Include="Utilities\WebAuthenticatorMAUI\" />
|
<Folder Include="Utilities\WebAuthenticatorMAUI\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -106,8 +110,10 @@
|
|||||||
</MauiXaml>
|
</MauiXaml>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="Utilities\Fido2\" />
|
||||||
<None Remove="Controls\Picker\" />
|
<None Remove="Controls\Picker\" />
|
||||||
<None Remove="Controls\Avatar\" />
|
<None Remove="Controls\Avatar\" />
|
||||||
|
<None Remove="Services\UserVerification\" />
|
||||||
<None Remove="Utilities\WebAuthenticatorMAUI\" />
|
<None Remove="Utilities\WebAuthenticatorMAUI\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Domain;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api
|
namespace Bit.Core.Models.Api
|
||||||
{
|
{
|
||||||
@@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api
|
|||||||
RpName = fido2Key.RpName?.EncryptedString;
|
RpName = fido2Key.RpName?.EncryptedString;
|
||||||
UserHandle = fido2Key.UserHandle?.EncryptedString;
|
UserHandle = fido2Key.UserHandle?.EncryptedString;
|
||||||
UserName = fido2Key.UserName?.EncryptedString;
|
UserName = fido2Key.UserName?.EncryptedString;
|
||||||
|
UserDisplayName = fido2Key.UserDisplayName?.EncryptedString;
|
||||||
Counter = fido2Key.Counter?.EncryptedString;
|
Counter = fido2Key.Counter?.EncryptedString;
|
||||||
CreationDate = fido2Key.CreationDate;
|
CreationDate = fido2Key.CreationDate;
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data
|
|||||||
RpName = apiData.RpName;
|
RpName = apiData.RpName;
|
||||||
UserHandle = apiData.UserHandle;
|
UserHandle = apiData.UserHandle;
|
||||||
UserName = apiData.UserName;
|
UserName = apiData.UserName;
|
||||||
|
UserDisplayName = apiData.UserDisplayName;
|
||||||
Counter = apiData.Counter;
|
Counter = apiData.Counter;
|
||||||
CreationDate = apiData.CreationDate;
|
CreationDate = apiData.CreationDate;
|
||||||
}
|
}
|
||||||
@@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Models.Data;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Domain
|
namespace Bit.Core.Models.Domain
|
||||||
@@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
nameof(RpName),
|
nameof(RpName),
|
||||||
nameof(UserHandle),
|
nameof(UserHandle),
|
||||||
nameof(UserName),
|
nameof(UserName),
|
||||||
|
nameof(UserDisplayName),
|
||||||
nameof(Counter)
|
nameof(Counter)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
public EncString RpName { get; set; }
|
public EncString RpName { get; set; }
|
||||||
public EncString UserHandle { get; set; }
|
public EncString UserHandle { get; set; }
|
||||||
public EncString UserName { get; set; }
|
public EncString UserName { get; set; }
|
||||||
|
public EncString UserDisplayName { get; set; }
|
||||||
public EncString Counter { get; set; }
|
public EncString Counter { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Domain
|
namespace Bit.Core.Models.Domain
|
||||||
{
|
{
|
||||||
@@ -9,7 +8,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
{
|
{
|
||||||
if (key == null)
|
if (key == null)
|
||||||
{
|
{
|
||||||
throw new Exception("Must provide key.");
|
throw new ArgumentKeyNullException(nameof(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encType == null)
|
if (encType == null)
|
||||||
@@ -24,7 +23,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new Exception("Unable to determine encType.");
|
throw new InvalidKeyOperationException("Unable to determine encType.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ namespace Bit.Core.Models.Domain
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new Exception("Unsupported encType/key length.");
|
throw new InvalidKeyOperationException("Unsupported encType/key length.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Key != null)
|
if (Key != null)
|
||||||
@@ -72,6 +71,32 @@ namespace Bit.Core.Models.Domain
|
|||||||
public string KeyB64 { get; set; }
|
public string KeyB64 { get; set; }
|
||||||
public string EncKeyB64 { get; set; }
|
public string EncKeyB64 { get; set; }
|
||||||
public string MacKeyB64 { get; set; }
|
public string MacKeyB64 { get; set; }
|
||||||
|
|
||||||
|
public class ArgumentKeyNullException : ArgumentNullException
|
||||||
|
{
|
||||||
|
public ArgumentKeyNullException(string paramName) : base(paramName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArgumentKeyNullException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArgumentKeyNullException(string paramName, string message) : base(paramName, message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InvalidKeyOperationException : InvalidOperationException
|
||||||
|
{
|
||||||
|
public InvalidKeyOperationException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidKeyOperationException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UserKey : SymmetricCryptoKey
|
public class UserKey : SymmetricCryptoKey
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
|
||||||
namespace Bit.Core.Models.View
|
namespace Bit.Core.Models.View
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System.Text.Json.Serialization;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Models.View
|
namespace Bit.Core.Models.View
|
||||||
{
|
{
|
||||||
@@ -26,13 +26,42 @@ namespace Bit.Core.Models.View
|
|||||||
public string RpName { get; set; }
|
public string RpName { get; set; }
|
||||||
public string UserHandle { get; set; }
|
public string UserHandle { get; set; }
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
public string UserDisplayName { get; set; }
|
||||||
public string Counter { get; set; }
|
public string Counter { get; set; }
|
||||||
public DateTime CreationDate { 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public override string SubTitle => UserName;
|
public override string SubTitle => UserName;
|
||||||
|
|
||||||
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
|
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);
|
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
|
||||||
|
[JsonIgnore]
|
||||||
public string LaunchUri => $"https://{RpId}";
|
public string LaunchUri => $"https://{RpId}";
|
||||||
|
|
||||||
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
|
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Models.View
|
namespace Bit.Core.Models.View
|
||||||
{
|
{
|
||||||
@@ -40,4 +39,15 @@ namespace Bit.Core.Models.View
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class LoginViewExtensions
|
||||||
|
{
|
||||||
|
public static string GetMainFido2CredentialUsername(this LoginView loginView)
|
||||||
|
{
|
||||||
|
return loginView.MainFido2Credential.UserName
|
||||||
|
.FallbackOnNullOrWhiteSpace(loginView.MainFido2Credential.UserDisplayName)
|
||||||
|
.FallbackOnNullOrWhiteSpace(loginView.Username)
|
||||||
|
.FallbackOnNullOrWhiteSpace(AppResources.UnknownAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
<ScrollView>
|
<ScrollView>
|
||||||
<StackLayout Spacing="20">
|
<StackLayout Spacing="20">
|
||||||
<StackLayout StyleClass="box">
|
<StackLayout StyleClass="box">
|
||||||
|
<StackLayout StyleClass="box-row-header">
|
||||||
|
<Label Text="MAUI APP"
|
||||||
|
StyleClass="box-header, box-header-platform" />
|
||||||
|
</StackLayout>
|
||||||
<StackLayout StyleClass="box-row-header">
|
<StackLayout StyleClass="box-row-header">
|
||||||
<Label Text="{u:I18n SelfHostedEnvironment, Header=True}"
|
<Label Text="{u:I18n SelfHostedEnvironment, Header=True}"
|
||||||
StyleClass="box-header, box-header-platform" />
|
StyleClass="box-header, box-header-platform" />
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ namespace Bit.App.Pages
|
|||||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
|
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
|
||||||
!PinEnabled && !HasMasterPassword);
|
!PinEnabled && !HasMasterPassword) ?? false;
|
||||||
|
|
||||||
await _stateService.SetBiometricLockedAsync(!success);
|
await _stateService.SetBiometricLockedAsync(!success);
|
||||||
if (success)
|
if (success)
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
get
|
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,
|
AppResources.Version,
|
||||||
_platformUtilsService.GetApplicationVersion(),
|
_platformUtilsService.GetApplicationVersion(),
|
||||||
_deviceActionService.GetBuildNumber());
|
_deviceActionService.GetBuildNumber());
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
x:Class="Bit.App.Pages.AutofillPage"
|
x:Class="Bit.App.Pages.AutofillPage"
|
||||||
xmlns:pages="clr-namespace:Bit.App.Pages"
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
Title="{u:I18n PasswordAutofill}">
|
Title="{u:I18n SetUpAutofill}">
|
||||||
|
|
||||||
<ContentPage.ToolbarItems>
|
<ContentPage.ToolbarItems>
|
||||||
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
|
||||||
@@ -15,26 +15,22 @@
|
|||||||
<StackLayout Spacing="5"
|
<StackLayout Spacing="5"
|
||||||
Padding="20, 20, 20, 30"
|
Padding="20, 20, 20, 30"
|
||||||
VerticalOptions="FillAndExpand">
|
VerticalOptions="FillAndExpand">
|
||||||
<Label Text="{u:I18n ExtensionInstantAccess}"
|
<Label Text="{u:I18n GetInstantAccessToYourPasswordsAndPasskeys}"
|
||||||
HorizontalOptions="Center"
|
HorizontalOptions="Center"
|
||||||
HorizontalTextAlignment="Center"
|
HorizontalTextAlignment="Center"
|
||||||
LineBreakMode="WordWrap"
|
LineBreakMode="WordWrap"
|
||||||
StyleClass="text-lg"
|
StyleClass="text-lg"
|
||||||
Margin="0, 0, 0, 15" />
|
Margin="0, 0, 0, 15" />
|
||||||
<Label Text="{u:I18n AutofillTurnOn}"
|
<Label Text="{u:I18n SetUpAutoFillDescriptionLong}"
|
||||||
HorizontalOptions="Center"
|
HorizontalOptions="Center"
|
||||||
HorizontalTextAlignment="Center"
|
HorizontalTextAlignment="Center"
|
||||||
LineBreakMode="WordWrap"
|
LineBreakMode="WordWrap"
|
||||||
Margin="0, 0, 0, 15" />
|
Margin="0, 0, 0, 15" />
|
||||||
<Label Text="{u:I18n AutofillTurnOn1}"
|
<Label Text="{u:I18n FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions}"
|
||||||
LineBreakMode="WordWrap" />
|
LineBreakMode="WordWrap" />
|
||||||
<Label Text="{u:I18n AutofillTurnOn2}"
|
<Label Text="{u:I18n SecondDotTurnOnAutoFill}"
|
||||||
LineBreakMode="WordWrap" />
|
LineBreakMode="WordWrap" />
|
||||||
<Label Text="{u:I18n AutofillTurnOn3}"
|
<Label Text="{u:I18n ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys}"
|
||||||
LineBreakMode="WordWrap" />
|
|
||||||
<Label Text="{u:I18n AutofillTurnOn4}"
|
|
||||||
LineBreakMode="WordWrap" />
|
|
||||||
<Label Text="{u:I18n AutofillTurnOn5}"
|
|
||||||
LineBreakMode="WordWrap" />
|
LineBreakMode="WordWrap" />
|
||||||
<Image Source="autofill-kb.png"
|
<Image Source="autofill-kb.png"
|
||||||
VerticalOptions="CenterAndExpand"
|
VerticalOptions="CenterAndExpand"
|
||||||
|
|||||||
@@ -19,6 +19,15 @@
|
|||||||
Text="{u:I18n Autofill}"
|
Text="{u:I18n Autofill}"
|
||||||
StyleClass="settings-header" />
|
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
|
<controls:SwitchItemView
|
||||||
Title="{u:I18n AutofillServices}"
|
Title="{u:I18n AutofillServices}"
|
||||||
Subtitle="{u:I18n AutofillServicesExplanationLong}"
|
Subtitle="{u:I18n AutofillServicesExplanationLong}"
|
||||||
|
|||||||
@@ -6,12 +6,27 @@ namespace Bit.App.Pages
|
|||||||
{
|
{
|
||||||
public partial class AutofillSettingsPageViewModel
|
public partial class AutofillSettingsPageViewModel
|
||||||
{
|
{
|
||||||
|
private bool _useCredentialProviderService;
|
||||||
private bool _useAutofillServices;
|
private bool _useAutofillServices;
|
||||||
private bool _useInlineAutofill;
|
private bool _useInlineAutofill;
|
||||||
private bool _useAccessibility;
|
private bool _useAccessibility;
|
||||||
private bool _useDrawOver;
|
private bool _useDrawOver;
|
||||||
private bool _askToAddLogin;
|
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 SupportsAndroidAutofillServices => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsAutofillServices();
|
||||||
|
|
||||||
public bool UseAutofillServices
|
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 ToggleUseAutofillServicesCommand { get; private set; }
|
||||||
public AsyncRelayCommand ToggleUseInlineAutofillCommand { get; private set; }
|
public AsyncRelayCommand ToggleUseInlineAutofillCommand { get; private set; }
|
||||||
public AsyncRelayCommand ToggleUseAccessibilityCommand { get; private set; }
|
public AsyncRelayCommand ToggleUseAccessibilityCommand { get; private set; }
|
||||||
@@ -93,6 +109,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private void InitAndroidCommands()
|
private void InitAndroidCommands()
|
||||||
{
|
{
|
||||||
|
ToggleUseCredentialProviderServiceCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseCredentialProviderService()), () => _inited, allowsMultipleExecutions: false);
|
||||||
ToggleUseAutofillServicesCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited, allowsMultipleExecutions: false);
|
ToggleUseAutofillServicesCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited, allowsMultipleExecutions: false);
|
||||||
ToggleUseInlineAutofillCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited, allowsMultipleExecutions: false);
|
ToggleUseInlineAutofillCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited, allowsMultipleExecutions: false);
|
||||||
ToggleUseAccessibilityCommand = CreateDefaultAsyncRelayCommand(ToggleUseAccessibilityAsync, () => _inited, allowsMultipleExecutions: false);
|
ToggleUseAccessibilityCommand = CreateDefaultAsyncRelayCommand(ToggleUseAccessibilityAsync, () => _inited, allowsMultipleExecutions: false);
|
||||||
@@ -115,6 +132,9 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
private async Task UpdateAndroidAutofillSettingsAsync()
|
private async Task UpdateAndroidAutofillSettingsAsync()
|
||||||
{
|
{
|
||||||
|
// TODO - uncomment once _autofillHandler.CredentialProviderServiceEnabled() returns a real value
|
||||||
|
// _useCredentialProviderService =
|
||||||
|
// SupportsCredentialProviderService && _autofillHandler.CredentialProviderServiceEnabled();
|
||||||
_useAutofillServices =
|
_useAutofillServices =
|
||||||
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
||||||
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
|
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
|
||||||
@@ -123,6 +143,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
await MainThread.InvokeOnMainThreadAsync(() =>
|
await MainThread.InvokeOnMainThreadAsync(() =>
|
||||||
{
|
{
|
||||||
|
TriggerPropertyChanged(nameof(UseCredentialProviderService));
|
||||||
TriggerPropertyChanged(nameof(UseAutofillServices));
|
TriggerPropertyChanged(nameof(UseAutofillServices));
|
||||||
TriggerPropertyChanged(nameof(UseAccessibility));
|
TriggerPropertyChanged(nameof(UseAccessibility));
|
||||||
TriggerPropertyChanged(nameof(UseDrawOver));
|
TriggerPropertyChanged(nameof(UseDrawOver));
|
||||||
@@ -130,6 +151,18 @@ namespace Bit.App.Pages
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleUseCredentialProviderService()
|
||||||
|
{
|
||||||
|
if (UseCredentialProviderService)
|
||||||
|
{
|
||||||
|
_deviceActionService.OpenCredentialProviderSettings();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_autofillHandler.DisableCredentialProviderService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ToggleUseAutofillServices()
|
private void ToggleUseAutofillServices()
|
||||||
{
|
{
|
||||||
if (UseAutofillServices)
|
if (UseAutofillServices)
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
if (!_supportsBiometric
|
if (!_supportsBiometric
|
||||||
||
|
||
|
||||||
!await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null))
|
await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null) != true)
|
||||||
{
|
{
|
||||||
_canUnlockWithBiometrics = false;
|
_canUnlockWithBiometrics = false;
|
||||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
|
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
|
||||||
|
|||||||
225
src/Core/Resources/Localization/AppResources.Designer.cs
generated
225
src/Core/Resources/Localization/AppResources.Designer.cs
generated
@@ -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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Bitwarden Help Center.
|
/// Looks up a localized string similar to Bitwarden Help Center.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Choose file.
|
/// Looks up a localized string similar to Choose file.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Credits.
|
/// Looks up a localized string similar to Credits.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2596,6 +2632,24 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Error creating passkey.
|
||||||
|
/// </summary>
|
||||||
|
public static string ErrorCreatingPasskey {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ErrorCreatingPasskey", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Error reading passkey.
|
||||||
|
/// </summary>
|
||||||
|
public static string ErrorReadingPasskey {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ErrorReadingPasskey", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to EU.
|
/// Looks up a localized string similar to EU.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3136,6 +3190,15 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to 1. Go to your device's Settings > Passwords > Password Options.
|
||||||
|
/// </summary>
|
||||||
|
public static string FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to First name.
|
/// Looks up a localized string similar to First name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -3325,6 +3388,15 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Get instant access to your passwords and passkeys!.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetInstantAccessToYourPasswordsAndPasskeys {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("GetInstantAccessToYourPasswordsAndPasskeys", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Get master password hint.
|
/// Looks up a localized string similar to Get master password hint.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -5128,6 +5200,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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Ownership.
|
/// Looks up a localized string similar to Ownership.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -5155,6 +5236,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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Passkey will not be copied.
|
/// Looks up a localized string similar to Passkey will not be copied.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -5335,6 +5425,15 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Passwords.
|
||||||
|
/// </summary>
|
||||||
|
public static string Passwords {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Passwords", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to This password was not found in any known data breaches. It should be safe to use..
|
/// Looks up a localized string similar to This password was not found in any known data breaches. It should be safe to use..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -5344,6 +5443,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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Password type.
|
/// Looks up a localized string similar to Password type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -5849,6 +5957,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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Saving....
|
/// Looks up a localized string similar to Saving....
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -5957,6 +6083,15 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to 2. Turn on AutoFill.
|
||||||
|
/// </summary>
|
||||||
|
public static string SecondDotTurnOnAutoFill {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SecondDotTurnOnAutoFill", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Secure notes.
|
/// Looks up a localized string similar to Secure notes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -6281,6 +6416,24 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Set up auto-fill.
|
||||||
|
/// </summary>
|
||||||
|
public static string SetUpAutofill {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SetUpAutofill", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to To set up password auto-fill and passkey management, set Bitwarden as your preferred provider in the iOS Settings..
|
||||||
|
/// </summary>
|
||||||
|
public static string SetUpAutoFillDescriptionLong {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("SetUpAutoFillDescriptionLong", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Set up TOTP.
|
/// Looks up a localized string similar to Set up TOTP.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -6686,6 +6839,24 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to There was a problem creating a passkey for {0}. Try again later..
|
||||||
|
/// </summary>
|
||||||
|
public static string ThereWasAProblemCreatingAPasskeyForXTryAgainLater {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ThereWasAProblemCreatingAPasskeyForXTryAgainLater", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to There was a problem reading your passkey for {0}. Try again later..
|
||||||
|
/// </summary>
|
||||||
|
public static string ThereWasAProblemReadingAPasskeyForXTryAgainLater {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ThereWasAProblemReadingAPasskeyForXTryAgainLater", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to The URI {0} is already blocked.
|
/// Looks up a localized string similar to The URI {0} is already blocked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -6695,6 +6866,15 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to 3. Select "Bitwarden" to use for passwords and passkeys.
|
||||||
|
/// </summary>
|
||||||
|
public static string ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to 30 days.
|
/// Looks up a localized string similar to 30 days.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -6722,6 +6902,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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to This request is no longer valid.
|
/// Looks up a localized string similar to This request is no longer valid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -7019,6 +7208,15 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Unknown account.
|
||||||
|
/// </summary>
|
||||||
|
public static string UnknownAccount {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("UnknownAccount", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Unknown {0} error occurred..
|
/// Looks up a localized string similar to Unknown {0} error occurred..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -7505,6 +7703,24 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Verification required by {0}.
|
||||||
|
/// </summary>
|
||||||
|
public static string VerificationRequiredByX {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("VerificationRequiredByX", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Verification required for this action. Set up an unlock method in Bitwarden to continue..
|
||||||
|
/// </summary>
|
||||||
|
public static string VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Verify Face ID.
|
/// Looks up a localized string similar to Verify Face ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -7532,6 +7748,15 @@ namespace Bit.Core.Resources.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Verifying identity....
|
||||||
|
/// </summary>
|
||||||
|
public static string VerifyingIdentityEllipsis {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("VerifyingIdentityEllipsis", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Verify master password.
|
/// Looks up a localized string similar to Verify master password.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1191,6 +1191,9 @@ Scanning will happen automatically.</value>
|
|||||||
<data name="WindowsHello" xml:space="preserve">
|
<data name="WindowsHello" xml:space="preserve">
|
||||||
<value>Windows Hello</value>
|
<value>Windows Hello</value>
|
||||||
</data>
|
</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">
|
<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>
|
<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>
|
</data>
|
||||||
@@ -1816,6 +1819,9 @@ Scanning will happen automatically.</value>
|
|||||||
<data name="AccessibilityDrawOverPermissionAlert" xml:space="preserve">
|
<data name="AccessibilityDrawOverPermissionAlert" xml:space="preserve">
|
||||||
<value>Bitwarden needs attention - Turn on "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value>
|
<value>Bitwarden needs attention - Turn on "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="CredentialProviderService" xml:space="preserve">
|
||||||
|
<value>Credential Provider service</value>
|
||||||
|
</data>
|
||||||
<data name="AutofillServices" xml:space="preserve">
|
<data name="AutofillServices" xml:space="preserve">
|
||||||
<value>Auto-fill services</value>
|
<value>Auto-fill services</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2799,6 +2805,9 @@ Do you want to switch to this account?</value>
|
|||||||
<data name="XHours" xml:space="preserve">
|
<data name="XHours" xml:space="preserve">
|
||||||
<value>{0} hours</value>
|
<value>{0} hours</value>
|
||||||
</data>
|
</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">
|
<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>
|
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2877,6 +2886,27 @@ Do you want to switch to this account?</value>
|
|||||||
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
|
||||||
<value>Set up an unlock option to change your vault timeout action.</value>
|
<value>Set up an unlock option to change your vault timeout action.</value>
|
||||||
</data>
|
</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>
|
||||||
<data name="DuoTwoStepLoginIsRequiredForYourAccount" xml:space="preserve">
|
<data name="DuoTwoStepLoginIsRequiredForYourAccount" xml:space="preserve">
|
||||||
<value>Duo two-step login is required for your account. </value>
|
<value>Duo two-step login is required for your account. </value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2886,4 +2916,51 @@ Do you want to switch to this account?</value>
|
|||||||
<data name="LaunchDuo" xml:space="preserve">
|
<data name="LaunchDuo" xml:space="preserve">
|
||||||
<value>Launch Duo</value>
|
<value>Launch Duo</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="VerificationRequiredByX" xml:space="preserve">
|
||||||
|
<value>Verification required by {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue" xml:space="preserve">
|
||||||
|
<value>Verification required for this action. Set up an unlock method in Bitwarden to continue.</value>
|
||||||
|
</data>
|
||||||
|
<data name="ErrorCreatingPasskey" xml:space="preserve">
|
||||||
|
<value>Error creating passkey</value>
|
||||||
|
</data>
|
||||||
|
<data name="ErrorReadingPasskey" xml:space="preserve">
|
||||||
|
<value>Error reading passkey</value>
|
||||||
|
</data>
|
||||||
|
<data name="ThereWasAProblemCreatingAPasskeyForXTryAgainLater" xml:space="preserve">
|
||||||
|
<value>There was a problem creating a passkey for {0}. Try again later.</value>
|
||||||
|
<comment>The parameter is the RpId</comment>
|
||||||
|
</data>
|
||||||
|
<data name="ThereWasAProblemReadingAPasskeyForXTryAgainLater" xml:space="preserve">
|
||||||
|
<value>There was a problem reading your passkey for {0}. Try again later.</value>
|
||||||
|
<comment>The parameter is the RpId</comment>
|
||||||
|
</data>
|
||||||
|
<data name="VerifyingIdentityEllipsis" xml:space="preserve">
|
||||||
|
<value>Verifying identity...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Passwords" xml:space="preserve">
|
||||||
|
<value>Passwords</value>
|
||||||
|
</data>
|
||||||
|
<data name="UnknownAccount" xml:space="preserve">
|
||||||
|
<value>Unknown account</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetUpAutofill" xml:space="preserve">
|
||||||
|
<value>Set up auto-fill</value>
|
||||||
|
</data>
|
||||||
|
<data name="GetInstantAccessToYourPasswordsAndPasskeys" xml:space="preserve">
|
||||||
|
<value>Get instant access to your passwords and passkeys!</value>
|
||||||
|
</data>
|
||||||
|
<data name="SetUpAutoFillDescriptionLong" xml:space="preserve">
|
||||||
|
<value>To set up password auto-fill and passkey management, set Bitwarden as your preferred provider in the iOS Settings.</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstDotGoToYourDeviceSettingsPasswordsPasswordOptions" xml:space="preserve">
|
||||||
|
<value>1. Go to your device's Settings > Passwords > Password Options</value>
|
||||||
|
</data>
|
||||||
|
<data name="SecondDotTurnOnAutoFill" xml:space="preserve">
|
||||||
|
<value>2. Turn on AutoFill</value>
|
||||||
|
</data>
|
||||||
|
<data name="ThirdDotSelectBitwardenToUseForPasswordsAndPasskeys" xml:space="preserve">
|
||||||
|
<value>3. Select "Bitwarden" to use for passwords and passkeys</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ namespace Bit.Core.Services
|
|||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly Func<ISearchService> _searchService;
|
private readonly Func<ISearchService> _searchService;
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
|
private readonly ITotpService _totpService;
|
||||||
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly string _clearCipherCacheKey;
|
private readonly string _clearCipherCacheKey;
|
||||||
private readonly string[] _allClearCipherCacheKeys;
|
private readonly string[] _allClearCipherCacheKeys;
|
||||||
private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>>
|
private Dictionary<string, HashSet<string>> _domainMatchBlacklist = new Dictionary<string, HashSet<string>>
|
||||||
@@ -53,6 +55,8 @@ namespace Bit.Core.Services
|
|||||||
II18nService i18nService,
|
II18nService i18nService,
|
||||||
Func<ISearchService> searchService,
|
Func<ISearchService> searchService,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
|
ITotpService totpService,
|
||||||
|
IClipboardService clipboardService,
|
||||||
string clearCipherCacheKey,
|
string clearCipherCacheKey,
|
||||||
string[] allClearCipherCacheKeys)
|
string[] allClearCipherCacheKeys)
|
||||||
{
|
{
|
||||||
@@ -65,6 +69,8 @@ namespace Bit.Core.Services
|
|||||||
_i18nService = i18nService;
|
_i18nService = i18nService;
|
||||||
_searchService = searchService;
|
_searchService = searchService;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_totpService = totpService;
|
||||||
|
_clipboardService = clipboardService;
|
||||||
_clearCipherCacheKey = clearCipherCacheKey;
|
_clearCipherCacheKey = clearCipherCacheKey;
|
||||||
_allClearCipherCacheKeys = allClearCipherCacheKeys;
|
_allClearCipherCacheKeys = allClearCipherCacheKeys;
|
||||||
}
|
}
|
||||||
@@ -1286,6 +1292,51 @@ namespace Bit.Core.Services
|
|||||||
cipher.PasswordHistory = encPhs;
|
cipher.PasswordHistory = encPhs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams)
|
||||||
|
{
|
||||||
|
var newCipher = new CipherView
|
||||||
|
{
|
||||||
|
Name = newPasskeyParams.CredentialName,
|
||||||
|
Type = CipherType.Login,
|
||||||
|
Login = new LoginView
|
||||||
|
{
|
||||||
|
Username = newPasskeyParams.UserName,
|
||||||
|
Uris = new List<LoginUriView>
|
||||||
|
{
|
||||||
|
new LoginUriView { Uri = newPasskeyParams.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CopyTotpCodeIfNeededAsync(CipherView cipher)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cipher?.Login?.Totp)
|
||||||
|
||
|
||||||
|
await _stateService.GetDisableAutoTotpCopyAsync() == true)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipher.OrganizationUseTotp || await _stateService.CanAccessPremiumAsync())
|
||||||
|
{
|
||||||
|
var totpCode = await _totpService.GetCodeAsync(cipher.Login.Totp);
|
||||||
|
await _clipboardService.CopyTextAsync(totpCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class CipherLocaleComparer : IComparer<CipherView>
|
private class CipherLocaleComparer : IComparer<CipherView>
|
||||||
{
|
{
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
@@ -11,7 +8,8 @@ namespace Bit.Core.Services
|
|||||||
private readonly ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>> _preconditionsTasks = new ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>>
|
private readonly ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>> _preconditionsTasks = new ConcurrentDictionary<AwaiterPrecondition, TaskCompletionSource<bool>>
|
||||||
{
|
{
|
||||||
[AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource<bool>(),
|
[AwaiterPrecondition.EnvironmentUrlsInited] = new TaskCompletionSource<bool>(),
|
||||||
[AwaiterPrecondition.AndroidWindowCreated] = new TaskCompletionSource<bool>()
|
[AwaiterPrecondition.AndroidWindowCreated] = new TaskCompletionSource<bool>(),
|
||||||
|
[AwaiterPrecondition.AutofillIOSExtensionViewDidAppear] = new TaskCompletionSource<bool>()
|
||||||
};
|
};
|
||||||
|
|
||||||
public Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition)
|
public Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition)
|
||||||
@@ -39,5 +37,15 @@ namespace Bit.Core.Services
|
|||||||
tcs.TrySetException(ex);
|
tcs.TrySetException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Recreate(AwaiterPrecondition awaiterPrecondition)
|
||||||
|
{
|
||||||
|
if (_preconditionsTasks.TryRemove(awaiterPrecondition, out var oldTcs))
|
||||||
|
{
|
||||||
|
oldTcs.TrySetCanceled();
|
||||||
|
|
||||||
|
_preconditionsTasks.TryAdd(awaiterPrecondition, new TaskCompletionSource<bool>());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
509
src/Core/Services/Fido2AuthenticatorService.cs
Normal file
509
src/Core/Services/Fido2AuthenticatorService.cs
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
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 readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||||
|
|
||||||
|
public Fido2AuthenticatorService(ICipherService cipherService,
|
||||||
|
ISyncService syncService,
|
||||||
|
ICryptoFunctionService cryptoFunctionService,
|
||||||
|
IUserVerificationMediatorService userVerificationMediatorService)
|
||||||
|
{
|
||||||
|
_cipherService = cipherService;
|
||||||
|
_syncService = syncService;
|
||||||
|
_cryptoFunctionService = cryptoFunctionService;
|
||||||
|
_userVerificationMediatorService = userVerificationMediatorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface)
|
||||||
|
{
|
||||||
|
if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Alg != (int)Fido2AlgorithmIdentifier.ES256))
|
||||||
|
{
|
||||||
|
throw new NotSupportedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
await userInterface.EnsureUnlockedVaultAsync();
|
||||||
|
await _syncService.FullSyncAsync(false);
|
||||||
|
|
||||||
|
var existingCipherIds = await FindExcludedCredentialsAsync(
|
||||||
|
makeCredentialParams.ExcludeCredentialDescriptorList
|
||||||
|
);
|
||||||
|
if (existingCipherIds.Length > 0)
|
||||||
|
{
|
||||||
|
await userInterface.InformExcludedCredentialAsync(existingCipherIds);
|
||||||
|
throw new NotAllowedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await userInterface.ConfirmNewCredentialAsync(new Fido2ConfirmNewCredentialParams
|
||||||
|
{
|
||||||
|
CredentialName = makeCredentialParams.RpEntity.Name,
|
||||||
|
UserName = makeCredentialParams.UserEntity.Name,
|
||||||
|
UserVerificationPreference = makeCredentialParams.UserVerificationPreference,
|
||||||
|
RpId = makeCredentialParams.RpEntity.Id
|
||||||
|
});
|
||||||
|
|
||||||
|
var cipherId = response.CipherId;
|
||||||
|
var userVerified = response.UserVerified;
|
||||||
|
string credentialId;
|
||||||
|
if (cipherId == null)
|
||||||
|
{
|
||||||
|
throw new NotAllowedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var keyPair = GenerateKeyPair();
|
||||||
|
var fido2Credential = CreateCredentialView(makeCredentialParams, keyPair.privateKey);
|
||||||
|
|
||||||
|
var encrypted = await _cipherService.GetAsync(cipherId);
|
||||||
|
var cipher = await encrypted.DecryptAsync();
|
||||||
|
|
||||||
|
if (!userVerified
|
||||||
|
&&
|
||||||
|
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||||
|
cipher.Reprompt != CipherRepromptType.None,
|
||||||
|
makeCredentialParams.UserVerificationPreference,
|
||||||
|
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
||||||
|
{
|
||||||
|
throw new NotAllowedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
cipher.Login.Fido2Credentials = new List<Fido2CredentialView> { fido2Credential };
|
||||||
|
var reencrypted = await _cipherService.EncryptAsync(cipher);
|
||||||
|
await _cipherService.SaveWithServerAsync(reencrypted);
|
||||||
|
credentialId = fido2Credential.CredentialId;
|
||||||
|
|
||||||
|
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 ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
throw new UnknownError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface)
|
||||||
|
{
|
||||||
|
List<CipherView> cipherOptions;
|
||||||
|
|
||||||
|
await userInterface.EnsureUnlockedVaultAsync();
|
||||||
|
await _syncService.FullSyncAsync(false);
|
||||||
|
|
||||||
|
if (assertionParams.AllowCredentialDescriptorList?.Length > 0)
|
||||||
|
{
|
||||||
|
cipherOptions = await FindCredentialsByIdAsync(
|
||||||
|
assertionParams.AllowCredentialDescriptorList,
|
||||||
|
assertionParams.RpId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cipherOptions = await FindCredentialsByRpAsync(assertionParams.RpId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipherOptions.Count == 0)
|
||||||
|
{
|
||||||
|
throw new NotAllowedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await userInterface.PickCredentialAsync(
|
||||||
|
cipherOptions.Select((cipher) => new Fido2GetAssertionUserInterfaceCredential
|
||||||
|
{
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.GetUserVerificationPreferenceFrom(assertionParams.UserVerificationPreference, cipher.Reprompt)
|
||||||
|
}).ToArray()
|
||||||
|
);
|
||||||
|
var selectedCipherId = response.CipherId;
|
||||||
|
var userVerified = response.UserVerified;
|
||||||
|
|
||||||
|
var selectedCipher = cipherOptions.FirstOrDefault((c) => c.Id == selectedCipherId);
|
||||||
|
if (selectedCipher == null)
|
||||||
|
{
|
||||||
|
throw new NotAllowedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userVerified
|
||||||
|
&&
|
||||||
|
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||||
|
selectedCipher.Reprompt != CipherRepromptType.None,
|
||||||
|
assertionParams.UserVerificationPreference,
|
||||||
|
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
||||||
|
{
|
||||||
|
throw new NotAllowedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var selectedFido2Credential = selectedCipher.Login.MainFido2Credential;
|
||||||
|
var selectedCredentialId = selectedFido2Credential.CredentialId;
|
||||||
|
|
||||||
|
await _cipherService.UpdateLastUsedDateAsync(selectedCipher.Id);
|
||||||
|
|
||||||
|
if (selectedFido2Credential.CounterValue != 0)
|
||||||
|
{
|
||||||
|
++selectedFido2Credential.CounterValue;
|
||||||
|
var encrypted = await _cipherService.EncryptAsync(selectedCipher);
|
||||||
|
await _cipherService.SaveWithServerAsync(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticatorData = await GenerateAuthDataAsync(
|
||||||
|
rpId: selectedFido2Credential.RpId,
|
||||||
|
userPresence: true,
|
||||||
|
userVerification: userVerified,
|
||||||
|
counter: selectedFido2Credential.CounterValue
|
||||||
|
);
|
||||||
|
|
||||||
|
var signature = GenerateSignature(
|
||||||
|
authData: authenticatorData,
|
||||||
|
clientDataHash: assertionParams.Hash,
|
||||||
|
privateKey: selectedFido2Credential.KeyBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Fido2AuthenticatorGetAssertionResult
|
||||||
|
{
|
||||||
|
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential
|
||||||
|
{
|
||||||
|
Id = selectedCredentialId.GuidToRawFormat(),
|
||||||
|
UserHandle = selectedFido2Credential.UserHandleValue,
|
||||||
|
Cipher = selectedCipher
|
||||||
|
},
|
||||||
|
AuthenticatorData = authenticatorData,
|
||||||
|
Signature = signature
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
throw new UnknownError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId)
|
||||||
|
{
|
||||||
|
var credentials = (await FindCredentialsByRpAsync(rpId)).Select(cipher => new Fido2AuthenticatorDiscoverableCredentialMetadata
|
||||||
|
{
|
||||||
|
Type = Constants.DefaultFido2CredentialType,
|
||||||
|
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
|
||||||
|
{
|
||||||
|
ids.Add(credential.Id.GuidToStandardFormat());
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ids.Count == 0)
|
||||||
|
{
|
||||||
|
return new List<CipherView>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ciphers = await _cipherService.GetAllDecryptedAsync();
|
||||||
|
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.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
var flags = AuthDataFlags(
|
||||||
|
extensionData: false,
|
||||||
|
attestationData: isAttestation,
|
||||||
|
userVerification: userVerification,
|
||||||
|
userPresence: userPresence
|
||||||
|
);
|
||||||
|
authData.Add(flags);
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
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 (backupState)
|
||||||
|
{
|
||||||
|
flags |= 0b00010000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupEligibility)
|
||||||
|
{
|
||||||
|
flags |= 0b00001000;
|
||||||
|
}
|
||||||
|
|
||||||
|
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((int)Fido2AlgorithmIdentifier.ES256);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
261
src/Core/Services/Fido2ClientService.cs
Normal file
261
src/Core/Services/Fido2ClientService.cs
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
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;
|
||||||
|
private readonly IFido2GetAssertionUserInterface _getAssertionUserInterface;
|
||||||
|
private readonly IFido2MakeCredentialUserInterface _makeCredentialUserInterface;
|
||||||
|
|
||||||
|
public Fido2ClientService(
|
||||||
|
IStateService stateService,
|
||||||
|
IEnvironmentService environmentService,
|
||||||
|
ICryptoFunctionService cryptoFunctionService,
|
||||||
|
IFido2AuthenticatorService fido2AuthenticatorService,
|
||||||
|
IFido2GetAssertionUserInterface getAssertionUserInterface,
|
||||||
|
IFido2MakeCredentialUserInterface makeCredentialUserInterface)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_environmentService = environmentService;
|
||||||
|
_cryptoFunctionService = cryptoFunctionService;
|
||||||
|
_fido2AuthenticatorService = fido2AuthenticatorService;
|
||||||
|
_getAssertionUserInterface = getAssertionUserInterface;
|
||||||
|
_makeCredentialUserInterface = makeCredentialUserInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
|
||||||
|
{
|
||||||
|
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||||
|
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
|
||||||
|
if (blockedUris != null && 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)
|
||||||
|
{
|
||||||
|
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 == (int)Fido2AlgorithmIdentifier.ES256 && kp.Type == Constants.DefaultFido2CredentialType)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Assign default algorithms
|
||||||
|
credTypesAndPubKeyAlgs = new PublicKeyCredentialParameters[]
|
||||||
|
{
|
||||||
|
new PublicKeyCredentialParameters { Alg = (int) Fido2AlgorithmIdentifier.ES256, Type = Constants.DefaultFido2CredentialType },
|
||||||
|
new PublicKeyCredentialParameters { Alg = (int) Fido2AlgorithmIdentifier.RS256, Type = Constants.DefaultFido2CredentialType }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, _makeCredentialUserInterface);
|
||||||
|
|
||||||
|
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" ? new string[] { "internal", "usb" } : new string[] { "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 != null && 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, _getAssertionUserInterface);
|
||||||
|
|
||||||
|
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,
|
||||||
|
Cipher = getAssertionResult.SelectedCredential.Cipher
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
|
||||||
|
return new Fido2AuthenticatorMakeCredentialParams
|
||||||
|
{
|
||||||
|
RequireResidentKey = requireResidentKey,
|
||||||
|
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(createCredentialParams.AuthenticatorSelection?.UserVerification),
|
||||||
|
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
|
||||||
|
CredTypesAndPubKeyAlgs = credTypesAndPubKeyAlgs,
|
||||||
|
Hash = clientDataHash,
|
||||||
|
RpEntity = createCredentialParams.Rp,
|
||||||
|
UserEntity = createCredentialParams.User,
|
||||||
|
Extensions = createCredentialParams.Extensions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Fido2AuthenticatorGetAssertionParams MapToGetAssertionParams(
|
||||||
|
Fido2ClientAssertCredentialParams assertCredentialParams,
|
||||||
|
byte[] cliendDataHash)
|
||||||
|
{
|
||||||
|
return new Fido2AuthenticatorGetAssertionParams {
|
||||||
|
RpId = assertCredentialParams.RpId,
|
||||||
|
Challenge = assertCredentialParams.Challenge,
|
||||||
|
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,
|
||||||
|
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(assertCredentialParams?.UserVerification),
|
||||||
|
Hash = cliendDataHash
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/Core/Services/Fido2MediatorService.cs
Normal file
60
src/Core/Services/Fido2MediatorService.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services
|
||||||
|
{
|
||||||
|
public class Fido2MediatorService : IFido2MediatorService
|
||||||
|
{
|
||||||
|
private readonly IFido2AuthenticatorService _fido2AuthenticatorService;
|
||||||
|
private readonly IFido2ClientService _fido2ClientService;
|
||||||
|
private readonly ICipherService _cipherService;
|
||||||
|
|
||||||
|
public Fido2MediatorService(IFido2AuthenticatorService fido2AuthenticatorService,
|
||||||
|
IFido2ClientService fido2ClientService,
|
||||||
|
ICipherService cipherService)
|
||||||
|
{
|
||||||
|
_fido2AuthenticatorService = fido2AuthenticatorService;
|
||||||
|
_fido2ClientService = fido2ClientService;
|
||||||
|
_cipherService = cipherService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams)
|
||||||
|
{
|
||||||
|
var result = await _fido2ClientService.AssertCredentialAsync(assertCredentialParams);
|
||||||
|
|
||||||
|
if (result?.Cipher != null)
|
||||||
|
{
|
||||||
|
await _cipherService.CopyTotpCodeIfNeededAsync(result.Cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
|
||||||
|
{
|
||||||
|
return _fido2ClientService.CreateCredentialAsync(createCredentialParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface)
|
||||||
|
{
|
||||||
|
var result = await _fido2AuthenticatorService.GetAssertionAsync(assertionParams, userInterface);
|
||||||
|
|
||||||
|
if (result?.SelectedCredential?.Cipher != null)
|
||||||
|
{
|
||||||
|
await _cipherService.CopyTotpCodeIfNeededAsync(result.SelectedCredential.Cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface)
|
||||||
|
{
|
||||||
|
return _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams, userInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId)
|
||||||
|
{
|
||||||
|
return _fido2AuthenticatorService.SilentCredentialDiscoveryAsync(rpId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/Core/Services/Logging/ClipLogger.cs
Normal file
66
src/Core/Services/Logging/ClipLogger.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
|
||||||
|
#if IOS
|
||||||
|
using UIKit;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Bit.Core.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This logger can be used to help debug iOS extensions where we cannot use the .NET debugger yet
|
||||||
|
/// so we can use this that copies the logs to the clipboard so one
|
||||||
|
/// can paste them and analyze its output.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
MainThread.BeginInvokeOnMainThread(() => 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,4 @@
|
|||||||
using System;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
@@ -22,9 +21,9 @@ namespace Bit.Core.Services
|
|||||||
#if !FDROID
|
#if !FDROID
|
||||||
// just in case the caller throws the exception in a moment where the logger can't be resolved
|
// 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
|
// 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
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ namespace Bit.App.Services
|
|||||||
return passwordValid;
|
return passwordValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> ShouldByPassMasterPasswordRepromptAsync()
|
public async Task<bool> ShouldByPassMasterPasswordRepromptAsync()
|
||||||
{
|
{
|
||||||
return await _cryptoService.GetMasterKeyHashAsync() is null;
|
return await _cryptoService.GetMasterKeyHashAsync() is null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
using System;
|
using Bit.App.Abstractions;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.App.Abstractions;
|
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
using Bit.Core.Resources.Localization;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Plugin.Fingerprint;
|
using Plugin.Fingerprint;
|
||||||
using Plugin.Fingerprint.Abstractions;
|
using Plugin.Fingerprint.Abstractions;
|
||||||
using Microsoft.Maui.ApplicationModel.DataTransfer;
|
|
||||||
using Microsoft.Maui.ApplicationModel;
|
|
||||||
using Microsoft.Maui.Devices;
|
|
||||||
using Microsoft.Maui.Controls;
|
|
||||||
using Microsoft.Maui;
|
|
||||||
|
|
||||||
namespace Bit.App.Services
|
namespace Bit.App.Services
|
||||||
{
|
{
|
||||||
@@ -245,31 +238,34 @@ namespace Bit.App.Services
|
|||||||
return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
|
return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
|
public async Task<bool?> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
|
||||||
Action fallback = null, bool logOutOnTooManyAttempts = false)
|
Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (text == null)
|
if (text == null)
|
||||||
{
|
{
|
||||||
text = AppResources.BiometricsDirection;
|
text = AppResources.BiometricsDirection;
|
||||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
#if IOS
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||||
{
|
text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
|
||||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
#endif
|
||||||
text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var biometricRequest = new AuthenticationRequestConfiguration(AppResources.Bitwarden, text)
|
var biometricRequest = new AuthenticationRequestConfiguration(AppResources.Bitwarden, text)
|
||||||
{
|
{
|
||||||
CancelTitle = AppResources.Cancel,
|
CancelTitle = AppResources.Cancel,
|
||||||
FallbackTitle = fallbackText
|
FallbackTitle = fallbackText,
|
||||||
|
AllowAlternativeAuthentication = allowAlternativeAuthentication
|
||||||
};
|
};
|
||||||
var result = await CrossFingerprint.Current.AuthenticateAsync(biometricRequest);
|
var result = await CrossFingerprint.Current.AuthenticateAsync(biometricRequest);
|
||||||
if (result.Authenticated)
|
if (result.Authenticated)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (result.Status == FingerprintAuthenticationResultStatus.Canceled)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
|
if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
|
||||||
{
|
{
|
||||||
fallback?.Invoke();
|
fallback?.Invoke();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Text;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
using PCLCrypto;
|
using PCLCrypto;
|
||||||
using static PCLCrypto.WinRTCrypto;
|
using static PCLCrypto.WinRTCrypto;
|
||||||
|
|
||||||
|
|||||||
@@ -1688,6 +1688,11 @@ namespace Bit.Core.Services
|
|||||||
await _storageService.SaveAsync(Constants.iOSExtensionActiveUserIdKey, userId);
|
await _storageService.SaveAsync(Constants.iOSExtensionActiveUserIdKey, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ReloadStateAsync()
|
||||||
|
{
|
||||||
|
_state = await GetStateFromStorageAsync() ?? new State();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CheckStateAsync()
|
private async Task CheckStateAsync()
|
||||||
{
|
{
|
||||||
if (!_migrationChecked)
|
if (!_migrationChecked)
|
||||||
@@ -1699,7 +1704,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if (_state == null)
|
if (_state == null)
|
||||||
{
|
{
|
||||||
_state = await GetStateFromStorageAsync() ?? new State();
|
await ReloadStateAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Threading.Tasks;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.App.Services
|
namespace Bit.App.Services
|
||||||
{
|
{
|
||||||
@@ -7,11 +8,25 @@ namespace Bit.App.Services
|
|||||||
{
|
{
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly ICryptoService _cryptoService;
|
private readonly ICryptoService _cryptoService;
|
||||||
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
|
|
||||||
public UserPinService(IStateService stateService, ICryptoService cryptoService)
|
public UserPinService(IStateService stateService, ICryptoService cryptoService, IVaultTimeoutService vaultTimeoutService)
|
||||||
{
|
{
|
||||||
_stateService = stateService;
|
_stateService = stateService;
|
||||||
_cryptoService = cryptoService;
|
_cryptoService = cryptoService;
|
||||||
|
_vaultTimeoutService = vaultTimeoutService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsPinLockEnabledAsync()
|
||||||
|
{
|
||||||
|
var pinLockType = await _vaultTimeoutService.GetPinLockTypeAsync();
|
||||||
|
|
||||||
|
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
||||||
|
?? await _stateService.GetPinProtectedKeyAsync();
|
||||||
|
|
||||||
|
return (pinLockType == PinLockType.Transient && ephemeralPinSet != null)
|
||||||
|
||
|
||||||
|
pinLockType == PinLockType.Persistent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart)
|
public async Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart)
|
||||||
@@ -34,5 +49,59 @@ namespace Bit.App.Services
|
|||||||
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
|
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> VerifyPinAsync(string inputPin)
|
||||||
|
{
|
||||||
|
var (email, kdfConfig) = await _stateService.GetActiveUserCustomDataAsync(a => a?.Profile is null ? (null, default) : (a.Profile.Email, new KdfConfig(a.Profile)));
|
||||||
|
if (kdfConfig.Type is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await VerifyPinAsync(inputPin, email, kdfConfig, await _vaultTimeoutService.GetPinLockTypeAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> VerifyPinAsync(string inputPin, string email, KdfConfig kdfConfig, PinLockType pinLockType)
|
||||||
|
{
|
||||||
|
EncString userKeyPin = null;
|
||||||
|
EncString oldPinProtected = null;
|
||||||
|
if (pinLockType == PinLockType.Persistent)
|
||||||
|
{
|
||||||
|
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||||
|
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||||
|
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||||
|
}
|
||||||
|
else if (pinLockType == PinLockType.Transient)
|
||||||
|
{
|
||||||
|
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
||||||
|
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserKey userKey;
|
||||||
|
if (oldPinProtected != null)
|
||||||
|
{
|
||||||
|
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
||||||
|
pinLockType == PinLockType.Transient,
|
||||||
|
inputPin,
|
||||||
|
email,
|
||||||
|
kdfConfig,
|
||||||
|
oldPinProtected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||||
|
inputPin,
|
||||||
|
email,
|
||||||
|
kdfConfig,
|
||||||
|
userKeyPin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||||
|
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||||
|
|
||||||
|
return decryptedPin == inputPin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services.UserVerification
|
||||||
|
{
|
||||||
|
public class Fido2UserVerificationPreferredServiceStrategy : IUserVerificationServiceStrategy
|
||||||
|
{
|
||||||
|
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||||
|
|
||||||
|
public Fido2UserVerificationPreferredServiceStrategy(IUserVerificationMediatorService userVerificationMediatorService)
|
||||||
|
{
|
||||||
|
_userVerificationMediatorService = userVerificationMediatorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||||
|
{
|
||||||
|
if (options.HasVaultBeenUnlockedInTransaction)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.OnNeedUITask != null)
|
||||||
|
{
|
||||||
|
await options.OnNeedUITask();
|
||||||
|
}
|
||||||
|
|
||||||
|
var osUnlockVerification = await _userVerificationMediatorService.PerformOSUnlockAsync();
|
||||||
|
if (osUnlockVerification.IsCancelled)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(false, true);
|
||||||
|
}
|
||||||
|
if (osUnlockVerification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(osUnlockVerification.Result.IsVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CancellableResult<bool>(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services.UserVerification
|
||||||
|
{
|
||||||
|
public class Fido2UserVerificationRequiredServiceStrategy : IUserVerificationServiceStrategy
|
||||||
|
{
|
||||||
|
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
|
||||||
|
public Fido2UserVerificationRequiredServiceStrategy(IUserVerificationMediatorService userVerificationMediatorService,
|
||||||
|
IPlatformUtilsService platformUtilsService)
|
||||||
|
{
|
||||||
|
_userVerificationMediatorService = userVerificationMediatorService;
|
||||||
|
_platformUtilsService = platformUtilsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||||
|
{
|
||||||
|
if (options.HasVaultBeenUnlockedInTransaction)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.OnNeedUITask != null)
|
||||||
|
{
|
||||||
|
await options.OnNeedUITask();
|
||||||
|
}
|
||||||
|
|
||||||
|
var osUnlockVerification = await _userVerificationMediatorService.PerformOSUnlockAsync();
|
||||||
|
if (osUnlockVerification.IsCancelled)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(false, true);
|
||||||
|
}
|
||||||
|
if (osUnlockVerification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(osUnlockVerification.Result.IsVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pinVerification = await _userVerificationMediatorService.VerifyPinCodeAsync();
|
||||||
|
if (pinVerification.IsCancelled)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(false, true);
|
||||||
|
}
|
||||||
|
if (pinVerification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(pinVerification.Result.IsVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpVerification = await _userVerificationMediatorService.VerifyMasterPasswordAsync(false);
|
||||||
|
if (mpVerification.IsCancelled)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(false, true);
|
||||||
|
}
|
||||||
|
if (mpVerification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(mpVerification.Result.IsVerified);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Setup PIN code. For the sake of simplicity, we're not implementing this step now and just telling the user to do it in the main app.
|
||||||
|
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue,
|
||||||
|
string.Format(AppResources.VerificationRequiredByX, options.RpId),
|
||||||
|
AppResources.Ok);
|
||||||
|
|
||||||
|
return new CancellableResult<bool>(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services.UserVerification
|
||||||
|
{
|
||||||
|
public interface IUserVerificationServiceStrategy
|
||||||
|
{
|
||||||
|
Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
using Plugin.Fingerprint;
|
||||||
|
using static Bit.Core.Abstractions.IUserVerificationMediatorService;
|
||||||
|
using FingerprintAvailability = Plugin.Fingerprint.Abstractions.FingerprintAvailability;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services.UserVerification
|
||||||
|
{
|
||||||
|
public class UserVerificationMediatorService : IUserVerificationMediatorService
|
||||||
|
{
|
||||||
|
private const byte MAX_ATTEMPTS = 5;
|
||||||
|
|
||||||
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||||
|
private readonly IUserPinService _userPinService;
|
||||||
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IUserVerificationService _userVerificationService;
|
||||||
|
|
||||||
|
private readonly Dictionary<Fido2UserVerificationPreference, IUserVerificationServiceStrategy> _fido2UserVerificationStrategies = new Dictionary<Fido2UserVerificationPreference, IUserVerificationServiceStrategy>();
|
||||||
|
|
||||||
|
public UserVerificationMediatorService(
|
||||||
|
IPlatformUtilsService platformUtilsService,
|
||||||
|
IPasswordRepromptService passwordRepromptService,
|
||||||
|
IUserPinService userPinService,
|
||||||
|
IDeviceActionService deviceActionService,
|
||||||
|
IUserVerificationService userVerificationService)
|
||||||
|
{
|
||||||
|
_platformUtilsService = platformUtilsService;
|
||||||
|
_passwordRepromptService = passwordRepromptService;
|
||||||
|
_userPinService = userPinService;
|
||||||
|
_deviceActionService = deviceActionService;
|
||||||
|
_userVerificationService = userVerificationService;
|
||||||
|
|
||||||
|
_fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Required, new Fido2UserVerificationRequiredServiceStrategy(this, _platformUtilsService));
|
||||||
|
_fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Preferred, new Fido2UserVerificationPreferredServiceStrategy(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||||
|
{
|
||||||
|
if (await ShouldPerformMasterPasswordRepromptAsync(options))
|
||||||
|
{
|
||||||
|
if (options.OnNeedUITask != null)
|
||||||
|
{
|
||||||
|
await options.OnNeedUITask();
|
||||||
|
}
|
||||||
|
|
||||||
|
var mpVerification = await VerifyMasterPasswordAsync(true);
|
||||||
|
return new CancellableResult<bool>(
|
||||||
|
!mpVerification.IsCancelled && mpVerification.Result.CanPerform && mpVerification.Result.IsVerified,
|
||||||
|
mpVerification.IsCancelled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_fido2UserVerificationStrategies.TryGetValue(options.UserVerificationPreference, out var userVerificationServiceStrategy))
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await userVerificationServiceStrategy.VerifyUserForFido2Async(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options)
|
||||||
|
{
|
||||||
|
if (await ShouldPerformMasterPasswordRepromptAsync(options))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.HasVaultBeenUnlockedInTransaction
|
||||||
|
||
|
||||||
|
await CrossFingerprint.Current.GetAvailabilityAsync() == FingerprintAvailability.Available
|
||||||
|
||
|
||||||
|
await CrossFingerprint.Current.GetAvailabilityAsync(true) == FingerprintAvailability.Available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options)
|
||||||
|
{
|
||||||
|
return options.ShouldCheckMasterPasswordReprompt && !await _passwordRepromptService.ShouldByPassMasterPasswordRepromptAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options)
|
||||||
|
{
|
||||||
|
switch (options.UserVerificationPreference)
|
||||||
|
{
|
||||||
|
case Fido2UserVerificationPreference.Required:
|
||||||
|
return true;
|
||||||
|
case Fido2UserVerificationPreference.Discouraged:
|
||||||
|
return await ShouldPerformMasterPasswordRepromptAsync(options);
|
||||||
|
default:
|
||||||
|
return await CanPerformUserVerificationPreferredAsync(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CancellableResult<UVResult>> PerformOSUnlockAsync()
|
||||||
|
{
|
||||||
|
var availability = await CrossFingerprint.Current.GetAvailabilityAsync();
|
||||||
|
if (availability == FingerprintAvailability.Available)
|
||||||
|
{
|
||||||
|
var isValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null);
|
||||||
|
if (!isValid.HasValue)
|
||||||
|
{
|
||||||
|
return new UVResult(false, false).AsCancellable(true);
|
||||||
|
}
|
||||||
|
return new UVResult(true, isValid.Value).AsCancellable();
|
||||||
|
}
|
||||||
|
|
||||||
|
var alternativeAuthAvailability = await CrossFingerprint.Current.GetAvailabilityAsync(true);
|
||||||
|
if (alternativeAuthAvailability == FingerprintAvailability.Available)
|
||||||
|
{
|
||||||
|
var isNonBioValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null, allowAlternativeAuthentication: true);
|
||||||
|
if (!isNonBioValid.HasValue)
|
||||||
|
{
|
||||||
|
return new UVResult(false, false).AsCancellable(true);
|
||||||
|
}
|
||||||
|
return new UVResult(true, isNonBioValid.Value).AsCancellable();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UVResult(false, false).AsCancellable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CancellableResult<UVResult>> VerifyPinCodeAsync()
|
||||||
|
{
|
||||||
|
return await VerifyWithAttemptsAsync(async () =>
|
||||||
|
{
|
||||||
|
if (!await _userPinService.IsPinLockEnabledAsync())
|
||||||
|
{
|
||||||
|
return new UVResult(false, false).AsCancellable();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
|
||||||
|
AppResources.VerifyPIN, null, AppResources.Ok, AppResources.Cancel, password: true);
|
||||||
|
if (pin is null)
|
||||||
|
{
|
||||||
|
// cancelled by the user
|
||||||
|
return new UVResult(true, false).AsCancellable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var isVerified = await _userPinService.VerifyPinAsync(pin);
|
||||||
|
return new UVResult(true, isVerified).AsCancellable();
|
||||||
|
}
|
||||||
|
catch (SymmetricCryptoKey.ArgumentKeyNullException)
|
||||||
|
{
|
||||||
|
return new UVResult(true, false).AsCancellable();
|
||||||
|
}
|
||||||
|
catch (SymmetricCryptoKey.InvalidKeyOperationException)
|
||||||
|
{
|
||||||
|
return new UVResult(true, false).AsCancellable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CancellableResult<UVResult>> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt)
|
||||||
|
{
|
||||||
|
return await VerifyWithAttemptsAsync(async () =>
|
||||||
|
{
|
||||||
|
if (!await _userVerificationService.HasMasterPasswordAsync(true))
|
||||||
|
{
|
||||||
|
return new UVResult(false, false).AsCancellable();
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = isMasterPasswordReprompt ? AppResources.PasswordConfirmation : AppResources.MasterPassword;
|
||||||
|
var body = isMasterPasswordReprompt ? AppResources.PasswordConfirmationDesc : string.Empty;
|
||||||
|
|
||||||
|
var (password, isValid) = await _platformUtilsService.ShowPasswordDialogAndGetItAsync(title, body, _userVerificationService.VerifyMasterPasswordAsync);
|
||||||
|
if (password is null)
|
||||||
|
{
|
||||||
|
return new UVResult(true, false).AsCancellable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UVResult(true, isValid).AsCancellable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CancellableResult<UVResult>> VerifyWithAttemptsAsync(Func<Task<CancellableResult<UVResult>>> verifyAsync)
|
||||||
|
{
|
||||||
|
byte attempts = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var verification = await verifyAsync();
|
||||||
|
if (verification.IsCancelled)
|
||||||
|
{
|
||||||
|
return new UVResult(false, false).AsCancellable(true);
|
||||||
|
}
|
||||||
|
if (!verification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new UVResult(false, false).AsCancellable();
|
||||||
|
}
|
||||||
|
if (verification.Result.IsVerified)
|
||||||
|
{
|
||||||
|
return new UVResult(true, true).AsCancellable();
|
||||||
|
}
|
||||||
|
} while (++attempts < MAX_ATTEMPTS);
|
||||||
|
|
||||||
|
return new UVResult(true, false).AsCancellable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UVResultExtensions
|
||||||
|
{
|
||||||
|
public static CancellableResult<UVResult> AsCancellable(this UVResult result, bool isCancelled = false)
|
||||||
|
{
|
||||||
|
return new CancellableResult<UVResult>(result, isCancelled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace Bit.Core.Services
|
|||||||
_keyConnectorService = keyConnectorService;
|
_keyConnectorService = keyConnectorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
async public Task<bool> VerifyUser(string secret, VerificationType verificationType)
|
public async Task<bool> VerifyUser(string secret, VerificationType verificationType)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(secret))
|
if (string.IsNullOrEmpty(secret))
|
||||||
{
|
{
|
||||||
@@ -61,6 +61,12 @@ namespace Bit.Core.Services
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> VerifyMasterPasswordAsync(string masterPassword)
|
||||||
|
{
|
||||||
|
var masterKey = await _cryptoService.GetOrDeriveMasterKeyAsync(masterPassword);
|
||||||
|
return await _cryptoService.CompareAndUpdateKeyHashAsync(masterPassword, masterKey);
|
||||||
|
}
|
||||||
|
|
||||||
async private Task InvalidSecretErrorAsync(VerificationType verificationType)
|
async private Task InvalidSecretErrorAsync(VerificationType verificationType)
|
||||||
{
|
{
|
||||||
var errorMessage = verificationType == VerificationType.OTP
|
var errorMessage = verificationType == VerificationType.OTP
|
||||||
|
|||||||
15
src/Core/Utilities/CancellableResult.cs
Normal file
15
src/Core/Utilities/CancellableResult.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Bit.Core.Utilities
|
||||||
|
{
|
||||||
|
public readonly struct CancellableResult<T>
|
||||||
|
{
|
||||||
|
public CancellableResult(T result, bool isCancelled = false)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
IsCancelled = isCancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Result { get; }
|
||||||
|
|
||||||
|
public bool IsCancelled { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,12 +38,38 @@ namespace Bit.Core.Utilities
|
|||||||
#endif
|
#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)
|
public static string GetHostname(string uriString)
|
||||||
{
|
{
|
||||||
var uri = GetUri(uriString);
|
var uri = GetUri(uriString);
|
||||||
return string.IsNullOrEmpty(uri?.Host) ? null : uri.Host;
|
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)
|
public static string GetHost(string uriString)
|
||||||
{
|
{
|
||||||
var uri = GetUri(uriString);
|
var uri = GetUri(uriString);
|
||||||
@@ -61,6 +87,19 @@ namespace Bit.Core.Utilities
|
|||||||
return null;
|
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)
|
public static string GetDomain(string uriString)
|
||||||
{
|
{
|
||||||
var uri = GetUri(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
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/Core/Utilities/Fido2/Fido2AlgorithmIdentifier.cs
Normal file
8
src/Core/Utilities/Fido2/Fido2AlgorithmIdentifier.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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,26 @@
|
|||||||
|
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 the user verification preference in order to complete the request. Examples of UV gestures are fingerprint scan or a PIN.
|
||||||
|
/// </summary>
|
||||||
|
public Fido2UserVerificationPreference UserVerificationPreference { 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,23 @@
|
|||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities.Fido2
|
||||||
|
{
|
||||||
|
public class Fido2AuthenticatorGetAssertionResult
|
||||||
|
{
|
||||||
|
public byte[] AuthenticatorData { get; set; }
|
||||||
|
|
||||||
|
public byte[] Signature { get; set; }
|
||||||
|
|
||||||
|
public Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Fido2AuthenticatorGetAssertionSelectedCredential {
|
||||||
|
public byte[] Id { get; set; }
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
public byte[]? UserHandle { get; set; }
|
||||||
|
|
||||||
|
public CipherView? Cipher { 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 preference for assertion, provided by the client.
|
||||||
|
/// </summary>
|
||||||
|
public Fido2UserVerificationPreference UserVerificationPreference { 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,49 @@
|
|||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The selected cipher login item that has the credential
|
||||||
|
/// </summary>
|
||||||
|
public CipherView? Cipher { 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 ErrorCode Code { get; }
|
||||||
|
public string Reason { get; }
|
||||||
|
|
||||||
|
public Fido2ClientException(ErrorCode code, string reason) : base($"{code} ({reason})")
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
Reason = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Core/Utilities/Fido2/Fido2DomainUtils.cs
Normal file
40
src/Core/Utilities/Fido2/Fido2DomainUtils.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities.Fido2
|
||||||
|
{
|
||||||
|
public class Fido2DomainUtils
|
||||||
|
{
|
||||||
|
// Loosely based on:
|
||||||
|
// 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 || rpId == "" || origin == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only care about the domain part of the origin, not the protocol or port so we remove them here,
|
||||||
|
// while still keeping ipv6 intact.
|
||||||
|
// https is enforced in the client, so we don't need to worry about that here
|
||||||
|
var originWithoutProtocolPortOrPath = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?$", "$2");
|
||||||
|
if (Uri.CheckHostName(rpId) != UriHostNameType.Dns || Uri.CheckHostName(originWithoutProtocolPortOrPath) != UriHostNameType.Dns)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rpId == originWithoutProtocolPortOrPath)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolPortOrPath, out var parsedOrgin))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedOrgin.Tld == parsedRpId.Tld &&
|
||||||
|
parsedOrgin.Domain == parsedRpId.Domain &&
|
||||||
|
(parsedOrgin.SubDomain == parsedRpId.SubDomain || parsedOrgin.SubDomain.EndsWith(parsedRpId.SubDomain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/Core/Utilities/Fido2/Fido2GetAssertionUserInterface.cs
Normal file
58
src/Core/Utilities/Fido2/Fido2GetAssertionUserInterface.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using Bit.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities.Fido2
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This implementation is used when all interactions are handled by the operating system.
|
||||||
|
/// Most often the user has already picked a credential by the time the Authenticator is called,
|
||||||
|
/// so this class just returns those values.
|
||||||
|
///
|
||||||
|
/// This class has no corresponding attestation variant, because that operation requires that the
|
||||||
|
/// user interacts with the app directly.
|
||||||
|
/// </summary>
|
||||||
|
public class Fido2GetAssertionUserInterface : IFido2GetAssertionUserInterface
|
||||||
|
{
|
||||||
|
private readonly string _cipherId;
|
||||||
|
private readonly bool _userVerified = false;
|
||||||
|
private readonly Func<Task> _ensureUnlockedVaultCallback;
|
||||||
|
private readonly Func<bool> _hasVaultBeenUnlockedInThisTransaction;
|
||||||
|
private readonly Func<string, Fido2UserVerificationPreference, Task<bool>> _verifyUserCallback;
|
||||||
|
|
||||||
|
/// <param name="cipherId">The cipherId for the credential that the user has already picker</param>
|
||||||
|
/// <param name="userVerified">True if the user has already been verified by the operating system</param>
|
||||||
|
public Fido2GetAssertionUserInterface(string cipherId,
|
||||||
|
bool userVerified,
|
||||||
|
Func<Task> ensureUnlockedVaultCallback,
|
||||||
|
Func<bool> hasVaultBeenUnlockedInThisTransaction,
|
||||||
|
Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback)
|
||||||
|
{
|
||||||
|
_cipherId = cipherId;
|
||||||
|
_userVerified = userVerified;
|
||||||
|
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
|
||||||
|
_hasVaultBeenUnlockedInThisTransaction = hasVaultBeenUnlockedInThisTransaction;
|
||||||
|
_verifyUserCallback = verifyUserCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasVaultBeenUnlockedInThisTransaction { get; private set; }
|
||||||
|
|
||||||
|
public async Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials)
|
||||||
|
{
|
||||||
|
if (credentials.Length == 0 || !credentials.Any(c => c.CipherId == _cipherId))
|
||||||
|
{
|
||||||
|
throw new NotAllowedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
var credential = credentials.First(c => c.CipherId == _cipherId);
|
||||||
|
var verified = _userVerified || await _verifyUserCallback(_cipherId, credential.UserVerificationPreference);
|
||||||
|
|
||||||
|
return (CipherId: _cipherId, UserVerified: verified);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureUnlockedVaultAsync()
|
||||||
|
{
|
||||||
|
await _ensureUnlockedVaultCallback();
|
||||||
|
|
||||||
|
HasVaultBeenUnlockedInThisTransaction = _hasVaultBeenUnlockedInThisTransaction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Core/Utilities/Fido2/Fido2UserVerificationOptions.cs
Normal file
24
src/Core/Utilities/Fido2/Fido2UserVerificationOptions.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
namespace Bit.Core.Utilities.Fido2
|
||||||
|
{
|
||||||
|
public readonly struct Fido2UserVerificationOptions
|
||||||
|
{
|
||||||
|
public Fido2UserVerificationOptions(bool shouldCheckMasterPasswordReprompt,
|
||||||
|
Fido2UserVerificationPreference userVerificationPreference,
|
||||||
|
bool hasVaultBeenUnlockedInTransaction,
|
||||||
|
string rpId = null,
|
||||||
|
Func<Task> onNeedUITask = null)
|
||||||
|
{
|
||||||
|
ShouldCheckMasterPasswordReprompt = shouldCheckMasterPasswordReprompt;
|
||||||
|
UserVerificationPreference = userVerificationPreference;
|
||||||
|
HasVaultBeenUnlockedInTransaction = hasVaultBeenUnlockedInTransaction;
|
||||||
|
RpId = rpId;
|
||||||
|
OnNeedUITask = onNeedUITask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldCheckMasterPasswordReprompt { get; }
|
||||||
|
public Fido2UserVerificationPreference UserVerificationPreference { get; }
|
||||||
|
public bool HasVaultBeenUnlockedInTransaction { get; }
|
||||||
|
public string RpId { get; }
|
||||||
|
public Func<Task> OnNeedUITask { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Core/Utilities/Fido2/Fido2UserVerificationPreference.cs
Normal file
39
src/Core/Utilities/Fido2/Fido2UserVerificationPreference.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Utilities.Fido2
|
||||||
|
{
|
||||||
|
public enum Fido2UserVerificationPreference
|
||||||
|
{
|
||||||
|
Discouraged,
|
||||||
|
Preferred,
|
||||||
|
Required
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Fido2UserVerificationPreferenceExtensions
|
||||||
|
{
|
||||||
|
public static Fido2UserVerificationPreference ToFido2UserVerificationPreference(string? preference)
|
||||||
|
{
|
||||||
|
switch (preference)
|
||||||
|
{
|
||||||
|
case "required":
|
||||||
|
return Fido2UserVerificationPreference.Required;
|
||||||
|
case "discouraged":
|
||||||
|
return Fido2UserVerificationPreference.Discouraged;
|
||||||
|
default:
|
||||||
|
return Fido2UserVerificationPreference.Preferred;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Fido2UserVerificationPreference GetUserVerificationPreferenceFrom(Fido2UserVerificationPreference preference, CipherRepromptType repromptType)
|
||||||
|
{
|
||||||
|
if (repromptType != CipherRepromptType.None)
|
||||||
|
{
|
||||||
|
return Fido2UserVerificationPreference.Required;
|
||||||
|
}
|
||||||
|
|
||||||
|
return preference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
@@ -30,6 +27,7 @@ namespace Bit.Core.Utilities
|
|||||||
var messagingService = Resolve<IMessagingService>("messagingService");
|
var messagingService = Resolve<IMessagingService>("messagingService");
|
||||||
var cryptoFunctionService = Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
var cryptoFunctionService = Resolve<ICryptoFunctionService>("cryptoFunctionService");
|
||||||
var cryptoService = Resolve<ICryptoService>("cryptoService");
|
var cryptoService = Resolve<ICryptoService>("cryptoService");
|
||||||
|
var clipboardService = Resolve<IClipboardService>();
|
||||||
var logger = Resolve<ILogger>();
|
var logger = Resolve<ILogger>();
|
||||||
|
|
||||||
SearchService searchService = null;
|
SearchService searchService = null;
|
||||||
@@ -46,8 +44,9 @@ namespace Bit.Core.Utilities
|
|||||||
var settingsService = new SettingsService(stateService);
|
var settingsService = new SettingsService(stateService);
|
||||||
var fileUploadService = new FileUploadService(apiService);
|
var fileUploadService = new FileUploadService(apiService);
|
||||||
var configService = new ConfigService(apiService, stateService, logger);
|
var configService = new ConfigService(apiService, stateService, logger);
|
||||||
|
var totpService = new TotpService(cryptoFunctionService);
|
||||||
var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService,
|
var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService,
|
||||||
fileUploadService, storageService, i18nService, () => searchService, configService, clearCipherCacheKey,
|
fileUploadService, storageService, i18nService, () => searchService, configService, totpService, clipboardService, clearCipherCacheKey,
|
||||||
allClearCipherCacheKeys);
|
allClearCipherCacheKeys);
|
||||||
var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService);
|
var folderService = new FolderService(cryptoService, stateService, apiService, i18nService, cipherService);
|
||||||
var collectionService = new CollectionService(cryptoService, stateService, i18nService);
|
var collectionService = new CollectionService(cryptoService, stateService, i18nService);
|
||||||
@@ -79,7 +78,6 @@ namespace Bit.Core.Utilities
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService);
|
var passwordGenerationService = new PasswordGenerationService(cryptoService, stateService, cryptoFunctionService, policyService);
|
||||||
var totpService = new TotpService(cryptoFunctionService);
|
|
||||||
var deviceTrustCryptoService = new DeviceTrustCryptoService(apiService, appIdService, cryptoFunctionService, cryptoService, stateService);
|
var deviceTrustCryptoService = new DeviceTrustCryptoService(apiService, appIdService, cryptoFunctionService, cryptoService, stateService);
|
||||||
var passwordResetEnrollmentService = new PasswordResetEnrollmentService(apiService, cryptoService, organizationService, stateService);
|
var passwordResetEnrollmentService = new PasswordResetEnrollmentService(apiService, cryptoService, organizationService, stateService);
|
||||||
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService,
|
var authService = new AuthService(cryptoService, cryptoFunctionService, apiService, stateService,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.Globalization;
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Bit.Core.Utilities
|
namespace Bit.Core.Utilities
|
||||||
@@ -30,5 +29,7 @@ namespace Bit.Core.Utilities
|
|||||||
.ToString()
|
.ToString()
|
||||||
.Normalize(NormalizationForm.FormC);
|
.Normalize(NormalizationForm.FormC);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string FallbackOnNullOrWhiteSpace(this string s, string fallback) => string.IsNullOrWhiteSpace(s) ? fallback : s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,11 @@ namespace Bit.App.Utilities
|
|||||||
// Currently on iOS when resuming the app after showing a System "Share/Sheet" (or other similar UI)
|
// Currently on iOS when resuming the app after showing a System "Share/Sheet" (or other similar UI)
|
||||||
// MAUI reports the incorrect Theme. To avoid this we are fetching the current OS Theme directly on iOS from the iOS API.
|
// MAUI reports the incorrect Theme. To avoid this we are fetching the current OS Theme directly on iOS from the iOS API.
|
||||||
// MAUI Issue: https://github.com/dotnet/maui/issues/19614
|
// MAUI Issue: https://github.com/dotnet/maui/issues/19614
|
||||||
|
#if IOS
|
||||||
|
public static bool OsDarkModeEnabled(UITraitCollection? traitCollection = null)
|
||||||
|
#else
|
||||||
public static bool OsDarkModeEnabled()
|
public static bool OsDarkModeEnabled()
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
#if UT
|
#if UT
|
||||||
return false;
|
return false;
|
||||||
@@ -167,8 +171,19 @@ namespace Bit.App.Utilities
|
|||||||
if (!OperatingSystem.IsIOSVersionAtLeast(13, 0))
|
if (!OperatingSystem.IsIOSVersionAtLeast(13, 0))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var traits = InvokeOnMainThread(() => WindowStateManager.Default.GetCurrentUIViewController()?.TraitCollection) ?? UITraitCollection.CurrentTraitCollection;
|
ClipLogger.Log($"TC, UIStyle: {traitCollection?.UserInterfaceStyle}");
|
||||||
var uiStyle = traits.UserInterfaceStyle;
|
var uiStyle = traitCollection?.UserInterfaceStyle;
|
||||||
|
if (traitCollection is null)
|
||||||
|
{
|
||||||
|
ClipLogger.Log($"TC null getting trait collection from wsm");
|
||||||
|
var traits = InvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
return WindowStateManager.Default.GetCurrentUIViewController()?.TraitCollection;
|
||||||
|
}) ?? UITraitCollection.CurrentTraitCollection;
|
||||||
|
uiStyle = traits.UserInterfaceStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipLogger.Log($"UIStyle: {uiStyle}");
|
||||||
|
|
||||||
requestedTheme = uiStyle switch
|
requestedTheme = uiStyle switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user