Encrypt image bytes in the browser, keep keys out of POST bodies (URL fragment or path secret), and POST ciphertext via typed upload API helpers.
35 lines
1.5 KiB
TypeScript
35 lines
1.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { decryptAesGcm } from '@/utils/crypto/decryptAesGcm';
|
|
import { importAesGcmKey } from '@/utils/crypto/importAesGcmKey';
|
|
import { base64UrlToBytes } from '@/utils/encoding/base64UrlToBytes';
|
|
import { bytesToBase64 } from '@/utils/encoding/bytesToBase64';
|
|
import { encryptHotlinkUpload } from '@/utils/upload/encryptHotlinkUpload';
|
|
|
|
describe('encryptHotlinkUpload', () => {
|
|
it('roundtrips plaintext using the path secret as the DEK', async () => {
|
|
const plaintext = new TextEncoder().encode('hotlink-image');
|
|
const encrypted = await encryptHotlinkUpload(plaintext);
|
|
const dek = base64UrlToBytes(encrypted.secret);
|
|
const key = await importAesGcmKey(dek);
|
|
|
|
const recovered = await decryptAesGcm(encrypted.ciphertext, key, encrypted.iv);
|
|
|
|
expect(recovered).toEqual(plaintext);
|
|
});
|
|
|
|
it('returns a distinct secret per call', async () => {
|
|
const plaintext = new Uint8Array([0]);
|
|
const first = await encryptHotlinkUpload(plaintext);
|
|
const second = await encryptHotlinkUpload(plaintext);
|
|
|
|
expect(first.secret).not.toEqual(second.secret);
|
|
});
|
|
|
|
it('serializes iv and ciphertext for the upload API', async () => {
|
|
const encrypted = await encryptHotlinkUpload(new Uint8Array([1, 2, 3]));
|
|
|
|
expect(bytesToBase64(encrypted.iv)).toMatch(/^[A-Za-z0-9+/]+=*$/);
|
|
expect(bytesToBase64(encrypted.ciphertext)).toMatch(/^[A-Za-z0-9+/]+=*$/);
|
|
});
|
|
});
|