initial commit
This commit is contained in:
189
test/contract.test.ts
Normal file
189
test/contract.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
FILEDROP_CONTRACT_VERSION,
|
||||
FILEDROP_WEBHOOK_SIGNATURE_HEADER,
|
||||
FILEDROP_WEBHOOK_TIMESTAMP_HEADER,
|
||||
createLocalFileDropStorage,
|
||||
createSignedFileDropWebhook,
|
||||
developmentRecipientPublicKey,
|
||||
fileDropEventSchema,
|
||||
fileDropUploadCommitSchema,
|
||||
getActiveDropboxFromEnvironment,
|
||||
parseFileDropWebhook,
|
||||
receiveCiphertextIntakeFromFormData,
|
||||
verifyFileDropWebhookSignature,
|
||||
type FileDropStorage
|
||||
} from "../dist/index.js";
|
||||
|
||||
const fixture = JSON.parse(await readFile(new URL("./fixtures/filedrop-v1.json", import.meta.url), "utf8")) as {
|
||||
ciphertext: string;
|
||||
contactEmail: string;
|
||||
contactName: string;
|
||||
dropboxId: string;
|
||||
envelope: Record<string, unknown>;
|
||||
languagePair: string;
|
||||
now: string;
|
||||
requestedDeadline: string;
|
||||
routingLabel: string;
|
||||
serviceType: string;
|
||||
uploadId: string;
|
||||
webhookId: string;
|
||||
};
|
||||
|
||||
test("discovery is generic and exposes key lifecycle state", async () => {
|
||||
const discovery = await getActiveDropboxFromEnvironment({
|
||||
FILEDROP_DROPBOX_ID: fixture.dropboxId,
|
||||
FILEDROP_DROPBOX_LABEL: "Fixture secure drop",
|
||||
FILEDROP_RECIPIENT_KEY_ROTATED_AT: fixture.now,
|
||||
FILEDROP_RECIPIENT_KEY_STATUS: "rotating",
|
||||
FILEDROP_RECIPIENT_PUBLIC_KEY_JWK: JSON.stringify(developmentRecipientPublicKey)
|
||||
});
|
||||
|
||||
assert.equal(discovery.contractVersion, FILEDROP_CONTRACT_VERSION);
|
||||
assert.equal(discovery.dropboxId, fixture.dropboxId);
|
||||
assert.equal(discovery.label, "Fixture secure drop");
|
||||
assert.equal(discovery.status, "rotating");
|
||||
assert.equal(discovery.key.status, "rotating");
|
||||
assert.equal(discovery.key.rotatedAt, fixture.now);
|
||||
assert.equal(discovery.source, "configured");
|
||||
});
|
||||
|
||||
test("discovery defaults do not contain host-specific branding", async () => {
|
||||
const discovery = await getActiveDropboxFromEnvironment({});
|
||||
assert.equal(discovery.label, "Secure file drop");
|
||||
assert.equal(discovery.dropboxId, "default");
|
||||
assert.equal(discovery.source, "development");
|
||||
});
|
||||
|
||||
test("receiveCiphertextIntakeFromFormData validates filedrop.v1 and supports custom storage", async () => {
|
||||
const saved: Array<{ id: string; manifest: Record<string, unknown>; text: string }> = [];
|
||||
const storage: FileDropStorage = {
|
||||
async saveCiphertextUpload(input) {
|
||||
saved.push({
|
||||
id: input.id,
|
||||
manifest: input.manifest,
|
||||
text: Buffer.from(await input.ciphertext.arrayBuffer()).toString("utf8")
|
||||
});
|
||||
return {
|
||||
driver: "memory",
|
||||
manifestKey: `memory/${input.id}.json`,
|
||||
storageKey: `memory/${input.id}.enc`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const received = await receiveCiphertextIntakeFromFormData(createFixtureFormData(), {}, {
|
||||
id: fixture.uploadId,
|
||||
now: new Date(fixture.now),
|
||||
storage
|
||||
});
|
||||
|
||||
assert.equal(received.id, fixture.uploadId);
|
||||
assert.equal(received.storageDriver, "memory");
|
||||
assert.equal(received.storageKey, `memory/${fixture.uploadId}.enc`);
|
||||
assert.equal(received.commit.contractVersion, FILEDROP_CONTRACT_VERSION);
|
||||
assert.equal(received.commit.sender.contactEmail, fixture.contactEmail);
|
||||
assert.equal(received.commit.routing.dropboxId, fixture.dropboxId);
|
||||
assert.equal(received.event.eventType, "filedrop.upload.received");
|
||||
assert.equal(received.event.storage.driver, "memory");
|
||||
assert.equal(saved[0]?.text, fixture.ciphertext);
|
||||
|
||||
assert.doesNotThrow(() => fileDropUploadCommitSchema.parse(received.commit));
|
||||
assert.doesNotThrow(() => fileDropEventSchema.parse(received.event));
|
||||
});
|
||||
|
||||
test("local storage adapter writes ciphertext and manifest", async () => {
|
||||
const storageDir = await mkdtemp(path.join(tmpdir(), "filedrop-core-"));
|
||||
try {
|
||||
const received = await receiveCiphertextIntakeFromFormData(createFixtureFormData(), {}, {
|
||||
id: fixture.uploadId,
|
||||
now: new Date(fixture.now),
|
||||
storage: createLocalFileDropStorage({ storageDir })
|
||||
});
|
||||
|
||||
const ciphertext = await readFile(path.join(storageDir, received.storageKey), "utf8");
|
||||
const manifest = JSON.parse(await readFile(path.join(storageDir, received.manifestKey), "utf8")) as {
|
||||
contractVersion: string;
|
||||
commit: { sender: { contactEmail: string } };
|
||||
};
|
||||
|
||||
assert.equal(ciphertext, fixture.ciphertext);
|
||||
assert.equal(manifest.contractVersion, FILEDROP_CONTRACT_VERSION);
|
||||
assert.equal(manifest.commit.sender.contactEmail, fixture.contactEmail);
|
||||
} finally {
|
||||
await rm(storageDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("webhook signatures verify and reject tampered payloads", async () => {
|
||||
const received = await receiveCiphertextIntakeFromFormData(createFixtureFormData(), {}, {
|
||||
id: fixture.uploadId,
|
||||
now: new Date(fixture.now),
|
||||
storage: memoryStorage()
|
||||
});
|
||||
const signed = createSignedFileDropWebhook(received.event, "test-secret", {
|
||||
now: new Date(fixture.now),
|
||||
webhookId: fixture.webhookId
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
verifyFileDropWebhookSignature({
|
||||
body: signed.body,
|
||||
now: new Date(fixture.now),
|
||||
secret: "test-secret",
|
||||
signatureHeader: signed.headers[FILEDROP_WEBHOOK_SIGNATURE_HEADER],
|
||||
timestampHeader: signed.headers[FILEDROP_WEBHOOK_TIMESTAMP_HEADER]
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
verifyFileDropWebhookSignature({
|
||||
body: `${signed.body} `,
|
||||
now: new Date(fixture.now),
|
||||
secret: "test-secret",
|
||||
signatureHeader: signed.headers[FILEDROP_WEBHOOK_SIGNATURE_HEADER],
|
||||
timestampHeader: signed.headers[FILEDROP_WEBHOOK_TIMESTAMP_HEADER]
|
||||
}),
|
||||
false
|
||||
);
|
||||
|
||||
const parsed = parseFileDropWebhook({
|
||||
body: signed.body,
|
||||
now: new Date(fixture.now),
|
||||
secret: "test-secret",
|
||||
signatureHeader: signed.headers[FILEDROP_WEBHOOK_SIGNATURE_HEADER],
|
||||
timestampHeader: signed.headers[FILEDROP_WEBHOOK_TIMESTAMP_HEADER]
|
||||
});
|
||||
assert.equal(parsed.webhookId, fixture.webhookId);
|
||||
assert.equal(parsed.event.upload.id, fixture.uploadId);
|
||||
});
|
||||
|
||||
function createFixtureFormData() {
|
||||
const formData = new FormData();
|
||||
formData.append("ciphertext", new Blob([fixture.ciphertext], { type: "application/octet-stream" }), "payload.enc");
|
||||
formData.append("contactEmail", fixture.contactEmail);
|
||||
formData.append("contactName", fixture.contactName);
|
||||
formData.append("dropboxId", fixture.dropboxId);
|
||||
formData.append("envelope", JSON.stringify(fixture.envelope));
|
||||
formData.append("languagePair", fixture.languagePair);
|
||||
formData.append("requestedDeadline", fixture.requestedDeadline);
|
||||
formData.append("routingLabel", fixture.routingLabel);
|
||||
formData.append("serviceType", fixture.serviceType);
|
||||
return formData;
|
||||
}
|
||||
|
||||
function memoryStorage(): FileDropStorage {
|
||||
return {
|
||||
async saveCiphertextUpload(input) {
|
||||
return {
|
||||
driver: "memory",
|
||||
manifestKey: `memory/${input.id}.json`,
|
||||
storageKey: `memory/${input.id}.enc`
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
89
test/e2ee.test.ts
Normal file
89
test/e2ee.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import {
|
||||
createRecipientKeyPair,
|
||||
decryptPayloadFromEnvelope,
|
||||
encryptBytesForRecipient,
|
||||
encryptBytesForRecipients,
|
||||
getEnvelopeRecipientWraps,
|
||||
rewrapEnvelopeForRecipients
|
||||
} from "../dist/index.js";
|
||||
|
||||
test("multi-recipient envelope decrypts with each recipient private key", async () => {
|
||||
const first = await createRecipientKeyPair();
|
||||
const second = await createRecipientKeyPair();
|
||||
const plaintext = new TextEncoder().encode("shared encrypted payload");
|
||||
|
||||
const encrypted = await encryptBytesForRecipients(plaintext.buffer as ArrayBuffer, [first.publicKey, second.publicKey], {
|
||||
purpose: "deliverable"
|
||||
});
|
||||
|
||||
const wraps = getEnvelopeRecipientWraps(encrypted.envelope);
|
||||
assert.equal(wraps.length, 2);
|
||||
assert.deepEqual(
|
||||
wraps.map((wrap) => wrap.keyFingerprint).sort(),
|
||||
[first.fingerprint, second.fingerprint].sort()
|
||||
);
|
||||
|
||||
const ciphertext = await encrypted.ciphertext.arrayBuffer();
|
||||
const firstDecrypted = await decryptPayloadFromEnvelope(ciphertext, encrypted.envelope, first.privateKey);
|
||||
const secondDecrypted = await decryptPayloadFromEnvelope(ciphertext, encrypted.envelope, second.privateKey);
|
||||
|
||||
assert.equal(new TextDecoder().decode(firstDecrypted.plaintext), "shared encrypted payload");
|
||||
assert.equal(new TextDecoder().decode(secondDecrypted.plaintext), "shared encrypted payload");
|
||||
assert.deepEqual(firstDecrypted.metadata, { purpose: "deliverable" });
|
||||
assert.deepEqual(secondDecrypted.metadata, { purpose: "deliverable" });
|
||||
});
|
||||
|
||||
test("single-recipient helper keeps the legacy recipient field", async () => {
|
||||
const recipient = await createRecipientKeyPair();
|
||||
const plaintext = new TextEncoder().encode("legacy payload");
|
||||
|
||||
const encrypted = await encryptBytesForRecipient(plaintext.buffer as ArrayBuffer, recipient.publicKey, {
|
||||
purpose: "source"
|
||||
});
|
||||
|
||||
assert.equal(encrypted.envelope.recipient.keyFingerprint, recipient.fingerprint);
|
||||
assert.equal(getEnvelopeRecipientWraps(encrypted.envelope).length, 1);
|
||||
|
||||
const decrypted = await decryptPayloadFromEnvelope(await encrypted.ciphertext.arrayBuffer(), encrypted.envelope, recipient.privateKey);
|
||||
assert.equal(new TextDecoder().decode(decrypted.plaintext), "legacy payload");
|
||||
assert.deepEqual(decrypted.metadata, { purpose: "source" });
|
||||
});
|
||||
|
||||
test("envelope can be rewrapped to a new recipient without changing ciphertext metadata", async () => {
|
||||
const admin = await createRecipientKeyPair();
|
||||
const client = await createRecipientKeyPair();
|
||||
const replacementClient = await createRecipientKeyPair();
|
||||
const plaintext = new TextEncoder().encode("deliverable payload");
|
||||
|
||||
const encrypted = await encryptBytesForRecipients(plaintext.buffer as ArrayBuffer, [admin.publicKey, client.publicKey], {
|
||||
purpose: "deliverable"
|
||||
});
|
||||
const originalContent = encrypted.envelope.content;
|
||||
const originalMetadata = encrypted.envelope.metadata;
|
||||
|
||||
const rewrappedEnvelope = await rewrapEnvelopeForRecipients(
|
||||
encrypted.envelope,
|
||||
admin.privateKey,
|
||||
[replacementClient.publicKey],
|
||||
{ keyFingerprint: admin.fingerprint }
|
||||
);
|
||||
|
||||
assert.deepEqual(rewrappedEnvelope.content, originalContent);
|
||||
assert.deepEqual(rewrappedEnvelope.metadata, originalMetadata);
|
||||
assert.deepEqual(
|
||||
getEnvelopeRecipientWraps(rewrappedEnvelope)
|
||||
.map((wrap) => wrap.keyFingerprint)
|
||||
.sort(),
|
||||
[admin.fingerprint, client.fingerprint, replacementClient.fingerprint].sort()
|
||||
);
|
||||
|
||||
const decrypted = await decryptPayloadFromEnvelope(
|
||||
await encrypted.ciphertext.arrayBuffer(),
|
||||
rewrappedEnvelope,
|
||||
replacementClient.privateKey
|
||||
);
|
||||
assert.equal(new TextDecoder().decode(decrypted.plaintext), "deliverable payload");
|
||||
assert.deepEqual(decrypted.metadata, { purpose: "deliverable" });
|
||||
});
|
||||
41
test/fixtures/filedrop-v1.json
vendored
Normal file
41
test/fixtures/filedrop-v1.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"ciphertext": "encrypted fixture payload",
|
||||
"contactEmail": "client@example.test",
|
||||
"contactName": "Fixture Client",
|
||||
"dropboxId": "fixture-dropbox",
|
||||
"envelope": {
|
||||
"version": 1,
|
||||
"suite": "ECDH-P256+A256KW+A256GCM",
|
||||
"createdAt": "2026-07-10T12:00:00.000Z",
|
||||
"recipient": {
|
||||
"keyFingerprint": "fixture-key-fingerprint",
|
||||
"ephemeralPublicKey": {
|
||||
"key_ops": [],
|
||||
"ext": true,
|
||||
"kty": "EC",
|
||||
"x": "5SL1U-NK-YxVJ3VogCvUj5NXye4egWvFzZUOS9mp1jM",
|
||||
"y": "dibuzB2lXxOlgyJ_relW52HKP16f_8d8yjhdGh5ILE8",
|
||||
"crv": "P-256"
|
||||
},
|
||||
"wrappedContentKey": "fixture-wrapped-content-key"
|
||||
},
|
||||
"content": {
|
||||
"algorithm": "AES-GCM-256",
|
||||
"iv": "fixture-content-iv",
|
||||
"byteLength": 25,
|
||||
"sha256": "fixture-ciphertext-sha256"
|
||||
},
|
||||
"metadata": {
|
||||
"algorithm": "AES-GCM-256",
|
||||
"iv": "fixture-metadata-iv",
|
||||
"ciphertext": "fixture-encrypted-metadata"
|
||||
}
|
||||
},
|
||||
"languagePair": "DE -> EN",
|
||||
"now": "2026-07-10T12:00:00.000Z",
|
||||
"requestedDeadline": "2026-08-01",
|
||||
"routingLabel": "translation",
|
||||
"serviceType": "translation",
|
||||
"uploadId": "upload-fixture-1",
|
||||
"webhookId": "webhook-fixture-1"
|
||||
}
|
||||
Reference in New Issue
Block a user