Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
/// 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 draft v0.12, with the
|
|
|
|
|
/// same checks in the same order, the same codes and the same texts:
|
|
|
|
|
/// - the data of the extension ([CapsuleInfo], [parseCapsuleInfo]), 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
|
|
|
|
|
/// ([openLocator]), whose plaintext says where the capsule is: the
|
|
|
|
|
/// addresses of the rest, and the key, the header and the digests of the
|
|
|
|
|
/// envelope;
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
/// - 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 ([hideRest], [Locator.restIn],
|
|
|
|
|
/// [Locator.openEnvelope]).
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
///
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
/// It downloads nothing. An application fetches the rest only when the
|
|
|
|
|
/// person asks, after showing her the host or the CID of the address
|
|
|
|
|
/// ([LocatorAddress.host]), and only from an address that [checkAddressUri]
|
|
|
|
|
/// accepts ([Locator.usable]); it follows no redirect to an address that
|
|
|
|
|
/// [checkAddressUri] 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 [Locator.openEnvelope], which checks
|
|
|
|
|
/// their SHA-256, decrypts the .dkc and checks its SHA-256 (spec §44.1).
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
///
|
|
|
|
|
/// Sealing a locator, Seal of Go, and making an envelope, NewEnvelope, are
|
|
|
|
|
/// in locator_seal.dart, over the writer of age: [splitEnvelope] is the
|
|
|
|
|
/// part of NewEnvelope after the encryption, the split of the age file of
|
|
|
|
|
/// the envelope.
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
///
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
/// 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 [LocatorException]
|
|
|
|
|
/// 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).
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
library;
|
|
|
|
|
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
|
|
import 'age.dart';
|
|
|
|
|
import 'agewrap.dart';
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'bytes.dart';
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'cbor.dart';
|
|
|
|
|
import 'chacha20poly1305.dart';
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'datekey.dart';
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'errors.dart';
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'extension.dart';
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'ipaddr.dart';
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'note.dart';
|
|
|
|
|
import 'profile.dart';
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
import 'release.dart';
|
|
|
|
|
import 'sha256.dart';
|
|
|
|
|
import 'tlock.dart';
|
|
|
|
|
|
|
|
|
|
/// The most addresses of a locator (spec §44.1).
|
|
|
|
|
const maxLocatorAddresses = 8;
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
|
|
|
|
|
/// The longest address, in bytes (spec §44.1).
|
|
|
|
|
const maxAddressUriLen = 1024;
|
|
|
|
|
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
/// The longest header of an envelope, in bytes (spec §44.1).
|
|
|
|
|
const maxEnvelopeHeaderLen = 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).
|
|
|
|
|
const locatorBlock = 4096;
|
|
|
|
|
|
|
|
|
|
// The largest sealed locator that a reader decrypts, and the most plaintext
|
|
|
|
|
// it reads of one: Go's maxSealed.
|
|
|
|
|
const _maxSealed = 1 << 20;
|
|
|
|
|
|
|
|
|
|
const _digestSize = 32;
|
|
|
|
|
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
/// 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`.
|
|
|
|
|
final class LocatorException implements Exception {
|
|
|
|
|
/// An exception with Go's [message].
|
|
|
|
|
const LocatorException(this.message);
|
|
|
|
|
|
|
|
|
|
/// The text of Go's err.Error().
|
|
|
|
|
final String message;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String toString() => message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LocatorException _fail(String detail) => LocatorException('locator: $detail');
|
|
|
|
|
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
// A key or a field that the schema does not define or that is missing, as
|
|
|
|
|
// the decoders of Go write it.
|
|
|
|
|
DateKeysException _undefined(String what) =>
|
|
|
|
|
DateKeysException(ErrorCode.nonCanonicalCbor, what);
|
|
|
|
|
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// The data of datekeys.capsule
|
|
|
|
|
|
|
|
|
|
/// The data of the extension datekeys.capsule (spec §44.1), as Info of Go.
|
|
|
|
|
final class CapsuleInfo {
|
|
|
|
|
/// The data with the copy of the public note [note], `''` for none, the
|
|
|
|
|
/// DateKey [dateKey] of the capsule, and the sealed locator [sealed],
|
|
|
|
|
/// which it copies, null for none.
|
|
|
|
|
CapsuleInfo({this.note = '', required this.dateKey, List<int>? sealed})
|
|
|
|
|
: sealed = sealed == null ? null : Uint8List.fromList(sealed);
|
|
|
|
|
|
|
|
|
|
/// Key 0, the copy of the public note of the capsule, `''` for none.
|
|
|
|
|
final String note;
|
|
|
|
|
|
|
|
|
|
/// Key 1, the DateKey of the capsule: it says when it opens.
|
|
|
|
|
final DateKey dateKey;
|
|
|
|
|
|
|
|
|
|
/// Key 2, the age file of the locator, sealed with tlock for the round of
|
|
|
|
|
/// [dateKey], or null for none.
|
|
|
|
|
final Uint8List? sealed;
|
|
|
|
|
|
|
|
|
|
/// The extension for the noncritical array of a .dkk, as 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 ([parseCapsuleInfo]), which also ties
|
|
|
|
|
/// the locator to the round of the DateKey (spec §72). The rules of the
|
|
|
|
|
/// note throw their [DateKeysException], ERR_EXTENSION_DATA_INVALID; the
|
|
|
|
|
/// others a [LocatorException].
|
|
|
|
|
Extension toExtension() {
|
|
|
|
|
DateKey? d;
|
|
|
|
|
try {
|
|
|
|
|
d = parseDateKey(compactDateKey(dateKey));
|
|
|
|
|
} on DateKeysException {
|
|
|
|
|
d = null;
|
|
|
|
|
}
|
|
|
|
|
if (d == null || d != dateKey) {
|
|
|
|
|
throw _fail('Info.DateKey is not a canonical DateKey');
|
|
|
|
|
}
|
|
|
|
|
// A writer writes the DateKey of a capsule it wrote: of a profile that
|
|
|
|
|
// this library pins, whose chain the check of the locator then compares.
|
|
|
|
|
if (d.profileId != quicknetId) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'Info.DateKey is of the profile ${goQuote(utf8Bytes(d.profileId))}, '
|
|
|
|
|
'which this module does not pin',
|
|
|
|
|
);
|
|
|
|
|
}
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
final s = sealed;
|
|
|
|
|
if (s != null && (s.isEmpty || s.length > _maxSealed)) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'a sealed locator of ${s.length} bytes, not 1 to $_maxSealed',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (note.isNotEmpty) checkNote(note);
|
|
|
|
|
final e = CborEncoder();
|
|
|
|
|
_encodeInfo(e, note, compactDateKey(dateKey), s);
|
|
|
|
|
final x = newExtension(capsuleExtensionId, 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 {
|
|
|
|
|
parseCapsuleInfo(x);
|
|
|
|
|
} on DateKeysException catch (err) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'self-check: a reader rejects this extension: ${err.message}',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return x;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The locator of the extension, opened with [release], the release of the
|
|
|
|
|
/// round of its own DateKey, in the profile that the DateKey names, as
|
|
|
|
|
/// OpenLocator of Go: a locator for another round or another chain does
|
|
|
|
|
/// not open, and is unusable (spec §44.1). [registry] holds the pinned
|
|
|
|
|
/// profiles, the default registry, Quicknet, when null, as in the opening
|
|
|
|
|
/// of a capsule. Throws a [LocatorException], also when the extension has
|
|
|
|
|
/// no locator.
|
|
|
|
|
Locator openLocator(Release release, {ProfileRegistry? registry}) {
|
|
|
|
|
final s = sealed;
|
|
|
|
|
if (s == null) throw _fail('the extension has no locator');
|
|
|
|
|
final p = (registry ?? defaultRegistry()).lookup(dateKey.profileId);
|
|
|
|
|
if (p == null) throw _fail('the profile of the DateKey is not pinned');
|
|
|
|
|
return _open(p, dateKey.round, release, s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _encodeInfo(CborEncoder e, String note, String dk, Uint8List? sealed) {
|
|
|
|
|
var pairs = 1;
|
|
|
|
|
if (note.isNotEmpty) pairs++;
|
|
|
|
|
if (sealed != null) pairs++;
|
|
|
|
|
e.map(pairs);
|
|
|
|
|
if (note.isNotEmpty) {
|
|
|
|
|
e
|
|
|
|
|
..uint(0)
|
|
|
|
|
..text(note);
|
|
|
|
|
}
|
|
|
|
|
e
|
|
|
|
|
..uint(1)
|
|
|
|
|
..text(dk);
|
|
|
|
|
if (sealed != null) {
|
|
|
|
|
e
|
|
|
|
|
..uint(2)
|
|
|
|
|
..bstr(sealed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateKeysException _dataInvalid(String detail) =>
|
|
|
|
|
DateKeysException(ErrorCode.extensionDataInvalid, 'locator: $detail');
|
|
|
|
|
|
|
|
|
|
/// Reads the data of a datekeys.capsule extension [x], 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 [DateKeysException] whose only code is
|
|
|
|
|
/// ERR_EXTENSION_DATA_INVALID.
|
|
|
|
|
CapsuleInfo parseCapsuleInfo(Extension x) {
|
|
|
|
|
final data = x.data;
|
|
|
|
|
if (x.id != capsuleExtensionId || x.version != 1 || data == null) {
|
|
|
|
|
throw _dataInvalid('not datekeys.capsule version 1 with data');
|
|
|
|
|
}
|
|
|
|
|
var note = '';
|
|
|
|
|
var dk = '';
|
|
|
|
|
Uint8List? sealed;
|
|
|
|
|
try {
|
|
|
|
|
unmarshalCbor(data, (d) {
|
|
|
|
|
final pairs = d.map(3);
|
|
|
|
|
var seen = 0;
|
|
|
|
|
for (var i = 0; i < pairs; i++) {
|
|
|
|
|
final k = d.key();
|
|
|
|
|
switch (k) {
|
|
|
|
|
case 0:
|
|
|
|
|
withContext('key 0', () {
|
|
|
|
|
note = d.text(maxNoteLen);
|
|
|
|
|
checkNote(note);
|
|
|
|
|
});
|
|
|
|
|
case 1:
|
|
|
|
|
dk = withContext('key 1', () => d.text(1024));
|
|
|
|
|
case 2:
|
|
|
|
|
sealed = withContext('key 2', () => d.bstr(1, _maxSealed));
|
|
|
|
|
default:
|
|
|
|
|
throw _undefined('key $k is not defined');
|
|
|
|
|
}
|
|
|
|
|
seen |= 1 << (k as int);
|
|
|
|
|
}
|
|
|
|
|
if (seen & 2 == 0) throw _undefined('key 1 is missing');
|
|
|
|
|
d.endMap();
|
|
|
|
|
}, (e) => _encodeInfo(e, note, dk, sealed));
|
|
|
|
|
} on DateKeysException catch (err) {
|
|
|
|
|
throw _dataInvalid('datekeys.capsule: ${err.message}');
|
|
|
|
|
}
|
|
|
|
|
DateKey? d;
|
|
|
|
|
try {
|
|
|
|
|
d = parseDateKey(dk);
|
|
|
|
|
} on DateKeysException {
|
|
|
|
|
d = null;
|
|
|
|
|
}
|
|
|
|
|
if (d == null || compactDateKey(d) != dk) {
|
|
|
|
|
throw _dataInvalid('compact_datekey is not a canonical DateKey');
|
|
|
|
|
}
|
|
|
|
|
final s = sealed;
|
|
|
|
|
if (s != null) {
|
|
|
|
|
final problem = _sealedProblem(s, d);
|
|
|
|
|
if (problem != null) throw _dataInvalid(problem);
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
}
|
|
|
|
|
return CapsuleInfo(note: note, dateKey: d, sealed: s);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Whether sealed is an age file whose header holds one tlock stanza with two
|
|
|
|
|
// arguments, the first of them round.
|
|
|
|
|
// checkSealed of Go: why the sealed locator does not have the form of spec
|
|
|
|
|
// §44.1, as far as it can be checked before the date, or null. An age file
|
|
|
|
|
// with one tlock stanza, whose arguments are the round of the DateKey d in
|
|
|
|
|
// decimal and a chain hash in lower-case hexadecimal (§28.1), the chain of
|
|
|
|
|
// the profile of d when this library pins it, Quicknet; and a body that
|
|
|
|
|
// holds a plaintext of 4096 bytes or a multiple, as every locator has.
|
|
|
|
|
String? _sealedProblem(Uint8List sealed, DateKey d) {
|
|
|
|
|
List<AgeStanza>? st;
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
try {
|
|
|
|
|
st = ageStanzas(sealed);
|
|
|
|
|
} on DateKeysException {
|
|
|
|
|
st = null;
|
|
|
|
|
}
|
|
|
|
|
if (st == null ||
|
|
|
|
|
st.length != 1 ||
|
|
|
|
|
st[0].type != stanzaTlock ||
|
|
|
|
|
st[0].args.length != 2 ||
|
|
|
|
|
st[0].args[0] != '${d.round}') {
|
|
|
|
|
return 'the locator is not an age file with one tlock stanza for round '
|
|
|
|
|
'${d.round}, the one of its DateKey';
|
|
|
|
|
}
|
|
|
|
|
final chain = st[0].args[1];
|
|
|
|
|
if (!RegExp(r'^[0-9a-f]{64}$').hasMatch(chain)) {
|
|
|
|
|
return 'the tlock stanza of the locator has no chain hash in lower-case '
|
|
|
|
|
'hexadecimal';
|
|
|
|
|
}
|
|
|
|
|
if (d.profileId == quicknetId && chain != quicknetChainHash) {
|
|
|
|
|
return 'the locator is sealed for the chain $chain, not for the one of '
|
|
|
|
|
'the profile ${d.profileId} of its DateKey';
|
|
|
|
|
}
|
|
|
|
|
// ageStanzas read the header up to its MAC line, so it ends.
|
|
|
|
|
final end = _headerEnd(sealed);
|
|
|
|
|
if (end == sealed.length) {
|
|
|
|
|
return 'the locator is an age header without a body';
|
|
|
|
|
}
|
|
|
|
|
if (!_sealedBodyLength(sealed.length - end)) {
|
|
|
|
|
return 'the body of the locator is ${sealed.length - end} bytes, which no '
|
|
|
|
|
'plaintext of 4096 bytes or a multiple gives';
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sealedBodyLength of Go: whether n bytes after the age header are the body
|
|
|
|
|
// of a plaintext of 4096·k bytes, k from 1: the nonce of 16 bytes and the
|
|
|
|
|
// plaintext in chunks of 64 KiB, each with its tag of 16 bytes.
|
|
|
|
|
bool _sealedBodyLength(int n) {
|
|
|
|
|
for (var p = locatorBlock; p <= _maxSealed; p += locatorBlock) {
|
|
|
|
|
if (n == 16 + p + 16 * ((p + 65535) ~/ 65536)) return true;
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
}
|
|
|
|
|
return false;
|
Stage 7a: the data of datekeys.capsule
lib/src/locator.dart gains the data of the extension, as Info, ParseInfo
and Info.Extension of Go, with the same checks in the same order and the
same texts:
- parseCapsuleInfo: the map of spec 44.1 with the note under the rules of
spec 24.1, the canonical DateKey, and a sealed locator that is an age
file with one tlock stanza for its round; every failure is
ERR_EXTENSION_DATA_INVALID and nothing else, so that it makes the
extension unusable and never the .dkk (spec 54).
- CapsuleInfo.toExtension, which reads back what it writes (spec 72), and
CapsuleInfo.openLocator, OpenLocator of Go, with the default registry of
pinned profiles when none is given, as the opening of a capsule takes it.
- checkCapsuleData, the ValidateCapsule of Go's locator.Standard, for
StandardExtensions.
The tests run the extension cases of locator.json with their code, the
writing and the reading of the data, OpenLocator, the registry of
locator.Standard through checkCritical, checkNoncritical and checkWrite,
and the opening of a fixture whose .dkk carries datekeys.capsule, whose
unusable extensions and checks are those of Go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The check of the data of datekeys.capsule that a reader that knows the
|
|
|
|
|
/// extension runs (spec §54), as the ValidateCapsule of Go's
|
|
|
|
|
/// locator.Standard: the rejection of [parseCapsuleInfo], or null when the
|
|
|
|
|
/// data reads. [StandardExtensions] runs it.
|
|
|
|
|
Object? checkCapsuleData(Extension e) {
|
|
|
|
|
try {
|
|
|
|
|
parseCapsuleInfo(e);
|
|
|
|
|
return null;
|
|
|
|
|
} on DateKeysException catch (err) {
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Addresses
|
|
|
|
|
|
|
|
|
|
/// An address of a locator: where the rest of the envelope is (spec §44.1),
|
|
|
|
|
/// as Address of Go.
|
|
|
|
|
final class LocatorAddress {
|
|
|
|
|
/// The address [uri] with the [offset] of the rest in its resource, 0 when
|
|
|
|
|
/// the rest is the whole resource.
|
|
|
|
|
LocatorAddress(this.uri, [this.offset = 0]) {
|
|
|
|
|
if (offset < 0) {
|
|
|
|
|
throw ArgumentError.value(offset, 'offset', 'a negative offset');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The URI: ASCII of RFC 3986, with the scheme https or ipfs, and no
|
|
|
|
|
/// userinfo, as [checkAddressUri] checks it.
|
|
|
|
|
final String uri;
|
|
|
|
|
|
|
|
|
|
/// The byte of the resource where the rest starts, 0 when the rest is the
|
|
|
|
|
/// whole resource.
|
|
|
|
|
final int offset;
|
|
|
|
|
|
|
|
|
|
/// 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 [checkAddressUri] rejects the address.
|
|
|
|
|
String get host {
|
|
|
|
|
if (!_accepts(uri)) return '';
|
|
|
|
|
final (_, h) = _splitAuthority(uri);
|
|
|
|
|
return _trimBrackets(h);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool operator ==(Object other) =>
|
|
|
|
|
other is LocatorAddress && other.uri == uri && other.offset == offset;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
int get hashCode => Object.hash(uri, offset);
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String toString() =>
|
|
|
|
|
'LocatorAddress(${goQuote(utf8Bytes(uri))}${offset == 0 ? '' : ', $offset'})';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _accepts(String uri) {
|
|
|
|
|
try {
|
|
|
|
|
checkAddressUri(uri);
|
|
|
|
|
return true;
|
|
|
|
|
} on LocatorException {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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
|
|
|
|
|
/// [LocatorException] with the text of Go.
|
|
|
|
|
void checkAddressUri(String uri) {
|
|
|
|
|
final b = utf8Bytes(uri);
|
|
|
|
|
if (b.isEmpty || b.length > maxAddressUriLen) {
|
|
|
|
|
throw _fail('an address of ${b.length} bytes, not 1 to $maxAddressUriLen');
|
|
|
|
|
}
|
|
|
|
|
for (var i = 0; i < b.length; i++) {
|
|
|
|
|
final 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.
|
|
|
|
|
final (scheme, host) = _splitAuthority(uri);
|
|
|
|
|
if (_dotSegment(uri)) {
|
|
|
|
|
throw _fail('an address with a "." or ".." segment in its path');
|
|
|
|
|
}
|
|
|
|
|
switch (scheme) {
|
|
|
|
|
case 'https':
|
|
|
|
|
_checkHost(host);
|
|
|
|
|
return;
|
|
|
|
|
case 'ipfs':
|
|
|
|
|
if (!_isCidV1(host)) {
|
|
|
|
|
throw _fail('an ipfs address without a CID v1 in base32');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw _fail('the scheme ${goQuote(utf8Bytes(scheme))}: only https and ipfs');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const _uriPunctuation = "-._~:/?#[]@!\$&'()*+,;=";
|
|
|
|
|
|
|
|
|
|
// Whether c may appear in a URI of RFC 3986, a percent sign apart: the
|
|
|
|
|
// unreserved characters, gen-delims and sub-delims.
|
|
|
|
|
bool _uriChar(int c) =>
|
|
|
|
|
(c >= 0x61 && c <= 0x7a) ||
|
|
|
|
|
(c >= 0x41 && c <= 0x5a) ||
|
|
|
|
|
(c >= 0x30 && c <= 0x39) ||
|
|
|
|
|
_uriPunctuation.codeUnits.contains(c);
|
|
|
|
|
|
|
|
|
|
bool _isHex(int c) =>
|
|
|
|
|
(c >= 0x30 && c <= 0x39) ||
|
|
|
|
|
(c >= 0x61 && c <= 0x66) ||
|
|
|
|
|
(c >= 0x41 && c <= 0x46);
|
|
|
|
|
|
|
|
|
|
// Go's %q of a byte, the rune of its value in single quotes, as
|
|
|
|
|
// strconv.QuoteRune writes it.
|
|
|
|
|
String _quoteRune(int c) {
|
|
|
|
|
final String body;
|
|
|
|
|
if (c == 0x27 || c == 0x5c) {
|
|
|
|
|
body = '\\${String.fromCharCode(c)}';
|
|
|
|
|
} else if (isPrint(c)) {
|
|
|
|
|
body = String.fromCharCode(c);
|
|
|
|
|
} else {
|
|
|
|
|
body = switch (c) {
|
|
|
|
|
0x07 => r'\a',
|
|
|
|
|
0x08 => r'\b',
|
|
|
|
|
0x0c => r'\f',
|
|
|
|
|
0x0a => r'\n',
|
|
|
|
|
0x0d => r'\r',
|
|
|
|
|
0x09 => r'\t',
|
|
|
|
|
0x0b => r'\v',
|
|
|
|
|
_ when c < 0x20 || c == 0x7f => '\\x${_hex(c, 2)}',
|
|
|
|
|
_ => '\\u${_hex(c, 4)}',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return "'$body'";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _hex(int v, int digits) => v.toRadixString(16).padLeft(digits, '0');
|
|
|
|
|
|
|
|
|
|
int _indexAny(String s, String chars, [int start = 0]) {
|
|
|
|
|
for (var i = start; i < s.length; i++) {
|
|
|
|
|
if (chars.contains(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.
|
|
|
|
|
bool _dotSegment(String uri) {
|
|
|
|
|
final i = uri.indexOf('://');
|
|
|
|
|
final rest = i < 0 ? '' : uri.substring(i + 3);
|
|
|
|
|
final j = rest.indexOf('/');
|
|
|
|
|
if (j < 0) return false;
|
|
|
|
|
var path = rest.substring(j);
|
|
|
|
|
final k = _indexAny(path, '?#');
|
|
|
|
|
if (k >= 0) path = path.substring(0, k);
|
|
|
|
|
for (final s in path.split('/')) {
|
|
|
|
|
final t = s.replaceAll('%2e', '.').replaceAll('%2E', '.');
|
|
|
|
|
if (t == '.' || t == '..') return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
(String, String) _splitAuthority(String uri) {
|
|
|
|
|
final i = uri.indexOf('://');
|
|
|
|
|
if (i <= 0) throw _fail('an address without a scheme and ://');
|
|
|
|
|
final scheme = uri.substring(0, i);
|
|
|
|
|
final rest = uri.substring(i + 3);
|
|
|
|
|
var authority = rest;
|
|
|
|
|
final j = _indexAny(rest, '/?#');
|
|
|
|
|
if (j >= 0) authority = rest.substring(0, j);
|
|
|
|
|
if (_indexAny(authority, '%@\\') >= 0) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'an address with a percent sign, userinfo or a backslash in its '
|
|
|
|
|
'authority',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
var host = authority;
|
|
|
|
|
if (scheme == 'https') {
|
|
|
|
|
if (authority.startsWith('[')) {
|
|
|
|
|
final end = authority.indexOf(']');
|
|
|
|
|
if (end < 0) throw _fail('an address with an unclosed IPv6 literal');
|
|
|
|
|
host = authority.substring(0, end + 1);
|
|
|
|
|
final tail = authority.substring(end + 1);
|
|
|
|
|
if (tail.isNotEmpty) _checkPort(tail);
|
|
|
|
|
} else {
|
|
|
|
|
final k = authority.indexOf(':');
|
|
|
|
|
if (k >= 0) {
|
|
|
|
|
host = authority.substring(0, k);
|
|
|
|
|
_checkPort(':${authority.substring(k + 1)}');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return (scheme, host);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The port of an authority, its ':' included: a number from 1 to 65535
|
|
|
|
|
// without leading zeros, so that a port is written in one way only.
|
|
|
|
|
void _checkPort(String s) {
|
|
|
|
|
if (s.length < 2 || s.codeUnitAt(0) != 0x3a || s.length > 6) {
|
|
|
|
|
throw _fail('an address with a malformed port');
|
|
|
|
|
}
|
|
|
|
|
var n = 0;
|
|
|
|
|
for (var i = 1; i < s.length; i++) {
|
|
|
|
|
final c = s.codeUnitAt(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.codeUnitAt(1) == 0x30) {
|
|
|
|
|
throw _fail('an address with a port written with a leading zero');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Go's strings.Trim(s, "[]"): every [ and ] at either end.
|
|
|
|
|
String _trimBrackets(String s) {
|
|
|
|
|
var a = 0;
|
|
|
|
|
var b = s.length;
|
|
|
|
|
while (a < b && (s[a] == '[' || s[a] == ']')) {
|
|
|
|
|
a++;
|
|
|
|
|
}
|
|
|
|
|
while (b > a && (s[b - 1] == '[' || s[b - 1] == ']')) {
|
|
|
|
|
b--;
|
|
|
|
|
}
|
|
|
|
|
return s.substring(a, b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _lowerAscii(String s) => String.fromCharCodes([
|
|
|
|
|
for (final c in s.codeUnits) c >= 0x41 && c <= 0x5a ? c + 0x20 : c,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 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; a name whose last label is numeric or starts with 0x only
|
|
|
|
|
// as a public IPv4 address in dotted decimal; and no name that only a
|
|
|
|
|
// machine or a local network resolves.
|
|
|
|
|
void _checkHost(String host) {
|
|
|
|
|
if (host.isEmpty) throw _fail('an https address without a host');
|
|
|
|
|
if (host.startsWith('[')) {
|
|
|
|
|
// One pair of brackets, as checkHost of Go: "[[2000::]" is not an IPv6
|
|
|
|
|
// literal. _splitAuthority ends the literal at its first ']'.
|
|
|
|
|
final a = parseIpAddress(host.substring(1, host.length - 1));
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
if (a == null || !a.is6 || a.zone.isNotEmpty || !isPublicIp(a)) {
|
|
|
|
|
throw _fail('an https address with an IPv6 literal that is not public');
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
final labels = host.split('.');
|
|
|
|
|
for (final l in labels) {
|
|
|
|
|
if (l.isEmpty ||
|
|
|
|
|
l.length > 63 ||
|
|
|
|
|
l.codeUnitAt(0) == 0x2d ||
|
|
|
|
|
l.codeUnitAt(l.length - 1) == 0x2d) {
|
|
|
|
|
throw _fail('an https address with a malformed host');
|
|
|
|
|
}
|
|
|
|
|
for (final c in l.codeUnits) {
|
|
|
|
|
if (!((c >= 0x61 && c <= 0x7a) ||
|
|
|
|
|
(c >= 0x41 && c <= 0x5a) ||
|
|
|
|
|
(c >= 0x30 && c <= 0x39) ||
|
|
|
|
|
c == 0x2d)) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'an https address whose host is not letters, digits and hyphens: '
|
|
|
|
|
'write its punycode form',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
final last = labels.last;
|
|
|
|
|
if (_allDigits(last) || _lowerAscii(last).startsWith('0x')) {
|
|
|
|
|
final a = parseIpAddress(host);
|
|
|
|
|
if (a == null || !a.is4 || !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 _localNames = [
|
|
|
|
|
'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).
|
|
|
|
|
bool _localName(String name) {
|
|
|
|
|
for (final s in _localNames) {
|
|
|
|
|
if (name == s || name.endsWith('.$s')) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool _allDigits(String s) {
|
|
|
|
|
for (final c in s.codeUnits) {
|
|
|
|
|
if (c < 0x30 || c > 0x39) return false;
|
|
|
|
|
}
|
|
|
|
|
return s.isNotEmpty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Whether s is a CID v1 of at most 128 characters in canonical base32, which
|
|
|
|
|
// starts with 'b': 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.
|
|
|
|
|
bool _isCidV1(String s) {
|
|
|
|
|
if (s.length < 2 || s.length > 128 || s.codeUnitAt(0) != 0x62) return false;
|
|
|
|
|
final out = <int>[];
|
|
|
|
|
var acc = 0;
|
|
|
|
|
var bits = 0;
|
|
|
|
|
for (var i = 1; i < s.length; i++) {
|
|
|
|
|
final c = s.codeUnitAt(i);
|
|
|
|
|
final int v;
|
|
|
|
|
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 4 bits before the shift: 12 bits at most.
|
|
|
|
|
acc = (acc << 5) | v;
|
|
|
|
|
bits += 5;
|
|
|
|
|
if (bits >= 8) {
|
|
|
|
|
bits -= 8;
|
|
|
|
|
out.add(acc >> bits);
|
|
|
|
|
acc &= (1 << bits) - 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Canonical base32 without padding: the last character carries fewer than
|
|
|
|
|
// 5 bits beyond the last byte, all zero.
|
|
|
|
|
if (bits >= 5 || acc != 0) return false;
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
final version = _uvarint(out, 0);
|
|
|
|
|
if (version == null || version.$1 != BigInt.one) return false;
|
|
|
|
|
final codec = _uvarint(out, version.$2);
|
|
|
|
|
if (codec == null) return false;
|
|
|
|
|
final hash = _uvarint(out, codec.$2);
|
|
|
|
|
if (hash == null) return false;
|
|
|
|
|
final n = _uvarint(out, hash.$2);
|
|
|
|
|
return n != null &&
|
|
|
|
|
n.$1 > BigInt.zero &&
|
|
|
|
|
BigInt.from(out.length - n.$2) == n.$1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
(BigInt, int)? _uvarint(List<int> b, int at) {
|
|
|
|
|
var v = BigInt.zero;
|
|
|
|
|
for (var i = 0; at + i < b.length && i < 9; i++) {
|
|
|
|
|
final x = b[at + i];
|
|
|
|
|
v |= BigInt.from(x & 0x7f) << (7 * i);
|
|
|
|
|
if (x & 0x80 == 0) {
|
|
|
|
|
if (i > 0 && x == 0) return null;
|
|
|
|
|
return (v, at + i + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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 included: an IPv4 address is checked as its 4 bytes.
|
|
|
|
|
///
|
|
|
|
|
/// On an IPv6-only network with DNS64 and NAT64, a name that has only IPv4
|
|
|
|
|
/// addresses resolves to an IPv6 address that holds one (RFC 6052): one of
|
|
|
|
|
/// the well-known prefix 64:ff9b::/96 is public when the IPv4 address in its
|
|
|
|
|
/// last 32 bits is (spec v0.13, §44.1). [nat64] is the NAT64 prefix of the
|
|
|
|
|
/// network, in the notation of netip.ParsePrefix (`64:ff9b:1::/48`), which
|
|
|
|
|
/// the application discovers with RFC 7050 or its system gives, or null for
|
|
|
|
|
/// none: an address in it is public only when the IPv4 address it holds, at
|
|
|
|
|
/// the positions of RFC 6052, is, even when the prefix is a public one. The
|
|
|
|
|
/// prefix must have one of the lengths of RFC 6052 and lie in 64:ff9b::/16
|
|
|
|
|
/// or be a public IPv6 prefix.
|
|
|
|
|
///
|
|
|
|
|
/// Throws a [LocatorException] for an address that is not public or a
|
|
|
|
|
/// prefix that cannot be one of NAT64, with the texts of
|
|
|
|
|
/// locator.CheckResolvedIP of Go, and an [ArgumentError] for an address of
|
|
|
|
|
/// another length or a prefix that does not parse.
|
|
|
|
|
void checkResolvedIp(List<int> ip, {String? nat64}) {
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
final a = IpAddress.fromBytes(ip);
|
|
|
|
|
_Nat64Prefix? network;
|
|
|
|
|
if (nat64 != null) {
|
|
|
|
|
network = _Nat64Prefix.parse(nat64);
|
|
|
|
|
network.check();
|
|
|
|
|
}
|
|
|
|
|
// The prefixes of NAT64 decide first: the prefix of a network may be a
|
|
|
|
|
// public one, and an address in it reaches the IPv4 address it holds,
|
|
|
|
|
// which may be private.
|
|
|
|
|
for (final p in [_nat64WellKnown, ?network]) {
|
|
|
|
|
if (!p.contains(a)) continue;
|
|
|
|
|
final v4 = p.ipv4(a);
|
|
|
|
|
if (v4 == null) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'an https address whose name resolves to $a, an address of the NAT64 '
|
|
|
|
|
'prefix $p whose bits 64 to 71 are not zero',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (!isPublicIp(v4)) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'an https address whose name resolves to $a, an address of NAT64 '
|
|
|
|
|
'that holds $v4, an IP address that is not public',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
Stage 7a: the addresses of a locator and the IP addresses of netip
lib/src/locator.dart starts with the addresses of spec 44.1, a port of
CheckURI and Address.Host of package locator of datekeys-go: the same
checks in the same order and the same texts, on the UTF-8 of the address,
with Go's %q of the first byte that RFC 3986 does not allow. And
checkResolvedIp, the check of the IP that a name resolves to, which a
reader runs on every connection: the classification of publicIP on the 4
or 16 bytes of an address, an IPv4-mapped one not public. Go has no such
function, since its reader does not download.
lib/src/ipaddr.dart, internal, parses IPv4 and IPv6 with the exact
acceptance of netip.ParseAddr, writes them as its String, and classifies
them as publicIP, with the blocks of spec 44.1. It works byte by byte, so
that the 128 bits of IPv6 stay exact on the web, and never uses dart:io.
The tests run the 247 addresses of locator.json and the 2 800 of the
vectors, the 1 700 strings of netip and the 868 byte strings of publicIP,
on the VM and on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
if (!isPublicIp(a)) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'an https address whose name resolves to $a, an IP address that is '
|
|
|
|
|
'not public',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
|
|
|
|
|
final _nat64WellKnown = _Nat64Prefix.parse('64:ff9b::/96');
|
|
|
|
|
final _nat64Block = _Nat64Prefix.parse('64:ff9b::/16');
|
|
|
|
|
|
|
|
|
|
// A NAT64 prefix (RFC 6052), as the netip.Prefix that CheckResolvedIP of Go
|
|
|
|
|
// receives: the address as written and its length, bits after the length
|
|
|
|
|
// kept, so that check can refuse them.
|
|
|
|
|
final class _Nat64Prefix {
|
|
|
|
|
_Nat64Prefix(this.addr, this.bits);
|
|
|
|
|
|
|
|
|
|
// netip.ParsePrefix: an address without a zone, '/', and a length in
|
|
|
|
|
// decimal without a sign or leading zeros, up to the bits of the address.
|
|
|
|
|
factory _Nat64Prefix.parse(String s) {
|
|
|
|
|
final slash = s.lastIndexOf('/');
|
|
|
|
|
final addr = slash < 0 ? null : parseIpAddress(s.substring(0, slash));
|
|
|
|
|
final len = slash < 0 ? '' : s.substring(slash + 1);
|
|
|
|
|
final bits = RegExp(r'^(0|[1-9][0-9]{0,2})$').hasMatch(len)
|
|
|
|
|
? int.parse(len)
|
|
|
|
|
: -1;
|
|
|
|
|
if (addr == null ||
|
|
|
|
|
addr.zone.isNotEmpty ||
|
|
|
|
|
bits < 0 ||
|
|
|
|
|
bits > addr.bytes.length * 8) {
|
|
|
|
|
throw ArgumentError.value(s, 'nat64', 'not an IP prefix');
|
|
|
|
|
}
|
|
|
|
|
return _Nat64Prefix(addr, bits);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final IpAddress addr;
|
|
|
|
|
final int bits;
|
|
|
|
|
|
|
|
|
|
// netip.Prefix.Contains: an address of the same family, without a zone,
|
|
|
|
|
// whose first bits are those of the prefix.
|
|
|
|
|
bool contains(IpAddress a) {
|
|
|
|
|
final p = addr.bytes;
|
|
|
|
|
final b = a.bytes;
|
|
|
|
|
if (a.zone.isNotEmpty || b.length != p.length) return false;
|
|
|
|
|
for (var i = 0; i < bits; i++) {
|
|
|
|
|
final m = 0x80 >> (i & 7);
|
|
|
|
|
if ((b[i >> 3] & m) != (p[i >> 3] & m)) return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkNAT64Prefix of Go.
|
|
|
|
|
void check() {
|
|
|
|
|
final p = addr.bytes;
|
|
|
|
|
if (!addr.is6 || addr.is4In6) {
|
|
|
|
|
throw _fail('the NAT64 prefix $this is not an IPv6 prefix');
|
|
|
|
|
}
|
|
|
|
|
for (var i = bits; i < 128; i++) {
|
|
|
|
|
if (p[i >> 3] & (0x80 >> (i & 7)) != 0) {
|
|
|
|
|
throw _fail('the NAT64 prefix $this has bits set after its length');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!const [32, 40, 48, 56, 64, 96].contains(bits)) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'the NAT64 prefix $this is not of 32, 40, 48, 56, 64 or 96 bits '
|
|
|
|
|
'(RFC 6052)',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (!_nat64Block.contains(addr) && !isPublicIp(addr)) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'the NAT64 prefix $this is neither in 64:ff9b::/16 nor a public IPv6 '
|
|
|
|
|
'prefix',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// nat64IPv4 of Go: the IPv4 address that [a], an address of this prefix,
|
|
|
|
|
// holds at the positions of RFC 6052, section 2.2, the 32 bits after the
|
|
|
|
|
// prefix without bits 64 to 71, which must be zero; null if they are not.
|
|
|
|
|
IpAddress? ipv4(IpAddress a) {
|
|
|
|
|
final b = a.bytes;
|
|
|
|
|
if (bits < 96 && b[8] != 0) return null;
|
|
|
|
|
final v4 = <int>[];
|
|
|
|
|
for (var i = bits >> 3; v4.length < 4; i++) {
|
|
|
|
|
if (i != 8) v4.add(b[i]);
|
|
|
|
|
}
|
|
|
|
|
return IpAddress.fromBytes(v4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String toString() => '$addr/$bits';
|
|
|
|
|
}
|
|
|
|
|
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// The locator
|
|
|
|
|
|
|
|
|
|
/// The plaintext of the sealed locator (spec §44.1), as Locator of Go.
|
|
|
|
|
final class Locator {
|
|
|
|
|
/// A locator of [addresses], with I_SOBRE [envelopeKey], the raw X25519
|
|
|
|
|
/// identity of the envelope, which it copies, the age header
|
|
|
|
|
/// [envelopeHeader] of the envelope, MAC line included, the SHA-256
|
|
|
|
|
/// [restDigest] and the length [restSize] of the rest, and the SHA-256
|
|
|
|
|
/// [capsuleDigest] of the .dkc. The keys and digests are 32 bytes and the
|
|
|
|
|
/// size is not negative, as the types of Go make them; the other rules are
|
|
|
|
|
/// those of [marshal].
|
|
|
|
|
Locator({
|
|
|
|
|
required List<LocatorAddress> addresses,
|
|
|
|
|
required List<int> envelopeKey,
|
|
|
|
|
required List<int> envelopeHeader,
|
|
|
|
|
required List<int> restDigest,
|
|
|
|
|
required this.restSize,
|
|
|
|
|
required List<int> capsuleDigest,
|
|
|
|
|
}) : addresses = List.unmodifiable(addresses),
|
|
|
|
|
envelopeKey = _fixed(envelopeKey, 'envelopeKey'),
|
|
|
|
|
envelopeHeader = Uint8List.fromList(envelopeHeader),
|
|
|
|
|
restDigest = _fixed(restDigest, 'restDigest'),
|
|
|
|
|
capsuleDigest = _fixed(capsuleDigest, 'capsuleDigest') {
|
|
|
|
|
if (restSize < 0) {
|
|
|
|
|
throw ArgumentError.value(restSize, 'restSize', 'a negative size');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Uint8List _fixed(List<int> b, String name) {
|
|
|
|
|
if (b.length != _digestSize) {
|
|
|
|
|
throw ArgumentError.value(b.length, name, 'not $_digestSize bytes');
|
|
|
|
|
}
|
|
|
|
|
return Uint8List.fromList(b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Key 0, where the rest of the envelope is, in their order.
|
|
|
|
|
final List<LocatorAddress> addresses;
|
|
|
|
|
|
|
|
|
|
/// Key 1, I_SOBRE, the raw X25519 identity of the envelope. SECRET: see
|
|
|
|
|
/// [wipe].
|
|
|
|
|
final Uint8List envelopeKey;
|
|
|
|
|
|
|
|
|
|
/// Key 2, the age header of the envelope, MAC line included.
|
|
|
|
|
final Uint8List envelopeHeader;
|
|
|
|
|
|
|
|
|
|
/// Key 3, the SHA-256 of the rest.
|
|
|
|
|
final Uint8List restDigest;
|
|
|
|
|
|
|
|
|
|
/// Key 4, the length of the rest, in bytes.
|
|
|
|
|
final int restSize;
|
|
|
|
|
|
|
|
|
|
/// Key 5, the SHA-256 of the .dkc (spec §43).
|
|
|
|
|
final Uint8List capsuleDigest;
|
|
|
|
|
|
|
|
|
|
/// This locator with [addresses] instead of its own: those where a writer
|
|
|
|
|
/// stored the rest of the envelope of [splitEnvelope].
|
|
|
|
|
Locator withAddresses(List<LocatorAddress> addresses) => Locator(
|
|
|
|
|
addresses: addresses,
|
|
|
|
|
envelopeKey: envelopeKey,
|
|
|
|
|
envelopeHeader: envelopeHeader,
|
|
|
|
|
restDigest: restDigest,
|
|
|
|
|
restSize: restSize,
|
|
|
|
|
capsuleDigest: capsuleDigest,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
|
List<LocatorAddress> get usable => [
|
|
|
|
|
for (final a in addresses)
|
|
|
|
|
if (_accepts(a.uri)) a,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// Clears [envelopeKey]; the envelope cannot be opened afterwards.
|
|
|
|
|
void wipe() => envelopeKey.fillRange(0, envelopeKey.length, 0);
|
|
|
|
|
|
|
|
|
|
// validateForm of Go: the form that a reader requires of the whole
|
|
|
|
|
// locator; a broken address makes only that address unusable.
|
|
|
|
|
void _validateForm() {
|
|
|
|
|
if (addresses.isEmpty || addresses.length > maxLocatorAddresses) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'${addresses.length} addresses, not 1 to $maxLocatorAddresses',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
for (final a in addresses) {
|
|
|
|
|
final n = utf8Bytes(a.uri).length;
|
|
|
|
|
if (n == 0 || n > maxAddressUriLen) {
|
|
|
|
|
throw _fail('an address of $n bytes, not 1 to $maxAddressUriLen');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
final n = envelopeHeader.length;
|
|
|
|
|
if (n < 1 || n > maxEnvelopeHeaderLen) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'an envelope header of $n bytes, not 1 to $maxEnvelopeHeaderLen',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (restSize > maxSafeUint) {
|
|
|
|
|
throw _fail('a rest larger than 2^53 - 1 bytes');
|
|
|
|
|
}
|
|
|
|
|
for (final a in addresses) {
|
|
|
|
|
if (a.offset > maxSafeUint) {
|
|
|
|
|
throw _fail('an offset larger than 2^53 - 1');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The map; pad < 0 leaves key 6 out.
|
|
|
|
|
void _encode(CborEncoder e, int pad) {
|
|
|
|
|
e
|
|
|
|
|
..map(pad >= 0 ? 7 : 6)
|
|
|
|
|
..uint(0)
|
|
|
|
|
..array(addresses.length);
|
|
|
|
|
for (final a in addresses) {
|
|
|
|
|
e
|
|
|
|
|
..map(a.offset == 0 ? 1 : 2)
|
|
|
|
|
..uint(0)
|
|
|
|
|
..text(a.uri);
|
|
|
|
|
if (a.offset != 0) {
|
|
|
|
|
e
|
|
|
|
|
..uint(1)
|
|
|
|
|
..uint(a.offset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
e
|
|
|
|
|
..uint(1)
|
|
|
|
|
..bstr(envelopeKey)
|
|
|
|
|
..uint(2)
|
|
|
|
|
..bstr(envelopeHeader)
|
|
|
|
|
..uint(3)
|
|
|
|
|
..bstr(restDigest)
|
|
|
|
|
..uint(4)
|
|
|
|
|
..uint(restSize)
|
|
|
|
|
..uint(5)
|
|
|
|
|
..bstr(capsuleDigest);
|
|
|
|
|
if (pad >= 0) {
|
|
|
|
|
e
|
|
|
|
|
..uint(6)
|
|
|
|
|
..bstr(Uint8List(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
|
|
|
|
|
/// [checkAddressUri]: a writer never writes one that a reader rejects.
|
|
|
|
|
/// Throws a [LocatorException]. The plaintext holds I_SOBRE: the caller
|
|
|
|
|
/// wipes it.
|
|
|
|
|
Uint8List marshal() {
|
|
|
|
|
_validateForm();
|
|
|
|
|
for (final a in addresses) {
|
|
|
|
|
checkAddressUri(a.uri);
|
|
|
|
|
}
|
|
|
|
|
return _marshal();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Uint8List _marshal() {
|
|
|
|
|
final e = CborEncoder();
|
|
|
|
|
_encode(e, -1);
|
|
|
|
|
final base = e.out();
|
|
|
|
|
final pad = _padFor(base.length);
|
|
|
|
|
if (pad < 0) return base;
|
|
|
|
|
// It holds I_SOBRE.
|
|
|
|
|
final n = base.length;
|
|
|
|
|
base.fillRange(0, n, 0);
|
|
|
|
|
final p = CborEncoder(capacity: n + pad + 8);
|
|
|
|
|
_encode(p, pad);
|
|
|
|
|
final out = p.out();
|
|
|
|
|
if (out.length % locatorBlock != 0) {
|
|
|
|
|
out.fillRange(0, out.length, 0);
|
|
|
|
|
throw _fail('internal error: ${out.length} bytes of plaintext');
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The rest of the envelope 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 [LocatorException] when
|
|
|
|
|
/// the resource is shorter.
|
|
|
|
|
Uint8List restIn(List<int> host, int offset) {
|
|
|
|
|
if (offset < 0) {
|
|
|
|
|
throw ArgumentError.value(offset, 'offset', 'a negative offset');
|
|
|
|
|
}
|
|
|
|
|
if (offset > host.length || restSize > host.length - offset) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'the resource has ${host.length} bytes, and the rest is $restSize '
|
|
|
|
|
'from $offset',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return Uint8List.fromList(host.sublist(offset, offset + restSize));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Joins the header of the locator and [rest], which a reader got from an
|
|
|
|
|
/// address, and decrypts the .dkc, as OpenEnvelope of Go. It checks the
|
|
|
|
|
/// size and the SHA-256 of the rest, and the SHA-256 of the .dkc, before
|
|
|
|
|
/// the caller uses it (spec §44.1): they protect against whoever stores
|
|
|
|
|
/// the rest, not against whoever wrote the .dkk. Throws a
|
|
|
|
|
/// [LocatorException].
|
|
|
|
|
Uint8List openEnvelope(List<int> rest) {
|
|
|
|
|
if (rest.length != restSize) {
|
|
|
|
|
throw _fail('the rest is ${rest.length} bytes, not $restSize');
|
|
|
|
|
}
|
|
|
|
|
if (!equalBytes(sha256(rest), restDigest)) {
|
|
|
|
|
throw _fail('the SHA-256 of the rest is not the one of the locator');
|
|
|
|
|
}
|
|
|
|
|
final id = x25519IdentityFromRaw(envelopeKey);
|
|
|
|
|
final Uint8List dkc;
|
|
|
|
|
try {
|
|
|
|
|
dkc = ageDecrypt(concatBytes([envelopeHeader, rest]), [id]);
|
|
|
|
|
} on AgeException catch (e) {
|
|
|
|
|
throw _fail('the envelope: ${e.message}');
|
|
|
|
|
} finally {
|
|
|
|
|
id.wipe();
|
|
|
|
|
}
|
|
|
|
|
if (!equalBytes(sha256(dkc), capsuleDigest)) {
|
|
|
|
|
dkc.fillRange(0, dkc.length, 0);
|
|
|
|
|
throw _fail(
|
|
|
|
|
'the SHA-256 of the .dkc is not the capsule_digest of the locator',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return dkc;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int _bstrHeadLen(int n) {
|
|
|
|
|
if (n < 24) return 1;
|
|
|
|
|
if (n < 256) return 2;
|
|
|
|
|
if (n < 65536) return 3;
|
|
|
|
|
return 5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// padFor of Go: the length of key 6 that makes the plaintext measure the
|
|
|
|
|
// least multiple of locatorBlock that holds it, or -1 when n0, the length
|
|
|
|
|
// without key 6, already is one. When no length of key 6 gives a multiple,
|
|
|
|
|
// as happens at the boundaries of the CBOR length, it takes the next one.
|
|
|
|
|
int _padFor(int n0) {
|
|
|
|
|
if (n0 % locatorBlock == 0) return -1;
|
|
|
|
|
for (
|
|
|
|
|
var total = (n0 ~/ locatorBlock + 1) * locatorBlock;
|
|
|
|
|
;
|
|
|
|
|
total += locatorBlock
|
|
|
|
|
) {
|
|
|
|
|
for (var 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).
|
|
|
|
|
int locatorPlaintextLength(int base) {
|
|
|
|
|
final pad = _padFor(base);
|
|
|
|
|
if (pad < 0) return base;
|
|
|
|
|
return base + 1 + _bstrHeadLen(pad) + pad;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Something of the decoding of a locator that Go reports without a code.
|
|
|
|
|
final class _Plain implements Exception {
|
|
|
|
|
const _Plain(this.message);
|
|
|
|
|
final String message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reads the plaintext of a locator, checking its profile and the length
|
|
|
|
|
/// that [Locator.marshal] 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
|
|
|
|
|
/// [Locator.usable] leaves it out. Throws a [LocatorException].
|
|
|
|
|
Locator unmarshalLocator(List<int> plaintext) {
|
|
|
|
|
final b = plaintext is Uint8List ? plaintext : Uint8List.fromList(plaintext);
|
|
|
|
|
final addresses = <LocatorAddress>[];
|
|
|
|
|
Uint8List? key;
|
|
|
|
|
Uint8List? header;
|
|
|
|
|
Uint8List? restDigest;
|
|
|
|
|
var restSize = 0;
|
|
|
|
|
Uint8List? capsuleDigest;
|
|
|
|
|
var pad = -1;
|
|
|
|
|
Locator? decoded;
|
|
|
|
|
Locator locatorOf() => decoded ??= Locator(
|
|
|
|
|
addresses: addresses,
|
|
|
|
|
envelopeKey: key!,
|
|
|
|
|
envelopeHeader: header!,
|
|
|
|
|
restDigest: restDigest!,
|
|
|
|
|
restSize: restSize,
|
|
|
|
|
capsuleDigest: capsuleDigest!,
|
|
|
|
|
);
|
|
|
|
|
try {
|
|
|
|
|
unmarshalCbor(b, (d) {
|
|
|
|
|
final pairs = d.map(7);
|
|
|
|
|
var seen = 0;
|
|
|
|
|
for (var i = 0; i < pairs; i++) {
|
|
|
|
|
final k = d.key();
|
|
|
|
|
switch (k) {
|
|
|
|
|
case 0:
|
|
|
|
|
withContext('key 0', () => _decodeAddresses(d, addresses));
|
|
|
|
|
case 1:
|
|
|
|
|
key = withContext('key 1', () => d.bstr(32, 32));
|
|
|
|
|
case 2:
|
|
|
|
|
header = withContext(
|
|
|
|
|
'key 2',
|
|
|
|
|
() => d.bstr(1, maxEnvelopeHeaderLen),
|
|
|
|
|
);
|
|
|
|
|
case 3:
|
|
|
|
|
restDigest = withContext('key 3', () => d.bstr(32, 32));
|
|
|
|
|
case 4:
|
|
|
|
|
restSize = withContext('key 4', () => d.uint(maxSafeUint));
|
|
|
|
|
case 5:
|
|
|
|
|
capsuleDigest = withContext('key 5', () => d.bstr(32, 32));
|
|
|
|
|
case 6:
|
|
|
|
|
final z = withContext('key 6', () => d.bstr(1, 1 << 20));
|
|
|
|
|
for (final x in z) {
|
|
|
|
|
if (x != 0) throw const _Plain('the padding is not zeros');
|
|
|
|
|
}
|
|
|
|
|
pad = z.length;
|
|
|
|
|
default:
|
|
|
|
|
throw _undefined('key $k is not defined');
|
|
|
|
|
}
|
|
|
|
|
seen |= 1 << (k as int);
|
|
|
|
|
}
|
|
|
|
|
if (seen & 0x3f != 0x3f) {
|
|
|
|
|
throw _undefined('a key from 0 to 5 is missing');
|
|
|
|
|
}
|
|
|
|
|
d.endMap();
|
|
|
|
|
}, (e) => locatorOf()._encode(e, pad));
|
|
|
|
|
} on DateKeysException catch (err) {
|
|
|
|
|
key?.fillRange(0, key!.length, 0);
|
|
|
|
|
throw _fail(err.message);
|
|
|
|
|
} on _Plain catch (err) {
|
|
|
|
|
key?.fillRange(0, key!.length, 0);
|
|
|
|
|
throw _fail(err.message);
|
|
|
|
|
}
|
|
|
|
|
final l = locatorOf();
|
|
|
|
|
key!.fillRange(0, key!.length, 0);
|
|
|
|
|
try {
|
|
|
|
|
l._validateForm();
|
|
|
|
|
// The length is the one Marshal gives: nothing else is canonical.
|
|
|
|
|
final want = l._marshal();
|
|
|
|
|
final same = equalBytes(want, b);
|
|
|
|
|
want.fillRange(0, want.length, 0);
|
|
|
|
|
if (!same) {
|
|
|
|
|
throw _fail(
|
|
|
|
|
'the plaintext is not $locatorBlock or the least multiple of '
|
|
|
|
|
'$locatorBlock that holds it',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} on LocatorException {
|
|
|
|
|
l.wipe();
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
return l;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _decodeAddresses(CborDecoder d, List<LocatorAddress> out) {
|
|
|
|
|
final n = d.array(maxLocatorAddresses);
|
|
|
|
|
for (var i = 0; i < n; i++) {
|
|
|
|
|
final pairs = d.map(2);
|
|
|
|
|
var uri = '';
|
|
|
|
|
var offset = 0;
|
|
|
|
|
var seen = 0;
|
|
|
|
|
for (var j = 0; j < pairs; j++) {
|
|
|
|
|
final k = d.key();
|
|
|
|
|
switch (k) {
|
|
|
|
|
case 0:
|
|
|
|
|
uri = d.text(maxAddressUriLen);
|
|
|
|
|
case 1:
|
|
|
|
|
offset = d.uint(maxSafeUint);
|
|
|
|
|
if (offset == 0) {
|
|
|
|
|
throw _undefined('an offset of 0 is written by leaving it out');
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
throw _undefined('address key $k is not defined');
|
|
|
|
|
}
|
|
|
|
|
seen |= 1 << (k as int);
|
|
|
|
|
}
|
|
|
|
|
if (seen & 1 == 0) throw _undefined('an address without URI');
|
|
|
|
|
d.endMap();
|
|
|
|
|
out.add(LocatorAddress(uri, offset));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// The sealed locator
|
|
|
|
|
|
|
|
|
|
/// The identity that opens a sealed locator, as Go's agewrap.TimeIdentity:
|
|
|
|
|
/// the complete stanza set and its arguments, the release again, the length
|
|
|
|
|
/// of the body, U and then the IBE.
|
|
|
|
|
final class _TimeIdentity implements AgeIdentity {
|
|
|
|
|
_TimeIdentity(this._p, this._round, this._release);
|
|
|
|
|
final PinnedProfile _p;
|
|
|
|
|
final int _round;
|
|
|
|
|
final Release _release;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Uint8List unwrap(List<AgeStanza> stanzas) {
|
|
|
|
|
checkTimeStanzas(
|
|
|
|
|
stanzas,
|
|
|
|
|
round: _round,
|
|
|
|
|
chainHashHex: toHex(_p.chainHash),
|
|
|
|
|
profileId: _p.id,
|
|
|
|
|
);
|
|
|
|
|
final s = stanzas.single;
|
|
|
|
|
return unwrapTlockStanza(_p, _round, _release, s.args, s.body);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const _encChunk = ageChunkSize + poly1305TagSize;
|
|
|
|
|
|
|
|
|
|
/// Opens the sealed locator [sealed] with [release], the release of its
|
|
|
|
|
/// [round] in the pinned profile [p], and reads its plaintext, as Open of
|
|
|
|
|
/// Go. A locator for another round or another chain does not open: it is
|
|
|
|
|
/// unusable (spec §44.1). Its errors carry no normative code: a
|
|
|
|
|
/// [LocatorException], whose text is that of Go, the texts of the checks of
|
|
|
|
|
/// the profile, the stanza, the release and age included.
|
|
|
|
|
///
|
|
|
|
|
/// As Go, it reads at most 1 MiB of plaintext, through io.LimitReader: what
|
|
|
|
|
/// follows is neither decrypted nor checked, and the plaintext read is then
|
|
|
|
|
/// not the length of a locator.
|
|
|
|
|
Locator openLocator(
|
|
|
|
|
PinnedProfile p,
|
|
|
|
|
int round,
|
|
|
|
|
Release release,
|
|
|
|
|
List<int> sealed,
|
|
|
|
|
) => _open(p, round, release, sealed);
|
|
|
|
|
|
|
|
|
|
Locator _open(PinnedProfile p, int round, Release release, List<int> sealed) {
|
|
|
|
|
try {
|
|
|
|
|
checkTlockProfile(p);
|
|
|
|
|
} on DateKeysException catch (e) {
|
|
|
|
|
throw _fail(e.message);
|
|
|
|
|
}
|
|
|
|
|
final file = sealed is Uint8List ? sealed : Uint8List.fromList(sealed);
|
|
|
|
|
final AgeOpened opened;
|
|
|
|
|
try {
|
|
|
|
|
opened = ageOpen(file, [_TimeIdentity(p, round, release)]);
|
|
|
|
|
} on AgeException catch (e) {
|
|
|
|
|
throw _fail(e.message);
|
|
|
|
|
} on DateKeysException catch (e) {
|
|
|
|
|
throw _fail(e.message);
|
|
|
|
|
}
|
|
|
|
|
final plain = _readLimited(file, opened);
|
|
|
|
|
try {
|
|
|
|
|
return unmarshalLocator(plain);
|
|
|
|
|
} finally {
|
|
|
|
|
plain.fillRange(0, plain.length, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The plaintext of the STREAM of file, at most _maxSealed bytes of it, as
|
|
|
|
|
// io.ReadAll of io.LimitReader of the reader of age.Decrypt: the chunks are
|
|
|
|
|
// decrypted one by one only while less than 1 MiB has been read, and the end
|
|
|
|
|
// of the STREAM is checked only then.
|
|
|
|
|
Uint8List _readLimited(Uint8List file, AgeOpened opened) {
|
|
|
|
|
final d = opened.payload;
|
|
|
|
|
final out = BytesBuilder(copy: false);
|
|
|
|
|
var total = 0;
|
|
|
|
|
var at = opened.payloadOffset;
|
|
|
|
|
try {
|
|
|
|
|
while (total < _maxSealed) {
|
|
|
|
|
if (at >= file.length) {
|
|
|
|
|
final last = d.close();
|
|
|
|
|
out.add(last);
|
|
|
|
|
total += last.length;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
final end = at + _encChunk < file.length ? at + _encChunk : file.length;
|
|
|
|
|
for (final chunk in d.add(file, at, end)) {
|
|
|
|
|
out.add(chunk);
|
|
|
|
|
total += chunk.length;
|
|
|
|
|
}
|
|
|
|
|
at = end;
|
|
|
|
|
}
|
|
|
|
|
} on AgeException catch (e) {
|
|
|
|
|
final partial = out.takeBytes();
|
|
|
|
|
partial.fillRange(0, partial.length, 0);
|
|
|
|
|
throw _fail(e.message);
|
|
|
|
|
}
|
|
|
|
|
d.wipe();
|
|
|
|
|
final b = out.takeBytes();
|
|
|
|
|
if (b.length <= _maxSealed) return b;
|
|
|
|
|
final cut = Uint8List.fromList(Uint8List.sublistView(b, 0, _maxSealed));
|
|
|
|
|
b.fillRange(0, b.length, 0);
|
|
|
|
|
return cut;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// The envelope
|
|
|
|
|
|
|
|
|
|
/// The envelope of [ageFile], the age file of the .dkc [dkc] encrypted for
|
|
|
|
|
/// the X25519 identity [envelopeKey], I_SOBRE: the locator with the key, the
|
|
|
|
|
/// header up to and including the line feed after the MAC line, the SHA-256
|
|
|
|
|
/// and the length of the rest and the SHA-256 of the .dkc, without
|
|
|
|
|
/// addresses; and the rest, the nonce and the STREAM, with no mark, which is
|
|
|
|
|
/// what the person keeps outside. It is the part of NewEnvelope of Go after
|
|
|
|
|
/// the encryption, which newEnvelope of locator_seal.dart makes first;
|
|
|
|
|
/// [ageFile] is not decrypted. A caller adds the addresses where it stored
|
|
|
|
|
/// the rest ([Locator.withAddresses]), alone or inside another file
|
|
|
|
|
/// ([hideRest]), and then seals the locator (sealLocator).
|
Stage 7a: the locator, its opening and the envelope
lib/src/locator.dart gains the rest of package locator of datekeys-go but
Seal and the data of the extension, with the same checks in the same
order and the same texts:
- Locator: unmarshalLocator, the map of spec 44.1 with the length that
Marshal gives and nothing else, and marshal, its form, its addresses and
key 6 up to the least multiple of 4096 that it fills, as padFor and
PlaintextLength; usable, the addresses that a reader uses.
- openLocator, Open of Go: the profile, then age with the tlock identity
of agewrap, and at most 1 MiB of plaintext, as io.LimitReader: the
chunks after it are neither decrypted nor checked.
- The envelope: restIn, openEnvelope with the size and the SHA-256 of the
rest and of the .dkc, hideRest, and splitEnvelope, the part of
NewEnvelope after its age encryption, which needs the writer of age.
A Locator keeps the types of Go: keys and digests of 32 bytes, and no
negative size or offset. Its errors carry no code, as in Go.
The tests run the cases of locator.json, the padding of every base, 482
plaintexts, Marshal at its limits, 118 openings, the files past 1 MiB, the
envelope, the rests and the split; a part also on Node.js.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 days ago
|
|
|
///
|
|
|
|
|
/// The header ends at the line feed after the first line that starts with
|
|
|
|
|
/// `--- `: no line of the header before it starts so, and the lines of the
|
|
|
|
|
/// body of a stanza are base64, which has no '-'. Throws a
|
|
|
|
|
/// [LocatorException] with the text of Go when there is none.
|
|
|
|
|
({Locator locator, Uint8List rest}) splitEnvelope(
|
|
|
|
|
List<int> ageFile,
|
|
|
|
|
List<int> envelopeKey,
|
|
|
|
|
List<int> dkc,
|
|
|
|
|
) {
|
|
|
|
|
final file = ageFile is Uint8List ? ageFile : Uint8List.fromList(ageFile);
|
|
|
|
|
final end = _headerEnd(file);
|
|
|
|
|
final rest = Uint8List.fromList(Uint8List.sublistView(file, end));
|
|
|
|
|
final locator = Locator(
|
|
|
|
|
addresses: const [],
|
|
|
|
|
envelopeKey: envelopeKey,
|
|
|
|
|
envelopeHeader: Uint8List.sublistView(file, 0, end),
|
|
|
|
|
restDigest: sha256(rest),
|
|
|
|
|
restSize: rest.length,
|
|
|
|
|
capsuleDigest: sha256(dkc),
|
|
|
|
|
);
|
|
|
|
|
return (locator: locator, rest: rest);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// headerEnd of Go: the length of the age header of file, up to and including
|
|
|
|
|
// the line feed after the MAC line.
|
|
|
|
|
int _headerEnd(Uint8List file) {
|
|
|
|
|
const mac = [0x0a, 0x2d, 0x2d, 0x2d, 0x20];
|
|
|
|
|
var i = -1;
|
|
|
|
|
for (var at = 0; at + mac.length <= file.length; at++) {
|
|
|
|
|
var match = true;
|
|
|
|
|
for (var k = 0; k < mac.length; k++) {
|
|
|
|
|
if (file[at + k] != mac[k]) {
|
|
|
|
|
match = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (match) {
|
|
|
|
|
i = at;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (i < 0) throw _fail('the age file has no MAC line');
|
|
|
|
|
final j = file.indexOf(0x0a, i + 1);
|
|
|
|
|
if (j < 0) throw _fail('the MAC line of the age file does not end');
|
|
|
|
|
return j + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
|
({Uint8List file, int offset}) hideRest(List<int> host, List<int> rest) =>
|
|
|
|
|
(file: concatBytes([host, rest]), offset: host.length);
|