diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index 2339c1756bd..35a29091761 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -38,7 +38,6 @@ ./libs/common/spec/shared/interceptConsole.ts ./libs/common/spec/models/view/passwordHistoryView.spec.ts ./libs/common/spec/models/view/cipherView.spec.ts -./libs/common/spec/models/view/folderView.spec.ts ./libs/common/spec/models/view/attachmentView.spec.ts ./libs/common/spec/models/view/loginView.spec.ts ./libs/common/spec/models/domain/loginUri.spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index fe586d49e99..30fc424a8ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["Popout", "Reprompt", "takeuntil"] + "cSpell.words": ["Decryptable", "Encryptable", "Popout", "Reprompt", "takeuntil"] } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index fcde4e7c15f..97b71f9fb9a 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -310,7 +310,11 @@ export default class MainBackground { this.cipherService, this.stateService ); - this.folderApiService = new FolderApiService(this.folderService, this.apiService); + this.folderApiService = new FolderApiService( + this.folderService, + this.cryptoService, + this.apiService + ); this.collectionService = new CollectionService( this.cryptoService, this.i18nService, diff --git a/apps/browser/src/popup/settings/folder-add-edit.component.ts b/apps/browser/src/popup/settings/folder-add-edit.component.ts index 8ff8ba3b1a2..8936f7f73fe 100644 --- a/apps/browser/src/popup/settings/folder-add-edit.component.ts +++ b/apps/browser/src/popup/settings/folder-add-edit.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/components/folder-add-edit.component"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; @@ -22,9 +23,17 @@ export class FolderAddEditComponent extends BaseFolderAddEditComponent { platformUtilsService: PlatformUtilsService, private router: Router, private route: ActivatedRoute, - logService: LogService + logService: LogService, + cryptoService: CryptoService ) { - super(folderService, folderApiService, i18nService, platformUtilsService, logService); + super( + folderService, + folderApiService, + i18nService, + platformUtilsService, + logService, + cryptoService + ); } async ngOnInit() { diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 4395d935ce2..abb5b2ec1ae 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -231,7 +231,11 @@ export class Main { this.stateService ); - this.folderApiService = new FolderApiService(this.folderService, this.apiService); + this.folderApiService = new FolderApiService( + this.folderService, + this.cryptoService, + this.apiService + ); this.collectionService = new CollectionService( this.cryptoService, diff --git a/apps/cli/src/commands/create.command.ts b/apps/cli/src/commands/create.command.ts index 92855ee6f79..5e59bcfbdf7 100644 --- a/apps/cli/src/commands/create.command.ts +++ b/apps/cli/src/commands/create.command.ts @@ -13,6 +13,7 @@ import { CollectionExport } from "@bitwarden/common/models/export/collection.exp import { FolderExport } from "@bitwarden/common/models/export/folder.export"; import { CollectionRequest } from "@bitwarden/common/models/request/collection.request"; import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request"; +import { FolderView } from "@bitwarden/common/models/view/folder.view"; import { OrganizationCollectionRequest } from "../models/request/organization-collection.request"; import { Response } from "../models/response"; @@ -148,11 +149,11 @@ export class CreateCommand { } private async createFolder(req: FolderExport) { - const folder = await this.folderService.encrypt(FolderExport.toView(req)); + const folderView = FolderExport.toView(req); try { - await this.folderApiService.save(folder); - const newFolder = await this.folderService.get(folder.id); - const decFolder = await newFolder.decrypt(); + await this.folderApiService.save(folderView); + const newFolder = await this.folderService.get(folderView.id); + const decFolder = await this.cryptoService.decryptDomain(FolderView, newFolder); const res = new FolderResponse(decFolder); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index d6cd120da0d..a5df1c0fb11 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -9,6 +9,7 @@ import { CollectionExport } from "@bitwarden/common/models/export/collection.exp import { FolderExport } from "@bitwarden/common/models/export/folder.export"; import { CollectionRequest } from "@bitwarden/common/models/request/collection.request"; import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request"; +import { FolderView } from "@bitwarden/common/models/view/folder.view"; import { OrganizationCollectionRequest } from "../models/request/organization-collection.request"; import { Response } from "../models/response"; @@ -123,13 +124,12 @@ export class EditCommand { return Response.notFound(); } - let folderView = await folder.decrypt(); + let folderView = await this.cryptoService.decryptDomain(FolderView, folder); folderView = FolderExport.toView(req, folderView); - const encFolder = await this.folderService.encrypt(folderView); try { - await this.folderApiService.save(encFolder); + await this.folderApiService.save(folderView); const updatedFolder = await this.folderService.get(folder.id); - const decFolder = await updatedFolder.decrypt(); + const decFolder = await this.cryptoService.decryptDomain(FolderView, updatedFolder); const res = new FolderResponse(decFolder); return Response.success(res); } catch (e) { diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 92d7e31258e..a693339a8c1 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -355,7 +355,7 @@ export class GetCommand extends DownloadCommand { if (Utils.isGuid(id)) { const folder = await this.folderService.getFromState(id); if (folder != null) { - decFolder = await folder.decrypt(); + decFolder = await this.cryptoService.decryptDomain(FolderView, folder); } } else if (id.trim() !== "") { let folders = await this.folderService.getAllDecryptedFromState(); diff --git a/apps/desktop/src/app/vault/folder-add-edit.component.ts b/apps/desktop/src/app/vault/folder-add-edit.component.ts index a014389577d..2d0ca85287d 100644 --- a/apps/desktop/src/app/vault/folder-add-edit.component.ts +++ b/apps/desktop/src/app/vault/folder-add-edit.component.ts @@ -1,6 +1,7 @@ import { Component } from "@angular/core"; import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/components/folder-add-edit.component"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; @@ -17,8 +18,16 @@ export class FolderAddEditComponent extends BaseFolderAddEditComponent { folderApiService: FolderApiServiceAbstraction, i18nService: I18nService, platformUtilsService: PlatformUtilsService, - logService: LogService + logService: LogService, + cryptoService: CryptoService ) { - super(folderService, folderApiService, i18nService, platformUtilsService, logService); + super( + folderService, + folderApiService, + i18nService, + platformUtilsService, + logService, + cryptoService + ); } } diff --git a/apps/web/src/app/settings/change-password.component.ts b/apps/web/src/app/settings/change-password.component.ts index 8d0266410ff..ae9fcd4c88b 100644 --- a/apps/web/src/app/settings/change-password.component.ts +++ b/apps/web/src/app/settings/change-password.component.ts @@ -6,6 +6,7 @@ import { ChangePasswordComponent as BaseChangePasswordComponent } from "@bitward import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service"; @@ -57,7 +58,8 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { private keyConnectorService: KeyConnectorService, private router: Router, private organizationApiService: OrganizationApiServiceAbstraction, - private organizationUserService: OrganizationUserService + private organizationUserService: OrganizationUserService, + private encryptService: EncryptService ) { super( i18nService, @@ -206,7 +208,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { if (folders[i].id == null) { continue; } - const folder = await this.folderService.encrypt(folders[i], encKey[0]); + const folder = await folders[i].encrypt(this.encryptService, encKey[0]); request.folders.push(new FolderWithIdRequest(folder)); } diff --git a/apps/web/src/app/settings/update-key.component.ts b/apps/web/src/app/settings/update-key.component.ts index 5ebd432fb4b..6221a33909c 100644 --- a/apps/web/src/app/settings/update-key.component.ts +++ b/apps/web/src/app/settings/update-key.component.ts @@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; @@ -32,7 +33,8 @@ export class UpdateKeyComponent { private syncService: SyncService, private folderService: FolderService, private cipherService: CipherService, - private logService: LogService + private logService: LogService, + private encryptService: EncryptService ) {} async submit() { @@ -87,7 +89,7 @@ export class UpdateKeyComponent { if (folders[i].id == null) { continue; } - const folder = await this.folderService.encrypt(folders[i], encKey[0]); + const folder = await folders[i].encrypt(this.encryptService, encKey[0]); request.folders.push(new FolderWithIdRequest(folder)); } diff --git a/apps/web/src/app/vault/folder-add-edit.component.ts b/apps/web/src/app/vault/folder-add-edit.component.ts index 9c1910b32ca..0ec4a14ff42 100644 --- a/apps/web/src/app/vault/folder-add-edit.component.ts +++ b/apps/web/src/app/vault/folder-add-edit.component.ts @@ -1,6 +1,7 @@ import { Component } from "@angular/core"; import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/components/folder-add-edit.component"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; @@ -18,8 +19,16 @@ export class FolderAddEditComponent extends BaseFolderAddEditComponent { folderApiService: FolderApiServiceAbstraction, i18nService: I18nService, platformUtilsService: PlatformUtilsService, - logService: LogService + logService: LogService, + cryptoService: CryptoService ) { - super(folderService, folderApiService, i18nService, platformUtilsService, logService); + super( + folderService, + folderApiService, + i18nService, + platformUtilsService, + logService, + cryptoService + ); } } diff --git a/libs/angular/src/components/folder-add-edit.component.ts b/libs/angular/src/components/folder-add-edit.component.ts index 02b8922b509..ea4d17137b5 100644 --- a/libs/angular/src/components/folder-add-edit.component.ts +++ b/libs/angular/src/components/folder-add-edit.component.ts @@ -1,5 +1,6 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/abstractions/folder/folder-api.service.abstraction"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; @@ -25,7 +26,8 @@ export class FolderAddEditComponent implements OnInit { protected folderApiService: FolderApiServiceAbstraction, protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, - private logService: LogService + private logService: LogService, + private cryptoService: CryptoService ) {} async ngOnInit() { @@ -43,8 +45,7 @@ export class FolderAddEditComponent implements OnInit { } try { - const folder = await this.folderService.encrypt(this.folder); - this.formPromise = this.folderApiService.save(folder); + this.formPromise = this.folderApiService.save(this.folder); await this.formPromise; this.platformUtilsService.showToast( "success", @@ -93,7 +94,7 @@ export class FolderAddEditComponent implements OnInit { this.editMode = true; this.title = this.i18nService.t("editFolder"); const folder = await this.folderService.get(this.folderId); - this.folder = await folder.decrypt(); + this.folder = await this.cryptoService.decryptDomain(FolderView, folder); } else { this.title = this.i18nService.t("addFolder"); } diff --git a/libs/common/spec/models/view/folderView.spec.ts b/libs/common/spec/models/view/folderView.spec.ts deleted file mode 100644 index 15663166f3f..00000000000 --- a/libs/common/spec/models/view/folderView.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { FolderView } from "@bitwarden/common/models/view/folder.view"; - -describe("FolderView", () => { - describe("fromJSON", () => { - it("initializes nested objects", () => { - const revisionDate = new Date("2022-08-04T01:06:40.441Z"); - const actual = FolderView.fromJSON({ - revisionDate: revisionDate.toISOString(), - name: "name", - id: "id", - }); - - const expected = { - revisionDate: revisionDate, - name: "name", - id: "id", - }; - - expect(actual).toMatchObject(expected); - }); - }); -}); diff --git a/libs/common/spec/services/folder.service.spec.ts b/libs/common/spec/services/folder.service.spec.ts index 1c1f0b2a464..f0ab24f944e 100644 --- a/libs/common/spec/services/folder.service.spec.ts +++ b/libs/common/spec/services/folder.service.spec.ts @@ -7,7 +7,7 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { FolderData } from "@bitwarden/common/models/data/folder.data"; -import { EncString } from "@bitwarden/common/models/domain/enc-string"; +import { Folder } from "@bitwarden/common/models/domain/folder"; import { FolderView } from "@bitwarden/common/models/view/folder.view"; import { ContainerService } from "@bitwarden/common/services/container.service"; import { FolderService } from "@bitwarden/common/services/folder/folder.service"; @@ -33,6 +33,13 @@ describe("Folder Service", () => { activeAccount = new BehaviorSubject("123"); activeAccountUnlocked = new BehaviorSubject(true); + cryptoService.decryptDomain(Arg.any(), Arg.any()).mimicks((view: any, model: Folder) => { + const v = new FolderView(); + v.id = model.id; + v.revisionDate = model.revisionDate; + return Promise.resolve(v); + }); + stateService.getEncryptedFolders().resolves({ "1": folderData("1", "test"), }); @@ -43,25 +50,6 @@ describe("Folder Service", () => { folderService = new FolderService(cryptoService, i18nService, cipherService, stateService); }); - it("encrypt", async () => { - const model = new FolderView(); - model.id = "2"; - model.name = "Test Folder"; - - cryptoService.encrypt(Arg.any()).resolves(new EncString("ENC")); - cryptoService.decryptToUtf8(Arg.any()).resolves("DEC"); - - const result = await folderService.encrypt(model); - - expect(result).toEqual({ - id: "2", - name: { - encryptedString: "ENC", - encryptionType: 0, - }, - }); - }); - describe("get", () => { it("exists", async () => { const result = await folderService.get("1"); @@ -69,7 +57,6 @@ describe("Folder Service", () => { expect(result).toEqual({ id: "1", name: { - decryptedValue: [], encryptedString: "test", encryptionType: 0, }, @@ -91,7 +78,6 @@ describe("Folder Service", () => { { id: "1", name: { - decryptedValue: [], encryptedString: "test", encryptionType: 0, }, @@ -100,7 +86,6 @@ describe("Folder Service", () => { { id: "2", name: { - decryptedValue: [], encryptedString: "test 2", encryptionType: 0, }, @@ -109,8 +94,8 @@ describe("Folder Service", () => { ]); expect(await firstValueFrom(folderService.folderViews$)).toEqual([ - { id: "1", name: [], revisionDate: null }, - { id: "2", name: [], revisionDate: null }, + { id: "1", name: null, revisionDate: null }, + { id: "2", name: null, revisionDate: null }, { id: null, name: [], revisionDate: null }, ]); }); @@ -122,7 +107,6 @@ describe("Folder Service", () => { { id: "2", name: { - decryptedValue: [], encryptedString: "test 2", encryptionType: 0, }, @@ -131,8 +115,8 @@ describe("Folder Service", () => { ]); expect(await firstValueFrom(folderService.folderViews$)).toEqual([ - { id: "2", name: [], revisionDate: null }, - { id: null, name: [], revisionDate: null }, + { id: "2", name: null, revisionDate: null }, + { id: null, name: null, revisionDate: null }, ]); }); diff --git a/libs/common/src/abstractions/crypto.service.ts b/libs/common/src/abstractions/crypto.service.ts index e4dd43cdf23..132a8462e6c 100644 --- a/libs/common/src/abstractions/crypto.service.ts +++ b/libs/common/src/abstractions/crypto.service.ts @@ -1,6 +1,12 @@ import { HashPurpose } from "../enums/hashPurpose"; import { KdfType } from "../enums/kdfType"; import { KeySuffixOptions } from "../enums/keySuffixOptions"; +import { + Decryptable, + DecryptableDomain, + Encryptable, + EncryptableDomain, +} from "../interfaces/crypto.interface"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; @@ -84,4 +90,9 @@ export abstract class CryptoService { decryptFromBytes: (encBuffer: EncArrayBuffer, key: SymmetricCryptoKey) => Promise; randomNumber: (min: number, max: number) => Promise; validateKey: (key: SymmetricCryptoKey) => Promise; + + decryptDomain: (view: Decryptable, model: D) => Promise; + encryptView: >>( + folder: V + ) => Promise>; } diff --git a/libs/common/src/abstractions/encrypt.service.ts b/libs/common/src/abstractions/encrypt.service.ts index 17e72907c8a..8251bb39134 100644 --- a/libs/common/src/abstractions/encrypt.service.ts +++ b/libs/common/src/abstractions/encrypt.service.ts @@ -1,5 +1,11 @@ import { IEncrypted } from "../interfaces/IEncrypted"; -import { Decryptable } from "../interfaces/decryptable.interface"; +import { + Decryptable, + DecryptableDomain, + Encryptable, + EncryptableDomain, +} from "../interfaces/crypto.interface"; +import { OldDecryptable } from "../interfaces/decryptable.interface"; import { InitializerMetadata } from "../interfaces/initializer-metadata.interface"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; @@ -15,7 +21,17 @@ export abstract class EncryptService { abstract decryptToBytes: (encThing: IEncrypted, key: SymmetricCryptoKey) => Promise; abstract resolveLegacyKey: (key: SymmetricCryptoKey, encThing: IEncrypted) => SymmetricCryptoKey; abstract decryptItems: ( - items: Decryptable[], + items: OldDecryptable[], key: SymmetricCryptoKey ) => Promise; + + abstract encryptView: >>( + view: V, + key: SymmetricCryptoKey + ) => Promise>; + abstract decryptDomain: ( + view: Decryptable, + domain: D, + key: SymmetricCryptoKey + ) => Promise; } diff --git a/libs/common/src/abstractions/folder/folder-api.service.abstraction.ts b/libs/common/src/abstractions/folder/folder-api.service.abstraction.ts index d29ff71290a..8564e15beb5 100644 --- a/libs/common/src/abstractions/folder/folder-api.service.abstraction.ts +++ b/libs/common/src/abstractions/folder/folder-api.service.abstraction.ts @@ -1,8 +1,8 @@ -import { Folder } from "../../models/domain/folder"; import { FolderResponse } from "../../models/response/folder.response"; +import { FolderView } from "../../models/view/folder.view"; export class FolderApiServiceAbstraction { - save: (folder: Folder) => Promise; + save: (folder: FolderView) => Promise; delete: (id: string) => Promise; get: (id: string) => Promise; } diff --git a/libs/common/src/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/abstractions/folder/folder.service.abstraction.ts index 90b6fc23323..5e20442f310 100644 --- a/libs/common/src/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/abstractions/folder/folder.service.abstraction.ts @@ -2,7 +2,6 @@ import { Observable } from "rxjs"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { FolderView } from "../../models/view/folder.view"; export abstract class FolderService { @@ -10,7 +9,6 @@ export abstract class FolderService { folderViews$: Observable; clearCache: () => Promise; - encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise; get: (id: string) => Promise; getAllFromState: () => Promise; /** diff --git a/libs/common/src/importers/bitwarden-json-importer.ts b/libs/common/src/importers/bitwarden-json-importer.ts index e17548fce2b..26ad0dd0e0d 100644 --- a/libs/common/src/importers/bitwarden-json-importer.ts +++ b/libs/common/src/importers/bitwarden-json-importer.ts @@ -5,6 +5,7 @@ import { ImportResult } from "../models/domain/import-result"; import { CipherWithIdExport } from "../models/export/cipher-with-ids.export"; import { CollectionWithIdExport } from "../models/export/collection-with-id.export"; import { FolderWithIdExport } from "../models/export/folder-with-id.export"; +import { FolderView } from "../models/view/folder.view"; import { BaseImporter } from "./base-importer"; import { Importer } from "./importer"; @@ -74,7 +75,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { const folder = FolderWithIdExport.toDomain(f); if (folder != null) { folder.id = null; - const view = await folder.decrypt(); + const view = await this.cryptoService.decryptDomain(FolderView, folder); groupingsMap.set(f.id, this.result.folders.length); this.result.folders.push(view); } diff --git a/libs/common/src/interfaces/crypto.interface.ts b/libs/common/src/interfaces/crypto.interface.ts new file mode 100644 index 00000000000..fab310dd968 --- /dev/null +++ b/libs/common/src/interfaces/crypto.interface.ts @@ -0,0 +1,53 @@ +import { EncryptService } from "../abstractions/encrypt.service"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +export function nullableFactory any>( + c: T, + ...args: ConstructorParameters +): InstanceType | undefined { + if (args[0] == null) { + return null; + } + + return new c(...args); +} + +/** + * Domain model that can be decrypted using views. + */ +export interface DecryptableDomain { + /** + * Unique GUID for the key used to encrypt the data + */ + keyIdentifier(): string | null; +} + +/** + * View model that encrypts to Domain. + */ +export interface Encryptable { + /** + * Converts the View to a Domain model by encrypting data. + * + * @param encryptService EncryptService + * @param key Key used to encrypt the data + */ + encrypt(encryptService: EncryptService, key: SymmetricCryptoKey): Promise; + + /** + * Unique GUID for the key used to encrypt the data + */ + keyIdentifier(): string | null; +} + +/** + * Helper type for defining the static decrypt operation on view models. + */ +export type Decryptable = { + decrypt(encryptService: EncryptService, key: SymmetricCryptoKey, model: TDomain): Promise; +}; + +/** + * Helper type for resolving which domain model the view encrypts to. + */ +export type EncryptableDomain = TView extends Encryptable ? TDomain : never; diff --git a/libs/common/src/interfaces/decryptable.interface.ts b/libs/common/src/interfaces/decryptable.interface.ts index ae5e8ebbf82..93994bc5067 100644 --- a/libs/common/src/interfaces/decryptable.interface.ts +++ b/libs/common/src/interfaces/decryptable.interface.ts @@ -7,6 +7,7 @@ import { InitializerMetadata } from "./initializer-metadata.interface"; * corresponding view object as the type argument. * @example Cipher implements Decryptable */ -export interface Decryptable extends InitializerMetadata { +export interface OldDecryptable + extends InitializerMetadata { decrypt: (key?: SymmetricCryptoKey) => Promise; } diff --git a/libs/common/src/models/domain/cipher.ts b/libs/common/src/models/domain/cipher.ts index 7b4a4d4b1fd..53e208ae139 100644 --- a/libs/common/src/models/domain/cipher.ts +++ b/libs/common/src/models/domain/cipher.ts @@ -2,7 +2,7 @@ import { Jsonify } from "type-fest"; import { CipherRepromptType } from "../../enums/cipherRepromptType"; import { CipherType } from "../../enums/cipherType"; -import { Decryptable } from "../../interfaces/decryptable.interface"; +import { OldDecryptable } from "../../interfaces/decryptable.interface"; import { InitializerKey } from "../../services/cryptography/initializer-key"; import { CipherData } from "../data/cipher.data"; import { LocalData } from "../data/local.data"; @@ -19,7 +19,7 @@ import { Password } from "./password"; import { SecureNote } from "./secure-note"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; -export class Cipher extends Domain implements Decryptable { +export class Cipher extends Domain implements OldDecryptable { readonly initializerKey = InitializerKey.Cipher; id: string; diff --git a/libs/common/src/models/domain/enc-string.ts b/libs/common/src/models/domain/enc-string.ts index f0ea7378f90..c6cb3e6a1cb 100644 --- a/libs/common/src/models/domain/enc-string.ts +++ b/libs/common/src/models/domain/enc-string.ts @@ -1,5 +1,6 @@ import { Jsonify } from "type-fest"; +import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptionType } from "../../enums/encryptionType"; import { IEncrypted } from "../../interfaces/IEncrypted"; import { Utils } from "../../misc/utils"; @@ -160,6 +161,18 @@ export class EncString implements IEncrypted { return this.decryptedValue; } + // TODO: Rename to decrypt once decrypt is removed. + async decryptWithEncryptService( + encryptService: EncryptService, + key: SymmetricCryptoKey + ): Promise { + try { + return encryptService.decryptToUtf8(this, key); + } catch (e) { + return "[error: cannot decrypt]"; + } + } + private async getKeyForDecryption(orgId: string) { const cryptoService = Utils.getContainerService().getCryptoService(); return orgId != null diff --git a/libs/common/spec/models/domain/folder.spec.ts b/libs/common/src/models/domain/folder.spec.ts similarity index 56% rename from libs/common/spec/models/domain/folder.spec.ts rename to libs/common/src/models/domain/folder.spec.ts index 9a95a51f613..100f560cd3a 100644 --- a/libs/common/spec/models/domain/folder.spec.ts +++ b/libs/common/src/models/domain/folder.spec.ts @@ -1,8 +1,6 @@ -import { FolderData } from "@bitwarden/common/models/data/folder.data"; -import { EncString } from "@bitwarden/common/models/domain/enc-string"; -import { Folder } from "@bitwarden/common/models/domain/folder"; +import { FolderData } from "../data/folder.data"; -import { mockEnc, mockFromJson } from "../../utils"; +import { Folder } from "./folder"; describe("Folder", () => { let data: FolderData; @@ -25,24 +23,14 @@ describe("Folder", () => { }); }); - it("Decrypt", async () => { - const folder = new Folder(); - folder.id = "id"; - folder.name = mockEnc("encName"); - folder.revisionDate = new Date("2022-01-31T12:00:00.000Z"); + it("keyIdentifier", () => { + const folder = new Folder(data); - const view = await folder.decrypt(); - - expect(view).toEqual({ - id: "id", - name: "encName", - revisionDate: new Date("2022-01-31T12:00:00.000Z"), - }); + expect(folder.keyIdentifier()).toEqual(null); }); describe("fromJSON", () => { jest.mock("@bitwarden/common/models/domain/enc-string"); - jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson); it("initializes nested objects", () => { const revisionDate = new Date("2022-08-04T01:06:40.441Z"); @@ -54,7 +42,10 @@ describe("Folder", () => { const expected = { revisionDate: revisionDate, - name: "name_fromJSON", + name: { + encryptedString: "name", + encryptionType: 0, + }, id: "id", }; diff --git a/libs/common/src/models/domain/folder.ts b/libs/common/src/models/domain/folder.ts index c01928e00cb..2bfe2ee4551 100644 --- a/libs/common/src/models/domain/folder.ts +++ b/libs/common/src/models/domain/folder.ts @@ -1,47 +1,37 @@ import { Jsonify } from "type-fest"; +import { DecryptableDomain, nullableFactory } from "../../interfaces/crypto.interface"; import { FolderData } from "../data/folder.data"; -import { FolderView } from "../view/folder.view"; -import Domain from "./domain-base"; import { EncString } from "./enc-string"; -export class Folder extends Domain { +export class Folder implements DecryptableDomain { id: string; name: EncString; revisionDate: Date; constructor(obj?: FolderData) { - super(); if (obj == null) { return; } - this.buildDomainModel( - this, - obj, - { - id: null, - name: null, - }, - ["id"] - ); - - this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null; + this.id = obj.id; + this.name = nullableFactory(EncString, obj.name); + this.revisionDate = nullableFactory(Date, obj.revisionDate); } - decrypt(): Promise { - return this.decryptObj( - new FolderView(this), - { - name: null, - }, - null - ); + keyIdentifier(): string | null { + return null; // Folders always belong to the user } static fromJSON(obj: Jsonify) { - const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); - return Object.assign(new Folder(), obj, { name: EncString.fromJSON(obj.name), revisionDate }); + if (obj == null) { + return null; + } + + return Object.assign(new Folder(), obj, { + name: nullableFactory(EncString, obj.name), + revisionDate: nullableFactory(Date, obj.revisionDate), + }); } } diff --git a/libs/common/src/models/view/folder.view.spec.ts b/libs/common/src/models/view/folder.view.spec.ts new file mode 100644 index 00000000000..3fe182be4b6 --- /dev/null +++ b/libs/common/src/models/view/folder.view.spec.ts @@ -0,0 +1,78 @@ +import { mock } from "jest-mock-extended"; + +import { mockEnc } from "../../../spec/utils"; +import { EncryptService } from "../../abstractions/encrypt.service"; +import { EncString } from "../domain/enc-string"; +import { Folder } from "../domain/folder"; + +import { FolderView } from "./folder.view"; + +describe("FolderView", () => { + describe("fromJSON", () => { + it("initializes nested objects", () => { + const revisionDate = new Date("2022-08-04T01:06:40.441Z"); + const actual = FolderView.fromJSON({ + revisionDate: revisionDate.toISOString(), + name: "name", + id: "id", + }); + + const expected = { + revisionDate: revisionDate, + name: "name", + id: "id", + }; + + expect(actual).toMatchObject(expected); + }); + }); + + describe("decrypt", () => { + it("with name", async () => { + const folder = new Folder(); + folder.id = "id"; + folder.name = mockEnc("encName"); + folder.revisionDate = new Date("2022-01-31T12:00:00.000Z"); + + const view = await FolderView.decrypt(null, null, folder); + + expect(view).toEqual({ + id: "id", + name: "encName", + revisionDate: new Date("2022-01-31T12:00:00.000Z"), + }); + }); + + it("without name", async () => { + const folder = new Folder(); + folder.id = "id"; + + const view = await FolderView.decrypt(null, null, folder); + + expect(view).toEqual({ + id: "id", + }); + }); + }); + + it("encrypt", async () => { + const view = new FolderView(); + view.id = "2"; + view.name = "Test Folder"; + view.revisionDate = new Date("2022-10-31T10:16:45+00:00"); + + const encryptService = mock(); + encryptService.encrypt.mockResolvedValue(new EncString("ENC")); + + const result = await view.encrypt(encryptService, null); + + expect(result).toEqual({ + id: "2", + name: { + encryptedString: "ENC", + encryptionType: 0, + }, + revisionDate: new Date("2022-10-31T10:16:45+00:00"), + }); + }); +}); diff --git a/libs/common/src/models/view/folder.view.ts b/libs/common/src/models/view/folder.view.ts index d18ef65aed3..3a58145af11 100644 --- a/libs/common/src/models/view/folder.view.ts +++ b/libs/common/src/models/view/folder.view.ts @@ -1,26 +1,47 @@ import { Jsonify } from "type-fest"; +import { EncryptService } from "../../abstractions/encrypt.service"; +import { Encryptable, nullableFactory } from "../../interfaces/crypto.interface"; import { Folder } from "../domain/folder"; +import { SymmetricCryptoKey } from "../domain/symmetric-crypto-key"; import { ITreeNodeObject } from "../domain/tree-node"; -import { View } from "./view"; - -export class FolderView implements View, ITreeNodeObject { +export class FolderView implements ITreeNodeObject, Encryptable { id: string = null; name: string = null; revisionDate: Date = null; - constructor(f?: Folder) { - if (!f) { - return; - } + keyIdentifier(): string | null { + return null; // Folders always belong to the user + } - this.id = f.id; - this.revisionDate = f.revisionDate; + async encrypt(encryptService: EncryptService, key: SymmetricCryptoKey): Promise { + const folder = new Folder(); + folder.id = this.id; + folder.revisionDate = this.revisionDate; + + folder.name = this.name != null ? await encryptService.encrypt(this.name, key) : null; + + return folder; + } + + static async decrypt(encryptService: EncryptService, key: SymmetricCryptoKey, model: Folder) { + const view = new FolderView(); + view.id = model.id; + view.revisionDate = model.revisionDate; + + view.name = await model.name?.decryptWithEncryptService(encryptService, key); + + return view; } static fromJSON(obj: Jsonify) { - const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate); - return Object.assign(new FolderView(), obj, { revisionDate }); + if (obj == null) { + return null; + } + + return Object.assign(new FolderView(), obj, { + revisionDate: nullableFactory(Date, obj.revisionDate), + }); } } diff --git a/libs/common/src/services/crypto.service.ts b/libs/common/src/services/crypto.service.ts index 8398fd07577..49e7d34b692 100644 --- a/libs/common/src/services/crypto.service.ts +++ b/libs/common/src/services/crypto.service.ts @@ -10,6 +10,12 @@ import { EncryptionType } from "../enums/encryptionType"; import { HashPurpose } from "../enums/hashPurpose"; import { DEFAULT_ARGON2_ITERATIONS, KdfType } from "../enums/kdfType"; import { KeySuffixOptions } from "../enums/keySuffixOptions"; +import { + Decryptable, + DecryptableDomain, + Encryptable, + EncryptableDomain, +} from "../interfaces/crypto.interface"; import { sequentialize } from "../misc/sequentialize"; import { Utils } from "../misc/utils"; import { EFFLongWordList } from "../misc/wordlist"; @@ -691,6 +697,29 @@ export class CryptoService implements CryptoServiceAbstraction { return true; } + async decryptDomain( + view: Decryptable, + domain: D + ): Promise { + const key = await this.getKeyFromIdentifier(domain.keyIdentifier()); + + return await this.encryptService.decryptDomain(view, domain, key); + } + + async encryptView>>( + view: V + ): Promise> { + const key = await this.getKeyFromIdentifier(view.keyIdentifier()); + + return this.encryptService.encryptView(view, key); + } + + private async getKeyFromIdentifier(keyIdentifier: string | null): Promise { + return Utils.isNullOrWhitespace(keyIdentifier) + ? await this.getKeyForUserEncryption() + : await this.getOrgKey(keyIdentifier); + } + // ---HELPERS--- protected async storeKey(key: SymmetricCryptoKey, userId?: string) { diff --git a/libs/common/src/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/services/cryptography/encrypt.service.implementation.ts index 86b2c795f84..e9e5aee6b66 100644 --- a/libs/common/src/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/services/cryptography/encrypt.service.implementation.ts @@ -3,7 +3,13 @@ import { EncryptService } from "../../abstractions/encrypt.service"; import { LogService } from "../../abstractions/log.service"; import { EncryptionType } from "../../enums/encryptionType"; import { IEncrypted } from "../../interfaces/IEncrypted"; -import { Decryptable } from "../../interfaces/decryptable.interface"; +import { + Decryptable, + DecryptableDomain, + Encryptable, + EncryptableDomain, +} from "../../interfaces/crypto.interface"; +import { OldDecryptable } from "../../interfaces/decryptable.interface"; import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; import { Utils } from "../../misc/utils"; import { EncArrayBuffer } from "../../models/domain/enc-array-buffer"; @@ -151,7 +157,7 @@ export class EncryptServiceImplementation implements EncryptService { } async decryptItems( - items: Decryptable[], + items: OldDecryptable[], key: SymmetricCryptoKey ): Promise { if (items == null || items.length < 1) { @@ -161,6 +167,21 @@ export class EncryptServiceImplementation implements EncryptService { return await Promise.all(items.map((item) => item.decrypt(key))); } + async decryptDomain( + view: Decryptable, + domain: D, + key: SymmetricCryptoKey + ): Promise { + return view.decrypt(this, key, domain); + } + + async encryptView>>( + view: V, + key: SymmetricCryptoKey + ): Promise> { + return view.encrypt(this, key); + } + private async aesEncrypt(data: ArrayBuffer, key: SymmetricCryptoKey): Promise { const obj = new EncryptedObject(); obj.key = key; diff --git a/libs/common/src/services/cryptography/encrypt.worker.ts b/libs/common/src/services/cryptography/encrypt.worker.ts index 0ee2914ad4b..8d15286883f 100644 --- a/libs/common/src/services/cryptography/encrypt.worker.ts +++ b/libs/common/src/services/cryptography/encrypt.worker.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { Decryptable } from "../../interfaces/decryptable.interface"; +import { OldDecryptable } from "../../interfaces/decryptable.interface"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { ConsoleLogService } from "../../services/consoleLog.service"; import { ContainerService } from "../../services/container.service"; @@ -38,13 +38,13 @@ workerApi.addEventListener("message", async (event: { data: string }) => { const request: { id: string; - items: Jsonify>[]; + items: Jsonify>[]; key: Jsonify; } = JSON.parse(event.data); const key = SymmetricCryptoKey.fromJSON(request.key); const items = request.items.map((jsonItem) => { - const initializer = getClassInitializer>(jsonItem.initializerKey); + const initializer = getClassInitializer>(jsonItem.initializerKey); return initializer(jsonItem); }); const result = await encryptService.decryptItems(items, key); diff --git a/libs/common/src/services/cryptography/multithread-encrypt.service.implementation.ts b/libs/common/src/services/cryptography/multithread-encrypt.service.implementation.ts index 4e7fa2ca48d..28b4a705f40 100644 --- a/libs/common/src/services/cryptography/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/services/cryptography/multithread-encrypt.service.implementation.ts @@ -1,7 +1,7 @@ import { defaultIfEmpty, filter, firstValueFrom, fromEvent, map, Subject, takeUntil } from "rxjs"; import { Jsonify } from "type-fest"; -import { Decryptable } from "../../interfaces/decryptable.interface"; +import { OldDecryptable } from "../../interfaces/decryptable.interface"; import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; import { Utils } from "../../misc/utils"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; @@ -23,7 +23,7 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple * This utilises multithreading to decrypt items faster without interrupting other operations (e.g. updating UI). */ async decryptItems( - items: Decryptable[], + items: OldDecryptable[], key: SymmetricCryptoKey ): Promise { if (items == null || items.length < 1) { diff --git a/libs/common/src/services/folder/folder-api.service.ts b/libs/common/src/services/folder/folder-api.service.ts index 587dbb84f7f..4099e8487fb 100644 --- a/libs/common/src/services/folder/folder-api.service.ts +++ b/libs/common/src/services/folder/folder-api.service.ts @@ -1,15 +1,21 @@ import { ApiService } from "../../abstractions/api.service"; +import { CryptoService } from "../../abstractions/crypto.service"; import { FolderApiServiceAbstraction } from "../../abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../abstractions/folder/folder.service.abstraction"; import { FolderData } from "../../models/data/folder.data"; -import { Folder } from "../../models/domain/folder"; import { FolderRequest } from "../../models/request/folder.request"; import { FolderResponse } from "../../models/response/folder.response"; +import { FolderView } from "../../models/view/folder.view"; export class FolderApiService implements FolderApiServiceAbstraction { - constructor(private folderService: InternalFolderService, private apiService: ApiService) {} - - async save(folder: Folder): Promise { + constructor( + private folderService: InternalFolderService, + private cryptoService: CryptoService, + private apiService: ApiService + ) {} + + async save(folderView: FolderView): Promise { + const folder = await this.cryptoService.encryptView(folderView); const request = new FolderRequest(folder); let response: FolderResponse; diff --git a/libs/common/src/services/folder/folder.service.ts b/libs/common/src/services/folder/folder.service.ts index 85ef564fd5a..60e6b747fa2 100644 --- a/libs/common/src/services/folder/folder.service.ts +++ b/libs/common/src/services/folder/folder.service.ts @@ -9,7 +9,6 @@ import { Utils } from "../../misc/utils"; import { CipherData } from "../../models/data/cipher.data"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; -import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { FolderView } from "../../models/view/folder.view"; export class FolderService implements InternalFolderServiceAbstraction { @@ -50,14 +49,6 @@ export class FolderService implements InternalFolderServiceAbstraction { this._folderViews.next([]); } - // TODO: This should be moved to EncryptService or something - async encrypt(model: FolderView, key?: SymmetricCryptoKey): Promise { - const folder = new Folder(); - folder.id = model.id; - folder.name = await this.cryptoService.encrypt(model.name, key); - return folder; - } - async get(id: string): Promise { const folders = this._folders.getValue(); @@ -179,7 +170,9 @@ export class FolderService implements InternalFolderServiceAbstraction { } private async decryptFolders(folders: Folder[]) { - const decryptFolderPromises = folders.map((f) => f.decrypt()); + const decryptFolderPromises = folders.map((f) => + this.cryptoService.decryptDomain(FolderView, f) + ); const decryptedFolders = await Promise.all(decryptFolderPromises); decryptedFolders.sort(Utils.getSortFunction(this.i18nService, "name")); diff --git a/libs/common/src/services/import.service.ts b/libs/common/src/services/import.service.ts index eec8edf6c56..02d63347fe2 100644 --- a/libs/common/src/services/import.service.ts +++ b/libs/common/src/services/import.service.ts @@ -296,7 +296,7 @@ export class ImportService implements ImportServiceAbstraction { } if (importResult.folders != null) { for (let i = 0; i < importResult.folders.length; i++) { - const f = await this.folderService.encrypt(importResult.folders[i]); + const f = await this.cryptoService.encryptView(importResult.folders[i]); request.folders.push(new FolderRequest(f)); } }