import { beforeEach, describe, expect, it } from "vitest"; import { CryptoUtils } from "./crypto"; let utils: CryptoUtils; describe("deriveKeyBundle", () => { beforeEach(async () => { utils = new CryptoUtils(); await utils.initialize(); }); it("should generate a valid CryptoKey and a 64-char hex authHash", async () => { const { masterKey, authHash } = await CryptoUtils.deriveKeyBundle( "password", "test@test.com", ); expect(masterKey.type).toBe("secret"); expect(masterKey).toBeInstanceOf(CryptoKey); expect(authHash).toHaveLength(64); expect(typeof authHash).toBe("string"); }); it("should produce identical bundles for identical credentials (deterministic)", async () => { const bundleA = await CryptoUtils.deriveKeyBundle( "password", "user@me.com", ); const bundleB = await CryptoUtils.deriveKeyBundle( "password", "user@me.com", ); expect(bundleA.authHash).toBe(bundleB.authHash); const secret = "shared-secret"; const encryptedContent = await utils.encryptLetter( secret, bundleA.masterKey, ); const decryptedContent = await utils.decryptLetter( encryptedContent, bundleB.masterKey, ); expect(decryptedContent).toBe(secret); }); it("should produce different keys and hashes for different users", async () => { const bundleA = await CryptoUtils.deriveKeyBundle( "password", "test1@gmail.com", ); const bundleB = await CryptoUtils.deriveKeyBundle( "password", "test2@gmail.com", ); expect(bundleA.authHash).not.toBe(bundleB.authHash); const encrypted = await utils.encryptLetter("secret", bundleA.masterKey); await expect( utils.decryptLetter(encrypted, bundleB.masterKey), ).rejects.toThrow(); }); }); describe("encryptLetter / decryptLetter", () => { let masterKey: CryptoKey; beforeEach(async () => { const bundle = await CryptoUtils.deriveKeyBundle( "password", "test@test.com", ); masterKey = bundle.masterKey; }); it("should restore the original plaintext after a roundtrip", async () => { const encrypted = await utils.encryptLetter("secret", masterKey); const decrypted = await utils.decryptLetter(encrypted, masterKey); expect(decrypted).toBe("secret"); }); it("should not contain plaintext in the payload", async () => { const secret = "super-secret-text"; const result = await utils.encryptLetter(secret, masterKey); expect(result.encrypted_content).not.toContain(secret); expect(result.encrypted_dek).toBeDefined(); }); it("should produce different ciphertexts each time for identical plaintexts", async () => { const text = "constant-text"; const enc1 = await utils.encryptLetter(text, masterKey); const enc2 = await utils.encryptLetter(text, masterKey); expect(enc1.encrypted_content).not.toBe(enc2.encrypted_content); expect(enc1.encrypted_dek).not.toBe(enc2.encrypted_dek); }); }); describe("encryptMetadata / decryptMetadata", () => { let masterKey: CryptoKey; beforeEach(async () => { const bundle = await CryptoUtils.deriveKeyBundle( "password", "test@test.com", ); masterKey = bundle.masterKey; }); it("should successfully encrypt and decrypt object content", async () => { const metadata = { title: "title", recipient: "self", tags: ["tag1", "tag2"], }; const encrypted = await utils.encryptMetadata(metadata, masterKey); const decrypted = await utils.decryptMetadata(encrypted, masterKey); expect(decrypted).toEqual(metadata); }); }); describe("encryptImage / decryptImage", () => { let masterKey: CryptoKey; beforeEach(async () => { const bundle = await CryptoUtils.deriveKeyBundle( "password", "test@test.com", ); masterKey = bundle.masterKey; }); it("should transform a File into an encrypted .bin Blob", async () => { const rawData = new TextEncoder().encode("image-data"); const file = new File([rawData], "photo.jpg", { type: "image/jpeg" }); const result = await utils.encryptImage(file, masterKey); const encryptedText = await result.encryptedBlob.text(); expect(result.encryptedBlob).toBeInstanceOf(Blob); expect(result.encryptedBlob.size).toBeGreaterThan(0); expect(result.filename).toMatch(/\.bin$/); expect(result.filename).not.toMatch(/photo|jpg/); expect(encryptedText).not.toContain("image-data"); }); it("should support decryption using a sharing key (guest access)", async () => { const rawData = new TextEncoder().encode("image-data"); const file = new File([rawData], "photo.jpg", { type: "image/jpeg" }); const result = await utils.encryptImage(file, masterKey); const encryptedLetter = await utils.encryptLetter("test", masterKey); const sharingKey = encryptedLetter.sharingKey as string; const blobUrl = await utils.decryptImageWithSharingKey( result.encryptedBlob, sharingKey, ); expect(blobUrl).toContain("blob:"); URL.revokeObjectURL(blobUrl); // cleanup }); }); describe("Sharing Key Decryption", () => { let masterKey: CryptoKey; beforeEach(async () => { const bundle = await CryptoUtils.deriveKeyBundle("password", "salt"); masterKey = bundle.masterKey; }); it("should decrypt a letter using ONLY the sharing key", async () => { const letterContent = "hello, guest"; const encryptedLetter = await utils.encryptLetter(letterContent, masterKey); const sharingKey = encryptedLetter.sharingKey as string; const decryptedLetter = await utils.decryptLetterWithSharingKey( encryptedLetter.encrypted_content, sharingKey, ); expect(decryptedLetter).toBe(letterContent); }); }); describe("extractSharingKey", () => { let masterKey: CryptoKey; beforeEach(async () => { utils = new CryptoUtils(); await utils.initialize(); const bundle = await CryptoUtils.deriveKeyBundle( "password", "test@test.com", ); masterKey = bundle.masterKey; }); it("should return the same key that encryptLetter embedded as sharingKey", async () => { const encrypted = await utils.encryptLetter("any content", masterKey); const extracted = await utils.extractSharingKey( encrypted.encrypted_dek, masterKey, ); expect(extracted).toBe(encrypted.sharingKey); }); it("extracted key should decrypt the ciphertext produced by encryptLetter", async () => { const plaintext = "hello"; const encrypted = await utils.encryptLetter(plaintext, masterKey); const extracted = await utils.extractSharingKey( encrypted.encrypted_dek, masterKey, ); const decrypted = await utils.decryptLetterWithSharingKey( encrypted.encrypted_content, extracted, ); expect(decrypted).toBe(plaintext); }); });