1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

[PM-5731] feat: ask for credentials when found

This commit is contained in:
Andreas Coroiu
2024-01-19 10:45:03 +01:00
parent cc89b6a5d5
commit 66a01e30d3
4 changed files with 174 additions and 23 deletions

View File

@@ -0,0 +1,46 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
/// <summary>
/// Parameters used to ask the user to pick a credential from a list of existing credentials.
/// </summary>
public struct Fido2PickCredentialParams
{
/// <summary>
/// The IDs of the credentials that the user can pick from.
/// </summary>
public string[] CipherIds { get; set; }
/// <summary>
/// Whether or not the user must be verified before completing the operation.
/// </summary>
public bool UserVerification { get; set; }
}
/// <summary>
/// The result of asking the user to pick a credential from a list of existing credentials.
/// </summary>
public struct Fido2PickCredentialResult
{
/// <summary>
/// The ID of the cipher that contains the credentials the user picked.
/// </summary>
public string CipherId { get; set; }
/// <summary>
/// Whether or not the user was verified before completing the operation.
/// </summary>
public bool UserVerified { get; set; }
}
public interface IFido2UserInterface
{
/// <summary>
/// Ask the user to pick a credential from a list of existing credentials.
/// </summary>
/// <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);
}
}

View File

@@ -29,7 +29,7 @@ namespace Bit.Core.Models.View
public override string SubTitle => UserName; public override string SubTitle => UserName;
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>(); public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
public bool IsDiscoverable => !string.IsNullOrWhiteSpace(Discoverable); public bool IsDiscoverable => bool.TryParse(Discoverable, out var isDiscoverable) && isDiscoverable;
public bool CanLaunch => !string.IsNullOrEmpty(RpId); public bool CanLaunch => !string.IsNullOrEmpty(RpId);
public string LaunchUri => $"https://{RpId}"; public string LaunchUri => $"https://{RpId}";

View File

@@ -1,27 +1,95 @@
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Enums;
using Bit.Core.Utilities.Fido2; using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public class Fido2AuthenticatorService : IFido2AuthenticatorService public class Fido2AuthenticatorService : IFido2AuthenticatorService
{ {
private INativeLogService _logService;
private ICipherService _cipherService; private ICipherService _cipherService;
private ISyncService _syncService;
private IFido2UserInterface _userInterface;
public Fido2AuthenticatorService(ICipherService cipherService) public Fido2AuthenticatorService(INativeLogService logService, ICipherService cipherService, ISyncService syncService, IFido2UserInterface userInterface)
{ {
_logService = logService;
_cipherService = cipherService; _cipherService = cipherService;
_syncService = syncService;
_userInterface = userInterface;
} }
public Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams) public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
{ {
throw new NotAllowedError(); // 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 = new List<CipherView>();
// cipherOptions = await this.findCredentialsByRp(params.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
});
// TODO: IMPLEMENT this // TODO: IMPLEMENT this
// return Task.FromResult(new Fido2AuthenticatorGetAssertionResult return new Fido2AuthenticatorGetAssertionResult
// { {
// AuthenticatorData = new byte[32], AuthenticatorData = new byte[32],
// Signature = new byte[8] Signature = new byte[8]
// }); };
} }
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.Fido2Credentials[0].RpId == rpId &&
ids.Contains(cipher.Login.Fido2Credentials[0].CredentialId)
);
}
private string GuidToStandardFormat(byte[] bytes)
{
return new Guid(bytes).ToString();
}
} }
} }

View File

@@ -5,6 +5,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.View; using Bit.Core.Models.View;
using Bit.Core.Enums;
using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture;
using Bit.Core.Utilities.Fido2; using Bit.Core.Utilities.Fido2;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
@@ -14,6 +15,7 @@ using NSubstitute.ExceptionExtensions;
using Xunit; using Xunit;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Test.Services namespace Bit.Core.Test.Services
{ {
@@ -33,37 +35,72 @@ namespace Bit.Core.Test.Services
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task GetAssertionAsync_Throws_CredentialExistsButRpIdDoesNotMatch(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorGetAssertionParams aParams) public async Task GetAssertionAsync_Throws_CredentialExistsButRpIdDoesNotMatch(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorGetAssertionParams aParams)
{ {
var credentialId = RandomBytes(32); var credentialId = Guid.NewGuid();
aParams.RpId = "bitwarden.com"; aParams.RpId = "bitwarden.com";
aParams.AllowCredentialDescriptorList = [ aParams.AllowCredentialDescriptorList = [
new PublicKeyCredentialDescriptor { new PublicKeyCredentialDescriptor {
Id = credentialId, Id = credentialId.ToByteArray(),
Type = "public-key" Type = "public-key"
} }
]; ];
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(new List<CipherView> { sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([
new CipherView { CreateCipherView(credentialId.ToString(), "mismatch-rpid", false),
Login = new LoginView { ]);
Fido2Credentials = new List<Fido2CredentialView> {
new Fido2CredentialView {
CredentialId = CoreHelpers.Base64UrlEncode(credentialId),
RpId = "mismatch-rpid"
}
}
}
}
});
var exception = await Assert.ThrowsAsync<NotAllowedError>(() => sutProvider.Sut.GetAssertionAsync(aParams)); var exception = await Assert.ThrowsAsync<NotAllowedError>(() => sutProvider.Sut.GetAssertionAsync(aParams));
} }
#endregion #endregion
#region vault contains credential
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task GetAssertionAsync_AsksForAllCredentials_ParamsContainsAllowedCredentialsList(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorGetAssertionParams aParams)
{
var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
List<CipherView> ciphers = [
CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false),
CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true)
];
aParams.RpId = "bitwarden.com";
aParams.AllowCredentialDescriptorList = credentialIds.Select((credentialId) => new PublicKeyCredentialDescriptor {
Id = credentialId.ToByteArray(),
Type = "public-key"
}).ToArray();
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(ciphers);
await sutProvider.Sut.GetAssertionAsync(aParams);
await sutProvider.GetDependency<IFido2UserInterface>().Received().PickCredentialAsync(Arg.Is<Fido2PickCredentialParams>(
(pickCredentialParams) => pickCredentialParams.CipherIds.SequenceEqual(ciphers.Select((cipher) => cipher.Id)) && pickCredentialParams.UserVerification == aParams.RequireUserVerification
));
}
#endregion
private byte[] RandomBytes(int length) private byte[] RandomBytes(int length)
{ {
var bytes = new byte[length]; var bytes = new byte[length];
new Random().NextBytes(bytes); new Random().NextBytes(bytes);
return bytes; return bytes;
} }
#nullable enable
private CipherView CreateCipherView(string? credentialId, string? rpId, bool? discoverable)
{
return new CipherView {
Type = CipherType.Login,
Login = new LoginView {
Fido2Credentials = new List<Fido2CredentialView> {
new Fido2CredentialView {
CredentialId = credentialId ?? Guid.NewGuid().ToString(),
RpId = rpId ?? "bitwarden.com",
Discoverable = discoverable.HasValue ? discoverable.ToString() : "true"
}
}
}
};
}
} }
} }