From 8626e1d4e6a354de5babe4c446348dcae4850c04 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 5 Aug 2022 08:07:24 +1000 Subject: [PATCH] [EC-281] Add de/serialization methods to CipherView objects (#2970) --- .../localBackedSessionStorage.service.ts | 2 +- .../{ => models}/domain/attachment.spec.ts | 2 +- .../spec/{ => models}/domain/card.spec.ts | 2 +- .../spec/{ => models}/domain/cipher.spec.ts | 2 +- .../{ => models}/domain/collection.spec.ts | 2 +- .../domain/encArrayBuffer.spec.ts | 0 .../{ => models}/domain/encString.spec.ts | 0 .../spec/{ => models}/domain/field.spec.ts | 2 +- .../spec/{ => models}/domain/folder.spec.ts | 2 +- .../spec/{ => models}/domain/identity.spec.ts | 2 +- .../spec/{ => models}/domain/login.spec.ts | 2 +- .../spec/{ => models}/domain/loginUri.spec.ts | 2 +- .../spec/{ => models}/domain/password.spec.ts | 2 +- .../{ => models}/domain/secureNote.spec.ts | 0 .../spec/{ => models}/domain/send.spec.ts | 2 +- .../{ => models}/domain/sendAccess.spec.ts | 2 +- .../spec/{ => models}/domain/sendFile.spec.ts | 2 +- .../spec/{ => models}/domain/sendText.spec.ts | 2 +- .../domain/symmetricCryptoKey.spec.ts | 19 +++- .../spec/models/view/attachmentView.spec.ts | 17 ++++ .../spec/models/view/cipherView.spec.ts | 76 ++++++++++++++++ .../common/spec/models/view/loginView.spec.ts | 27 ++++++ .../models/view/passwordHistoryView.spec.ts | 13 +++ .../src/models/domain/symmetricCryptoKey.ts | 29 +++--- libs/common/src/models/view/attachmentView.ts | 7 ++ libs/common/src/models/view/cardView.ts | 10 ++- libs/common/src/models/view/cipherView.ts | 38 ++++++++ libs/common/src/models/view/fieldView.ts | 6 ++ libs/common/src/models/view/identityView.ts | 6 ++ libs/common/src/models/view/loginUriView.ts | 6 ++ libs/common/src/models/view/loginView.ts | 13 +++ .../src/models/view/passwordHistoryView.ts | 10 +++ libs/common/src/models/view/secureNoteView.ts | 6 ++ libs/common/src/services/state.service.ts | 16 ++-- package-lock.json | 89 +++++++++++++------ package.json | 1 + 36 files changed, 349 insertions(+), 70 deletions(-) rename libs/common/spec/{ => models}/domain/attachment.spec.ts (97%) rename libs/common/spec/{ => models}/domain/card.spec.ts (97%) rename libs/common/spec/{ => models}/domain/cipher.spec.ts (99%) rename libs/common/spec/{ => models}/domain/collection.spec.ts (97%) rename libs/common/spec/{ => models}/domain/encArrayBuffer.spec.ts (100%) rename libs/common/spec/{ => models}/domain/encString.spec.ts (100%) rename libs/common/spec/{ => models}/domain/field.spec.ts (97%) rename libs/common/spec/{ => models}/domain/folder.spec.ts (96%) rename libs/common/spec/{ => models}/domain/identity.spec.ts (99%) rename libs/common/spec/{ => models}/domain/login.spec.ts (98%) rename libs/common/spec/{ => models}/domain/loginUri.spec.ts (96%) rename libs/common/spec/{ => models}/domain/password.spec.ts (97%) rename libs/common/spec/{ => models}/domain/secureNote.spec.ts (100%) rename libs/common/spec/{ => models}/domain/send.spec.ts (98%) rename libs/common/spec/{ => models}/domain/sendAccess.spec.ts (98%) rename libs/common/spec/{ => models}/domain/sendFile.spec.ts (96%) rename libs/common/spec/{ => models}/domain/sendText.spec.ts (96%) rename libs/common/spec/{ => models}/domain/symmetricCryptoKey.spec.ts (77%) create mode 100644 libs/common/spec/models/view/attachmentView.spec.ts create mode 100644 libs/common/spec/models/view/cipherView.spec.ts create mode 100644 libs/common/spec/models/view/loginView.spec.ts create mode 100644 libs/common/spec/models/view/passwordHistoryView.spec.ts diff --git a/apps/browser/src/services/localBackedSessionStorage.service.ts b/apps/browser/src/services/localBackedSessionStorage.service.ts index 9b507b7df57..0f5305366a4 100644 --- a/apps/browser/src/services/localBackedSessionStorage.service.ts +++ b/apps/browser/src/services/localBackedSessionStorage.service.ts @@ -92,7 +92,7 @@ export class LocalBackedSessionStorageService extends AbstractStorageService { storedKey = await this.keyGenerationService.makeEphemeralKey(); await this.setSessionEncKey(storedKey); } - return SymmetricCryptoKey.initFromJson( + return SymmetricCryptoKey.fromJSON( Object.create(SymmetricCryptoKey.prototype, Object.getOwnPropertyDescriptors(storedKey)) ); } diff --git a/libs/common/spec/domain/attachment.spec.ts b/libs/common/spec/models/domain/attachment.spec.ts similarity index 97% rename from libs/common/spec/domain/attachment.spec.ts rename to libs/common/spec/models/domain/attachment.spec.ts index 72db84ce636..4a608c3a4a7 100644 --- a/libs/common/spec/domain/attachment.spec.ts +++ b/libs/common/spec/models/domain/attachment.spec.ts @@ -6,7 +6,7 @@ import { Attachment } from "@bitwarden/common/models/domain/attachment"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; import { ContainerService } from "@bitwarden/common/services/container.service"; -import { makeStaticByteArray, mockEnc } from "../utils"; +import { makeStaticByteArray, mockEnc } from "../../utils"; describe("Attachment", () => { let data: AttachmentData; diff --git a/libs/common/spec/domain/card.spec.ts b/libs/common/spec/models/domain/card.spec.ts similarity index 97% rename from libs/common/spec/domain/card.spec.ts rename to libs/common/spec/models/domain/card.spec.ts index 0565f1da43c..01357e005e3 100644 --- a/libs/common/spec/domain/card.spec.ts +++ b/libs/common/spec/models/domain/card.spec.ts @@ -1,7 +1,7 @@ import { CardData } from "@bitwarden/common/models/data/cardData"; import { Card } from "@bitwarden/common/models/domain/card"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("Card", () => { let data: CardData; diff --git a/libs/common/spec/domain/cipher.spec.ts b/libs/common/spec/models/domain/cipher.spec.ts similarity index 99% rename from libs/common/spec/domain/cipher.spec.ts rename to libs/common/spec/models/domain/cipher.spec.ts index 6bc3b933ced..c7c0d36714f 100644 --- a/libs/common/spec/domain/cipher.spec.ts +++ b/libs/common/spec/models/domain/cipher.spec.ts @@ -15,7 +15,7 @@ import { CardView } from "@bitwarden/common/models/view/cardView"; import { IdentityView } from "@bitwarden/common/models/view/identityView"; import { LoginView } from "@bitwarden/common/models/view/loginView"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("Cipher DTO", () => { it("Convert from empty CipherData", () => { diff --git a/libs/common/spec/domain/collection.spec.ts b/libs/common/spec/models/domain/collection.spec.ts similarity index 97% rename from libs/common/spec/domain/collection.spec.ts rename to libs/common/spec/models/domain/collection.spec.ts index 1c2d8ed86a1..823bffad321 100644 --- a/libs/common/spec/domain/collection.spec.ts +++ b/libs/common/spec/models/domain/collection.spec.ts @@ -1,7 +1,7 @@ import { CollectionData } from "@bitwarden/common/models/data/collectionData"; import { Collection } from "@bitwarden/common/models/domain/collection"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("Collection", () => { let data: CollectionData; diff --git a/libs/common/spec/domain/encArrayBuffer.spec.ts b/libs/common/spec/models/domain/encArrayBuffer.spec.ts similarity index 100% rename from libs/common/spec/domain/encArrayBuffer.spec.ts rename to libs/common/spec/models/domain/encArrayBuffer.spec.ts diff --git a/libs/common/spec/domain/encString.spec.ts b/libs/common/spec/models/domain/encString.spec.ts similarity index 100% rename from libs/common/spec/domain/encString.spec.ts rename to libs/common/spec/models/domain/encString.spec.ts diff --git a/libs/common/spec/domain/field.spec.ts b/libs/common/spec/models/domain/field.spec.ts similarity index 97% rename from libs/common/spec/domain/field.spec.ts rename to libs/common/spec/models/domain/field.spec.ts index 8ca74f0e282..2902f7af7ee 100644 --- a/libs/common/spec/domain/field.spec.ts +++ b/libs/common/spec/models/domain/field.spec.ts @@ -2,7 +2,7 @@ import { FieldType } from "@bitwarden/common/enums/fieldType"; import { FieldData } from "@bitwarden/common/models/data/fieldData"; import { Field } from "@bitwarden/common/models/domain/field"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("Field", () => { let data: FieldData; diff --git a/libs/common/spec/domain/folder.spec.ts b/libs/common/spec/models/domain/folder.spec.ts similarity index 96% rename from libs/common/spec/domain/folder.spec.ts rename to libs/common/spec/models/domain/folder.spec.ts index 8ce5477ea7c..c36caa80ba3 100644 --- a/libs/common/spec/domain/folder.spec.ts +++ b/libs/common/spec/models/domain/folder.spec.ts @@ -1,7 +1,7 @@ import { FolderData } from "@bitwarden/common/models/data/folderData"; import { Folder } from "@bitwarden/common/models/domain/folder"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("Folder", () => { let data: FolderData; diff --git a/libs/common/spec/domain/identity.spec.ts b/libs/common/spec/models/domain/identity.spec.ts similarity index 99% rename from libs/common/spec/domain/identity.spec.ts rename to libs/common/spec/models/domain/identity.spec.ts index 84fd1e5c395..19f94582be4 100644 --- a/libs/common/spec/domain/identity.spec.ts +++ b/libs/common/spec/models/domain/identity.spec.ts @@ -1,7 +1,7 @@ import { IdentityData } from "@bitwarden/common/models/data/identityData"; import { Identity } from "@bitwarden/common/models/domain/identity"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("Identity", () => { let data: IdentityData; diff --git a/libs/common/spec/domain/login.spec.ts b/libs/common/spec/models/domain/login.spec.ts similarity index 98% rename from libs/common/spec/domain/login.spec.ts rename to libs/common/spec/models/domain/login.spec.ts index 74479ecb3a4..6021a4b2908 100644 --- a/libs/common/spec/domain/login.spec.ts +++ b/libs/common/spec/models/domain/login.spec.ts @@ -6,7 +6,7 @@ import { Login } from "@bitwarden/common/models/domain/login"; import { LoginUri } from "@bitwarden/common/models/domain/loginUri"; import { LoginUriView } from "@bitwarden/common/models/view/loginUriView"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("Login DTO", () => { it("Convert from empty LoginData", () => { diff --git a/libs/common/spec/domain/loginUri.spec.ts b/libs/common/spec/models/domain/loginUri.spec.ts similarity index 96% rename from libs/common/spec/domain/loginUri.spec.ts rename to libs/common/spec/models/domain/loginUri.spec.ts index b57721bd0c5..059910917dc 100644 --- a/libs/common/spec/domain/loginUri.spec.ts +++ b/libs/common/spec/models/domain/loginUri.spec.ts @@ -2,7 +2,7 @@ import { UriMatchType } from "@bitwarden/common/enums/uriMatchType"; import { LoginUriData } from "@bitwarden/common/models/data/loginUriData"; import { LoginUri } from "@bitwarden/common/models/domain/loginUri"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("LoginUri", () => { let data: LoginUriData; diff --git a/libs/common/spec/domain/password.spec.ts b/libs/common/spec/models/domain/password.spec.ts similarity index 97% rename from libs/common/spec/domain/password.spec.ts rename to libs/common/spec/models/domain/password.spec.ts index 24377d6238a..b24f15059ef 100644 --- a/libs/common/spec/domain/password.spec.ts +++ b/libs/common/spec/models/domain/password.spec.ts @@ -1,7 +1,7 @@ import { PasswordHistoryData } from "@bitwarden/common/models/data/passwordHistoryData"; import { Password } from "@bitwarden/common/models/domain/password"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("Password", () => { let data: PasswordHistoryData; diff --git a/libs/common/spec/domain/secureNote.spec.ts b/libs/common/spec/models/domain/secureNote.spec.ts similarity index 100% rename from libs/common/spec/domain/secureNote.spec.ts rename to libs/common/spec/models/domain/secureNote.spec.ts diff --git a/libs/common/spec/domain/send.spec.ts b/libs/common/spec/models/domain/send.spec.ts similarity index 98% rename from libs/common/spec/domain/send.spec.ts rename to libs/common/spec/models/domain/send.spec.ts index 23f570b9b90..92a3a5ed2da 100644 --- a/libs/common/spec/domain/send.spec.ts +++ b/libs/common/spec/models/domain/send.spec.ts @@ -8,7 +8,7 @@ import { Send } from "@bitwarden/common/models/domain/send"; import { SendText } from "@bitwarden/common/models/domain/sendText"; import { ContainerService } from "@bitwarden/common/services/container.service"; -import { makeStaticByteArray, mockEnc } from "../utils"; +import { makeStaticByteArray, mockEnc } from "../../utils"; describe("Send", () => { let data: SendData; diff --git a/libs/common/spec/domain/sendAccess.spec.ts b/libs/common/spec/models/domain/sendAccess.spec.ts similarity index 98% rename from libs/common/spec/domain/sendAccess.spec.ts rename to libs/common/spec/models/domain/sendAccess.spec.ts index 49c635c8d34..7772fe428ac 100644 --- a/libs/common/spec/domain/sendAccess.spec.ts +++ b/libs/common/spec/models/domain/sendAccess.spec.ts @@ -5,7 +5,7 @@ import { SendAccess } from "@bitwarden/common/models/domain/sendAccess"; import { SendText } from "@bitwarden/common/models/domain/sendText"; import { SendAccessResponse } from "@bitwarden/common/models/response/sendAccessResponse"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("SendAccess", () => { let request: SendAccessResponse; diff --git a/libs/common/spec/domain/sendFile.spec.ts b/libs/common/spec/models/domain/sendFile.spec.ts similarity index 96% rename from libs/common/spec/domain/sendFile.spec.ts rename to libs/common/spec/models/domain/sendFile.spec.ts index a6c57349e1b..c24c1a48c93 100644 --- a/libs/common/spec/domain/sendFile.spec.ts +++ b/libs/common/spec/models/domain/sendFile.spec.ts @@ -1,7 +1,7 @@ import { SendFileData } from "@bitwarden/common/models/data/sendFileData"; import { SendFile } from "@bitwarden/common/models/domain/sendFile"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("SendFile", () => { let data: SendFileData; diff --git a/libs/common/spec/domain/sendText.spec.ts b/libs/common/spec/models/domain/sendText.spec.ts similarity index 96% rename from libs/common/spec/domain/sendText.spec.ts rename to libs/common/spec/models/domain/sendText.spec.ts index 683331ba9c3..0fb7edc0a0a 100644 --- a/libs/common/spec/domain/sendText.spec.ts +++ b/libs/common/spec/models/domain/sendText.spec.ts @@ -1,7 +1,7 @@ import { SendTextData } from "@bitwarden/common/models/data/sendTextData"; import { SendText } from "@bitwarden/common/models/domain/sendText"; -import { mockEnc } from "../utils"; +import { mockEnc } from "../../utils"; describe("SendText", () => { let data: SendTextData; diff --git a/libs/common/spec/domain/symmetricCryptoKey.spec.ts b/libs/common/spec/models/domain/symmetricCryptoKey.spec.ts similarity index 77% rename from libs/common/spec/domain/symmetricCryptoKey.spec.ts rename to libs/common/spec/models/domain/symmetricCryptoKey.spec.ts index 31410f254be..5670fe46992 100644 --- a/libs/common/spec/domain/symmetricCryptoKey.spec.ts +++ b/libs/common/spec/models/domain/symmetricCryptoKey.spec.ts @@ -1,7 +1,7 @@ import { EncryptionType } from "@bitwarden/common/enums/encryptionType"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; -import { makeStaticByteArray } from "../utils"; +import { makeStaticByteArray } from "../../utils"; describe("SymmetricCryptoKey", () => { it("errors if no key", () => { @@ -66,4 +66,21 @@ describe("SymmetricCryptoKey", () => { expect(t).toThrowError("Unable to determine encType."); }); }); + + it("toJSON creates object for serialization", () => { + const key = new SymmetricCryptoKey(makeStaticByteArray(64).buffer); + const actual = key.toJSON(); + + const expected = { keyB64: key.keyB64 }; + + expect(actual).toEqual(expected); + }); + + it("fromJSON hydrates new object", () => { + const expected = new SymmetricCryptoKey(makeStaticByteArray(64).buffer); + const actual = SymmetricCryptoKey.fromJSON({ keyB64: expected.keyB64 }); + + expect(actual).toEqual(expected); + expect(actual).toBeInstanceOf(SymmetricCryptoKey); + }); }); diff --git a/libs/common/spec/models/view/attachmentView.spec.ts b/libs/common/spec/models/view/attachmentView.spec.ts new file mode 100644 index 00000000000..d94456daa9c --- /dev/null +++ b/libs/common/spec/models/view/attachmentView.spec.ts @@ -0,0 +1,17 @@ +import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; +import { AttachmentView } from "@bitwarden/common/models/view/attachmentView"; + +jest.mock("@bitwarden/common/models/domain/symmetricCryptoKey"); + +describe("AttachmentView", () => { + it("fromJSON initializes nested objects", () => { + const mockFromJson = (stub: string) => stub + "_fromJSON"; + jest.spyOn(SymmetricCryptoKey, "fromJSON").mockImplementation(mockFromJson as any); + + const actual = AttachmentView.fromJSON({ + key: "encKeyB64" as any, + }); + + expect(actual.key).toEqual("encKeyB64_fromJSON"); + }); +}); diff --git a/libs/common/spec/models/view/cipherView.spec.ts b/libs/common/spec/models/view/cipherView.spec.ts new file mode 100644 index 00000000000..f69bb089dce --- /dev/null +++ b/libs/common/spec/models/view/cipherView.spec.ts @@ -0,0 +1,76 @@ +import { CipherType } from "@bitwarden/common/enums/cipherType"; +import { AttachmentView } from "@bitwarden/common/models/view/attachmentView"; +import { CardView } from "@bitwarden/common/models/view/cardView"; +import { CipherView } from "@bitwarden/common/models/view/cipherView"; +import { FieldView } from "@bitwarden/common/models/view/fieldView"; +import { IdentityView } from "@bitwarden/common/models/view/identityView"; +import { LoginView } from "@bitwarden/common/models/view/loginView"; +import { PasswordHistoryView } from "@bitwarden/common/models/view/passwordHistoryView"; +import { SecureNoteView } from "@bitwarden/common/models/view/secureNoteView"; + +jest.mock("@bitwarden/common/models/view/loginView"); +jest.mock("@bitwarden/common/models/view/attachmentView"); +jest.mock("@bitwarden/common/models/view/fieldView"); +jest.mock("@bitwarden/common/models/view/passwordHistoryView"); + +describe("CipherView", () => { + beforeEach(() => { + (LoginView as any).mockClear(); + (AttachmentView as any).mockClear(); + (FieldView as any).mockClear(); + (PasswordHistoryView as any).mockClear(); + }); + + describe("fromJSON", () => { + const mockFromJson = (stub: any) => (stub + "_fromJSON") as any; + + it("initializes nested objects", () => { + jest.spyOn(AttachmentView, "fromJSON").mockImplementation(mockFromJson); + jest.spyOn(FieldView, "fromJSON").mockImplementation(mockFromJson); + jest.spyOn(PasswordHistoryView, "fromJSON").mockImplementation(mockFromJson); + + const revisionDate = new Date("2022-08-04T01:06:40.441Z"); + const deletedDate = new Date("2022-09-04T01:06:40.441Z"); + const actual = CipherView.fromJSON({ + revisionDate: revisionDate.toISOString(), + deletedDate: deletedDate.toISOString(), + attachments: ["attachment1", "attachment2"] as any, + fields: ["field1", "field2"] as any, + passwordHistory: ["ph1", "ph2", "ph3"] as any, + }); + + const expected = { + revisionDate: revisionDate, + deletedDate: deletedDate, + attachments: ["attachment1_fromJSON", "attachment2_fromJSON"], + fields: ["field1_fromJSON", "field2_fromJSON"], + passwordHistory: ["ph1_fromJSON", "ph2_fromJSON", "ph3_fromJSON"], + }; + + expect(actual).toMatchObject(expected); + }); + + test.each([ + // Test description, CipherType, expected output + ["LoginView", CipherType.Login, { login: "myLogin_fromJSON" }], + ["CardView", CipherType.Card, { card: "myCard_fromJSON" }], + ["IdentityView", CipherType.Identity, { identity: "myIdentity_fromJSON" }], + ["Secure Note", CipherType.SecureNote, { secureNote: "mySecureNote_fromJSON" }], + ])("initializes %s", (description: string, cipherType: CipherType, expected: any) => { + jest.spyOn(LoginView, "fromJSON").mockImplementation(mockFromJson); + jest.spyOn(IdentityView, "fromJSON").mockImplementation(mockFromJson); + jest.spyOn(CardView, "fromJSON").mockImplementation(mockFromJson); + jest.spyOn(SecureNoteView, "fromJSON").mockImplementation(mockFromJson); + + const actual = CipherView.fromJSON({ + login: "myLogin", + card: "myCard", + identity: "myIdentity", + secureNote: "mySecureNote", + type: cipherType, + } as any); + + expect(actual).toMatchObject(expected); + }); + }); +}); diff --git a/libs/common/spec/models/view/loginView.spec.ts b/libs/common/spec/models/view/loginView.spec.ts new file mode 100644 index 00000000000..e9b76279574 --- /dev/null +++ b/libs/common/spec/models/view/loginView.spec.ts @@ -0,0 +1,27 @@ +import { LoginUriView } from "@bitwarden/common/models/view/loginUriView"; +import { LoginView } from "@bitwarden/common/models/view/loginView"; + +jest.mock("@bitwarden/common/models/view/loginUriView"); + +describe("LoginView", () => { + beforeEach(() => { + (LoginUriView as any).mockClear(); + }); + + it("fromJSON initializes nested objects", () => { + const mockFromJson = (stub: string) => stub + "_fromJSON"; + jest.spyOn(LoginUriView, "fromJSON").mockImplementation(mockFromJson as any); + + const passwordRevisionDate = new Date(); + + const actual = LoginView.fromJSON({ + passwordRevisionDate: passwordRevisionDate.toISOString(), + uris: ["uri1", "uri2", "uri3"] as any, + }); + + expect(actual).toMatchObject({ + passwordRevisionDate: passwordRevisionDate, + uris: ["uri1_fromJSON", "uri2_fromJSON", "uri3_fromJSON"], + }); + }); +}); diff --git a/libs/common/spec/models/view/passwordHistoryView.spec.ts b/libs/common/spec/models/view/passwordHistoryView.spec.ts new file mode 100644 index 00000000000..99662c40c0b --- /dev/null +++ b/libs/common/spec/models/view/passwordHistoryView.spec.ts @@ -0,0 +1,13 @@ +import { PasswordHistoryView } from "@bitwarden/common/models/view/passwordHistoryView"; + +describe("PasswordHistoryView", () => { + it("fromJSON initializes nested objects", () => { + const lastUsedDate = new Date(); + + const actual = PasswordHistoryView.fromJSON({ + lastUsedDate: lastUsedDate.toISOString(), + }); + + expect(actual.lastUsedDate).toEqual(lastUsedDate); + }); +}); diff --git a/libs/common/src/models/domain/symmetricCryptoKey.ts b/libs/common/src/models/domain/symmetricCryptoKey.ts index 5e0c437ba3b..1218c5dc70f 100644 --- a/libs/common/src/models/domain/symmetricCryptoKey.ts +++ b/libs/common/src/models/domain/symmetricCryptoKey.ts @@ -1,5 +1,8 @@ +import { Jsonify } from "type-fest"; + +import { Utils } from "@bitwarden/common/misc/utils"; + import { EncryptionType } from "../../enums/encryptionType"; -import { Utils } from "../../misc/utils"; export class SymmetricCryptoKey { key: ArrayBuffer; @@ -55,21 +58,17 @@ export class SymmetricCryptoKey { } } - static initFromJson(jsonResult: SymmetricCryptoKey): SymmetricCryptoKey { - if (jsonResult == null) { - return jsonResult; + toJSON() { + // The whole object is constructed from the initial key, so just store the B64 key + return { keyB64: this.keyB64 }; + } + + static fromJSON(obj: Jsonify): SymmetricCryptoKey { + if (obj == null) { + return null; } - if (jsonResult.keyB64 != null) { - jsonResult.key = Utils.fromB64ToArray(jsonResult.keyB64).buffer; - } - if (jsonResult.encKeyB64 != null) { - jsonResult.encKey = Utils.fromB64ToArray(jsonResult.encKeyB64).buffer; - } - if (jsonResult.macKeyB64 != null) { - jsonResult.macKey = Utils.fromB64ToArray(jsonResult.macKeyB64).buffer; - } - - return jsonResult; + const arrayBuffer = Utils.fromB64ToArray(obj.keyB64).buffer; + return new SymmetricCryptoKey(arrayBuffer); } } diff --git a/libs/common/src/models/view/attachmentView.ts b/libs/common/src/models/view/attachmentView.ts index 4ebbec7ee85..6e81d58ca8d 100644 --- a/libs/common/src/models/view/attachmentView.ts +++ b/libs/common/src/models/view/attachmentView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { Attachment } from "../domain/attachment"; import { SymmetricCryptoKey } from "../domain/symmetricCryptoKey"; @@ -32,4 +34,9 @@ export class AttachmentView implements View { } return 0; } + + static fromJSON(obj: Partial>): AttachmentView { + const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key); + return Object.assign(new AttachmentView(), obj, { key: key }); + } } diff --git a/libs/common/src/models/view/cardView.ts b/libs/common/src/models/view/cardView.ts index 769fa4402f7..fa57bd2b35f 100644 --- a/libs/common/src/models/view/cardView.ts +++ b/libs/common/src/models/view/cardView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { CardLinkedId as LinkedId } from "../../enums/linkedIdType"; import { linkedFieldOption } from "../../misc/linkedFieldOption.decorator"; @@ -17,10 +19,6 @@ export class CardView extends ItemView { private _number: string = null; private _subTitle: string = null; - constructor() { - super(); - } - get maskedCode(): string { return this.code != null ? "•".repeat(this.code.length) : null; } @@ -79,4 +77,8 @@ export class CardView extends ItemView { private formatYear(year: string): string { return year.length === 2 ? "20" + year : year; } + + static fromJSON(obj: Partial>): CardView { + return Object.assign(new CardView(), obj); + } } diff --git a/libs/common/src/models/view/cipherView.ts b/libs/common/src/models/view/cipherView.ts index 3d1f16c14fd..7471be89b6d 100644 --- a/libs/common/src/models/view/cipherView.ts +++ b/libs/common/src/models/view/cipherView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { CipherRepromptType } from "../../enums/cipherRepromptType"; import { CipherType } from "../../enums/cipherType"; import { LinkedIdType } from "../../enums/linkedIdType"; @@ -131,4 +133,40 @@ export class CipherView implements View { linkedFieldI18nKey(id: LinkedIdType): string { return this.linkedFieldOptions.get(id)?.i18nKey; } + + static fromJSON(obj: Partial>): CipherView { + const view = new CipherView(); + const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); + const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate); + const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a)); + const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f)); + const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph)); + + Object.assign(view, obj, { + revisionDate: revisionDate, + deletedDate: deletedDate, + attachments: attachments, + fields: fields, + passwordHistory: passwordHistory, + }); + + switch (obj.type) { + case CipherType.Card: + view.card = CardView.fromJSON(obj.card); + break; + case CipherType.Identity: + view.identity = IdentityView.fromJSON(obj.identity); + break; + case CipherType.Login: + view.login = LoginView.fromJSON(obj.login); + break; + case CipherType.SecureNote: + view.secureNote = SecureNoteView.fromJSON(obj.secureNote); + break; + default: + break; + } + + return view; + } } diff --git a/libs/common/src/models/view/fieldView.ts b/libs/common/src/models/view/fieldView.ts index 51e3aaef35d..a022ad6f61b 100644 --- a/libs/common/src/models/view/fieldView.ts +++ b/libs/common/src/models/view/fieldView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { FieldType } from "../../enums/fieldType"; import { LinkedIdType } from "../../enums/linkedIdType"; import { Field } from "../domain/field"; @@ -25,4 +27,8 @@ export class FieldView implements View { get maskedValue(): string { return this.value != null ? "••••••••" : null; } + + static fromJSON(obj: Partial>): FieldView { + return Object.assign(new FieldView(), obj); + } } diff --git a/libs/common/src/models/view/identityView.ts b/libs/common/src/models/view/identityView.ts index 2630563609d..437652e2ac8 100644 --- a/libs/common/src/models/view/identityView.ts +++ b/libs/common/src/models/view/identityView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { IdentityLinkedId as LinkedId } from "../../enums/linkedIdType"; import { linkedFieldOption } from "../../misc/linkedFieldOption.decorator"; import { Utils } from "../../misc/utils"; @@ -139,4 +141,8 @@ export class IdentityView extends ItemView { addressPart2 += ", " + postalCode; return addressPart2; } + + static fromJSON(obj: Partial>): IdentityView { + return Object.assign(new IdentityView(), obj); + } } diff --git a/libs/common/src/models/view/loginUriView.ts b/libs/common/src/models/view/loginUriView.ts index cca9520df1f..28ca18e3a90 100644 --- a/libs/common/src/models/view/loginUriView.ts +++ b/libs/common/src/models/view/loginUriView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { UriMatchType } from "../../enums/uriMatchType"; import { Utils } from "../../misc/utils"; import { LoginUri } from "../domain/loginUri"; @@ -124,4 +126,8 @@ export class LoginUriView implements View { ? "http://" + this.uri : this.uri; } + + static fromJSON(obj: Partial>): LoginUriView { + return Object.assign(new LoginUriView(), obj); + } } diff --git a/libs/common/src/models/view/loginView.ts b/libs/common/src/models/view/loginView.ts index e86dd14b2f5..1e4b2201e36 100644 --- a/libs/common/src/models/view/loginView.ts +++ b/libs/common/src/models/view/loginView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { LoginLinkedId as LinkedId } from "../../enums/linkedIdType"; import { linkedFieldOption } from "../../misc/linkedFieldOption.decorator"; import { Utils } from "../../misc/utils"; @@ -60,4 +62,15 @@ export class LoginView extends ItemView { get hasUris(): boolean { return this.uris != null && this.uris.length > 0; } + + static fromJSON(obj: Partial>): LoginView { + const passwordRevisionDate = + obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); + const uris = obj.uris?.map((uri: any) => LoginUriView.fromJSON(uri)); + + return Object.assign(new LoginView(), obj, { + passwordRevisionDate: passwordRevisionDate, + uris: uris, + }); + } } diff --git a/libs/common/src/models/view/passwordHistoryView.ts b/libs/common/src/models/view/passwordHistoryView.ts index aa780079d3a..1d0b9eb8dda 100644 --- a/libs/common/src/models/view/passwordHistoryView.ts +++ b/libs/common/src/models/view/passwordHistoryView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { Password } from "../domain/password"; import { View } from "./view"; @@ -13,4 +15,12 @@ export class PasswordHistoryView implements View { this.lastUsedDate = ph.lastUsedDate; } + + static fromJSON(obj: Partial>): PasswordHistoryView { + const lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate); + + return Object.assign(new PasswordHistoryView(), obj, { + lastUsedDate: lastUsedDate, + }); + } } diff --git a/libs/common/src/models/view/secureNoteView.ts b/libs/common/src/models/view/secureNoteView.ts index c324b07ebb3..984beee17c4 100644 --- a/libs/common/src/models/view/secureNoteView.ts +++ b/libs/common/src/models/view/secureNoteView.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { SecureNoteType } from "../../enums/secureNoteType"; import { SecureNote } from "../domain/secureNote"; @@ -18,4 +20,8 @@ export class SecureNoteView extends ItemView { get subTitle(): string { return null; } + + static fromJSON(obj: Partial>): SecureNoteView { + return Object.assign(new SecureNoteView(), obj); + } } diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index 8570d3ef649..c7c5080d99f 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -498,7 +498,7 @@ export class StateService< ); } - @withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.initFromJson) + @withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getCryptoMasterKey(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -661,7 +661,7 @@ export class StateService< ); } - @withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.initFromJson) + @withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -682,7 +682,7 @@ export class StateService< ); } - @withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.initFromJson) + @withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedOrganizationKeys( options?: StorageOptions ): Promise> { @@ -789,7 +789,7 @@ export class StateService< ); } - @withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.initFromJson) + @withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedProviderKeys( options?: StorageOptions ): Promise> { @@ -2748,7 +2748,7 @@ export class StateService< export function withPrototype( constructor: new (...args: any[]) => T, - converter: (input: T) => T = (i) => i + converter: (input: any) => T = (i) => i ): ( target: any, propertyKey: string | symbol, @@ -2784,7 +2784,7 @@ export function withPrototype( function withPrototypeForArrayMembers( memberConstructor: new (...args: any[]) => T, - memberConverter: (input: T) => T = (i) => i + memberConverter: (input: any) => T = (i) => i ): ( target: any, propertyKey: string | symbol, @@ -2832,7 +2832,7 @@ function withPrototypeForArrayMembers( function withPrototypeForObjectValues( valuesConstructor: new (...args: any[]) => T, - valuesConverter: (input: T) => T = (i) => i + valuesConverter: (input: any) => T = (i) => i ): ( target: any, propertyKey: string | symbol, @@ -2879,7 +2879,7 @@ function withPrototypeForObjectValues( function withPrototypeForMap( valuesConstructor: new (...args: any[]) => T, - valuesConverter: (input: T) => T = (i) => i + valuesConverter: (input: any) => T = (i) => i ): ( target: any, propertyKey: string | symbol, diff --git a/package-lock.json b/package-lock.json index 474e2a9b518..49fa99c43ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -166,6 +166,7 @@ "ts-jest": "^28.0.6", "ts-loader": "^9.2.5", "tsconfig-paths-webpack-plugin": "^3.5.2", + "type-fest": "^2.18.0", "typescript": "4.6.4", "url": "^0.11.0", "util": "^0.12.4", @@ -3965,6 +3966,18 @@ "node": "*" } }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@figspec/components": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@figspec/components/-/components-1.0.1.tgz", @@ -15965,6 +15978,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bplist-parser": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.1.1.tgz", @@ -20353,18 +20378,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron-store/node_modules/type-fest": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.17.0.tgz", - "integrity": "sha512-U+g3/JVXnOki1kLSc+xZGPRll3Ah9u2VIG6Sn9iH9YX6UkPERmt6O/0fIyTgsd2/whV0+gAaHAg8fz6sG1QzMA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/electron-to-chromium": { "version": "1.4.200", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.200.tgz", @@ -21732,6 +21745,18 @@ "node": "*" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", @@ -40504,12 +40529,12 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.18.0.tgz", + "integrity": "sha512-pRS+/yrW5TjPPHNOvxhbNZexr2bS63WjrMU8a+VzEBhUi9Tz1pZeD+vQz3ut0svZ46P+SRqMEPnJmk2XnvNzTw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -45457,6 +45482,12 @@ "requires": { "brace-expansion": "^1.1.7" } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true } } }, @@ -54924,6 +54955,12 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true } } }, @@ -58370,14 +58407,6 @@ "requires": { "conf": "^10.1.2", "type-fest": "^2.12.2" - }, - "dependencies": { - "type-fest": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.17.0.tgz", - "integrity": "sha512-U+g3/JVXnOki1kLSc+xZGPRll3Ah9u2VIG6Sn9iH9YX6UkPERmt6O/0fIyTgsd2/whV0+gAaHAg8fz6sG1QzMA==", - "dev": true - } } }, "electron-to-chromium": { @@ -59119,6 +59148,12 @@ "requires": { "brace-expansion": "^1.1.7" } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true } } }, @@ -73936,9 +73971,9 @@ "dev": true }, "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.18.0.tgz", + "integrity": "sha512-pRS+/yrW5TjPPHNOvxhbNZexr2bS63WjrMU8a+VzEBhUi9Tz1pZeD+vQz3ut0svZ46P+SRqMEPnJmk2XnvNzTw==", "dev": true }, "type-is": { diff --git a/package.json b/package.json index 58ab8f4117d..7a4f6e34abe 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "ts-jest": "^28.0.6", "ts-loader": "^9.2.5", "tsconfig-paths-webpack-plugin": "^3.5.2", + "type-fest": "^2.18.0", "typescript": "4.6.4", "url": "^0.11.0", "util": "^0.12.4",