The locator of datekeys.capsule without its cryptography

Port of package locator of datekeys-go at spec-v0.12 (spec §43 to §44.1),
with the checks, the order, the codes and the texts of Go: the data of the
extension (parseInfo, infoExtension), the registry of locator.Standard,
the addresses with every rule of §44.1 (checkURI, addressHost,
usableAddresses), the IP addresses as netip reads them, compared byte by
byte with the IANA blocks, checkResolvedIp as datekeys-dart has it, the
plaintext with its padding, and the rest in its host.

scripts/locator-go-vectors.go, the generator of datekeys-dart stage 7a
with the seeds of this repository, writes testing/locator-uris.json and
locator-vectors.json from an export of datekeys-go at spec-v0.12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 1 day ago
parent 45e7e499d4
commit 0d895bf362

File diff suppressed because it is too large Load Diff

@ -0,0 +1,289 @@
// The IP addresses of the addresses of a locator (spec §44.1), as Go's
// net/netip reads and writes them and package locator of datekeys-go
// classifies them: parseIpAddress accepts exactly what netip.ParseAddr
// accepts, ipString is netip's String, and isPublicIp is publicIP, with the
// blocks of the IANA registries of special-purpose addresses that §44.1
// lists.
//
// An address is held as its bytes, 4 for IPv4 and 16 for IPv6, and every
// operation works byte by byte: the 128 bits of an IPv6 address are never a
// number, which in JavaScript is a double with 53 bits and whose bit
// operators work on 32 bits.
//
// Internal: index.ts does not re-export it.
import { utf8Bytes } from './bytes.ts';
/**
* An IP address, as Go's netip.Addr: an IPv4 address of 4 bytes, or an IPv6
* address of 16 bytes with a zone, "" for none. An IPv4-mapped IPv6 address
* is an IPv6 address.
*/
export interface IpAddress {
readonly bytes: Uint8Array;
readonly zone: string;
}
/**
* The address of the 4 or 16 bytes `b`, without a zone, as Go's
* netip.AddrFromSlice; any other length is a RangeError. It keeps a copy.
*/
export function ipFromBytes(b: Uint8Array): IpAddress {
if (!(b instanceof Uint8Array) || (b.length !== 4 && b.length !== 16)) {
throw new RangeError('ipaddr: an IP address is 4 or 16 bytes');
}
return { bytes: b.slice(), zone: '' };
}
/** Whether `a` is an IPv4 address, as netip's Is4: an IPv4-mapped IPv6 address is not. */
export const isIpv4 = (a: IpAddress): boolean => a.bytes.length === 4;
/** Whether `a` is an IPv6 address, IPv4-mapped ones included. */
export const isIpv6 = (a: IpAddress): boolean => a.bytes.length === 16;
// Whether the IPv6 address a is an IPv4-mapped one, of ::ffff:0:0/96.
function is4In6(a: IpAddress): boolean {
for (let i = 0; i < 10; i++) if (a.bytes[i] !== 0) return false;
return a.bytes[10] === 0xff && a.bytes[11] === 0xff;
}
const dotted = (b: Uint8Array, at: number): string => `${b[at]}.${b[at + 1]}.${b[at + 2]}.${b[at + 3]}`;
/**
* The address as netip's String writes it: dotted decimal for IPv4; "::ffff:"
* and dotted decimal for an IPv4-mapped IPv6 address; and the form of RFC
* 5952 for the others, the first longest run of two or more groups of zeros
* written "::"; with "%" and the zone when it has one.
*/
export function ipString(a: IpAddress): string {
const b = a.bytes;
if (isIpv4(a)) return dotted(b, 0);
const zoned = a.zone === '' ? '' : `%${a.zone}`;
if (is4In6(a)) return `::ffff:${dotted(b, 12)}${zoned}`;
const groups: number[] = [];
for (let i = 0; i < 8; i++) groups.push((b[2 * i]! << 8) | b[2 * i + 1]!);
// The first longest run of two or more groups of zeros, as appendTo6.
let zeroStart = 255;
let zeroEnd = 255;
for (let i = 0; i < 8; i++) {
let j = i;
while (j < 8 && groups[j] === 0) j++;
if (j - i >= 2 && j - i > zeroEnd - zeroStart) {
zeroStart = i;
zeroEnd = j;
}
}
let out = '';
for (let i = 0; i < 8; i++) {
if (i === zeroStart) {
out += '::';
i = zeroEnd;
if (i >= 8) break;
} else if (i > 0) {
out += ':';
}
out += groups[i]!.toString(16);
}
return out + zoned;
}
const DOT = 0x2e;
const COLON = 0x3a;
const PERCENT = 0x25;
const isDigit = (c: number): boolean => c >= 0x30 && c <= 0x39;
/**
* The address that `s` writes, or undefined when Go's netip.ParseAddr
* rejects it: dotted decimal IPv4 of four fields of 0 to 255 without leading
* zeros ("192.0.2.1"), or IPv6 of eight groups of one to four hexadecimal
* digits in either case, with at most one "::" that stands for one or more
* groups of zeros and the last two groups possibly written as an IPv4
* address ("2001:db8::68", "::ffff:192.0.2.1"), and possibly "%" and a
* non-empty zone of any characters ("fe80::1%eth0"). The first ".", ":" or
* "%" of `s` decides which, as in Go. Nothing else is accepted: no brackets,
* no spaces, no other notation of IPv4. `s` is read as its UTF-8 bytes, as Go
* reads a string.
*/
export function parseIpAddress(s: string): IpAddress | undefined {
const b = utf8Bytes(s);
for (const c of b) {
if (c === DOT) {
const fields = new Uint8Array(4);
return ipv4Fields(b, 0, b.length, fields, 0) ? { bytes: fields, zone: '' } : undefined;
}
if (c === COLON) return parseIpv6(b, s);
// An IPv6 address with a zone and without the address.
if (c === PERCENT) return undefined;
}
return undefined;
}
// Go's parseIPv4Fields: the four fields of b[off:end] into
// fields[at..at+4], or false.
function ipv4Fields(b: Uint8Array, off: number, end: number, fields: Uint8Array, at: number): boolean {
let val = 0;
let pos = 0;
let digLen = 0;
const n = end - off;
for (let i = 0; i < n; i++) {
const c = b[off + i]!;
if (isDigit(c)) {
// A field with a leading zero.
if (digLen === 1 && val === 0) return false;
val = val * 10 + c - 0x30;
digLen++;
if (val > 255) return false;
} else if (c === DOT) {
// .1.2.3, 1.2.3. and 1..2.3: a field without digits.
if (i === 0 || i === n - 1 || b[off + i - 1] === DOT) return false;
// 1.2.3.4.5
if (pos === 3) return false;
fields[at + pos] = val;
pos++;
val = 0;
digLen = 0;
} else {
return false;
}
}
if (pos < 3) return false;
fields[at + 3] = val;
return true;
}
function hexValue(c: number): number {
if (isDigit(c)) return c - 0x30;
if (c >= 0x61 && c <= 0x66) return c - 0x61 + 10;
if (c >= 0x41 && c <= 0x46) return c - 0x41 + 10;
return -1;
}
// Go's parseIPv6 of the bytes b of the string s.
function parseIpv6(b: Uint8Array, s: string): IpAddress | undefined {
// The zone, split off first. Once the address before it parses, it is
// ASCII, so the byte offset of the % is also its index in s.
let end = b.length;
const pct = b.indexOf(PERCENT);
if (pct >= 0) {
if (pct + 1 === b.length) return undefined;
end = pct;
}
const zone = (): string => (pct < 0 ? '' : s.slice(pct + 1));
const ip = new Uint8Array(16);
let ellipsis = -1;
let p = 0;
if (end - p >= 2 && b[p] === COLON && b[p + 1] === COLON) {
ellipsis = 0;
p += 2;
if (p === end) return { bytes: ip, zone: zone() };
}
let i = 0;
while (i < 16) {
// One group of at most four hexadecimal digits.
let off = 0;
let acc = 0;
for (; p + off < end; off++) {
const v = hexValue(b[p + off]!);
if (v < 0) break;
acc = acc * 16 + v;
if (off > 3) return undefined;
}
if (off === 0) return undefined;
// A dot: the group starts the trailing IPv4 address.
if (p + off < end && b[p + off] === DOT) {
if (ellipsis < 0 && i !== 12) return undefined;
if (i + 4 > 16) return undefined;
if (!ipv4Fields(b, p, end, ip, i)) return undefined;
p = end;
i += 4;
break;
}
ip[i] = acc >> 8;
ip[i + 1] = acc & 0xff;
i += 2;
p += off;
if (p === end) break;
if (b[p] !== COLON) return undefined;
if (end - p === 1) return undefined;
p++;
if (b[p] === COLON) {
if (ellipsis >= 0) return undefined;
ellipsis = i;
p++;
if (p === end) break;
}
}
if (p !== end) return undefined;
if (i < 16) {
if (ellipsis < 0) return undefined;
const n = 16 - i;
for (let j = i - 1; j >= ellipsis; j--) ip[j + n] = ip[j]!;
ip.fill(0, ellipsis, ellipsis + n);
} else if (ellipsis >= 0) {
// "::" stands for at least one group of zeros.
return undefined;
}
return { bytes: ip, zone: zone() };
}
// A block of addresses, as Go's netip.Prefix: its first bytes and the number
// of bits that count.
interface Prefix {
readonly bytes: Uint8Array;
readonly bits: number;
}
function prefix(s: string): Prefix {
const [addr, bits] = s.split('/');
return { bytes: parseIpAddress(addr!)!.bytes, bits: Number(bits) };
}
// Whether a is in the block p, as netip's Prefix.Contains: an address of the
// other family, or with a zone, never is.
function contains(p: Prefix, a: IpAddress): boolean {
if (a.zone !== '' || a.bytes.length !== p.bytes.length) return false;
const full = p.bits >> 3;
for (let i = 0; i < full; i++) if (a.bytes[i] !== p.bytes[i]) return false;
const rest = p.bits & 7;
if (rest === 0) return true;
const mask = (0xff << (8 - rest)) & 0xff;
return (a.bytes[full]! & mask) === (p.bytes[full]! & mask);
}
// The blocks of the IANA registries of special-purpose addresses that an
// address of a locator may not use (spec §44.1). An IPv6 address must also
// be a global unicast one, of 2000::/3.
const NOT_PUBLIC_4 = [
'0.0.0.0/8',
'10.0.0.0/8',
'100.64.0.0/10',
'127.0.0.0/8',
'169.254.0.0/16',
'172.16.0.0/12',
'192.0.0.0/24',
'192.0.2.0/24',
'192.88.99.0/24',
'192.168.0.0/16',
'198.18.0.0/15',
'198.51.100.0/24',
'203.0.113.0/24',
'224.0.0.0/4',
'240.0.0.0/4',
].map(prefix);
const GLOBAL_6 = ['2000::/3'].map(prefix);
const NOT_PUBLIC_6 = ['2001::/23', '2001:db8::/32', '2002::/16', '3fff::/20'].map(prefix);
const inAny = (a: IpAddress, ps: readonly Prefix[]): boolean => ps.some((p) => contains(p, a));
/**
* Whether `a` is an address of the public Internet (spec §44.1), as publicIP
* of Go: an IPv4 address outside the blocks of §44.1, or an IPv6 address of
* 2000::/3 outside 2001::/23, 2001:db8::/32, 2002::/16 and 3fff::/20. An IPv6
* address that holds an IPv4 one, mapped, compatible, of NAT64, 6to4 or
* Teredo, is not: it would reach the IPv4 address without the check of an
* IPv4 address. Nor is one with a zone.
*/
export function isPublicIp(a: IpAddress): boolean {
if (isIpv4(a)) return !inAny(a, NOT_PUBLIC_4);
return inAny(a, GLOBAL_6) && !inAny(a, NOT_PUBLIC_6);
}

