diff --git a/src/Core/Models/View/Fido2CredentialView.cs b/src/Core/Models/View/Fido2CredentialView.cs index acb5dec4e..2cf74f380 100644 --- a/src/Core/Models/View/Fido2CredentialView.cs +++ b/src/Core/Models/View/Fido2CredentialView.cs @@ -1,5 +1,6 @@ using Bit.Core.Enums; using Bit.Core.Models.Domain; +using Bit.Core.Utilities; namespace Bit.Core.Models.View { @@ -32,6 +33,11 @@ namespace Bit.Core.Models.View set => Counter = value.ToString(); } + public byte[] UserHandleValue { + get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle); + set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value); + } + public override string SubTitle => UserName; public override List> LinkedFieldOptions => new List>(); public bool IsDiscoverable => bool.TryParse(Discoverable, out var isDiscoverable) && isDiscoverable; diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index 685bc176c..1816457d6 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -2,6 +2,7 @@ using Bit.Core.Models.View; using Bit.Core.Enums; using Bit.Core.Utilities.Fido2; +using System.Buffers.Binary; namespace Bit.Core.Services { @@ -10,13 +11,15 @@ namespace Bit.Core.Services private INativeLogService _logService; private ICipherService _cipherService; private ISyncService _syncService; + private ICryptoFunctionService _cryptoFunctionService; private IFido2UserInterface _userInterface; - public Fido2AuthenticatorService(INativeLogService logService, ICipherService cipherService, ISyncService syncService, IFido2UserInterface userInterface) + public Fido2AuthenticatorService(INativeLogService logService, ICipherService cipherService, ISyncService syncService, ICryptoFunctionService cryptoFunctionService, IFido2UserInterface userInterface) { _logService = logService; _cipherService = cipherService; _syncService = syncService; + _cryptoFunctionService = cryptoFunctionService; _userInterface = userInterface; } @@ -77,10 +80,33 @@ namespace Bit.Core.Services ++selectedFido2Credential.CounterValue; } + await _cipherService.UpdateLastUsedDateAsync(selectedCipher.Id); var encrypted = await _cipherService.EncryptAsync(selectedCipher); await _cipherService.SaveWithServerAsync(encrypted); - + var authenticatorData = await GenerateAuthData( + rpId: selectedFido2Credential.RpId, + userPresence: true, + userVerification: userVerified, + counter: selectedFido2Credential.CounterValue); + + // const signature = await generateSignature({ + // authData: authenticatorData, + // clientDataHash: params.hash, + // privateKey: await getPrivateKeyFromFido2Credential(selectedFido2Credential), + // }); + + // TODO: IMPLEMENT this + return new Fido2AuthenticatorGetAssertionResult + { + SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential + { + Id = GuidToRawFormat(selectedCredentialId), + UserHandle = selectedFido2Credential.UserHandleValue + }, + AuthenticatorData = authenticatorData, + Signature = new byte[8] + }; } catch { _logService.Info( "[Fido2Authenticator] Aborting because no matching credentials were found in the vault." @@ -88,13 +114,6 @@ namespace Bit.Core.Services throw new UnknownError(); } - - // TODO: IMPLEMENT this - return new Fido2AuthenticatorGetAssertionResult - { - AuthenticatorData = new byte[32], - Signature = new byte[8] - }; } private async Task> FindCredentialsById(PublicKeyCredentialDescriptor[] credentials, string rpId) @@ -137,9 +156,63 @@ namespace Bit.Core.Services ); } + private async Task GenerateAuthData( + string rpId, + bool userVerification, + bool userPresence, + int counter + // byte[] credentialId, + // CryptoKey? cryptoKey - only needed for attestation + ) { + List authData = new List(); + + var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256); + authData.AddRange(rpIdHash); + + var flags = AuthDataFlags(false, false, userVerification, userPresence); + authData.Add(flags); + + authData.AddRange([ + (byte)(counter >> 24), + (byte)(counter >> 16), + (byte)(counter >> 8), + (byte)counter + ]); + + return authData.ToArray(); + } + + private byte AuthDataFlags(bool extensionData, bool attestationData, bool userVerification, bool userPresence) { + byte flags = 0; + + if (extensionData) { + flags |= 0b1000000; + } + + if (attestationData) { + flags |= 0b01000000; + } + + if (userVerification) { + flags |= 0b00000100; + } + + if (userPresence) { + flags |= 0b00000001; + } + + return flags; + } + + private string GuidToStandardFormat(byte[] bytes) { return new Guid(bytes).ToString(); } + + private byte[] GuidToRawFormat(string guid) + { + return Guid.Parse(guid).ToByteArray(); + } } } diff --git a/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs b/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs index 845931143..70331029b 100644 --- a/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs +++ b/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs @@ -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; } } } diff --git a/test/Core.Test/Services/Fido2AuthenticatorTests.cs b/test/Core.Test/Services/Fido2AuthenticatorTests.cs index 1afafbf16..2f8ec44f8 100644 --- a/test/Core.Test/Services/Fido2AuthenticatorTests.cs +++ b/test/Core.Test/Services/Fido2AuthenticatorTests.cs @@ -288,6 +288,42 @@ namespace Bit.Core.Test.Services )); } + [Theory] + [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] + // Spec: Increment the credential associated signature counter + public async Task GetAssertionAsync_ReturnsAssertion(SutProvider sutProvider, Fido2AuthenticatorGetAssertionParams aParams, Cipher encryptedCipher) { + // Common Arrange + var cipherView = CreateCipherView(null, "bitwarden.com", true); + aParams.RpId = cipherView.Login.MainFido2Credential.RpId; + aParams.AllowCredentialDescriptorList = null; + sutProvider.GetDependency().GetAllDecryptedAsync().Returns(new List { cipherView }); + sutProvider.GetDependency().PickCredentialAsync(Arg.Any()).Returns(new Fido2PickCredentialResult { + CipherId = cipherView.Id, + UserVerified = true + }); + + // Arrange + var rpIdHashMock = RandomBytes(32); + sutProvider.GetDependency().HashAsync(aParams.RpId, CryptoHashAlgorithm.Sha256).Returns(rpIdHashMock); + cipherView.Login.MainFido2Credential.CounterValue = 9000; + + // Act + var result = await sutProvider.Sut.GetAssertionAsync(aParams); + + // Assert + var encAuthData = result.AuthenticatorData; + var rpIdHash = encAuthData.Take(32); + var flags = encAuthData.Skip(32).Take(1); + var counter = encAuthData.Skip(33).Take(4); + + Assert.Equal(result.SelectedCredential.Id, Guid.Parse(cipherView.Login.MainFido2Credential.CredentialId).ToByteArray()); + Assert.Equal(result.SelectedCredential.UserHandle, CoreHelpers.Base64UrlDecode(cipherView.Login.MainFido2Credential.UserHandle)); + Assert.Equal(rpIdHash, rpIdHashMock); + Assert.Equal(flags, new byte[] { 0b00000101 }); // UP = true, UV = true + Assert.Equal(counter, new byte[] { 0, 0, 0x23, 0x29 }); // 9001 in binary big-endian format + // TODO: Assert signature... + } + [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] // Spec: Increment the credential associated signature counter @@ -331,7 +367,8 @@ namespace Bit.Core.Test.Services new Fido2CredentialView { CredentialId = credentialId ?? Guid.NewGuid().ToString(), RpId = rpId ?? "bitwarden.com", - Discoverable = discoverable.HasValue ? discoverable.ToString() : "true" + Discoverable = discoverable.HasValue ? discoverable.ToString() : "true", + UserHandleValue = RandomBytes(32), } } }