mirror of
https://github.com/bitwarden/mobile
synced 2026-02-25 08:53:20 +00:00
[PM-5731] feat: implement credential assertion in client
This commit is contained in:
@@ -192,7 +192,7 @@ namespace Bit.Core.Services
|
||||
|
||||
var signature = GenerateSignature(
|
||||
authData: authenticatorData,
|
||||
clientDataHash: assertionParams.ClientDataHash,
|
||||
clientDataHash: assertionParams.Hash,
|
||||
privateKey: selectedFido2Credential.KeyBytes
|
||||
);
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ namespace Bit.Core.Services
|
||||
IStateService stateService,
|
||||
IEnvironmentService environmentService,
|
||||
ICryptoFunctionService cryptoFunctionService,
|
||||
IFido2AuthenticatorService fido2AuthenticatorService
|
||||
)
|
||||
IFido2AuthenticatorService fido2AuthenticatorService)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_environmentService = environmentService;
|
||||
@@ -133,7 +132,74 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) => throw new NotImplementedException();
|
||||
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);
|
||||
|
||||
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,
|
||||
@@ -160,5 +226,23 @@ namespace Bit.Core.Services
|
||||
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,
|
||||
RequireUserPresence = true,
|
||||
RequireUserVerification = requireUserVerification,
|
||||
Hash = cliendDataHash
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
public string RpId { get; set; }
|
||||
|
||||
/** The hash of the serialized client data, provided by the client. */
|
||||
public byte[] ClientDataHash { get; set; }
|
||||
public byte[] Hash { get; set; }
|
||||
|
||||
public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList { get; set; }
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
/// </summary>
|
||||
public bool RequireUserPresence { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The challenge to be signed by the authenticator.
|
||||
/// </summary>
|
||||
public byte[] Challenge { get; set; }
|
||||
|
||||
public object Extensions { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,13 @@ namespace Bit.Core.Utilities.Fido2
|
||||
public class Fido2ClientAssertCredentialParams
|
||||
{
|
||||
/// <summary>
|
||||
/// S challenge that the selected authenticator signs, along with other data, when producing an authentication
|
||||
/// 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; }
|
||||
|
||||
@@ -16,5 +16,27 @@ namespace Bit.Core.Utilities.Fido2
|
||||
/// 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace Bit.Core.Utilities.Fido2
|
||||
/// </summary>
|
||||
public required string Origin { get; set; }
|
||||
|
||||
// TODO: Check if we actually need this
|
||||
/// <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.
|
||||
|
||||
Reference in New Issue
Block a user