The rule of encoders of spec §72, and the note read from its bytes

As extension.CheckWrite of the Go reference on the branch v0.12:

- extension.ts holds NOTE_ID and CAPSULE_ID, and checkWrite: datekeys.note
  goes only in the noncritical array of PUBLIC_HEADER and datekeys.capsule
  only in that of a .dkk, version 1, with data that is checked; any other
  extension is the application's own.
- The writer of capsules applies it in newSealer, after the public note
  and before the profile, to the arrays of PUBLIC_HEADER, CONTROL_CBOR and
  the head, with the rules of the note; the writer of a .dkk applies it
  after checkDisjoint. The texts are those of Go, taken from a probe.
- note.ts: checkNoteData checks the bytes of a note in the order of Go's
  CheckNote, length, then UTF-8, then the rules of text; unusableNote is
  Go's Header.UnusableNote. lengths.ts no longer keeps its own NOTE_ID.

Green on the test data of spec-v0.11: it changes no output of a valid
writer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 2 days ago
parent fc5fb24fbe
commit 997f3318c8

@ -256,4 +256,25 @@ describe('marshalAccessKeyBody', () => {
const big = [{ id: 'a', version: 1, data: new Uint8Array(MAX_DKK_BODY_LEN) }];
expectCode(() => marshalAccessKeyBody({ ...key, noncritical: big }), 'ERR_INTEGRITY', /exceeds 16777216/);
});
// Spec §72, extension.CheckWrite in Go's MarshalBody: datekeys.capsule goes only in the noncritical array of a
// .dkk, with data, and datekeys.note never in a .dkk. The texts of Go on the branch v0.12.
it('writes the extensions of the specification only where §72 registers them', () => {
const capsule = { id: 'datekeys.capsule', version: 1, data: h('a0') };
const misplaced = (id: string, arr: string): string =>
`accesskey: extension: ${id} version 1 is not registered for ${arr} of .dkk: an encoder must not write it there (spec §72)`;
expect(() => marshalAccessKeyBody({ ...key, noncritical: [{ id: 'datekeys.note', version: 1, data: h('41') }] })).toThrow(
new Error(misplaced('datekeys.note', 'noncritical_extensions')),
);
expect(() => marshalAccessKeyBody({ ...key, critical: [capsule], noncritical: [] })).toThrow(new Error(misplaced('datekeys.capsule', 'critical_extensions')));
expectCode(
() => marshalAccessKeyBody({ ...key, noncritical: [{ ...capsule, data: undefined }] }),
'ERR_EXTENSION_DATA_INVALID',
/^accesskey: extension: datekeys\.capsule version 1: datekeys\.capsule without data: ERR_EXTENSION_DATA_INVALID$/,
);
// Its data is the locator's, which this library does not check: one byte is enough here.
expect(decodeAccessKeyBody(marshalAccessKeyBody({ ...key, noncritical: [capsule] })).noncritical).toEqual([capsule]);
// Another version is the application's own.
expect(() => marshalAccessKeyBody({ ...key, noncritical: [{ id: 'datekeys.note', version: 2, data: h('41') }] })).not.toThrow();
});
});

