PM-5154 [Passkeys iOS] Fix Credential ID handling on bytes and string formats. Fix Discoverable to be lowercase on set so it doesn't break parsing on clients. Added UserDisplayName on Fido2 entities. Extracted the Guid Standard/Raw format helpers to a extensions class.

This commit is contained in:
Federico Maccaroni 2024-02-16 19:30:33 -03:00
parent a1c9ebf01f
commit 41161864db
No known key found for this signature in database
GPG Key ID: 5D233F8F2B034536
10 changed files with 56 additions and 52 deletions

View File

@ -1,5 +1,4 @@
using System;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Domain;
namespace Bit.Core.Models.Api
{
@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api
RpName = fido2Key.RpName?.EncryptedString;
UserHandle = fido2Key.UserHandle?.EncryptedString;
UserName = fido2Key.UserName?.EncryptedString;
UserDisplayName = fido2Key.UserDisplayName?.EncryptedString;
Counter = fido2Key.Counter?.EncryptedString;
CreationDate = fido2Key.CreationDate;
}
@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
public string RpName { get; set; }
public string UserHandle { get; set; }
public string UserName { get; set; }
public string UserDisplayName { get; set; }
public string Counter { get; set; }
public DateTime CreationDate { get; set; }
}

View File

@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data
RpName = apiData.RpName;
UserHandle = apiData.UserHandle;
UserName = apiData.UserName;
UserDisplayName = apiData.UserDisplayName;
Counter = apiData.Counter;
CreationDate = apiData.CreationDate;
}
@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
public string RpName { get; set; }
public string UserHandle { get; set; }
public string UserName { get; set; }
public string UserDisplayName { get; set; }
public string Counter { get; set; }
public DateTime CreationDate { get; set; }
}

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data;
using Bit.Core.Models.View;
namespace Bit.Core.Models.Domain
@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain
nameof(RpName),
nameof(UserHandle),
nameof(UserName),
nameof(UserDisplayName),
nameof(Counter)
};
@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain
public EncString RpName { get; set; }
public EncString UserHandle { get; set; }
public EncString UserName { get; set; }
public EncString UserDisplayName { get; set; }
public EncString Counter { get; set; }
public DateTime CreationDate { get; set; }

View File

@ -26,6 +26,7 @@ namespace Bit.Core.Models.View
public string RpName { get; set; }
public string UserHandle { get; set; }
public string UserName { get; set; }
public string UserDisplayName { get; set; }
public string Counter { get; set; }
public DateTime CreationDate { get; set; }
@ -50,7 +51,7 @@ namespace Bit.Core.Models.View
[JsonIgnore]
public bool DiscoverableValue {
get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
set => Discoverable = value.ToString();
set => Discoverable = value.ToString().ToLower(); // must be lowercase so it can be parsed in the current version of clients
}
[JsonIgnore]

View File

