import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; import { BehaviorSubject } 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 { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/enums/cipherType"; import { KdfType } from "@bitwarden/common/enums/kdfType"; import { Utils } from "@bitwarden/common/misc/utils"; import { Cipher } from "@bitwarden/common/models/domain/cipher"; import { EncString } from "@bitwarden/common/models/domain/encString"; import { Login } from "@bitwarden/common/models/domain/login"; import { CipherWithIdExport as CipherExport } from "@bitwarden/common/models/export/cipherWithIdsExport"; import { CipherView } from "@bitwarden/common/models/view/cipherView"; import { LoginView } from "@bitwarden/common/models/view/loginView"; import { ExportService } from "@bitwarden/common/services/export.service"; import { BuildTestObject, GetUniqueString } from "../utils"; const UserCipherViews = [ generateCipherView(false), generateCipherView(false), generateCipherView(true), ]; const UserCipherDomains = [ generateCipherDomain(false), generateCipherDomain(false), generateCipherDomain(true), ]; function generateCipherView(deleted: boolean) { return BuildTestObject( { id: GetUniqueString("id"), notes: GetUniqueString("notes"), type: CipherType.Login, login: BuildTestObject( { username: GetUniqueString("username"), password: GetUniqueString("password"), }, LoginView ), collectionIds: null, deletedDate: deleted ? new Date() : null, }, CipherView ); } function generateCipherDomain(deleted: boolean) { return BuildTestObject( { id: GetUniqueString("id"), notes: new EncString(GetUniqueString("notes")), type: CipherType.Login, login: BuildTestObject( { username: new EncString(GetUniqueString("username")), password: new EncString(GetUniqueString("password")), }, Login ), collectionIds: null, deletedDate: deleted ? new Date() : null, }, Cipher ); } function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).items); const items: CipherExport[] = []; ciphers.forEach((c: CipherView | Cipher) => { const item = new CipherExport(); item.build(c); items.push(item); }); expect(actual).toEqual(JSON.stringify(items)); } describe("ExportService", () => { let exportService: ExportService; let apiService: SubstituteOf; let cryptoFunctionService: SubstituteOf; let cipherService: SubstituteOf; let folderService: SubstituteOf; let cryptoService: SubstituteOf; beforeEach(() => { apiService = Substitute.for(); cryptoFunctionService = Substitute.for(); cipherService = Substitute.for(); folderService = Substitute.for(); cryptoService = Substitute.for(); folderService.folderViews$.returns(new BehaviorSubject([])); folderService.folders$.returns(new BehaviorSubject([])); exportService = new ExportService( folderService, cipherService, apiService, cryptoService, cryptoFunctionService ); }); it("exports unecrypted user ciphers", async () => { cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1)); const actual = await exportService.getExport("json"); expectEqualCiphers(UserCipherViews.slice(0, 1), actual); }); it("exports encrypted json user ciphers", async () => { cipherService.getAll().resolves(UserCipherDomains.slice(0, 1)); const actual = await exportService.getExport("encrypted_json"); expectEqualCiphers(UserCipherDomains.slice(0, 1), actual); }); it("does not unecrypted export trashed user items", async () => { cipherService.getAllDecrypted().resolves(UserCipherViews); const actual = await exportService.getExport("json"); expectEqualCiphers(UserCipherViews.slice(0, 2), actual); }); it("does not encrypted export trashed user items", async () => { cipherService.getAll().resolves(UserCipherDomains); const actual = await exportService.getExport("encrypted_json"); expectEqualCiphers(UserCipherDomains.slice(0, 2), actual); }); describe("password protected export", () => { let exportString: string; let exportObject: any; let mac: SubstituteOf; let data: SubstituteOf; const password = "password"; const salt = "salt"; describe("export json object", () => { beforeEach(async () => { mac = Substitute.for(); data = Substitute.for(); mac.encryptedString.returns("mac"); data.encryptedString.returns("encData"); jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(salt); cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1)); exportString = await exportService.getPasswordProtectedExport(password); exportObject = JSON.parse(exportString); }); it("specifies it is encrypted", () => { expect(exportObject.encrypted).toBe(true); }); it("specifies it's password protected", () => { expect(exportObject.passwordProtected).toBe(true); }); it("specifies salt", () => { expect(exportObject.salt).toEqual("salt"); }); it("specifies kdfIterations", () => { expect(exportObject.kdfIterations).toEqual(100000); }); it("has kdfType", () => { expect(exportObject.kdfType).toEqual(KdfType.PBKDF2_SHA256); }); it("has a mac property", async () => { cryptoService.encrypt(Arg.any(), Arg.any()).resolves(mac); exportString = await exportService.getPasswordProtectedExport(password); exportObject = JSON.parse(exportString); expect(exportObject.encKeyValidation_DO_NOT_EDIT).toEqual(mac.encryptedString); }); it("has data property", async () => { cryptoService.encrypt(Arg.any(), Arg.any()).resolves(data); exportString = await exportService.getPasswordProtectedExport(password); exportObject = JSON.parse(exportString); expect(exportObject.data).toEqual(data.encryptedString); }); it("encrypts the data property", async () => { const unencrypted = await exportService.getExport(); expect(exportObject.data).not.toEqual(unencrypted); }); }); }); });