Files
quak/test/crypto/encrypt-blob.test.ts
sneak 6cb679d62f Add tests for thumbnail-helpers branch (20 new tests)
test/crypto/encrypt-blob.test.ts (8 tests):
  Round-trip with decryptBlob, zero-length payload, ciphertext
  overhead check, header size check, different keys produce different
  output, same key produces different output each call (random nonce),
  wrong-key rejection, tamper detection.

test/api/upload.test.ts (4 tests):
  putJSON sends PUT with auth headers and JSON body. putFile sends
  PUT to the exact presigned URL with Content-Type octet-stream and
  does NOT send X-Auth-Token or X-Client-Package (S3 would reject
  them). getUploadURL POSTs with contentLength and contentMD5.
  updateThumbnail PUTs to /files/thumbnail with correct body shape.

test/thumbnails/thumbnails.test.ts (8 tests):
  listMissingThumbnails identifies empty (0 byte) and 404 thumbnails
  while ignoring working ones; deduplicates across collections.
  fixMissingThumbnails verifies the full pipeline: download original,
  generate JPEG via sharp, encrypt with encryptBlob, upload via
  presigned URL, register via PUT /files/thumbnail. The test
  decrypts the uploaded ciphertext and verifies it starts with JPEG
  magic bytes (FF D8 FF). Also tests: nonexistent file ID reports
  failure without crashing; mixed success/failure across multiple
  files; Client.getApiClient() works when logged in, throws after
  logout.
2026-06-09 12:29:24 -04:00

100 lines
3.9 KiB
TypeScript

/**
* Tests for `crypto.encryptBlob`.
*
* `encryptBlob` is the push-side counterpart to `decryptBlob`. It
* encrypts a small payload as a single secretstream chunk with
* TAG_FINAL and returns the header + ciphertext. Used for encrypting
* thumbnails before upload to the Ente server.
*
* The critical invariant is that `decryptBlob(encryptBlob(...))` is the
* identity function. If either side drifts, uploaded thumbnails become
* unreadable.
*/
import sodium from "libsodium-wrappers-sumo";
import { beforeAll, describe, expect, it } from "vitest";
import {
decryptBlob,
encryptBlob,
init,
STREAM_CHUNK_OVERHEAD,
} from "../../src/crypto/index.js";
describe("crypto.encryptBlob", () => {
beforeAll(async () => {
await init();
await sodium.ready;
});
it("round-trips with decryptBlob for arbitrary data", () => {
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const plaintext = sodium.randombytes_buf(500);
const { header, ciphertext } = encryptBlob(plaintext, key);
const recovered = decryptBlob(ciphertext, header, key);
expect(recovered).toEqual(plaintext);
});
it("round-trips a zero-length payload", () => {
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const { header, ciphertext } = encryptBlob(new Uint8Array(0), key);
const recovered = decryptBlob(ciphertext, header, key);
expect(recovered.length).toBe(0);
});
it("ciphertext is exactly STREAM_CHUNK_OVERHEAD longer than plaintext", () => {
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const plaintext = sodium.randombytes_buf(1234);
const { ciphertext } = encryptBlob(plaintext, key);
expect(ciphertext.length).toBe(
plaintext.length + STREAM_CHUNK_OVERHEAD,
);
});
it("header is 24 bytes (secretstream XChaCha20 header size)", () => {
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const { header } = encryptBlob(new Uint8Array([1, 2, 3]), key);
expect(header.length).toBe(
sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES,
);
});
it("produces different ciphertext for different keys", () => {
const k1 = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const k2 = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const plaintext = new Uint8Array([1, 2, 3, 4, 5]);
const enc1 = encryptBlob(plaintext, k1);
const enc2 = encryptBlob(plaintext, k2);
expect(enc1.ciphertext).not.toEqual(enc2.ciphertext);
});
it("produces different ciphertext on each call (random nonce in header)", () => {
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const plaintext = new Uint8Array([9, 9, 9]);
const a = encryptBlob(plaintext, key);
const b = encryptBlob(plaintext, key);
expect(a.header).not.toEqual(b.header);
expect(a.ciphertext).not.toEqual(b.ciphertext);
});
it("decryptBlob rejects ciphertext encrypted with a different key", () => {
const k1 = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const k2 = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const { header, ciphertext } = encryptBlob(
new Uint8Array([1, 2, 3]),
k1,
);
expect(() => decryptBlob(ciphertext, header, k2)).toThrow();
});
it("decryptBlob rejects tampered ciphertext from encryptBlob", () => {
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const { header, ciphertext } = encryptBlob(
sodium.randombytes_buf(100),
key,
);
ciphertext[ciphertext.length - 1] =
ciphertext[ciphertext.length - 1]! ^ 0x01;
expect(() => decryptBlob(ciphertext, header, key)).toThrow();
});
});