1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-22 11:35:21 +01:00

[PM-5731] feat: implement credential exclusion

This commit is contained in:
Andreas Coroiu 2024-01-24 14:18:27 +01:00
parent 19639b61c3
commit f0dde7eb82
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
4 changed files with 168 additions and 21 deletions

View File

@ -42,5 +42,12 @@ namespace Bit.Core.Abstractions
/// <param name="pickCredentialParams">The parameters to use when asking the user to pick a credential.</param>
/// <returns>The ID of the cipher that contains the credentials the user picked.</returns>
Task<Fido2PickCredentialResult> PickCredentialAsync(Fido2PickCredentialParams pickCredentialParams);
/// <summary>
/// Inform the user that the operation was cancelled because their vault contains excluded credentials.
/// </summary>
/// <param name="existingCipherIds">The IDs of the excluded credentials.</param>
/// <returns>When user has confirmed the message</returns>
Task InformExcludedCredential(string[] existingCipherIds);
}
}

View File

@ -23,7 +23,7 @@ namespace Bit.Core.Services
_userInterface = userInterface;
}
public Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams)
public async Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams)
{
if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Algorithm != (int) Fido2AlgorithmIdentifier.ES256))
{
@ -34,6 +34,20 @@ namespace Bit.Core.Services
throw new NotSupportedError();
}
// await _userInterface.EnsureUnlockedVault();
await _syncService.FullSyncAsync(false);
var existingCipherIds = await FindExcludedCredentials(
makeCredentialParams.ExcludeCredentialDescriptorList
);
if (existingCipherIds.Length > 0) {
_logService.Info(
"[Fido2Authenticator] Aborting due to excluded credential found in vault."
);
await _userInterface.InformExcludedCredential(existingCipherIds);
throw new NotAllowedError();
}
throw new NotImplementedException();
}
@ -130,6 +144,40 @@ namespace Bit.Core.Services
}
}
///<summary>
/// Finds existing crendetials and returns the `CipherId` for each one
///</summary>
private async Task<string[]> FindExcludedCredentials(
PublicKeyCredentialDescriptor[] credentials
) {
var ids = new List<string>();
foreach (var credential in credentials)
{
try
{
ids.Add(GuidToStandardFormat(credential.Id));
} catch {}
}
if (ids.Count == 0) {
return [];
}
var ciphers = await _cipherService.GetAllDecryptedAsync();
return ciphers
.FindAll(
(cipher) =>
!cipher.IsDeleted &&
cipher.OrganizationId == null &&
cipher.Type == CipherType.Login &&
cipher.Login.HasFido2Credentials &&
ids.Contains(cipher.Login.MainFido2Credential.CredentialId)
)
.Select((cipher) => cipher.Id)
.ToArray();
}
private async Task<List<CipherView>> FindCredentialsById(PublicKeyCredentialDescriptor[] credentials, string rpId)
{
var ids = new List<string>();

View File

@ -22,6 +22,11 @@ namespace Bit.Core.Utilities.Fido2
///</summary>
public PublicKeyCredentialAlgorithmDescriptor[] CredTypesAndPubKeyAlgs { get; set; }
///<summary>
///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials.
///</summary>
public PublicKeyCredentialDescriptor[] ExcludeCredentialDescriptorList { get; set; }
///<summary>
/// The effective resident key requirement for credential creation, a Boolean value determined by the client. Resident is synonymous with discoverable. */
///</summary>

View File

@ -21,12 +21,12 @@ namespace Bit.Core.Test.Services
{
public class Fido2AuthenticatorMakeCredentialTests
{
#region missing non-discoverable credential
#region invalid input parameters
// Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation.
// Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation.
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task GetAssertionAsync_ThrowsNotSupported_NoSupportedAlgorithm(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorMakeCredentialParams mParams)
public async Task MakeCredentialAsync_ThrowsNotSupported_NoSupportedAlgorithm(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorMakeCredentialParams mParams)
{
mParams.CredTypesAndPubKeyAlgs = [
new PublicKeyCredentialAlgorithmDescriptor {
@ -38,24 +38,111 @@ namespace Bit.Core.Test.Services
await Assert.ThrowsAsync<NotSupportedError>(() => sutProvider.Sut.MakeCredentialAsync(mParams));
}
// [Theory]
// [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
// public async Task GetAssertionAsync_Throws_CredentialExistsButRpIdDoesNotMatch(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorGetAssertionParams aParams)
// {
// var credentialId = Guid.NewGuid();
// aParams.RpId = "bitwarden.com";
// aParams.AllowCredentialDescriptorList = [
// new PublicKeyCredentialDescriptor {
// Id = credentialId.ToByteArray(),
// Type = "public-key"
// }
// ];
// sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([
// CreateCipherView(credentialId.ToString(), "mismatch-rpid", false),
// ]);
#endregion
// await Assert.ThrowsAsync<NotAllowedError>(() => sutProvider.Sut.GetAssertionAsync(aParams));
// }
#region vault contains excluded credential
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
// Spec: collect an authorization gesture confirming user consent for creating a new credential.
// Deviation: Consent is not asked and the user is simply informed of the situation.
public async Task MakeCredentialAsync_InformsUser_ExcludedCredentialFound(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorMakeCredentialParams mParams)
{
var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
List<CipherView> ciphers = [
CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false),
CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true)
];
mParams.CredTypesAndPubKeyAlgs = [
new PublicKeyCredentialAlgorithmDescriptor {
Type = "public-key",
Algorithm = -7 // ES256
}
];
mParams.RpId = "bitwarden.com";
mParams.RequireUserVerification = false;
mParams.ExcludeCredentialDescriptorList = [
new PublicKeyCredentialDescriptor {
Type = "public-key",
Id = credentialIds[0].ToByteArray()
}
];
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(ciphers);
try
{
await sutProvider.Sut.MakeCredentialAsync(mParams);
}
catch {}
await sutProvider.GetDependency<IFido2UserInterface>().Received().InformExcludedCredential(Arg.Is<string[]>(
(c) => c.SequenceEqual(new string[] { ciphers[0].Id })
));
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
// Spec: return an error code equivalent to "NotAllowedError" and terminate the operation.
public async Task MakeCredentialAsync_ThrowsNotAllowed_ExcludedCredentialFound(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorMakeCredentialParams mParams)
{
var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
List<CipherView> ciphers = [
CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false),
CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true)
];
mParams.CredTypesAndPubKeyAlgs = [
new PublicKeyCredentialAlgorithmDescriptor {
Type = "public-key",
Algorithm = -7 // ES256
}
];
mParams.RpId = "bitwarden.com";
mParams.RequireUserVerification = false;
mParams.ExcludeCredentialDescriptorList = [
new PublicKeyCredentialDescriptor {
Type = "public-key",
Id = credentialIds[0].ToByteArray()
}
];
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(ciphers);
await Assert.ThrowsAsync<NotAllowedError>(() => sutProvider.Sut.MakeCredentialAsync(mParams));
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
// Deviation: Organization ciphers are not checked against excluded credentials, even if the user has access to them.
public async Task MakeCredentialAsync_DoesNotInformAboutExcludedCredential_ExcludedCredentialBelongsToOrganization(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorMakeCredentialParams mParams)
{
var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
List<CipherView> ciphers = [
CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false),
CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true)
];
ciphers[0].OrganizationId = "someOrganizationId";
mParams.CredTypesAndPubKeyAlgs = [
new PublicKeyCredentialAlgorithmDescriptor {
Type = "public-key",
Algorithm = -7 // ES256
}
];
mParams.RpId = "bitwarden.com";
mParams.RequireUserVerification = false;
mParams.ExcludeCredentialDescriptorList = [
new PublicKeyCredentialDescriptor {
Type = "public-key",
Id = credentialIds[0].ToByteArray()
}
];
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(ciphers);
try
{
await sutProvider.Sut.MakeCredentialAsync(mParams);
} catch {}
await sutProvider.GetDependency<IFido2UserInterface>().DidNotReceive().InformExcludedCredential(Arg.Any<string[]>());
}
#endregion