You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1252 lines
42 KiB
1252 lines
42 KiB
/// 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;
|
|
/// - 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]).
|
|
///
|
|
/// 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).
|
|
///
|
|
/// Sealing a locator, Seal of Go, and making an envelope, the age encryption
|
|
/// of NewEnvelope, need the writer of age: [splitEnvelope] is the rest of
|
|
/// NewEnvelope, the split of the age file of the envelope.
|
|
///
|
|
/// 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).
|
|
library;
|
|
|
|
import 'dart:typed_data';
|
|
|
|
import 'age.dart';
|
|
import 'agewrap.dart';
|
|
import 'bytes.dart';
|
|
import 'cbor.dart';
|
|
import 'chacha20poly1305.dart';
|
|
import 'datekey.dart';
|
|
import 'errors.dart';
|
|
import 'extension.dart';
|
|
import 'ipaddr.dart';
|
|
import 'note.dart';
|
|
import 'profile.dart';
|
|
import 'release.dart';
|
|
import 'sha256.dart';
|
|
import 'tlock.dart';
|
|
|
|
/// The most addresses of a locator (spec §44.1).
|
|
const maxLocatorAddresses = 8;
|
|
|
|
/// The longest address, in bytes (spec §44.1).
|
|
const maxAddressUriLen = 1024;
|
|
|
|
/// 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;
|
|
|
|
/// 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');
|
|
|
|
// 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);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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');
|
|
}
|
|
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 && !_sealedFor(s, d.round)) {
|
|
throw _dataInvalid(
|
|
'the locator is not an age file with one tlock stanza for round '
|
|
'${d.round}, the one of its DateKey',
|
|
);
|
|
}
|
|
return 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.
|
|
bool _sealedFor(Uint8List sealed, int round) {
|
|
final List<AgeStanza> st;
|
|
try {
|
|
st = ageStanzas(sealed);
|
|
} on DateKeysException {
|
|
return false;
|
|
}
|
|
return st.length == 1 &&
|
|
st[0].type == stanzaTlock &&
|
|
st[0].args.length == 2 &&
|
|
st[0].args[0] == '$round';
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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('[')) {
|
|
final a = parseIpAddress(_trimBrackets(host));
|
|
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;
|
|
}
|
|
}
|
|
// The bits of the last character beyond a byte are zero.
|
|
if (acc != 0) return false;
|
|
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.
|
|
///
|
|
/// Throws a [LocatorException] for an address that is not public, and an
|
|
/// [ArgumentError] for any other length. Go has no such function: a reader
|
|
/// of the reference does not download.
|
|
void checkResolvedIp(List<int> ip) {
|
|
final a = IpAddress.fromBytes(ip);
|
|
if (!isPublicIp(a)) {
|
|
throw _fail(
|
|
'an https address whose name resolves to $a, an IP address that is '
|
|
'not public',
|
|
);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 needs the writer of age; [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.
|
|
///
|
|
/// 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);
|