mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-22 11:35:21 +01:00
[PM-5731] feat: implement key generation
This commit is contained in:
parent
c87728027e
commit
da7326b0cc
@ -32,7 +32,6 @@ namespace Bit.Core.Abstractions
|
||||
Task<byte[]> RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm);
|
||||
Task<byte[]> RsaExtractPublicKeyAsync(byte[] privateKey);
|
||||
Task<Tuple<byte[], byte[]>> RsaGenerateKeyPairAsync(int length);
|
||||
Task<(byte[] PublicKey, byte[] PrivateKey)> EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm algorithm);
|
||||
Task<byte[]> RandomBytesAsync(int length);
|
||||
byte[] RandomBytes(int length);
|
||||
Task<uint> RandomNumberAsync();
|
||||
|
@ -70,8 +70,8 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
try {
|
||||
var (publicKey, privateKey) = await _cryptoFunctionService.EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm.P256Sha256);
|
||||
var fido2Credential = CreateCredentialView(makeCredentialParams, privateKey);
|
||||
var keyPair = GenerateKeyPair();
|
||||
var fido2Credential = CreateCredentialView(makeCredentialParams, keyPair.privateKey);
|
||||
|
||||
var encrypted = await _cipherService.GetAsync(cipherId);
|
||||
var cipher = await encrypted.DecryptAsync();
|
||||
@ -94,8 +94,7 @@ namespace Bit.Core.Services
|
||||
userPresence: true,
|
||||
userVerification: userVerified,
|
||||
credentialId: GuidToRawFormat(credentialId),
|
||||
publicKey: publicKey,
|
||||
privateKey: privateKey
|
||||
publicKey: keyPair.publicKey
|
||||
);
|
||||
|
||||
return new Fido2AuthenticatorMakeCredentialResult
|
||||
@ -284,6 +283,24 @@ namespace Bit.Core.Services
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Move this to a separate service
|
||||
private (PublicKey publicKey, byte[] privateKey) GenerateKeyPair()
|
||||
{
|
||||
using (System.Security.Cryptography.ECDsa dsa = System.Security.Cryptography.ECDsa.Create())
|
||||
{
|
||||
dsa.GenerateKey(System.Security.Cryptography.ECCurve.NamedCurves.nistP256);
|
||||
var privateKey = dsa.ExportPkcs8PrivateKey();
|
||||
|
||||
System.Security.Cryptography.ECParameters parameters = dsa.ExportParameters(true);
|
||||
|
||||
return (
|
||||
new PublicKey {
|
||||
X = parameters.Q.X,
|
||||
Y = parameters.Q.Y
|
||||
}, privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
private Fido2CredentialView CreateCredentialView(Fido2AuthenticatorMakeCredentialParams makeCredentialsParams, byte[] privateKey)
|
||||
{
|
||||
return new Fido2CredentialView {
|
||||
@ -309,10 +326,9 @@ namespace Bit.Core.Services
|
||||
bool userPresence,
|
||||
int counter,
|
||||
byte[] credentialId = null,
|
||||
byte[] publicKey = null,
|
||||
byte[] privateKey = null
|
||||
PublicKey? publicKey = null
|
||||
) {
|
||||
var isAttestation = credentialId != null && publicKey != null && privateKey != null;
|
||||
var isAttestation = credentialId != null && publicKey.HasValue;
|
||||
|
||||
List<byte> authData = new List<byte>();
|
||||
|
||||
@ -347,25 +363,9 @@ namespace Bit.Core.Services
|
||||
};
|
||||
attestedCredentialData.AddRange(credentialIdLength);
|
||||
attestedCredentialData.AddRange(credentialId);
|
||||
attestedCredentialData.AddRange(publicKey.Value.ToCose());
|
||||
|
||||
var base64PrivateKey = CoreHelpers.Base64UrlEncode(privateKey);
|
||||
|
||||
// const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
|
||||
// // COSE format of the EC256 key
|
||||
// const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);
|
||||
// const keyY = Utils.fromUrlB64ToArray(publicKeyJwk.y);
|
||||
|
||||
// // Can't get `cbor-redux` to encode in CTAP2 canonical CBOR. So we do it manually:
|
||||
// const coseBytes = new Uint8Array(77);
|
||||
// coseBytes.set([0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20], 0);
|
||||
// coseBytes.set(keyX, 10);
|
||||
// coseBytes.set([0x22, 0x58, 0x20], 10 + 32);
|
||||
// coseBytes.set(keyY, 10 + 32 + 3);
|
||||
|
||||
// // credential public key - convert to array from CBOR encoded COSE key
|
||||
// attestedCredentialData.push(...coseBytes);
|
||||
|
||||
// authData.push(...attestedCredentialData);
|
||||
authData.AddRange(attestedCredentialData);
|
||||
}
|
||||
|
||||
return authData.ToArray();
|
||||
@ -434,5 +434,40 @@ namespace Bit.Core.Services
|
||||
return Guid.Parse(guid).ToByteArray();
|
||||
}
|
||||
|
||||
private struct PublicKey
|
||||
{
|
||||
public byte[] X { get; set; }
|
||||
public byte[] Y { get; set; }
|
||||
|
||||
public byte[] ToCose()
|
||||
{
|
||||
var result = new CborWriter(CborConformanceMode.Ctap2Canonical);
|
||||
result.WriteStartMap(5);
|
||||
|
||||
// kty = EC2
|
||||
result.WriteInt32(1);
|
||||
result.WriteInt32(2);
|
||||
|
||||
// alg = ES256
|
||||
result.WriteInt32(3);
|
||||
result.WriteInt32(-7);
|
||||
|
||||
// crv = P-256
|
||||
result.WriteInt32(-1);
|
||||
result.WriteInt32(1);
|
||||
|
||||
// x
|
||||
result.WriteInt32(-2);
|
||||
result.WriteByteString(X);
|
||||
|
||||
// y
|
||||
result.WriteInt32(-3);
|
||||
result.WriteByteString(Y);
|
||||
|
||||
result.WriteEndMap();
|
||||
|
||||
return result.Encode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -228,20 +228,6 @@ namespace Bit.Core.Services
|
||||
return Task.FromResult(new Tuple<byte[], byte[]>(publicKey, privateKey));
|
||||
}
|
||||
|
||||
public Task<(byte[], byte[])> EcdsaGenerateKeyPairAsync(CryptoEcdsaAlgorithm algorithm)
|
||||
{
|
||||
if (algorithm != CryptoEcdsaAlgorithm.P256Sha256)
|
||||
{
|
||||
throw new ArgumentException("Unsupported algorithm.");
|
||||
}
|
||||
|
||||
var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.EcdsaP256Sha256);
|
||||
var cryptoKey = provider.CreateKeyPair(256);
|
||||
var publicKey = cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo);
|
||||
var privateKey = cryptoKey.Export(CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo);
|
||||
return Task.FromResult((publicKey, privateKey));
|
||||
}
|
||||
|
||||
public Task<byte[]> RandomBytesAsync(int length)
|
||||
{
|
||||
return Task.FromResult(CryptographicBuffer.GenerateRandom(length));
|
||||
|
@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
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;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -16,8 +14,6 @@ using Xunit;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Policy;
|
||||
using NSubstitute.Extensions;
|
||||
using System.Formats.Cbor;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
@ -183,8 +179,6 @@ namespace Bit.Core.Test.Services
|
||||
];
|
||||
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||
mParams.RequireUserVerification = false;
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(ciphers);
|
||||
sutProvider.GetDependency<IFido2UserInterface>().ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns(new Fido2ConfirmNewCredentialResult {
|
||||
CipherId = null,
|
||||
@ -220,8 +214,6 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
];
|
||||
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(ciphers);
|
||||
|
||||
// Arrange
|
||||
@ -249,8 +241,6 @@ namespace Bit.Core.Test.Services
|
||||
];
|
||||
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||
mParams.RequireUserVerification = false;
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||
_encryptedCipher.Key = null;
|
||||
_encryptedCipher.Attachments = [];
|
||||
|
||||
@ -297,8 +287,6 @@ namespace Bit.Core.Test.Services
|
||||
];
|
||||
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||
mParams.RequireUserVerification = false;
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICipherService>().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher);
|
||||
@ -324,8 +312,6 @@ namespace Bit.Core.Test.Services
|
||||
];
|
||||
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||
mParams.RequireUserVerification = true;
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICipherService>().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher);
|
||||
@ -351,8 +337,6 @@ namespace Bit.Core.Test.Services
|
||||
];
|
||||
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||
mParams.RequireUserVerification = false;
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||
_encryptedCipher.Reprompt = CipherRepromptType.Password;
|
||||
|
||||
// Arrange
|
||||
@ -379,8 +363,6 @@ namespace Bit.Core.Test.Services
|
||||
];
|
||||
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||
mParams.RequireUserVerification = false;
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICipherService>().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher);
|
||||
@ -407,8 +389,6 @@ namespace Bit.Core.Test.Services
|
||||
];
|
||||
mParams.RpEntity = new PublicKeyCredentialRpEntity { Id = "bitwarden.com" };
|
||||
mParams.RequireUserVerification = false;
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().EcdsaGenerateKeyPairAsync(Arg.Any<CryptoEcdsaAlgorithm>())
|
||||
.Returns((RandomBytes(32), RandomBytes(32)));
|
||||
_encryptedCipher.Key = null;
|
||||
_encryptedCipher.Attachments = [];
|
||||
|
||||
@ -416,7 +396,6 @@ namespace Bit.Core.Test.Services
|
||||
var rpIdHashMock = RandomBytes(32);
|
||||
mParams.RequireResidentKey = false;
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().HashAsync(mParams.RpEntity.Id, CryptoHashAlgorithm.Sha256).Returns(rpIdHashMock);
|
||||
// sutProvider.GetDependency<ICipherService>().EncryptAsync(Arg.Any<CipherView>()).Returns(_encryptedCipher);
|
||||
sutProvider.GetDependency<ICipherService>().GetAsync(Arg.Is(_encryptedCipher.Id)).Returns(_encryptedCipher);
|
||||
sutProvider.GetDependency<IFido2UserInterface>().ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns(new Fido2ConfirmNewCredentialResult {
|
||||
CipherId = _encryptedCipher.Id,
|
||||
@ -446,14 +425,13 @@ namespace Bit.Core.Test.Services
|
||||
// Unsure how to test public key
|
||||
// const publicKey = authData.Skip(71).ToArray(); // Key data is 77 bytes long
|
||||
|
||||
// Not implemented yet
|
||||
// Assert.Equal(71 + 77, authData.Length);
|
||||
// Assert.Equal(rpIdHashMock, rpIdHash);
|
||||
// Assert.Equal([0b01000001], flags); // UP = true, AD = true
|
||||
// Assert.Equal([0, 0, 0, 0], counter);
|
||||
// Assert.Equal(Fido2AuthenticatorService.AAGUID, aaguid);
|
||||
// Assert.Equal([0, 16], credentialIdLength); // 16 bytes because we're using GUIDs
|
||||
// Assert.Equal(credentialIdBytes, credentialId);
|
||||
Assert.Equal(71 + 77, authData.Length);
|
||||
Assert.Equal(rpIdHashMock, rpIdHash);
|
||||
Assert.Equal([0b01000001], flags); // UP = true, AD = true
|
||||
Assert.Equal([0, 0, 0, 0], counter);
|
||||
Assert.Equal(Fido2AuthenticatorService.AAGUID, aaguid);
|
||||
Assert.Equal([0, 16], credentialIdLength); // 16 bytes because we're using GUIDs
|
||||
Assert.Equal(credentialIdBytes, credentialId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -494,16 +472,19 @@ namespace Bit.Core.Test.Services
|
||||
};
|
||||
}
|
||||
|
||||
private class AttestationObject
|
||||
private struct AttestationObject
|
||||
{
|
||||
public string Fmt { get; set; }
|
||||
public object AttStmt { get; set; }
|
||||
public byte[] AuthData { get; set; }
|
||||
public string? Fmt { get; set; }
|
||||
public object? AttStmt { get; set; }
|
||||
public byte[]? AuthData { get; set; }
|
||||
}
|
||||
|
||||
private AttestationObject DecodeAttestationObject(byte[] attestationObject)
|
||||
{
|
||||
var result = new AttestationObject();
|
||||
string? fmt = null;
|
||||
object? attStmt = null;
|
||||
byte[]? authData = null;
|
||||
|
||||
var reader = new CborReader(attestationObject, CborConformanceMode.Ctap2Canonical);
|
||||
reader.ReadStartMap();
|
||||
|
||||
@ -513,21 +494,25 @@ namespace Bit.Core.Test.Services
|
||||
switch (key)
|
||||
{
|
||||
case "fmt":
|
||||
result.Fmt = reader.ReadTextString();
|
||||
fmt = reader.ReadTextString();
|
||||
break;
|
||||
case "attStmt":
|
||||
reader.ReadStartMap();
|
||||
reader.ReadEndMap();
|
||||
break;
|
||||
case "authData":
|
||||
result.AuthData = reader.ReadByteString();
|
||||
authData = reader.ReadByteString();
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown key");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return new AttestationObject {
|
||||
Fmt = fmt,
|
||||
AttStmt = attStmt,
|
||||
AuthData = authData
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user