1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[EC-598] feat: successfully store passkeys in vault

This commit is contained in:
Andreas Coroiu
2023-01-04 12:36:44 +01:00
parent cb07b2121e
commit f56971aa89
6 changed files with 113 additions and 30 deletions

View File

@@ -468,7 +468,7 @@ export default class MainBackground {
); );
this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.popupUtilsService); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.popupUtilsService);
this.fido2Service = new Fido2Service(this.fido2UserInterfaceService); this.fido2Service = new Fido2Service(this.fido2UserInterfaceService, this.cipherService);
const systemUtilsServiceReloadCallback = () => { const systemUtilsServiceReloadCallback = () => {
const forceWindowReload = const forceWindowReload =

View File

@@ -33,7 +33,7 @@ export abstract class CipherService {
updateLastUsedDate: (id: string) => Promise<void>; updateLastUsedDate: (id: string) => Promise<void>;
updateLastLaunchedDate: (id: string) => Promise<void>; updateLastLaunchedDate: (id: string) => Promise<void>;
saveNeverDomain: (domain: string) => Promise<void>; saveNeverDomain: (domain: string) => Promise<void>;
createWithServer: (cipher: Cipher) => Promise<any>; createWithServer: (cipher: Cipher) => Promise<Cipher>;
updateWithServer: (cipher: Cipher) => Promise<any>; updateWithServer: (cipher: Cipher) => Promise<any>;
shareWithServer: ( shareWithServer: (
cipher: CipherView, cipher: CipherView,

View File

@@ -1,6 +1,7 @@
import { CipherRepromptType } from "../../enums/cipherRepromptType"; import { CipherRepromptType } from "../../enums/cipherRepromptType";
import { CipherType } from "../../enums/cipherType"; import { CipherType } from "../../enums/cipherType";
import { CardApi } from "../api/card.api"; import { CardApi } from "../api/card.api";
import { Fido2KeyApi } from "../api/fido2-key.api";
import { FieldApi } from "../api/field.api"; import { FieldApi } from "../api/field.api";
import { IdentityApi } from "../api/identity.api"; import { IdentityApi } from "../api/identity.api";
import { LoginUriApi } from "../api/login-uri.api"; import { LoginUriApi } from "../api/login-uri.api";
@@ -22,6 +23,7 @@ export class CipherRequest {
secureNote: SecureNoteApi; secureNote: SecureNoteApi;
card: CardApi; card: CardApi;
identity: IdentityApi; identity: IdentityApi;
fido2Key: Fido2KeyApi;
fields: FieldApi[]; fields: FieldApi[];
passwordHistory: PasswordHistoryRequest[]; passwordHistory: PasswordHistoryRequest[];
// Deprecated, remove at some point and rename attachments2 to attachments // Deprecated, remove at some point and rename attachments2 to attachments
@@ -121,6 +123,17 @@ export class CipherRequest {
? cipher.identity.licenseNumber.encryptedString ? cipher.identity.licenseNumber.encryptedString
: null; : null;
break; break;
case CipherType.Fido2Key:
this.fido2Key = new Fido2KeyApi();
this.fido2Key.key =
cipher.fido2Key.key != null ? cipher.fido2Key.key.encryptedString : null;
this.fido2Key.origin =
cipher.fido2Key.origin != null ? cipher.fido2Key.origin.encryptedString : null;
this.fido2Key.rpId =
cipher.fido2Key.rpId != null ? cipher.fido2Key.rpId.encryptedString : null;
this.fido2Key.userHandle =
cipher.fido2Key.userHandle != null ? cipher.fido2Key.userHandle.encryptedString : null;
break;
default: default:
break; break;
} }

View File

@@ -1,5 +1,4 @@
export class Fido2KeyView { export class Fido2KeyView {
id: string;
key: string; key: string;
rpId: string; rpId: string;
origin: string; origin: string;

View File

@@ -23,6 +23,7 @@ import { Cipher } from "../models/domain/cipher";
import Domain from "../models/domain/domain-base"; import Domain from "../models/domain/domain-base";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { Fido2Key } from "../models/domain/fido2-key";
import { Field } from "../models/domain/field"; import { Field } from "../models/domain/field";
import { Identity } from "../models/domain/identity"; import { Identity } from "../models/domain/identity";
import { Login } from "../models/domain/login"; import { Login } from "../models/domain/login";
@@ -1244,6 +1245,20 @@ export class CipherService implements CipherServiceAbstraction {
key key
); );
return; return;
case CipherType.Fido2Key:
cipher.fido2Key = new Fido2Key();
await this.encryptObjProperty(
model.fido2Key,
cipher.fido2Key,
{
key: null,
rpId: null,
origin: null,
userHandle: null,
},
key
);
break;
default: default:
throw new Error("Unknown cipher type."); throw new Error("Unknown cipher type.");
} }

View File

@@ -1,5 +1,6 @@
import { CBOR } from "cbor-redux"; import { CBOR } from "cbor-redux";
import { CipherService } from "../../abstractions/cipher.service";
import { Fido2UserInterfaceService } from "../../abstractions/fido2/fido2-user-interface.service.abstraction"; import { Fido2UserInterfaceService } from "../../abstractions/fido2/fido2-user-interface.service.abstraction";
import { Fido2Utils } from "../../abstractions/fido2/fido2-utils"; import { Fido2Utils } from "../../abstractions/fido2/fido2-utils";
import { import {
@@ -11,7 +12,11 @@ import {
NoCredentialFoundError, NoCredentialFoundError,
OriginMismatchError, OriginMismatchError,
} from "../../abstractions/fido2/fido2.service.abstraction"; } from "../../abstractions/fido2/fido2.service.abstraction";
import { CipherType } from "../../enums/cipherType";
import { Utils } from "../../misc/utils"; import { Utils } from "../../misc/utils";
import { Cipher } from "../../models/domain/cipher";
import { CipherView } from "../../models/view/cipher.view";
import { Fido2KeyView } from "../../models/view/fido2-key.view";
import { CredentialId } from "./credential-id"; import { CredentialId } from "./credential-id";
import { joseToDer } from "./ecdsa-utils"; import { joseToDer } from "./ecdsa-utils";
@@ -20,16 +25,21 @@ const STANDARD_ATTESTATION_FORMAT = "packed";
interface BitCredential { interface BitCredential {
credentialId: CredentialId; credentialId: CredentialId;
keyPair: CryptoKeyPair; privateKey: CryptoKey;
rpId: string; rpId: string;
origin: string; origin: string;
userHandle: Uint8Array; userHandle: Uint8Array;
} }
const KeyUsages: KeyUsage[] = ["sign"];
export class Fido2Service implements Fido2ServiceAbstraction { export class Fido2Service implements Fido2ServiceAbstraction {
private credentials = new Map<string, BitCredential>(); private credentials = new Map<string, BitCredential>();
constructor(private fido2UserInterfaceService: Fido2UserInterfaceService) {} constructor(
private fido2UserInterfaceService: Fido2UserInterfaceService,
private cipherService: CipherService
) {}
async createCredential( async createCredential(
params: CredentialRegistrationParams params: CredentialRegistrationParams
@@ -40,7 +50,6 @@ export class Fido2Service implements Fido2ServiceAbstraction {
const attestationFormat = STANDARD_ATTESTATION_FORMAT; const attestationFormat = STANDARD_ATTESTATION_FORMAT;
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const credentialId = new CredentialId(Utils.newGuid());
const clientData = encoder.encode( const clientData = encoder.encode(
JSON.stringify({ JSON.stringify({
@@ -55,14 +64,21 @@ export class Fido2Service implements Fido2ServiceAbstraction {
namedCurve: "P-256", namedCurve: "P-256",
}, },
true, true,
["sign", "verify"] KeyUsages
); );
const credentialId = await this.saveCredential({
privateKey: keyPair.privateKey,
origin: params.origin,
rpId: params.rp.id,
userHandle: Fido2Utils.stringToBuffer(params.user.id),
});
const authData = await generateAuthData({ const authData = await generateAuthData({
rpId: params.rp.id, rpId: params.rp.id,
credentialId, credentialId,
userPresence: presence, userPresence: presence,
userVerification: false, userVerification: true, // TODO: Change to false
keyPair, keyPair,
attestationFormat: STANDARD_ATTESTATION_FORMAT, attestationFormat: STANDARD_ATTESTATION_FORMAT,
}); });
@@ -70,7 +86,7 @@ export class Fido2Service implements Fido2ServiceAbstraction {
const asn1Der_signature = await generateSignature({ const asn1Der_signature = await generateSignature({
authData, authData,
clientData, clientData,
keyPair, privateKey: keyPair.privateKey,
}); });
const attestationObject = new Uint8Array( const attestationObject = new Uint8Array(
@@ -84,14 +100,6 @@ export class Fido2Service implements Fido2ServiceAbstraction {
}) })
); );
await this.saveCredential({
credentialId,
keyPair,
origin: params.origin,
rpId: params.rp.id,
userHandle: Fido2Utils.stringToBuffer(params.user.id),
});
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log("Fido2Service.createCredential => result", { console.log("Fido2Service.createCredential => result", {
credentialId: Fido2Utils.bufferToString(credentialId.raw), credentialId: Fido2Utils.bufferToString(credentialId.raw),
@@ -111,7 +119,8 @@ export class Fido2Service implements Fido2ServiceAbstraction {
if (params.allowedCredentialIds && params.allowedCredentialIds.length > 0) { if (params.allowedCredentialIds && params.allowedCredentialIds.length > 0) {
// We're looking for regular non-resident keys // We're looking for regular non-resident keys
credential = this.getCredential(params.allowedCredentialIds); credential = await this.getCredential(params.allowedCredentialIds);
console.log("Found credential: ", credential);
} else { } else {
// We're looking for a resident key // We're looking for a resident key
credential = this.getCredentialByRp(params.rpId); credential = this.getCredentialByRp(params.rpId);
@@ -140,13 +149,13 @@ export class Fido2Service implements Fido2ServiceAbstraction {
credentialId: credential.credentialId, credentialId: credential.credentialId,
rpId: params.rpId, rpId: params.rpId,
userPresence: presence, userPresence: presence,
userVerification: false, userVerification: true, // TODO: Change to false!
}); });
const signature = await generateSignature({ const signature = await generateSignature({
authData, authData,
clientData, clientData,
keyPair: credential.keyPair, privateKey: credential.privateKey,
}); });
return { return {
@@ -158,20 +167,67 @@ export class Fido2Service implements Fido2ServiceAbstraction {
}; };
} }
private getCredential(allowedCredentialIds: string[]): BitCredential | undefined { private async getCredential(allowedCredentialIds: string[]): Promise<BitCredential | undefined> {
let credential: BitCredential | undefined; let cipher: Cipher | undefined;
for (const allowedCredential of allowedCredentialIds) { for (const allowedCredential of allowedCredentialIds) {
const id = new CredentialId(allowedCredential); cipher = await this.cipherService.get(allowedCredential);
if (this.credentials.has(id.encoded)) {
credential = this.credentials.get(id.encoded); if (cipher != undefined) {
break; break;
} }
} }
return credential;
if (cipher == null) {
return undefined;
}
const cipherView = await cipher.decrypt();
const keyBuffer = Fido2Utils.stringToBuffer(cipherView.fido2Key.key);
let privateKey;
try {
privateKey = await crypto.subtle.importKey(
"pkcs8",
keyBuffer,
{
name: "ECDSA",
namedCurve: "P-256",
},
true,
KeyUsages
);
} catch (err) {
console.log("Error importing key", { err });
throw err;
}
return {
credentialId: new CredentialId(cipherView.id),
privateKey,
origin: cipherView.fido2Key.origin,
rpId: cipherView.fido2Key.rpId,
userHandle: Fido2Utils.stringToBuffer(cipherView.fido2Key.userHandle),
};
} }
private async saveCredential(credential: BitCredential): Promise<void> { private async saveCredential(
this.credentials.set(credential.credentialId.encoded, credential); credential: Omit<BitCredential, "credentialId">
): Promise<CredentialId> {
const pcks8Key = await crypto.subtle.exportKey("pkcs8", credential.privateKey);
const view = new CipherView();
view.type = CipherType.Fido2Key;
view.name = credential.origin;
view.fido2Key = new Fido2KeyView();
view.fido2Key.key = Fido2Utils.bufferToString(pcks8Key);
view.fido2Key.origin = credential.origin;
view.fido2Key.rpId = credential.rpId;
view.fido2Key.userHandle = Fido2Utils.bufferToString(credential.userHandle);
const cipher = await this.cipherService.encrypt(view);
await this.cipherService.createWithServer(cipher);
// TODO: Cipher service modifies supplied object, we might wanna change that.
return new CredentialId(cipher.id);
} }
private getCredentialByRp(rpId: string): BitCredential | undefined { private getCredentialByRp(rpId: string): BitCredential | undefined {
@@ -267,7 +323,7 @@ async function generateAuthData(params: AuthDataParams) {
interface SignatureParams { interface SignatureParams {
authData: Uint8Array; authData: Uint8Array;
clientData: Uint8Array; clientData: Uint8Array;
keyPair: CryptoKeyPair; privateKey: CryptoKey;
} }
async function generateSignature(params: SignatureParams) { async function generateSignature(params: SignatureParams) {
@@ -279,7 +335,7 @@ async function generateSignature(params: SignatureParams) {
name: "ECDSA", name: "ECDSA",
hash: { name: "SHA-256" }, hash: { name: "SHA-256" },
}, },
params.keyPair.privateKey, params.privateKey,
sigBase sigBase
) )
); );