mirror of
https://github.com/bitwarden/mobile
synced 2026-01-16 07:23:29 +00:00
[PM-5731] Create C# WebAuthn authenticator to support maui apps (#2951)
* [PM-5731] feat: implement get assertion params object * [PM-5731] feat: add first test * [PM-5731] feat: add rp mismatch test * [PM-5731] feat: ask for credentials when found * [PM-5731] feat: find discoverable credentials * [PM-5731] feat: add tests for successful UV requests * [PM-5731] feat: add user does not consent test * [PM-5731] feat: check for UV when reprompt is active * [PM-5731] fix: tests a bit, needed some additional "arrange" steps * [PM-5731] feat: add support for counter * [PM-5731] feat: implement assertion without signature * [PM-5732] feat: finish authenticator assertion implementation note: CryptoFunctionService still needs Sign implemenation * [PM-5731] chore: minor clean up * [PM-5731] feat: scaffold make credential * [PM-5731] feat: start implementing attestation * [PM-5731] feat: implement credential exclusion * [PM-5731] feat: add new credential confirmaiton * [PM-5731] feat: implement credential creation * [PM-5731] feat: add user verification checks * [PM-5731] feat: add unknown error handling * [PM-5731] chore: clean up unusued params * [PM-5731] feat: partial attestation implementation * [PM-5731] feat: implement key generation * [PM-5731] feat: return public key in DER format * [PM-5731] feat: implement signing * [PM-5731] feat: remove logging * [PM-5731] chore: use primary constructor * [PM-5731] chore: add Async to method names * [PM-5731] feat: add support for silent discoverability * [PM-5731] feat: add support for specifying user presence requirement * [PM-5731] feat: ensure unlocked vault * [PM-5731] chore: clean up and refactor assertion tests * [PM-5731] chore: clean up and refactor attestation tests * [PM-5731] chore: add user presence todo comment * [PM-5731] feat: scaffold fido2 client * PM-5731 Fix build updating discoverable flag * [PM-5731] fix: failing test * [PM-5731] feat: add sameOriginWithAncestor and user id length checks * [PM-5731] feat: add incomplete rpId verification * [PM-5731] chore: document uri helpers * [PM-5731] feat: implement fido2 client createCredential * [PM-5731] feat: implement credential assertion in client * fix wrong signature format (cherry picked from commita1c9ebf01f) * [PM-5731] fix: issues after cherry-pick * Fix incompatible GUID conversions (cherry picked from commitc801b2fc3a) * [PM-5731] chore: remove default constructor * [PM-5731] feat: refactor user interface to increase flexibility * [PM-5731] feat: implement generic assertion user interface class * [PM-5731] feat: remove ability to make user presence optional * [PM-5731] chore: remove logging comments * [PM-5731] feat: add native reprompt support to the authenticator * [PM-5731] feat: allow pre and post UV * [PM-5731] chore: add `Async` to method name. Remove `I` from struct * [PM-5731] fix: discoverable string repr lowercase * [PM-5731] chore: don't use C# 12 features * [PM-5731] fix: replace magic strings and numbers with contants and enums * [PM-5731] fix: use UTC creation date * [PM-5731] fix: formatting * [PM-5731] chore: use properties for public fields * [PM-5731] chore: remove TODO * [PM-5731] fix: IsValidRpId --------- Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: mpbw2 <59324545+mpbw2@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
18
src/Core/Abstractions/IFido2GetAssertionUserInterface.cs
Normal file
18
src/Core/Abstractions/IFido2GetAssertionUserInterface.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public struct Fido2GetAssertionUserInterfaceCredential
|
||||
{
|
||||
public string CipherId { get; set; }
|
||||
public bool RequireUserVerification { 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);
|
||||
}
|
||||
}
|
||||
37
src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs
Normal file
37
src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
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>
|
||||
/// Whether or not the user must be verified before completing the operation.
|
||||
/// </summary>
|
||||
public bool UserVerification { 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);
|
||||
}
|
||||
}
|
||||
12
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
12
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IFido2UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Make sure that the vault is unlocked.
|
||||
/// This should open a window and ask the user to login or unlock the vault if necessary.
|
||||
/// </summary>
|
||||
/// <returns>When vault has been unlocked.</returns>
|
||||
Task EnsureUnlockedVaultAsync();
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||
<PackageReference Include="PCLCrypto" Version="2.1.40-alpha" />
|
||||
<PackageReference Include="System.Formats.Cbor" Version="8.0.0" />
|
||||
<PackageReference Include="zxcvbn-core" Version="7.0.92" />
|
||||
<PackageReference Include="MessagePack.MSBuild.Tasks" Version="2.5.124">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.View
|
||||
{
|
||||
@@ -27,9 +28,28 @@ namespace Bit.Core.Models.View
|
||||
public string Counter { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
|
||||
public int CounterValue {
|
||||
get => int.TryParse(Counter, out var counter) ? counter : 0;
|
||||
set => Counter = value.ToString();
|
||||
}
|
||||
|
||||
public byte[] UserHandleValue {
|
||||
get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle);
|
||||
set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
||||
}
|
||||
|
||||
public byte[] KeyBytes {
|
||||
get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue);
|
||||
set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value);
|
||||
}
|
||||
|
||||
public bool DiscoverableValue {
|
||||
get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
|
||||
set => Discoverable = value.ToString().ToLower();
|
||||
}
|
||||
|
||||
public override string SubTitle => UserName;
|
||||
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
|
||||
public bool IsDiscoverable => !string.IsNullOrWhiteSpace(Discoverable);
|
||||
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
|
||||
public string LaunchUri => $"https://{RpId}";
|
||||
|
||||
|
||||
456
src/Core/Services/Fido2AuthenticatorService.cs
Normal file
456
src/Core/Services/Fido2AuthenticatorService.cs
Normal file
@@ -0,0 +1,456 @@
|
||||
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;
|
||||
|
||||
public Fido2AuthenticatorService(ICipherService cipherService, ISyncService syncService, ICryptoFunctionService cryptoFunctionService)
|
||||
{
|
||||
_cipherService = cipherService;
|
||||
_syncService = syncService;
|
||||
_cryptoFunctionService = cryptoFunctionService;
|
||||
}
|
||||
|
||||
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,
|
||||
UserVerification = makeCredentialParams.RequireUserVerification
|
||||
});
|
||||
|
||||
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 && (makeCredentialParams.RequireUserVerification || cipher.Reprompt != CipherRepromptType.None)) {
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
cipher.Login.Fido2Credentials = [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) {
|
||||
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,
|
||||
RequireUserVerification = assertionParams.RequireUserVerification || cipher.Reprompt != CipherRepromptType.None
|
||||
}).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 && (assertionParams.RequireUserVerification || selectedCipher.Reprompt != CipherRepromptType.None)) {
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
try {
|
||||
var selectedFido2Credential = selectedCipher.Login.MainFido2Credential;
|
||||
var selectedCredentialId = selectedFido2Credential.CredentialId;
|
||||
|
||||
if (selectedFido2Credential.CounterValue != 0) {
|
||||
++selectedFido2Credential.CounterValue;
|
||||
}
|
||||
|
||||
await _cipherService.UpdateLastUsedDateAsync(selectedCipher.Id);
|
||||
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
|
||||
},
|
||||
AuthenticatorData = authenticatorData,
|
||||
Signature = signature
|
||||
};
|
||||
} catch (Exception) {
|
||||
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([
|
||||
(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
252
src/Core/Services/Fido2ClientService.cs
Normal file
252
src/Core/Services/Fido2ClientService.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
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.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 { 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" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side
|
||||
};
|
||||
} catch (InvalidStateError) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
||||
} catch (Exception) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams)
|
||||
{
|
||||
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin);
|
||||
if (blockedUris.Contains(domain))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.UriBlockedError,
|
||||
"Origin is blocked by the user");
|
||||
}
|
||||
|
||||
if (!await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.InvalidStateError,
|
||||
"No user is logged in");
|
||||
}
|
||||
|
||||
if (assertCredentialParams.Origin == _environmentService.GetWebVaultUrl())
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.NotAllowedError,
|
||||
"Saving Bitwarden credentials in a Bitwarden vault is not allowed");
|
||||
}
|
||||
|
||||
if (!assertCredentialParams.Origin.StartsWith("https://"))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.SecurityError,
|
||||
"Origin is not a valid https origin");
|
||||
}
|
||||
|
||||
if (!Fido2DomainUtils.IsValidRpId(assertCredentialParams.RpId, assertCredentialParams.Origin))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.SecurityError,
|
||||
"RP ID cannot be used with this origin");
|
||||
}
|
||||
|
||||
var clientDataJSON = JsonSerializer.Serialize(new {
|
||||
type = "webauthn.get",
|
||||
challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge),
|
||||
origin = assertCredentialParams.Origin,
|
||||
crossOrigin = !assertCredentialParams.SameOriginWithAncestors,
|
||||
});
|
||||
var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON);
|
||||
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
|
||||
var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);
|
||||
|
||||
try {
|
||||
var getAssertionResult = await _fido2AuthenticatorService.GetAssertionAsync(getAssertionParams, _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
|
||||
};
|
||||
} catch (InvalidStateError) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
||||
} catch (Exception) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private Fido2AuthenticatorMakeCredentialParams MapToMakeCredentialParams(
|
||||
Fido2ClientCreateCredentialParams createCredentialParams,
|
||||
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs,
|
||||
byte[] clientDataHash)
|
||||
{
|
||||
var requireResidentKey = createCredentialParams.AuthenticatorSelection?.ResidentKey == "required" ||
|
||||
createCredentialParams.AuthenticatorSelection?.ResidentKey == "preferred" ||
|
||||
(createCredentialParams.AuthenticatorSelection?.ResidentKey == null &&
|
||||
createCredentialParams.AuthenticatorSelection?.RequireResidentKey == true);
|
||||
|
||||
var requireUserVerification = createCredentialParams.AuthenticatorSelection?.UserVerification == "required" ||
|
||||
createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" ||
|
||||
createCredentialParams.AuthenticatorSelection?.UserVerification == null;
|
||||
|
||||
return new Fido2AuthenticatorMakeCredentialParams {
|
||||
RequireResidentKey = requireResidentKey,
|
||||
RequireUserVerification = requireUserVerification,
|
||||
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
|
||||
CredTypesAndPubKeyAlgs = credTypesAndPubKeyAlgs,
|
||||
Hash = clientDataHash,
|
||||
RpEntity = createCredentialParams.Rp,
|
||||
UserEntity = createCredentialParams.User,
|
||||
Extensions = createCredentialParams.Extensions
|
||||
};
|
||||
}
|
||||
|
||||
private Fido2AuthenticatorGetAssertionParams MapToGetAssertionParams(
|
||||
Fido2ClientAssertCredentialParams assertCredentialParams,
|
||||
byte[] cliendDataHash)
|
||||
{
|
||||
var requireUserVerification = assertCredentialParams.UserVerification == "required" ||
|
||||
assertCredentialParams.UserVerification == "preferred" ||
|
||||
assertCredentialParams.UserVerification == null;
|
||||
|
||||
return new Fido2AuthenticatorGetAssertionParams {
|
||||
RpId = assertCredentialParams.RpId,
|
||||
Challenge = assertCredentialParams.Challenge,
|
||||
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,
|
||||
RequireUserVerification = requireUserVerification,
|
||||
Hash = cliendDataHash
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using PCLCrypto;
|
||||
using static PCLCrypto.WinRTCrypto;
|
||||
|
||||
|
||||
@@ -38,12 +38,38 @@ namespace Bit.Core.Utilities
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the host (and not port) of the given uri.
|
||||
/// Does not support plain hostnames without a protocol.
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => login.bitwarden.com</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => sub.login.bitwarden.com</para>
|
||||
/// <para>https://localhost:8080 => localhost</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetHostname(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
return string.IsNullOrEmpty(uri?.Host) ? null : uri.Host;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the host and port of the given uri.
|
||||
/// Does not support plain hostnames without
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => login.bitwarden.com:1337</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => sub.login.bitwarden.com:1337</para>
|
||||
/// <para>https://localhost:8080 => localhost:8080</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetHost(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
@@ -61,6 +87,19 @@ namespace Bit.Core.Utilities
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the second and top level domain of the given uri.
|
||||
/// Does not support plain hostnames without
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
/// <para>https://login.bitwarden.com:1337 => bitwarden.com</para>
|
||||
/// <para>https://sub.login.bitwarden.com:1337 => bitwarden.com</para>
|
||||
/// <para>https://localhost:8080 => localhost</para>
|
||||
/// <para>localhost => null</para>
|
||||
/// <para>bitwarden => null</para>
|
||||
/// <para>127.0.0.1 => 127.0.0.1</para>
|
||||
/// </summary>
|
||||
public static string GetDomain(string uriString)
|
||||
{
|
||||
var uri = GetUri(uriString);
|
||||
|
||||
18
src/Core/Utilities/Fido2/AuthenticatorSelectionCriteria.cs
Normal file
18
src/Core/Utilities/Fido2/AuthenticatorSelectionCriteria.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements of the authenticator used in the creation of the credential.
|
||||
/// </summary>
|
||||
public class AuthenticatorSelectionCriteria
|
||||
{
|
||||
public bool? RequireResidentKey { get; set; }
|
||||
public string? ResidentKey { get; set; }
|
||||
public string UserVerification { get; set; } = "preferred";
|
||||
|
||||
/// <summary>
|
||||
/// This member is intended for use by Relying Parties that wish to select the appropriate authenticators to participate in the create() operation.
|
||||
/// </summary>
|
||||
// public AuthenticatorAttachment? AuthenticatorAttachment { get; set; } // not used
|
||||
}
|
||||
}
|
||||
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")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,25 @@
|
||||
{
|
||||
public class Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
/** The caller’s RP ID, as determined by the user agent and the client. */
|
||||
public string RpId { get; set; }
|
||||
|
||||
public string CredentialId { get; set; }
|
||||
/** The hash of the serialized client data, provided by the client. */
|
||||
public byte[] Hash { get; set; }
|
||||
|
||||
public string Counter { get; set; }
|
||||
public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the authenticator to require a user-verifying gesture in order to complete the request. Examples of such gestures are fingerprint scan or a PIN.
|
||||
/// </summary>
|
||||
public bool RequireUserVerification { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The challenge to be signed by the authenticator.
|
||||
/// </summary>
|
||||
public byte[] Challenge { get; set; }
|
||||
|
||||
public object Extensions { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
using System;
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorMakeCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The Relying Party's PublicKeyCredentialRpEntity.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialRpEntity RpEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's PublicKeyCredentialRpEntity.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialUserEntity UserEntity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The hash of the serialized client data, provided by the client.
|
||||
/// </summary>
|
||||
public byte[] Hash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A sequence of pairs of PublicKeyCredentialType and public key algorithms (COSEAlgorithmIdentifier) requested by the Relying Party. This sequence is ordered from most preferred to least preferred. The authenticator makes a best-effort to create the most preferred credential that it can.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialParameters[] CredTypesAndPubKeyAlgs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[] ExcludeCredentialDescriptorList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The effective resident key requirement for credential creation, a Boolean value determined by the client. Resident is synonymous with discoverable. */
|
||||
/// </summary>
|
||||
public bool RequireResidentKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The effective user verification requirement for assertion, a Boolean value provided by the client.
|
||||
/// </summary>
|
||||
public bool RequireUserVerification { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CTAP2 authenticators support setting this to false, but we only support the WebAuthn authenticator model which does not have that option.
|
||||
/// </summary>
|
||||
// public bool RequireUserPresence { get; set; } // Always required
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator's attestation preference, a string provided by the client. This is a hint that the client gives to the authenticator about what kind of attestation statement it would like. The authenticator makes a best-effort to satisfy the preference.
|
||||
/// Note: Attestation statements are not supported at this time.
|
||||
/// </summary>
|
||||
// public string AttestationPreference { get; set; }
|
||||
|
||||
public object Extensions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2AuthenticatorMakeCredentialResult
|
||||
{
|
||||
public byte[] CredentialId { get; set; }
|
||||
|
||||
public byte[] AttestationObject { get; set; }
|
||||
|
||||
public byte[] AuthData { get; set; }
|
||||
|
||||
public byte[] PublicKey { get; set; }
|
||||
|
||||
public int PublicKeyAlgorithm { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for asserting a credential.
|
||||
///
|
||||
/// This class is an extended version of the WebAuthn struct:
|
||||
/// https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialrequestoptions
|
||||
/// </summary>
|
||||
public class Fido2ClientAssertCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// A value which is true if and only if the caller’s environment settings object is same-origin with its ancestors.
|
||||
/// It is false if caller is cross-origin.
|
||||
/// </summary>
|
||||
public bool SameOriginWithAncestors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The challenge that the selected authenticator signs, along with other data, when producing an authentication
|
||||
/// assertion.
|
||||
/// </summary>
|
||||
public required byte[] Challenge { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The relying party identifier claimed by the caller. If omitted, its value will be the CredentialsContainer
|
||||
/// object's relevant settings object's origin's effective domain.
|
||||
/// </summary>
|
||||
public string RpId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's origin (e.g., "https://example.com").
|
||||
/// </summary>
|
||||
public string Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of PublicKeyCredentialDescriptor objects representing public key credentials acceptable to the caller,
|
||||
/// in descending order of the caller’s preference (the first item in the list is the most preferred credential,
|
||||
/// and so on down the list).
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[] AllowCredentials { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements regarding user verification for the get() operation.
|
||||
/// </summary>
|
||||
public string UserVerification { get; set; } = "preferred";
|
||||
|
||||
/// <summary>
|
||||
/// This time, in milliseconds, that the caller is willing to wait for the call to complete.
|
||||
/// This is treated as a hint, and MAY be overridden by the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not currently supported.
|
||||
/// </remarks>
|
||||
public int? Timeout { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of asserting a credential.
|
||||
///
|
||||
/// See: https://www.w3.org/TR/webauthn-2/#publickeycredential
|
||||
/// </summary>
|
||||
public class Fido2ClientAssertCredentialResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Base64url encoding of the credential identifer.
|
||||
/// </summary>
|
||||
public required string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The credential identifier.
|
||||
/// </summary>
|
||||
public required byte[] RawId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The JSON-compatible serialization of client datapassed to the authenticator by the client in
|
||||
/// order to generate this assertion.
|
||||
/// </summary>
|
||||
public required byte[] ClientDataJSON { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator data returned by the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] AuthenticatorData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw signature returned from the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user handle returned from the authenticator, or null if the authenticator did not
|
||||
/// return a user handle.
|
||||
/// </summary>
|
||||
public byte[]? UserHandle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents an authenticator's response to a client's request for generation of a
|
||||
/// new authentication assertion given the WebAuthn Relying Party's challenge.
|
||||
/// This response contains a cryptographic signature proving possession of the credential private key,
|
||||
/// and optionally evidence of user consent to a specific transaction.
|
||||
///
|
||||
/// See: https://www.w3.org/TR/webauthn-2/#iface-authenticatorassertionresponse
|
||||
/// </summary>
|
||||
public class Fido2ClientAuthenticatorAssertionResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The JSON-compatible serialization of client data passed to the authenticator by the client
|
||||
/// in order to generate this assertion. The exact JSON serialization MUST be preserved, as the
|
||||
/// hash of the serialized client data has been computed over it.
|
||||
/// </summary>
|
||||
public required byte[] ClientDataJSON { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The authenticator data returned by the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] AuthenticatorData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raw signature returned from the authenticator.
|
||||
/// </summary>
|
||||
public required byte[] Signature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user handle returned from the authenticator, or null if the authenticator did not return a user handle.
|
||||
/// </summary>
|
||||
public byte[] UserHandle { get; set; } = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Parameters for creating a new credential.
|
||||
/// </summary>
|
||||
public class Fido2ClientCreateCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The Relaying Parties origin, see: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin
|
||||
/// </summary>
|
||||
public required string Origin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A value which is true if and only if the caller’s environment settings object is same-origin with its ancestors.
|
||||
/// It is false if caller is cross-origin.
|
||||
/// </summary>
|
||||
public bool SameOriginWithAncestors { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's preference for attestation conveyance
|
||||
/// </summary>
|
||||
public string? Attestation { get; set; } = "none";
|
||||
|
||||
/// <summary>
|
||||
/// The Relying Party's requirements of the authenticator used in the creation of the credential.
|
||||
/// </summary>
|
||||
public AuthenticatorSelectionCriteria? AuthenticatorSelection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Challenge intended to be used for generating the newly created credential's attestation object.
|
||||
/// </summary>
|
||||
public required byte[] Challenge { get; set; } // base64url encoded
|
||||
|
||||
/// <summary>
|
||||
/// This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for
|
||||
/// the same account on a single authenticator. The client is requested to return an error if the new credential would
|
||||
/// be created on an authenticator that also contains one of the credentials enumerated in this parameter.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialDescriptor[]? ExcludeCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member contains additional parameters requesting additional processing by the client and authenticator.
|
||||
/// Not currently supported.
|
||||
/// </summary>
|
||||
public object? Extensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member contains information about the desired properties of the credential to be created.
|
||||
/// The sequence is ordered from most preferred to least preferred.
|
||||
/// The client makes a best-effort to create the most preferred credential that it can.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialParameters[] PubKeyCredParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data about the Relying Party responsible for the request.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialRpEntity Rp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data about the user account for which the Relying Party is requesting attestation.
|
||||
/// </summary>
|
||||
public required PublicKeyCredentialUserEntity User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member specifies a time, in milliseconds, that the caller is willing to wait for the call to complete.
|
||||
/// This is treated as a hint, and MAY be overridden by the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not currently supported.
|
||||
/// </remarks>
|
||||
public int? Timeout { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of creating a new credential.
|
||||
///
|
||||
/// This class is an extended version of the WebAuthn struct:
|
||||
/// https://www.w3.org/TR/webauthn-3/#credentialcreationdata-attestationobjectresult
|
||||
/// </summary>
|
||||
public class Fido2ClientCreateCredentialResult
|
||||
{
|
||||
public byte[] CredentialId { get; set; }
|
||||
public byte[] ClientDataJSON { get; set; }
|
||||
public byte[] AttestationObject { get; set; }
|
||||
public byte[] AuthData { get; set; }
|
||||
public byte[] PublicKey { get; set; }
|
||||
public int PublicKeyAlgorithm { get; set; }
|
||||
public string[] Transports { get; set; }
|
||||
}
|
||||
}
|
||||
25
src/Core/Utilities/Fido2/Fido2ClientException.cs
Normal file
25
src/Core/Utilities/Fido2/Fido2ClientException.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public class Fido2ClientException : Exception
|
||||
{
|
||||
public enum ErrorCode
|
||||
{
|
||||
NotAllowedError,
|
||||
TypeError,
|
||||
SecurityError,
|
||||
UriBlockedError,
|
||||
NotSupportedError,
|
||||
InvalidStateError,
|
||||
UnknownError
|
||||
}
|
||||
|
||||
public 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 originWithoutProtocolOrPort = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?", "$2$4");
|
||||
if (Uri.CheckHostName(rpId) != UriHostNameType.Dns || Uri.CheckHostName(originWithoutProtocolOrPort) != UriHostNameType.Dns)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rpId == originWithoutProtocolOrPort)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolOrPort, out var parsedOrgin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return parsedOrgin.Tld == parsedRpId.Tld &&
|
||||
parsedOrgin.Domain == parsedRpId.Domain &&
|
||||
(parsedOrgin.SubDomain == parsedRpId.SubDomain || parsedOrgin.SubDomain.EndsWith(parsedRpId.SubDomain));
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/Core/Utilities/Fido2/Fido2GetAssertionUserInterface.cs
Normal file
52
src/Core/Utilities/Fido2/Fido2GetAssertionUserInterface.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
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<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<Task<bool>> verifyUserCallback)
|
||||
{
|
||||
_cipherId = cipherId;
|
||||
_userVerified = userVerified;
|
||||
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
|
||||
_verifyUserCallback = verifyUserCallback;
|
||||
}
|
||||
|
||||
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;
|
||||
if (credential.RequireUserVerification && !verified)
|
||||
{
|
||||
verified = await _verifyUserCallback();
|
||||
}
|
||||
|
||||
return (CipherId: _cipherId, UserVerified: verified);
|
||||
}
|
||||
|
||||
public Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
return _ensureUnlockedVaultCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user