Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-3807] Store passkeys as array #6288

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ <h2 class="box-header">
</div>

<!--Passkey-->
<div class="box" *ngIf="cipher.login.fido2Key && !cloneMode">
<div class="box" *ngIf="cipher.login.fido2Keys[0] && !cloneMode">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume fido2keys will always be an array and never null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The view and the domain class will always have that field defined yes

<div class="box-content">
<div class="box-content-row text-muted">
<span class="row-label">{{ "typePasskey" | i18n }}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ <h2 class="box-header">
</div>

<!--Passkey-->
<div class="box" *ngIf="cipher.login.fido2Key">
<div class="box" *ngIf="cipher.login.fido2Keys[0]">
<div class="box-content">
<div class="box-content-row text-muted">
<span class="row-label">{{ "typePasskey" | i18n }}</span>
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/vault/app/vault/add-edit.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ <h2 class="box-header">
<!--Passkey-->
<div
class="box-content-row text-muted"
*ngIf="cipher.login.fido2Key && !cloneMode"
*ngIf="cipher.login.fido2Keys[0] && !cloneMode"
appBoxRow
>
<span class="row-label">{{ "typePasskey" | i18n }}</span>
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/vault/app/vault/view.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ <h2 class="box-header">
</div>
</div>
<!--Passkey-->
<div class="box-content-row text-muted" *ngIf="cipher.login.fido2Key">
<div class="box-content-row text-muted" *ngIf="cipher.login.fido2Keys[0]">
<span class="row-label">{{ "typePasskey" | i18n }}</span>
{{ "passkeyTwoStepLogin" | i18n }}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ <h1 class="modal-title" id="cipherAddEditTitle">{{ title }}</h1>
</button>
</div>
</div>
<ng-container *ngIf="cipher.login.fido2Key">
<ng-container *ngIf="cipher.login.fido2Keys[0]">
<div class="row">
<div class="col-6 form-group">
<label for="loginFido2key">{{ "typePasskey" | i18n }}</label>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/vault/individual-vault/vault.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
6 changes: 3 additions & 3 deletions libs/angular/src/components/share.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,15 @@ export class ShareComponent implements OnInit, OnDestroy {
}

private async checkFido2KeyExistsInOrg(cipher: CipherView, orgId: string): Promise<boolean> {
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 &&
Expand Down
4 changes: 2 additions & 2 deletions libs/angular/src/vault/components/add-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
}
}

Expand Down
2 changes: 1 addition & 1 deletion libs/angular/src/vault/components/view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
10 changes: 6 additions & 4 deletions libs/common/src/models/api/login.api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { JsonObject } from "type-fest";

import { Fido2KeyApi } from "../../vault/api/fido2-key.api";
import { BaseResponse } from "../response/base.response";

Expand All @@ -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);
Expand All @@ -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));
}
}
}
12 changes: 6 additions & 6 deletions libs/common/src/models/export/login.export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions libs/common/src/vault/models/data/login.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class LoginData {
passwordRevisionDate: string;
totp: string;
autofillOnPageLoad: boolean;
fido2Key?: Fido2KeyData;
fido2Keys?: Fido2KeyData[];

constructor(data?: LoginApi) {
if (data == null) {
Expand All @@ -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));
}
}
}
2 changes: 2 additions & 0 deletions libs/common/src/vault/models/domain/cipher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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: [
{
Expand Down
108 changes: 81 additions & 27 deletions libs/common/src/vault/models/domain/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -20,17 +25,20 @@ 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",
password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
totp: "123",
autofillOnPageLoad: false,
fido2Keys: [fido2KeyData],
};
const login = new Login(data);

Expand All @@ -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 () => {
Expand All @@ -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({
Expand All @@ -81,6 +94,7 @@ describe("Login DTO", () => {
},
],
autofillOnPageLoad: true,
fido2Keys: [decryptedFido2Key],
});
});

Expand All @@ -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);

Expand All @@ -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({
Expand All @@ -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);
});
Expand All @@ -155,3 +174,38 @@ describe("Login DTO", () => {
});
});
});

type Fido2KeyLike = Fido2KeyData | Fido2KeyView | Fido2KeyApi;
function initializeFido2Key<T extends Fido2KeyLike>(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;
}
Loading
Loading