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.
This commit is contained in:
99
test/crypto/encrypt-blob.test.ts
Normal file
99
test/crypto/encrypt-blob.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user