@ -7,7 +7,7 @@
import { concatBytes, copyBytes, goQuote, utf8Length } from './bytes.ts';
import { checkSchema, type Decoder, Encoder, unmarshal } from './cbor.ts';
import { DateKeysError, withContext } from './errors.ts';
import { canonicalExtensions, checkDisjoint, decodeArray, type Extension } from './extension.ts';
import { canonicalExtensions, checkDisjoint, checkWrite, decodeArray, type Extension } from './extension.ts';
import { dkkPreludeBytes, MAX_DKK_BODY_LEN, splitAccessKey } from './framing.ts';
import { encodeExtensionArrays, fieldOf, presence, requireKeys } from './schema.ts';
@ -240,6 +240,12 @@ export function marshalAccessKeyBody(k: AccessKey): Uint8Array {
const critical = canonicalExtensions(k.critical);
const noncritical = canonicalExtensions(k.noncritical);
checkDisjoint(k.critical, k.noncritical);
// Spec §72: datekeys.capsule goes only in the noncritical array of a .dkk,
// and datekeys.note never in a .dkk. The data of datekeys.capsule is the
// locator's, which this library does not write yet: only its presence is
// checked, as Go does with the Standard registry.
checkWrite('accesskey', '.dkk', 'critical_extensions', critical ?? []);
checkWrite('accesskey', '.dkk', 'noncritical_extensions', noncritical ?? []);
const e = new Encoder();
encodeWire(e, {
credentialId: k.credentialId,

@ -279,6 +279,38 @@ describe('encryptFiles, the writer of spec v0.11', () => {
];
for (const [name, files, opts, text] of cases) expect((await failure(encryptFiles(files, opts))).message, name).toBe(text);
});
// Spec §72, extension.CheckWrite in capsule.newSealer: datekeys.note goes only in the noncritical array of
// PUBLIC_HEADER, with valid data, and datekeys.capsule never in a capsule. The texts of Go on the branch v0.12.
const misplaced = (id: string, arr: string, obj: string): string =>
`capsule: extension: ${id} version 1 is not registered for ${arr} of ${obj}: an encoder must not write it there (spec §72)`;
const note: Extension = { id: 'datekeys.note', version: 1, data: utf8Bytes('Cartas') };
it.each([
[
'a note with a tab',
{ noncritical: [{ ...note, data: utf8Bytes('a\tb') }] },
'capsule: extension: datekeys.note version 1: a public note that breaks the rules of text: text: control U+0009 in the declared author: ERR_EXTENSION_DATA_INVALID',
],
['a note without data', { noncritical: [{ ...note, data: undefined }] }, 'capsule: extension: datekeys.note version 1: a public note without data: ERR_EXTENSION_DATA_INVALID'],
['a note in critical', { critical: [note] }, misplaced('datekeys.note', 'critical_extensions', 'PUBLIC_HEADER')],
['a note in the control', { controlNoncritical: [note] }, misplaced('datekeys.note', 'noncritical_extensions', 'CONTROL_CBOR')],
['a note in the control, critical', { controlCritical: [note] }, misplaced('datekeys.note', 'critical_extensions', 'CONTROL_CBOR')],
['a note in the head', { headNoncritical: [note] }, misplaced('datekeys.note', 'noncritical_extensions', 'head')],
['a note in the head, critical', { headCritical: [note] }, misplaced('datekeys.note', 'critical_extensions', 'head')],
['datekeys.capsule', { noncritical: [{ id: 'datekeys.capsule', version: 1, data: new Uint8Array([0xa0]) }] }, misplaced('datekeys.capsule', 'noncritical_extensions', 'PUBLIC_HEADER')],
] as [string, Partial<EncryptOptions>, string][])('refuses %s, as §72 asks of an encoder, with the text of Go and nothing written', async (_name, set, text) => {
const out = recorder();
const err = await failure(encryptFiles([source('a', 'x')], options({ ...set, output: out.stream })));
expect([err.message, out.chunks.length]).toEqual([text, 0]);
});
it('writes a valid note given as an extension, and checks the public note before §72, as Go', async () => {
const r = await encryptFiles([source('a', 'x')], options({ noncritical: [note] }));
const opened = await open(r.dkc!, opening(R1000, { sink: new MemorySink() }));
expect(opened.inspection.header!.noncritical).toEqual([note]);
const err = await failure(encryptFiles([source('a', 'x')], options({ publicNote: ' x', critical: [note] })));
expect(err.message).toBe(rules('the declared author starts or ends with U+0020'));
});
});
function indexOfText(hay: Uint8Array, s: string): number {

@ -2,15 +2,19 @@
// the Go package extension at 3820066 (TestNew, TestCanonical*,
// TestEncodeArrayRejects, TestDecodeArrayRejects, TestData,
// TestOrderIsUnsignedBytewise, TestCheckDisjoint*, TestCheckCritical,
// TestCheckNoncritical), with the error texts of the reference.
// TestCheckNoncritical), and TestCheckWrite of v0.12, with the error texts of
// the reference.
import { describe, expect, it } from 'vitest';
import { Decoder, Encoder, unmarshal } from './cbor.ts';
import { DateKeysError } from './errors.ts';
import {
CAPSULE_ID,
canonicalExtensions,
checkCritical,
checkDisjoint,
checkNoncritical,
checkWrite,
decodeArray,
encodeArray,
type Extension,
@ -21,6 +25,7 @@ import {
MAX_ID_LEN,
MAX_VERSION,
newExtension,
NOTE_ID,
} from './extension.ts';
import { arr, b, ext as extItem, map, t, u } from './testing/cborhex.ts';
import { expectCode, h, hx } from './testing/testdata.ts';
@ -330,3 +335,58 @@ describe('registries', () => {
checkCritical([ext('org.a', 1)], new ExtensionSet([['org.a', [1]]]), '.dkk');
});
});
// Go's TestCheckWrite with the Standard registry: what the specification
// defines goes only where §72 registers it, with data that validate accepts;
// the rest is the application's own, and is not checked.
describe('checkWrite', () => {
const note: Extension = { id: NOTE_ID, version: 1, data: Uint8Array.of(0x41) };
const capsule: Extension = { id: CAPSULE_ID, version: 1, data: Uint8Array.of(0xa0) };
it('lets each through only where it is registered', () => {
expect(() => checkWrite('capsule', 'PUBLIC_HEADER', 'noncritical_extensions', [note])).not.toThrow();
expect(() => checkWrite('accesskey', '.dkk', 'noncritical_extensions', [capsule])).not.toThrow();
expect(() => checkWrite('accesskey', '.dkk', 'noncritical_extensions', [note])).toThrow(
new Error('accesskey: extension: datekeys.note version 1 is not registered for noncritical_extensions of .dkk: an encoder must not write it there (spec §72)'),
);
expect(() => checkWrite('capsule', 'head', 'critical_extensions', [capsule])).toThrow(
new Error('capsule: extension: datekeys.capsule version 1 is not registered for critical_extensions of head: an encoder must not write it there (spec §72)'),
);
});
it('checks the data with validate, after its place, and wraps its errors', () => {
const calls: string[] = [];
checkWrite('capsule', 'PUBLIC_HEADER', 'noncritical_extensions', [note], (e) => void calls.push(e.id));
expect(calls).toEqual([NOTE_ID]);
const refuse = (): void => {
throw new DateKeysError('ERR_EXTENSION_DATA_INVALID', 'no');
};
expect(() => checkWrite('capsule', 'PUBLIC_HEADER', 'noncritical_extensions', [note], refuse)).toThrow(
'capsule: extension: datekeys.note version 1: no: ERR_EXTENSION_DATA_INVALID',
);
// An error that is not a DateKeysError, as a bug in validate, goes through as it is.
const broken = new TypeError('broken');
expect(() =>
checkWrite('capsule', 'PUBLIC_HEADER', 'noncritical_extensions', [note], () => {
throw broken;
}),
).toThrow(broken);
// Data that is absent is refused before validate is called.
expect(() => checkWrite('capsule', 'PUBLIC_HEADER', 'noncritical_extensions', [{ ...note, data: undefined }], refuse)).toThrow(
'capsule: extension: datekeys.note version 1: a public note without data: ERR_EXTENSION_DATA_INVALID',
);
});
it('does not check another id or another version', () => {
const others: Extension[] = [
{ id: 'org.example', version: 1, data: undefined },
{ id: NOTE_ID, version: 2, data: undefined },
{ id: CAPSULE_ID, version: 0, data: undefined },
];
expect(() =>
checkWrite('capsule', 'CONTROL_CBOR', 'critical_extensions', others, () => {
throw new Error('validated');
}),
).not.toThrow();
});
});

