encryptFiles writes the security area of 32 KiB, as rule 13 of 62.1 asks of a writer of v0.11, and accepts publicNote, the extension datekeys.note of PUBLIC_HEADER, with the rules of text of 24.1 (note.ts). Another area is for a generator of test vectors, with testVectors and areaLen, so that the tests still reproduce byte for byte the fixtures that a writer of v0.10 wrote with 512 bytes. lengths.ts and the page plan L with the new area. A note changed after writing fails at step 15. npm run verify passes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>main
parent
92b4d196eb
commit
3f80e589fa
@ -0,0 +1,43 @@
|
||||
// Tests of note.ts, the public note of spec v0.11 §24.1: the same cases as the
|
||||
// tests of extension.CheckNote, NewNote and Note of the Go reference.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { checkNote, MAX_NOTE_LEN, newNote, NOTE_ID, publicNote } from './note.ts';
|
||||
|
||||
describe('checkNote', () => {
|
||||
it('accepts a line of 1 to 1024 bytes that meets the rules of the declared author', () => {
|
||||
expect(() => checkNote('Cartas del viaje a Lisboa')).not.toThrow();
|
||||
expect(() => checkNote('x'.repeat(MAX_NOTE_LEN))).not.toThrow();
|
||||
expect(() => checkNote('Ñandú, 日本')).not.toThrow();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['empty', ''],
|
||||
['a tab', 'a\tb'],
|
||||
['a line feed', 'a\nb'],
|
||||
['a space at the start', ' a'],
|
||||
['a space at the end', 'a '],
|
||||
['too long', 'x'.repeat(MAX_NOTE_LEN + 1)],
|
||||
['a right-to-left override', 'a\u202eb'],
|
||||
['a zero-width space', 'a\u200bb'],
|
||||
])('refuses %s', (_name, text) => {
|
||||
expect(() => checkNote(text)).toThrow(/public note/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('newNote and publicNote', () => {
|
||||
it('makes the extension, and reads it back among the noncritical ones', () => {
|
||||
const e = newNote('Hola');
|
||||
expect([e.id, e.version, new TextDecoder().decode(e.data)]).toEqual([NOTE_ID, 1, 'Hola']);
|
||||
expect(publicNote([{ id: 'x.other', version: 1, data: undefined }, e])).toBe('Hola');
|
||||
expect(publicNote([])).toBeUndefined();
|
||||
expect(() => newNote('a\nb')).toThrow(/public note/);
|
||||
});
|
||||
|
||||
it('shows nothing for a note that is unusable: another version, no data, bad text or bad UTF-8', () => {
|
||||
expect(publicNote([{ id: NOTE_ID, version: 2, data: new TextEncoder().encode('v2') }])).toBeUndefined();
|
||||
expect(publicNote([{ id: NOTE_ID, version: 1, data: undefined }])).toBeUndefined();
|
||||
expect(publicNote([{ id: NOTE_ID, version: 1, data: new TextEncoder().encode('a\nb') }])).toBeUndefined();
|
||||
expect(publicNote([{ id: NOTE_ID, version: 1, data: Uint8Array.of(0xff, 0xfe) }])).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,51 @@
|
||||
// The public note of a capsule (spec v0.11, §24.1): the extension
|
||||
// datekeys.note, version 1, in the noncritical array of PUBLIC_HEADER. Its
|
||||
// data is the text in UTF-8, from 1 to 1024 bytes, with no CBOR around it, and
|
||||
// it meets the rules of text of the declared author of §29.6: one line, no
|
||||
// tabs and no spaces at the ends. It is public: whoever holds the .dkc reads it
|
||||
// before the date, and nobody can check who wrote it. As Go's extension.CheckNote,
|
||||
// NewNote and Note.
|
||||
|
||||
import { utf8Length } from './bytes.ts';
|
||||
import { DateKeysError } from './errors.ts';
|
||||
import type { Extension } 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 const MAX_NOTE_LEN = 1024;
|
||||
|
||||
/** The text of a public note is from 1 to 1024 bytes of valid UTF-8 that meets the rules of the declared author (spec §24.1). Throws a DateKeysError when it is not. */
|
||||
export function checkNote(text: string): void {
|
||||
const n = utf8Length(text);
|
||||
if (n < 1 || n > MAX_NOTE_LEN) throw new DateKeysError('ERR_EXTENSION_DATA_INVALID', `a public note of ${n} bytes, not 1 to ${MAX_NOTE_LEN}`);
|
||||
try {
|
||||
checkAuthor(text);
|
||||
} catch (err) {
|
||||
throw new DateKeysError('ERR_EXTENSION_DATA_INVALID', `a public note that breaks the rules of text: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The extension of a public note for `text`, which must pass checkNote. */
|
||||
export function newNote(text: string): Extension {
|
||||
checkNote(text);
|
||||
return { id: NOTE_ID, version: 1, data: new TextEncoder().encode(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).
|
||||
*/
|
||||
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;
|
||||
let text: string;
|
||||
try {
|
||||
text = new TextDecoder('utf-8', { fatal: true }).decode(e.data);
|
||||
checkNote(text);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
Loading…
Reference in new issue