From 3609127858f505f489743fb7da1b96364aad159e Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 24 Oct 2025 09:43:38 -0400 Subject: [PATCH] [PM-25683] Migrate Cipher model and sub-models (#16974) * Made domain classes ts-strict compliant and fixed spec files * Fixed domain base class and other test files * Added conditional utils and fixed small nits * removed comments * removd ts expect errors * Added removed counter * renamed test name * fixed tests --- .../src/platform/models/domain/domain-base.ts | 8 +- .../models/api/cipher-permissions.api.ts | 4 +- .../vault/models/domain/attachment.spec.ts | 16 +- .../src/vault/models/domain/attachment.ts | 92 ++--- .../src/vault/models/domain/card.spec.ts | 16 +- libs/common/src/vault/models/domain/card.ts | 81 ++--- .../src/vault/models/domain/cipher.spec.ts | 103 +++--- libs/common/src/vault/models/domain/cipher.ts | 340 ++++++++++-------- .../models/domain/fido2-credential.spec.ts | 52 +-- .../vault/models/domain/fido2-credential.ts | 135 +++---- .../src/vault/models/domain/field.spec.ts | 16 +- libs/common/src/vault/models/domain/field.ts | 53 ++- .../src/vault/models/domain/identity.spec.ts | 40 +-- .../src/vault/models/domain/identity.ts | 186 +++++----- .../src/vault/models/domain/login-uri.spec.ts | 14 +- .../src/vault/models/domain/login-uri.ts | 58 ++- .../src/vault/models/domain/login.spec.ts | 12 +- libs/common/src/vault/models/domain/login.ts | 124 +++---- .../src/vault/models/domain/password.spec.ts | 10 +- .../src/vault/models/domain/password.ts | 30 +- .../vault/models/domain/secure-note.spec.ts | 6 +- .../src/vault/models/domain/secure-note.ts | 21 +- .../src/vault/models/domain/ssh-key.spec.ts | 15 +- .../common/src/vault/models/domain/ssh-key.ts | 45 +-- .../models/request/cipher-partial.request.ts | 2 +- .../src/vault/services/cipher.service.ts | 3 +- libs/common/src/vault/utils/domain-utils.ts | 27 ++ .../individual-vault-export.service.spec.ts | 2 +- 28 files changed, 762 insertions(+), 749 deletions(-) create mode 100644 libs/common/src/vault/utils/domain-utils.ts diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index bab9f0f8ac..a144353f5b 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -14,15 +14,15 @@ export type DecryptedObject< // extracts shared keys from the domain and view types type EncryptableKeys = (keyof D & - ConditionalKeys) & - (keyof V & ConditionalKeys); + ConditionalKeys) & + (keyof V & ConditionalKeys); type DomainEncryptableKeys = { - [key in ConditionalKeys]: EncString | null; + [key in ConditionalKeys]?: EncString | null | undefined; }; type ViewEncryptableKeys = { - [key in ConditionalKeys]: string | null; + [key in ConditionalKeys]?: string | null | undefined; }; // https://contributing.bitwarden.com/architecture/clients/data-model#domain diff --git a/libs/common/src/vault/models/api/cipher-permissions.api.ts b/libs/common/src/vault/models/api/cipher-permissions.api.ts index f9b62c4fc8..cca5ffce79 100644 --- a/libs/common/src/vault/models/api/cipher-permissions.api.ts +++ b/libs/common/src/vault/models/api/cipher-permissions.api.ts @@ -24,7 +24,9 @@ export class CipherPermissionsApi extends BaseResponse implements SdkCipherPermi /** * Converts the SDK CipherPermissionsApi to a CipherPermissionsApi. */ - static fromSdkCipherPermissions(obj: SdkCipherPermissions): CipherPermissionsApi | undefined { + static fromSdkCipherPermissions( + obj: SdkCipherPermissions | undefined, + ): CipherPermissionsApi | undefined { if (!obj) { return undefined; } diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 93f693f14c..972c77537f 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -32,12 +32,12 @@ describe("Attachment", () => { const attachment = new Attachment(data); expect(attachment).toEqual({ - id: null, - url: null, + id: undefined, + url: undefined, size: undefined, - sizeName: null, - key: null, - fileName: null, + sizeName: undefined, + key: undefined, + fileName: undefined, }); }); @@ -79,6 +79,8 @@ describe("Attachment", () => { attachment.key = mockEnc("key"); attachment.fileName = mockEnc("fileName"); + const userKey = new SymmetricCryptoKey(makeStaticByteArray(64)); + keyService.getUserKey.mockResolvedValue(userKey as UserKey); encryptService.decryptFileData.mockResolvedValue(makeStaticByteArray(32)); encryptService.unwrapSymmetricKey.mockResolvedValue( new SymmetricCryptoKey(makeStaticByteArray(64)), @@ -152,8 +154,8 @@ describe("Attachment", () => { expect(actual).toBeInstanceOf(Attachment); }); - it("returns null if object is null", () => { - expect(Attachment.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Attachment.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 4ace8ce0e7..7b43af9be5 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -1,23 +1,23 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; +import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { Attachment as SdkAttachment } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { AttachmentData } from "../data/attachment.data"; import { AttachmentView } from "../view/attachment.view"; export class Attachment extends Domain { - id: string; - url: string; - size: string; - sizeName: string; // Readable size, ex: "4.2 KB" or "1.43 GB" - key: EncString; - fileName: EncString; + id?: string; + url?: string; + size?: string; + sizeName?: string; // Readable size, ex: "4.2 KB" or "1.43 GB" + key?: EncString; + fileName?: EncString; constructor(obj?: AttachmentData) { super(); @@ -25,32 +25,24 @@ export class Attachment extends Domain { return; } + this.id = obj.id; + this.url = obj.url; this.size = obj.size; - this.buildDomainModel( - this, - obj, - { - id: null, - url: null, - sizeName: null, - fileName: null, - key: null, - }, - ["id", "url", "sizeName"], - ); + this.sizeName = obj.sizeName; + this.fileName = conditionalEncString(obj.fileName); + this.key = conditionalEncString(obj.key); } async decrypt( - orgId: string, + orgId: string | undefined, context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { const view = await this.decryptObj( this, - // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new AttachmentView(this), ["fileName"], - orgId, + orgId ?? null, encKey, "DomainType: Attachment; " + context, ); @@ -63,30 +55,46 @@ export class Attachment extends Domain { return view; } - private async decryptAttachmentKey(orgId: string, encKey?: SymmetricCryptoKey) { + private async decryptAttachmentKey( + orgId: string | undefined, + encKey?: SymmetricCryptoKey, + ): Promise { try { + if (this.key == null) { + return undefined; + } + if (encKey == null) { - encKey = await this.getKeyForDecryption(orgId); + const key = await this.getKeyForDecryption(orgId); + + // If we don't have a key, we can't decrypt + if (key == null) { + return undefined; + } + + encKey = key; } const encryptService = Utils.getContainerService().getEncryptService(); const decValue = await encryptService.unwrapSymmetricKey(this.key, encKey); return decValue; - // FIXME: Remove when updating file. Eslint update - // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - // TODO: error? + // eslint-disable-next-line no-console + console.error("[Attachment] Error decrypting attachment", e); + return undefined; } } - private async getKeyForDecryption(orgId: string) { + private async getKeyForDecryption(orgId: string | undefined): Promise { const keyService = Utils.getContainerService().getKeyService(); return orgId != null ? await keyService.getOrgKey(orgId) : await keyService.getUserKey(); } toAttachmentData(): AttachmentData { const a = new AttachmentData(); - a.size = this.size; + if (this.size != null) { + a.size = this.size; + } this.buildDataModel( this, a, @@ -102,18 +110,20 @@ export class Attachment extends Domain { return a; } - static fromJSON(obj: Partial>): Attachment { + static fromJSON(obj: Partial> | undefined): Attachment | undefined { if (obj == null) { - return null; + return undefined; } - const key = EncString.fromJSON(obj.key); - const fileName = EncString.fromJSON(obj.fileName); + const attachment = new Attachment(); + attachment.id = obj.id; + attachment.url = obj.url; + attachment.size = obj.size; + attachment.sizeName = obj.sizeName; + attachment.key = encStringFrom(obj.key); + attachment.fileName = encStringFrom(obj.fileName); - return Object.assign(new Attachment(), obj, { - key, - fileName, - }); + return attachment; } /** @@ -136,7 +146,7 @@ export class Attachment extends Domain { * Maps an SDK Attachment object to an Attachment * @param obj - The SDK attachment object */ - static fromSdkAttachment(obj: SdkAttachment): Attachment | undefined { + static fromSdkAttachment(obj?: SdkAttachment): Attachment | undefined { if (!obj) { return undefined; } @@ -146,8 +156,8 @@ export class Attachment extends Domain { attachment.url = obj.url; attachment.size = obj.size; attachment.sizeName = obj.sizeName; - attachment.fileName = EncString.fromJSON(obj.fileName); - attachment.key = EncString.fromJSON(obj.key); + attachment.fileName = encStringFrom(obj.fileName); + attachment.key = encStringFrom(obj.key); return attachment; } diff --git a/libs/common/src/vault/models/domain/card.spec.ts b/libs/common/src/vault/models/domain/card.spec.ts index 4da62c631d..a4d242329a 100644 --- a/libs/common/src/vault/models/domain/card.spec.ts +++ b/libs/common/src/vault/models/domain/card.spec.ts @@ -22,12 +22,12 @@ describe("Card", () => { const card = new Card(data); expect(card).toEqual({ - cardholderName: null, - brand: null, - number: null, - expMonth: null, - expYear: null, - code: null, + cardholderName: undefined, + brand: undefined, + number: undefined, + expMonth: undefined, + expYear: undefined, + code: undefined, }); }); @@ -94,8 +94,8 @@ describe("Card", () => { expect(actual).toBeInstanceOf(Card); }); - it("returns null if object is null", () => { - expect(Card.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Card.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index 89cc361b45..b3a087d44f 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Card as SdkCard } from "@bitwarden/sdk-internal"; @@ -7,16 +5,17 @@ import { Card as SdkCard } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { CardData } from "../data/card.data"; import { CardView } from "../view/card.view"; export class Card extends Domain { - cardholderName: EncString; - brand: EncString; - number: EncString; - expMonth: EncString; - expYear: EncString; - code: EncString; + cardholderName?: EncString; + brand?: EncString; + number?: EncString; + expMonth?: EncString; + expYear?: EncString; + code?: EncString; constructor(obj?: CardData) { super(); @@ -24,23 +23,16 @@ export class Card extends Domain { return; } - this.buildDomainModel( - this, - obj, - { - cardholderName: null, - brand: null, - number: null, - expMonth: null, - expYear: null, - code: null, - }, - [], - ); + this.cardholderName = conditionalEncString(obj.cardholderName); + this.brand = conditionalEncString(obj.brand); + this.number = conditionalEncString(obj.number); + this.expMonth = conditionalEncString(obj.expMonth); + this.expYear = conditionalEncString(obj.expYear); + this.code = conditionalEncString(obj.code); } async decrypt( - orgId: string, + orgId: string | undefined, context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { @@ -48,7 +40,7 @@ export class Card extends Domain { this, new CardView(), ["cardholderName", "brand", "number", "expMonth", "expYear", "code"], - orgId, + orgId ?? null, encKey, "DomainType: Card; " + context, ); @@ -67,25 +59,20 @@ export class Card extends Domain { return c; } - static fromJSON(obj: Partial>): Card { + static fromJSON(obj: Partial> | undefined): Card | undefined { if (obj == null) { - return null; + return undefined; } - const cardholderName = EncString.fromJSON(obj.cardholderName); - const brand = EncString.fromJSON(obj.brand); - const number = EncString.fromJSON(obj.number); - const expMonth = EncString.fromJSON(obj.expMonth); - const expYear = EncString.fromJSON(obj.expYear); - const code = EncString.fromJSON(obj.code); - return Object.assign(new Card(), obj, { - cardholderName, - brand, - number, - expMonth, - expYear, - code, - }); + const card = new Card(); + card.cardholderName = encStringFrom(obj.cardholderName); + card.brand = encStringFrom(obj.brand); + card.number = encStringFrom(obj.number); + card.expMonth = encStringFrom(obj.expMonth); + card.expYear = encStringFrom(obj.expYear); + card.code = encStringFrom(obj.code); + + return card; } /** @@ -108,18 +95,18 @@ export class Card extends Domain { * Maps an SDK Card object to a Card * @param obj - The SDK Card object */ - static fromSdkCard(obj: SdkCard): Card | undefined { - if (obj == null) { + static fromSdkCard(obj?: SdkCard): Card | undefined { + if (!obj) { return undefined; } const card = new Card(); - card.cardholderName = EncString.fromJSON(obj.cardholderName); - card.brand = EncString.fromJSON(obj.brand); - card.number = EncString.fromJSON(obj.number); - card.expMonth = EncString.fromJSON(obj.expMonth); - card.expYear = EncString.fromJSON(obj.expYear); - card.code = EncString.fromJSON(obj.code); + card.cardholderName = encStringFrom(obj.cardholderName); + card.brand = encStringFrom(obj.brand); + card.number = encStringFrom(obj.number); + card.expMonth = encStringFrom(obj.expMonth); + card.expYear = encStringFrom(obj.expYear); + card.code = encStringFrom(obj.code); return card; } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index c2cb99740d..4052c9e533 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -44,31 +44,28 @@ describe("Cipher DTO", () => { const data = new CipherData(); const cipher = new Cipher(data); - expect(cipher).toEqual({ - initializerKey: InitializerKey.Cipher, - id: null, - organizationId: null, - folderId: null, - name: null, - notes: null, - type: undefined, - favorite: undefined, - organizationUseTotp: undefined, - edit: undefined, - viewPassword: true, - revisionDate: null, - collectionIds: undefined, - localData: null, - creationDate: null, - deletedDate: undefined, - reprompt: undefined, - attachments: null, - fields: null, - passwordHistory: null, - key: null, - permissions: undefined, - archivedDate: undefined, - }); + expect(cipher.id).toBeUndefined(); + expect(cipher.organizationId).toBeUndefined(); + expect(cipher.folderId).toBeUndefined(); + expect(cipher.name).toBeInstanceOf(EncString); + expect(cipher.notes).toBeUndefined(); + expect(cipher.type).toBeUndefined(); + expect(cipher.favorite).toBeUndefined(); + expect(cipher.organizationUseTotp).toBeUndefined(); + expect(cipher.edit).toBeUndefined(); + expect(cipher.viewPassword).toBeUndefined(); + expect(cipher.revisionDate).toBeInstanceOf(Date); + expect(cipher.collectionIds).toEqual([]); + expect(cipher.localData).toBeUndefined(); + expect(cipher.creationDate).toBeInstanceOf(Date); + expect(cipher.deletedDate).toBeUndefined(); + expect(cipher.reprompt).toBeUndefined(); + expect(cipher.attachments).toBeUndefined(); + expect(cipher.fields).toBeUndefined(); + expect(cipher.passwordHistory).toBeUndefined(); + expect(cipher.key).toBeUndefined(); + expect(cipher.permissions).toBeUndefined(); + expect(cipher.archivedDate).toBeUndefined(); }); it("Decrypt should handle cipher key error", async () => { @@ -121,7 +118,7 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, decryptionFailure: true, - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -155,6 +152,7 @@ describe("Cipher DTO", () => { reprompt: CipherRepromptType.None, key: "EncryptedString", archivedDate: undefined, + collectionIds: [], login: { uris: [ { @@ -223,8 +221,8 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, revisionDate: new Date("2022-01-31T12:00:00.000Z"), - collectionIds: undefined, - localData: null, + collectionIds: [], + localData: undefined, creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, permissions: new CipherPermissionsApi(), @@ -265,13 +263,13 @@ describe("Cipher DTO", () => { ], fields: [ { - linkedId: null, + linkedId: undefined, name: { encryptedString: "EncryptedString", encryptionType: 0 }, type: 0, value: { encryptedString: "EncryptedString", encryptionType: 0 }, }, { - linkedId: null, + linkedId: undefined, name: { encryptedString: "EncryptedString", encryptionType: 0 }, type: 1, value: { encryptedString: "EncryptedString", encryptionType: 0 }, @@ -348,7 +346,7 @@ describe("Cipher DTO", () => { attachments: [], fields: [], passwordHistory: [], - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -380,6 +378,7 @@ describe("Cipher DTO", () => { deletedDate: undefined, reprompt: CipherRepromptType.None, key: "EncKey", + collectionIds: [], secureNote: { type: SecureNoteType.Generic, }, @@ -404,15 +403,15 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, revisionDate: new Date("2022-01-31T12:00:00.000Z"), - collectionIds: undefined, - localData: null, + collectionIds: [], + localData: undefined, creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, reprompt: 0, secureNote: { type: SecureNoteType.Generic }, - attachments: null, - fields: null, - passwordHistory: null, + attachments: undefined, + fields: undefined, + passwordHistory: undefined, key: { encryptedString: "EncKey", encryptionType: 0 }, permissions: new CipherPermissionsApi(), archivedDate: undefined, @@ -475,7 +474,7 @@ describe("Cipher DTO", () => { attachments: [], fields: [], passwordHistory: [], - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -507,6 +506,7 @@ describe("Cipher DTO", () => { deletedDate: undefined, permissions: new CipherPermissionsApi(), reprompt: CipherRepromptType.None, + collectionIds: [], card: { cardholderName: "EncryptedString", brand: "EncryptedString", @@ -536,8 +536,8 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, revisionDate: new Date("2022-01-31T12:00:00.000Z"), - collectionIds: undefined, - localData: null, + collectionIds: [], + localData: undefined, creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, reprompt: 0, @@ -549,9 +549,9 @@ describe("Cipher DTO", () => { expYear: { encryptedString: "EncryptedString", encryptionType: 0 }, code: { encryptedString: "EncryptedString", encryptionType: 0 }, }, - attachments: null, - fields: null, - passwordHistory: null, + attachments: undefined, + fields: undefined, + passwordHistory: undefined, key: { encryptedString: "EncKey", encryptionType: 0 }, permissions: new CipherPermissionsApi(), archivedDate: undefined, @@ -620,7 +620,7 @@ describe("Cipher DTO", () => { attachments: [], fields: [], passwordHistory: [], - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -654,6 +654,7 @@ describe("Cipher DTO", () => { reprompt: CipherRepromptType.None, key: "EncKey", archivedDate: undefined, + collectionIds: [], identity: { title: "EncryptedString", firstName: "EncryptedString", @@ -693,8 +694,8 @@ describe("Cipher DTO", () => { edit: true, viewPassword: true, revisionDate: new Date("2022-01-31T12:00:00.000Z"), - collectionIds: undefined, - localData: null, + collectionIds: [], + localData: undefined, creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, reprompt: 0, @@ -719,9 +720,9 @@ describe("Cipher DTO", () => { passportNumber: { encryptedString: "EncryptedString", encryptionType: 0 }, licenseNumber: { encryptedString: "EncryptedString", encryptionType: 0 }, }, - attachments: null, - fields: null, - passwordHistory: null, + attachments: undefined, + fields: undefined, + passwordHistory: undefined, key: { encryptedString: "EncKey", encryptionType: 0 }, permissions: new CipherPermissionsApi(), }); @@ -789,7 +790,7 @@ describe("Cipher DTO", () => { attachments: [], fields: [], passwordHistory: [], - collectionIds: undefined, + collectionIds: [], revisionDate: new Date("2022-01-31T12:00:00.000Z"), creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: undefined, @@ -858,8 +859,8 @@ describe("Cipher DTO", () => { expect(actual).toMatchObject(expected); }); - it("returns null if object is null", () => { - expect(Cipher.fromJSON(null)).toBeNull(); + it("returns undefined if object is undefined", () => { + expect(Cipher.fromJSON(undefined)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index 8ba81c7bbd..5e28423293 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Cipher as SdkCipher } from "@bitwarden/sdk-internal"; @@ -13,6 +11,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherType } from "../../enums/cipher-type"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { CipherPermissionsApi } from "../api/cipher-permissions.api"; import { CipherData } from "../data/cipher.data"; import { LocalData, fromSdkLocalData, toSdkLocalData } from "../data/local.data"; @@ -33,71 +32,60 @@ import { SshKey } from "./ssh-key"; export class Cipher extends Domain implements Decryptable { readonly initializerKey = InitializerKey.Cipher; - id: string; - organizationId: string; - folderId: string; - name: EncString; - notes: EncString; - type: CipherType; - favorite: boolean; - organizationUseTotp: boolean; - edit: boolean; - viewPassword: boolean; - permissions: CipherPermissionsApi; + id: string = ""; + organizationId?: string; + folderId?: string; + name: EncString = new EncString(""); + notes?: EncString; + type: CipherType = CipherType.Login; + favorite: boolean = false; + organizationUseTotp: boolean = false; + edit: boolean = false; + viewPassword: boolean = true; + permissions?: CipherPermissionsApi; revisionDate: Date; - localData: LocalData; - login: Login; - identity: Identity; - card: Card; - secureNote: SecureNote; - sshKey: SshKey; - attachments: Attachment[]; - fields: Field[]; - passwordHistory: Password[]; - collectionIds: string[]; + localData?: LocalData; + login?: Login; + identity?: Identity; + card?: Card; + secureNote?: SecureNote; + sshKey?: SshKey; + attachments?: Attachment[]; + fields?: Field[]; + passwordHistory?: Password[]; + collectionIds: string[] = []; creationDate: Date; - deletedDate: Date | undefined; - archivedDate: Date | undefined; - reprompt: CipherRepromptType; - key: EncString; + deletedDate?: Date; + archivedDate?: Date; + reprompt: CipherRepromptType = CipherRepromptType.None; + key?: EncString; - constructor(obj?: CipherData, localData: LocalData = null) { + constructor(obj?: CipherData, localData?: LocalData) { super(); if (obj == null) { + this.creationDate = this.revisionDate = new Date(); return; } - this.buildDomainModel( - this, - obj, - { - id: null, - organizationId: null, - folderId: null, - name: null, - notes: null, - key: null, - }, - ["id", "organizationId", "folderId"], - ); - + this.id = obj.id; + this.organizationId = obj.organizationId; + this.folderId = obj.folderId; + this.name = new EncString(obj.name); + this.notes = conditionalEncString(obj.notes); this.type = obj.type; this.favorite = obj.favorite; this.organizationUseTotp = obj.organizationUseTotp; this.edit = obj.edit; - if (obj.viewPassword != null) { - this.viewPassword = obj.viewPassword; - } else { - this.viewPassword = true; // Default for already synced Ciphers without viewPassword - } + this.viewPassword = obj.viewPassword; this.permissions = obj.permissions; - this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; - this.collectionIds = obj.collectionIds; + this.revisionDate = new Date(obj.revisionDate); this.localData = localData; - this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null; + this.collectionIds = obj.collectionIds ?? []; + this.creationDate = new Date(obj.creationDate); this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : undefined; this.archivedDate = obj.archivedDate != null ? new Date(obj.archivedDate) : undefined; this.reprompt = obj.reprompt; + this.key = conditionalEncString(obj.key); switch (this.type) { case CipherType.Login: @@ -121,20 +109,14 @@ export class Cipher extends Domain implements Decryptable { if (obj.attachments != null) { this.attachments = obj.attachments.map((a) => new Attachment(a)); - } else { - this.attachments = null; } if (obj.fields != null) { this.fields = obj.fields.map((f) => new Field(f)); - } else { - this.fields = null; } if (obj.passwordHistory != null) { this.passwordHistory = obj.passwordHistory.map((ph) => new Password(ph)); - } else { - this.passwordHistory = null; } } @@ -161,46 +143,54 @@ export class Cipher extends Domain implements Decryptable { await this.decryptObj( this, - // @ts-expect-error Ciphers have optional Ids which are getting swallowed by the ViewEncryptableKeys type - // The ViewEncryptableKeys type should be fixed to allow for optional Ids, but is out of scope for now. model, ["name", "notes"], - this.organizationId, + this.organizationId ?? null, encKey, ); switch (this.type) { case CipherType.Login: - model.login = await this.login.decrypt( - this.organizationId, - bypassValidation, - `Cipher Id: ${this.id}`, - encKey, - ); + if (this.login != null) { + model.login = await this.login.decrypt( + this.organizationId, + bypassValidation, + `Cipher Id: ${this.id}`, + encKey, + ); + } break; case CipherType.SecureNote: - model.secureNote = await this.secureNote.decrypt( - this.organizationId, - `Cipher Id: ${this.id}`, - encKey, - ); + if (this.secureNote != null) { + model.secureNote = await this.secureNote.decrypt(); + } break; case CipherType.Card: - model.card = await this.card.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); + if (this.card != null) { + model.card = await this.card.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); + } break; case CipherType.Identity: - model.identity = await this.identity.decrypt( - this.organizationId, - `Cipher Id: ${this.id}`, - encKey, - ); + if (this.identity != null) { + model.identity = await this.identity.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); + } break; case CipherType.SshKey: - model.sshKey = await this.sshKey.decrypt( - this.organizationId, - `Cipher Id: ${this.id}`, - encKey, - ); + if (this.sshKey != null) { + model.sshKey = await this.sshKey.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); + } break; default: break; @@ -209,9 +199,12 @@ export class Cipher extends Domain implements Decryptable { if (this.attachments != null && this.attachments.length > 0) { const attachments: AttachmentView[] = []; for (const attachment of this.attachments) { - attachments.push( - await attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey), + const decryptedAttachment = await attachment.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, ); + attachments.push(decryptedAttachment); } model.attachments = attachments; } @@ -219,7 +212,8 @@ export class Cipher extends Domain implements Decryptable { if (this.fields != null && this.fields.length > 0) { const fields: FieldView[] = []; for (const field of this.fields) { - fields.push(await field.decrypt(this.organizationId, encKey)); + const decryptedField = await field.decrypt(this.organizationId, encKey); + fields.push(decryptedField); } model.fields = fields; } @@ -227,7 +221,8 @@ export class Cipher extends Domain implements Decryptable { if (this.passwordHistory != null && this.passwordHistory.length > 0) { const passwordHistory: PasswordHistoryView[] = []; for (const ph of this.passwordHistory) { - passwordHistory.push(await ph.decrypt(this.organizationId, encKey)); + const decryptedPh = await ph.decrypt(this.organizationId, encKey); + passwordHistory.push(decryptedPh); } model.passwordHistory = passwordHistory; } @@ -238,20 +233,32 @@ export class Cipher extends Domain implements Decryptable { toCipherData(): CipherData { const c = new CipherData(); c.id = this.id; - c.organizationId = this.organizationId; - c.folderId = this.folderId; + if (this.organizationId != null) { + c.organizationId = this.organizationId; + } + + if (this.folderId != null) { + c.folderId = this.folderId; + } c.edit = this.edit; c.viewPassword = this.viewPassword; c.organizationUseTotp = this.organizationUseTotp; c.favorite = this.favorite; - c.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null; + c.revisionDate = this.revisionDate.toISOString(); c.type = this.type; c.collectionIds = this.collectionIds; - c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null; + c.creationDate = this.creationDate.toISOString(); c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : undefined; c.reprompt = this.reprompt; - c.key = this.key?.encryptedString; - c.permissions = this.permissions; + + if (this.key != null && this.key.encryptedString != null) { + c.key = this.key.encryptedString; + } + + if (this.permissions != null) { + c.permissions = this.permissions; + } + c.archivedDate = this.archivedDate != null ? this.archivedDate.toISOString() : undefined; this.buildDataModel(this, c, { @@ -261,19 +268,29 @@ export class Cipher extends Domain implements Decryptable { switch (c.type) { case CipherType.Login: - c.login = this.login.toLoginData(); + if (this.login != null) { + c.login = this.login.toLoginData(); + } break; case CipherType.SecureNote: - c.secureNote = this.secureNote.toSecureNoteData(); + if (this.secureNote != null) { + c.secureNote = this.secureNote.toSecureNoteData(); + } break; case CipherType.Card: - c.card = this.card.toCardData(); + if (this.card != null) { + c.card = this.card.toCardData(); + } break; case CipherType.Identity: - c.identity = this.identity.toIdentityData(); + if (this.identity != null) { + c.identity = this.identity.toIdentityData(); + } break; case CipherType.SshKey: - c.sshKey = this.sshKey.toSshKeyData(); + if (this.sshKey != null) { + c.sshKey = this.sshKey.toSshKeyData(); + } break; default: break; @@ -291,51 +308,71 @@ export class Cipher extends Domain implements Decryptable { return c; } - static fromJSON(obj: Jsonify) { + static fromJSON(obj: Jsonify | undefined): Cipher | undefined { if (obj == null) { - return null; + return undefined; } const domain = new Cipher(); - const name = EncString.fromJSON(obj.name); - const notes = EncString.fromJSON(obj.notes); - const creationDate = obj.creationDate == null ? null : new Date(obj.creationDate); - const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); - const deletedDate = obj.deletedDate == null ? undefined : new Date(obj.deletedDate); - const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a)); - const fields = obj.fields?.map((f: any) => Field.fromJSON(f)); - const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph)); - const key = EncString.fromJSON(obj.key); - const archivedDate = obj.archivedDate == null ? undefined : new Date(obj.archivedDate); - Object.assign(domain, obj, { - name, - notes, - creationDate, - revisionDate, - deletedDate, - attachments, - fields, - passwordHistory, - key, - archivedDate, - }); + domain.id = obj.id; + domain.organizationId = obj.organizationId; + domain.folderId = obj.folderId; + domain.type = obj.type; + domain.favorite = obj.favorite; + domain.organizationUseTotp = obj.organizationUseTotp; + domain.edit = obj.edit; + domain.viewPassword = obj.viewPassword; + + if (obj.permissions != null) { + domain.permissions = new CipherPermissionsApi(obj.permissions); + } + + domain.collectionIds = obj.collectionIds; + domain.localData = obj.localData; + domain.reprompt = obj.reprompt; + domain.creationDate = new Date(obj.creationDate); + domain.revisionDate = new Date(obj.revisionDate); + domain.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : undefined; + domain.archivedDate = obj.archivedDate != null ? new Date(obj.archivedDate) : undefined; + domain.name = EncString.fromJSON(obj.name); + domain.notes = encStringFrom(obj.notes); + domain.key = encStringFrom(obj.key); + domain.attachments = obj.attachments + ?.map((a: any) => Attachment.fromJSON(a)) + .filter((a): a is Attachment => a != null); + domain.fields = obj.fields + ?.map((f: any) => Field.fromJSON(f)) + .filter((f): f is Field => f != null); + domain.passwordHistory = obj.passwordHistory + ?.map((ph: any) => Password.fromJSON(ph)) + .filter((ph): ph is Password => ph != null); switch (obj.type) { case CipherType.Card: - domain.card = Card.fromJSON(obj.card); + if (obj.card != null) { + domain.card = Card.fromJSON(obj.card); + } break; case CipherType.Identity: - domain.identity = Identity.fromJSON(obj.identity); + if (obj.identity != null) { + domain.identity = Identity.fromJSON(obj.identity); + } break; case CipherType.Login: - domain.login = Login.fromJSON(obj.login); + if (obj.login != null) { + domain.login = Login.fromJSON(obj.login); + } break; case CipherType.SecureNote: - domain.secureNote = SecureNote.fromJSON(obj.secureNote); + if (obj.secureNote != null) { + domain.secureNote = SecureNote.fromJSON(obj.secureNote); + } break; case CipherType.SshKey: - domain.sshKey = SshKey.fromJSON(obj.sshKey); + if (obj.sshKey != null) { + domain.sshKey = SshKey.fromJSON(obj.sshKey); + } break; default: break; @@ -359,22 +396,22 @@ export class Cipher extends Domain implements Decryptable { name: this.name.toSdk(), notes: this.notes?.toSdk(), type: this.type, - favorite: this.favorite ?? false, - organizationUseTotp: this.organizationUseTotp ?? false, - edit: this.edit ?? true, + favorite: this.favorite, + organizationUseTotp: this.organizationUseTotp, + edit: this.edit, permissions: this.permissions ? { delete: this.permissions.delete, restore: this.permissions.restore, } : undefined, - viewPassword: this.viewPassword ?? true, + viewPassword: this.viewPassword, localData: toSdkLocalData(this.localData), attachments: this.attachments?.map((a) => a.toSdkAttachment()), fields: this.fields?.map((f) => f.toSdkField()), passwordHistory: this.passwordHistory?.map((ph) => ph.toSdkPasswordHistory()), - revisionDate: this.revisionDate?.toISOString(), - creationDate: this.creationDate?.toISOString(), + revisionDate: this.revisionDate.toISOString(), + creationDate: this.creationDate.toISOString(), deletedDate: this.deletedDate?.toISOString(), archivedDate: this.archivedDate?.toISOString(), reprompt: this.reprompt, @@ -388,19 +425,29 @@ export class Cipher extends Domain implements Decryptable { switch (this.type) { case CipherType.Login: - sdkCipher.login = this.login.toSdkLogin(); + if (this.login != null) { + sdkCipher.login = this.login.toSdkLogin(); + } break; case CipherType.SecureNote: - sdkCipher.secureNote = this.secureNote.toSdkSecureNote(); + if (this.secureNote != null) { + sdkCipher.secureNote = this.secureNote.toSdkSecureNote(); + } break; case CipherType.Card: - sdkCipher.card = this.card.toSdkCard(); + if (this.card != null) { + sdkCipher.card = this.card.toSdkCard(); + } break; case CipherType.Identity: - sdkCipher.identity = this.identity.toSdkIdentity(); + if (this.identity != null) { + sdkCipher.identity = this.identity.toSdkIdentity(); + } break; case CipherType.SshKey: - sdkCipher.sshKey = this.sshKey.toSdkSshKey(); + if (this.sshKey != null) { + sdkCipher.sshKey = this.sshKey.toSdkSshKey(); + } break; default: break; @@ -413,22 +460,22 @@ export class Cipher extends Domain implements Decryptable { * Maps an SDK Cipher object to a Cipher * @param sdkCipher - The SDK Cipher object */ - static fromSdkCipher(sdkCipher: SdkCipher | null): Cipher | undefined { + static fromSdkCipher(sdkCipher?: SdkCipher): Cipher | undefined { if (sdkCipher == null) { return undefined; } const cipher = new Cipher(); - cipher.id = sdkCipher.id ? uuidAsString(sdkCipher.id) : undefined; + cipher.id = sdkCipher.id ? uuidAsString(sdkCipher.id) : ""; cipher.organizationId = sdkCipher.organizationId ? uuidAsString(sdkCipher.organizationId) : undefined; cipher.folderId = sdkCipher.folderId ? uuidAsString(sdkCipher.folderId) : undefined; cipher.collectionIds = sdkCipher.collectionIds ? sdkCipher.collectionIds.map(uuidAsString) : []; - cipher.key = EncString.fromJSON(sdkCipher.key); + cipher.key = encStringFrom(sdkCipher.key); cipher.name = EncString.fromJSON(sdkCipher.name); - cipher.notes = EncString.fromJSON(sdkCipher.notes); + cipher.notes = encStringFrom(sdkCipher.notes); cipher.type = sdkCipher.type; cipher.favorite = sdkCipher.favorite; cipher.organizationUseTotp = sdkCipher.organizationUseTotp; @@ -436,10 +483,15 @@ export class Cipher extends Domain implements Decryptable { cipher.permissions = CipherPermissionsApi.fromSdkCipherPermissions(sdkCipher.permissions); cipher.viewPassword = sdkCipher.viewPassword; cipher.localData = fromSdkLocalData(sdkCipher.localData); - cipher.attachments = sdkCipher.attachments?.map((a) => Attachment.fromSdkAttachment(a)) ?? []; - cipher.fields = sdkCipher.fields?.map((f) => Field.fromSdkField(f)) ?? []; - cipher.passwordHistory = - sdkCipher.passwordHistory?.map((ph) => Password.fromSdkPasswordHistory(ph)) ?? []; + cipher.attachments = sdkCipher.attachments + ?.map((a) => Attachment.fromSdkAttachment(a)) + .filter((a): a is Attachment => a != null); + cipher.fields = sdkCipher.fields + ?.map((f) => Field.fromSdkField(f)) + .filter((f): f is Field => f != null); + cipher.passwordHistory = sdkCipher.passwordHistory + ?.map((ph) => Password.fromSdkPasswordHistory(ph)) + .filter((ph): ph is Password => ph != null); cipher.creationDate = new Date(sdkCipher.creationDate); cipher.revisionDate = new Date(sdkCipher.revisionDate); cipher.deletedDate = sdkCipher.deletedDate ? new Date(sdkCipher.deletedDate) : undefined; diff --git a/libs/common/src/vault/models/domain/fido2-credential.spec.ts b/libs/common/src/vault/models/domain/fido2-credential.spec.ts index e245e54de7..3f43775433 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.spec.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.spec.ts @@ -13,25 +13,23 @@ describe("Fido2Credential", () => { }); describe("constructor", () => { - it("returns all fields null when given empty data parameter", () => { + it("returns all fields undefined when given empty data parameter", () => { const data = new Fido2CredentialData(); const credential = new Fido2Credential(data); - expect(credential).toEqual({ - credentialId: null, - keyType: null, - keyAlgorithm: null, - keyCurve: null, - keyValue: null, - rpId: null, - userHandle: null, - userName: null, - rpName: null, - userDisplayName: null, - counter: null, - discoverable: null, - creationDate: null, - }); + expect(credential.credentialId).toBeDefined(); + expect(credential.keyType).toBeDefined(); + expect(credential.keyAlgorithm).toBeDefined(); + expect(credential.keyCurve).toBeDefined(); + expect(credential.keyValue).toBeDefined(); + expect(credential.rpId).toBeDefined(); + expect(credential.counter).toBeDefined(); + expect(credential.discoverable).toBeDefined(); + expect(credential.userHandle).toBeUndefined(); + expect(credential.userName).toBeUndefined(); + expect(credential.rpName).toBeUndefined(); + expect(credential.userDisplayName).toBeUndefined(); + expect(credential.creationDate).toBeInstanceOf(Date); }); it("returns all fields as EncStrings except creationDate when given full Fido2CredentialData", () => { @@ -69,12 +67,22 @@ describe("Fido2Credential", () => { }); }); - it("should not populate fields when data parameter is not given", () => { + it("should not populate fields when data parameter is not given except creationDate", () => { const credential = new Fido2Credential(); - expect(credential).toEqual({ - credentialId: null, - }); + expect(credential.credentialId).toBeUndefined(); + expect(credential.keyType).toBeUndefined(); + expect(credential.keyAlgorithm).toBeUndefined(); + expect(credential.keyCurve).toBeUndefined(); + expect(credential.keyValue).toBeUndefined(); + expect(credential.rpId).toBeUndefined(); + expect(credential.userHandle).toBeUndefined(); + expect(credential.userName).toBeUndefined(); + expect(credential.counter).toBeUndefined(); + expect(credential.rpName).toBeUndefined(); + expect(credential.userDisplayName).toBeUndefined(); + expect(credential.discoverable).toBeUndefined(); + expect(credential.creationDate).toBeInstanceOf(Date); }); }); @@ -163,8 +171,8 @@ describe("Fido2Credential", () => { expect(result).toEqual(credential); }); - it("returns null if input is null", () => { - expect(Fido2Credential.fromJSON(null)).toBeNull(); + it("returns undefined if input is null", () => { + expect(Fido2Credential.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/fido2-credential.ts b/libs/common/src/vault/models/domain/fido2-credential.ts index bdfac9a85a..eff95c4d0b 100644 --- a/libs/common/src/vault/models/domain/fido2-credential.ts +++ b/libs/common/src/vault/models/domain/fido2-credential.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal"; @@ -7,56 +5,53 @@ import { Fido2Credential as SdkFido2Credential } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { Fido2CredentialData } from "../data/fido2-credential.data"; import { Fido2CredentialView } from "../view/fido2-credential.view"; export class Fido2Credential extends Domain { - credentialId: EncString | null = null; - keyType: EncString; - keyAlgorithm: EncString; - keyCurve: EncString; - keyValue: EncString; - rpId: EncString; - userHandle: EncString; - userName: EncString; - counter: EncString; - rpName: EncString; - userDisplayName: EncString; - discoverable: EncString; - creationDate: Date; + credentialId!: EncString; + keyType!: EncString; + keyAlgorithm!: EncString; + keyCurve!: EncString; + keyValue!: EncString; + rpId!: EncString; + userHandle?: EncString; + userName?: EncString; + counter!: EncString; + rpName?: EncString; + userDisplayName?: EncString; + discoverable!: EncString; + creationDate!: Date; constructor(obj?: Fido2CredentialData) { super(); if (obj == null) { + this.creationDate = new Date(); return; } - this.buildDomainModel( - this, - obj, - { - credentialId: null, - keyType: null, - keyAlgorithm: null, - keyCurve: null, - keyValue: null, - rpId: null, - userHandle: null, - userName: null, - counter: null, - rpName: null, - userDisplayName: null, - discoverable: null, - }, - [], - ); - this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null; + this.credentialId = new EncString(obj.credentialId); + this.keyType = new EncString(obj.keyType); + this.keyAlgorithm = new EncString(obj.keyAlgorithm); + this.keyCurve = new EncString(obj.keyCurve); + this.keyValue = new EncString(obj.keyValue); + this.rpId = new EncString(obj.rpId); + this.counter = new EncString(obj.counter); + this.discoverable = new EncString(obj.discoverable); + this.userHandle = conditionalEncString(obj.userHandle); + this.userName = conditionalEncString(obj.userName); + this.rpName = conditionalEncString(obj.rpName); + this.userDisplayName = conditionalEncString(obj.userDisplayName); + this.creationDate = new Date(obj.creationDate); } - async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + async decrypt( + orgId: string | undefined, + encKey?: SymmetricCryptoKey, + ): Promise { const view = await this.decryptObj( this, - // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new Fido2CredentialView(), [ "credentialId", @@ -70,7 +65,7 @@ export class Fido2Credential extends Domain { "rpName", "userDisplayName", ], - orgId, + orgId ?? null, encKey, ); @@ -79,7 +74,7 @@ export class Fido2Credential extends Domain { { counter: string; } - >(this, { counter: "" }, ["counter"], orgId, encKey); + >(this, { counter: "" }, ["counter"], orgId ?? null, encKey); // Counter will end up as NaN if this fails view.counter = parseInt(counter); @@ -87,7 +82,7 @@ export class Fido2Credential extends Domain { this, { discoverable: "" }, ["discoverable"], - orgId, + orgId ?? null, encKey, ); view.discoverable = discoverable === "true"; @@ -116,40 +111,28 @@ export class Fido2Credential extends Domain { return i; } - static fromJSON(obj: Jsonify): Fido2Credential { + static fromJSON(obj: Jsonify | undefined): Fido2Credential | undefined { if (obj == null) { - return null; + return undefined; } - const credentialId = EncString.fromJSON(obj.credentialId); - const keyType = EncString.fromJSON(obj.keyType); - const keyAlgorithm = EncString.fromJSON(obj.keyAlgorithm); - const keyCurve = EncString.fromJSON(obj.keyCurve); - const keyValue = EncString.fromJSON(obj.keyValue); - const rpId = EncString.fromJSON(obj.rpId); - const userHandle = EncString.fromJSON(obj.userHandle); - const userName = EncString.fromJSON(obj.userName); - const counter = EncString.fromJSON(obj.counter); - const rpName = EncString.fromJSON(obj.rpName); - const userDisplayName = EncString.fromJSON(obj.userDisplayName); - const discoverable = EncString.fromJSON(obj.discoverable); - const creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null; + const credential = new Fido2Credential(); - return Object.assign(new Fido2Credential(), obj, { - credentialId, - keyType, - keyAlgorithm, - keyCurve, - keyValue, - rpId, - userHandle, - userName, - counter, - rpName, - userDisplayName, - discoverable, - creationDate, - }); + credential.credentialId = EncString.fromJSON(obj.credentialId); + credential.keyType = EncString.fromJSON(obj.keyType); + credential.keyAlgorithm = EncString.fromJSON(obj.keyAlgorithm); + credential.keyCurve = EncString.fromJSON(obj.keyCurve); + credential.keyValue = EncString.fromJSON(obj.keyValue); + credential.rpId = EncString.fromJSON(obj.rpId); + credential.userHandle = encStringFrom(obj.userHandle); + credential.userName = encStringFrom(obj.userName); + credential.counter = EncString.fromJSON(obj.counter); + credential.rpName = encStringFrom(obj.rpName); + credential.userDisplayName = encStringFrom(obj.userDisplayName); + credential.discoverable = EncString.fromJSON(obj.discoverable); + credential.creationDate = new Date(obj.creationDate); + + return credential; } /** @@ -179,8 +162,8 @@ export class Fido2Credential extends Domain { * Maps an SDK Fido2Credential object to a Fido2Credential * @param obj - The SDK Fido2Credential object */ - static fromSdkFido2Credential(obj: SdkFido2Credential): Fido2Credential | undefined { - if (!obj) { + static fromSdkFido2Credential(obj?: SdkFido2Credential): Fido2Credential | undefined { + if (obj == null) { return undefined; } @@ -192,11 +175,11 @@ export class Fido2Credential extends Domain { credential.keyCurve = EncString.fromJSON(obj.keyCurve); credential.keyValue = EncString.fromJSON(obj.keyValue); credential.rpId = EncString.fromJSON(obj.rpId); - credential.userHandle = EncString.fromJSON(obj.userHandle); - credential.userName = EncString.fromJSON(obj.userName); credential.counter = EncString.fromJSON(obj.counter); - credential.rpName = EncString.fromJSON(obj.rpName); - credential.userDisplayName = EncString.fromJSON(obj.userDisplayName); + credential.userHandle = encStringFrom(obj.userHandle); + credential.userName = encStringFrom(obj.userName); + credential.rpName = encStringFrom(obj.rpName); + credential.userDisplayName = encStringFrom(obj.userDisplayName); credential.discoverable = EncString.fromJSON(obj.discoverable); credential.creationDate = new Date(obj.creationDate); diff --git a/libs/common/src/vault/models/domain/field.spec.ts b/libs/common/src/vault/models/domain/field.spec.ts index b5e26199e7..d99336adad 100644 --- a/libs/common/src/vault/models/domain/field.spec.ts +++ b/libs/common/src/vault/models/domain/field.spec.ts @@ -30,8 +30,8 @@ describe("Field", () => { expect(field).toEqual({ type: undefined, - name: null, - value: null, + name: undefined, + value: undefined, linkedId: undefined, }); }); @@ -41,9 +41,9 @@ describe("Field", () => { expect(field).toEqual({ type: FieldType.Text, - name: { encryptedString: "encName", encryptionType: 0 }, - value: { encryptedString: "encValue", encryptionType: 0 }, - linkedId: null, + name: new EncString("encName"), + value: new EncString("encValue"), + linkedId: undefined, }); }); @@ -82,12 +82,14 @@ describe("Field", () => { expect(actual).toEqual({ name: "myName_fromJSON", value: "myValue_fromJSON", + type: FieldType.Text, + linkedId: undefined, }); expect(actual).toBeInstanceOf(Field); }); - it("returns null if object is null", () => { - expect(Field.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Field.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/field.ts b/libs/common/src/vault/models/domain/field.ts index 130d1cc56d..2ee3a9af8a 100644 --- a/libs/common/src/vault/models/domain/field.ts +++ b/libs/common/src/vault/models/domain/field.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Field as SdkField, LinkedIdType as SdkLinkedIdType } from "@bitwarden/sdk-internal"; @@ -8,14 +6,15 @@ import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { FieldType, LinkedIdType } from "../../enums"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { FieldData } from "../data/field.data"; import { FieldView } from "../view/field.view"; export class Field extends Domain { - name: EncString; - value: EncString; - type: FieldType; - linkedId: LinkedIdType; + name?: EncString; + value?: EncString; + type: FieldType = FieldType.Text; + linkedId?: LinkedIdType; constructor(obj?: FieldData) { super(); @@ -24,25 +23,17 @@ export class Field extends Domain { } this.type = obj.type; - this.linkedId = obj.linkedId; - this.buildDomainModel( - this, - obj, - { - name: null, - value: null, - }, - [], - ); + this.linkedId = obj.linkedId ?? undefined; + this.name = conditionalEncString(obj.name); + this.value = conditionalEncString(obj.value); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt(orgId: string | undefined, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj( this, - // @ts-expect-error ViewEncryptableKeys type should be fixed to allow for optional values, but is out of scope for now. new FieldView(this), ["name", "value"], - orgId, + orgId ?? null, encKey, ); } @@ -63,18 +54,18 @@ export class Field extends Domain { return f; } - static fromJSON(obj: Partial>): Field { + static fromJSON(obj: Partial> | undefined): Field | undefined { if (obj == null) { - return null; + return undefined; } - const name = EncString.fromJSON(obj.name); - const value = EncString.fromJSON(obj.value); + const field = new Field(); + field.type = obj.type ?? FieldType.Text; + field.linkedId = obj.linkedId ?? undefined; + field.name = encStringFrom(obj.name); + field.value = encStringFrom(obj.value); - return Object.assign(new Field(), obj, { - name, - value, - }); + return field; } /** @@ -96,14 +87,14 @@ export class Field extends Domain { * Maps SDK Field to Field * @param obj The SDK Field object to map */ - static fromSdkField(obj: SdkField): Field | undefined { - if (!obj) { + static fromSdkField(obj?: SdkField): Field | undefined { + if (obj == null) { return undefined; } const field = new Field(); - field.name = EncString.fromJSON(obj.name); - field.value = EncString.fromJSON(obj.value); + field.name = encStringFrom(obj.name); + field.value = encStringFrom(obj.value); field.type = obj.type; field.linkedId = obj.linkedId; diff --git a/libs/common/src/vault/models/domain/identity.spec.ts b/libs/common/src/vault/models/domain/identity.spec.ts index 9fbcb92e4a..c2c2363fa0 100644 --- a/libs/common/src/vault/models/domain/identity.spec.ts +++ b/libs/common/src/vault/models/domain/identity.spec.ts @@ -34,24 +34,24 @@ describe("Identity", () => { const identity = new Identity(data); expect(identity).toEqual({ - address1: null, - address2: null, - address3: null, - city: null, - company: null, - country: null, - email: null, - firstName: null, - lastName: null, - licenseNumber: null, - middleName: null, - passportNumber: null, - phone: null, - postalCode: null, - ssn: null, - state: null, - title: null, - username: null, + address1: undefined, + address2: undefined, + address3: undefined, + city: undefined, + company: undefined, + country: undefined, + email: undefined, + firstName: undefined, + lastName: undefined, + licenseNumber: undefined, + middleName: undefined, + passportNumber: undefined, + phone: undefined, + postalCode: undefined, + ssn: undefined, + state: undefined, + title: undefined, + username: undefined, }); }); @@ -179,8 +179,8 @@ describe("Identity", () => { expect(actual).toBeInstanceOf(Identity); }); - it("returns null if object is null", () => { - expect(Identity.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Identity.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index f0d5b3123a..e2def3eb38 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Identity as SdkIdentity } from "@bitwarden/sdk-internal"; @@ -7,28 +5,29 @@ import { Identity as SdkIdentity } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { IdentityData } from "../data/identity.data"; import { IdentityView } from "../view/identity.view"; export class Identity extends Domain { - title: EncString; - firstName: EncString; - middleName: EncString; - lastName: EncString; - address1: EncString; - address2: EncString; - address3: EncString; - city: EncString; - state: EncString; - postalCode: EncString; - country: EncString; - company: EncString; - email: EncString; - phone: EncString; - ssn: EncString; - username: EncString; - passportNumber: EncString; - licenseNumber: EncString; + title?: EncString; + firstName?: EncString; + middleName?: EncString; + lastName?: EncString; + address1?: EncString; + address2?: EncString; + address3?: EncString; + city?: EncString; + state?: EncString; + postalCode?: EncString; + country?: EncString; + company?: EncString; + email?: EncString; + phone?: EncString; + ssn?: EncString; + username?: EncString; + passportNumber?: EncString; + licenseNumber?: EncString; constructor(obj?: IdentityData) { super(); @@ -36,35 +35,28 @@ export class Identity extends Domain { return; } - this.buildDomainModel( - this, - obj, - { - title: null, - firstName: null, - middleName: null, - lastName: null, - address1: null, - address2: null, - address3: null, - city: null, - state: null, - postalCode: null, - country: null, - company: null, - email: null, - phone: null, - ssn: null, - username: null, - passportNumber: null, - licenseNumber: null, - }, - [], - ); + this.title = conditionalEncString(obj.title); + this.firstName = conditionalEncString(obj.firstName); + this.middleName = conditionalEncString(obj.middleName); + this.lastName = conditionalEncString(obj.lastName); + this.address1 = conditionalEncString(obj.address1); + this.address2 = conditionalEncString(obj.address2); + this.address3 = conditionalEncString(obj.address3); + this.city = conditionalEncString(obj.city); + this.state = conditionalEncString(obj.state); + this.postalCode = conditionalEncString(obj.postalCode); + this.country = conditionalEncString(obj.country); + this.company = conditionalEncString(obj.company); + this.email = conditionalEncString(obj.email); + this.phone = conditionalEncString(obj.phone); + this.ssn = conditionalEncString(obj.ssn); + this.username = conditionalEncString(obj.username); + this.passportNumber = conditionalEncString(obj.passportNumber); + this.licenseNumber = conditionalEncString(obj.licenseNumber); } decrypt( - orgId: string, + orgId: string | undefined, context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { @@ -91,7 +83,7 @@ export class Identity extends Domain { "passportNumber", "licenseNumber", ], - orgId, + orgId ?? null, encKey, "DomainType: Identity; " + context, ); @@ -122,50 +114,32 @@ export class Identity extends Domain { return i; } - static fromJSON(obj: Jsonify): Identity { + static fromJSON(obj: Jsonify | undefined): Identity | undefined { if (obj == null) { - return null; + return undefined; } - const title = EncString.fromJSON(obj.title); - const firstName = EncString.fromJSON(obj.firstName); - const middleName = EncString.fromJSON(obj.middleName); - const lastName = EncString.fromJSON(obj.lastName); - const address1 = EncString.fromJSON(obj.address1); - const address2 = EncString.fromJSON(obj.address2); - const address3 = EncString.fromJSON(obj.address3); - const city = EncString.fromJSON(obj.city); - const state = EncString.fromJSON(obj.state); - const postalCode = EncString.fromJSON(obj.postalCode); - const country = EncString.fromJSON(obj.country); - const company = EncString.fromJSON(obj.company); - const email = EncString.fromJSON(obj.email); - const phone = EncString.fromJSON(obj.phone); - const ssn = EncString.fromJSON(obj.ssn); - const username = EncString.fromJSON(obj.username); - const passportNumber = EncString.fromJSON(obj.passportNumber); - const licenseNumber = EncString.fromJSON(obj.licenseNumber); + const identity = new Identity(); + identity.title = encStringFrom(obj.title); + identity.firstName = encStringFrom(obj.firstName); + identity.middleName = encStringFrom(obj.middleName); + identity.lastName = encStringFrom(obj.lastName); + identity.address1 = encStringFrom(obj.address1); + identity.address2 = encStringFrom(obj.address2); + identity.address3 = encStringFrom(obj.address3); + identity.city = encStringFrom(obj.city); + identity.state = encStringFrom(obj.state); + identity.postalCode = encStringFrom(obj.postalCode); + identity.country = encStringFrom(obj.country); + identity.company = encStringFrom(obj.company); + identity.email = encStringFrom(obj.email); + identity.phone = encStringFrom(obj.phone); + identity.ssn = encStringFrom(obj.ssn); + identity.username = encStringFrom(obj.username); + identity.passportNumber = encStringFrom(obj.passportNumber); + identity.licenseNumber = encStringFrom(obj.licenseNumber); - return Object.assign(new Identity(), obj, { - title, - firstName, - middleName, - lastName, - address1, - address2, - address3, - city, - state, - postalCode, - country, - company, - email, - phone, - ssn, - username, - passportNumber, - licenseNumber, - }); + return identity; } /** @@ -200,30 +174,30 @@ export class Identity extends Domain { * Maps an SDK Identity object to an Identity * @param obj - The SDK Identity object */ - static fromSdkIdentity(obj: SdkIdentity): Identity | undefined { + static fromSdkIdentity(obj?: SdkIdentity): Identity | undefined { if (obj == null) { return undefined; } const identity = new Identity(); - identity.title = EncString.fromJSON(obj.title); - identity.firstName = EncString.fromJSON(obj.firstName); - identity.middleName = EncString.fromJSON(obj.middleName); - identity.lastName = EncString.fromJSON(obj.lastName); - identity.address1 = EncString.fromJSON(obj.address1); - identity.address2 = EncString.fromJSON(obj.address2); - identity.address3 = EncString.fromJSON(obj.address3); - identity.city = EncString.fromJSON(obj.city); - identity.state = EncString.fromJSON(obj.state); - identity.postalCode = EncString.fromJSON(obj.postalCode); - identity.country = EncString.fromJSON(obj.country); - identity.company = EncString.fromJSON(obj.company); - identity.email = EncString.fromJSON(obj.email); - identity.phone = EncString.fromJSON(obj.phone); - identity.ssn = EncString.fromJSON(obj.ssn); - identity.username = EncString.fromJSON(obj.username); - identity.passportNumber = EncString.fromJSON(obj.passportNumber); - identity.licenseNumber = EncString.fromJSON(obj.licenseNumber); + identity.title = encStringFrom(obj.title); + identity.firstName = encStringFrom(obj.firstName); + identity.middleName = encStringFrom(obj.middleName); + identity.lastName = encStringFrom(obj.lastName); + identity.address1 = encStringFrom(obj.address1); + identity.address2 = encStringFrom(obj.address2); + identity.address3 = encStringFrom(obj.address3); + identity.city = encStringFrom(obj.city); + identity.state = encStringFrom(obj.state); + identity.postalCode = encStringFrom(obj.postalCode); + identity.country = encStringFrom(obj.country); + identity.company = encStringFrom(obj.company); + identity.email = encStringFrom(obj.email); + identity.phone = encStringFrom(obj.phone); + identity.ssn = encStringFrom(obj.ssn); + identity.username = encStringFrom(obj.username); + identity.passportNumber = encStringFrom(obj.passportNumber); + identity.licenseNumber = encStringFrom(obj.licenseNumber); return identity; } diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index e67ba77141..982b435384 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -27,9 +27,9 @@ describe("LoginUri", () => { const loginUri = new LoginUri(data); expect(loginUri).toEqual({ - match: null, - uri: null, - uriChecksum: null, + match: undefined, + uri: undefined, + uriChecksum: undefined, }); }); @@ -77,7 +77,7 @@ describe("LoginUri", () => { loginUri.uriChecksum = mockEnc("checksum"); encryptService.hash.mockResolvedValue("checksum"); - const actual = await loginUri.validateChecksum("uri", null, null); + const actual = await loginUri.validateChecksum("uri", undefined, undefined); expect(actual).toBe(true); expect(encryptService.hash).toHaveBeenCalledWith("uri", "sha256"); @@ -88,7 +88,7 @@ describe("LoginUri", () => { loginUri.uriChecksum = mockEnc("checksum"); encryptService.hash.mockResolvedValue("incorrect checksum"); - const actual = await loginUri.validateChecksum("uri", null, null); + const actual = await loginUri.validateChecksum("uri", undefined, undefined); expect(actual).toBe(false); }); @@ -112,8 +112,8 @@ describe("LoginUri", () => { expect(actual).toBeInstanceOf(LoginUri); }); - it("returns null if object is null", () => { - expect(LoginUri.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(LoginUri.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 973e25c8ff..cac487747f 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { LoginUri as SdkLoginUri } from "@bitwarden/sdk-internal"; @@ -9,13 +7,14 @@ import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { LoginUriData } from "../data/login-uri.data"; import { LoginUriView } from "../view/login-uri.view"; export class LoginUri extends Domain { - uri: EncString; - uriChecksum: EncString | undefined; - match: UriMatchStrategySetting; + uri?: EncString; + uriChecksum?: EncString; + match?: UriMatchStrategySetting; constructor(obj?: LoginUriData) { super(); @@ -23,20 +22,13 @@ export class LoginUri extends Domain { return; } - this.match = obj.match; - this.buildDomainModel( - this, - obj, - { - uri: null, - uriChecksum: null, - }, - [], - ); + this.uri = conditionalEncString(obj.uri); + this.uriChecksum = conditionalEncString(obj.uriChecksum); + this.match = obj.match ?? undefined; } decrypt( - orgId: string, + orgId: string | undefined, context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { @@ -44,13 +36,13 @@ export class LoginUri extends Domain { this, new LoginUriView(this), ["uri"], - orgId, + orgId ?? null, encKey, context, ); } - async validateChecksum(clearTextUri: string, orgId: string, encKey: SymmetricCryptoKey) { + async validateChecksum(clearTextUri: string, orgId?: string, encKey?: SymmetricCryptoKey) { if (this.uriChecksum == null) { return false; } @@ -58,7 +50,7 @@ export class LoginUri extends Domain { const keyService = Utils.getContainerService().getEncryptService(); const localChecksum = await keyService.hash(clearTextUri, "sha256"); - const remoteChecksum = await this.uriChecksum.decrypt(orgId, encKey); + const remoteChecksum = await this.uriChecksum.decrypt(orgId ?? null, encKey); return remoteChecksum === localChecksum; } @@ -77,17 +69,17 @@ export class LoginUri extends Domain { return u; } - static fromJSON(obj: Jsonify): LoginUri { + static fromJSON(obj: Jsonify | undefined): LoginUri | undefined { if (obj == null) { - return null; + return undefined; } - const uri = EncString.fromJSON(obj.uri); - const uriChecksum = EncString.fromJSON(obj.uriChecksum); - return Object.assign(new LoginUri(), obj, { - uri, - uriChecksum, - }); + const loginUri = new LoginUri(); + loginUri.uri = encStringFrom(obj.uri); + loginUri.match = obj.match ?? undefined; + loginUri.uriChecksum = encStringFrom(obj.uriChecksum); + + return loginUri; } /** @@ -103,16 +95,16 @@ export class LoginUri extends Domain { }; } - static fromSdkLoginUri(obj: SdkLoginUri): LoginUri | undefined { + static fromSdkLoginUri(obj?: SdkLoginUri): LoginUri | undefined { if (obj == null) { return undefined; } - const view = new LoginUri(); - view.uri = EncString.fromJSON(obj.uri); - view.uriChecksum = obj.uriChecksum ? EncString.fromJSON(obj.uriChecksum) : undefined; - view.match = obj.match; + const loginUri = new LoginUri(); + loginUri.uri = encStringFrom(obj.uri); + loginUri.uriChecksum = encStringFrom(obj.uriChecksum); + loginUri.match = obj.match; - return view; + return loginUri; } } diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts index 99ceb2b0a3..9f03e225b7 100644 --- a/libs/common/src/vault/models/domain/login.spec.ts +++ b/libs/common/src/vault/models/domain/login.spec.ts @@ -19,11 +19,11 @@ describe("Login DTO", () => { const login = new Login(data); expect(login).toEqual({ - passwordRevisionDate: null, + passwordRevisionDate: undefined, autofillOnPageLoad: undefined, - username: null, - password: null, - totp: null, + username: undefined, + password: undefined, + totp: undefined, }); }); @@ -193,8 +193,8 @@ describe("Login DTO", () => { expect(actual).toBeInstanceOf(Login); }); - it("returns null if object is null", () => { - expect(Login.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Login.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index b34fb01125..13342c6901 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { Login as SdkLogin } from "@bitwarden/sdk-internal"; @@ -7,6 +5,7 @@ import { Login as SdkLogin } from "@bitwarden/sdk-internal"; import { EncString } from "../../../key-management/crypto/models/enc-string"; import Domain from "../../../platform/models/domain/domain-base"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; +import { conditionalEncString, encStringFrom } from "../../utils/domain-utils"; import { LoginData } from "../data/login.data"; import { LoginView } from "../view/login.view"; @@ -14,13 +13,13 @@ import { Fido2Credential } from "./fido2-credential"; import { LoginUri } from "./login-uri"; export class Login extends Domain { - uris: LoginUri[]; - username: EncString; - password: EncString; + uris?: LoginUri[]; + username?: EncString; + password?: EncString; passwordRevisionDate?: Date; - totp: EncString; - autofillOnPageLoad: boolean; - fido2Credentials: Fido2Credential[]; + totp?: EncString; + autofillOnPageLoad?: boolean; + fido2Credentials?: Fido2Credential[]; constructor(obj?: LoginData) { super(); @@ -29,24 +28,14 @@ export class Login extends Domain { } this.passwordRevisionDate = - obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : null; + obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : undefined; this.autofillOnPageLoad = obj.autofillOnPageLoad; - this.buildDomainModel( - this, - obj, - { - username: null, - password: null, - totp: null, - }, - [], - ); + this.username = conditionalEncString(obj.username); + this.password = conditionalEncString(obj.password); + this.totp = conditionalEncString(obj.totp); if (obj.uris) { - this.uris = []; - obj.uris.forEach((u) => { - this.uris.push(new LoginUri(u)); - }); + this.uris = obj.uris.map((u) => new LoginUri(u)); } if (obj.fido2Credentials) { @@ -55,7 +44,7 @@ export class Login extends Domain { } async decrypt( - orgId: string, + orgId: string | undefined, bypassValidation: boolean, context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, @@ -64,7 +53,7 @@ export class Login extends Domain { this, new LoginView(this), ["username", "password", "totp"], - orgId, + orgId ?? null, encKey, `DomainType: Login; ${context}`, ); @@ -78,12 +67,21 @@ export class Login extends Domain { } const uri = await this.uris[i].decrypt(orgId, context, encKey); + const uriString = uri.uri; + + if (uriString == null) { + continue; + } + // URIs are shared remotely after decryption // we need to validate that the string hasn't been changed by a compromised server // This validation is tied to the existence of cypher.key for backwards compatibility - // So we bypass the validation if there's no cipher.key or procceed with the validation and + // So we bypass the validation if there's no cipher.key or proceed with the validation and // Skip the value if it's been tampered with. - if (bypassValidation || (await this.uris[i].validateChecksum(uri.uri, orgId, encKey))) { + const isValidUri = + bypassValidation || (await this.uris[i].validateChecksum(uriString, orgId, encKey)); + + if (isValidUri) { view.uris.push(uri); } } @@ -100,9 +98,12 @@ export class Login extends Domain { toLoginData(): LoginData { const l = new LoginData(); - l.passwordRevisionDate = - this.passwordRevisionDate != null ? this.passwordRevisionDate.toISOString() : null; - l.autofillOnPageLoad = this.autofillOnPageLoad; + if (this.passwordRevisionDate != null) { + l.passwordRevisionDate = this.passwordRevisionDate.toISOString(); + } + if (this.autofillOnPageLoad != null) { + l.autofillOnPageLoad = this.autofillOnPageLoad; + } this.buildDataModel(this, l, { username: null, password: null, @@ -123,28 +124,27 @@ export class Login extends Domain { return l; } - static fromJSON(obj: Partial>): Login { + static fromJSON(obj: Partial> | undefined): Login | undefined { if (obj == null) { - return null; + return undefined; } - const username = EncString.fromJSON(obj.username); - const password = EncString.fromJSON(obj.password); - const totp = EncString.fromJSON(obj.totp); - const passwordRevisionDate = - obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate); - const uris = obj.uris?.map((uri: any) => LoginUri.fromJSON(uri)); - const fido2Credentials = - obj.fido2Credentials?.map((key) => Fido2Credential.fromJSON(key)) ?? []; + const login = new Login(); + login.passwordRevisionDate = + obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : undefined; + login.autofillOnPageLoad = obj.autofillOnPageLoad; + login.username = encStringFrom(obj.username); + login.password = encStringFrom(obj.password); + login.totp = encStringFrom(obj.totp); + login.uris = obj.uris + ?.map((uri: any) => LoginUri.fromJSON(uri)) + .filter((u): u is LoginUri => u != null); + login.fido2Credentials = + obj.fido2Credentials + ?.map((key) => Fido2Credential.fromJSON(key)) + .filter((c): c is Fido2Credential => c != null) ?? undefined; - return Object.assign(new Login(), obj, { - username, - password, - totp, - passwordRevisionDate, - uris, - fido2Credentials, - }); + return login; } /** @@ -168,25 +168,27 @@ export class Login extends Domain { * Maps an SDK Login object to a Login * @param obj - The SDK Login object */ - static fromSdkLogin(obj: SdkLogin): Login | undefined { + static fromSdkLogin(obj?: SdkLogin): Login | undefined { if (!obj) { return undefined; } const login = new Login(); - - login.uris = - obj.uris?.filter((u) => u.uri != null).map((uri) => LoginUri.fromSdkLoginUri(uri)) ?? []; - login.username = EncString.fromJSON(obj.username); - login.password = EncString.fromJSON(obj.password); - login.passwordRevisionDate = obj.passwordRevisionDate - ? new Date(obj.passwordRevisionDate) - : undefined; - login.totp = EncString.fromJSON(obj.totp); + login.passwordRevisionDate = + obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : undefined; login.autofillOnPageLoad = obj.autofillOnPageLoad; - login.fido2Credentials = obj.fido2Credentials?.map((f) => - Fido2Credential.fromSdkFido2Credential(f), - ); + login.username = encStringFrom(obj.username); + login.password = encStringFrom(obj.password); + login.totp = encStringFrom(obj.totp); + login.uris = + obj.uris + ?.filter((u) => u.uri != null) + .map((uri) => LoginUri.fromSdkLoginUri(uri)) + .filter((u): u is LoginUri => u != null) ?? undefined; + login.fido2Credentials = + obj.fido2Credentials + ?.map((f) => Fido2Credential.fromSdkFido2Credential(f)) + .filter((c): c is Fido2Credential => c != null) ?? undefined; return login; } diff --git a/libs/common/src/vault/models/domain/password.spec.ts b/libs/common/src/vault/models/domain/password.spec.ts index a75fca048f..2e37c5e837 100644 --- a/libs/common/src/vault/models/domain/password.spec.ts +++ b/libs/common/src/vault/models/domain/password.spec.ts @@ -17,9 +17,9 @@ describe("Password", () => { const data = new PasswordHistoryData(); const password = new Password(data); - expect(password).toMatchObject({ - password: null, - }); + expect(password).toBeInstanceOf(Password); + expect(password.password).toBeInstanceOf(EncString); + expect(password.lastUsedDate).toBeInstanceOf(Date); }); it("Convert", () => { @@ -66,8 +66,8 @@ describe("Password", () => { expect(actual).toBeInstanceOf(Password); }); - it("returns null if object is null", () => { - expect(Password.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(Password.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index ca594075e0..84e8919b90 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { PasswordHistory } from "@bitwarden/sdk-internal"; @@ -11,8 +9,8 @@ import { PasswordHistoryData } from "../data/password-history.data"; import { PasswordHistoryView } from "../view/password-history.view"; export class Password extends Domain { - password: EncString; - lastUsedDate: Date; + password!: EncString; + lastUsedDate!: Date; constructor(obj?: PasswordHistoryData) { super(); @@ -20,18 +18,16 @@ export class Password extends Domain { return; } - this.buildDomainModel(this, obj, { - password: null, - }); + this.password = new EncString(obj.password); this.lastUsedDate = new Date(obj.lastUsedDate); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt(orgId: string | undefined, encKey?: SymmetricCryptoKey): Promise { return this.decryptObj( this, new PasswordHistoryView(this), ["password"], - orgId, + orgId ?? null, encKey, "DomainType: PasswordHistory", ); @@ -46,18 +42,16 @@ export class Password extends Domain { return ph; } - static fromJSON(obj: Partial>): Password { + static fromJSON(obj: Jsonify | undefined): Password | undefined { if (obj == null) { - return null; + return undefined; } - const password = EncString.fromJSON(obj.password); - const lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate); + const passwordHistory = new Password(); + passwordHistory.password = EncString.fromJSON(obj.password); + passwordHistory.lastUsedDate = new Date(obj.lastUsedDate); - return Object.assign(new Password(), obj, { - password, - lastUsedDate, - }); + return passwordHistory; } /** @@ -76,7 +70,7 @@ export class Password extends Domain { * Maps an SDK PasswordHistory object to a Password * @param obj - The SDK PasswordHistory object */ - static fromSdkPasswordHistory(obj: PasswordHistory): Password | undefined { + static fromSdkPasswordHistory(obj?: PasswordHistory): Password | undefined { if (!obj) { return undefined; } diff --git a/libs/common/src/vault/models/domain/secure-note.spec.ts b/libs/common/src/vault/models/domain/secure-note.spec.ts index ff71e53238..4c8e8d470c 100644 --- a/libs/common/src/vault/models/domain/secure-note.spec.ts +++ b/libs/common/src/vault/models/domain/secure-note.spec.ts @@ -38,7 +38,7 @@ describe("SecureNote", () => { const secureNote = new SecureNote(); secureNote.type = SecureNoteType.Generic; - const view = await secureNote.decrypt(null); + const view = await secureNote.decrypt(); expect(view).toEqual({ type: 0, @@ -46,8 +46,8 @@ describe("SecureNote", () => { }); describe("fromJSON", () => { - it("returns null if object is null", () => { - expect(SecureNote.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(SecureNote.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts index 1426ff85ea..fb568f482b 100644 --- a/libs/common/src/vault/models/domain/secure-note.ts +++ b/libs/common/src/vault/models/domain/secure-note.ts @@ -1,17 +1,14 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { SecureNote as SdkSecureNote } from "@bitwarden/sdk-internal"; import Domain from "../../../platform/models/domain/domain-base"; -import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SecureNoteType } from "../../enums"; import { SecureNoteData } from "../data/secure-note.data"; import { SecureNoteView } from "../view/secure-note.view"; export class SecureNote extends Domain { - type: SecureNoteType; + type: SecureNoteType = SecureNoteType.Generic; constructor(obj?: SecureNoteData) { super(); @@ -22,11 +19,7 @@ export class SecureNote extends Domain { this.type = obj.type; } - async decrypt( - orgId: string, - context = "No Cipher Context", - encKey?: SymmetricCryptoKey, - ): Promise { + async decrypt(): Promise { return new SecureNoteView(this); } @@ -36,12 +29,14 @@ export class SecureNote extends Domain { return n; } - static fromJSON(obj: Jsonify): SecureNote { + static fromJSON(obj: Jsonify | undefined): SecureNote | undefined { if (obj == null) { - return null; + return undefined; } - return Object.assign(new SecureNote(), obj); + const secureNote = new SecureNote(); + secureNote.type = obj.type; + return secureNote; } /** @@ -59,7 +54,7 @@ export class SecureNote extends Domain { * Maps an SDK SecureNote object to a SecureNote * @param obj - The SDK SecureNote object */ - static fromSdkSecureNote(obj: SdkSecureNote): SecureNote | undefined { + static fromSdkSecureNote(obj?: SdkSecureNote): SecureNote | undefined { if (obj == null) { return undefined; } diff --git a/libs/common/src/vault/models/domain/ssh-key.spec.ts b/libs/common/src/vault/models/domain/ssh-key.spec.ts index 6576d1a41e..38228e54a4 100644 --- a/libs/common/src/vault/models/domain/ssh-key.spec.ts +++ b/libs/common/src/vault/models/domain/ssh-key.spec.ts @@ -1,3 +1,5 @@ +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; + import { mockEnc } from "../../../../spec"; import { SshKeyApi } from "../api/ssh-key.api"; import { SshKeyData } from "../data/ssh-key.data"; @@ -31,11 +33,10 @@ describe("Sshkey", () => { const data = new SshKeyData(); const sshKey = new SshKey(data); - expect(sshKey).toEqual({ - privateKey: null, - publicKey: null, - keyFingerprint: null, - }); + expect(sshKey).toBeInstanceOf(SshKey); + expect(sshKey.privateKey).toBeInstanceOf(EncString); + expect(sshKey.publicKey).toBeInstanceOf(EncString); + expect(sshKey.keyFingerprint).toBeInstanceOf(EncString); }); it("toSshKeyData", () => { @@ -60,8 +61,8 @@ describe("Sshkey", () => { }); describe("fromJSON", () => { - it("returns null if object is null", () => { - expect(SshKey.fromJSON(null)).toBeNull(); + it("returns undefined if object is null", () => { + expect(SshKey.fromJSON(null)).toBeUndefined(); }); }); diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index ab1685955a..a7028321a4 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Jsonify } from "type-fest"; import { SshKey as SdkSshKey } from "@bitwarden/sdk-internal"; @@ -11,9 +9,9 @@ import { SshKeyData } from "../data/ssh-key.data"; import { SshKeyView } from "../view/ssh-key.view"; export class SshKey extends Domain { - privateKey: EncString; - publicKey: EncString; - keyFingerprint: EncString; + privateKey!: EncString; + publicKey!: EncString; + keyFingerprint!: EncString; constructor(obj?: SshKeyData) { super(); @@ -21,20 +19,13 @@ export class SshKey extends Domain { return; } - this.buildDomainModel( - this, - obj, - { - privateKey: null, - publicKey: null, - keyFingerprint: null, - }, - [], - ); + this.privateKey = new EncString(obj.privateKey); + this.publicKey = new EncString(obj.publicKey); + this.keyFingerprint = new EncString(obj.keyFingerprint); } decrypt( - orgId: string, + orgId: string | undefined, context = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { @@ -42,7 +33,7 @@ export class SshKey extends Domain { this, new SshKeyView(), ["privateKey", "publicKey", "keyFingerprint"], - orgId, + orgId ?? null, encKey, "DomainType: SshKey; " + context, ); @@ -58,19 +49,17 @@ export class SshKey extends Domain { return c; } - static fromJSON(obj: Partial>): SshKey { + static fromJSON(obj: Jsonify | undefined): SshKey | undefined { if (obj == null) { - return null; + return undefined; } - const privateKey = EncString.fromJSON(obj.privateKey); - const publicKey = EncString.fromJSON(obj.publicKey); - const keyFingerprint = EncString.fromJSON(obj.keyFingerprint); - return Object.assign(new SshKey(), obj, { - privateKey, - publicKey, - keyFingerprint, - }); + const sshKey = new SshKey(); + sshKey.privateKey = EncString.fromJSON(obj.privateKey); + sshKey.publicKey = EncString.fromJSON(obj.publicKey); + sshKey.keyFingerprint = EncString.fromJSON(obj.keyFingerprint); + + return sshKey; } /** @@ -90,7 +79,7 @@ export class SshKey extends Domain { * Maps an SDK SshKey object to a SshKey * @param obj - The SDK SshKey object */ - static fromSdkSshKey(obj: SdkSshKey): SshKey | undefined { + static fromSdkSshKey(obj?: SdkSshKey): SshKey | undefined { if (obj == null) { return undefined; } diff --git a/libs/common/src/vault/models/request/cipher-partial.request.ts b/libs/common/src/vault/models/request/cipher-partial.request.ts index 6037dff6cb..a50ea10d0c 100644 --- a/libs/common/src/vault/models/request/cipher-partial.request.ts +++ b/libs/common/src/vault/models/request/cipher-partial.request.ts @@ -1,7 +1,7 @@ import { Cipher } from "../domain/cipher"; export class CipherPartialRequest { - folderId: string; + folderId?: string; favorite: boolean; constructor(cipher: Cipher) { diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 52c83c5a10..efe7bc2b89 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -869,13 +869,14 @@ export class CipherService implements CipherServiceAbstraction { response = await this.apiService.postCipherAdmin(request); const data = new CipherData(response, cipher.collectionIds); return new Cipher(data); - } else if (cipher.collectionIds != null) { + } else if (cipher.collectionIds != null && cipher.collectionIds.length > 0) { const request = new CipherCreateRequest({ cipher, encryptedFor }); response = await this.apiService.postCipherCreate(request); } else { const request = new CipherRequest({ cipher, encryptedFor }); response = await this.apiService.postCipher(request); } + cipher.id = response.id; const data = new CipherData(response, cipher.collectionIds); diff --git a/libs/common/src/vault/utils/domain-utils.ts b/libs/common/src/vault/utils/domain-utils.ts new file mode 100644 index 0000000000..ee071b29ec --- /dev/null +++ b/libs/common/src/vault/utils/domain-utils.ts @@ -0,0 +1,27 @@ +import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string"; +import { EncString as SdkEncString } from "@bitwarden/sdk-internal"; + +/** + * Converts a string value to an EncString, handling null/undefined gracefully. + * + * @param value - The string value to convert, or undefined + * @returns An EncString instance if value is defined, otherwise undefined + * + */ +export const conditionalEncString = (value?: string): EncString | undefined => { + return value != null ? new EncString(value) : undefined; +}; + +/** + * Converts an EncString representation (from JSON or SDK) to a domain EncString instance. + * Handles both serialized JSON representations and SDK EncString objects. + * + * @param value - The EncString representation (string, object, or SdkEncString), or undefined + * @returns A domain EncString instance if value is defined, otherwise undefined + * + */ +export const encStringFrom = ( + value?: T, +): EncString | undefined => { + return value != null ? EncString.fromJSON(value) : undefined; +}; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index df31783539..4214873fee 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -343,7 +343,7 @@ describe("VaultExportService", () => { const exportData: BitwardenJsonExport = JSON.parse(data); expect(exportData.items.length).toBe(1); expect(exportData.items[0].id).toBe("mock-id"); - expect(exportData.items[0].organizationId).toBe(null); + expect(exportData.items[0].organizationId).toBeUndefined(); }); it.each([[400], [401], [404], [500]])(