mirror of
https://github.com/bitwarden/mobile
synced 2025-12-20 18:23:51 +00:00
Fix FIDO2 client bugs (#3056)
* fix: blockedUris null issue * fix: trailing slash in origin breaking check
This commit is contained in:
@@ -36,7 +36,7 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||||
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
|
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
|
||||||
if (blockedUris.Contains(domain))
|
if (blockedUris != null && blockedUris.Contains(domain))
|
||||||
{
|
{
|
||||||
throw new Fido2ClientException(
|
throw new Fido2ClientException(
|
||||||
Fido2ClientException.ErrorCode.UriBlockedError,
|
Fido2ClientException.ErrorCode.UriBlockedError,
|
||||||
@@ -107,7 +107,8 @@ namespace Bit.Core.Services
|
|||||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found");
|
throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found");
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientDataJSON = JsonSerializer.Serialize(new {
|
var clientDataJSON = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
type = "webauthn.create",
|
type = "webauthn.create",
|
||||||
challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge),
|
challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge),
|
||||||
origin = createCredentialParams.Origin,
|
origin = createCredentialParams.Origin,
|
||||||
@@ -118,10 +119,12 @@ namespace Bit.Core.Services
|
|||||||
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
|
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
|
||||||
var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
|
var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams, _makeCredentialUserInterface);
|
var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams, _makeCredentialUserInterface);
|
||||||
|
|
||||||
return new Fido2ClientCreateCredentialResult {
|
return new Fido2ClientCreateCredentialResult
|
||||||
|
{
|
||||||
CredentialId = makeCredentialResult.CredentialId,
|
CredentialId = makeCredentialResult.CredentialId,
|
||||||
AttestationObject = makeCredentialResult.AttestationObject,
|
AttestationObject = makeCredentialResult.AttestationObject,
|
||||||
AuthData = makeCredentialResult.AuthData,
|
AuthData = makeCredentialResult.AuthData,
|
||||||
@@ -130,9 +133,13 @@ namespace Bit.Core.Services
|
|||||||
PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm,
|
PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm,
|
||||||
Transports = createCredentialParams.Rp.Id == "google.com" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side
|
Transports = createCredentialParams.Rp.Id == "google.com" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side
|
||||||
};
|
};
|
||||||
} catch (InvalidStateError) {
|
}
|
||||||
|
catch (InvalidStateError)
|
||||||
|
{
|
||||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
||||||
} catch (Exception) {
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,7 +148,7 @@ namespace Bit.Core.Services
|
|||||||
{
|
{
|
||||||
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||||
var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin);
|
var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin);
|
||||||
if (blockedUris.Contains(domain))
|
if (blockedUris != null && blockedUris.Contains(domain))
|
||||||
{
|
{
|
||||||
throw new Fido2ClientException(
|
throw new Fido2ClientException(
|
||||||
Fido2ClientException.ErrorCode.UriBlockedError,
|
Fido2ClientException.ErrorCode.UriBlockedError,
|
||||||
@@ -176,7 +183,8 @@ namespace Bit.Core.Services
|
|||||||
"RP ID cannot be used with this origin");
|
"RP ID cannot be used with this origin");
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientDataJSON = JsonSerializer.Serialize(new {
|
var clientDataJSON = JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
type = "webauthn.get",
|
type = "webauthn.get",
|
||||||
challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge),
|
challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge),
|
||||||
origin = assertCredentialParams.Origin,
|
origin = assertCredentialParams.Origin,
|
||||||
@@ -186,10 +194,12 @@ namespace Bit.Core.Services
|
|||||||
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
|
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
|
||||||
var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);
|
var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
var getAssertionResult = await _fido2AuthenticatorService.GetAssertionAsync(getAssertionParams, _getAssertionUserInterface);
|
var getAssertionResult = await _fido2AuthenticatorService.GetAssertionAsync(getAssertionParams, _getAssertionUserInterface);
|
||||||
|
|
||||||
return new Fido2ClientAssertCredentialResult {
|
return new Fido2ClientAssertCredentialResult
|
||||||
|
{
|
||||||
AuthenticatorData = getAssertionResult.AuthenticatorData,
|
AuthenticatorData = getAssertionResult.AuthenticatorData,
|
||||||
ClientDataJSON = clientDataJSONBytes,
|
ClientDataJSON = clientDataJSONBytes,
|
||||||
Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id),
|
Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id),
|
||||||
@@ -197,9 +207,13 @@ namespace Bit.Core.Services
|
|||||||
Signature = getAssertionResult.Signature,
|
Signature = getAssertionResult.Signature,
|
||||||
UserHandle = getAssertionResult.SelectedCredential.UserHandle
|
UserHandle = getAssertionResult.SelectedCredential.UserHandle
|
||||||
};
|
};
|
||||||
} catch (InvalidStateError) {
|
}
|
||||||
|
catch (InvalidStateError)
|
||||||
|
{
|
||||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
||||||
} catch (Exception) {
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +234,8 @@ namespace Bit.Core.Services
|
|||||||
createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" ||
|
createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" ||
|
||||||
createCredentialParams.AuthenticatorSelection?.UserVerification == null;
|
createCredentialParams.AuthenticatorSelection?.UserVerification == null;
|
||||||
|
|
||||||
return new Fido2AuthenticatorMakeCredentialParams {
|
return new Fido2AuthenticatorMakeCredentialParams
|
||||||
|
{
|
||||||
RequireResidentKey = requireResidentKey,
|
RequireResidentKey = requireResidentKey,
|
||||||
RequireUserVerification = requireUserVerification,
|
RequireUserVerification = requireUserVerification,
|
||||||
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
|
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
|
||||||
@@ -240,7 +255,8 @@ namespace Bit.Core.Services
|
|||||||
assertCredentialParams.UserVerification == "preferred" ||
|
assertCredentialParams.UserVerification == "preferred" ||
|
||||||
assertCredentialParams.UserVerification == null;
|
assertCredentialParams.UserVerification == null;
|
||||||
|
|
||||||
return new Fido2AuthenticatorGetAssertionParams {
|
return new Fido2AuthenticatorGetAssertionParams
|
||||||
|
{
|
||||||
RpId = assertCredentialParams.RpId,
|
RpId = assertCredentialParams.RpId,
|
||||||
Challenge = assertCredentialParams.Challenge,
|
Challenge = assertCredentialParams.Challenge,
|
||||||
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,
|
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,
|
||||||
|
|||||||
@@ -16,18 +16,18 @@ namespace Bit.Core.Utilities.Fido2
|
|||||||
// We only care about the domain part of the origin, not the protocol or port so we remove them here,
|
// We only care about the domain part of the origin, not the protocol or port so we remove them here,
|
||||||
// while still keeping ipv6 intact.
|
// while still keeping ipv6 intact.
|
||||||
// https is enforced in the client, so we don't need to worry about that here
|
// https is enforced in the client, so we don't need to worry about that here
|
||||||
var originWithoutProtocolOrPort = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?", "$2$4");
|
var originWithoutProtocolPortOrPath = Regex.Replace(origin, @"(https?://)?([^:/]+)(:\d+)?(/.*)?$", "$2");
|
||||||
if (Uri.CheckHostName(rpId) != UriHostNameType.Dns || Uri.CheckHostName(originWithoutProtocolOrPort) != UriHostNameType.Dns)
|
if (Uri.CheckHostName(rpId) != UriHostNameType.Dns || Uri.CheckHostName(originWithoutProtocolPortOrPath) != UriHostNameType.Dns)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rpId == originWithoutProtocolOrPort)
|
if (rpId == originWithoutProtocolPortOrPath)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolOrPort, out var parsedOrgin))
|
if (!DomainName.TryParse(rpId, out var parsedRpId) || !DomainName.TryParse(originWithoutProtocolPortOrPath, out var parsedOrgin))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,17 @@ namespace Bit.Core.Test.Utilities.Fido2
|
|||||||
[InlineData("bitwarden.com", "https://login.bitwarden.com:1337", true)]
|
[InlineData("bitwarden.com", "https://login.bitwarden.com:1337", true)]
|
||||||
[InlineData("login.bitwarden.com", "https://login.bitwarden.com:1337", true)]
|
[InlineData("login.bitwarden.com", "https://login.bitwarden.com:1337", true)]
|
||||||
[InlineData("login.bitwarden.com", "https://sub.login.bitwarden.com:1337", true)]
|
[InlineData("login.bitwarden.com", "https://sub.login.bitwarden.com:1337", true)]
|
||||||
|
// Origin with trailing slash
|
||||||
|
[InlineData("sub.login.bitwarden.com", "https://login.bitwarden.com:1337/", false)]
|
||||||
|
[InlineData("passwordless.dev", "https://login.bitwarden.com:1337/", false)]
|
||||||
|
[InlineData("login.passwordless.dev", "https://login.bitwarden.com:1337/", false)]
|
||||||
|
[InlineData("bitwarden", "localhost/", false)]
|
||||||
|
[InlineData("bitwarden", "bitwarden/", true)]
|
||||||
|
[InlineData("localhost", "https://localhost:8080/", true)]
|
||||||
|
[InlineData("bitwarden.com", "https://bitwarden.com/", true)]
|
||||||
|
[InlineData("bitwarden.com", "https://login.bitwarden.com:1337/", true)]
|
||||||
|
[InlineData("login.bitwarden.com", "https://login.bitwarden.com:1337/", true)]
|
||||||
|
[InlineData("login.bitwarden.com", "https://sub.login.bitwarden.com:1337/", true)]
|
||||||
public void ValidateRpId(string rpId, string origin, bool isValid)
|
public void ValidateRpId(string rpId, string origin, bool isValid)
|
||||||
{
|
{
|
||||||
Assert.Equal(isValid, Fido2DomainUtils.IsValidRpId(rpId, origin));
|
Assert.Equal(isValid, Fido2DomainUtils.IsValidRpId(rpId, origin));
|
||||||
|
|||||||
Reference in New Issue
Block a user