mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 11:13:46 +00:00
[EC-598] feat: complete implementation of makeCredential
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import { CBOR } from "cbor-redux";
|
||||
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { CipherType } from "../../vault/enums/cipher-type";
|
||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||
import { CipherService } from "../../vault/services/cipher.service";
|
||||
@@ -12,6 +15,11 @@ import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface.
|
||||
import { Fido2Utils } from "../abstractions/fido2-utils";
|
||||
import { Fido2KeyView } from "../models/view/fido2-key.view";
|
||||
|
||||
// AAGUID: 6e8248d5-b479-40db-a3d8-11116f7e8349
|
||||
export const AAGUID = new Uint8Array([
|
||||
0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49,
|
||||
]);
|
||||
|
||||
const KeyUsages: KeyUsage[] = ["sign"];
|
||||
|
||||
/**
|
||||
@@ -24,7 +32,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
private userInterface: Fido2UserInterfaceService
|
||||
) {}
|
||||
|
||||
async makeCredential(params: Fido2AuthenticatorMakeCredentialsParams): Promise<void> {
|
||||
async makeCredential(params: Fido2AuthenticatorMakeCredentialsParams): Promise<Uint8Array> {
|
||||
if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) {
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported);
|
||||
}
|
||||
@@ -60,6 +68,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
|
||||
}
|
||||
|
||||
let cipher: CipherView;
|
||||
let keyPair: CryptoKeyPair;
|
||||
if (params.requireResidentKey) {
|
||||
const userVerification = await this.userInterface.confirmNewCredential({
|
||||
credentialName: params.rpEntity.name,
|
||||
@@ -71,14 +81,15 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPair = await this.createKeyPair();
|
||||
keyPair = await this.createKeyPair();
|
||||
|
||||
const cipher = new CipherView();
|
||||
cipher = new CipherView();
|
||||
cipher.type = CipherType.Fido2Key;
|
||||
cipher.name = params.rpEntity.name;
|
||||
cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey);
|
||||
const encrypted = await this.cipherService.encrypt(cipher);
|
||||
await this.cipherService.createWithServer(encrypted);
|
||||
await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here
|
||||
cipher.id = encrypted.id;
|
||||
} catch {
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
|
||||
}
|
||||
@@ -93,10 +104,10 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPair = await this.createKeyPair();
|
||||
keyPair = await this.createKeyPair();
|
||||
|
||||
const encrypted = await this.cipherService.get(cipherId);
|
||||
const cipher = await encrypted.decrypt();
|
||||
cipher = await encrypted.decrypt();
|
||||
cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey);
|
||||
const reencrypted = await this.cipherService.encrypt(cipher);
|
||||
await this.cipherService.updateWithServer(reencrypted);
|
||||
@@ -104,6 +115,23 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
const attestationObject = new Uint8Array(
|
||||
CBOR.encode({
|
||||
fmt: "none",
|
||||
attStmt: {},
|
||||
authData: await generateAuthData({
|
||||
rpId: params.rpEntity.id,
|
||||
credentialId: cipher.id,
|
||||
counter: cipher.fido2Key.counter,
|
||||
userPresence: true,
|
||||
userVerification: false,
|
||||
keyPair,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
return attestationObject;
|
||||
}
|
||||
|
||||
private async vaultContainsId(ids: string[]): Promise<boolean> {
|
||||
@@ -147,3 +175,100 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
return fido2Key;
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthDataParams {
|
||||
rpId: string;
|
||||
credentialId: string;
|
||||
userPresence: boolean;
|
||||
userVerification: boolean;
|
||||
counter: number;
|
||||
keyPair?: CryptoKeyPair;
|
||||
}
|
||||
|
||||
async function generateAuthData(params: AuthDataParams) {
|
||||
const authData: Array<number> = [];
|
||||
|
||||
const rpIdHash = new Uint8Array(
|
||||
await crypto.subtle.digest({ name: "SHA-256" }, Utils.fromByteStringToArray(params.rpId))
|
||||
);
|
||||
authData.push(...rpIdHash);
|
||||
|
||||
const flags = authDataFlags({
|
||||
extensionData: false,
|
||||
attestationData: false,
|
||||
userVerification: params.userVerification,
|
||||
userPresence: params.userPresence,
|
||||
});
|
||||
authData.push(flags);
|
||||
|
||||
// add 4 bytes of counter - we use time in epoch seconds as monotonic counter
|
||||
// TODO: Consider changing this to a cryptographically safe random number
|
||||
const counter = params.counter;
|
||||
authData.push(
|
||||
((counter & 0xff000000) >> 24) & 0xff,
|
||||
((counter & 0x00ff0000) >> 16) & 0xff,
|
||||
((counter & 0x0000ff00) >> 8) & 0xff,
|
||||
counter & 0x000000ff
|
||||
);
|
||||
|
||||
// attestedCredentialData
|
||||
const attestedCredentialData: Array<number> = [];
|
||||
|
||||
attestedCredentialData.push(...AAGUID);
|
||||
|
||||
// credentialIdLength (2 bytes) and credential Id
|
||||
const rawId = Utils.guidToRawFormat(params.credentialId);
|
||||
const credentialIdLength = [(rawId.length - (rawId.length & 0xff)) / 256, rawId.length & 0xff];
|
||||
attestedCredentialData.push(...credentialIdLength);
|
||||
attestedCredentialData.push(...rawId);
|
||||
|
||||
if (params.keyPair) {
|
||||
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);
|
||||
}
|
||||
|
||||
return new Uint8Array(authData);
|
||||
}
|
||||
|
||||
interface Flags {
|
||||
extensionData: boolean;
|
||||
attestationData: boolean;
|
||||
userVerification: boolean;
|
||||
userPresence: boolean;
|
||||
}
|
||||
|
||||
function authDataFlags(options: Flags): number {
|
||||
let flags = 0;
|
||||
|
||||
if (options.extensionData) {
|
||||
flags |= 0b1000000;
|
||||
}
|
||||
|
||||
if (options.attestationData) {
|
||||
flags |= 0b01000000;
|
||||
}
|
||||
|
||||
if (options.userVerification) {
|
||||
flags |= 0b00000100;
|
||||
}
|
||||
|
||||
if (options.userPresence) {
|
||||
flags |= 0b00000001;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user