@ -79,6 +79,8 @@ namespace Bit.Core.Services
var keyPair = GenerateKeyPair();
var fido2Credential = CreateCredentialView(makeCredentialParams, keyPair.privateKey);
ClipLogger.Log($"[Fido2Authenticator] IsDiscoverable {fido2Credential.Discoverable} - {fido2Credential.DiscoverableValue}");
var encrypted = await _cipherService.GetAsync(cipherId);
var cipher = await encrypted.DecryptAsync();
@ -95,18 +97,20 @@ namespace Bit.Core.Services
await _cipherService.SaveWithServerAsync(reencrypted);
credentialId = fido2Credential.CredentialId;
ClipLogger.Log($"[Fido2Authenticator] IsDiscoverable {cipher.Login.MainFido2Credential.Discoverable} - {cipher.Login.MainFido2Credential.DiscoverableValue}");
var authData = await GenerateAuthDataAsync(
rpId: makeCredentialParams.RpEntity.Id,
counter: fido2Credential.CounterValue,
userPresence: true,
userVerification: userVerified,
credentialId: GuidToRawFormat(credentialId),
credentialId: credentialId.GuidToRawFormat(),
publicKey: keyPair.publicKey
);
return new Fido2AuthenticatorMakeCredentialResult
{
CredentialId = GuidToRawFormat(credentialId),
CredentialId = credentialId.GuidToRawFormat(),
AttestationObject = EncodeAttestationObject(authData),
AuthData = authData,
PublicKey = keyPair.publicKey.ExportDer(),
@ -197,6 +201,9 @@ namespace Bit.Core.Services
throw new NotAllowedError();
}
// TODO: Remove this hardcoding
userVerified = true;
if (!userVerified && (assertionParams.RequireUserVerification || selectedCipher.Reprompt != CipherRepromptType.None)) {
// _logService.Info(
// "[Fido2Authenticator] Aborting because user verification was unsuccessful."
@ -221,6 +228,9 @@ namespace Bit.Core.Services
var encrypted = await _cipherService.EncryptAsync(selectedCipher);
await _cipherService.SaveWithServerAsync(encrypted);
ClipLogger.Log($"[Fido2Authenticator] Selected fido2 cred RPID {selectedFido2Credential.RpId}");
ClipLogger.Log($"[Fido2Authenticator] param RpId {assertionParams.RpId}");
var authenticatorData = await GenerateAuthDataAsync(
rpId: selectedFido2Credential.RpId,
userPresence: true,
@ -245,7 +255,7 @@ namespace Bit.Core.Services
{
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential
{
Id = GuidToRawFormat(selectedCredentialId),
Id = selectedCredentialId.GuidToRawFormat(),
UserHandle = selectedFido2Credential.UserHandleValue
},
AuthenticatorData = authenticatorData,
@ -265,7 +275,7 @@ namespace Bit.Core.Services
{
var credentials = (await FindCredentialsByRpAsync(rpId)).Select(cipher => new Fido2AuthenticatorDiscoverableCredentialMetadata {
Type = "public-key",
Id = GuidToRawFormat(cipher.Login.MainFido2Credential.CredentialId),
Id = cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat(),
RpId = cipher.Login.MainFido2Credential.RpId,
UserHandle = cipher.Login.MainFido2Credential.UserHandleValue,
UserName = cipher.Login.MainFido2Credential.UserName
@ -290,7 +300,7 @@ namespace Bit.Core.Services
{
try
{
ids.Add(GuidToStandardFormat(credential.Id));
ids.Add(credential.Id.GuidToStandardFormat());
} catch {}
}
@ -320,15 +330,8 @@ namespace Bit.Core.Services
{
try
{
if (credential.IdStr != null)
{
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> Adding credID: {credential.IdStr}");
ids.Add(credential.IdStr);
continue;
}
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> Converting Guid byte length: {credential.Id.Length}");
ids.Add(GuidToStandardFormat(credential.Id));
ids.Add(credential.Id.GuidToStandardFormat());
}
catch(Exception ex)
{
@ -384,16 +387,16 @@ namespace Bit.Core.Services
{
return new Fido2CredentialView {
CredentialId = Guid.NewGuid().ToString(),
KeyType = Bit.Core.Constants.DefaultFido2CredentialType,
KeyAlgorithm = Bit.Core.Constants.DefaultFido2CredentialAlgorithm,
KeyCurve = Bit.Core.Constants.DefaultFido2CredentialCurve,
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,
UserDisplayName = makeCredentialsParams.UserEntity.DisplayName,
DiscoverableValue = makeCredentialsParams.RequireResidentKey,
CreationDate = DateTime.Now
};
@ -414,6 +417,7 @@ namespace Bit.Core.Services
var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256);
authData.AddRange(rpIdHash);
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> rpIdHash: {rpIdHash}");
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> ad: {isAttestation} - uv: {userVerification} - up: {userPresence}");
@ -520,16 +524,6 @@ namespace Bit.Core.Services
return dsa.SignData(sigBase, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
}
private string GuidToStandardFormat(byte[] bytes)
{
return new Guid(bytes).ToString();
}
private byte[] GuidToRawFormat(string guid)
{
return Guid.Parse(guid).ToByteArray();
}
private class PublicKey
{
private readonly ECDsa _dsa;

View File

@ -2,7 +2,6 @@ namespace Bit.Core.Utilities.Fido2
{
public class PublicKeyCredentialDescriptor {
public byte[] Id { get; set; }
public string IdStr { get; set; }
public string[] Transports { get; set; }
public string Type { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace Bit.Core.Utilities
{
public static class GuidExtensions
{
public static string GuidToStandardFormat(this byte[] bytes)
{
return new Guid(bytes).ToString();
}
public static byte[] GuidToRawFormat(this string guid)
{
return Guid.Parse(guid).ToByteArray();
}
}
}

View File

@ -14,7 +14,6 @@ using Foundation;
using Microsoft.Maui.ApplicationModel;
using ObjCRuntime;
using UIKit;
using Vision;
namespace Bit.iOS.Autofill
{
@ -110,10 +109,13 @@ namespace Bit.iOS.Autofill
UserEntity = new PublicKeyCredentialUserEntity
{
Id = credIdentity.UserHandle.ToArray(),
Name = credIdentity.UserName
Name = credIdentity.UserName,
DisplayName = credIdentity.UserName
}
});
await ASHelpers.ReplaceAllIdentitiesAsync();
ClipLogger.Log($"PIFPR Completing");
ClipLogger.Log($"PIFPR Completing - RpId: {credIdentity.RelyingPartyIdentifier}");
ClipLogger.Log($"PIFPR Completing - CDH: {passkeyRegistrationRequest.ClientDataHash.GetBase64EncodedString(NSDataBase64EncodingOptions.None)}");
@ -127,15 +129,6 @@ namespace Bit.iOS.Autofill
NSData.FromArray(result.AttestationObject)));
ClipLogger.Log($"CompleteRegistrationRequestAsync: {expired}");
//else if (await IsLocked())
//{
// PerformSegue("lockPasswordSegue", this);
//}
//else
//{
// PerformSegue("loginListSegue", this);
//}
}
private PublicKeyCredentialParameters[] GetCredTypesAndPubKeyAlgs(NSNumber[] supportedAlgorithms)
@ -204,14 +197,14 @@ namespace Bit.iOS.Autofill
var fido2AssertionResult = await Fido2AuthService.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
{
RpId = rpId,
Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToByteArray(),
Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(),
RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required",
RequireUserPresence = false,
AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[]
{
new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor
{
IdStr = credentialIdData.ToString()
Id = credentialIdData.ToArray()
}
}
});
@ -227,7 +220,7 @@ namespace Bit.iOS.Autofill
ClipLogger.Log("selectedUserHandleData:" + selectedUserHandleData);
var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null
? new Guid(fido2AssertionResult.SelectedCredential.Id).ToString()
? NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
: credentialIdData;
ClipLogger.Log("selectedCredentialIdData:" + selectedCredentialIdData);

View File

@ -509,7 +509,7 @@ namespace Bit.iOS.Autofill
try
{
ClipLogger.Log("ProvideCredentialAsync");
if (_context.IsPasskey && UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
if (_context.PasskeyCredentialIdentity is null)

View File

@ -3,6 +3,7 @@ using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Foundation;
using UIKit;
namespace Bit.iOS.Core.Utilities
@ -146,7 +147,7 @@ namespace Bit.iOS.Core.Utilities
return new ASPasskeyCredentialIdentity(cipher.Login.MainFido2Credential.RpId,
cipher.Login.MainFido2Credential.UserName,
cipher.Login.MainFido2Credential.CredentialId,
NSData.FromArray(cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat()),
cipher.Login.MainFido2Credential.UserHandle,
cipher.Id);
}