You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/src/lib/dkc/head.ts

325 lines
12 KiB

// The head of a format 3 capsule (spec §29.4 to §29.6), as EncodeHead,
// DecodeHead and CheckHeadEnd of the Go package capsule at spec-v0.10
// (format3.go). HEAD_CBOR is the map
//
// {0: "datekeys-head", 1: 1, 2: salt, ? 3: comment, ? 4: declared author,
// ? 5: [+ file], ? 6: critical extensions, ? 7: noncritical extensions}
//
// and each file is {0: path, 1: size, 2: start, 3: end, 4: SHA-256,
// ? 5: mtime}. It is decoded in the layers of spec §69.1: the limit of §57
// (layer 1, ERR_INTEGRITY); the type tag and the version (layer 2); the CDDL
// with R1 and R8 (layer 3, ERR_NON_CANONICAL_CBOR); and then, in key order,
// the comment and the declared author (§29.6), the files with R2 to R6c,
// R10 and their layout, and R7 and R9 over the tree (§29.5), all
// ERR_HEAD_INVALID, and the critical extensions (layer 4).
//
// Internal: index.ts does not re-export it, since it brings the tables of
// pathrule.ts.
import { compareBytes, utf8Length } from './bytes.ts';
import { checkSchema, type Decoder, Encoder, unmarshal } from './cbor.ts';
import { MAX_HEAD_LEN } from './body.ts';
import { DateKeysError, withContext } from './errors.ts';
import { canonicalExtensions, checkCritical, checkDisjoint, decodeArray, type Extension, type ExtensionRegistry } from './extension.ts';
import { MAX_PAYLOAD_LENGTH } from './padding.ts';
import { checkAuthor, checkComment, checkPath, checkTree, MAX_AUTHOR_LEN, MAX_COMMENT_LEN, MAX_PATH_LEN, PathRuleError } from './pathrule.ts';
import { encodeExtensionArrays, fieldOf, presence, requireKeys } from './schema.ts';
export const HEAD_TYPE_TAG = 'datekeys-head';
export const HEAD_VERSION = 1;
/** The size of the salt of the head (spec §29.4). */
export const SALT_SIZE = 32;
/** The number of files of a head, at most, fixed with format 3 (spec §29.4). */
export const MAX_FILES = 65535;
/** 9999-12-31T23:59:59Z in seconds since 1970-01-01 UTC, the last mtime. */
export const MAX_MTIME = 253402300799;
const SHA256_SIZE = 32;
/** An entry of the head: a file of CONTENT. */
export interface HeadFile {
readonly path: string;
readonly size: number;
readonly start: number;
readonly end: number;
/** 32 bytes. */
readonly sha256: Uint8Array;
/**
* The modification time of the file at its source, in seconds since
* 1970-01-01 UTC, when known. It is informative: it proves nothing.
*/
readonly mtime?: number;
}
/** The head of a format 3 capsule (spec §29.4). */
export interface Head {
/** 32 bytes. */
readonly salt: Uint8Array;
/**
* The comment and the declared author, '' when absent. The declared author
* is text of the creator and proves nothing (spec §55.1).
*/
readonly comment: string;
readonly author: string;
/** The files, in strictly ascending byte order of their paths. */
readonly files: readonly HeadFile[];
/** Keys 6 and 7. */
readonly critical: readonly Extension[];
readonly noncritical: readonly Extension[];
}
// HEAD_CBOR as it is encoded.
interface HeadWire {
salt: Uint8Array;
comment: string;
author: string;
files: readonly HeadFile[];
critical: Extension[] | undefined;
noncritical: Extension[] | undefined;
}
const nonCanonical = (detail: string): DateKeysError => new DateKeysError('ERR_NON_CANONICAL_CBOR', detail);
// Reads HEAD_CBOR with the rules of the third layer: the CDDL, R1 and R8.
function decodeWire(d: Decoder): HeadWire {
const w: HeadWire = { salt: new Uint8Array(SALT_SIZE), comment: '', author: '', files: [], critical: undefined, noncritical: undefined };
const pairs = d.map(8);
const seen = new Set<number>();
for (let i = 0; i < pairs; i++) {
const k = d.key();
const field = fieldOf(k);
switch (k) {
case 0:
field(() => d.text(utf8Length(HEAD_TYPE_TAG)));
break;
case 1:
field(() => d.uint(HEAD_VERSION));
break;
case 2:
w.salt = field(() => d.bstr(SALT_SIZE, SALT_SIZE));
break;
case 3:
w.comment = field(() => decodeText(d, MAX_COMMENT_LEN, 'comment'));
break;
case 4:
w.author = field(() => decodeText(d, MAX_AUTHOR_LEN, 'declared author'));
break;
case 5:
w.files = field(() => decodeFiles(d));
break;
case 6:
w.critical = field(() => decodeArray(d));
break;
case 7:
w.noncritical = field(() => decodeArray(d));
break;
default:
throw nonCanonical(`key ${k} is not defined`);
}
seen.add(Number(k));
}
requireKeys(seen, 3);
d.endMap();
return w;
}
// Reads a text of 1 to max bytes.
function decodeText(d: Decoder, max: number, what: string): string {
const s = d.text(max);
if (s === '') throw nonCanonical(`empty ${what}`);
return s;
}
// Reads the array of files: 1 to MAX_FILES, each path of 1 to MAX_PATH_LEN
// bytes (R1), in strictly ascending order of their UTF-8 bytes (R8), which
// is not the order of JavaScript strings, by UTF-16 code units.
function decodeFiles(d: Decoder): HeadFile[] {
const n = d.array(MAX_FILES);
if (n === 0) throw nonCanonical('empty array of files');
const files: HeadFile[] = [];
let previous: Uint8Array | undefined;
for (let i = 0; i < n; i++) {
const { file, path } = withContext(`file ${i + 1}`, () => decodeFile(d));
if (previous !== undefined && compareBytes(path, previous) <= 0) {
throw nonCanonical(`file ${i + 1}: R8: the path is not after the path of file ${i} in byte order`);
}
previous = path;
files.push(file);
}
return files;
}
// Reads a file: keys 0 to 4 required, and 5 optional. It returns the UTF-8
// bytes of its path too, for R8.
function decodeFile(d: Decoder): { file: HeadFile; path: Uint8Array } {
const pairs = d.map(6);
const seen = new Set<number>();
let path: { text: string; utf8: Uint8Array } = { text: '', utf8: new Uint8Array(0) };
let size = 0;
let start = 0;
let end = 0;
let sha256: Uint8Array = new Uint8Array(SHA256_SIZE);
let mtime: number | undefined;
for (let i = 0; i < pairs; i++) {
const k = d.key();
fieldOf(k)(() => {
switch (k) {
case 0:
path = d.textUtf8(MAX_PATH_LEN);
if (path.text === '') throw nonCanonical('R1: the path is empty');
break;
case 1:
size = d.uint(MAX_PAYLOAD_LENGTH);
break;
case 2:
start = d.uint(MAX_PAYLOAD_LENGTH);
break;
case 3:
end = d.uint(MAX_PAYLOAD_LENGTH);
break;
case 4:
sha256 = d.bstr(SHA256_SIZE, SHA256_SIZE);
break;
case 5:
mtime = d.uint(MAX_MTIME);
break;
default:
throw nonCanonical(`key ${k} is not defined`);
}
});
seen.add(Number(k));
}
requireKeys(seen, 5);
d.endMap();
const file: HeadFile = { path: path.text, size, start, end, sha256, ...(mtime === undefined ? {} : { mtime }) };
return { file, path: path.utf8 };
}
function encodeWire(e: Encoder, w: HeadWire): void {
const optional = (w.comment === '' ? 0 : 1) + (w.author === '' ? 0 : 1) + (w.files.length === 0 ? 0 : 1);
e.map(3 + optional + presence(w.critical) + presence(w.noncritical));
e.uint(0);
e.text(HEAD_TYPE_TAG);
e.uint(1);
e.uint(HEAD_VERSION);
e.uint(2);
e.bstr(w.salt);
if (w.comment !== '') {
e.uint(3);
e.text(w.comment);
}
if (w.author !== '') {
e.uint(4);
e.text(w.author);
}
if (w.files.length > 0) {
e.uint(5);
e.array(w.files.length);
for (const f of w.files) encodeFile(e, f);
}
encodeExtensionArrays(e, 6, w.critical, w.noncritical);
}
function encodeFile(e: Encoder, f: HeadFile): void {
e.map(f.mtime === undefined ? 5 : 6);
e.uint(0);
e.text(f.path);
e.uint(1);
e.uint(f.size);
e.uint(2);
e.uint(f.start);
e.uint(3);
e.uint(f.end);
e.uint(4);
e.bstr(f.sha256);
if (f.mtime !== undefined) {
e.uint(5);
e.uint(f.mtime);
}
}
/**
* The Deterministic CBOR bytes of HEAD_CBOR for h. The files must already
* be in the byte order of their paths. The writer checks the result with
* decodeWrittenHead, the rules of the reader (spec §62.1 rule 17).
*/
export function encodeHead(h: Head): Uint8Array {
if (h.salt.length !== SALT_SIZE) throw new RangeError(`capsule: head: salt of ${h.salt.length} bytes, want ${SALT_SIZE}`);
h.files.forEach((f, i) => {
if (f.sha256.length !== SHA256_SIZE) throw new RangeError(`capsule: head: file ${i + 1}: SHA-256 of ${f.sha256.length} bytes, want ${SHA256_SIZE}`);
});
const critical = canonicalExtensions(h.critical);
const noncritical = canonicalExtensions(h.noncritical);
checkDisjoint(h.critical, h.noncritical);
if (h.files.length > MAX_FILES) throw new Error(`capsule: head: ${h.files.length} files, more than ${MAX_FILES}`);
const e = new Encoder();
encodeWire(e, { salt: h.salt, comment: h.comment, author: h.author, files: h.files, critical, noncritical });
return e.out();
}
/**
* Validates and decodes HEAD_CBOR with the layers of spec §69.1 (spec
* §29.4, §63 step 17.4): the type tag and the version (layer 2), the CDDL
* with R1 and R8 (layer 3), and then, in key order, the comment and the
* declared author (§29.6), the files with R2 to R6c, R10 and their layout,
* R7 and R9 over the tree (§29.5), all ERR_HEAD_INVALID, and the critical
* extensions with reg (layer 4).
*/
export function decodeHead(b: Uint8Array, reg?: ExtensionRegistry): Head {
const h = decodeWrittenHead(b);
withContext('capsule: head', () => checkCritical(h.critical, reg, 'head'));
return h;
}
/**
* decodeHead but for the critical extensions, whose knowledge depends on the
* reader: the self-check of the writer decodes with it, as it decodes the
* control with decodeControl (spec §62.1 rule 17).
*/
export function decodeWrittenHead(b: Uint8Array): Head {
if (b.length > MAX_HEAD_LEN) throw new DateKeysError('ERR_INTEGRITY', `capsule: head: ${b.length} bytes, more than ${MAX_HEAD_LEN}`);
const w = withContext('capsule: head', () => {
checkSchema(b, HEAD_TYPE_TAG, HEAD_VERSION);
const w = unmarshal(b, decodeWire, encodeWire);
checkDisjoint(w.critical ?? [], w.noncritical ?? []);
return w;
});
const h: Head = { salt: w.salt, comment: w.comment, author: w.author, files: w.files, critical: w.critical ?? [], noncritical: w.noncritical ?? [] };
checkHeadFields(h);
return h;
}
// The rules of the fourth layer with a code of their own, ERR_HEAD_INVALID:
// keys 3, 4 and 5, in that order.
function checkHeadFields(h: Head): void {
const invalid = (what: string, detail: string): DateKeysError => new DateKeysError('ERR_HEAD_INVALID', `capsule: head: ${what}${detail}`);
const rule = (what: string, check: () => void): void => {
try {
check();
} catch (err) {
/* v8 ignore next -- @preserve: the rules throw only PathRuleError */
if (!(err instanceof PathRuleError)) throw err;
throw invalid(what, err.message);
}
};
if (h.comment !== '') rule('comment: ', () => checkComment(h.comment));
if (h.author !== '') rule('declared author: ', () => checkAuthor(h.author));
let end = 0;
h.files.forEach((f, i) => {
const what = `file ${i + 1}: `;
rule(what, () => checkPath(f.path));
if (f.start !== end) throw invalid(what, `start ${f.start} is not ${end}, the end of the file before`);
if (f.end < f.start || f.end - f.start !== f.size) throw invalid(what, `from start ${f.start} to end ${f.end} is not the size ${f.size}`);
end = f.end;
});
rule('', () => checkTree(h.files.map((f) => f.path)));
}
/**
* Checks that the files fill CONTENT, of c bytes: the last one ends at C, or
* C is 0 without files (spec §29.4, §63 step 17.5).
*/
export function checkHeadEnd(h: Head, c: number): void {
const end = h.files.length > 0 ? h.files[h.files.length - 1]!.end : 0;
if (end !== c) throw new DateKeysError('ERR_INTEGRITY', `capsule: BODY: the files end at byte ${end} of a content of ${c} bytes`);
}

Powered by TurnKey Linux.