| 1 | import assert from "node:assert/strict" ; |
| 2 | import {webcrypto} from "node:crypto" ; |
| 3 | import {readFile} from "node:fs/promises" ; |
| 4 | import test from "node:test" ; |
| 5 | import vm from "node:vm" ; |
| 6 | |
| 7 | const source = await readFile(new URL("../tinyclient/blossom-encryption.js" , import.meta.url), "utf8" ); |
| 8 | const shared = await readFile(new URL("../tinyclient/tiny.js" , import.meta.url), "utf8" ); |
| 9 | const sandbox = {crypto: webcrypto, TextEncoder, URL, URLSearchParams, Uint8Array, ArrayBuffer}; |
| 10 | sandbox.globalThis = sandbox; |
| 11 | vm.runInNewContext(shared + source, sandbox); |
| 12 | const api = sandbox.tiny.blossom.encryption; |
| 13 | const utf8 = value => new TextEncoder().encode(value); |
| 14 | const hex = bytes => Buffer.from(bytes).toString("hex" ); |
| 15 | |
| 16 | test("matches BUD-15 empty plaintext vector" , async () => { |
| 17 | const result = await api.encryptCHK(new Uint8Array()); |
| 18 | assert.equal(result.key, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ); |
| 19 | assert.equal(hex(result.ciphertext), "7cd161ae8406d82cdf553c1100d012db" ); |
| 20 | assert.equal(result.hash, "346c46e7cc6722c99efe7f7bc316d8f3ff5f025f1031bf94418ef4db891e04cd" ); |
| 21 | assert.deepEqual(await api.decryptCHK(result.ciphertext, result.key, result.hash), new Uint8Array()); |
| 22 | }); |
| 23 | |
| 24 | test("matches BUD-15 hello vector" , async () => { |
| 25 | const result = await api.encryptCHK(utf8("hello" )); |
| 26 | assert.equal(result.key, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" ); |
| 27 | assert.equal(hex(result.ciphertext), "c65308d9c8649ff1c59820d0b3a030db34ad00f92d" ); |
| 28 | assert.equal(result.hash, "70b977414934faa6270f323f117d05dbcc412e9d7ba2354b4d0f88f60aad2461" ); |
| 29 | assert.deepEqual(await api.decryptCHK(result.ciphertext, result.key, result.hash), utf8("hello" )); |
| 30 | }); |
| 31 | |
| 32 | test("rejects tampered ciphertext, hash, and key" , async () => { |
| 33 | const result = await api.encryptCHK(utf8("private" )); |
| 34 | const tampered = new Uint8Array(result.ciphertext); |
| 35 | tampered[0] ^= 1; |
| 36 | await assert.rejects(api.decryptCHK(tampered, result.key, result.hash), /ciphertext hash mismatch/); |
| 37 | await assert.rejects(api.decryptCHK(result.ciphertext, result.key, "0" .repeat(64)), /ciphertext hash mismatch/); |
| 38 | const wrongKey = "f" .repeat(64); |
| 39 | await assert.rejects(api.decryptCHK(result.ciphertext, wrongKey, result.hash), /CHK decryption failed|plaintext hash mismatch/); |
| 40 | }); |
| 41 | |
| 42 | test("creates and parses a CHK URI without altering the key" , async () => { |
| 43 | const result = await api.encryptCHK(utf8("hello" )); |
| 44 | const uri = api.createURI(result.hash, "txt" , result.key); |
| 45 | assert.equal(uri, "blossom:" + result.hash + ".txt?enc=chk-v1&k=" + result.key); |
| 46 | const parsed = api.parseURI(uri); |
| 47 | assert.equal(parsed.hash, result.hash); |
| 48 | assert.equal(parsed.extension, "txt" ); |
| 49 | assert.equal(parsed.key, result.key); |
| 50 | assert.throws(() => api.parseURI("blossom:" + result.hash + "?enc=chk-v1&k=" + result.key.toUpperCase()), /lowercase hex key/); |
| 51 | }); |
| 52 | |
| 53 | test("derives a distinct key and zero-nonce ciphertext for each content" , async () => { |
| 54 | const first = await api.encryptCHK(utf8("first" )); |
| 55 | const second = await api.encryptCHK(utf8("second" )); |
| 56 | assert.notEqual(first.key, second.key); |
| 57 | assert.notEqual(first.hash, second.hash); |
| 58 | assert.notDeepEqual(first.ciphertext, second.ciphertext); |
| 59 | }); |