diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.html b/apps/browser/src/vault/popup/components/vault/add-edit.component.html index f88e2b8ee6a..7b4b1204479 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.html +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.html @@ -134,7 +134,7 @@ -
+
{{ "typePasskey" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault/view.component.html b/apps/browser/src/vault/popup/components/vault/view.component.html index 0748a23b8d4..7428e5640a8 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.html +++ b/apps/browser/src/vault/popup/components/vault/view.component.html @@ -203,7 +203,7 @@
-
+
{{ "typePasskey" | i18n }} diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.html b/apps/desktop/src/vault/app/vault/add-edit.component.html index c81e2061c02..f5aeb1658fd 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.html +++ b/apps/desktop/src/vault/app/vault/add-edit.component.html @@ -117,7 +117,7 @@
{{ "typePasskey" | i18n }} diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html index 3a8e935758c..ad953246aa0 100644 --- a/apps/desktop/src/vault/app/vault/view.component.html +++ b/apps/desktop/src/vault/app/vault/view.component.html @@ -119,7 +119,7 @@
-
+
{{ "typePasskey" | i18n }} {{ "passkeyTwoStepLogin" | i18n }}
diff --git a/apps/web/src/app/vault/individual-vault/add-edit.component.html b/apps/web/src/app/vault/individual-vault/add-edit.component.html index 1d4d7522ac2..8390f74cb42 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit.component.html +++ b/apps/web/src/app/vault/individual-vault/add-edit.component.html @@ -191,7 +191,7 @@
- +
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 6a4e66d6828..139fce27669 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -669,7 +669,7 @@ export class VaultComponent implements OnInit, OnDestroy { } async cloneCipher(cipher: CipherView) { - if (cipher.login?.fido2Key) { + if (cipher.login?.fido2Keys.length > 0) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "passkeyNotCopied" }, content: { key: "passkeyNotCopiedAlert" }, diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 3bfaa5e5354..ee2b4348c5a 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -140,15 +140,15 @@ export class ShareComponent implements OnInit, OnDestroy { } private async checkFido2KeyExistsInOrg(cipher: CipherView, orgId: string): Promise { - if (cipher.type === CipherType.Fido2Key || cipher.login?.fido2Key) { + if (cipher.type === CipherType.Fido2Key || cipher.login?.fido2Keys[0]) { //Determine if Fido2Key object is disvoverable or non discoverable - const newFido2Key = cipher.login?.fido2Key || cipher.fido2Key; + const newFido2Key = cipher.login?.fido2Keys[0] || cipher.fido2Key; const ciphers = await this.cipherService.getAllDecrypted(); const exisitingOrgCiphers = ciphers.filter((c) => c.organizationId === orgId); return exisitingOrgCiphers.some((c) => { - const existingFido2key = c.login?.fido2Key || c.fido2Key; + const existingFido2key = c.login?.fido2Keys[0] || c.fido2Key; return ( !c.isDeleted && diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index cab452ff77e..665071d462c 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -323,8 +323,8 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.cloneMode) { this.cipher.id = null; - if (this.cipher.type === CipherType.Login && this.cipher.login.fido2Key) { - this.cipher.login.fido2Key = null; + if (this.cipher.type === CipherType.Login && this.cipher.login.fido2Keys.length > 0) { + this.cipher.login.fido2Keys = []; } } diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 53e3e840afc..75d2f500891 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -155,7 +155,7 @@ export class ViewComponent implements OnDestroy, OnInit { } async clone() { - if (this.cipher.login?.fido2Key) { + if (this.cipher.login?.fido2Keys.length > 0) { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "passkeyNotCopied" }, content: { key: "passkeyNotCopiedAlert" }, diff --git a/libs/common/src/models/api/login.api.ts b/libs/common/src/models/api/login.api.ts index da7522536a4..600a3f21daf 100644 --- a/libs/common/src/models/api/login.api.ts +++ b/libs/common/src/models/api/login.api.ts @@ -1,3 +1,5 @@ +import { JsonObject } from "type-fest"; + import { Fido2KeyApi } from "../../vault/api/fido2-key.api"; import { BaseResponse } from "../response/base.response"; @@ -10,7 +12,7 @@ export class LoginApi extends BaseResponse { passwordRevisionDate: string; totp: string; autofillOnPageLoad: boolean; - fido2Key?: Fido2KeyApi; + fido2Keys?: Fido2KeyApi[]; constructor(data: any = null) { super(data); @@ -28,9 +30,9 @@ export class LoginApi extends BaseResponse { this.uris = uris.map((u: any) => new LoginUriApi(u)); } - const fido2Key = this.getResponseProperty("Fido2Key"); - if (fido2Key != null) { - this.fido2Key = new Fido2KeyApi(fido2Key); + const fido2Keys = this.getResponseProperty("Fido2Keys"); + if (fido2Keys != null) { + this.fido2Keys = fido2Keys.map((key: JsonObject) => new Fido2KeyApi(key)); } } } diff --git a/libs/common/src/models/export/login.export.ts b/libs/common/src/models/export/login.export.ts index 0e26def1fee..6fa663171dc 100644 --- a/libs/common/src/models/export/login.export.ts +++ b/libs/common/src/models/export/login.export.ts @@ -12,7 +12,7 @@ export class LoginExport { req.username = "jdoe"; req.password = "myp@ssword123"; req.totp = "JBSWY3DPEHPK3PXP"; - req.fido2Key = Fido2KeyExport.template(); + req.fido2Keys = [Fido2KeyExport.template()]; return req; } @@ -23,8 +23,8 @@ export class LoginExport { view.username = req.username; view.password = req.password; view.totp = req.totp; - if (req.fido2Key != null) { - view.fido2Key = Fido2KeyExport.toView(req.fido2Key); + if (req.fido2Keys != null) { + view.fido2Keys = req.fido2Keys.map((key) => Fido2KeyExport.toView(key)); } return view; } @@ -44,7 +44,7 @@ export class LoginExport { username: string; password: string; totp: string; - fido2Key: Fido2KeyExport = null; + fido2Keys: Fido2KeyExport[] = []; constructor(o?: LoginView | LoginDomain) { if (o == null) { @@ -59,8 +59,8 @@ export class LoginExport { } } - if (o.fido2Key != null) { - this.fido2Key = new Fido2KeyExport(o.fido2Key); + if (o.fido2Keys != null) { + this.fido2Keys = o.fido2Keys.map((key) => new Fido2KeyExport(key)); } if (o instanceof LoginView) { diff --git a/libs/common/src/vault/models/data/login.data.ts b/libs/common/src/vault/models/data/login.data.ts index 5108989ef5a..b3f27e6773b 100644 --- a/libs/common/src/vault/models/data/login.data.ts +++ b/libs/common/src/vault/models/data/login.data.ts @@ -10,7 +10,7 @@ export class LoginData { passwordRevisionDate: string; totp: string; autofillOnPageLoad: boolean; - fido2Key?: Fido2KeyData; + fido2Keys?: Fido2KeyData[]; constructor(data?: LoginApi) { if (data == null) { @@ -27,8 +27,8 @@ export class LoginData { this.uris = data.uris.map((u) => new LoginUriData(u)); } - if (data.fido2Key) { - this.fido2Key = new Fido2KeyData(data.fido2Key); + if (data.fido2Keys) { + this.fido2Keys = data.fido2Keys?.map((key) => new Fido2KeyData(key)); } } } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index d5c141487a2..b4d50c99829 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -76,6 +76,7 @@ describe("Cipher DTO", () => { passwordRevisionDate: "2022-01-31T12:00:00.000Z", totp: "EncryptedString", autofillOnPageLoad: false, + fido2Keys: [], }, passwordHistory: [ { password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" }, @@ -143,6 +144,7 @@ describe("Cipher DTO", () => { password: { encryptedString: "EncryptedString", encryptionType: 0 }, totp: { encryptedString: "EncryptedString", encryptionType: 0 }, uris: [{ match: 0, uri: { encryptedString: "EncryptedString", encryptionType: 0 } }], + fido2Keys: [], }, attachments: [ { diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts index f2ec3dbf965..b86573d80e6 100644 --- a/libs/common/src/vault/models/domain/login.spec.ts +++ b/libs/common/src/vault/models/domain/login.spec.ts @@ -4,10 +4,15 @@ import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { mockEnc, mockFromJson } from "../../../../spec"; import { UriMatchType } from "../../../enums"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; +import { Fido2KeyApi } from "../../api/fido2-key.api"; import { LoginData } from "../../models/data/login.data"; import { Login } from "../../models/domain/login"; import { LoginUri } from "../../models/domain/login-uri"; import { LoginUriView } from "../../models/view/login-uri.view"; +import { Fido2KeyData } from "../data/fido2-key.data"; +import { Fido2KeyView } from "../view/fido2-key.view"; + +import { Fido2Key } from "./fido2-key"; describe("Login DTO", () => { it("Convert from empty LoginData", () => { @@ -20,10 +25,12 @@ describe("Login DTO", () => { username: null, password: null, totp: null, + fido2Keys: [], }); }); it("Convert from full LoginData", () => { + const fido2KeyData = initializeFido2Key(new Fido2KeyData()); const data: LoginData = { uris: [{ uri: "uri", match: UriMatchType.Domain }], username: "username", @@ -31,6 +38,7 @@ describe("Login DTO", () => { passwordRevisionDate: "2022-01-31T12:00:00.000Z", totp: "123", autofillOnPageLoad: false, + fido2Keys: [fido2KeyData], }; const login = new Login(data); @@ -41,13 +49,16 @@ describe("Login DTO", () => { password: { encryptedString: "password", encryptionType: 0 }, totp: { encryptedString: "123", encryptionType: 0 }, uris: [{ match: 0, uri: { encryptedString: "uri", encryptionType: 0 } }], + fido2Keys: [encryptFido2Key(fido2KeyData)], }); }); it("Initialize without LoginData", () => { const login = new Login(); - expect(login).toEqual({}); + expect(login).toEqual({ + fido2Keys: [], + }); }); it("Decrypts correctly", async () => { @@ -57,12 +68,14 @@ describe("Login DTO", () => { loginUri.decrypt(Arg.any()).resolves(loginUriView); const login = new Login(); + const decryptedFido2Key = Symbol(); login.uris = [loginUri]; login.username = mockEnc("encrypted username"); login.password = mockEnc("encrypted password"); login.passwordRevisionDate = new Date("2022-01-31T12:00:00.000Z"); login.totp = mockEnc("encrypted totp"); login.autofillOnPageLoad = true; + login.fido2Keys = [{ decrypt: jest.fn().mockReturnValue(decryptedFido2Key) } as any]; const loginView = await login.decrypt(null); expect(loginView).toEqual({ @@ -81,6 +94,7 @@ describe("Login DTO", () => { }, ], autofillOnPageLoad: true, + fido2Keys: [decryptedFido2Key], }); }); @@ -92,6 +106,7 @@ describe("Login DTO", () => { passwordRevisionDate: "2022-01-31T12:00:00.000Z", totp: "123", autofillOnPageLoad: false, + fido2Keys: [initializeFido2Key(new Fido2KeyData())], }; const login = new Login(data); @@ -112,19 +127,21 @@ describe("Login DTO", () => { password: "myPassword" as EncryptedString, passwordRevisionDate: passwordRevisionDate.toISOString(), totp: "myTotp" as EncryptedString, - fido2Key: { - credentialId: "keyId" as EncryptedString, - keyType: "keyType" as EncryptedString, - keyAlgorithm: "keyAlgorithm" as EncryptedString, - keyCurve: "keyCurve" as EncryptedString, - keyValue: "keyValue" as EncryptedString, - rpId: "rpId" as EncryptedString, - userHandle: "userHandle" as EncryptedString, - counter: "counter" as EncryptedString, - rpName: "rpName" as EncryptedString, - userDisplayName: "userDisplayName" as EncryptedString, - discoverable: "discoverable" as EncryptedString, - }, + fido2Keys: [ + { + credentialId: "keyId" as EncryptedString, + keyType: "keyType" as EncryptedString, + keyAlgorithm: "keyAlgorithm" as EncryptedString, + keyCurve: "keyCurve" as EncryptedString, + keyValue: "keyValue" as EncryptedString, + rpId: "rpId" as EncryptedString, + userHandle: "userHandle" as EncryptedString, + counter: "counter" as EncryptedString, + rpName: "rpName" as EncryptedString, + userDisplayName: "userDisplayName" as EncryptedString, + discoverable: "discoverable" as EncryptedString, + }, + ], }); expect(actual).toEqual({ @@ -133,19 +150,21 @@ describe("Login DTO", () => { password: "myPassword_fromJSON", passwordRevisionDate: passwordRevisionDate, totp: "myTotp_fromJSON", - fido2Key: { - credentialId: "keyId_fromJSON", - keyType: "keyType_fromJSON", - keyAlgorithm: "keyAlgorithm_fromJSON", - keyCurve: "keyCurve_fromJSON", - keyValue: "keyValue_fromJSON", - rpId: "rpId_fromJSON", - userHandle: "userHandle_fromJSON", - counter: "counter_fromJSON", - rpName: "rpName_fromJSON", - userDisplayName: "userDisplayName_fromJSON", - discoverable: "discoverable_fromJSON", - }, + fido2Keys: [ + { + credentialId: "keyId_fromJSON", + keyType: "keyType_fromJSON", + keyAlgorithm: "keyAlgorithm_fromJSON", + keyCurve: "keyCurve_fromJSON", + keyValue: "keyValue_fromJSON", + rpId: "rpId_fromJSON", + userHandle: "userHandle_fromJSON", + counter: "counter_fromJSON", + rpName: "rpName_fromJSON", + userDisplayName: "userDisplayName_fromJSON", + discoverable: "discoverable_fromJSON", + }, + ], }); expect(actual).toBeInstanceOf(Login); }); @@ -155,3 +174,38 @@ describe("Login DTO", () => { }); }); }); + +type Fido2KeyLike = Fido2KeyData | Fido2KeyView | Fido2KeyApi; +function initializeFido2Key(key: T): T { + key.credentialId = "credentialId"; + key.keyType = "public-key"; + key.keyAlgorithm = "ECDSA"; + key.keyCurve = "P-256"; + key.keyValue = "keyValue"; + key.rpId = "rpId"; + key.userHandle = "userHandle"; + key.counter = "counter"; + key.rpName = "rpName"; + key.userDisplayName = "userDisplayName"; + key.discoverable = "discoverable"; + return key; +} + +function encryptFido2Key(key: Fido2KeyLike): Fido2Key { + const encrypted = new Fido2Key(); + encrypted.credentialId = { encryptedString: key.credentialId, encryptionType: 0 } as EncString; + encrypted.keyType = { encryptedString: key.keyType, encryptionType: 0 } as EncString; + encrypted.keyAlgorithm = { encryptedString: key.keyAlgorithm, encryptionType: 0 } as EncString; + encrypted.keyCurve = { encryptedString: key.keyCurve, encryptionType: 0 } as EncString; + encrypted.keyValue = { encryptedString: key.keyValue, encryptionType: 0 } as EncString; + encrypted.rpId = { encryptedString: key.rpId, encryptionType: 0 } as EncString; + encrypted.userHandle = { encryptedString: key.userHandle, encryptionType: 0 } as EncString; + encrypted.counter = { encryptedString: key.counter, encryptionType: 0 } as EncString; + encrypted.rpName = { encryptedString: key.rpName, encryptionType: 0 } as EncString; + encrypted.userDisplayName = { + encryptedString: key.userDisplayName, + encryptionType: 0, + } as EncString; + encrypted.discoverable = { encryptedString: key.discoverable, encryptionType: 0 } as EncString; + return encrypted; +} diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index c9f18151f2e..fe9ad15c75d 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -16,7 +16,7 @@ export class Login extends Domain { passwordRevisionDate?: Date; totp: EncString; autofillOnPageLoad: boolean; - fido2Key: Fido2Key; + fido2Keys: Fido2Key[] = []; constructor(obj?: LoginData) { super(); @@ -45,8 +45,8 @@ export class Login extends Domain { }); } - if (obj.fido2Key) { - this.fido2Key = new Fido2Key(obj.fido2Key); + if (obj.fido2Keys) { + this.fido2Keys = obj.fido2Keys.map((key) => new Fido2Key(key)); } } @@ -70,8 +70,8 @@ export class Login extends Domain { } } - if (this.fido2Key != null) { - view.fido2Key = await this.fido2Key.decrypt(orgId, encKey); + if (this.fido2Keys != null) { + view.fido2Keys = await Promise.all(this.fido2Keys.map((key) => key.decrypt(orgId, encKey))); } return view; @@ -95,9 +95,7 @@ export class Login extends Domain { }); } - if (this.fido2Key != null) { - l.fido2Key = this.fido2Key.toFido2KeyData(); - } + l.fido2Keys = this.fido2Keys.map((key) => key.toFido2KeyData()); return l; } @@ -113,7 +111,7 @@ export class Login extends Domain { const passwordRevisionDate = obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); const uris = obj.uris?.map((uri: any) => LoginUri.fromJSON(uri)); - const fido2Key = obj.fido2Key == null ? null : Fido2Key.fromJSON(obj.fido2Key); + const fido2Keys = obj.fido2Keys?.map((key) => Fido2Key.fromJSON(key)) ?? []; return Object.assign(new Login(), obj, { username, @@ -121,7 +119,7 @@ export class Login extends Domain { totp, passwordRevisionDate, uris, - fido2Key, + fido2Keys, }); } } diff --git a/libs/common/src/vault/models/request/cipher.request.ts b/libs/common/src/vault/models/request/cipher.request.ts index 8c27ec597b5..375a9d2d44d 100644 --- a/libs/common/src/vault/models/request/cipher.request.ts +++ b/libs/common/src/vault/models/request/cipher.request.ts @@ -63,50 +63,28 @@ export class CipherRequest { }); } - if (cipher.login.fido2Key != null) { - this.login.fido2Key = new Fido2KeyApi(); - this.login.fido2Key.credentialId = - cipher.login.fido2Key.credentialId != null - ? cipher.login.fido2Key.credentialId.encryptedString - : null; - this.login.fido2Key.keyType = - cipher.login.fido2Key.keyType != null - ? (cipher.login.fido2Key.keyType.encryptedString as "public-key") - : null; - this.login.fido2Key.keyAlgorithm = - cipher.login.fido2Key.keyAlgorithm != null - ? (cipher.login.fido2Key.keyAlgorithm.encryptedString as "ECDSA") - : null; - this.login.fido2Key.keyCurve = - cipher.login.fido2Key.keyCurve != null - ? (cipher.login.fido2Key.keyCurve.encryptedString as "P-256") - : null; - this.login.fido2Key.keyValue = - cipher.login.fido2Key.keyValue != null - ? cipher.login.fido2Key.keyValue.encryptedString - : null; - this.login.fido2Key.rpId = - cipher.login.fido2Key.rpId != null ? cipher.login.fido2Key.rpId.encryptedString : null; - this.login.fido2Key.rpName = - cipher.login.fido2Key.rpName != null - ? cipher.login.fido2Key.rpName.encryptedString - : null; - this.login.fido2Key.counter = - cipher.login.fido2Key.counter != null - ? cipher.login.fido2Key.counter.encryptedString - : null; - this.login.fido2Key.userHandle = - cipher.login.fido2Key.userHandle != null - ? cipher.login.fido2Key.userHandle.encryptedString - : null; - this.login.fido2Key.userDisplayName = - cipher.login.fido2Key.userDisplayName != null - ? cipher.login.fido2Key.userDisplayName.encryptedString - : null; - this.login.fido2Key.discoverable = - cipher.login.fido2Key.discoverable != null - ? cipher.login.fido2Key.discoverable.encryptedString - : null; + if (cipher.login.fido2Keys != null) { + this.login.fido2Keys = cipher.login.fido2Keys.map((key) => { + const keyApi = new Fido2KeyApi(); + keyApi.credentialId = + key.credentialId != null ? key.credentialId.encryptedString : null; + keyApi.keyType = + key.keyType != null ? (key.keyType.encryptedString as "public-key") : null; + keyApi.keyAlgorithm = + key.keyAlgorithm != null ? (key.keyAlgorithm.encryptedString as "ECDSA") : null; + keyApi.keyCurve = + key.keyCurve != null ? (key.keyCurve.encryptedString as "P-256") : null; + keyApi.keyValue = key.keyValue != null ? key.keyValue.encryptedString : null; + keyApi.rpId = key.rpId != null ? key.rpId.encryptedString : null; + keyApi.rpName = key.rpName != null ? key.rpName.encryptedString : null; + keyApi.counter = key.counter != null ? key.counter.encryptedString : null; + keyApi.userHandle = key.userHandle != null ? key.userHandle.encryptedString : null; + keyApi.userDisplayName = + key.userDisplayName != null ? key.userDisplayName.encryptedString : null; + keyApi.discoverable = + key.discoverable != null ? key.discoverable.encryptedString : null; + return keyApi; + }); } break; case CipherType.SecureNote: diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 86284151a3f..9518725ad12 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -19,7 +19,7 @@ export class LoginView extends ItemView { totp: string = null; uris: LoginUriView[] = null; autofillOnPageLoad: boolean = null; - fido2Key?: Fido2KeyView; + fido2Keys: Fido2KeyView[] = []; constructor(l?: Login) { super(); @@ -81,12 +81,12 @@ export class LoginView extends ItemView { const passwordRevisionDate = obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); const uris = obj.uris?.map((uri: any) => LoginUriView.fromJSON(uri)); - const fido2Key = obj.fido2Key == null ? null : Fido2KeyView.fromJSON(obj.fido2Key); + const fido2Keys = obj.fido2Keys?.map((key) => Fido2KeyView.fromJSON(key)) ?? []; return Object.assign(new LoginView(), obj, { passwordRevisionDate, uris, - fido2Key, + fido2Keys, }); } } diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index e1801c47899..05ab34ae5fc 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1089,32 +1089,34 @@ export class CipherService implements CipherServiceAbstraction { } } - if (model.login.fido2Key != null) { - cipher.login.fido2Key = new Fido2Key(); - await this.encryptObjProperty( - model.login.fido2Key, - cipher.login.fido2Key, - { - credentialId: null, - keyType: null, - keyAlgorithm: null, - keyCurve: null, - keyValue: null, - rpId: null, - rpName: null, - userHandle: null, - userDisplayName: null, - origin: null, - }, - key - ); - cipher.login.fido2Key.counter = await this.cryptoService.encrypt( - String(model.login.fido2Key.counter), - key - ); - cipher.login.fido2Key.discoverable = await this.cryptoService.encrypt( - String(model.login.fido2Key.discoverable), - key + if (model.login.fido2Keys != null) { + cipher.login.fido2Keys = await Promise.all( + model.login.fido2Keys.map(async (viewKey) => { + const domainKey = new Fido2Key(); + await this.encryptObjProperty( + viewKey, + domainKey, + { + credentialId: null, + keyType: null, + keyAlgorithm: null, + keyCurve: null, + keyValue: null, + rpId: null, + rpName: null, + userHandle: null, + userDisplayName: null, + origin: null, + }, + key + ); + domainKey.counter = await this.cryptoService.encrypt(String(viewKey.counter), key); + domainKey.discoverable = await this.cryptoService.encrypt( + String(viewKey.discoverable), + key + ); + return domainKey; + }) ); } return; diff --git a/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts index 49adf1a794f..0e030630d02 100644 --- a/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/vault/services/fido2/fido2-authenticator.service.spec.ts @@ -116,7 +116,7 @@ describe("FidoAuthenticatorService", () => { params = await createParams({ excludeCredentialDescriptorList: [ { - id: guidToRawFormat(excludedCipher.login.fido2Key.credentialId), + id: guidToRawFormat(excludedCipher.login.fido2Keys[0].credentialId), type: "public-key", }, ], @@ -237,18 +237,20 @@ describe("FidoAuthenticatorService", () => { name: existingCipher.name, login: expect.objectContaining({ - fido2Key: expect.objectContaining({ - credentialId: expect.anything(), - keyType: "public-key", - keyAlgorithm: "ECDSA", - keyCurve: "P-256", - rpId: params.rpEntity.id, - rpName: params.rpEntity.name, - userHandle: Fido2Utils.bufferToString(params.userEntity.id), - counter: 0, - userDisplayName: params.userEntity.displayName, - discoverable: false, - }), + fido2Keys: [ + expect.objectContaining({ + credentialId: expect.anything(), + keyType: "public-key", + keyAlgorithm: "ECDSA", + keyCurve: "P-256", + rpId: params.rpEntity.id, + rpName: params.rpEntity.name, + userHandle: Fido2Utils.bufferToString(params.userEntity.id), + counter: 0, + userDisplayName: params.userEntity.displayName, + discoverable: false, + }), + ], }), }) ); @@ -305,7 +307,7 @@ describe("FidoAuthenticatorService", () => { ); cipherService.getAllDecrypted.mockResolvedValue([await cipher]); cipherService.encrypt.mockImplementation(async (cipher) => { - cipher.login.fido2Key.credentialId = credentialId; // Replace id for testability + cipher.login.fido2Keys[0].credentialId = credentialId; // Replace id for testability return {} as any; }); cipherService.createWithServer.mockImplementation(async (cipher) => { @@ -467,8 +469,8 @@ describe("FidoAuthenticatorService", () => { it("should inform user if credential exists but rpId does not match", async () => { const cipher = await createCipherView({ type: CipherType.Login }); - cipher.login.fido2Key.credentialId = credentialId; - cipher.login.fido2Key.rpId = "mismatch-rpid"; + cipher.login.fido2Keys[0].credentialId = credentialId; + cipher.login.fido2Keys[0].rpId = "mismatch-rpid"; cipherService.getAllDecrypted.mockResolvedValue([cipher]); userInterfaceSession.informCredentialNotFound.mockResolvedValue(); @@ -543,7 +545,7 @@ describe("FidoAuthenticatorService", () => { it("should only ask for discoverable credentials matched by rpId when params does not contains allowedCredentials list", async () => { params.allowCredentialDescriptorList = undefined; - const discoverableCiphers = ciphers.filter((c) => c.login.fido2Key.discoverable); + const discoverableCiphers = ciphers.filter((c) => c.login.fido2Keys[0].discoverable); userInterfaceSession.pickCredential.mockResolvedValue({ cipherId: discoverableCiphers[0].id, userVerified: false, @@ -608,7 +610,7 @@ describe("FidoAuthenticatorService", () => { { credentialId: id, rpId: RpId, counter: 9000, keyValue } ) ); - fido2Keys = ciphers.map((c) => c.login.fido2Key); + fido2Keys = ciphers.map((c) => c.login.fido2Keys[0]); selectedCredentialId = credentialIds[0]; params = await createParams({ allowCredentialDescriptorList: credentialIds.map((credentialId) => ({ @@ -638,9 +640,11 @@ describe("FidoAuthenticatorService", () => { expect.objectContaining({ id: ciphers[0].id, login: expect.objectContaining({ - fido2Key: expect.objectContaining({ - counter: 9001, - }), + fido2Keys: [ + expect.objectContaining({ + counter: 9001, + }), + ], }), }) ); @@ -761,7 +765,7 @@ function createCipherView( if (cipher.type === CipherType.Login) { cipher.login = new LoginView(); - cipher.login.fido2Key = fido2KeyView; + cipher.login.fido2Keys = [fido2KeyView]; } else { cipher.fido2Key = fido2KeyView; } diff --git a/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts b/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts index 1f787c0c026..e322bf7e752 100644 --- a/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-authenticator.service.ts @@ -131,10 +131,11 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr const encrypted = await this.cipherService.get(cipherId); cipher = await encrypted.decrypt(); - cipher.login.fido2Key = fido2Key = await createKeyView(params, keyPair.privateKey); + fido2Key = await createKeyView(params, keyPair.privateKey); + cipher.login.fido2Keys = [fido2Key]; const reencrypted = await this.cipherService.encrypt(cipher); await this.cipherService.updateWithServer(reencrypted); - credentialId = cipher.login.fido2Key.credentialId; + credentialId = fido2Key.credentialId; } catch (error) { this.logService?.error( `[Fido2Authenticator] Aborting because of unknown error when creating credential: ${error}` @@ -233,10 +234,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr } try { - const selectedFido2Key = - selectedCipher.type === CipherType.Login - ? selectedCipher.login.fido2Key - : selectedCipher.fido2Key; + const selectedFido2Key = selectedCipher.login.fido2Keys[0]; const selectedCredentialId = selectedFido2Key.credentialId; ++selectedFido2Key.counter; @@ -310,8 +308,8 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr !cipher.isDeleted && cipher.organizationId == undefined && cipher.type === CipherType.Login && - cipher.login.fido2Key != undefined && - ids.includes(cipher.login.fido2Key.credentialId) + cipher.login.fido2Keys.length > 0 && + ids.includes(cipher.login.fido2Keys[0].credentialId) ) .map((cipher) => cipher.id); } @@ -343,9 +341,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr (cipher) => !cipher.isDeleted && cipher.type === CipherType.Login && - cipher.login.fido2Key != undefined && - cipher.login.fido2Key.rpId === rpId && - ids.includes(cipher.login.fido2Key.credentialId) + cipher.login.fido2Keys.length > 0 && + cipher.login.fido2Keys[0].rpId === rpId && + ids.includes(cipher.login.fido2Keys[0].credentialId) ); } @@ -360,9 +358,9 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr (cipher) => !cipher.isDeleted && cipher.type === CipherType.Login && - cipher.login.fido2Key != undefined && - cipher.login.fido2Key.rpId === rpId && - cipher.login.fido2Key.discoverable + cipher.login.fido2Keys.length > 0 && + cipher.login.fido2Keys[0].rpId === rpId && + cipher.login.fido2Keys[0].discoverable ); } }