@ -0,0 +1,412 @@
// The locator without its cryptography against Go (spec §43 to §44.1):
//
// - testing/locator-uris.json, from scripts/locator-go-vectors.go: checkURI
// and addressHost on the 247 addresses of testdata/vectors/locator.json
// and on 2 800 built at the edges of §44.1 and drawn from a seed, with the
// texts of CheckURI and Host; parseIpAddress against netip.ParseAddr and
// String; and isPublicIp on the bytes of addresses, as checkResolvedIp
// checks the IP that a name resolves to;
// - testing/locator-vectors.json: PlaintextLength on every base from -4100
// to 16484; Unmarshal on 482 plaintexts, valid and broken; Marshal at
// every limit; RestIn and Hide; Info.Extension and ParseInfo; the registry
// of locator.Standard; and capsule.Open of a fixture whose .dkk carries
// datekeys.capsule, with locator.Standard.
//
// Every text is Go's, byte for byte.
import { describe, expect, it } from 'vitest';
import { decodeAccessKey, encodeAccessKey } from './accesskey.ts';
import { fromHex, toHex } from './bytes.ts';
import { compactDateKey, parseRFC3339 } from './datekey.ts';
import { DateKeysError } from './errors.ts';
import { CAPSULE_ID, checkCritical, checkNoncritical, checkWrite, type Extension, NOTE_ID } from './extension.ts';
import { ipFromBytes, ipString, isPublicIp, parseIpAddress } from './ipaddr.ts';
import {
addressHost,
checkResolvedIp,
checkURI,
hide,
infoExtension,
type Locator,
LocatorError,
marshalLocator,
parseInfo,
plaintextLength,
restIn,
standardExtensions,
unmarshalLocator,
usableAddresses,
} from './locator.ts';
import { MemorySink } from './sink.ts';
import { open } from './open.ts';
import { suppliedRelease } from './release.ts';
import {
applyEdits,
errorText,
expandSummary,
fromWtf8,
type Json,
readVectors,
releaseOf,
type Row,
rows,
sha256Hex,
summaryOf,
textOf,
uriOf,
} from './testing/locator.ts';
import { readBytes } from './testing/testdata.ts';
const uris = readVectors('locator-uris.json');
const v = readVectors('locator-vectors.json');
describe('checkURI and addressHost (locator-uris.json)', () => {
function expectUri(c: Row): void {
const uri = c[0] as string;
const want = textOf(uris, c[1]);
expect(errorText(() => checkURI(uri)), uri).toBe(want);
const host = addressHost({ uri, offset: 0 });
expect(host, uri).toBe(c[2]);
// As FuzzCheckURI of Go: the host shown is in the address as it is
// written, with no decoding.
if (want === '') expect(uri.includes(host) && host !== '').toBe(true);
}
it('the addresses of locator.json, with the texts of Go', () => {
const cases = rows(uris, 'testdata');
expect(cases.length).toBe(247);
cases.forEach(expectUri);
});
it('2 800 addresses at the edges of §44.1 and drawn from a seed', () => {
const cases = rows(uris, 'uris');
expect(cases.length).toBe(2800);
cases.forEach(expectUri);
// Every rule rejects some and some pass.
expect(new Set(cases.map((c) => c[1])).size).toBeGreaterThan(20);
});
it('an address that is not ASCII is measured and quoted in UTF-8, and a lone surrogate as the bytes of WTF-8', () => {
const c = rows(uris, 'uris').find((x) => x[0] === 'https://ejemplo.org/é')!;
expectUri(c);
expect(textOf(uris, c[1])).toContain("'Ã'");
const wtf8 = rows(uris, 'wtf8');
expect(wtf8.length).toBe(4);
for (const w of wtf8) {
const uri = fromWtf8(fromHex(w[0] as string));
expect(errorText(() => checkURI(uri))).toBe(textOf(uris, w[1]));
expect(addressHost({ uri, offset: 0 })).toBe('');
}
});
it('Go accepts a doubled opening bracket, and a CID with one more character whose bits are zero (pending decisions 1 and 2 of the author)', () => {
for (const uri of ['https://[[2000::]/', 'ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdia']) {
const c = rows(uris, 'uris').find((x) => x[0] === uri);
expect(c?.[1], uri).toBe(0);
expectUri(c!);
}
});
});
describe('IP addresses (locator-uris.json)', () => {
it('parseIpAddress accepts what netip.ParseAddr accepts, with its bytes, its zone and its String', () => {
const cases = rows(uris, 'ips');
expect(cases.length).toBe(1700);
for (const c of cases) {
const s = c[0] as string;
const a = parseIpAddress(s);
if (c[1] === false) {
expect(a, s).toBeUndefined();
continue;
}
expect([toHex(a!.bytes), a!.zone, ipString(a!), isPublicIp(a!)], s).toEqual(c.slice(2));
}
});
it('checkResolvedIp checks the bytes of an address as publicIP of Go, IPv4-mapped and NAT64 addresses not public', () => {
const cases = rows(uris, 'public');
expect(cases.length).toBe(868);
for (const c of cases) {
const b = fromHex(c[0] as string);
if (c[1] === null) {
expect(() => checkResolvedIp(b)).toThrow(RangeError);
continue;
}
const a = ipFromBytes(b);
expect(ipString(a)).toBe(c[2]);
expect(isPublicIp(a), ipString(a)).toBe(c[1]);
expect(errorText(() => checkResolvedIp(b))).toBe(
c[1] === true ? '' : `locator: an https address whose name resolves to ${c[2] as string}, an IP address that is not public`,
);
}
expect(() => checkResolvedIp(Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 8, 8, 8, 8))).toThrow(LocatorError);
expect(() => checkResolvedIp(fromHex('0064ff9b000000000000000008080808'))).toThrow(LocatorError);
checkResolvedIp(Uint8Array.of(8, 8, 8, 8));
// An embedded IPv4 address with no room left, which netip refuses too.
expect(parseIpAddress('1::2:3:4:5:6:7:1.2.3.4')).toBeUndefined();
expect(ipString(parseIpAddress('1:2:3:4:5:6:1.2.3.4')!)).toBe('1:2:3:4:5:6:102:304');
expect(() => checkResolvedIp([8, 8, 8, 8] as unknown as Uint8Array)).toThrow(RangeError);
});
});
describe('the locator against Go (locator-vectors.json)', () => {
const envelope = v.envelope as Json;
const plainBases = (v.plaintext_bases as string[]).map(fromHex);
const sealedBases = (v.sealed_bases as Json[]).map((s) => fromHex(s.sealed as string));
const parseBases = (v.parse_bases as string[]).map(fromHex);
const datas = (v.datas as string[]).map(fromHex);
function base(over: Partial<Locator> = {}): Locator {
return {
addresses: [{ uri: 'https://ejemplo.org/foto.jpg', offset: 3000 }],
envelopeKey: fromHex(envelope.key as string),
envelopeHeader: fromHex(envelope.header as string),
restDigest: fromHex(envelope.rest_digest as string),
restSize: envelope.rest_size as number,
capsuleDigest: fromHex(envelope.capsule_digest as string),
...over,
};
}
it('plaintextLength gives the least multiple that key 6 fills, on every base from -4100 to 16484', () => {
let n = 0;
for (const r of rows(v, 'padding')) {
for (let b = r[0] as number; b <= (r[1] as number); b++) {
expect(plaintextLength(b), String(b)).toBe(r[2]);
n++;
}
}
expect(n).toBe(4100 + 1 + 16484);
expect(() => plaintextLength(0.5)).toThrow(RangeError);
});
it('unmarshalLocator reads plaintexts as Unmarshal', () => {
const cases = rows(v, 'plaintexts');
expect(cases.length).toBe(482);
for (const c of cases) {
const b = applyEdits(plainBases[c[0] as number]!, c[1]);
let l: Locator | undefined;
expect(errorText(() => (l = unmarshalLocator(b))), JSON.stringify(c[1])).toBe(textOf(v, c[2]));
if (c[3] !== null) {
expect(summaryOf(l!)).toEqual(expandSummary(c[3]));
// As FuzzUnmarshal of Go: an address that a reader uses passes the
// rules, and has a host to show.
for (const a of usableAddresses(l!)) {
checkURI(a.uri);
expect(addressHost(a)).not.toBe('');
}
}
}
});
it('marshalLocator writes the plaintext of Marshal, or its error', () => {
const cases = rows(v, 'marshal');
expect(cases.length).toBe(49);
for (const c of cases) {
const l = base({
addresses: (c[0] as Row[]).map((a) => ({ uri: uriOf(a[0]), offset: a[1] as number })),
...(c[1] === null ? {} : { envelopeHeader: fromHex(c[1] as string) }),
restSize: c[2] as number,
});
let b: Uint8Array | undefined;
expect(errorText(() => (b = marshalLocator(l))), JSON.stringify(c.slice(1, 3))).toBe(textOf(v, c[3]));
if (b !== undefined) {
expect([b.length, sha256Hex(b)]).toEqual([c[4], c[5]]);
// It reads back as it was written.
expect(summaryOf(unmarshalLocator(b))).toEqual(summaryOf(l));
}
}
});
it('restIn reads rest_size bytes from the offset, as RestIn', () => {
const resources = [fromHex(v.rest_in_resource as string), new Uint8Array(0)];
const cases = rows(v, 'rest_in');
expect(cases.length).toBe(20);
for (const c of cases) {
const l = base({ restSize: c[2] as number });
let r: Uint8Array | undefined;
expect(errorText(() => (r = restIn(l, resources[c[0] as number]!, c[1] as number))), JSON.stringify(c)).toBe(textOf(v, c[3]));
if (r !== undefined) expect(sha256Hex(r)).toBe(c[4]);
}
expect(() => restIn(base(), new Uint8Array(3), -1)).toThrow(RangeError);
});
it('hide appends the rest, as Hide', () => {
const cases = rows(v, 'hide');
expect(cases.length).toBe(4);
for (const c of cases) {
const { file, offset } = hide(fromHex(c[0] as string), fromHex(c[1] as string));
expect([toHex(file), offset]).toEqual([c[2], c[3]]);
}
});
it('infoExtension writes the data of datekeys.capsule as Info.Extension', () => {
const cases = rows(v, 'info');
expect(cases.length).toBe(29);
for (const c of cases) {
const s = c[3];
let sealed: Uint8Array | undefined;
if (typeof s === 'number') sealed = sealedBases[s];
else if (typeof s === 'string') sealed = fromHex(s);
else if (s !== null) sealed = new Uint8Array((s as Json).zeros as number);
const info = { note: c[0] as string, dateKey: { profileId: c[1] as string, round: c[2] as number }, sealed };
let x: Extension | undefined;
expect(errorText(() => (x = infoExtension(info))), JSON.stringify(c.slice(0, 3))).toBe(textOf(v, c[4]));
if (x !== undefined) {
expect([x.id, x.version]).toEqual([CAPSULE_ID, 1]);
expect([x.data!.length, sha256Hex(x.data!)]).toEqual([c[5], c[6]]);
// A reader reads it back.
const back = parseInfo(x);
expect([back.note, back.dateKey]).toEqual([info.note, info.dateKey]);
expect(back.sealed).toEqual(info.sealed);
}
}
});
it('parseInfo reads the data of datekeys.capsule as ParseInfo, with ERR_EXTENSION_DATA_INVALID only', () => {
const std = standardExtensions();
const cases = rows(v, 'parse');
expect(cases.length).toBe(127);
for (const c of cases) {
const b = c[2] as number;
const data = b < 0 ? undefined : applyEdits(parseBases[b]!, c[3]);
const x: Extension = { id: c[0] as string, version: c[1] as number, data };
const want = textOf(v, c[4]);
let info: ReturnType<typeof parseInfo> | undefined;
let err: unknown;
try {
info = parseInfo(x);
} catch (e) {
err = e;
}
expect(err === undefined ? '' : (err as Error).message, JSON.stringify(c.slice(0, 4))).toBe(want);
if (want !== '') {
expect((err as DateKeysError).code).toBe('ERR_EXTENSION_DATA_INVALID');
if (x.id === CAPSULE_ID && x.version === 1 && x.data !== undefined) {
expect(std.validateData!(x)?.message).toBe(want);
}
continue;
}
expect(std.validateData!(x)).toBeUndefined();
expect([info!.note, compactDateKey(info!.dateKey), info!.dateKey.round]).toEqual([c[5], c[6], c[7]]);
expect(info!.sealed === undefined ? null : sha256Hex(info!.sealed)).toBe(c[8]);
}
});
it('the registry of locator.Standard checks the data of datekeys.capsule in a .dkk, and an encoder only its presence', () => {
const std = standardExtensions();
const presence = standardExtensions(null);
const validate = (e: Extension): void => {
parseInfo(e);
};
// checkWrite of this library names its writer before the text of Go.
const write = (arr: 'critical_extensions' | 'noncritical_extensions', exts: Extension[], check?: (e: Extension) => void): string => {
const t = errorText(() => checkWrite('w', '.dkk', arr, exts, check));
return t === '' ? '' : t.replace(/^w: /, '');
};
const cases = rows(v, 'registry');
expect(cases.length).toBe(8);
for (const c of cases) {
const d = c[2] as number;
const x: Extension = { id: c[0] as string, version: c[1] as number, data: d < 0 ? undefined : datas[d] };
expect(
checkNoncritical([x], std, '.dkk').map((u) => u.error.message),
JSON.stringify(c.slice(0, 3)),
).toEqual((c[3] as number[]).map((i) => textOf(v, i)));
expect(errorText(() => checkCritical([x], std, '.dkk'))).toBe(textOf(v, c[4]));
expect(write('noncritical_extensions', [x], validate)).toBe(textOf(v, c[5]));
expect(write('critical_extensions', [x], validate)).toBe(textOf(v, c[6]));
expect(write('noncritical_extensions', [x])).toBe(textOf(v, c[7]));
// Without ValidateCapsule only the presence of the data counts.
expect(presence.validateData!(x)?.message ?? '').toBe(x.data === undefined && x.version === 1 ? 'datekeys.capsule without data: ERR_EXTENSION_DATA_INVALID' : '');
}
// The note: in PUBLIC_HEADER only, with the rules of §24.1.
expect(std.registeredIn!(NOTE_ID, 1, 'PUBLIC_HEADER', 'noncritical_extensions')).toBe(true);
expect(std.registeredIn!(NOTE_ID, 1, '.dkk', 'noncritical_extensions')).toBe(false);
expect(std.known(NOTE_ID, 1) && !std.known(NOTE_ID, 2) && !std.known('org.example', 1)).toBe(true);
expect(std.validateData!({ id: NOTE_ID, version: 1, data: undefined })?.message).toBe('a public note without data: ERR_EXTENSION_DATA_INVALID');
expect(std.validateData!({ id: NOTE_ID, version: 1, data: new TextEncoder().encode(' x') })?.message).toMatch(/^a public note that breaks the rules of text/);
expect(std.validateData!({ id: NOTE_ID, version: 1, data: new TextEncoder().encode('Cartas') })).toBeUndefined();
expect(std.validateData!({ id: 'org.example', version: 1, data: Uint8Array.of(1) })).toBeUndefined();
});
it('open of a capsule whose .dkk carries datekeys.capsule reports its data as capsule.Open does with locator.Standard', async () => {
const cap = v.capsule as Json;
const name = cap.fixture as string;
const dkc = readBytes(`fixtures/${name}.dkc`);
const dkk = decodeAccessKey(readBytes(`fixtures/${name}.dkk`));
const now = parseRFC3339(cap.now as string);
const cases = cap.cases as Json[];
expect(cases.length).toBe(9);
for (const c of cases) {
const file = encodeAccessKey({
...dkk,
material: dkk.material.slice(),
noncritical: (c.noncritical as Row[]).map((x) => ({ id: x[0] as string, version: x[1] as number, data: fromHex(x[3] as string) })),
});
expect(sha256Hex(file)).toBe(c.dkk_sha256);
const opened = await open(dkc, {
source: suppliedRelease(releaseOf(v, 1000)),
now: () => now,
accessKeyFile: file,
sink: new MemorySink(),
...(c.standard === true ? { extensions: standardExtensions() } : {}),
});
expect(opened.error?.message ?? '').toBe(textOf(v, c.text));
expect(opened.unusableAccessKeyExtensions.map((u) => [u.id, u.version, u.error.message])).toEqual(
(c.unusable as Row[]).map((u) => [u[0], u[1], textOf(v, u[2])]),
);
expect(opened.inspection.checks.map((ch) => [ch.step, ch.name, ch.ok, ch.detail ?? '', ch.error ?? ''])).toEqual(c.checks);
expect(sha256Hex(opened.plaintext ?? new Uint8Array(0))).toBe(c.content);
}
});
});
describe('the types of Go, and what a caller gets wrong', () => {
const l: Locator = {
addresses: [{ uri: 'https://a.org/', offset: 5 }],
envelopeKey: new Uint8Array(32),
envelopeHeader: Uint8Array.of(1),
restDigest: new Uint8Array(32),
restSize: 0,
capsuleDigest: new Uint8Array(32),
};
it('a key or a digest of another length, a negative size or offset and another type are errors of the caller', () => {
expect(marshalLocator(l).length).toBe(4096);
for (const bad of [
{ envelopeKey: new Uint8Array(31) },
{ restDigest: new Uint8Array(33) },
{ capsuleDigest: [0] as unknown as Uint8Array },
{ envelopeHeader: 'h' as unknown as Uint8Array },
{ restSize: -1 },
{ restSize: 1.5 },
{ addresses: [{ uri: 'https://a.org/', offset: -1 }] },
{ addresses: [{ uri: 5 as unknown as string, offset: 0 }] },
{ addresses: 'x' as unknown as Locator['addresses'] },
]) {
expect(() => marshalLocator({ ...l, ...bad }), JSON.stringify(Object.keys(bad))).toThrow(/^locator: /);
expect(() => marshalLocator({ ...l, ...bad })).not.toThrow(LocatorError);
}
});
it('a size or an offset above 2^53 - 1 is the error of Go', () => {
expect(errorText(() => marshalLocator({ ...l, restSize: 2 ** 53 }))).toBe('locator: a rest larger than 2^53 - 1 bytes');
expect(errorText(() => marshalLocator({ ...l, addresses: [{ uri: 'https://a.org/', offset: 2 ** 53 }] }))).toBe('locator: an offset larger than 2^53 - 1');
});
it('an address with a lone surrogate is three bytes that are not UTF-8, as Go reads them', () => {
expect(errorText(() => checkURI('https://a.org/\ud800'))).toBe("locator: an address with the character 'í', which RFC 3986 does not allow");
expect(errorText(() => marshalLocator({ ...l, addresses: [{ uri: '\udc00'.repeat(342), offset: 0 }] }))).toBe('locator: an address of 1026 bytes, not 1 to 1024');
expect(errorText(() => checkURI('https://a.org/\ud800a'))).toBe("locator: an address with the character 'í', which RFC 3986 does not allow");
expect(errorText(() => checkURI('https://a.org/\u{1F600}'))).toBe("locator: an address with the character 'ð', which RFC 3986 does not allow");
expect(errorText(() => checkURI('https://a.org/\u0080'))).toBe("locator: an address with the character 'Â', which RFC 3986 does not allow");
});
it('infoExtension refuses a DateKey that is not canonical and a note that breaks its rules, with their errors', () => {
expect(errorText(() => infoExtension({ note: '', dateKey: { profileId: 'datekeys:quicknet:v1', round: 0 } }))).toBe(
'locator: Info.DateKey is not a canonical DateKey',
);
expect(() => infoExtension({ note: 'a\tb', dateKey: { profileId: 'datekeys:quicknet:v1', round: 1 } })).toThrow(DateKeysError);
});
});

