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:
parent
cc89b6a5d5
commit
66a01e30d3
46
src/Core/Abstractions/IFido2UserInterface.cs
Normal file
46
src/Core/Abstractions/IFido2UserInterface.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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}";
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user