@ -350,3 +350,46 @@ function validateData(e: Extension, reg: ExtensionRegistry): DateKeysError | und
if (err === undefined) return undefined;
return new DateKeysError('ERR_EXTENSION_DATA_INVALID', `extension ${e.id} v${e.version}: data: ${err.message}`);
}
// ---------------------------------------------------------------------------
// The extensions of the specification
/** The public note of a capsule, in the noncritical array of PUBLIC_HEADER (spec §24.1). */
export const NOTE_ID = 'datekeys.note';
/** The extension of a .dkk that says what its capsule is and when it opens, in its noncritical array (spec §44.1). */
export const CAPSULE_ID = 'datekeys.capsule';
/**
* The rule of an encoder of spec §72 for the extensions that the
* specification defines, as Go's extension.CheckWrite with its Standard
* registry: datekeys.note goes only in the noncritical array of
* PUBLIC_HEADER and datekeys.capsule only in that of a .dkk, both of version
* 1 and with data, which `validate` checks further. The writer of capsules
* passes the rules of the note, which bring the Unicode tables (note.ts); a
* .dkk never holds a note. Any other extension is the application's own, and
* it answers for it. `context` names the writer, "capsule" or "accesskey",
* at the start of the error, as Go wraps it.
*/
export function checkWrite(
context: string,
obj: ExtensionObject,
arr: ExtensionArray,
exts: readonly Extension[],
validate?: (e: Extension) => void,
): void {
for (const e of exts) {
if (e.version !== 1 || (e.id !== NOTE_ID && e.id !== CAPSULE_ID)) continue;
if (arr !== 'noncritical_extensions' || obj !== (e.id === NOTE_ID ? 'PUBLIC_HEADER' : '.dkk')) {
throw new Error(`${context}: extension: ${e.id} version ${e.version} is not registered for ${arr} of ${obj}: an encoder must not write it there (spec §72)`);
}
try {
if (e.data === undefined) {
throw new DateKeysError('ERR_EXTENSION_DATA_INVALID', e.id === NOTE_ID ? 'a public note without data' : `${CAPSULE_ID} without data`);
}
validate?.(e);
} catch (err) {
if (err instanceof DateKeysError) throw err.wrap(`extension: ${e.id} version ${e.version}`).wrap(context);
throw err;
}
}
}

