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/lengths.ts

217 lines
8.7 KiB

// The lengths of a .dkc of format 2 or 3 that follow from its inputs,
// without writing it (spec §62.1, informative note, §29.1 and §29.2): what a
// page shows before encrypting, since the size is visible to anyone who
// holds the file (§55.2), and what the writer checks its seal against. No
// noble, no age-encryption and no Unicode tables, so that a page can load it
// with its first load.
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 { 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';
// The chunks of an age STREAM of n bytes: at least one, even when empty.
const chunks = (n: number): number => Math.max(1, Math.ceil(n / 65536));
/**
* SEALED_CONTROL_LEN from the lengths of spec §62.1 (informative note), with
* C the length of CONTROL_CBOR: INNER_ACCESS_AGE holds 16 X25519 stanzas of
* 98 bytes, and the tlock stanza of OUTER_TIME_AGE is 249 bytes plus the
* digits of the round. The writer checks that the real seal measures exactly
* this.
*/
export function sealedControlLength(policy: Policy, controlLength: number, round: number): number {
const n = policy === TIME_AND_KEY ? 86 + 98 * ACCESS_SLOTS + controlLength + 16 * chunks(controlLength) : controlLength;
return 335 + String(round).length + n + 16 * chunks(n);
}
/** What the size of a .dkc depends on: every input of encrypt but the content and the credentials. */
export interface CapsuleLengthInput {
/** The profile_id of the DateKey, datekeys:quicknet:v1 for Quicknet. */
readonly profileId: string;
readonly round: number;
readonly policy: Policy;
/** L, the length of the content, or of BODY in format 3 (bodyLength). */
readonly length: number;
/** The padding rule; reforzado when omitted, as in encrypt. */
readonly padding?: Padding;
readonly critical?: readonly Extension[];
readonly noncritical?: readonly Extension[];
readonly controlCritical?: readonly Extension[];
readonly controlNoncritical?: readonly Extension[];
}
/**
* The exact size of the .dkc that encrypt, or encryptFiles with L from
* bodyLength, writes for these inputs: PRELUDE,
* PUBLIC_HEADER, SEALED_CONTROL_LEN and the length of PAYLOAD_AGE for P =
* rule(L). The random values and the credentials do not change it: a
* time_and_key capsule always holds 16 stanzas (§39). It throws, as the
* encoders do, for an invalid DateKey, policy, extension or L.
*/
export function capsuleLength(input: CapsuleLengthInput): number {
const padding = input.padding ?? REFORZADO;
const header = encodeHeader({
capsuleId: new Uint8Array(CAPSULE_ID_SIZE),
dateKey: { profileId: input.profileId, round: input.round },
policy: input.policy,
critical: input.critical ?? [],
noncritical: input.noncritical ?? [],
});
const control = encodeControl(
{
headerBinding: new Uint8Array(32),
payloadIdentity: new Uint8Array(32),
payloadLength: input.length,
padding,
critical: input.controlCritical ?? [],
noncritical: input.controlNoncritical ?? [],
},
FORMAT_2,
);
return (
DKC_PRELUDE_SIZE +
header.length +
sealedControlLength(input.policy, control.length, input.round) +
payloadAgeLength(paddedLength(input.length, padding))
);
}
// ---------------------------------------------------------------------------
// Format 3
// 9999-12-31T23:59:59Z in seconds, the last mtime of a head (MAX_MTIME of
// head.ts, which this module does not import: its rules of the paths bring
// the Unicode tables).
const LAST_MTIME = 253402300799;
// The frame of BODY and the security area that writers write, of 32 KiB (body.ts, spec v0.11 §29.2).
const FRAME_AND_AREA = 12 + AREA_LEN;
/**
* The comment as the writer of format 3 stores it: CR LF, and any lone CR,
* turned into LF (spec §29.6).
*/
export function headComment(comment: string): string {
return comment.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
}
/**
* The mtime a head stores for a file modified at `ms`, milliseconds since
* 1970-01-01 UTC as File.lastModified gives it: its seconds when they fall
* from 1970-01-01 to 9999-12-31T23:59:59Z, and none otherwise, never clipped
* (spec §62.1 rule 16).
*/
export function mtimeSeconds(ms: number | undefined): number | undefined {
if (ms === undefined) return undefined;
const s = Math.floor(ms / 1000);
return s >= 0 && s <= LAST_MTIME ? s : undefined;
}
/** What the length of the head of a format 3 capsule depends on. */
export interface HeadShape {
/** Each file: its path, its size and its mtime in milliseconds, as FileSource. */
readonly files: readonly { readonly path: string; readonly size: number; readonly mtime?: number }[];
/** As EncryptOptions gives them; the comment is taken as headComment stores it. */
readonly comment?: string;
readonly author?: string;
readonly critical?: readonly Extension[];
readonly noncritical?: readonly Extension[];
}
// The bytes of the head of a CBOR item whose argument is n.
const cborHead = (n: number): number => (n < 24 ? 1 : n < 0x100 ? 2 : n < 0x10000 ? 3 : n < 0x100000000 ? 5 : 9);
// A text or a byte string of n bytes, head included.
const cborString = (n: number): number => cborHead(n) + n;
// An extension map: {0: extension_id, 1: extension_version, ? 2: data}.
const extensionLength = (e: Extension): number =>
cborHead(e.data === undefined ? 2 : 3) +
1 +
cborString(utf8Length(e.id)) +
1 +
cborHead(e.version) +
(e.data === undefined ? 0 : 1 + cborString(e.data.length));
/**
* The files of a head measured once, so that a page can give the length of
* BODY for other texts without sorting the files again: see headLengthOf.
*/
export interface MeasuredFiles {
/** The number of files. */
readonly count: number;
/** The bytes that key 5, the array of the files, takes in HEAD_CBOR; 0 without files. */
readonly head: number;
/** C, the bytes of the files together. */
readonly content: number;
}
/**
* The files of a head measured as the writer of format 3 lays them out, in
* the byte order of their paths, which decides their start and end.
*/
export function measureFiles(files: HeadShape['files']): MeasuredFiles {
const sorted = files.map((f) => ({ ...f, key: utf8Bytes(f.path) })).sort((a, b) => compareBytes(a.key, b.key));
if (sorted.length === 0) return { count: 0, head: 0, content: 0 };
let n = 1 + cborHead(sorted.length);
let end = 0;
for (const f of sorted) {
const mtime = mtimeSeconds(f.mtime);
n += cborHead(mtime === undefined ? 5 : 6) + 1 + cborString(f.key.length) + 1 + cborHead(f.size) + 1 + cborHead(end);
end += f.size;
n += 1 + cborHead(end) + 1 + cborString(32) + (mtime === undefined ? 0 : 1 + cborHead(mtime));
}
return { count: sorted.length, head: n, content: end };
}
/**
* The length of the HEAD_CBOR that the writer of format 3 writes for these
* files and texts, from the sizes of its CBOR items (spec §29.4), without
* encoding it.
*/
export function headLength(h: HeadShape): number {
return headLengthOf(measureFiles(h.files), h);
}
/** headLength for files already measured, and the texts and extensions of `h`. */
export function headLengthOf(files: MeasuredFiles, h: Omit<HeadShape, 'files'>): number {
const comment = headComment(h.comment ?? '');
const author = h.author ?? '';
let pairs = 3;
// Keys 0, 1 and 2: "datekeys-head", 1 and the salt of 32 bytes.
let n = 1 + cborString(13) + 1 + 1 + 1 + cborString(32);
for (const text of [comment, author]) {
if (text === '') continue;
pairs++;
n += 1 + cborString(utf8Length(text));
}
if (files.count > 0) {
pairs++;
n += files.head;
}
for (const list of [h.critical ?? [], h.noncritical ?? []]) {
if (list.length === 0) continue;
pairs++;
n += 1 + cborHead(list.length) + list.reduce((sum, e) => sum + extensionLength(e), 0);
}
return cborHead(pairs) + n;
}
/**
* L of a format 3 capsule: the length of its BODY, the frame, the security
* area of 32 KiB, the head and the files (spec §29.2). With it,
* capsuleLength gives the size of the .dkc, since the control of format 3
* has the length of the control of format 2.
*/
export function bodyLength(h: HeadShape): number {
return bodyLengthOf(measureFiles(h.files), h);
}
/** bodyLength for files already measured, and the texts and extensions of `h`. */
export function bodyLengthOf(files: MeasuredFiles, h: Omit<HeadShape, 'files'>): number {
return FRAME_AND_AREA + headLengthOf(files, h) + files.content;
}

Powered by TurnKey Linux.