1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-10 05:13:31 +00:00
Files
mobile/src/Core/Services/Fido2AuthenticatorService.cs
2024-01-22 16:08:15 +01:00

219 lines
7.6 KiB
C#

using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Enums;
using Bit.Core.Utilities.Fido2;
using System.Buffers.Binary;
namespace Bit.Core.Services
{
public class Fido2AuthenticatorService : IFido2AuthenticatorService
{
private INativeLogService _logService;
private ICipherService _cipherService;
private ISyncService _syncService;
private ICryptoFunctionService _cryptoFunctionService;
private IFido2UserInterface _userInterface;
public Fido2AuthenticatorService(INativeLogService logService, ICipherService cipherService, ISyncService syncService, ICryptoFunctionService cryptoFunctionService, IFido2UserInterface userInterface)
{
_logService = logService;
_cipherService = cipherService;
_syncService = syncService;
_cryptoFunctionService = cryptoFunctionService;
_userInterface = userInterface;
}
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
{
// throw new NotAllowedError();
List<CipherView> cipherOptions;
// await userInterfaceSession.ensureUnlockedVault();
await _syncService.FullSyncAsync(false);
if (assertionParams.AllowCredentialDescriptorList?.Length > 0) {
cipherOptions = await FindCredentialsById(
assertionParams.AllowCredentialDescriptorList,
assertionParams.RpId
);
} else {
cipherOptions = await FindCredentialsByRp(assertionParams.RpId);
}
if (cipherOptions.Count == 0) {
_logService.Info(
"[Fido2Authenticator] Aborting because no matching credentials were found in the vault."
);
throw new NotAllowedError();
}
var response = await _userInterface.PickCredentialAsync(new Fido2PickCredentialParams {
CipherIds = cipherOptions.Select((cipher) => cipher.Id).ToArray(),
UserVerification = assertionParams.RequireUserVerification
});
var selectedCipherId = response.CipherId;
var userVerified = response.UserVerified;
var selectedCipher = cipherOptions.FirstOrDefault((c) => c.Id == selectedCipherId);
if (selectedCipher == null) {
_logService.Info(
"[Fido2Authenticator] Aborting because the selected credential could not be found."
);
throw new NotAllowedError();
}
if (!userVerified && (assertionParams.RequireUserVerification || selectedCipher.Reprompt != CipherRepromptType.None)) {
_logService.Info(
"[Fido2Authenticator] Aborting because user verification was unsuccessful."
);
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 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."
);
throw new UnknownError();
}
}
private async Task<List<CipherView>> FindCredentialsById(PublicKeyCredentialDescriptor[] credentials, string rpId)
{
var ids = new List<string>();
foreach (var credential in credentials)
{
try
{
ids.Add(GuidToStandardFormat(credential.Id));
}
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>> FindCredentialsByRp(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.IsDiscoverable
);
}
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)
{
return new Guid(bytes).ToString();
}
private byte[] GuidToRawFormat(string guid)
{
return Guid.Parse(guid).ToByteArray();
}
}
}