@ -0,0 +1,913 @@
// The extension datekeys.capsule of a .dkk and what it points to (spec §43
// to §44.1), as package locator of datekeys-go at the tag spec-v0.12, with
// the same checks in the same order, the same codes and the same texts:
//
// - the data of the extension (Info, parseInfo, infoExtension), which says
// what the capsule of a key is and when it opens;
// - the locator (Locator), an age file sealed with tlock for that date,
// whose plaintext says where the capsule is: the addresses of the rest,
// and the key, the header and the digests of the envelope;
// - the envelope, the .dkc encrypted with age and split into a header, which
// the locator carries, and a rest, the only thing kept outside, alone or
// inside another file (hide, restIn).
//
// This module holds what needs no cryptography. Opening a sealed locator,
// opening the envelope, sealing a locator and making an envelope are in
// envelope.ts, over the age reader and writer of ageio.ts.
//
// It downloads nothing. An application fetches the rest only when the
// person asks, after showing her the host or the CID of the address
// (addressHost), and only from an address that checkURI accepts
// (usableAddresses); it follows no redirect to an address that checkURI
// rejects, checks with checkResolvedIp on every connection that the IP a
// name resolves to is public, reads only the bytes of the rest, and gives
// them to openEnvelope, which checks their SHA-256, decrypts the .dkc and
// checks its SHA-256 (spec §44.1).
//
// The errors of the locator carry no normative code, as in Go: a locator
// that does not read or does not open, or an address that breaks the rules
// of §44.1, is unusable (spec §44.1, §57), and each is a LocatorError with
// the text of Go. The data of the extension has one code,
// ERR_EXTENSION_DATA_INVALID: data that does not read makes the extension
// unusable, never the .dkk (spec §54).
//
// Internal to the opening and the writing: index.ts does not re-export it,
// since the rules of the note bring the Unicode tables of pathrule.ts.
import { ageStanzas, STANZA_TLOCK } from './age.ts';
import { goQuote, goIsPrintNonASCII } from './bytes.ts';
import { type Decoder, Encoder, MAX_SAFE_UINT, unmarshal } from './cbor.ts';
import { compactDateKey, type DateKey, parseDateKey } from './datekey.ts';
import { DateKeysError, withContext } from './errors.ts';
import { CAPSULE_ID, type Extension, type ExtensionRegistry, newExtension, NOTE_ID } from './extension.ts';
import { ipFromBytes, ipString, isIpv4, isIpv6, isPublicIp, parseIpAddress } from './ipaddr.ts';
import { checkNote, checkNoteData, MAX_NOTE_LEN } from './note.ts';
/** The most addresses of a locator (spec §44.1). */
export const MAX_ADDRESSES = 8;
/** The longest address, in bytes (spec §44.1). */
export const MAX_URI_LEN = 1024;
/** The longest header of an envelope, in bytes (spec §44.1). */
export const MAX_HEADER_LEN = 1024;
/**
* The unit of the plaintext of a locator: it measures exactly 4096 bytes, or
* the least multiple of 4096 that key 6 can fill (spec §44.1).
*/
export const BLOCK = 4096;
/** The largest sealed locator that a reader decrypts, and the most plaintext it reads of one: Go's maxSealed. */
export const MAX_SEALED = 1 << 20;
const DIGEST_LEN = 32;
/**
* A failure of the locator, without a normative code, with the text of the
* error of Go, such as "locator: the rest is 808 bytes, not 809".
*/
export class LocatorError extends Error {
constructor(message: string) {
super(message);
this.name = 'LocatorError';
}
}
const fail = (detail: string): LocatorError => new LocatorError(`locator: ${detail}`);
// A key or a field that the schema does not define or that is missing, as
// the decoders of Go write it.
const undefinedKey = (what: string): DateKeysError => new DateKeysError('ERR_NON_CANONICAL_CBOR', what);
// ---------------------------------------------------------------------------
// The data of datekeys.capsule
/** The data of the extension datekeys.capsule (spec §44.1), as Info of Go. */
export interface Info {
/** Key 0, the copy of the public note of the capsule, "" for none. */
readonly note: string;
/** Key 1, the DateKey of the capsule: it says when it opens. */
readonly dateKey: DateKey;
/** Key 2, the age file of the locator, sealed with tlock for the round of dateKey, or undefined for none. */
readonly sealed?: Uint8Array | undefined;
}
function encodeInfo(e: Encoder, note: string, dk: string, sealed: Uint8Array | undefined): void {
let pairs = 1;
if (note !== '') pairs++;
if (sealed !== undefined) pairs++;
e.map(pairs);
if (note !== '') {
e.uint(0);
e.text(note);
}
e.uint(1);
e.text(dk);
if (sealed !== undefined) {
e.uint(2);
e.bstr(sealed);
}
}
/**
* The extension for the noncritical array of a .dkk, as Info.Extension of
* Go: the DateKey must be canonical, the sealed locator of 1 byte to 1 MiB,
* and the note must meet the rules of spec §24.1; the extension is read back
* with the rules of a reader (parseInfo), which also ties the locator to the
* round of the DateKey (spec §72). The rules of the note throw their
* DateKeysError, ERR_EXTENSION_DATA_INVALID; the others a LocatorError.
*/
export function infoExtension(i: Info): Extension {
let d: DateKey | undefined;
try {
d = parseDateKey(compactDateKey(i.dateKey));
} catch {
d = undefined;
}
if (d === undefined || d.profileId !== i.dateKey.profileId || d.round !== i.dateKey.round) {
throw fail('Info.DateKey is not a canonical DateKey');
}
const s = i.sealed;
if (s !== undefined && (s.length < 1 || s.length > MAX_SEALED)) {
throw fail(`a sealed locator of ${s.length} bytes, not 1 to ${MAX_SEALED}`);
}
if (i.note !== '') checkNote(i.note);
const e = new Encoder();
encodeInfo(e, i.note, compactDateKey(i.dateKey), s);
const x = newExtension(CAPSULE_ID, 1, e.out());
// Spec §72: the encoder reads what it writes with the rules of a reader,
// which also ties the locator to the round of the DateKey.
try {
parseInfo(x);
} catch (err) {
throw fail(`self-check: a reader rejects this extension: ${(err as Error).message}`);
}
return x;
}
const dataInvalid = (detail: string): DateKeysError => new DateKeysError('ERR_EXTENSION_DATA_INVALID', `locator: ${detail}`);
/**
* Reads the data of a datekeys.capsule extension, as ParseInfo of Go: a map
* of the profile of spec §58 with the note, key 0, which meets the rules of
* spec §24.1, the canonical DateKey, key 1, and the sealed locator, key 2,
* an age file with one tlock stanza for the round of the DateKey; its chain
* is checked when it opens. A failure makes the extension unusable, not the
* .dkk (spec §54): it is a DateKeysError whose only code is
* ERR_EXTENSION_DATA_INVALID.
*/
export function parseInfo(x: Extension): Info {
const data = x.data;
if (x.id !== CAPSULE_ID || x.version !== 1 || data === undefined) {
throw dataInvalid('not datekeys.capsule version 1 with data');
}
let note = '';
let dk = '';
let sealed: Uint8Array | undefined;
try {
unmarshal(
data,
(d: Decoder) => {
const pairs = d.map(3);
let seen = 0;
for (let i = 0; i < pairs; i++) {
const k = d.key();
switch (k) {
case 0:
withContext('key 0', () => {
note = d.text(MAX_NOTE_LEN);
checkNote(note);
});
break;
case 1:
dk = withContext('key 1', () => d.text(1024));
break;
case 2:
sealed = withContext('key 2', () => d.bstr(1, MAX_SEALED));
break;
default:
throw undefinedKey(`key ${k} is not defined`);
}
seen |= 1 << k;
}
if ((seen & 2) === 0) throw undefinedKey('key 1 is missing');
d.endMap();
},
(e: Encoder) => encodeInfo(e, note, dk, sealed),
);
} catch (err) {
throw dataInvalid(`datekeys.capsule: ${(err as Error).message}`);
}
let d: DateKey | undefined;
try {
d = parseDateKey(dk);
} catch {
d = undefined;
}
if (d === undefined || compactDateKey(d) !== dk) throw dataInvalid('compact_datekey is not a canonical DateKey');
if (sealed !== undefined && !sealedFor(sealed, d.round)) {
throw dataInvalid(`the locator is not an age file with one tlock stanza for round ${d.round}, the one of its DateKey`);
}
return { note, dateKey: d, sealed };
}
// Whether sealed is an age file whose header holds one tlock stanza with two
// arguments, the first of them round.
function sealedFor(sealed: Uint8Array, round: number): boolean {
let st: ReturnType<typeof ageStanzas>;
try {
st = ageStanzas(sealed);
} catch {
return false;
}
return st.length === 1 && st[0]!.type === STANZA_TLOCK && st[0]!.args.length === 2 && st[0]!.args[0] === String(round);
}
/**
* The registry of the extensions that the specification defines (spec §72),
* as Go's extension.Standard: the public note in the noncritical array of
* PUBLIC_HEADER and datekeys.capsule in that of a .dkk, both of version 1,
* each known only there. It validates the data of a note with the rules of
* §24.1, and the data of datekeys.capsule with `validateCapsule`, which by
* default is parseInfo, as Go's locator.Standard does (spec §54): with it,
* a datekeys.capsule that does not read is unusable, with the text of Go.
* With `validateCapsule` null, only the presence of the data of
* datekeys.capsule is checked, as Go's extension.Standard without
* ValidateCapsule.
*/
export function standardExtensions(validateCapsule: ((e: Extension) => void) | null = (e) => void parseInfo(e)): ExtensionRegistry {
const errorOf = (check: () => void): Error | undefined => {
try {
check();
return undefined;
} catch (err) {
return err as Error;
}
};
return {
known: (id, version) => version === 1 && (id === NOTE_ID || id === CAPSULE_ID),
registeredIn: (id, version, obj, arr) =>
arr === 'noncritical_extensions' && version === 1 && ((id === NOTE_ID && obj === 'PUBLIC_HEADER') || (id === CAPSULE_ID && obj === '.dkk')),
validateData(e: Extension): Error | undefined {
if (e.id === NOTE_ID) {
if (e.data === undefined) return new DateKeysError('ERR_EXTENSION_DATA_INVALID', 'a public note without data');
const data = e.data;
return errorOf(() => checkNoteData(data));
}
if (e.id === CAPSULE_ID) {
if (e.data === undefined) return new DateKeysError('ERR_EXTENSION_DATA_INVALID', `${CAPSULE_ID} without data`);
if (validateCapsule !== null) return errorOf(() => validateCapsule(e));
}
return undefined;
},
};
}
// ---------------------------------------------------------------------------
// Addresses
/** An address of a locator: where the rest of the envelope is (spec §44.1), as Address of Go. */
export interface Address {
/** ASCII of RFC 3986, with the scheme https or ipfs (a CID v1), and no userinfo, as checkURI checks it. */
readonly uri: string;
/** The byte of the resource where the rest starts, 0 when the rest is the whole resource. */
readonly offset: number;
}
/**
* The bytes of `s` as Go holds a string: UTF-8, with a lone surrogate as the
* three bytes that would encode its code point (WTF-8), which are not valid
* UTF-8, so that an address with one is measured and refused as Go refuses
* those bytes, never read with U+FFFD in its place.
*/
function wtf8(s: string): Uint8Array {
const out: number[] = [];
for (let i = 0; i < s.length; i++) {
let c = s.charCodeAt(i);
if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) {
const lo = s.charCodeAt(i + 1);
if (lo >= 0xdc00 && lo <= 0xdfff) {
c = 0x10000 + ((c - 0xd800) << 10) + (lo - 0xdc00);
i++;
}
}
if (c < 0x80) out.push(c);
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
else out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
}
return Uint8Array.from(out);
}
const accepts = (uri: string): boolean => {
try {
checkURI(uri);
return true;
} catch {
return false;
}
};
/**
* What a reader shows before it downloads (spec §44.1), as Host of Go: the
* host of an https address, without the brackets of an IPv6 literal, or the
* CID of an ipfs one, as the address writes it, with no decoding; "" when
* checkURI rejects the address.
*/
export function addressHost(a: Address): string {
if (!accepts(a.uri)) return '';
return trimBrackets(splitAuthority(a.uri)[1]);
}
/**
* Checks an address with the rules of spec §44.1, as CheckURI of Go, in its
* order and with its texts:
* - 1 to 1024 bytes of ASCII of RFC 3986, each percent sign followed by two
* hexadecimal digits;
* - a scheme and "://", and an authority without a percent sign, userinfo or
* a backslash, whose port, in an https address, is a number from 1 to
* 65535 without leading zeros;
* - no "." or ".." segment in its path, written or with %2e;
* - the scheme https, with a host of labels of 1 to 63 letters, digits and
* hyphens that neither start nor end with a hyphen, or a public IP
* address; a host whose last label is numeric or starts with 0x in either
* case only as a public IPv4 address in dotted decimal without leading
* zeros; and no name of one label or that only a machine or a local
* network resolves, in either case;
* - or the scheme ipfs, with a CID v1 of at most 128 characters in
* canonical base32.
*
* The address is read as it is written, with no decoding. Throws a
* LocatorError with the text of Go.
*/
export function checkURI(uri: string): void {
const b = wtf8(uri);
if (b.length === 0 || b.length > MAX_URI_LEN) throw fail(`an address of ${b.length} bytes, not 1 to ${MAX_URI_LEN}`);
for (let i = 0; i < b.length; i++) {
const c = b[i]!;
if (c === 0x25) {
if (i + 2 >= b.length || !isHex(b[i + 1]!) || !isHex(b[i + 2]!)) {
throw fail('an address with a percent sign not followed by two hexadecimal digits');
}
} else if (!uriChar(c)) {
throw fail(`an address with the character ${quoteRune(c)}, which RFC 3986 does not allow`);
}
}
// From here on uri is ASCII: each code unit is a byte.
const [scheme, host] = splitAuthority(uri);
if (dotSegment(uri)) throw fail('an address with a "." or ".." segment in its path');
if (scheme === 'https') return checkHost(host);
if (scheme === 'ipfs') {
if (!isCIDv1(host)) throw fail('an ipfs address without a CID v1 in base32');
return;
}
throw fail(`the scheme ${goQuote(scheme)}: only https and ipfs`);
}
const URI_PUNCTUATION = "-._~:/?#[]@!$&'()*+,;=";
// Whether c may appear in a URI of RFC 3986, a percent sign apart: the
// unreserved characters, gen-delims and sub-delims.
function uriChar(c: number): boolean {
return (c >= 0x61 && c <= 0x7a) || (c >= 0x41 && c <= 0x5a) || (c >= 0x30 && c <= 0x39) || URI_PUNCTUATION.includes(String.fromCharCode(c));
}
const isHex = (c: number): boolean => (c >= 0x30 && c <= 0x39) || (c >= 0x61 && c <= 0x66) || (c >= 0x41 && c <= 0x46);
const RUNE_ESCAPES: ReadonlyMap<number, string> = new Map([
[0x07, '\\a'],
[0x08, '\\b'],
[0x0c, '\\f'],
[0x0a, '\\n'],
[0x0d, '\\r'],
[0x09, '\\t'],
[0x0b, '\\v'],
]);
// Go's %q of a byte: the rune of its value in single quotes, as
// strconv.QuoteRune writes it. The first byte above 0x7f of a string is a
// leading byte, 0xc2 to 0xf4, whose rune Go prints: a string never has a
// byte of continuation alone, so the escape \u of QuoteRune is never needed.
function quoteRune(c: number): string {
let body: string;
if (c === 0x27 || c === 0x5c) body = `\\${String.fromCharCode(c)}`;
else if (c < 0x80 ? c >= 0x20 && c < 0x7f : goIsPrintNonASCII(c)) body = String.fromCharCode(c);
else if (RUNE_ESCAPES.has(c)) body = RUNE_ESCAPES.get(c)!;
else body = `\\x${c.toString(16).padStart(2, '0')}`;
return `'${body}'`;
}
function indexAny(s: string, chars: string): number {
for (let i = 0; i < s.length; i++) if (chars.includes(s[i]!)) return i;
return -1;
}
// Whether the path of uri has a segment "." or "..", written or
// percent-encoded: a client or a gateway that resolves it would ask for
// something other than what the address shows, as another CID behind an
// ipfs address. splitAuthority has found the "://" of uri.
function dotSegment(uri: string): boolean {
const rest = uri.slice(uri.indexOf('://') + 3);
const j = rest.indexOf('/');
if (j < 0) return false;
let path = rest.slice(j);
const k = indexAny(path, '?#');
if (k >= 0) path = path.slice(0, k);
return path.split('/').some((s) => {
const t = s.replaceAll('%2e', '.').replaceAll('%2E', '.');
return t === '.' || t === '..';
});
}
// The scheme and the raw host of an address, without decoding anything: a
// percent sign in the authority, userinfo and a malformed port are refused,
// so that the host a reader shows is the host an HTTP client would use.
function splitAuthority(uri: string): [string, string] {
const i = uri.indexOf('://');
if (i <= 0) throw fail('an address without a scheme and ://');
const scheme = uri.slice(0, i);
const rest = uri.slice(i + 3);
const j = indexAny(rest, '/?#');
const authority = j >= 0 ? rest.slice(0, j) : rest;
if (indexAny(authority, '%@\\') >= 0) throw fail('an address with a percent sign, userinfo or a backslash in its authority');
let host = authority;
if (scheme === 'https') {
if (authority.startsWith('[')) {
const end = authority.indexOf(']');
if (end < 0) throw fail('an address with an unclosed IPv6 literal');
host = authority.slice(0, end + 1);
const tail = authority.slice(end + 1);
if (tail !== '') checkPort(tail);
} else {
const k = authority.indexOf(':');
if (k >= 0) {
host = authority.slice(0, k);
checkPort(`:${authority.slice(k + 1)}`);
}
}
}
return [scheme, host];
}
// The port of an authority, its ':' included: a number from 1 to 65535
// without leading zeros (spec v0.12, §44.1), so that a port is written in one
// way only.
function checkPort(s: string): void {
if (s.length < 2 || s[0] !== ':' || s.length > 6) throw fail('an address with a malformed port');
let n = 0;
for (let i = 1; i < s.length; i++) {
const c = s.charCodeAt(i);
if (c < 0x30 || c > 0x39) throw fail('an address with a malformed port');
n = n * 10 + c - 0x30;
}
if (n < 1 || n > 65535) throw fail('an address with a port outside 1 to 65535');
if (s[1] === '0') throw fail('an address with a port written with a leading zero');
}
// Go's strings.Trim(s, "[]"): every [ and ] at either end.
function trimBrackets(s: string): string {
let a = 0;
let b = s.length;
while (a < b && (s[a] === '[' || s[a] === ']')) a++;
while (b > a && (s[b - 1] === '[' || s[b - 1] === ']')) b--;
return s.slice(a, b);
}
// Lower case of an ASCII string, as strings.ToLower of Go on ASCII.
const lowerAscii = (s: string): string => s.replace(/[A-Z]/g, (c) => String.fromCharCode(c.charCodeAt(0) + 0x20));
// The host of an https address: a name of labels of 1 to 63 letters, digits
// and hyphens, none of which starts or ends with a hyphen, or an IP literal
// that is public: the spec forbids following a redirect to the others, and
// an address that starts there would defeat the same rule (§44.1). A name
// whose last label is numeric, or starts with 0x in either case, is refused
// unless it is a public IPv4 address in dotted decimal without leading
// zeros, the only form netip reads: some clients read the others, 0x7f000001
// or 0177.0.0.1, as an IPv4 address too. So are the names that only resolve
// inside a machine or a local network, in either case.
function checkHost(host: string): void {
if (host === '') throw fail('an https address without a host');
if (host.startsWith('[')) {
const a = parseIpAddress(trimBrackets(host));
if (a === undefined || !isIpv6(a) || a.zone !== '' || !isPublicIp(a)) throw fail('an https address with an IPv6 literal that is not public');
return;
}
const labels = host.split('.');
for (const l of labels) {
if (l === '' || l.length > 63 || l[0] === '-' || l[l.length - 1] === '-') throw fail('an https address with a malformed host');
if (!/^[A-Za-z0-9-]*$/.test(l)) throw fail('an https address whose host is not letters, digits and hyphens: write its punycode form');
}
const last = labels[labels.length - 1]!;
if (/^[0-9]+$/.test(last) || lowerAscii(last).startsWith('0x')) {
const a = parseIpAddress(host);
if (a === undefined || !isIpv4(a) || !isPublicIp(a)) throw fail('an https address with a numeric host that is not a public IPv4 address');
return;
}
if (labels.length === 1 || localName(lowerAscii(host))) {
throw fail('an https address with a name that only a machine or a local network resolves');
}
}
const LOCAL_NAMES = ['localhost', 'local', 'home.arpa', 'internal', 'invalid', 'test', 'example', 'onion'];
// Whether name, in lower case, is one of the special-use names that never
// resolve on the public Internet, or below one of them: localhost (RFC
// 6761), .local (RFC 6762), .home.arpa (RFC 8375), .internal, .invalid,
// .test, .example and .onion (RFC 7686).
const localName = (name: string): boolean => LOCAL_NAMES.some((s) => name === s || name.endsWith(`.${s}`));
// Whether s is a CID v1 of at most 128 characters in canonical base32, which
// starts with 'b' (spec v0.12, §44.1): the alphabet in lower case, without
// padding, the bits left over set to zero, and decoded, minimal varints of
// at most 9 bytes, the version 1, a content codec and a multihash with a
// digest of at least one byte and of the length it gives, with nothing after
// it. As in Go, the count of the bits left over is not checked.
function isCIDv1(s: string): boolean {
if (s.length < 2 || s.length > 128 || s[0] !== 'b') return false;
const out: number[] = [];
let acc = 0;
let bits = 0;
for (let i = 1; i < s.length; i++) {
const c = s.charCodeAt(i);
let v: number;
if (c >= 0x61 && c <= 0x7a) v = c - 0x61;
else if (c >= 0x32 && c <= 0x37) v = c - 0x32 + 26;
else return false;
// acc holds at most 7 bits before the shift: 12 bits at most.
acc = (acc << 5) | v;
bits += 5;
if (bits >= 8) {
bits -= 8;
out.push(acc >> bits);
acc &= (1 << bits) - 1;
}
}
// The bits of the last character beyond a byte are zero.
if (acc !== 0) return false;
const version = uvarint(out, 0);
if (version === undefined || version[0] !== 1n) return false;
const codec = uvarint(out, version[1]);
if (codec === undefined) return false;
const hash = uvarint(out, codec[1]);
if (hash === undefined) return false;
const n = uvarint(out, hash[1]);
return n !== undefined && n[0] > 0n && BigInt(out.length - n[1]) === n[0];
}
// An unsigned varint of multiformats, minimal and of at most 9 bytes, from
// b[at]: its value, exact as a bigint up to 63 bits, and the offset after it.
function uvarint(b: readonly number[], at: number): [bigint, number] | undefined {
let v = 0n;
for (let i = 0; at + i < b.length && i < 9; i++) {
const x = b[at + i]!;
v |= BigInt(x & 0x7f) << BigInt(7 * i);
if ((x & 0x80) === 0) {
if (i > 0 && x === 0) return undefined;
return [v, at + i + 1];
}
}
return undefined;
}
/**
* Checks the IP address `ip`, the 4 bytes of an IPv4 address or the 16 of an
* IPv6 address, to which the name of an https address resolved: a reader
* connects only to a public one, and checks it on every connection,
* redirections included (spec §44.1). Public is what an IP literal of an
* address must be, as publicIP of Go: an IPv4 address outside the blocks of
* §44.1, or an IPv6 address of 2000::/3 outside 2001::/23, 2001:db8::/32,
* 2002::/16 and 3fff::/20. An IPv6 address that holds an IPv4 one is not,
* IPv4-mapped and NAT64 (64:ff9b::/96) included: an IPv4 address is checked
* as its 4 bytes.
*
* Throws a LocatorError for an address that is not public, with the text of
* datekeys-dart's checkResolvedIp, and a RangeError for any other length. Go
* has no such function: a reader of the reference does not download.
*/
export function checkResolvedIp(ip: Uint8Array): void {
const a = ipFromBytes(ip);
if (!isPublicIp(a)) throw fail(`an https address whose name resolves to ${ipString(a)}, an IP address that is not public`);
}
// ---------------------------------------------------------------------------
// The locator
/**
* The plaintext of the sealed locator (spec §44.1), as Locator of Go. The
* keys and digests are 32 bytes and the size and the offsets are integers
* from 0, as the types of Go make them; a value of another type is a
* TypeError or a RangeError of the caller.
*/
export interface Locator {
/** Key 0, where the rest of the envelope is, in their order. */
readonly addresses: readonly Address[];
/** Key 1, I_SOBRE, the raw X25519 identity of the envelope. SECRET: see wipeLocator. */
readonly envelopeKey: Uint8Array;
/** Key 2, the age header of the envelope, MAC line included. */
readonly envelopeHeader: Uint8Array;
/** Key 3, the SHA-256 of the rest. */
readonly restDigest: Uint8Array;
/** Key 4, the length of the rest, in bytes. */
readonly restSize: number;
/** Key 5, the SHA-256 of the .dkc (spec §43). */
readonly capsuleDigest: Uint8Array;
}
/** Clears the envelope key of `l`: the envelope cannot be opened afterwards. */
export function wipeLocator(l: Locator): void {
l.envelopeKey.fill(0);
}
/**
* The addresses that meet the rules of spec §44.1, in their order, as Usable
* of Go. A reader rejects each address that breaks them and uses the others:
* a locator whose addresses are all rejected has nothing to download.
*/
export function usableAddresses(l: Locator): Address[] {
return l.addresses.filter((a) => accepts(a.uri));
}
const isCount = (n: unknown): n is number => typeof n === 'number' && Number.isInteger(n) && n >= 0;
// The types of Go: what Go's compiler would refuse.
function checkTypes(l: Locator): void {
const fixed = (b: unknown, name: string): void => {
if (!(b instanceof Uint8Array)) throw new TypeError(`locator: ${name} is not a Uint8Array`);
if (b.length !== DIGEST_LEN) throw new RangeError(`locator: ${name} is ${b.length} bytes, not ${DIGEST_LEN}`);
};
if (!Array.isArray(l.addresses)) throw new TypeError('locator: addresses is not an array');
for (const a of l.addresses) {
if (typeof a?.uri !== 'string') throw new TypeError('locator: an address without a string uri');
if (!isCount(a.offset)) throw new RangeError(`locator: an offset of ${String(a.offset)}, not an integer from 0`);
}
fixed(l.envelopeKey, 'envelopeKey');
if (!(l.envelopeHeader instanceof Uint8Array)) throw new TypeError('locator: envelopeHeader is not a Uint8Array');
fixed(l.restDigest, 'restDigest');
if (!isCount(l.restSize)) throw new RangeError(`locator: a rest size of ${String(l.restSize)}, not an integer from 0`);
fixed(l.capsuleDigest, 'capsuleDigest');
}
// validateForm of Go: the form that a reader requires of the whole locator;
// a broken address makes only that address unusable (usableAddresses).
function validateForm(l: Locator): void {
if (l.addresses.length < 1 || l.addresses.length > MAX_ADDRESSES) throw fail(`${l.addresses.length} addresses, not 1 to ${MAX_ADDRESSES}`);
for (const a of l.addresses) {
const n = wtf8(a.uri).length;
if (n === 0 || n > MAX_URI_LEN) throw fail(`an address of ${n} bytes, not 1 to ${MAX_URI_LEN}`);
}
const n = l.envelopeHeader.length;
if (n < 1 || n > MAX_HEADER_LEN) throw fail(`an envelope header of ${n} bytes, not 1 to ${MAX_HEADER_LEN}`);
if (l.restSize > MAX_SAFE_UINT) throw fail('a rest larger than 2^53 - 1 bytes');
for (const a of l.addresses) if (a.offset > MAX_SAFE_UINT) throw fail('an offset larger than 2^53 - 1');
}
// The map; pad < 0 leaves key 6 out.
function encode(e: Encoder, l: Locator, pad: number): void {
e.map(pad >= 0 ? 7 : 6);
e.uint(0);
e.array(l.addresses.length);
for (const a of l.addresses) {
e.map(a.offset === 0 ? 1 : 2);
e.uint(0);
e.text(a.uri);
if (a.offset !== 0) {
e.uint(1);
e.uint(a.offset);
}
}
e.uint(1);
e.bstr(l.envelopeKey);
e.uint(2);
e.bstr(l.envelopeHeader);
e.uint(3);
e.bstr(l.restDigest);
e.uint(4);
e.uint(l.restSize);
e.uint(5);
e.bstr(l.capsuleDigest);
if (pad >= 0) {
e.uint(6);
e.bstr(new Uint8Array(pad));
}
}
// The length of the head of a byte string of n bytes, for the n of key 6,
// which is below two blocks.
function bstrHeadLen(n: number): number {
if (n < 24) return 1;
if (n < 256) return 2;
return 3;
}
// padFor of Go: the length of key 6 that makes the plaintext measure the
// least multiple of BLOCK that holds it, or -1 when n0, the length without
// key 6, already is one. When no length of key 6 gives a given multiple, as
// happens at the boundaries of the CBOR length, it takes the next one. The
// division truncates toward zero, as Go's.
function padFor(n0: number): number {
if (n0 % BLOCK === 0) return -1;
for (let total = (Math.trunc(n0 / BLOCK) + 1) * BLOCK; ; total += BLOCK) {
for (let pad = 1; pad <= total - n0; pad++) {
if (n0 + 1 + bstrHeadLen(pad) + pad === total) return pad;
}
}
}
/**
* The length of the plaintext of a locator whose CBOR without key 6
* measures `base` bytes, as PlaintextLength of Go: `base` when it already is
* a multiple of 4096, and otherwise the least multiple that key 6 can fill
* exactly. Key 6 takes at least 3 bytes, and the head of its byte string
* grows at 24 and at 256 bytes: a base that lacks 1, 2, 26 or 259 bytes for
* a multiple takes the next one (spec §44.1).
*/
export function plaintextLength(base: number): number {
if (!Number.isSafeInteger(base)) throw new RangeError(`locator: a base of ${base}, not an integer`);
const pad = padFor(base);
return pad < 0 ? base : base + 1 + bstrHeadLen(pad) + pad;
}
/**
* The plaintext of the locator, as Marshal of Go: CBOR with the profile of
* spec §58, completed with zeros in key 6 up to the least multiple of 4096
* bytes that key 6 can fill, so that its length does not tell how many
* addresses there are. It checks the form of the locator, 1 to 8 addresses
* of 1 to 1024 bytes, a header of 1 to 1024 bytes and a size and offsets of
* at most 2^53 - 1, and each address with checkURI: a writer never writes
* one that a reader rejects. Throws a LocatorError. The plaintext holds
* I_SOBRE: the caller wipes it.
*/
export function marshalLocator(l: Locator): Uint8Array {
checkTypes(l);
validateForm(l);
for (const a of l.addresses) checkURI(a.uri);
return marshalChecked(l);
}
// marshal of Go: Marshal once the locator is checked.
function marshalChecked(l: Locator): Uint8Array {
const e = new Encoder();
encode(e, l, -1);
const base = e.out();
e.wipe();
const pad = padFor(base.length);
if (pad < 0) return base;
// It holds I_SOBRE.
base.fill(0);
const p = new Encoder();
encode(p, l, pad);
const out = p.out();
p.wipe();
/* v8 ignore next 4 -- @preserve: padFor always reaches a multiple */
if (out.length % BLOCK !== 0) {
out.fill(0);
throw fail(`internal error: ${out.length} bytes of plaintext`);
}
return out;
}
// Something of the decoding of a locator that Go reports without a code.
class Plain extends Error {}
/**
* Reads the plaintext of a locator, checking its profile and the length
* that marshalLocator gives, as Unmarshal of Go. Its errors carry no
* normative code: a locator that does not read is unusable (spec §44.1,
* §57). An address that breaks the rules of §44.1 is kept, and
* usableAddresses leaves it out. Throws a LocatorError.
*/
export function unmarshalLocator(b: Uint8Array): Locator {
const addresses: Address[] = [];
const envelopeKey = new Uint8Array(DIGEST_LEN);
let envelopeHeader: Uint8Array = new Uint8Array(0);
const restDigest = new Uint8Array(DIGEST_LEN);
let restSize = 0;
const capsuleDigest = new Uint8Array(DIGEST_LEN);
let pad = -1;
const l = (): Locator => ({ addresses, envelopeKey, envelopeHeader, restDigest, restSize, capsuleDigest });
const copyFixed = (d: Decoder, dst: Uint8Array): void => {
const v = d.bstr(dst.length, dst.length);
dst.set(v);
v.fill(0);
};
try {
unmarshal(
b,
(d: Decoder) => {
const pairs = d.map(7);
let seen = 0;
for (let i = 0; i < pairs; i++) {
const k = d.key();
switch (k) {
case 0:
withContext('key 0', () => decodeAddresses(d, addresses));
break;
case 1:
withContext('key 1', () => copyFixed(d, envelopeKey));
break;
case 2:
envelopeHeader = withContext('key 2', () => d.bstr(1, MAX_HEADER_LEN));
break;
case 3:
withContext('key 3', () => copyFixed(d, restDigest));
break;
case 4:
restSize = withContext('key 4', () => d.uint(MAX_SAFE_UINT));
break;
case 5:
withContext('key 5', () => copyFixed(d, capsuleDigest));
break;
case 6: {
const z = withContext('key 6', () => d.bstr(1, 1 << 20));
if (z.some((x) => x !== 0)) throw new Plain('the padding is not zeros');
pad = z.length;
break;
}
default:
throw undefinedKey(`key ${k} is not defined`);
}
seen |= 1 << k;
}
if ((seen & 0x3f) !== 0x3f) throw undefinedKey('a key from 0 to 5 is missing');
d.endMap();
},
(e: Encoder) => encode(e, l(), pad),
);
} catch (err) {
envelopeKey.fill(0);
/* v8 ignore next -- @preserve: the decoder throws nothing else */
if (!(err instanceof DateKeysError || err instanceof Plain)) throw err;
throw fail(err.message);
}
const out = l();
try {
validateForm(out);
// The length is the one Marshal gives: nothing else is canonical.
const want = marshalChecked(out);
let same = want.length === b.length;
for (let i = 0; same && i < b.length; i++) same = want[i] === b[i];
want.fill(0);
if (!same) throw fail(`the plaintext is not ${BLOCK} or the least multiple of ${BLOCK} that holds it`);
} catch (err) {
envelopeKey.fill(0);
throw err;
}
return out;
}
function decodeAddresses(d: Decoder, out: Address[]): void {
const n = d.array(MAX_ADDRESSES);
for (let i = 0; i < n; i++) {
const pairs = d.map(2);
let uri = '';
let offset = 0;
let seen = 0;
for (let j = 0; j < pairs; j++) {
const k = d.key();
switch (k) {
case 0:
uri = d.text(MAX_URI_LEN);
break;
case 1:
offset = d.uint(MAX_SAFE_UINT);
if (offset === 0) throw undefinedKey('an offset of 0 is written by leaving it out');
break;
default:
throw undefinedKey(`address key ${k} is not defined`);
}
seen |= 1 << k;
}
if ((seen & 1) === 0) throw undefinedKey('an address without URI');
d.endMap();
out.push({ uri, offset });
}
}
// ---------------------------------------------------------------------------
// The rest and its host
/**
* The rest of the envelope that `l` describes from the resource `host`,
* which starts at the `offset` of its address: only restSize bytes are
* read, whatever follows (spec §44.1), as RestIn of Go. Throws a LocatorError
* when the resource is shorter.
*/
export function restIn(l: Locator, host: Uint8Array, offset: number): Uint8Array {
if (!isCount(offset)) throw new RangeError(`locator: an offset of ${offset}, not an integer from 0`);
if (offset > host.length || l.restSize > host.length - offset) {
throw fail(`the resource has ${host.length} bytes, and the rest is ${l.restSize} from ${offset}`);
}
return host.slice(offset, offset + l.restSize);
}
/**
* Appends `rest` to `host`, a file of any kind, as Hide of Go: the result and
* the offset where the rest starts, which is what an address says (spec
* §44.1). It is hiding, not steganography: whoever analyses the host sees
* that it has extra bytes, but not what they are. Only a store that keeps
* the file byte by byte keeps it: a social network or a messaging app
* recompress or strip what is left over.
*/
export function hide(host: Uint8Array, rest: Uint8Array): { file: Uint8Array; offset: number } {
const file = new Uint8Array(host.length + rest.length);
file.set(host);
file.set(rest, host.length);
return { file, offset: host.length };
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,185 @@
// Helpers of the tests of the locator: the vectors that scripts/
// locator-go-vectors.go and locator-seal-go-vectors.go wrote with Go, their
// edits and their compact addresses, and what a reader reads of a locator,
// as those generators write it.
import { readFileSync } from 'node:fs';
import { fromHex, toHex } from '../bytes.ts';
import { sha256Hasher } from '../digest.ts';
import { addressHost, type Locator, usableAddresses } from '../locator.ts';
import { quicknet, type Profile } from '../profile.ts';
import type { Release } from '../release.ts';
export type Json = Record<string, unknown>;
export type Row = unknown[];
/** A JSON file of src/lib/dkc/testing. */
export const readVectors = (name: string): Json => JSON.parse(readFileSync(new URL(`./${name}`, import.meta.url), 'utf8')) as Json;
/** The list `key` of `v`, each item a list. */
export const rows = (v: Json, key: string): Row[] => v[key] as Row[];
/** The text `i` of the table of `v`: "" for no error. */
export const textOf = (v: Json, i: unknown): string => (v.texts as string[])[i as number]!;
/** An address as the generators write it: the string, or [before, byte, count, after] for one with a run of count copies of a byte. */
export function uriOf(x: unknown): string {
if (typeof x === 'string') return x;
const [before, byte, count, after] = x as [string, string, number, string];
return before + byte.repeat(count) + after;
}
/**
* The edits of `base` applied in one pass: each [at, delete, insert]
* replaces delete bytes at the offset at of the base with the bytes of the
* hexadecimal insert, and [at, delete, byte, count] with count copies of the
* byte. The offsets are those of the base, in order.
*/
export function applyEdits(base: Uint8Array, edits: unknown): Uint8Array {
const parts: Uint8Array[] = [];
let pos = 0;
for (const e of edits as Row[]) {
const at = e[0] as number;
const del = e[1] as number;
if (at < pos || at + del > base.length) throw new Error(`edit at ${at} out of order or beyond the base`);
parts.push(base.subarray(pos, at));
const insert = fromHex(e[2] as string);
const repeat = e.length > 3 ? (e[3] as number) : 1;
for (let i = 0; i < repeat; i++) parts.push(insert);
pos = at + del;
}
parts.push(base.subarray(pos));
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
let at = 0;
for (const p of parts) {
out.set(p, at);
at += p.length;
}
return out;
}
/** The lower-case hexadecimal SHA-256 of `b`. */
export function sha256Hex(b: Uint8Array): string {
const h = sha256Hasher();
h.update(b);
return toHex(h.digest());
}
/**
* What a reader reads of `l`, as the generators write it: [[uri, offset,
* host, usable]...], the key, the SHA-256 of the header, the digest of the
* rest, its size and capsule_digest, with each address in full.
*/
export function summaryOf(l: Locator): Row {
const usable = usableAddresses(l);
return [
l.addresses.map((a) => [a.uri, a.offset, addressHost(a), usable.includes(a)]),
toHex(l.envelopeKey),
sha256Hex(l.envelopeHeader),
toHex(l.restDigest),
l.restSize,
toHex(l.capsuleDigest),
];
}
/** The summary `s` of a generator, with each address in full. */
export function expandSummary(s: unknown): Row {
const l = s as Row;
return [(l[0] as Row[]).map((a) => [uriOf(a[0]), a[1], uriOf(a[2]), a[3]]), ...l.slice(1)];
}
/** The release of `round` of the vectors `v`, public data of drand. */
export function releaseOf(v: Json, round: number): Release {
return { round, signature: fromHex((v.releases as Record<string, string>)[String(round)]!) };
}
/** The pinned Quicknet profile, edited as the generator names the edit. */
export function profileOf(edit: string): Profile {
const p = quicknet();
switch (edit) {
case '':
return p;
case 'key not on the curve': {
const k = p.publicKey.slice();
k[k.length - 1]! ^= 1;
return { ...p, publicKey: k };
}
case 'key at infinity':
return { ...p, publicKey: Uint8Array.of(0xc0, ...new Uint8Array(95)) };
case 'key of another network':
// The generator of G2, compressed.
return {
...p,
publicKey: fromHex(
'93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8',
),
};
case 'chain hash of another network': {
const h = p.chainHash.slice();
h[0]! ^= 1;
return { ...p, chainHash: h };
}
}
throw new Error(edit);
}
/** The release that an open case names: a round, or an edit of the release of the case. */
export function openRelease(v: Json, round: number, release: unknown): Release {
if (typeof release === 'number') return releaseOf(v, release);
const r = releaseOf(v, round);
switch (release) {
case 'flipped': {
const s = r.signature.slice();
s[s.length - 1]! ^= 1;
return { round, signature: s };
}
case 'short':
return { round, signature: r.signature.subarray(0, 47) };
case 'other round':
return { round, signature: releaseOf(v, 1001).signature };
}
throw new Error(String(release));
}
/** The text of the error that `body` throws, "" when it returns. */
export function errorText(body: () => unknown): string {
try {
body();
return '';
} catch (err) {
return (err as Error).message;
}
}
/**
* The string of the bytes `b`, UTF-8 with lone surrogates written as the
* three bytes of their code point (WTF-8), as the vectors write a string of
* JavaScript with a lone surrogate.
*/
export function fromWtf8(b: Uint8Array): string {
let out = '';
for (let i = 0; i < b.length; ) {
const c = b[i]!;
if (c < 0x80) {
out += String.fromCharCode(c);
i++;
} else if (c >= 0xe0 && c < 0xf0) {
out += String.fromCharCode(((c & 0x0f) << 12) | ((b[i + 1]! & 0x3f) << 6) | (b[i + 2]! & 0x3f));
i += 3;
} else if (c >= 0xc0 && c < 0xe0) {
out += String.fromCharCode(((c & 0x1f) << 6) | (b[i + 1]! & 0x3f));
i += 2;
} else {
out += String.fromCodePoint(((c & 0x07) << 18) | ((b[i + 1]! & 0x3f) << 12) | ((b[i + 2]! & 0x3f) << 6) | (b[i + 3]! & 0x3f));
i += 4;
}
}
return out;
}
/** The plaintext of a .dkc of n bytes in the vectors of the sealing: (31·i + 7) mod 256 as byte i. */
export function pattern(n: number): Uint8Array {
const b = new Uint8Array(n);
for (let i = 0; i < n; i++) b[i] = (31 * i + 7) & 0xff;
return b;
}
Loading…
Cancel
Save

Powered by TurnKey Linux.