The writers as Go: test vectors only through testing/, and the note

Fixes T9, T11 and T12 of the review of the session of 1 and 2 October:

- T9: EncryptOptions no longer has testVectors nor areaLen, with which any
  caller could write format 2, or an area of 512 bytes, which rule 13
  forbids and which tells that the capsule has no signature (§55.2). What
  only a generator of test vectors asks, as Go's TestVectors, is the
  TestVectors argument of the core of writer.ts, which only the helpers of
  testing/encrypt.ts pass: encryptVectors and encryptWith write format 2,
  and encryptFilesWith another area, with which the tests still reproduce
  byte for byte the fixtures of 512 bytes. encrypt keeps the shape of
  capsule.Encrypt: without a generator it fails with the text of Go,
  whatever the caller adds. dependencies.test.ts refuses an import of
  testing/ from anything but the tests and testing/ itself, check-build.mjs
  refuses a test or a module of testing/ in the bundle of the pages, and
  note.ts joins the modules that index.ts must not re-export.
- T12: encrypt as a generator refuses a public note and an area with the
  text of Go, "capsule: format 2 has no security area or public note: ...",
  after the head and the length, as capsule.Encrypt. The errors of the note
  carry "capsule: ", and newSealer checks the note, then the profile and the
  clock, in the order of Go. The tests compare the texts byte for byte, also
  for two faults at once.
- T11: capsuleLength takes the public note and predicts exactly the size of
  the .dkc with it: eight notes of 1 to 1024 bytes, across the boundaries of
  the heads of CBOR, with both policies and with other extensions.

The 40 texts that capsule.EncryptFiles and extension.CheckNote give at
spec-v0.11 on the same notes and options, taken with an oracle, are those
of this library; HEAD gave another one in 23 of them. npm run verify
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 6 days ago
parent 67a9037839
commit 4377a87f7f

