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:
@@ -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 =
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export class Fido2KeyView {
|
export class Fido2KeyView {
|
||||||
id: string;
|
|
||||||
key: string;
|
key: string;
|
||||||
rpId: string;
|
rpId: string;
|
||||||
origin: string;
|
origin: string;
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user