1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-22 11:35:21 +01: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
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
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 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 string LaunchUri => $"https://{RpId}";

View File

@ -1,27 +1,95 @@
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Enums;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services
{
public class Fido2AuthenticatorService : IFido2AuthenticatorService
{
private INativeLogService _logService;
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;
_syncService = syncService;
_userInterface = userInterface;
}
public Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
{
// 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
// return Task.FromResult(new Fido2AuthenticatorGetAssertionResult
// {
// AuthenticatorData = new byte[32],
// Signature = new byte[8]
// });
return new Fido2AuthenticatorGetAssertionResult
{
AuthenticatorData = new byte[32],
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.Models.Domain;
using Bit.Core.Models.View;
using Bit.Core.Enums;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Utilities.Fido2;
using Bit.Test.Common.AutoFixture;
@ -14,6 +15,7 @@ using NSubstitute.ExceptionExtensions;
using Xunit;
using Bit.Core.Utilities;
using System.Collections.Generic;
using System.Linq;
namespace Bit.Core.Test.Services
{
@ -33,37 +35,72 @@ namespace Bit.Core.Test.Services
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task GetAssertionAsync_Throws_CredentialExistsButRpIdDoesNotMatch(SutProvider<Fido2AuthenticatorService> sutProvider, Fido2AuthenticatorGetAssertionParams aParams)
{
var credentialId = RandomBytes(32);
var credentialId = Guid.NewGuid();
aParams.RpId = "bitwarden.com";
aParams.AllowCredentialDescriptorList = [
new PublicKeyCredentialDescriptor {
Id = credentialId,
Id = credentialId.ToByteArray(),
Type = "public-key"
}
];
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(new List<CipherView> {
new CipherView {
Login = new LoginView {
Fido2Credentials = new List<Fido2CredentialView> {
new Fido2CredentialView {
CredentialId = CoreHelpers.Base64UrlEncode(credentialId),
RpId = "mismatch-rpid"
}
}
}
}
});
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([
CreateCipherView(credentialId.ToString(), "mismatch-rpid", false),
]);
var exception = await Assert.ThrowsAsync<NotAllowedError>(() => sutProvider.Sut.GetAssertionAsync(aParams));
}
#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)
{
var bytes = new byte[length];
new Random().NextBytes(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"
}
}
}
};
}
}
}