@ -9,7 +9,7 @@ import { ACCESS_SLOTS } from './age.ts';
import { AREA_LEN } from './body.ts';
import { compareBytes, utf8Bytes, utf8Length } from './bytes.ts';
import { encodeControl } from './control.ts';
import type { Extension } from './extension.ts';
import { type Extension, NOTE_ID } from './extension.ts';
import { DKC_PRELUDE_SIZE, FORMAT_2 } from './framing.ts';
import { CAPSULE_ID_SIZE, encodeHeader, type Policy, TIME_AND_KEY } from './header.ts';
import { paddedLength, type Padding, payloadAgeLength, REFORZADO } from './padding.ts';
@ -51,10 +51,6 @@ export interface CapsuleLengthInput {
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
* bodyLength, writes for these inputs: PRELUDE,

@ -3,7 +3,7 @@
// texts that extension.CheckNote gives at spec-v0.11.
import { describe, expect, it } from 'vitest';
import { checkNote, MAX_NOTE_LEN, newNote, NOTE_ID, publicNote } from './note.ts';
import { checkNote, checkNoteData, MAX_NOTE_LEN, newNote, NOTE_ID, publicNote, unusableNote } from './note.ts';
const lone = (unit: number): string => String.fromCharCode(unit);
const invalid = (detail: string): string => `a public note that breaks the rules of text: text: ${detail}: ERR_EXTENSION_DATA_INVALID`;
@ -75,3 +75,34 @@ describe('newNote and publicNote', () => {
expect(publicNote([{ id: NOTE_ID, version: 1, data: Uint8Array.of(0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63) }])).toBeUndefined();
});
});
// The bytes of a note, as Go's CheckNote checks the string of the data: the
// length first, then UTF-8, then the rules of text (testdata/vectors/note.json
// runs the shared cases).
describe('checkNoteData', () => {
const bytes = (s: string): Uint8Array => new TextEncoder().encode(s);
it('gives the text of a note that passes', () => {
expect(checkNoteData(bytes('Cartas del viaje a Lisboa'))).toBe('Cartas del viaje a Lisboa');
});
it('checks the length before UTF-8, and UTF-8 before the rules, with the texts of Go', () => {
expect(() => checkNoteData(new Uint8Array(0))).toThrow('a public note of 0 bytes, not 1 to 1024: ERR_EXTENSION_DATA_INVALID');
expect(() => checkNoteData(new Uint8Array(MAX_NOTE_LEN + 1).fill(0xff))).toThrow('a public note of 1025 bytes, not 1 to 1024: ERR_EXTENSION_DATA_INVALID');
expect(() => checkNoteData(Uint8Array.of(0x09, 0xff))).toThrow('a public note that is not valid UTF-8: ERR_EXTENSION_DATA_INVALID');
expect(() => checkNoteData(bytes('a\tb'))).toThrow(invalid('control U+0009 in the declared author'));
});
});
// Go's TestUnusableNote: a reader tells a note that breaks the rules of §24.1
// from no note, so that it can say that it does not show one.
describe('unusableNote', () => {
it.each([
['no note', [], false],
['a note that passes', [{ id: NOTE_ID, version: 1, data: new TextEncoder().encode('Cartas') }], false],
['a note with a tab', [{ id: NOTE_ID, version: 1, data: new TextEncoder().encode('a\tb') }], true],
['a note without data', [{ id: NOTE_ID, version: 1, data: undefined }], true],
['a note of version 2, which is not the public note', [{ id: NOTE_ID, version: 2, data: new TextEncoder().encode('a\tb') }], false],
] as const)('%s: %s', (_name, exts, want) => {
expect(unusableNote(exts)).toBe(want);
});
});