@ -26,6 +26,9 @@
// nested copy of a package other than the noble copy under // nested copy of a package other than the noble copy under
// @noble/post-quantum (plan of phase 2, section 3 and decision 5), as // @noble/post-quantum (plan of phase 2, section 3 and decision 5), as
// .svelte-kit/output/client-modules.json records it (vite.config.ts); // .svelte-kit/output/client-modules.json records it (vite.config.ts);
// - the client bundle holds a test, or a module of src/lib/dkc/testing/,
// whose helpers write what only a generator of test vectors may write
// (format 2, another security area than 32 KiB);
// - a page loads noble, @scure/base or age-encryption with the page instead // - a page loads noble, @scure/base or age-encryption with the page instead
// of on demand, or a page of ON_DEMAND cannot load its code on demand: the // of on demand, or a page of ON_DEMAND cannot load its code on demand: the
// opening on /inspect (plan of phase 2, section 9) and the writer on // opening on /inspect (plan of phase 2, section 9) and the writer on
@ -250,6 +253,8 @@ for (const f of files) {
const MODULES = join(ROOT, '.svelte-kit', 'output', 'client-modules.json'); const MODULES = join(ROOT, '.svelte-kit', 'output', 'client-modules.json');
const FORBIDDEN_PACKAGES = [/^tlock-js$/, /^drand-client$/, /^@babel\//]; const FORBIDDEN_PACKAGES = [/^tlock-js$/, /^drand-client$/, /^@babel\//];
// The modules that only the tests load: the tests and the helpers of testing/.
const TEST_ONLY = /\/src\/(?:lib\/dkc\/testing\/|.*\.test\.ts$)/;
const chunks = existsSync(MODULES) ? JSON.parse(readFileSync(MODULES, 'utf8')).chunks : {}; const chunks = existsSync(MODULES) ? JSON.parse(readFileSync(MODULES, 'utf8')).chunks : {};
if (!existsSync(MODULES)) fail(`${rel(MODULES)} is missing: vite.config.ts writes it during "npm run build"`); if (!existsSync(MODULES)) fail(`${rel(MODULES)} is missing: vite.config.ts writes it during "npm run build"`);
@ -268,6 +273,7 @@ const packages = new Set();
for (const [file, chunk] of Object.entries(chunks)) { for (const [file, chunk] of Object.entries(chunks)) {
if (!existsSync(join(BUILD, file))) fail(`client chunk ${file} is not in the site`); if (!existsSync(join(BUILD, file))) fail(`client chunk ${file} is not in the site`);
for (const id of Object.keys(chunk.modules)) { for (const id of Object.keys(chunk.modules)) {
if (TEST_ONLY.test(id)) fail(`${file} bundles ${id}, which only the tests may load`);
const pkg = packageOf(id); const pkg = packageOf(id);
if (pkg === null) continue; if (pkg === null) continue;
if (FORBIDDEN_PACKAGES.some((re) => re.test(pkg.name))) fail(`${file} bundles ${pkg.name}: ${id}`); if (FORBIDDEN_PACKAGES.some((re) => re.test(pkg.name))) fail(`${file} bundles ${pkg.name}: ${id}`);

@ -18,7 +18,12 @@
// - a file of src/ other than author.ts, cms.ts, digest.ts, ed25519strict.ts, ibe.ts, // - a file of src/ other than author.ts, cms.ts, digest.ts, ed25519strict.ts, ibe.ts,
// release.ts, x25519.ts and the tests names @noble/, or a noble import is not a subpath of @noble/curves, // release.ts, x25519.ts and the tests names @noble/, or a noble import is not a subpath of @noble/curves,
// @noble/hashes or @noble/ciphers, the root copies that those files // @noble/hashes or @noble/ciphers, the root copies that those files
// resolve to. // resolve to;
// - a file of src/ other than the tests and src/lib/dkc/testing/ itself
// imports a module of testing/, whose helpers write what only a generator
// of test vectors may write (format 2, another security area): no page and
// no module of the library may reach them. check-build.mjs checks the
// bundle of the pages as well.
// //
// Each check is also run on bad inputs, so that a guard that no longer // Each check is also run on bad inputs, so that a guard that no longer
// catches anything fails too. // catches anything fails too.
@ -63,6 +68,8 @@ const AGE_IMPORTERS = ['src/lib/dkc/agefile.ts', 'src/lib/dkc/open.ts', 'src/lib
// crypto.getRandomValues, and the test helpers of testing/. // crypto.getRandomValues, and the test helpers of testing/.
const WRITER_IMPORTERS = ['src/lib/dkc/encrypt.ts']; const WRITER_IMPORTERS = ['src/lib/dkc/encrypt.ts'];
const WRITER_IMPORTER_DIRS = ['src/lib/dkc/testing/']; const WRITER_IMPORTER_DIRS = ['src/lib/dkc/testing/'];
// The helpers of the tests, which only the tests and the helpers themselves import.
const TESTING_DIR = 'src/lib/dkc/testing/';
// The modules that index.ts must not re-export: the opening and the writer, // The modules that index.ts must not re-export: the opening and the writer,
// which would bring noble or age-encryption into the first load of a page, // which would bring noble or age-encryption into the first load of a page,
// and the internals of both; and the head of format 3 and the rules of its // and the internals of both; and the head of format 3 and the rules of its
@ -78,6 +85,7 @@ const NOT_IN_INDEX = [
'encrypt.ts', 'encrypt.ts',
'head.ts', 'head.ts',
'ibe.ts', 'ibe.ts',
'note.ts',
'open.ts', 'open.ts',
'open3.ts', 'open3.ts',
'pathrule-tables.ts', 'pathrule-tables.ts',
@ -165,6 +173,7 @@ function importProblems(files: Source[]): string[] {
if (!test && reaches(name, s, 'writer.ts') && !WRITER_IMPORTERS.includes(name) && !WRITER_IMPORTER_DIRS.some((d) => name.startsWith(d))) { if (!test && reaches(name, s, 'writer.ts') && !WRITER_IMPORTERS.includes(name) && !WRITER_IMPORTER_DIRS.some((d) => name.startsWith(d))) {
problems.push(`${name} imports the core of the writer`); problems.push(`${name} imports the core of the writer`);
} }
if (!test && !name.startsWith(TESTING_DIR) && /(?:^|\/)testing\//.test(s)) problems.push(`${name} imports ${s}, a helper of the tests`);
if (name === 'src/lib/dkc/index.ts' && NOT_IN_INDEX.some((m) => s === `./${m}`)) problems.push(`index.ts re-exports ${s}`); if (name === 'src/lib/dkc/index.ts' && NOT_IN_INDEX.some((m) => s === `./${m}`)) problems.push(`index.ts re-exports ${s}`);
if (FORBIDDEN.includes(packageOf(s))) problems.push(`${name}: ${s} is forbidden`); if (FORBIDDEN.includes(packageOf(s))) problems.push(`${name}: ${s} is forbidden`);
else if (s.includes('node_modules')) problems.push(`${name}: ${s} reaches into node_modules`); else if (s.includes('node_modules')) problems.push(`${name}: ${s} reaches into node_modules`);
@ -228,6 +237,12 @@ describe('runtime dependencies', () => {
expect(AGE_IMPORTERS.filter((f) => f !== 'src/lib/dkc/writer.ts').every((f) => sources().some((s) => s.name === f))).toBe(true); expect(AGE_IMPORTERS.filter((f) => f !== 'src/lib/dkc/writer.ts').every((f) => sources().some((s) => s.name === f))).toBe(true);
}); });
it('only the tests and testing/ itself import the helpers of testing/, which write what only a generator of test vectors may write', () => {
// The same check as above, whose problems include this rule.
expect(importProblems(sources())).toEqual([]);
expect(sources().some((s) => s.name.startsWith(TESTING_DIR) && s.name.endsWith('/encrypt.ts'))).toBe(true);
});
it('the import check rejects each forbidden import', () => { it('the import check rejects each forbidden import', () => {
const noble = "import { bls12_381 } from '@noble/curves/bls12-381.js';"; const noble = "import { bls12_381 } from '@noble/curves/bls12-381.js';";
const chacha = "import { chacha20poly1305 } from '@noble/ciphers/chacha.js';"; const chacha = "import { chacha20poly1305 } from '@noble/ciphers/chacha.js';";
@ -241,6 +256,9 @@ describe('runtime dependencies', () => {
{ name: 'src/lib/dkc/testing/encrypt.ts', text: "import { seal } from '../writer.ts';" }, { name: 'src/lib/dkc/testing/encrypt.ts', text: "import { seal } from '../writer.ts';" },
{ name: 'src/lib/dkc/writer.test.ts', text: "import { seal } from './writer.ts';\nimport { Decrypter } from 'age-encryption';" }, { name: 'src/lib/dkc/writer.test.ts', text: "import { seal } from './writer.ts';\nimport { Decrypter } from 'age-encryption';" },
{ name: 'src/lib/dkc/index.ts', text: "export * from './padding.ts';" }, { name: 'src/lib/dkc/index.ts', text: "export * from './padding.ts';" },
{ name: 'src/lib/dkc/encrypt.test.ts', text: "import { encryptVectors } from './testing/encrypt.ts';" },
{ name: 'src/lib/inspector/opener.test.ts', text: "import { encryptVectors } from '../dkc/testing/encrypt.ts';" },
{ name: 'src/lib/dkc/testing/interop.ts', text: "import { sampleIdentity } from './testdata.ts';" },
]), ]),
).toEqual([]); ).toEqual([]);
const bad: [label: string, name: string, text: string][] = [ const bad: [label: string, name: string, text: string][] = [
@ -260,6 +278,11 @@ describe('runtime dependencies', () => {
['the writer in index.ts', 'src/lib/dkc/index.ts', "export * from './encrypt.ts';"], ['the writer in index.ts', 'src/lib/dkc/index.ts', "export * from './encrypt.ts';"],
['the opening in index.ts', 'src/lib/dkc/index.ts', "export { open } from './open.ts';"], ['the opening in index.ts', 'src/lib/dkc/index.ts', "export { open } from './open.ts';"],
['recipient.ts in index.ts', 'src/lib/dkc/index.ts', "export * from './recipient.ts';"], ['recipient.ts in index.ts', 'src/lib/dkc/index.ts', "export * from './recipient.ts';"],
['note.ts in index.ts', 'src/lib/dkc/index.ts', "export * from './note.ts';"],
['a helper of the tests in index.ts', 'src/lib/dkc/index.ts', "export * from './testing/encrypt.ts';"],
['a helper of the tests from the library', 'src/lib/dkc/encrypt.ts', "import { encryptWith } from './testing/encrypt.ts';"],
['a helper of the tests from a page', 'src/lib/inspector/creator.ts', "const t = await import('../dkc/testing/encrypt.ts');"],
['a helper of the tests from a route', 'src/routes/create/+page.svelte', "import { fixedDraws } from '$lib/dkc/testing/encrypt.ts';"],
]; ];
for (const [label, name, text] of bad) { for (const [label, name, text] of bad) {
expect(importProblems([{ name, text }]), label).not.toEqual([]); expect(importProblems([{ name, text }]), label).not.toEqual([]);

@ -153,9 +153,6 @@ describe('encryptFiles, the fixtures of format 3 of the Go reference', () => {
policy: keyed ? TIME_AND_KEY : TIME_ONLY, policy: keyed ? TIME_AND_KEY : TIME_ONLY,
newPortableKey: dkk !== undefined, newPortableKey: dkk !== undefined,
padding: fx.padding, padding: fx.padding,
// The fixtures of format 3 were written by a writer of v0.10, with the area of 512 bytes, which only a generator of test vectors writes now.
testVectors: true,
areaLen: AREA_UNIT,
...(fx.comment === undefined ? {} : { comment: fx.comment }), ...(fx.comment === undefined ? {} : { comment: fx.comment }),
...(fx.declared_author === undefined ? {} : { author: fx.declared_author }), ...(fx.declared_author === undefined ? {} : { author: fx.declared_author }),
}), }),
@ -166,6 +163,9 @@ describe('encryptFiles, the fixtures of format 3 of the Go reference', () => {
...(dkk === undefined ? {} : { accessIdentity: dkk.material, credentialId: dkk.credentialId }), ...(dkk === undefined ? {} : { accessIdentity: dkk.material, credentialId: dkk.credentialId }),
...(keyed ? { words: wordsFor([fx.access_key_stanza!]) } : {}), ...(keyed ? { words: wordsFor([fx.access_key_stanza!]) } : {}),
}, },
// The fixtures of format 3 were written by a writer of v0.10, with the area of 512 bytes, which only a generator
// of test vectors writes now.
{ areaLen: AREA_UNIT },
); );
const written = split(res.dkc!); const written = split(res.dkc!);
const original = split(readBytes(`fixtures/${name}.dkc`)); const original = split(readBytes(`fixtures/${name}.dkc`));
@ -203,16 +203,24 @@ describe('encryptFiles, the writer of spec v0.11', () => {
expect(AREA_LEN).toBe(32768); expect(AREA_LEN).toBe(32768);
expect(res.length).toBeGreaterThan(AREA_LEN); expect(res.length).toBeGreaterThan(AREA_LEN);
// The area of the writers of v0.10 is for a generator of test vectors, and only in multiples of its unit. // The area of the writers of v0.10 is for a generator of test vectors, and only in multiples of its unit.
const old = await encryptFiles([source('a', 'x')], options({ testVectors: true, areaLen: AREA_UNIT })); const old = await encryptFilesWith([source('a', 'x')], options(), {}, { areaLen: AREA_UNIT });
const oldOpened = await open(old.dkc!, opening(R1000, { sink: new MemorySink() })); const oldOpened = await open(old.dkc!, opening(R1000, { sink: new MemorySink() }));
expect([oldOpened.error, oldOpened.areaLen]).toEqual([undefined, AREA_UNIT]); expect([oldOpened.error, oldOpened.areaLen]).toEqual([undefined, AREA_UNIT]);
for (const [name, extra] of [ // A generator that asks for no area writes the common one.
['512 without testVectors', { areaLen: AREA_UNIT }], const generated = await encryptFilesWith([source('a', 'x')], options(), {}, {});
['an area that is not a multiple of 512', { testVectors: true, areaLen: 1000 }], expect((await open(generated.dkc!, opening(R1000, { sink: new MemorySink() }))).areaLen).toBe(AREA_LEN);
['an area above 64 KiB', { testVectors: true, areaLen: 66_048 }], for (const [name, areaLen] of [
['an area that is not a multiple of 512', 1000],
['an area above 64 KiB', 66_048],
['an area below 512', 0],
['an area that is not an integer', 512.5],
] as const) { ] as const) {
expect((await failure(encryptFiles([source('a', 'x')], options({ ...extra })))).message, name).toMatch(/the security area is 32768 bytes/); const err = await failure(encryptFilesWith([source('a', 'x')], options(), {}, { areaLen }));
expect(err.message, name).toBe('capsule: the security area is 32768 bytes: another size is for a generator of test vectors, a multiple of 512 up to 65536 (spec §62.1 rule 13)');
} }
// No option of encryptFiles asks for another area: what a caller adds is nothing, and the area is 32 KiB.
const asked = await encryptFiles([source('a', 'x')], { ...options(), testVectors: true, areaLen: AREA_UNIT } as EncryptOptions);
expect((await open(asked.dkc!, opening(R1000, { sink: new MemorySink() }))).areaLen).toBe(AREA_LEN);
}); });
it('writes the public note in PUBLIC_HEADER, which anyone reads and header_binding ties to the control', async () => { it('writes the public note in PUBLIC_HEADER, which anyone reads and header_binding ties to the control', async () => {
@ -226,13 +234,50 @@ describe('encryptFiles, the writer of spec v0.11', () => {
changed.set(new TextEncoder().encode('Lisbon'), at); changed.set(new TextEncoder().encode('Lisbon'), at);
const bad = await open(changed, opening(R1000, { sink: new MemorySink() })); const bad = await open(changed, opening(R1000, { sink: new MemorySink() }));
expect(bad.error?.code).toBe('ERR_HEADER_BINDING'); expect(bad.error?.code).toBe('ERR_HEADER_BINDING');
// A note that breaks the rules of text is refused before anything is written.
for (const text of ['a\tb', 'a\nb', ' a', 'a ', 'x'.repeat(1025)]) {
expect((await failure(encryptFiles([source('a', 'x')], options({ publicNote: text })))).message, JSON.stringify(text.slice(0, 8))).toMatch(/public note/);
}
// With no note, or an empty one, there is none. // With no note, or an empty one, there is none.
const none = await encryptFiles([source('a', 'x')], options({ publicNote: '' })); const none = await encryptFiles([source('a', 'x')], options({ publicNote: '' }));
expect(none.dkc).toBeDefined(); const noneOpened = await open(none.dkc!, opening(R1000, { sink: new MemorySink() }));
expect(noneOpened.inspection.header!.noncritical).toEqual([]);
});
// A note that breaks the rules of §24.1 is refused before anything is written, never corrected, with the texts that
// capsule.EncryptFiles gives at spec-v0.11: those of extension.CheckNote after "capsule: ".
const rules = (detail: string): string => `capsule: a public note that breaks the rules of text: text: ${detail}: ERR_EXTENSION_DATA_INVALID`;
it.each([
['a tab', 'a\tb', rules('control U+0009 in the declared author')],
['a line feed', 'a\nb', rules('control U+000A in the declared author')],
['a carriage return', 'a\rb', rules('control U+000D')],
['a space at the start', ' a', rules('the declared author starts or ends with U+0020')],
['a space at the end', 'a ', rules('the declared author starts or ends with U+0020')],
['1025 bytes', 'x'.repeat(1025), 'capsule: a public note of 1025 bytes, not 1 to 1024: ERR_EXTENSION_DATA_INVALID'],
['a right-to-left override', 'a\u{202e}b', rules('bidirectional control U+202E')],
['a zero-width space', 'a\u{200b}b', rules('invisible U+200B')],
['a byte order mark at the start', '\u{feff}abc', rules('byte order mark U+FEFF')],
['a lone surrogate', `a${String.fromCharCode(0xd800)}b`, 'capsule: a public note that is not valid UTF-8: ERR_EXTENSION_DATA_INVALID'],
])('refuses a public note with %s, with the text of Go, and aborts the output', async (_name, note, text) => {
const out = recorder();
const err = await failure(encryptFiles([source('a', 'x')], options({ publicNote: note, output: out.stream })));
expect([err.message, errorCode(err), out.chunks.length, out.state.aborted]).toEqual([text, 'ERR_EXTENSION_DATA_INVALID', 0, err]);
});
// capsule.newSealer checks the public note first, then the profile and the clock, and EncryptFiles checks Length
// before newSealer and the files after it: the texts that Go gives for two faults at once.
it('checks the public note in the order of Go', async () => {
const tab = rules('control U+0009 in the declared author');
const bad = { publicNote: 'a\tb' };
const cases: [string, FileSource[], EncryptOptions, string][] = [
['a bad note and no profile', [source('a', 'x')], { ...options(bad), profile: undefined } as unknown as EncryptOptions, tab],
['a bad note and no clock', [source('a', 'x')], { ...options(bad), now: undefined } as unknown as EncryptOptions, tab],
['a bad note and an instant in the past', [source('a', 'x')], options({ ...bad, unlockAt: GENESIS }), tab],
['a bad note and policy 7', [source('a', 'x')], options({ ...bad, policy: 7 }), tab],
['a bad note and padding code 3', [source('a', 'x')], options({ ...bad, padding: 3 as Padding }), tab],
['a bad note and a bad declared author', [source('a', 'x')], options({ ...bad, author: ' x' }), tab],
['a bad note and no files', [], options(bad), tab],
['a bad note and Length', [source('a', 'x')], options({ ...bad, length: 5 }), 'capsule: EncryptOptions.Length is for Encrypt: EncryptFiles computes L from the files'],
['no profile and no clock', [source('a', 'x')], { ...options(), profile: undefined, now: undefined } as unknown as EncryptOptions, 'capsule: EncryptOptions.Profile is required'],
['no clock and an instant in the past', [source('a', 'x')], { ...options({ unlockAt: GENESIS }), now: undefined } as unknown as EncryptOptions, 'capsule: EncryptOptions.Now is required'],
];
for (const [name, files, opts, text] of cases) expect((await failure(encryptFiles(files, opts))).message, name).toBe(text);
}); });
}); });
@ -366,6 +411,27 @@ describe('the lengths of format 3', () => {
} }
}); });
// Spec v0.11 §24.1: the public note is an extension of PUBLIC_HEADER, which capsuleLength measures with the others,
// around the boundaries of the heads of CBOR and up to its 1024 bytes.
it('gives the exact size of the .dkc with a public note', async () => {
const notes = ['x', 'a'.repeat(23), 'a'.repeat(24), 'b'.repeat(255), 'c'.repeat(256), 'Cartas del viaje a Lisboa · 2026', 'ñ'.repeat(512), '\u{1f600}'.repeat(256)];
for (const [i, publicNote] of notes.entries()) {
const shape: HeadShape = { files: [{ path: 'nota.txt', size: 1000 * i, mtime: 1_790_769_600_000 }] };
const noncritical = i % 2 === 0 ? [] : [{ id: 'a.example', version: 2, data: Uint8Array.of(1, 2) }];
for (const [policy, extra] of [
[TIME_ONLY, {}],
[TIME_AND_KEY, { newPortableKey: true }],
] as const) {
const files = shape.files.map((f) => source(f.path, new Uint8Array(f.size), f.mtime));
const res = await encryptFiles(files, options({ policy, ...extra, noncritical, publicNote }));
const input = { profileId: 'datekeys:quicknet:v1', round: res.dateKey.round, policy, length: bodyLength(shape), noncritical };
expect([capsuleLength({ ...input, publicNote }), res.size], `note ${i}, policy ${policy}`).toEqual([res.dkc!.length, res.dkc!.length]);
// Without the note, the size falls short: the note is public, and the .dkc carries it as it is.
expect(capsuleLength(input)).toBeLessThan(res.dkc!.length - new TextEncoder().encode(publicNote).length);
}
}
});
// A seeded generator, and the head that the writer builds for a shape. // A seeded generator, and the head that the writer builds for a shape.
function random(seed: number): () => number { function random(seed: number): () => number {
let x = seed >>> 0; let x = seed >>> 0;
@ -539,6 +605,7 @@ describe('encryptFiles, invalid inputs', () => {
[[{ path: 'a', size: 0, mtime: Number.NaN, open: a.open }], {}, 'encrypt: file 0: mtime is not a finite number of milliseconds'], [[{ path: 'a', size: 0, mtime: Number.NaN, open: a.open }], {}, 'encrypt: file 0: mtime is not a finite number of milliseconds'],
[[a], { comment: 5 }, 'encrypt: EncryptOptions.comment is not a string'], [[a], { comment: 5 }, 'encrypt: EncryptOptions.comment is not a string'],
[[a], { author: {} }, 'encrypt: EncryptOptions.author is not a string'], [[a], { author: {} }, 'encrypt: EncryptOptions.author is not a string'],
[[a], { publicNote: 5 }, 'encrypt: EncryptOptions.publicNote is not a string'],
] as const) { ] as const) {
const err = await failure(encryptFiles(files as unknown as FileSource[], options(extra as Partial<EncryptOptions>))); const err = await failure(encryptFiles(files as unknown as FileSource[], options(extra as Partial<EncryptOptions>)));
expect([err instanceof TypeError, err.message]).toEqual([true, text]); expect([err instanceof TypeError, err.message]).toEqual([true, text]);

@ -7,11 +7,12 @@ import { Encrypter, type ReadableStreamWithSize } from 'age-encryption';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { concatBytes } from './bytes.ts'; import { concatBytes } from './bytes.ts';
import { parseRFC3339 } from './datekey.ts'; import { parseRFC3339 } from './datekey.ts';
import { encrypt, type EncryptOptions } from './encrypt.ts'; import type { EncryptOptions } from './encrypt.ts';
import { errorCode } from './errors.ts'; import { errorCode } from './errors.ts';
import { TIME_ONLY } from './header.ts'; import { TIME_ONLY } from './header.ts';
import { quicknet } from './profile.ts'; import { quicknet } from './profile.ts';
import { formatX25519Recipient } from './recipient.ts'; import { formatX25519Recipient } from './recipient.ts';
import { encryptVectors } from './testing/encrypt.ts';
import { newX25519Identity, x25519PublicKey } from './x25519.ts'; import { newX25519Identity, x25519PublicKey } from './x25519.ts';
afterEach(() => { afterEach(() => {
@ -30,7 +31,6 @@ const options = (output: WritableStream<Uint8Array>, extra: Partial<EncryptOptio
policy: TIME_ONLY, policy: TIME_ONLY,
now: () => parseRFC3339('2023-08-23T15:09:27Z'), now: () => parseRFC3339('2023-08-23T15:09:27Z'),
output, output,
testVectors: true,
...extra, ...extra,
}); });
@ -90,7 +90,7 @@ describe('the internal errors of the writer', () => {
const gone = new Error('Web Crypto is gone'); const gone = new Error('Web Crypto is gone');
onCall(1, () => Promise.reject(gone)); onCall(1, () => Promise.reject(gone));
const out = recorder(); const out = recorder();
const err = await failure(encrypt(new Uint8Array(1), options(out.stream))); const err = await failure(encryptVectors(new Uint8Array(1), options(out.stream)));
expect([err.message, err.cause, errorCode(err)]).toEqual(['capsule: age failed to seal', gone, '']); expect([err.message, err.cause, errorCode(err)]).toEqual(['capsule: age failed to seal', gone, '']);
expect([out.chunks.length, out.state.closed, out.state.aborted]).toEqual([0, false, err]); expect([out.chunks.length, out.state.closed, out.state.aborted]).toEqual([0, false, err]);
}); });
@ -98,7 +98,7 @@ describe('the internal errors of the writer', () => {
it('refuses a SEALED_CONTROL that does not measure what the formula gives', async () => { it('refuses a SEALED_CONTROL that does not measure what the formula gives', async () => {
onCall(1, async (self, file) => concatBytes(await sealBytes(self, file as Uint8Array), new Uint8Array(1))); onCall(1, async (self, file) => concatBytes(await sealBytes(self, file as Uint8Array), new Uint8Array(1)));
const out = recorder(); const out = recorder();
const err = await failure(encrypt(new Uint8Array(1), options(out.stream))); const err = await failure(encryptVectors(new Uint8Array(1), options(out.stream)));
expect(err.message).toBe('capsule: internal error: SEALED_CONTROL is 459 bytes, measured 458'); expect(err.message).toBe('capsule: internal error: SEALED_CONTROL is 459 bytes, measured 458');
expect([out.chunks.length, out.state.aborted]).toEqual([0, err]); expect([out.chunks.length, out.state.aborted]).toEqual([0, err]);
}); });
@ -110,7 +110,7 @@ describe('the internal errors of the writer', () => {
return sealBytes(other, file as Uint8Array); return sealBytes(other, file as Uint8Array);
}); });
const out = recorder(); const out = recorder();
const err = await failure(encrypt(new Uint8Array(1), options(out.stream))); const err = await failure(encryptVectors(new Uint8Array(1), options(out.stream)));
expect(err.message).toMatch(/^capsule: self-check: OUTER_TIME_AGE: agewrap: /); expect(err.message).toMatch(/^capsule: self-check: OUTER_TIME_AGE: agewrap: /);
expect(errorCode(err)).not.toBe(''); expect(errorCode(err)).not.toBe('');
expect([out.chunks.length, out.state.aborted]).toEqual([0, err]); expect([out.chunks.length, out.state.aborted]).toEqual([0, err]);
@ -123,14 +123,14 @@ describe('the internal errors of the writer', () => {
return s; return s;
}); });
const out = recorder(); const out = recorder();
const err = await failure(encrypt(new Uint8Array(1), options(out.stream))); const err = await failure(encryptVectors(new Uint8Array(1), options(out.stream)));
expect(err.message).toBe('capsule: internal error: PAYLOAD_AGE would be 0 bytes, P = 256 gives 456'); expect(err.message).toBe('capsule: internal error: PAYLOAD_AGE would be 0 bytes, P = 256 gives 456');
expect([out.chunks.length, out.state.aborted]).toEqual([0, err]); expect([out.chunks.length, out.state.aborted]).toEqual([0, err]);
vi.restoreAllMocks(); vi.restoreAllMocks();
onCall(2, (self, file) => headerOnly(self, file as ReadableStream<Uint8Array>)); onCall(2, (self, file) => headerOnly(self, file as ReadableStream<Uint8Array>));
const out2 = recorder(); const out2 = recorder();
const err2 = await failure(encrypt(new Uint8Array(1), options(out2.stream))); const err2 = await failure(encryptVectors(new Uint8Array(1), options(out2.stream)));
expect(err2.message).toBe('capsule: self-check: PAYLOAD_AGE is 168 bytes, P = 256 gives 456'); expect(err2.message).toBe('capsule: self-check: PAYLOAD_AGE is 168 bytes, P = 256 gives 456');
expect([out2.state.closed, out2.state.aborted]).toEqual([false, err2]); expect([out2.state.closed, out2.state.aborted]).toEqual([false, err2]);
}); });
@ -144,7 +144,7 @@ describe('the internal errors of the writer', () => {
return empty; return empty;
}); });
const out = recorder(); const out = recorder();
const err = await failure(encrypt(new Uint8Array(1), options(out.stream))); const err = await failure(encryptVectors(new Uint8Array(1), options(out.stream)));
expect(err.message).toBe('capsule: self-check: the age header of PAYLOAD_AGE does not parse'); expect(err.message).toBe('capsule: self-check: the age header of PAYLOAD_AGE does not parse');
expect([out.chunks.length, out.state.aborted]).toEqual([0, err]); expect([out.chunks.length, out.state.aborted]).toEqual([0, err]);
}); });
@ -153,7 +153,7 @@ describe('the internal errors of the writer', () => {
const broke = new Error('the STREAM broke'); const broke = new Error('the STREAM broke');
onCall(2, (self, file) => headerOnly(self, file as ReadableStream<Uint8Array>, broke)); onCall(2, (self, file) => headerOnly(self, file as ReadableStream<Uint8Array>, broke));
const out = recorder(); const out = recorder();
const err = await failure(encrypt(new Uint8Array(1), options(out.stream))); const err = await failure(encryptVectors(new Uint8Array(1), options(out.stream)));
expect([err.message, err.cause]).toEqual(['capsule: age failed to seal', broke]); expect([err.message, err.cause]).toEqual(['capsule: age failed to seal', broke]);
expect([out.state.closed, out.state.aborted]).toEqual([false, err]); expect([out.state.closed, out.state.aborted]).toEqual([false, err]);
}); });
@ -161,7 +161,7 @@ describe('the internal errors of the writer', () => {
it('refuses a SEALED_CONTROL above 64 MiB before sealing anything (§57)', async () => { it('refuses a SEALED_CONTROL above 64 MiB before sealing anything (§57)', async () => {
const out = recorder(); const out = recorder();
const big = { id: 'org.example.big', version: 1, data: new Uint8Array(64 << 20) }; const big = { id: 'org.example.big', version: 1, data: new Uint8Array(64 << 20) };
const err = await failure(encrypt(new Uint8Array(1), options(out.stream, { controlNoncritical: [big] }))); const err = await failure(encryptVectors(new Uint8Array(1), options(out.stream, { controlNoncritical: [big] })));
expect(err.message).toMatch(/^capsule: SEALED_CONTROL of \d+ bytes exceeds 67108864: ERR_INTEGRITY$/); expect(err.message).toMatch(/^capsule: SEALED_CONTROL of \d+ bytes exceeds 67108864: ERR_INTEGRITY$/);
expect(errorCode(err)).toBe('ERR_INTEGRITY'); expect(errorCode(err)).toBe('ERR_INTEGRITY');
expect([out.chunks.length, out.state.aborted]).toEqual([0, err]); expect([out.chunks.length, out.state.aborted]).toEqual([0, err]);

@ -15,7 +15,7 @@ import { ACCESS_SLOTS, ageStanzas } from './age.ts';
import { decryptAll } from './agefile.ts'; import { decryptAll } from './agefile.ts';
import { sha256 } from './bytes.ts'; import { sha256 } from './bytes.ts';
import { type Instant, parseRFC3339 } from './datekey.ts'; import { type Instant, parseRFC3339 } from './datekey.ts';
import { encrypt, type EncryptOptions } from './encrypt.ts'; import type { EncryptOptions } from './encrypt.ts';
import type { Extension } from './extension.ts'; import type { Extension } from './extension.ts';
import { TIME_AND_KEY, TIME_ONLY } from './header.ts'; import { TIME_AND_KEY, TIME_ONLY } from './header.ts';
import { inspect } from './inspect.ts'; import { inspect } from './inspect.ts';
@ -25,6 +25,7 @@ import { paddedLength, payloadAgeLength, REFORZADO } from './padding.ts';
import { quicknet } from './profile.ts'; import { quicknet } from './profile.ts';
import { type Release, suppliedRelease } from './release.ts'; import { type Release, suppliedRelease } from './release.ts';
import { split } from './testing/capsule.ts'; import { split } from './testing/capsule.ts';
import { encryptVectors } from './testing/encrypt.ts';
import { h, hx, readJSON } from './testing/testdata.ts'; import { h, hx, readJSON } from './testing/testdata.ts';
import { newX25519Identity, unwrapX25519, x25519PublicKey } from './x25519.ts'; import { newX25519Identity, unwrapX25519, x25519PublicKey } from './x25519.ts';
@ -147,7 +148,6 @@ function makeCase(seed: number): Case {
controlCritical, controlCritical,
controlNoncritical, controlNoncritical,
now: () => GENESIS, now: () => GENESIS,
testVectors: true,
}, },
body: full, body: full,
release, release,
@ -171,7 +171,7 @@ describe('the property loop of the writer', () => {
}); });
let res; let res;
try { try {
res = await encrypt(c.body, { ...c.opts, output }); res = await encryptVectors(c.body, { ...c.opts, output });
} catch (err) { } catch (err) {
expect(c.invalid, `unexpected failure: ${(err as Error).message}`).toBeDefined(); expect(c.invalid, `unexpected failure: ${(err as Error).message}`).toBeDefined();
expect([chunks.length, state.closed, state.aborted]).toEqual([0, false, err]); expect([chunks.length, state.closed, state.aborted]).toEqual([0, false, err]);

@ -6,13 +6,14 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { concatBytes, sha256 } from './bytes.ts'; import { concatBytes, sha256 } from './bytes.ts';
import { type Instant, parseRFC3339 } from './datekey.ts'; import { type Instant, parseRFC3339 } from './datekey.ts';
import { encrypt, type EncryptOptions } from './encrypt.ts'; import type { EncryptOptions } from './encrypt.ts';
import { errorCode } from './errors.ts'; import { errorCode } from './errors.ts';
import { TIME_AND_KEY, TIME_ONLY } from './header.ts'; import { TIME_AND_KEY, TIME_ONLY } from './header.ts';
import { open, type OpenOptions } from './open.ts'; import { open, type OpenOptions } from './open.ts';
import { quicknet } from './profile.ts'; import { quicknet } from './profile.ts';
import { type Release, suppliedRelease } from './release.ts'; import { type Release, suppliedRelease } from './release.ts';
import { split } from './testing/capsule.ts'; import { split } from './testing/capsule.ts';
import { encryptVectors } from './testing/encrypt.ts';
import { h, hx, readJSON } from './testing/testdata.ts'; import { h, hx, readJSON } from './testing/testdata.ts';
const R1000: Release = (() => { const R1000: Release = (() => {
@ -28,7 +29,6 @@ const options = (extra: Partial<EncryptOptions> = {}): EncryptOptions => ({
unlockAt: T1000, unlockAt: T1000,
policy: TIME_ONLY, policy: TIME_ONLY,
now: () => GENESIS, now: () => GENESIS,
testVectors: true,
...extra, ...extra,
}); });
const opening = (extra: Partial<OpenOptions> = {}): OpenOptions => ({ source: suppliedRelease(R1000), now: () => T1000, ...extra }); const opening = (extra: Partial<OpenOptions> = {}): OpenOptions => ({ source: suppliedRelease(R1000), now: () => T1000, ...extra });
@ -97,7 +97,7 @@ describe('streaming', () => {
for (const [label, src] of sources) { for (const [label, src] of sources) {
const out = recorder(); const out = recorder();
const progress: [number, number][] = []; const progress: [number, number][] = [];
const res = await encrypt(src(), options({ length: n, output: out.stream, progress: (w, t) => void progress.push([w, t]) })); const res = await encryptVectors(src(), options({ length: n, output: out.stream, progress: (w, t) => void progress.push([w, t]) }));
const dkc = concatBytes(...out.chunks); const dkc = concatBytes(...out.chunks);
expect([res.dkc, res.size, out.state.closed, out.state.aborted], `${n}, ${label}`).toEqual([undefined, dkc.length, true, undefined]); expect([res.dkc, res.size, out.state.closed, out.state.aborted], `${n}, ${label}`).toEqual([undefined, dkc.length, true, undefined]);
expect(progress[0], label).toEqual([0, res.size]); expect(progress[0], label).toEqual([0, res.size]);
@ -112,7 +112,7 @@ describe('streaming', () => {
it('digests exactly what it writes into the capsule_digest of the .dkk', async () => { it('digests exactly what it writes into the capsule_digest of the .dkk', async () => {
const out = recorder(); const out = recorder();
const res = await encrypt(chunked(content(100_000), [4096]), options({ length: 100_000, output: out.stream, policy: TIME_AND_KEY, newPortableKey: true })); const res = await encryptVectors(chunked(content(100_000), [4096]), options({ length: 100_000, output: out.stream, policy: TIME_AND_KEY, newPortableKey: true }));
expect(hx(res.portableKey!.verification!.capsuleDigest)).toBe(hx(await sha256(concatBytes(...out.chunks)))); expect(hx(res.portableKey!.verification!.capsuleDigest)).toBe(hx(await sha256(concatBytes(...out.chunks))));
}); });
@ -129,7 +129,7 @@ describe('streaming', () => {
], ],
] as const) { ] as const) {
const out = recorder(); const out = recorder();
const err = await failure(encrypt(src, options({ length: 100, output: out.stream }))); const err = await failure(encryptVectors(src, options({ length: 100, output: out.stream })));
expect([err.message, errorCode(err)], label).toEqual([text, '']); expect([err.message, errorCode(err)], label).toEqual([text, '']);
expect([out.state.closed, out.state.aborted], label).toEqual([false, err]); expect([out.state.closed, out.state.aborted], label).toEqual([false, err]);
} }
@ -138,7 +138,7 @@ describe('streaming', () => {
it('rethrows the error of the source and of the output as it is, and aborts the output', async () => { it('rethrows the error of the source and of the output as it is, and aborts the output', async () => {
const boom = new Error('disk read failed'); const boom = new Error('disk read failed');
const out = recorder(); const out = recorder();
const err = await failure(encrypt(chunked(content(300_000), [65536], { fail: boom }), options({ length: 400_000, output: out.stream }))); const err = await failure(encryptVectors(chunked(content(300_000), [65536], { fail: boom }), options({ length: 400_000, output: out.stream })));
expect(err).toBe(boom); expect(err).toBe(boom);
expect([out.state.closed, out.state.aborted]).toEqual([false, boom]); expect([out.state.closed, out.state.aborted]).toEqual([false, boom]);
expect(out.chunks.length).toBeGreaterThan(0); expect(out.chunks.length).toBeGreaterThan(0);
@ -146,12 +146,12 @@ describe('streaming', () => {
const full = new Error('disk full'); const full = new Error('disk full');
let writes = 0; let writes = 0;
const failing = new WritableStream<Uint8Array>({ write: () => (++writes === 3 ? Promise.reject(full) : undefined) }); const failing = new WritableStream<Uint8Array>({ write: () => (++writes === 3 ? Promise.reject(full) : undefined) });
expect(await failure(encrypt(content(300_000), options({ output: failing })))).toBe(full); expect(await failure(encryptVectors(content(300_000), options({ output: failing })))).toBe(full);
const late = new Error('no more room'); const late = new Error('no more room');
const out2 = recorder(); const out2 = recorder();
const err2 = await failure( const err2 = await failure(
encrypt( encryptVectors(
content(300_000), content(300_000),
options({ options({
output: out2.stream, output: out2.stream,
@ -172,13 +172,13 @@ describe('streaming', () => {
}, },
}); });
const refused = new WritableStream<Uint8Array>({ write: () => Promise.reject(full) }); const refused = new WritableStream<Uint8Array>({ write: () => Promise.reject(full) });
expect(await failure(encrypt(endless, options({ length: 1 << 20, output: refused })))).toBe(full); expect(await failure(encryptVectors(endless, options({ length: 1 << 20, output: refused })))).toBe(full);
// A stream that another reader holds fails as the caller's own error. // A stream that another reader holds fails as the caller's own error.
const locked = chunked(content(10), [10]); const locked = chunked(content(10), [10]);
locked.getReader(); locked.getReader();
const out3 = recorder(); const out3 = recorder();
const err3 = await failure(encrypt(locked, options({ length: 10, output: out3.stream }))); const err3 = await failure(encryptVectors(locked, options({ length: 10, output: out3.stream })));
expect(err3.name).toBe('TypeError'); expect(err3.name).toBe('TypeError');
expect(err3.message).not.toBe('capsule: age failed to seal'); expect(err3.message).not.toBe('capsule: age failed to seal');
expect([out3.chunks.length, out3.state.aborted]).toEqual([0, err3]); expect([out3.chunks.length, out3.state.aborted]).toEqual([0, err3]);
@ -188,7 +188,7 @@ describe('streaming', () => {
const out = recorder(); const out = recorder();
const refused = new Error('quota'); const refused = new Error('quota');
const err = await failure( const err = await failure(
encrypt( encryptVectors(
content(10), content(10),
options({ options({
output: out.stream, output: out.stream,
@ -201,15 +201,15 @@ describe('streaming', () => {
expect(err).toBe(refused); expect(err).toBe(refused);
expect([out.chunks.length, out.state.closed, out.state.aborted]).toEqual([0, false, refused]); expect([out.chunks.length, out.state.closed, out.state.aborted]).toEqual([0, false, refused]);
const stubborn = new WritableStream<Uint8Array>({ abort: () => Promise.reject(new Error('cannot abort')) }); const stubborn = new WritableStream<Uint8Array>({ abort: () => Promise.reject(new Error('cannot abort')) });
expect((await failure(encrypt(content(10), options({ output: stubborn, policy: 7 })))).message).toBe('capsule: unknown access policy 7'); expect((await failure(encryptVectors(content(10), options({ output: stubborn, policy: 7 })))).message).toBe('capsule: unknown access policy 7');
}); });
}); });
describe('mixes of capsules written here', () => { describe('mixes of capsules written here', () => {
it('gives the code and the step of the reader for each mix of two capsules of one round', async () => { it('gives the code and the step of the reader for each mix of two capsules of one round', async () => {
const a = split((await encrypt(te.encode('A'), options())).dkc!); const a = split((await encryptVectors(te.encode('A'), options())).dkc!);
const b = split((await encrypt(te.encode('B'), options())).dkc!); const b = split((await encryptVectors(te.encode('B'), options())).dkc!);
const k = split((await encrypt(te.encode('K'), options({ policy: TIME_AND_KEY, newPortableKey: true }))).dkc!); const k = split((await encryptVectors(te.encode('K'), options({ policy: TIME_AND_KEY, newPortableKey: true }))).dkc!);
const frame = (prelude: Uint8Array, header: Uint8Array, sealed: Uint8Array, payload: Uint8Array): Uint8Array => { const frame = (prelude: Uint8Array, header: Uint8Array, sealed: Uint8Array, payload: Uint8Array): Uint8Array => {
const out = concatBytes(prelude, header, sealed, payload); const out = concatBytes(prelude, header, sealed, payload);
const v = new DataView(out.buffer); const v = new DataView(out.buffer);

@ -24,7 +24,7 @@ import { quicknet } from './profile.ts';
import { formatX25519Recipient } from './recipient.ts'; import { formatX25519Recipient } from './recipient.ts';
import { type Release, suppliedRelease } from './release.ts'; import { type Release, suppliedRelease } from './release.ts';
import { split } from './testing/capsule.ts'; import { split } from './testing/capsule.ts';
import { encryptWith, wordsFor } from './testing/encrypt.ts'; import { encryptVectors, encryptWith, wordsFor } from './testing/encrypt.ts';
import { h, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts'; import { h, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts';
import { sealedControlLength } from './lengths.ts'; import { sealedControlLength } from './lengths.ts';
import { selfCheckInner, selfCheckPayloadHeader } from './writer.ts'; import { selfCheckInner, selfCheckPayloadHeader } from './writer.ts';
@ -67,7 +67,6 @@ const options = (extra: Partial<EncryptOptions> = {}): EncryptOptions => ({
unlockAt: roundAt(1000), unlockAt: roundAt(1000),
policy: TIME_ONLY, policy: TIME_ONLY,
now: () => GENESIS, now: () => GENESIS,
testVectors: true,
...extra, ...extra,
}); });
const opening = (r: Release, extra: Partial<OpenOptions> = {}): OpenOptions => ({ source: suppliedRelease(r), now: () => roundAt(r.round), ...extra }); const opening = (r: Release, extra: Partial<OpenOptions> = {}): OpenOptions => ({ source: suppliedRelease(r), now: () => roundAt(r.round), ...extra });
@ -112,7 +111,7 @@ async function sealedPlaintext(dkc: Uint8Array, r: Release): Promise<Uint8Array>
describe('encrypt', () => { describe('encrypt', () => {
it('writes a time_only capsule of format 2 that open opens to its content', async () => { it('writes a time_only capsule of format 2 that open opens to its content', async () => {
const text = te.encode('una carta para el futuro'); const text = te.encode('una carta para el futuro');
const res = await encrypt(text, options()); const res = await encryptVectors(text, options());
expect([res.format, res.length, res.padding, res.paddedLength]).toEqual([2, text.length, REFORZADO, 256]); expect([res.format, res.length, res.padding, res.paddedLength]).toEqual([2, text.length, REFORZADO, 256]);
expect([res.dateKey.round, res.unlockAt, res.portableKey, res.size]).toEqual([1000, roundAt(1000), undefined, res.dkc!.length]); expect([res.dateKey.round, res.unlockAt, res.portableKey, res.size]).toEqual([1000, roundAt(1000), undefined, res.dkc!.length]);
expect(res.dkc![4]).toBe(FORMAT_2); expect(res.dkc![4]).toBe(FORMAT_2);
@ -132,7 +131,7 @@ describe('encrypt', () => {
[16, false], [16, false],
] as const) { ] as const) {
const ids = Array.from({ length: recipients }, newX25519Identity); const ids = Array.from({ length: recipients }, newX25519Identity);
const res = await encrypt(body, options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), recipients: ids.map(x25519PublicKey), newPortableKey: portable })); const res = await encryptVectors(body, options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), recipients: ids.map(x25519PublicKey), newPortableKey: portable }));
const label = `${recipients} recipients${portable ? ' and a portable key' : ''}`; const label = `${recipients} recipients${portable ? ' and a portable key' : ''}`;
expect(res.portableKey === undefined, label).toBe(!portable); expect(res.portableKey === undefined, label).toBe(!portable);
const dkk = portable ? encodeAccessKey(res.portableKey!) : undefined; const dkk = portable ? encodeAccessKey(res.portableKey!) : undefined;
@ -157,7 +156,7 @@ describe('encrypt', () => {
for (const n of [0, 1, 255, 256, 257, 8192, 8193, 65535, 65536, 65537, 78000, 320000, 5000000]) { for (const n of [0, 1, 255, 256, 257, 8192, 8193, 65535, 65536, 65537, 78000, 320000, 5000000]) {
for (const code of [BLOQUE256, REFORZADO] as const) { for (const code of [BLOQUE256, REFORZADO] as const) {
const body = content(n, n + code); const body = content(n, n + code);
const res = await encrypt(body, options({ padding: code })); const res = await encryptVectors(body, options({ padding: code }));
const p = paddedLength(n, code); const p = paddedLength(n, code);
expect([res.length, res.padding, res.paddedLength], `${n}, ${code}`).toEqual([n, code, p]); expect([res.length, res.padding, res.paddedLength], `${n}, ${code}`).toEqual([n, code, p]);
expect(split(res.dkc!).payload.length, `${n}, ${code}`).toBe(payloadAgeLength(p)); expect(split(res.dkc!).payload.length, `${n}, ${code}`).toBe(payloadAgeLength(p));
@ -176,7 +175,7 @@ describe('encrypt', () => {
[{ seconds: roundAt(1000).seconds, nanos: 1 }, 1001, R1001], [{ seconds: roundAt(1000).seconds, nanos: 1 }, 1001, R1001],
[{ seconds: roundAt(2000).seconds - 1, nanos: 999_999_999 }, 2000, R2000], [{ seconds: roundAt(2000).seconds - 1, nanos: 999_999_999 }, 2000, R2000],
] as const) { ] as const) {
const res = await encrypt(te.encode('x'), options({ unlockAt: requested })); const res = await encryptVectors(te.encode('x'), options({ unlockAt: requested }));
expect([res.dateKey.round, res.unlockAt]).toEqual([round, roundAt(round)]); expect([res.dateKey.round, res.unlockAt]).toEqual([round, roundAt(round)]);
expect((await open(res.dkc!, opening(r))).plaintext).toEqual(te.encode('x')); expect((await open(res.dkc!, opening(r))).plaintext).toEqual(te.encode('x'));
} }
@ -362,7 +361,7 @@ describe('invalid options', () => {
it.each(cases)('%s: fails with the text and code of Go, writes nothing and aborts the output', async (_label, src, extra, type, message, code) => { it.each(cases)('%s: fails with the text and code of Go, writes nothing and aborts the output', async (_label, src, extra, type, message, code) => {
const out = recorder(); const out = recorder();
const err = await failure(encrypt(src as Uint8Array, { ...options(), output: out.stream, ...extra } as EncryptOptions)); const err = await failure(encryptVectors(src as Uint8Array, { ...options(), output: out.stream, ...extra } as EncryptOptions));
expect(err.name === 'Error' && errorCode(err) !== '' ? 'DateKeysError' : err.name).toBe(type); expect(err.name === 'Error' && errorCode(err) !== '' ? 'DateKeysError' : err.name).toBe(type);
if (typeof message === 'string') expect(err.message).toBe(code === '' ? message : `${message}: ${code}`); if (typeof message === 'string') expect(err.message).toBe(code === '' ? message : `${message}: ${code}`);
else expect(err.message).toMatch(message); else expect(err.message).toMatch(message);
@ -372,39 +371,59 @@ describe('invalid options', () => {
}); });
it('keeps in memory a capsule of at most MAX_MEMORY_DKC bytes, and needs an output above', async () => { it('keeps in memory a capsule of at most MAX_MEMORY_DKC bytes, and needs an output above', async () => {
const err = await failure(encrypt(stream(), { ...options(), length: MAX_MEMORY_DKC })); const err = await failure(encryptVectors(stream(), { ...options(), length: MAX_MEMORY_DKC }));
expect([err.name, err.message]).toEqual([ expect([err.name, err.message]).toEqual([
'TypeError', 'TypeError',
`encrypt: a capsule of ${16 + 121 + 458 + payloadAgeLength(paddedLength(MAX_MEMORY_DKC, REFORZADO))} bytes needs EncryptOptions.output; in memory the limit is ${MAX_MEMORY_DKC}`, `encrypt: a capsule of ${16 + 121 + 458 + payloadAgeLength(paddedLength(MAX_MEMORY_DKC, REFORZADO))} bytes needs EncryptOptions.output; in memory the limit is ${MAX_MEMORY_DKC}`,
]); ]);
}); });
// Spec §62.1 rule 1, §70: only a generator of test vectors writes format // Spec §62.1 rule 1, §70: only a generator of test vectors writes format 2, which has no head, no security area and
// 2, and format 2 has no head; the texts of capsule.Encrypt. // no public note; the texts of capsule.Encrypt, in its order: the generator, the head, the length, the area and the
it('writes format 2 only for a generator of test vectors, and with no head', async () => { // note, and then the options of every writer.
it('writes format 2 only for a generator of test vectors, and with no head, no area and no note', async () => {
const generator = 'capsule: Encrypt writes format 2, which only a generator of test vectors may write (spec §62.1 rule 1): EncryptFiles writes format 3'; const generator = 'capsule: Encrypt writes format 2, which only a generator of test vectors may write (spec §62.1 rule 1): EncryptFiles writes format 3';
const head = 'capsule: format 2 has no head: Comment, Author and the head extensions are for EncryptFiles'; const head = 'capsule: format 2 has no head: Comment, Author and the head extensions are for EncryptFiles';
const { testVectors: _, ...plain } = options(); const area = 'capsule: format 2 has no security area or public note: AuthorKey, CMSSigner, Sealer, LargeArea and PublicNote are for EncryptFiles';
for (const [label, opts, text] of [ // The encrypt of encrypt.ts is no generator: no option asks for format 2, and a testVectors of the caller is nothing.
['no testVectors', plain, generator], for (const [label, opts] of [
['testVectors false', { ...plain, testVectors: false }, generator], ['the options of the tests', options()],
['a comment', options({ comment: 'x' }), head], ['testVectors', { ...options(), testVectors: true } as EncryptOptions],
['a declared author', options({ author: 'x' }), head], ['a comment and a public note', options({ comment: 'x', publicNote: 'Hola' })],
['critical head extensions', options({ headCritical: [] }), head],
['noncritical head extensions', options({ headNoncritical: [] }), head],
] as const) { ] as const) {
const out = recorder(); const out = recorder();
const err = await failure(encrypt(content(10), { ...opts, output: out.stream })); const err = await failure(encrypt(content(10), { ...opts, output: out.stream }));
expect([err.message, out.chunks.length, out.state.aborted], label).toEqual([generator, 0, err]);
}
const noProfile = { ...options({ publicNote: 'Hola' }), profile: undefined } as unknown as EncryptOptions;
for (const [label, opts, vectors, text] of [
['a comment', options({ comment: 'x' }), {}, head],
['a declared author', options({ author: 'x' }), {}, head],
['critical head extensions', options({ headCritical: [] }), {}, head],
['noncritical head extensions', options({ headNoncritical: [] }), {}, head],
['a comment and a public note', options({ comment: 'x', publicNote: 'Hola' }), {}, head],
['a public note', options({ publicNote: 'Hola' }), {}, area],
['a public note that breaks its rules', options({ publicNote: 'a\tb' }), {}, area],
['a public note and no profile', noProfile, {}, area],
['a security area of 512 bytes', options(), { areaLen: 512 }, area],
['a security area of 32 KiB', options(), { areaLen: 32768 }, area],
] as const) {
const out = recorder();
const err = await failure(encryptVectors(content(10), { ...opts, output: out.stream }, vectors));
expect([err.message, out.chunks.length, out.state.aborted], label).toEqual([text, 0, err]); expect([err.message, out.chunks.length, out.state.aborted], label).toEqual([text, 0, err]);
} }
// An empty comment or author is none. // The length of the source comes before the note, as Length < 0 does in Go.
expect((await encrypt(content(10), options({ comment: '', author: '' }))).format).toBe(FORMAT_2); expect((await failure(encryptVectors(content(10), options({ length: -1, publicNote: 'Hola' })))).message).toBe(
'encrypt: EncryptOptions.length -1 is not a non-negative safe integer',
);
// An empty comment, author or note is none.
expect((await encryptVectors(content(10), options({ comment: '', author: '', publicNote: '' }))).format).toBe(FORMAT_2);
}); });
it('rejects options that are not an object, and an output that is not a WritableStream', async () => { it('rejects options that are not an object, and an output that is not a WritableStream', async () => {
expect((await failure(encrypt(te.encode('x'), undefined as unknown as EncryptOptions))).message).toBe('encrypt: options are required'); expect((await failure(encryptVectors(te.encode('x'), undefined as unknown as EncryptOptions))).message).toBe('encrypt: options are required');
expect((await failure(encrypt(te.encode('x'), { ...options(), output: {} as WritableStream<Uint8Array> }))).message).toMatch(/output is not a WritableStream/); expect((await failure(encryptVectors(te.encode('x'), { ...options(), output: {} as WritableStream<Uint8Array> }))).message).toMatch(/output is not a WritableStream/);
expect((await failure(encrypt(te.encode('x'), { ...options(), progress: 3 as unknown as () => void }))).message).toMatch(/progress is not a function/); expect((await failure(encryptVectors(te.encode('x'), { ...options(), progress: 3 as unknown as () => void }))).message).toMatch(/progress is not a function/);
}); });
it('rejects a random draw of the wrong length', async () => { it('rejects a random draw of the wrong length', async () => {
@ -422,8 +441,8 @@ describe('invalid options', () => {
describe('properties of the Go reference', () => { describe('properties of the Go reference', () => {
it('never reuses a portable key, a capsule_id, an I_PAYLOAD or a dummy', async () => { it('never reuses a portable key, a capsule_id, an I_PAYLOAD or a dummy', async () => {
const one = x25519PublicKey(newX25519Identity()); const one = x25519PublicKey(newX25519Identity());
const a = await encrypt(te.encode('x'), options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), recipients: [one], newPortableKey: true })); const a = await encryptVectors(te.encode('x'), options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), recipients: [one], newPortableKey: true }));
const b = await encrypt(te.encode('x'), options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), recipients: [one], newPortableKey: true })); const b = await encryptVectors(te.encode('x'), options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), recipients: [one], newPortableKey: true }));
expect(hx(a.portableKey!.material)).not.toBe(hx(b.portableKey!.material)); expect(hx(a.portableKey!.material)).not.toBe(hx(b.portableKey!.material));
expect(hx(a.portableKey!.credentialId)).not.toBe(hx(b.portableKey!.credentialId)); expect(hx(a.portableKey!.credentialId)).not.toBe(hx(b.portableKey!.credentialId));
expect(hx(a.capsuleId)).not.toBe(hx(b.capsuleId)); expect(hx(a.capsuleId)).not.toBe(hx(b.capsuleId));
@ -439,12 +458,12 @@ describe('properties of the Go reference', () => {
expect(shares.size).toBe(32); expect(shares.size).toBe(32);
expect(payloadIds.size).toBe(2); expect(payloadIds.size).toBe(2);
// Without a portable key, no key comes back. // Without a portable key, no key comes back.
expect((await encrypt(te.encode('x'), options({ policy: TIME_AND_KEY, recipients: [one] }))).portableKey).toBeUndefined(); expect((await encryptVectors(te.encode('x'), options({ policy: TIME_AND_KEY, recipients: [one] }))).portableKey).toBeUndefined();
}); });
it('opens a .dkk only on its capsule, before any request', async () => { it('opens a .dkk only on its capsule, before any request', async () => {
const a = await encrypt(te.encode('a'), options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), newPortableKey: true })); const a = await encryptVectors(te.encode('a'), options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), newPortableKey: true }));
const b = await encrypt(te.encode('b'), options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), newPortableKey: true })); const b = await encryptVectors(te.encode('b'), options({ policy: TIME_AND_KEY, unlockAt: roundAt(1001), newPortableKey: true }));
let calls = 0; let calls = 0;
const source = { fetch: () => (calls++, Promise.resolve(R1001)) }; const source = { fetch: () => (calls++, Promise.resolve(R1001)) };
const withDkk = await open(b.dkc!, { source, now: () => roundAt(1001), accessKeyFile: encodeAccessKey(a.portableKey!) }); const withDkk = await open(b.dkc!, { source, now: () => roundAt(1001), accessKeyFile: encodeAccessKey(a.portableKey!) });
@ -456,7 +475,7 @@ describe('properties of the Go reference', () => {
it('seals for the first round at or after now + 1 h, which nobody can open yet', async () => { it('seals for the first round at or after now + 1 h, which nobody can open yet', async () => {
const now = { seconds: roundAt(5000).seconds + 1, nanos: 7 }; const now = { seconds: roundAt(5000).seconds + 1, nanos: 7 };
const requested = { seconds: now.seconds + 3600, nanos: now.nanos }; const requested = { seconds: now.seconds + 3600, nanos: now.nanos };
const res = await encrypt(te.encode('pronto'), options({ unlockAt: requested, now: () => now })); const res = await encryptVectors(te.encode('pronto'), options({ unlockAt: requested, now: () => now }));
const effective = res.unlockAt; const effective = res.unlockAt;
const period = quicknet().period; const period = quicknet().period;
expect(effective.seconds * 1e9 + effective.nanos).toBeGreaterThanOrEqual(requested.seconds * 1e9 + requested.nanos); expect(effective.seconds * 1e9 + effective.nanos).toBeGreaterThanOrEqual(requested.seconds * 1e9 + requested.nanos);
@ -472,7 +491,7 @@ describe('properties of the Go reference', () => {
for (const n of [0, 1, 300, 70000]) { for (const n of [0, 1, 300, 70000]) {
for (const code of [BLOQUE256, REFORZADO] as const) { for (const code of [BLOQUE256, REFORZADO] as const) {
for (const policy of [TIME_ONLY, TIME_AND_KEY]) { for (const policy of [TIME_ONLY, TIME_AND_KEY]) {
const res = await encrypt(content(n), options({ padding: code, policy, ...(policy === TIME_AND_KEY ? { newPortableKey: true } : {}) })); const res = await encryptVectors(content(n), options({ padding: code, policy, ...(policy === TIME_AND_KEY ? { newPortableKey: true } : {}) }));
const sealed = split(res.dkc!).sealed.length; const sealed = split(res.dkc!).sealed.length;
expect(sealed).toBe(sealedControlLength(policy, 103, 1000)); expect(sealed).toBe(sealedControlLength(policy, 103, 1000));
lengths.add(sealed); lengths.add(sealed);

@ -1,10 +1,11 @@
// The writers: encryptFiles writes a .dkc of capsule format 3, and encrypt // The writers: encryptFiles writes a .dkc of capsule format 3, with, when
// one of format 2, each with, when asked, a portable .dkk (spec §61, §62, // asked, a portable .dkk (spec §61, §62, §62.1), as capsule.EncryptFiles of
// §62.1), as capsule.EncryptFiles and capsule.Encrypt of the Go reference at // the Go reference at spec-v0.11; encrypt, as capsule.Encrypt, writes format
// spec-v0.10. Their random values all come from crypto.getRandomValues // 2 only for a generator of test vectors, which these options cannot ask
// (§62.1 rule 5); the core of writer.ts, which takes them from its caller, is // for. Their random values all come from crypto.getRandomValues (§62.1 rule
// imported only here and by the tests. Loaded on demand: index.ts does not // 5); the core of writer.ts, which takes them from its caller, is imported
// re-export it. // only here and by the helpers of the tests. Loaded on demand: index.ts does
// not re-export it.
import { cryptoWords } from './random.ts'; import { cryptoWords } from './random.ts';
import { import {
@ -35,30 +36,25 @@ export { MAX_MEMORY_DKC };
* has changed makes it fail (§62.1 rule 18). The files go in the byte order * has changed makes it fail (§62.1 rule 18). The files go in the byte order
* of their paths, whatever the order given (R8), without the empty folders, * of their paths, whatever the order given (R8), without the empty folders,
* which a path cannot name. The security area is the empty one of this * which a path cannot name. The security area is the empty one of this
* version, in an area of 512 bytes (rule 13). * library, which does not sign, in an area of 32 KiB, whatever the capsule
* holds (rule 13); `opts.publicNote` goes in PUBLIC_HEADER (§24.1).
* *
* Without `opts.output` the .dkc is returned in memory, up to * Without `opts.output` the .dkc is returned in memory, up to
* MAX_MEMORY_DKC; with it, the .dkc is streamed into the output, closed only * MAX_MEMORY_DKC; with it, the .dkc is streamed into the output, closed only
* once the capsule is complete and checked and aborted on any failure. The * once the capsule is complete and checked and aborted on any failure. The
* checks, codes and texts are those of capsule.EncryptFiles; `opts.length` * checks, codes and texts are those of capsule.EncryptFiles, in its order;
* is for encrypt, and L is the length of BODY. * `opts.length` is for encrypt, and L is the length of BODY.
*/ */
export function encryptFiles(files: readonly FileSource[], opts: EncryptOptions): Promise<Encrypted> { export function encryptFiles(files: readonly FileSource[], opts: EncryptOptions): Promise<Encrypted> {
return writeFiles(files, opts, cryptoDraws()); return writeFiles(files, opts, cryptoDraws());
} }
/** /**
* Writes a .dkc of format 2 for the content of `src`, which must be exactly * capsule.Encrypt, which writes a .dkc of format 2 only for a generator of
* L bytes (§62.1 rule 6): the size of a Uint8Array or a Blob, or * test vectors (§62.1 rule 1, §70): no option of a caller asks for it, so it
* `opts.length` for a stream. It needs no network: the round is resolved * fails with the text of Go, and its output, if any, is aborted. Capsules
* locally, and tlock uses only the pinned public key. * are written with encryptFiles. The tests write format 2 with the helpers of
* * testing/encrypt.ts, which pass the core what only they may ask.
* Without `opts.output` the .dkc is returned in memory, up to
* MAX_MEMORY_DKC. With it, the .dkc is streamed into the output, which is
* closed only once the capsule is complete and checked, and aborted on any
* failure; the content is read in pieces of 64 KiB and never held whole.
* The checks, codes and texts are those of capsule.Encrypt; the errors of
* the source and of the output are rethrown as they are.
*/ */
export function encrypt(src: EncryptSource, opts: EncryptOptions): Promise<Encrypted> { export function encrypt(src: EncryptSource, opts: EncryptOptions): Promise<Encrypted> {
return writeCapsule(src, opts, cryptoDraws()); return writeCapsule(src, opts, cryptoDraws());

@ -14,7 +14,6 @@ import { marshalAccessKeyBody } from './accesskey.ts';
import { concatBytes, fromHex, sha256, toHex } from './bytes.ts'; import { concatBytes, fromHex, sha256, toHex } from './bytes.ts';
import { encodeControl } from './control.ts'; import { encodeControl } from './control.ts';
import type { Instant } from './datekey.ts'; import type { Instant } from './datekey.ts';
import { encrypt } from './encrypt.ts';
import { errorCode } from './errors.ts'; import { errorCode } from './errors.ts';
import { ExtensionSet } from './extension.ts'; import { ExtensionSet } from './extension.ts';
import { FORMAT_2, FORMAT_3 } from './framing.ts'; import { FORMAT_2, FORMAT_3 } from './framing.ts';
@ -25,6 +24,7 @@ import { open, type OpenOptions } from './open.ts';
import { checkX25519Recipient, parseX25519Recipient } from './recipient.ts'; import { checkX25519Recipient, parseX25519Recipient } from './recipient.ts';
import { suppliedRelease } from './release.ts'; import { suppliedRelease } from './release.ts';
import { MemorySink } from './sink.ts'; import { MemorySink } from './sink.ts';
import { encryptVectors } from './testing/encrypt.ts';
import { encoderCases, errorCaseInput, errorCases, recipientStrings, type ErrorCase } from './testing/interop.ts'; import { encoderCases, errorCaseInput, errorCases, recipientStrings, type ErrorCase } from './testing/interop.ts';
// A file of a format 3 capsule, as the Go script records it. // A file of a format 3 capsule, as the Go script records it.
@ -249,7 +249,7 @@ describe('the invalid options against capsule.Encrypt', () => {
for (const c of V.errors) { for (const c of V.errors) {
it(`rejects ${c.name} with the text of Go`, async () => { it(`rejects ${c.name} with the text of Go`, async () => {
const { src, opts } = errorCaseInput(c); const { src, opts } = errorCaseInput(c);
const err = await failure(encrypt(src, opts)); const err = await failure(encryptVectors(src, opts));
expect([err.message, c.ts]).toEqual([c.go, c.go]); expect([err.message, c.ts]).toEqual([c.go, c.go]);
}); });
} }

@ -6,12 +6,12 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { decodeControl } from './control.ts'; import { decodeControl } from './control.ts';
import { parseRFC3339 } from './datekey.ts'; import { parseRFC3339 } from './datekey.ts';
import { encrypt } from './encrypt.ts';
import { FORMAT_2 } from './framing.ts'; import { FORMAT_2 } from './framing.ts';
import { decodeHeader, TIME_AND_KEY, TIME_ONLY } from './header.ts'; import { decodeHeader, TIME_AND_KEY, TIME_ONLY } from './header.ts';
import { capsuleLength, sealedControlLength } from './lengths.ts'; import { capsuleLength, sealedControlLength } from './lengths.ts';
import { BLOQUE256, MAX_PAYLOAD_LENGTH, REFORZADO } from './padding.ts'; import { BLOQUE256, MAX_PAYLOAD_LENGTH, REFORZADO } from './padding.ts';
import { QUICKNET_ID, quicknet } from './profile.ts'; import { QUICKNET_ID, quicknet } from './profile.ts';
import { encryptVectors } from './testing/encrypt.ts';
import { h, listTestdata, readBytes, readJSON } from './testing/testdata.ts'; import { h, listTestdata, readBytes, readJSON } from './testing/testdata.ts';
import { newX25519Identity, x25519PublicKey } from './x25519.ts'; import { newX25519Identity, x25519PublicKey } from './x25519.ts';
@ -68,12 +68,11 @@ describe('capsuleLength', () => {
]; ];
for (const c of cases) { for (const c of cases) {
const keyed = c.policy === TIME_AND_KEY; const keyed = c.policy === TIME_AND_KEY;
const res = await encrypt(new Uint8Array(c.length), { const res = await encryptVectors(new Uint8Array(c.length), {
profile: quicknet(), profile: quicknet(),
unlockAt: roundAt(c.round), unlockAt: roundAt(c.round),
policy: c.policy, policy: c.policy,
now: () => ({ seconds: GENESIS.seconds - 3600, nanos: 0 }), now: () => ({ seconds: GENESIS.seconds - 3600, nanos: 0 }),
testVectors: true,
...(keyed ? { recipients: [x25519PublicKey(newX25519Identity())], newPortableKey: true } : {}), ...(keyed ? { recipients: [x25519PublicKey(newX25519Identity())], newPortableKey: true } : {}),
}); });
expect(res.dkc!.length).toBe(res.size); expect(res.dkc!.length).toBe(res.size);
@ -89,6 +88,19 @@ describe('capsuleLength', () => {
expect(capsuleLength({ ...at, length: 70_000, padding: REFORZADO }) - capsuleLength({ ...at, length: 70_000, padding: BLOQUE256 })).toBe(1536); expect(capsuleLength({ ...at, length: 70_000, padding: REFORZADO }) - capsuleLength({ ...at, length: 70_000, padding: BLOQUE256 })).toBe(1536);
}); });
// Spec v0.11 §24.1: encryptFiles adds the public note to the noncritical extensions of PUBLIC_HEADER, as the
// extension datekeys.note of version 1; an empty note is none. encrypt.files.test.ts compares the size with what
// encryptFiles writes.
it('counts the public note as the extension that the writer adds, and an empty note as none', () => {
const at = { profileId: QUICKNET_ID, round: 1000, policy: TIME_ONLY, length: 100 };
expect(capsuleLength({ ...at, publicNote: '' })).toBe(capsuleLength(at));
const note = 'Cartas del viaje a Lisboa';
const extension = { id: 'datekeys.note', version: 1, data: new TextEncoder().encode(note) };
expect(capsuleLength({ ...at, publicNote: note })).toBe(capsuleLength({ ...at, noncritical: [extension] }));
const other = { id: 'z.example', version: 1, data: undefined };
expect(capsuleLength({ ...at, noncritical: [other], publicNote: note })).toBe(capsuleLength({ ...at, noncritical: [extension, other] }));
});
it('throws, as the encoders do, for an invalid DateKey, policy or L', () => { it('throws, as the encoders do, for an invalid DateKey, policy or L', () => {
const at = { profileId: QUICKNET_ID, round: 1000, policy: TIME_ONLY, length: 1 }; const at = { profileId: QUICKNET_ID, round: 1000, policy: TIME_ONLY, length: 1 };
expect(() => capsuleLength({ ...at, round: 0 })).toThrow('capsule: invalid DateKey'); expect(() => capsuleLength({ ...at, round: 0 })).toThrow('capsule: invalid DateKey');

@ -41,10 +41,20 @@ export interface CapsuleLengthInput {
readonly padding?: Padding; readonly padding?: Padding;
readonly critical?: readonly Extension[]; readonly critical?: readonly Extension[];
readonly noncritical?: readonly Extension[]; readonly noncritical?: readonly Extension[];
/**
* The public note that encryptFiles adds to the noncritical extensions of
* PUBLIC_HEADER (spec v0.11, §24.1); absent or '' for none. Only its length
* counts here: the writer checks its rules.
*/
readonly publicNote?: string;
readonly controlCritical?: readonly Extension[]; readonly controlCritical?: readonly Extension[];
readonly controlNoncritical?: readonly Extension[]; readonly controlNoncritical?: readonly Extension[];
} }
// The extension_id of the public note (NOTE_ID of note.ts, which this module
// does not import: its rules of text bring the Unicode tables).
const NOTE_ID = 'datekeys.note';
/** /**
* The exact size of the .dkc that encrypt, or encryptFiles with L from * The exact size of the .dkc that encrypt, or encryptFiles with L from
* bodyLength, writes for these inputs: PRELUDE, * bodyLength, writes for these inputs: PRELUDE,
@ -55,12 +65,14 @@ export interface CapsuleLengthInput {
*/ */
export function capsuleLength(input: CapsuleLengthInput): number { export function capsuleLength(input: CapsuleLengthInput): number {
const padding = input.padding ?? REFORZADO; const padding = input.padding ?? REFORZADO;
const noncritical = [...(input.noncritical ?? [])];
if ((input.publicNote ?? '') !== '') noncritical.push({ id: NOTE_ID, version: 1, data: utf8Bytes(input.publicNote!) });
const header = encodeHeader({ const header = encodeHeader({
capsuleId: new Uint8Array(CAPSULE_ID_SIZE), capsuleId: new Uint8Array(CAPSULE_ID_SIZE),
dateKey: { profileId: input.profileId, round: input.round }, dateKey: { profileId: input.profileId, round: input.round },
policy: input.policy, policy: input.policy,
critical: input.critical ?? [], critical: input.critical ?? [],
noncritical: input.noncritical ?? [], noncritical,
}); });
const control = encodeControl( const control = encodeControl(
{ {

@ -1,11 +1,15 @@
// Tests only: the writer with its random values fixed by name (plan of phase // Tests only: the writer as a generator of test vectors (spec §62.1 rules 1
// 3, decision 4), to reproduce the deterministic sections of the fixtures of // and 13, §70), the only one that writes format 2 or another security area
// the Go reference and to reach the checks of the writer. A value that is // than the 32 KiB of AREA_LEN, which the options of encrypt.ts cannot ask
// not given is drawn from crypto.getRandomValues, as encrypt does. The draws // for; and with its random values fixed by name (plan of phase 3, decision
// hand the writer copies, and the writer wipes them. // 4), to reproduce the deterministic sections of the fixtures of the Go
// reference and to reach the checks of the writer. A value that is not given
// is drawn from crypto.getRandomValues, as encrypt does. The draws hand the
// writer copies, and the writer wipes them. index.ts does not re-export this
// module, and a guard keeps every module of testing/ out of the pages.
import { cryptoWords, type RandomWords } from '../random.ts'; import { cryptoWords, type RandomWords } from '../random.ts';
import { type Draws, type EncryptOptions, type Encrypted, type EncryptSource, type FileSource, writeCapsule, writeFiles } from '../writer.ts'; import { type Draws, type EncryptOptions, type Encrypted, type EncryptSource, type FileSource, type TestVectors, writeCapsule, writeFiles } from '../writer.ts';
import { newX25519Identity } from '../x25519.ts'; import { newX25519Identity } from '../x25519.ts';
/** The random values to fix; any other is drawn at random. */ /** The random values to fix; any other is drawn at random. */
@ -58,16 +62,27 @@ export function fixedDraws(f: FixedDraws): Draws {
} }
/** /**
* encrypt with some of its random values fixed, as a generator of test * encrypt as a generator of test vectors, the only writer of format 2, with
* vectors, which may write format 2: testVectors is set unless given. * every random value drawn as encrypt draws it: what the tests of format 2
* and scripts/capsule-ts-samples.mjs write with. `vectors` reaches the
* writer as it is, so that a test can ask for what format 2 refuses.
*/ */
export function encryptWith(src: EncryptSource, opts: EncryptOptions, f: FixedDraws): Promise<Encrypted> { export function encryptVectors(src: EncryptSource, opts: EncryptOptions, vectors: TestVectors = {}): Promise<Encrypted> {
return writeCapsule(src, { testVectors: true, ...opts }, fixedDraws(f)); return writeCapsule(src, opts, fixedDraws({}), vectors);
} }
/** encryptFiles with some of its random values fixed. */ /** encryptVectors with some of its random values fixed. */
export function encryptFilesWith(files: readonly FileSource[], opts: EncryptOptions, f: FixedDraws): Promise<Encrypted> { export function encryptWith(src: EncryptSource, opts: EncryptOptions, f: FixedDraws, vectors: TestVectors = {}): Promise<Encrypted> {
return writeFiles(files, opts, fixedDraws(f)); return writeCapsule(src, opts, fixedDraws(f), vectors);
}
/**
* encryptFiles with some of its random values fixed and, given `vectors`, as
* a generator of test vectors: with another security area, such as the 512
* bytes of the fixtures of format 3 that the writers of v0.10 wrote.
*/
export function encryptFilesWith(files: readonly FileSource[], opts: EncryptOptions, f: FixedDraws, vectors?: TestVectors): Promise<Encrypted> {
return writeFiles(files, opts, fixedDraws(f), vectors);
} }
/** /**

@ -2,8 +2,8 @@
// reference and interop.test.ts checks again (plan of phase 3, decision 11 // reference and interop.test.ts checks again (plan of phase 3, decision 11
// and section 8, point 9): fixed identities, the inputs of the encoder // and section 8, point 9): fixed identities, the inputs of the encoder
// differential, drawn from a seed, the corpus of recipient strings and keys, // differential, drawn from a seed, the corpus of recipient strings and keys,
// and the table of invalid options of encrypt in a form the Go script reads. // and the table of invalid options of encrypt as a generator of test
// Everything here is deterministic. // vectors, in a form the Go script reads. Everything here is deterministic.
import type { AccessKey } from '../accesskey.ts'; import type { AccessKey } from '../accesskey.ts';
import { ACCESS_TYPE_X25519 } from '../accesskey.ts'; import { ACCESS_TYPE_X25519 } from '../accesskey.ts';
@ -269,7 +269,6 @@ export function errorCaseInput(c: ErrorCase): { src: EncryptSource; opts: Encryp
noncritical: ext(c.noncritical), noncritical: ext(c.noncritical),
controlCritical: ext(c.control_critical), controlCritical: ext(c.control_critical),
controlNoncritical: ext(c.control_noncritical), controlNoncritical: ext(c.control_noncritical),
testVectors: true,
}, },
}; };
} }

@ -1,12 +1,14 @@
// The core of the writer of phase 3: a .dkc of capsule format 2 and, when // The core of the writers: a .dkc of capsule format 3, or of format 2 for a
// asked, a portable .dkk (spec §61, §62, §62.1), in the order and with the // generator of test vectors, and, when asked, a portable .dkk (spec §61, §62,
// texts of capsule.Encrypt of the Go reference at spec-v0.10, split as it // §62.1), in the order and with the texts of capsule.EncryptFiles and
// is: newSealer checks the options that do not depend on the content, and // capsule.Encrypt of the Go reference at spec-v0.11, split as they are:
// seal writes a capsule of a format around a content given in pieces. Its // newSealer checks the options that do not depend on the content, and seal
// random values come from the caller (plan of phase 3, decision 4): // writes a capsule of a format around a content given in pieces. Its random
// encrypt.ts passes those of crypto.getRandomValues, and only the tests of // values come from the caller (plan of phase 3, decision 4): encrypt.ts
// testing/ fix them. Internal: only encrypt.ts and testing/ import it, which // passes those of crypto.getRandomValues, and only the helpers of testing/
// a guard checks. // fix them and ask for what only a generator of test vectors writes
// (TestVectors). Internal: only encrypt.ts and testing/ import it, which a
// guard checks.
// //
// Nothing is written before every check that can precede the content has // Nothing is written before every check that can precede the content has
// passed, the header of PAYLOAD_AGE included. The output is closed only once // passed, the header of PAYLOAD_AGE included. The output is closed only once
@ -24,7 +26,7 @@ import { compareBytes, copyBytes, equalBytes, goQuote, toHex, utf8Bytes, utf8Len
import { decodeControl, encodeControl } from './control.ts'; import { decodeControl, encodeControl } from './control.ts';
import { compareInstants, type DateKey, formatRFC3339Nano, type Instant, isInstant, resolveDateKey, roundTime } from './datekey.ts'; import { compareInstants, type DateKey, formatRFC3339Nano, type Instant, isInstant, resolveDateKey, roundTime } from './datekey.ts';
import { sha256Hasher } from './digest.ts'; import { sha256Hasher } from './digest.ts';
import { DateKeysError } from './errors.ts'; import { DateKeysError, withContext } from './errors.ts';
import type { Extension } from './extension.ts'; import type { Extension } from './extension.ts';
import { DKC_PRELUDE_SIZE, FORMAT_2, FORMAT_3, headerBinding, MAX_SEALED_CONTROL_LEN, preludeBytes } from './framing.ts'; import { DKC_PRELUDE_SIZE, FORMAT_2, FORMAT_3, headerBinding, MAX_SEALED_CONTROL_LEN, preludeBytes } from './framing.ts';
import { checkHeadEnd, decodeWrittenHead, encodeHead, type Head, type HeadFile, MAX_FILES, SALT_SIZE } from './head.ts'; import { checkHeadEnd, decodeWrittenHead, encodeHead, type Head, type HeadFile, MAX_FILES, SALT_SIZE } from './head.ts';
@ -114,18 +116,11 @@ export interface EncryptOptions {
* The public note of the capsule (spec v0.11, §24.1): the extension * The public note of the capsule (spec v0.11, §24.1): the extension
* datekeys.note in PUBLIC_HEADER, a line of text that anyone who holds the * datekeys.note in PUBLIC_HEADER, a line of text that anyone who holds the
* .dkc reads before the date and that nobody can check. It must pass * .dkc reads before the date and that nobody can check. It must pass
* checkNote. Absent or '' for none. The writer SHOULD warn the person that * checkNote. Absent or '' for none; encryptFiles only, since format 2 has
* it is public and that, with the date, it can identify someone. * no note. The writer SHOULD warn the person that it is public and that,
* with the date, it can identify someone.
*/ */
readonly publicNote?: string; readonly publicNote?: string;
/**
* Lets encrypt write format 2, and encryptFiles another area than the 32 KiB
* of AREA_LEN, which only a generator of test vectors may write (spec §62.1
* rules 1 and 13, §70).
*/
readonly testVectors?: boolean;
/** The size of the security area, with testVectors only: 512 reproduces the fixtures of the writers of v0.10. */
readonly areaLen?: number;
/** The clock. Required, and called once. */ /** The clock. Required, and called once. */
readonly now: () => Instant; readonly now: () => Instant;
/** /**
@ -180,25 +175,47 @@ export interface Draws {
readonly salt: () => Uint8Array; readonly salt: () => Uint8Array;
} }
/**
* What only a generator of test vectors asks of the writers, as
* capsule.EncryptOptions.TestVectors (spec §62.1 rules 1 and 13, §70): format
* 2, and another security area than the 32 KiB of AREA_LEN. Only the helpers
* of testing/ pass it, and encrypt.ts never does: no caller of the library
* writes format 2, or an area that would tell that the capsule has no
* signature (spec §55.2).
*/
export interface TestVectors {
/**
* The size of the security area of writeFiles, a multiple of AREA_UNIT up
* to MAX_AREA_LEN: 512 reproduces the fixtures of the writers of v0.10.
* Format 2 has no area, and takes none.
*/
readonly areaLen?: number;
}
const CHUNK = 64 << 10; const CHUNK = 64 << 10;
/** /**
* Writes a .dkc of format 2 for the content of `src` with the random values * Writes a .dkc of format 2 for the content of `src` with the random values
* of `draws`, as capsule.Encrypt. See encrypt. * of `draws`, as capsule.Encrypt: only for a generator of test vectors, the
* one that passes `vectors`. See encrypt.
*/ */
export async function writeCapsule(src: EncryptSource, opts: EncryptOptions, draws: Draws): Promise<Encrypted> { export async function writeCapsule(src: EncryptSource, opts: EncryptOptions, draws: Draws, vectors?: TestVectors): Promise<Encrypted> {
return guarded(opts, async (state) => { return guarded(opts, async (state) => {
// Step 1: the inputs, before anything is used. Format 2 first: only a // Step 1: the inputs, before anything is used, in the order of
// generator of test vectors writes it, and it has no head. // capsule.Encrypt. Format 2 first: only a generator of test vectors
// writes it, and it has no head, no security area and no public note.
if (typeof opts !== 'object' || opts === null) throw new TypeError('encrypt: options are required'); if (typeof opts !== 'object' || opts === null) throw new TypeError('encrypt: options are required');
if (opts.testVectors !== true) { if (vectors === undefined) {
throw new Error('capsule: Encrypt writes format 2, which only a generator of test vectors may write (spec §62.1 rule 1): EncryptFiles writes format 3'); throw new Error('capsule: Encrypt writes format 2, which only a generator of test vectors may write (spec §62.1 rule 1): EncryptFiles writes format 3');
} }
if ((opts.comment ?? '') !== '' || (opts.author ?? '') !== '' || opts.headCritical !== undefined || opts.headNoncritical !== undefined) { if ((opts.comment ?? '') !== '' || (opts.author ?? '') !== '' || opts.headCritical !== undefined || opts.headNoncritical !== undefined) {
throw new Error('capsule: format 2 has no head: Comment, Author and the head extensions are for EncryptFiles'); throw new Error('capsule: format 2 has no head: Comment, Author and the head extensions are for EncryptFiles');
} }
checkOptions(opts); checkTypes(opts);
const length = sourceLength(src, opts.length); const length = sourceLength(src, opts.length);
if ((opts.publicNote ?? '') !== '' || vectors.areaLen !== undefined) {
throw new Error('capsule: format 2 has no security area or public note: AuthorKey, CMSSigner, Sealer, LargeArea and PublicNote are for EncryptFiles');
}
const s = await newSealer(opts, length); const s = await newSealer(opts, length);
return seal(s, FORMAT_2, length, draws, state, () => sourceContent(src, length)); return seal(s, FORMAT_2, length, draws, state, () => sourceContent(src, length));
}); });
@ -207,17 +224,19 @@ export async function writeCapsule(src: EncryptSource, opts: EncryptOptions, dra
/** /**
* Writes a .dkc of format 3 holding `files` and the comment and declared * Writes a .dkc of format 3 holding `files` and the comment and declared
* author of `opts`, with the random values of `draws`, as * author of `opts`, with the random values of `draws`, as
* capsule.EncryptFiles. See encryptFiles. * capsule.EncryptFiles, with the area that `vectors` asks for, when given.
* See encryptFiles.
*/ */
export async function writeFiles(files: readonly FileSource[], opts: EncryptOptions, draws: Draws): Promise<Encrypted> { export async function writeFiles(files: readonly FileSource[], opts: EncryptOptions, draws: Draws, vectors?: TestVectors): Promise<Encrypted> {
return guarded(opts, async (state) => { return guarded(opts, async (state) => {
// Step 1: the inputs, before anything is used. // Step 1: the inputs, before anything is used, in the order of
// capsule.EncryptFiles.
if (typeof opts !== 'object' || opts === null) throw new TypeError('encrypt: options are required'); if (typeof opts !== 'object' || opts === null) throw new TypeError('encrypt: options are required');
if (opts.length !== undefined) throw new Error('capsule: EncryptOptions.Length is for Encrypt: EncryptFiles computes L from the files'); if (opts.length !== undefined) throw new Error('capsule: EncryptOptions.Length is for Encrypt: EncryptFiles computes L from the files');
checkOptions(opts); checkTypes(opts);
// Spec §62.1 rule 13: the area is 32 KiB, and only a generator of test vectors writes another. // Spec §62.1 rule 13: the area is 32 KiB, and only a generator of test vectors writes another.
const areaLen = opts.areaLen ?? AREA_LEN; const areaLen = vectors?.areaLen ?? AREA_LEN;
if (areaLen !== AREA_LEN && (opts.testVectors !== true || !Number.isInteger(areaLen) || areaLen < AREA_UNIT || areaLen > MAX_AREA_LEN || areaLen % AREA_UNIT !== 0)) { if (!Number.isInteger(areaLen) || areaLen < AREA_UNIT || areaLen > MAX_AREA_LEN || areaLen % AREA_UNIT !== 0) {
throw new Error(`capsule: the security area is ${AREA_LEN} bytes: another size is for a generator of test vectors, a multiple of ${AREA_UNIT} up to ${MAX_AREA_LEN} (spec §62.1 rule 13)`); throw new Error(`capsule: the security area is ${AREA_LEN} bytes: another size is for a generator of test vectors, a multiple of ${AREA_UNIT} up to ${MAX_AREA_LEN} (spec §62.1 rule 13)`);
} }
const sources = copySources(files); const sources = copySources(files);
@ -424,13 +443,14 @@ async function guarded(opts: EncryptOptions, write: (state: WriteState) => Promi
} }
} }
// The options that every writer takes, checked as types before anything is // The types of the options that every writer takes and that Go's types
// used; each writer has checked first that they are an object. // enforce, checked before anything is used; each writer has checked first
function checkOptions(opts: EncryptOptions): void { // that they are an object. The profile and the clock, which Go requires
if (opts.profile === undefined || opts.profile === null) throw new TypeError('capsule: EncryptOptions.Profile is required'); // with its own texts, are checked by newSealer, in the order of Go.
if (typeof opts.now !== 'function') throw new TypeError('capsule: EncryptOptions.Now is required'); function checkTypes(opts: EncryptOptions): void {
if (typeof opts.policy !== 'number') throw new TypeError('encrypt: EncryptOptions.policy is required'); if (typeof opts.policy !== 'number') throw new TypeError('encrypt: EncryptOptions.policy is required');
if (!isInstant(opts.unlockAt)) throw new TypeError('encrypt: EncryptOptions.unlockAt is not an Instant'); if (!isInstant(opts.unlockAt)) throw new TypeError('encrypt: EncryptOptions.unlockAt is not an Instant');
if (opts.publicNote !== undefined && typeof opts.publicNote !== 'string') throw new TypeError('encrypt: EncryptOptions.publicNote is not a string');
if (opts.output !== undefined && !(opts.output instanceof WritableStream)) throw new TypeError('encrypt: EncryptOptions.output is not a WritableStream'); if (opts.output !== undefined && !(opts.output instanceof WritableStream)) throw new TypeError('encrypt: EncryptOptions.output is not a WritableStream');
if (opts.progress !== undefined && typeof opts.progress !== 'function') throw new TypeError('encrypt: EncryptOptions.progress is not a function'); if (opts.progress !== undefined && typeof opts.progress !== 'function') throw new TypeError('encrypt: EncryptOptions.progress is not a function');
} }
@ -458,28 +478,35 @@ interface Sealer {
readonly unlock: Instant; readonly unlock: Instant;
} }
// newSealer of the reference: copies of the inputs, then the profile, the // newSealer of the reference: copies of the inputs, then, in the order of Go,
// clock, the padding rule with length, a first L, checked against L_MAX, the // the public note, the presence of the profile and of the clock, the
// DateKey and the credentials (§15, §62.1 rules 2, 3 and 8). // profile, the clock, the padding rule with length, a first L, checked
// against L_MAX, the DateKey and the credentials (§15, §24.1, §62.1 rules 2,
// 3 and 8).
async function newSealer(opts: EncryptOptions, length: number): Promise<Sealer> { async function newSealer(opts: EncryptOptions, length: number): Promise<Sealer> {
// Step 2: copies, since the caller could change its inputs during an await. // Step 2: copies, since the caller could change its inputs during an await,
// each checked as a type as it is copied.
const recipients = (opts.recipients ?? []).map((r, i) => { const recipients = (opts.recipients ?? []).map((r, i) => {
if (!(r instanceof Uint8Array) || r.length !== 32) throw new TypeError(`encrypt: recipient ${i} is not a 32-byte X25519 public key`); if (!(r instanceof Uint8Array) || r.length !== 32) throw new TypeError(`encrypt: recipient ${i} is not a 32-byte X25519 public key`);
return copyBytes(r); return copyBytes(r);
}); });
const profile = cloneProfile(opts.profile);
const unlockAt: Instant = { seconds: opts.unlockAt.seconds, nanos: opts.unlockAt.nanos };
const critical = copyExtensions(opts.critical); const critical = copyExtensions(opts.critical);
const noncritical = copyExtensions(opts.noncritical); const noncritical = copyExtensions(opts.noncritical);
if (opts.publicNote !== undefined && opts.publicNote !== '') noncritical.push(newNote(opts.publicNote));
const controlCritical = copyExtensions(opts.controlCritical); const controlCritical = copyExtensions(opts.controlCritical);
const controlNoncritical = copyExtensions(opts.controlNoncritical); const controlNoncritical = copyExtensions(opts.controlNoncritical);
const { policy, output, progress } = opts;
const portable = opts.newPortableKey === true;
const words = (opts.words ?? []).map((w, i) => { const words = (opts.words ?? []).map((w, i) => {
if (typeof w !== 'string') throw new TypeError(`encrypt: word ${i} is not a string`); if (typeof w !== 'string') throw new TypeError(`encrypt: word ${i} is not a string`);
return w; return w;
}); });
// The public note, among the noncritical extensions of PUBLIC_HEADER, with
// the texts of Go after "capsule: ".
if ((opts.publicNote ?? '') !== '') noncritical.push(withContext('capsule', () => newNote(opts.publicNote!)));
if (opts.profile === undefined || opts.profile === null) throw new TypeError('capsule: EncryptOptions.Profile is required');
if (typeof opts.now !== 'function') throw new TypeError('capsule: EncryptOptions.Now is required');
const profile = cloneProfile(opts.profile);
const unlockAt: Instant = { seconds: opts.unlockAt.seconds, nanos: opts.unlockAt.nanos };
const { policy, output, progress } = opts;
const portable = opts.newPortableKey === true;
const code = opts.padding === undefined ? REFORZADO : opts.padding; const code = opts.padding === undefined ? REFORZADO : opts.padding;
// Step 3: the profile. // Step 3: the profile.

@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { encrypt } from '../dkc/encrypt.ts';
import { equalBytes, fromHex, type Instant, quicknet, roundTime, sha256, TIME_ONLY, toHex } from '../dkc/index.ts'; import { equalBytes, fromHex, type Instant, quicknet, roundTime, sha256, TIME_ONLY, toHex } from '../dkc/index.ts';
import { encryptVectors } from '../dkc/testing/encrypt.ts';
import { listTestdata, readBytes, readJSON } from '../dkc/testing/testdata.ts'; import { listTestdata, readBytes, readJSON } from '../dkc/testing/testdata.ts';
import { memoryFile } from '../dkc/testing/zip.ts'; import { memoryFile } from '../dkc/testing/zip.ts';
import { openCapsule, type OpenRequest, parseIdentities, PREVIEW_BYTES, systemClock } from './opener.ts'; import { openCapsule, type OpenRequest, parseIdentities, PREVIEW_BYTES, systemClock } from './opener.ts';
@ -141,7 +141,7 @@ describe('openCapsule', () => {
const body = Uint8Array.from({ length: PREVIEW_BYTES + 1000 }, (_, i) => 0x61 + (i % 26)); const body = Uint8Array.from({ length: PREVIEW_BYTES + 1000 }, (_, i) => 0x61 + (i % 26));
const round = f.record.release.round; const round = f.record.release.round;
const p = quicknet(); const p = quicknet();
const sealed = await encrypt(body, { profile: p, unlockAt: roundTime(p, round), policy: TIME_ONLY, now: () => ({ seconds: 0, nanos: 0 }), testVectors: true }); const sealed = await encryptVectors(body, { profile: p, unlockAt: roundTime(p, round), policy: TIME_ONLY, now: () => ({ seconds: 0, nanos: 0 }) });
const big = { ...f.request, capsule: sealed.dkc! }; const big = { ...f.request, capsule: sealed.dkc! };
for (const r of [await openCapsule(big), await openCapsule({ ...big, capsule: blob(sealed.dkc!), output: memoryTemp() })]) { for (const r of [await openCapsule(big), await openCapsule({ ...big, capsule: blob(sealed.dkc!), output: memoryTemp() })]) {
if (!r.ok) throw new Error(r.problem); if (!r.ok) throw new Error(r.problem);

Loading…
Cancel
Save

Powered by TurnKey Linux.