mirror of
https://github.com/bitwarden/mobile
synced 2025-12-10 05:13:31 +00:00
[PM-5731] feat: implement assertion without signature
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
@@ -32,6 +33,11 @@ namespace Bit.Core.Models.View
|
|||||||
set => Counter = value.ToString();
|
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 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 => bool.TryParse(Discoverable, out var isDiscoverable) && isDiscoverable;
|
public bool IsDiscoverable => bool.TryParse(Discoverable, out var isDiscoverable) && isDiscoverable;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
{
|
{
|
||||||
@@ -10,13 +11,15 @@ namespace Bit.Core.Services
|
|||||||
private INativeLogService _logService;
|
private INativeLogService _logService;
|
||||||
private ICipherService _cipherService;
|
private ICipherService _cipherService;
|
||||||
private ISyncService _syncService;
|
private ISyncService _syncService;
|
||||||
|
private ICryptoFunctionService _cryptoFunctionService;
|
||||||
private IFido2UserInterface _userInterface;
|
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;
|
_logService = logService;
|
||||||
_cipherService = cipherService;
|
_cipherService = cipherService;
|
||||||
_syncService = syncService;
|
_syncService = syncService;
|
||||||
|
_cryptoFunctionService = cryptoFunctionService;
|
||||||
_userInterface = userInterface;
|
_userInterface = userInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +80,33 @@ namespace Bit.Core.Services
|
|||||||
++selectedFido2Credential.CounterValue;
|
++selectedFido2Credential.CounterValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _cipherService.UpdateLastUsedDateAsync(selectedCipher.Id);
|
||||||
var encrypted = await _cipherService.EncryptAsync(selectedCipher);
|
var encrypted = await _cipherService.EncryptAsync(selectedCipher);
|
||||||
await _cipherService.SaveWithServerAsync(encrypted);
|
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 {
|
} catch {
|
||||||
_logService.Info(
|
_logService.Info(
|
||||||
"[Fido2Authenticator] Aborting because no matching credentials were found in the vault."
|
"[Fido2Authenticator] Aborting because no matching credentials were found in the vault."
|
||||||
@@ -88,13 +114,6 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
throw new UnknownError();
|
throw new UnknownError();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: IMPLEMENT this
|
|
||||||
return new Fido2AuthenticatorGetAssertionResult
|
|
||||||
{
|
|
||||||
AuthenticatorData = new byte[32],
|
|
||||||
Signature = new byte[8]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<CipherView>> FindCredentialsById(PublicKeyCredentialDescriptor[] credentials, string rpId)
|
private async Task<List<CipherView>> FindCredentialsById(PublicKeyCredentialDescriptor[] credentials, string rpId)
|
||||||
@@ -137,9 +156,63 @@ namespace Bit.Core.Services
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<byte[]> GenerateAuthData(
|
||||||
|
string rpId,
|
||||||
|
bool userVerification,
|
||||||
|
bool userPresence,
|
||||||
|
int counter
|
||||||
|
// byte[] credentialId,
|
||||||
|
// CryptoKey? cryptoKey - only needed for attestation
|
||||||
|
) {
|
||||||
|
List<byte> authData = new List<byte>();
|
||||||
|
|
||||||
|
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)
|
private string GuidToStandardFormat(byte[] bytes)
|
||||||
{
|
{
|
||||||
return new Guid(bytes).ToString();
|
return new Guid(bytes).ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] GuidToRawFormat(string guid)
|
||||||
|
{
|
||||||
|
return Guid.Parse(guid).ToByteArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
using System;
|
namespace Bit.Core.Utilities.Fido2
|
||||||
namespace Bit.Core.Utilities.Fido2
|
|
||||||
{
|
{
|
||||||
public class Fido2AuthenticatorGetAssertionResult
|
public class Fido2AuthenticatorGetAssertionResult
|
||||||
{
|
{
|
||||||
public byte[] AuthenticatorData { get; set; }
|
public byte[] AuthenticatorData { get; set; }
|
||||||
|
|
||||||
public byte[] Signature { 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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<Fido2AuthenticatorService> 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<ICipherService>().GetAllDecryptedAsync().Returns(new List<CipherView> { cipherView });
|
||||||
|
sutProvider.GetDependency<IFido2UserInterface>().PickCredentialAsync(Arg.Any<Fido2PickCredentialParams>()).Returns(new Fido2PickCredentialResult {
|
||||||
|
CipherId = cipherView.Id,
|
||||||
|
UserVerified = true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
var rpIdHashMock = RandomBytes(32);
|
||||||
|
sutProvider.GetDependency<ICryptoFunctionService>().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]
|
[Theory]
|
||||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
|
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
|
||||||
// Spec: Increment the credential associated signature counter
|
// Spec: Increment the credential associated signature counter
|
||||||
@@ -331,7 +367,8 @@ namespace Bit.Core.Test.Services
|
|||||||
new Fido2CredentialView {
|
new Fido2CredentialView {
|
||||||
CredentialId = credentialId ?? Guid.NewGuid().ToString(),
|
CredentialId = credentialId ?? Guid.NewGuid().ToString(),
|
||||||
RpId = rpId ?? "bitwarden.com",
|
RpId = rpId ?? "bitwarden.com",
|
||||||
Discoverable = discoverable.HasValue ? discoverable.ToString() : "true"
|
Discoverable = discoverable.HasValue ? discoverable.ToString() : "true",
|
||||||
|
UserHandleValue = RandomBytes(32),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user