@ -8,11 +8,12 @@
import { decodeUtf8, utf8Bytes, utf8Length } from './bytes.ts';
import { DateKeysError } from './errors.ts';
import type { Extension } from './extension.ts';
import { type Extension, NOTE_ID } from './extension.ts';
import { checkAuthor } from './pathrule.ts';
/** The extension_id of the public note, and the longest note in bytes. */
export const NOTE_ID = 'datekeys.note';
export { NOTE_ID } from './extension.ts';
/** The longest public note, in bytes. */
export const MAX_NOTE_LEN = 1024;
/**
@ -41,22 +42,44 @@ export function newNote(text: string): Extension {
return { id: NOTE_ID, version: 1, data: utf8Bytes(text) };
}
/**
* Checks the data of a public note as Go's CheckNote checks the string of its
* bytes: from 1 to 1024 bytes, then valid UTF-8, then the rules of text, in
* that order and with the texts of Go (testdata/vectors/note.json). Its bytes
* are read as Go reads them: a leading U+FEFF stays, and the rules of text
* refuse it. Returns the text, and throws a DateKeysError when it is not one.
*/
export function checkNoteData(data: Uint8Array): string {
if (data.length < 1 || data.length > MAX_NOTE_LEN) {
throw new DateKeysError('ERR_EXTENSION_DATA_INVALID', `a public note of ${data.length} bytes, not 1 to ${MAX_NOTE_LEN}`);
}
const text = decodeUtf8(data);
if (text === undefined) throw new DateKeysError('ERR_EXTENSION_DATA_INVALID', 'a public note that is not valid UTF-8');
checkNote(text);
return text;
}
/**
* The text of the public note among the noncritical extensions of a
* PUBLIC_HEADER, and undefined when there is none that is usable: a note whose
* data breaks the rules of §24.1 is unusable, and shows nothing (spec §54).
* Its bytes are read as Go reads them: a leading U+FEFF stays, and the rules
* of text refuse it.
*/
export function publicNote(noncritical: readonly Extension[]): string | undefined {
const e = noncritical.find((x) => x.id === NOTE_ID && x.version === 1);
if (e?.data === undefined) return undefined;
const text = decodeUtf8(e.data);
if (text === undefined) return undefined;
try {
checkNote(text);
return checkNoteData(e.data);
} catch {
return undefined;
}
return text;
}
/**
* Whether the noncritical extensions of a PUBLIC_HEADER hold a public note
* that breaks the rules of §24.1: a reader does not show it, and says so
* (Go's Header.UnusableNote). publicNote cannot tell that case from a header
* without a note.
*/
export function unusableNote(noncritical: readonly Extension[]): boolean {
return noncritical.some((x) => x.id === NOTE_ID && x.version === 1) && publicNote(noncritical) === undefined;
}

