add optional Argon2id password wrap for zk uploads.

Wrap the image DEK for password-protected batches, keep the URL fragment as a separate secret, and extend prepare/decrypt helpers with tests.
This commit is contained in:
2026-10-20 18:31:33 +02:00
parent 4376ed7ff1
commit 47361a83b9
20 changed files with 360 additions and 20 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import { base64ToBytes } from '@/utils/encoding/base64ToBytes';
import { buildZkViewerPath } from '@/utils/share/buildZkViewerPath';
import { decryptPasswordProtectedZkUpload } from '@/utils/upload/decryptPasswordProtectedZkUpload';
import { decryptZkUpload } from '@/utils/upload/decryptZkUpload';
import { prepareZkUploadRequest } from '@/utils/upload/prepareZkUploadRequest';
@@ -58,4 +59,54 @@ describe('prepareZkUploadRequest', () => {
expect(recovered).toEqual(plaintext);
});
describe('password protected', () => {
const passwordOptions = {
expiresAt: null,
maxViews: null,
passwordProtected: true as const,
password: 'batch-secret'
};
it('includes wrap fields and omits password from the POST body', async () => {
const { request, fragmentKey } = await prepareZkUploadRequest(new Uint8Array([1]), passwordOptions);
expect(request.passwordProtected).toBe(true);
expect(request.wrappedDek).toMatch(/^[A-Za-z0-9+/]+=*$/);
expect(request.wrapIv).toMatch(/^[A-Za-z0-9+/]+=*$/);
expect(request.salt).toMatch(/^[A-Za-z0-9+/]+=*$/);
expect(request).not.toHaveProperty('password');
expect(fragmentKey).toMatch(/^[A-Za-z0-9_-]+$/);
});
it('does not decrypt with URL fragment alone', async () => {
const plaintext = new TextEncoder().encode('protected');
const { request, fragmentKey } = await prepareZkUploadRequest(plaintext, passwordOptions);
await expect(
decryptZkUpload(
base64ToBytes(request.ciphertext),
base64ToBytes(request.iv),
fragmentKey
)
).rejects.toThrow();
});
it('roundtrips with fragment key and password', async () => {
const plaintext = new TextEncoder().encode('protected-payload');
const { request, fragmentKey } = await prepareZkUploadRequest(plaintext, passwordOptions);
const recovered = await decryptPasswordProtectedZkUpload(
base64ToBytes(request.ciphertext),
base64ToBytes(request.iv),
fragmentKey,
passwordOptions.password,
base64ToBytes(request.wrappedDek!),
base64ToBytes(request.wrapIv!),
base64ToBytes(request.salt!)
);
expect(recovered).toEqual(plaintext);
});
});
});