@ -21,13 +21,13 @@ import { ACCESS_TYPE_X25519, type AccessKey } from './accesskey.ts';
import { ACCESS_SLOTS, ageStanzas, checkAccessStanzas, checkTimeStanzas, parseAgeHeader } from './age.ts';
import { decryptAll } from './agefile.ts';
import { AREA_LEN, AREA_UNIT, BODY_FRAME_SIZE, bodyFrameBytes, contentLength, MAX_AREA_LEN, MAX_HEAD_LEN, parseBodyFrame } from './body.ts';
import { newNote } from './note.ts';
import { checkNoteData, newNote } from './note.ts';
import { compareBytes, copyBytes, equalBytes, goQuote, toHex, utf8Bytes, utf8Length } from './bytes.ts';
import { decodeControl, encodeControl } from './control.ts';
import { compareInstants, type DateKey, formatRFC3339Nano, type Instant, isInstant, resolveDateKey, roundTime } from './datekey.ts';
import { sha256Hasher } from './digest.ts';
import { DateKeysError, withContext } from './errors.ts';
import type { Extension } from './extension.ts';
import { checkWrite, type Extension, type ExtensionArray, type ExtensionObject } from './extension.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 { decodeHeader, encodeHeader, type Policy, TIME_AND_KEY, TIME_ONLY } from './header.ts';
@ -244,7 +244,7 @@ export async function writeFiles(files: readonly FileSource[], opts: EncryptOpti
const author = checkText(opts.author, 'author');
const headCritical = copyExtensions(opts.headCritical);
const headNoncritical = copyExtensions(opts.headNoncritical);
const s = await newSealer(opts, 0);
const s = await newSealer(opts, 0, [headCritical, headNoncritical]);
const { head, order } = newHead(sources, headComment(comment), author, headCritical, headNoncritical);
// Step 2 of spec §61: L, with a head as long as the final one.
@ -479,11 +479,12 @@ interface Sealer {
}
// newSealer of the reference: copies of the inputs, then, in the order of Go,
// the public note, the presence of the profile and of the clock, the
// 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> {
// the public note, the extensions where §72 registers them, the presence of
// the profile and of the clock, the 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). `head` holds the copies of the head
// extensions of a capsule of format 3.
async function newSealer(opts: EncryptOptions, length: number, head: readonly [readonly Extension[], readonly Extension[]] = [[], []]): Promise<Sealer> {
// 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) => {
@ -501,6 +502,20 @@ async function newSealer(opts: EncryptOptions, length: number): Promise<Sealer>
// 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!)));
// Spec §72: the extensions that the specification registers go only where
// it registers them, with valid data. datekeys.capsule never goes in a
// capsule, and datekeys.note only in the noncritical array of PUBLIC_HEADER:
// anywhere else a reader would ignore it, or, in a critical array, refuse
// the capsule after the date.
const arrays: [ExtensionObject, ExtensionArray, readonly Extension[]][] = [
['PUBLIC_HEADER', 'critical_extensions', critical],
['PUBLIC_HEADER', 'noncritical_extensions', noncritical],
['CONTROL_CBOR', 'critical_extensions', controlCritical],
['CONTROL_CBOR', 'noncritical_extensions', controlNoncritical],
['head', 'critical_extensions', head[0]],
['head', 'noncritical_extensions', head[1]],
];
for (const [obj, arr, exts] of arrays) checkWrite('capsule', obj, arr, exts, (e) => void checkNoteData(e.data!));
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);

Loading…
Cancel
Save

Powered by TurnKey Linux.