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.
1003 lines
31 KiB
1003 lines
31 KiB
// Package locator implements the extension datekeys.capsule of a .dkk and
|
|
// what it points to (spec v0.11, §44.1): the data of the extension, which
|
|
// says what a key's capsule is and when it opens; the locator, an age file
|
|
// sealed with tlock for that date, which says where the capsule is; and the
|
|
// envelope, the .dkc encrypted with age and split into a header, which the
|
|
// locator carries, and a rest, the only thing that is kept outside, alone or
|
|
// inside another file.
|
|
//
|
|
// It does not download anything: a reader fetches the rest only when the
|
|
// person asks, after showing her the host or the CID (§44.1), and gives it
|
|
// to OpenEnvelope.
|
|
package locator
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"filippo.io/age"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
"g.activething.com/go/DateKeys/datekey"
|
|
"g.activething.com/go/DateKeys/extension"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
"g.activething.com/go/DateKeys/provider"
|
|
)
|
|
|
|
// Limits of §44.1.
|
|
const (
|
|
// MaxAddresses is the most addresses of a locator.
|
|
MaxAddresses = 8
|
|
// MaxURILen is the longest address, in bytes.
|
|
MaxURILen = 1024
|
|
// MaxHeaderLen is the longest header of an envelope.
|
|
MaxHeaderLen = 1024
|
|
// Block is the unit of the plaintext of a locator: it measures exactly
|
|
// 4096 bytes, or the least multiple of 4096 that holds it.
|
|
Block = 4096
|
|
// maxSealed bounds a sealed locator that a reader decrypts.
|
|
maxSealed = 1 << 20
|
|
)
|
|
|
|
// Info is the data of the extension datekeys.capsule (spec §44.1).
|
|
type Info struct {
|
|
// Note is the copy of the public note of the capsule, "" for none.
|
|
Note string
|
|
// DateKey is the DateKey of the capsule: it says when it opens.
|
|
DateKey datekey.DateKey
|
|
// Sealed is the age file of the locator, sealed with tlock for the round
|
|
// of DateKey, nil for none.
|
|
Sealed []byte
|
|
}
|
|
|
|
// Extension returns the extension for the noncritical array of a .dkk.
|
|
func (i *Info) Extension() (extension.Extension, error) {
|
|
if d, err := datekey.Parse(i.DateKey.Compact()); err != nil || d != i.DateKey {
|
|
return extension.Extension{}, errors.New("locator: Info.DateKey is not a canonical DateKey")
|
|
}
|
|
// A writer writes the DateKey of a capsule it wrote: of a profile that
|
|
// this module pins, whose chain the check of the locator then compares.
|
|
if reg, err := profile.Default(); err != nil {
|
|
return extension.Extension{}, err
|
|
} else if _, ok := reg.Lookup(i.DateKey.ProfileID); !ok {
|
|
return extension.Extension{}, fmt.Errorf("locator: Info.DateKey is of the profile %q, which this module does not pin", i.DateKey.ProfileID)
|
|
}
|
|
if i.Sealed != nil && (len(i.Sealed) < 1 || len(i.Sealed) > maxSealed) {
|
|
return extension.Extension{}, fmt.Errorf("locator: a sealed locator of %d bytes, not 1 to %d", len(i.Sealed), maxSealed)
|
|
}
|
|
if i.Note != "" {
|
|
if err := extension.CheckNote(i.Note); err != nil {
|
|
return extension.Extension{}, err
|
|
}
|
|
}
|
|
var e codec.Encoder
|
|
pairs := 1
|
|
if i.Note != "" {
|
|
pairs++
|
|
}
|
|
if i.Sealed != nil {
|
|
pairs++
|
|
}
|
|
e.Map(pairs)
|
|
if i.Note != "" {
|
|
e.Uint(0)
|
|
e.Text(i.Note)
|
|
}
|
|
e.Uint(1)
|
|
e.Text(i.DateKey.Compact())
|
|
if i.Sealed != nil {
|
|
e.Uint(2)
|
|
e.Bstr(i.Sealed)
|
|
}
|
|
data, err := e.Out()
|
|
if err != nil {
|
|
return extension.Extension{}, err
|
|
}
|
|
x, err := extension.New(extension.CapsuleID, 1, data)
|
|
if err != nil {
|
|
return extension.Extension{}, err
|
|
}
|
|
// Spec §72: the encoder reads what it writes with the rules of a reader,
|
|
// which also ties the locator to the round of DateKey.
|
|
if _, err := ParseInfo(x); err != nil {
|
|
return extension.Extension{}, fmt.Errorf("locator: self-check: a reader rejects this extension: %v", err)
|
|
}
|
|
return x, nil
|
|
}
|
|
|
|
// ParseInfo reads the data of a datekeys.capsule extension. A failure makes
|
|
// the extension unusable, not the .dkk (spec §54): its only normative code is
|
|
// ErrExtensionDataInvalid. A locator must be an age file with one tlock
|
|
// stanza, for the round of the DateKey; its chain is checked when it opens.
|
|
func ParseInfo(x extension.Extension) (*Info, error) {
|
|
if x.ID != extension.CapsuleID || x.Version != 1 || x.Data == nil {
|
|
return nil, fmt.Errorf("locator: not datekeys.capsule version 1 with data: %w", datekeys.ErrExtensionDataInvalid)
|
|
}
|
|
var i Info
|
|
var dk string
|
|
decode := func(d *codec.Decoder) error {
|
|
pairs, err := d.Map(3)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var seen uint
|
|
for range pairs {
|
|
k, err := d.Key()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch k {
|
|
case 0:
|
|
i.Note, err = d.Text(extension.MaxNoteLen)
|
|
if err == nil {
|
|
err = extension.CheckNote(i.Note)
|
|
}
|
|
case 1:
|
|
dk, err = d.Text(1024)
|
|
case 2:
|
|
i.Sealed, err = d.Bstr(1, maxSealed)
|
|
default:
|
|
return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("key %d: %w", k, err)
|
|
}
|
|
seen |= 1 << k
|
|
}
|
|
if seen&2 == 0 {
|
|
return fmt.Errorf("key 1 is missing: %w", datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
return d.EndMap()
|
|
}
|
|
encode := func(e *codec.Encoder) {
|
|
pairs := 1
|
|
if i.Note != "" {
|
|
pairs++
|
|
}
|
|
if i.Sealed != nil {
|
|
pairs++
|
|
}
|
|
e.Map(pairs)
|
|
if i.Note != "" {
|
|
e.Uint(0)
|
|
e.Text(i.Note)
|
|
}
|
|
e.Uint(1)
|
|
e.Text(dk)
|
|
if i.Sealed != nil {
|
|
e.Uint(2)
|
|
e.Bstr(i.Sealed)
|
|
}
|
|
}
|
|
if err := codec.Unmarshal(x.Data, decode, encode); err != nil {
|
|
return nil, fmt.Errorf("locator: datekeys.capsule: %v: %w", err, datekeys.ErrExtensionDataInvalid)
|
|
}
|
|
d, err := datekey.Parse(dk)
|
|
if err != nil || d.Compact() != dk {
|
|
return nil, fmt.Errorf("locator: compact_datekey is not a canonical DateKey: %w", datekeys.ErrExtensionDataInvalid)
|
|
}
|
|
i.DateKey = d
|
|
if i.Sealed != nil {
|
|
if err := checkSealed(i.Sealed, d); err != nil {
|
|
return nil, fmt.Errorf("locator: %v: %w", err, datekeys.ErrExtensionDataInvalid)
|
|
}
|
|
}
|
|
return &i, nil
|
|
}
|
|
|
|
// checkSealed checks the form of a sealed locator against the DateKey d of
|
|
// its extension, as far as it can be checked before the date (spec §44.1):
|
|
// an age file with one tlock stanza, whose arguments are the round of d in
|
|
// decimal and a chain hash in lower-case hexadecimal (§28.1), the chain of
|
|
// the profile of d when this module pins it; and a body that holds a
|
|
// plaintext of 4096 bytes or a multiple, as every locator has. A locator of
|
|
// another round or chain never opens, and one of another length is not a
|
|
// locator.
|
|
func checkSealed(sealed []byte, d datekey.DateKey) error {
|
|
st, err := agewrap.Stanzas(bytes.NewReader(sealed))
|
|
if err != nil || len(st) != 1 || st[0].Type != agewrap.StanzaTLock || len(st[0].Args) != 2 || st[0].Args[0] != strconv.FormatUint(d.Round, 10) {
|
|
return fmt.Errorf("the locator is not an age file with one tlock stanza for round %d, the one of its DateKey", d.Round)
|
|
}
|
|
chain := st[0].Args[1]
|
|
if len(chain) != 64 || strings.Trim(chain, "0123456789abcdef") != "" {
|
|
return errors.New("the tlock stanza of the locator has no chain hash in lower-case hexadecimal")
|
|
}
|
|
if reg, err := profile.Default(); err == nil {
|
|
if p, ok := reg.Lookup(d.ProfileID); ok && chain != hex.EncodeToString(p.ChainHash[:]) {
|
|
return fmt.Errorf("the locator is sealed for the chain %s, not for the one of the profile %s of its DateKey", chain, d.ProfileID)
|
|
}
|
|
}
|
|
// agewrap.Stanzas read the header up to its MAC line, so it ends.
|
|
end, err := headerEnd(sealed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if end == len(sealed) {
|
|
return errors.New("the locator is an age header without a body")
|
|
}
|
|
if !sealedBodyLength(uint64(len(sealed) - end)) {
|
|
return fmt.Errorf("the body of the locator is %d bytes, which no plaintext of 4096 bytes or a multiple gives", len(sealed)-end)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sealedBodyLength reports 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.
|
|
func sealedBodyLength(n uint64) bool {
|
|
for p := uint64(Block); p <= maxSealed; p += Block {
|
|
if n == 16+p+16*((p+64<<10-1)/(64<<10)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Address says where the rest of the envelope is.
|
|
type Address struct {
|
|
// URI is ASCII, RFC 3986, with the scheme https or ipfs (a CID v1), and
|
|
// no userinfo.
|
|
URI string
|
|
// Offset is the byte of the resource where the rest starts, 0 when the
|
|
// rest is the whole resource.
|
|
Offset uint64
|
|
}
|
|
|
|
// CheckURI checks an address with the rules of spec §44.1: ASCII of RFC 3986,
|
|
// with its percent signs followed by two hexadecimal digits, the scheme
|
|
// https or ipfs in lower case, no "." or ".." segment in its path, and the
|
|
// host or the CID that checkHost and isCIDv1 accept.
|
|
func CheckURI(uri string) error {
|
|
if uri == "" || len(uri) > MaxURILen {
|
|
return fmt.Errorf("locator: an address of %d bytes, not 1 to %d", len(uri), MaxURILen)
|
|
}
|
|
for i := 0; i < len(uri); i++ {
|
|
c := uri[i]
|
|
switch {
|
|
case c == '%':
|
|
if i+2 >= len(uri) || !isHex(uri[i+1]) || !isHex(uri[i+2]) {
|
|
return errors.New("locator: an address with a percent sign not followed by two hexadecimal digits")
|
|
}
|
|
case !uriChar(c):
|
|
return fmt.Errorf("locator: an address with the character %q, which RFC 3986 does not allow", c)
|
|
}
|
|
}
|
|
scheme, host, err := splitAuthority(uri)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if dotSegment(uri) {
|
|
return errors.New("locator: an address with a \".\" or \"..\" segment in its path")
|
|
}
|
|
switch scheme {
|
|
case "https":
|
|
return checkHost(host)
|
|
case "ipfs":
|
|
if !isCIDv1(host) {
|
|
return errors.New("locator: an ipfs address without a CID v1 in base32")
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("locator: the scheme %q: only https and ipfs", scheme)
|
|
}
|
|
|
|
// uriChar reports whether c may appear in a URI of RFC 3986, a percent sign
|
|
// apart: the unreserved characters, gen-delims and sub-delims.
|
|
func uriChar(c byte) bool {
|
|
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.IndexByte("-._~:/?#[]@!$&'()*+,;=", c) >= 0
|
|
}
|
|
|
|
func isHex(c byte) bool {
|
|
return c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'
|
|
}
|
|
|
|
// dotSegment reports whether the path of uri has a segment "." or "..",
|
|
// written or percent-encoded: a client or a gateway that resolves it would
|
|
// ask for something other than what the address shows, as another CID behind
|
|
// an ipfs address.
|
|
func dotSegment(uri string) bool {
|
|
_, rest, _ := strings.Cut(uri, "://")
|
|
i := strings.IndexByte(rest, '/')
|
|
if i < 0 {
|
|
return false
|
|
}
|
|
path := rest[i:]
|
|
if j := strings.IndexAny(path, "?#"); j >= 0 {
|
|
path = path[:j]
|
|
}
|
|
for _, s := range strings.Split(path, "/") {
|
|
s = strings.ReplaceAll(strings.ReplaceAll(s, "%2e", "."), "%2E", ".")
|
|
if s == "." || s == ".." {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// splitAuthority returns 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 (spec v0.11, §44.1).
|
|
func splitAuthority(uri string) (scheme, host string, err error) {
|
|
scheme, rest, ok := strings.Cut(uri, "://")
|
|
if !ok || scheme == "" {
|
|
return "", "", errors.New("locator: an address without a scheme and ://")
|
|
}
|
|
authority := rest
|
|
if i := strings.IndexAny(rest, "/?#"); i >= 0 {
|
|
authority = rest[:i]
|
|
}
|
|
if strings.ContainsAny(authority, "%@\\") {
|
|
return "", "", errors.New("locator: an address with a percent sign, userinfo or a backslash in its authority")
|
|
}
|
|
host = authority
|
|
if scheme == "https" {
|
|
if strings.HasPrefix(authority, "[") {
|
|
end := strings.Index(authority, "]")
|
|
if end < 0 {
|
|
return "", "", errors.New("locator: an address with an unclosed IPv6 literal")
|
|
}
|
|
host = authority[:end+1]
|
|
if tail := authority[end+1:]; tail != "" {
|
|
if err := checkPort(tail); err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
} else if h, port, found := strings.Cut(authority, ":"); found {
|
|
host = h
|
|
if err := checkPort(":" + port); err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
}
|
|
return scheme, host, nil
|
|
}
|
|
|
|
// checkPort checks the port of an authority, its ':' included: a number from 1
|
|
// to 65535 without leading zeros (spec v0.12, §44.1), so that a port is
|
|
// written in one way only.
|
|
func checkPort(s string) error {
|
|
if len(s) < 2 || s[0] != ':' || len(s) > 6 {
|
|
return errors.New("locator: an address with a malformed port")
|
|
}
|
|
n := 0
|
|
for _, c := range s[1:] {
|
|
if c < '0' || c > '9' {
|
|
return errors.New("locator: an address with a malformed port")
|
|
}
|
|
n = n*10 + int(c-'0')
|
|
}
|
|
if n < 1 || n > 65535 {
|
|
return errors.New("locator: an address with a port outside 1 to 65535")
|
|
}
|
|
if s[1] == '0' {
|
|
return errors.New("locator: an address with a port written with a leading zero")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkHost accepts a name of labels of 1 to 63 letters, digits and hyphens,
|
|
// separated by dots, none of which starts or ends with a hyphen, or an IP
|
|
// literal that is public: the spec forbids following a redirect to the
|
|
// others, and an address that starts there would defeat the same rule
|
|
// (§44.1). A name whose last label is numeric, or starts with 0x in either
|
|
// case, is refused unless it is a public IPv4 address in dotted decimal
|
|
// without leading zeros, the only form netip reads: some clients read the
|
|
// others, 0x7f000001 or 0177.0.0.1, as an IPv4 address too. So are the names
|
|
// that only resolve inside a machine or a local network, in either case:
|
|
// localhost, a name of one label, and the special-use names of localName.
|
|
func checkHost(host string) error {
|
|
if host == "" {
|
|
return errors.New("locator: an https address without a host")
|
|
}
|
|
if strings.HasPrefix(host, "[") {
|
|
// One pair of brackets: splitAuthority ends the literal at the
|
|
// first ']', and "[[2000::]" is not an IPv6 literal.
|
|
a, err := netip.ParseAddr(strings.TrimSuffix(strings.TrimPrefix(host, "["), "]"))
|
|
if err != nil || !a.Is6() || a.Zone() != "" || !publicIP(a) {
|
|
return errors.New("locator: an https address with an IPv6 literal that is not public")
|
|
}
|
|
return nil
|
|
}
|
|
labels := strings.Split(host, ".")
|
|
for _, l := range labels {
|
|
if l == "" || len(l) > 63 || l[0] == '-' || l[len(l)-1] == '-' {
|
|
return errors.New("locator: an https address with a malformed host")
|
|
}
|
|
for i := 0; i < len(l); i++ {
|
|
c := l[i]
|
|
if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-') {
|
|
return errors.New("locator: an https address whose host is not letters, digits and hyphens: write its punycode form")
|
|
}
|
|
}
|
|
}
|
|
last := labels[len(labels)-1]
|
|
if allDigits(last) || strings.HasPrefix(strings.ToLower(last), "0x") {
|
|
a, err := netip.ParseAddr(host)
|
|
if err != nil || !a.Is4() || !publicIP(a) {
|
|
return errors.New("locator: an https address with a numeric host that is not a public IPv4 address")
|
|
}
|
|
return nil
|
|
}
|
|
if len(labels) == 1 || localName(strings.ToLower(host)) {
|
|
return errors.New("locator: an https address with a name that only a machine or a local network resolves")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// localName reports whether the name, in lower case, is one of the
|
|
// special-use names that never resolve on the public Internet: localhost
|
|
// (RFC 6761), .local (RFC 6762), .home.arpa (RFC 8375), .internal, .invalid,
|
|
// .test, .example and .onion (RFC 7686), or below one of them.
|
|
func localName(name string) bool {
|
|
for _, s := range []string{"localhost", "local", "home.arpa", "internal", "invalid", "test", "example", "onion"} {
|
|
if name == s || strings.HasSuffix(name, "."+s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func allDigits(s string) bool {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] < '0' || s[i] > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return s != ""
|
|
}
|
|
|
|
// The blocks of the IANA registries of special-purpose addresses that an
|
|
// address of a locator may not use (spec §44.1). An IPv6 address must also
|
|
// be a global unicast one, of 2000::/3.
|
|
var (
|
|
notPublic4 = prefixes("0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12",
|
|
"192.0.0.0/24", "192.0.2.0/24", "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24",
|
|
"203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4")
|
|
global6 = prefixes("2000::/3")
|
|
notPublic6 = prefixes("2001::/23", "2001:db8::/32", "2002::/16", "3fff::/20")
|
|
)
|
|
|
|
func prefixes(s ...string) []netip.Prefix {
|
|
out := make([]netip.Prefix, len(s))
|
|
for i, p := range s {
|
|
out[i] = netip.MustParsePrefix(p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func inAny(a netip.Addr, ps []netip.Prefix) bool {
|
|
for _, p := range ps {
|
|
if p.Contains(a) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// publicIP reports whether a is an address of the public Internet: an IPv4
|
|
// address outside the blocks of notPublic4, or an IPv6 address of 2000::/3
|
|
// outside those of notPublic6. An IPv6 address that holds an IPv4 one, mapped,
|
|
// compatible, of NAT64, 6to4 or Teredo, is not: it would reach the IPv4
|
|
// address without the check of an IPv4 address.
|
|
func publicIP(a netip.Addr) bool {
|
|
if a.Is4() {
|
|
return !inAny(a, notPublic4)
|
|
}
|
|
return inAny(a, global6) && !inAny(a, notPublic6)
|
|
}
|
|
|
|
// isCIDv1 reports whether s is a CID v1 of at most 128 characters in
|
|
// canonical base32, which starts with 'b' (spec v0.12, §44.1): the alphabet
|
|
// in lower case, without padding, the bits left over set to zero, and
|
|
// decoded, minimal varints of at most 9 bytes, the version 1, a content codec
|
|
// and a multihash with a digest of at least one byte and of the length it
|
|
// gives, with nothing after it.
|
|
func isCIDv1(s string) bool {
|
|
if len(s) < 2 || len(s) > 128 || s[0] != 'b' {
|
|
return false
|
|
}
|
|
var out []byte
|
|
var acc, bits uint
|
|
for i := 1; i < len(s); i++ {
|
|
c := s[i]
|
|
var v byte
|
|
switch {
|
|
case c >= 'a' && c <= 'z':
|
|
v = c - 'a'
|
|
case c >= '2' && c <= '7':
|
|
v = c - '2' + 26
|
|
default:
|
|
return false
|
|
}
|
|
acc, bits = acc<<5|uint(v), bits+5
|
|
if bits >= 8 {
|
|
bits -= 8
|
|
out = append(out, byte(acc>>bits))
|
|
acc &= 1<<bits - 1
|
|
}
|
|
}
|
|
// Canonical base32 without padding: the last character carries fewer
|
|
// than 5 bits beyond the last byte, all zero. A character more, even
|
|
// one of zero bits, is another encoding of the same bytes.
|
|
if bits >= 5 || acc != 0 {
|
|
return false
|
|
}
|
|
version, out, ok := uvarint(out)
|
|
if !ok || version != 1 {
|
|
return false
|
|
}
|
|
if _, out, ok = uvarint(out); !ok { // the content codec
|
|
return false
|
|
}
|
|
if _, out, ok = uvarint(out); !ok { // the hash function
|
|
return false
|
|
}
|
|
n, out, ok := uvarint(out)
|
|
return ok && n > 0 && uint64(len(out)) == n
|
|
}
|
|
|
|
// uvarint reads an unsigned varint of multiformats, minimal and of at most 9
|
|
// bytes, from the start of b.
|
|
func uvarint(b []byte) (uint64, []byte, bool) {
|
|
var v uint64
|
|
for i := 0; i < len(b) && i < 9; i++ {
|
|
v |= uint64(b[i]&0x7f) << (7 * i)
|
|
if b[i]&0x80 == 0 {
|
|
if i > 0 && b[i] == 0 {
|
|
return 0, nil, false
|
|
}
|
|
return v, b[i+1:], true
|
|
}
|
|
}
|
|
return 0, nil, false
|
|
}
|
|
|
|
// Host returns what a reader shows before it downloads: the host of an https
|
|
// address, or the CID of an ipfs one (spec §44.1).
|
|
func (a Address) Host() string {
|
|
if CheckURI(a.URI) != nil {
|
|
return ""
|
|
}
|
|
_, host, _ := splitAuthority(a.URI)
|
|
return strings.Trim(host, "[]")
|
|
}
|
|
|
|
// Locator is the plaintext of the sealed locator (spec §44.1).
|
|
type Locator struct {
|
|
Addresses []Address
|
|
// EnvelopeKey is I_SOBRE, the raw X25519 identity of the envelope. SECRET.
|
|
EnvelopeKey [32]byte
|
|
// EnvelopeHeader is the age header of the envelope, MAC line included.
|
|
EnvelopeHeader []byte
|
|
// RestDigest is the SHA-256 of the rest, and RestSize its length.
|
|
RestDigest [32]byte
|
|
RestSize uint64
|
|
// CapsuleDigest is the SHA-256 of the .dkc (spec §43).
|
|
CapsuleDigest [32]byte
|
|
}
|
|
|
|
// Usable returns the addresses that meet the rules of spec §44.1, in their
|
|
// order. A reader rejects each address that breaks them, and uses the others:
|
|
// a locator whose addresses are all rejected has nothing to download.
|
|
func (l *Locator) Usable() []Address {
|
|
var out []Address
|
|
for _, a := range l.Addresses {
|
|
if CheckURI(a.URI) == nil {
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// validate checks what Marshal writes: the form, and each address, since a
|
|
// writer never writes one that a reader would reject.
|
|
func (l *Locator) validate() error {
|
|
if err := l.validateForm(); err != nil {
|
|
return err
|
|
}
|
|
for _, a := range l.Addresses {
|
|
if err := CheckURI(a.URI); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateForm checks the form that a reader requires of the whole locator:
|
|
// a broken address makes only that address unusable (Usable).
|
|
func (l *Locator) validateForm() error {
|
|
if len(l.Addresses) < 1 || len(l.Addresses) > MaxAddresses {
|
|
return fmt.Errorf("locator: %d addresses, not 1 to %d", len(l.Addresses), MaxAddresses)
|
|
}
|
|
for _, a := range l.Addresses {
|
|
if a.URI == "" || len(a.URI) > MaxURILen {
|
|
return fmt.Errorf("locator: an address of %d bytes, not 1 to %d", len(a.URI), MaxURILen)
|
|
}
|
|
}
|
|
if n := len(l.EnvelopeHeader); n < 1 || n > MaxHeaderLen {
|
|
return fmt.Errorf("locator: an envelope header of %d bytes, not 1 to %d", n, MaxHeaderLen)
|
|
}
|
|
if l.RestSize > codec.MaxSafeUint {
|
|
return errors.New("locator: a rest larger than 2^53 - 1 bytes")
|
|
}
|
|
for _, a := range l.Addresses {
|
|
if a.Offset > codec.MaxSafeUint {
|
|
return errors.New("locator: an offset larger than 2^53 - 1")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// encode writes the map; pad < 0 leaves key 6 out.
|
|
func (l *Locator) encode(e *codec.Encoder, pad int) {
|
|
n := 6
|
|
if pad >= 0 {
|
|
n = 7
|
|
}
|
|
e.Map(n)
|
|
e.Uint(0)
|
|
e.Array(len(l.Addresses))
|
|
for _, a := range l.Addresses {
|
|
if a.Offset == 0 {
|
|
e.Map(1)
|
|
} else {
|
|
e.Map(2)
|
|
}
|
|
e.Uint(0)
|
|
e.Text(a.URI)
|
|
if a.Offset != 0 {
|
|
e.Uint(1)
|
|
e.Uint(a.Offset)
|
|
}
|
|
}
|
|
e.Uint(1)
|
|
e.Bstr(l.EnvelopeKey[:])
|
|
e.Uint(2)
|
|
e.Bstr(l.EnvelopeHeader)
|
|
e.Uint(3)
|
|
e.Bstr(l.RestDigest[:])
|
|
e.Uint(4)
|
|
e.Uint(l.RestSize)
|
|
e.Uint(5)
|
|
e.Bstr(l.CapsuleDigest[:])
|
|
if pad >= 0 {
|
|
e.Uint(6)
|
|
e.Bstr(make([]byte, pad))
|
|
}
|
|
}
|
|
|
|
func bstrHeadLen(n int) int {
|
|
switch {
|
|
case n < 24:
|
|
return 1
|
|
case n < 256:
|
|
return 2
|
|
case n < 65536:
|
|
return 3
|
|
}
|
|
return 5
|
|
}
|
|
|
|
// padFor returns the length of key 6 that makes the plaintext measure the
|
|
// least multiple of Block that holds it, or -1 when n0, the length without
|
|
// key 6, already is one. When no length of key 6 gives a given multiple, as
|
|
// happens at the boundaries of the CBOR length, it takes the next one.
|
|
func padFor(n0 int) int {
|
|
if n0%Block == 0 {
|
|
return -1
|
|
}
|
|
for total := (n0/Block + 1) * Block; ; total += Block {
|
|
for pad := 1; pad <= total-n0; pad++ {
|
|
if n0+1+bstrHeadLen(pad)+pad == total {
|
|
return pad
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// PlaintextLength returns the length of the plaintext of a locator whose CBOR
|
|
// without key 6 measures base bytes: base when it already is a multiple of
|
|
// Block, and otherwise the least multiple that key 6 can fill exactly.
|
|
func PlaintextLength(base int) int {
|
|
pad := padFor(base)
|
|
if pad < 0 {
|
|
return base
|
|
}
|
|
return base + 1 + bstrHeadLen(pad) + pad
|
|
}
|
|
|
|
// Marshal returns the plaintext of the locator: CBOR with the profile of
|
|
// spec §58, completed with zeros in key 6 up to the least multiple of 4096
|
|
// bytes that holds it, so that its length does not tell how many addresses
|
|
// there are.
|
|
func (l *Locator) Marshal() ([]byte, error) {
|
|
if err := l.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return l.marshal()
|
|
}
|
|
|
|
// marshal is Marshal once the locator is checked.
|
|
func (l *Locator) marshal() ([]byte, error) {
|
|
var e codec.Encoder
|
|
l.encode(&e, -1)
|
|
base, err := e.Out()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pad := padFor(len(base))
|
|
if pad < 0 {
|
|
return base, nil
|
|
}
|
|
defer clear(base) // it holds I_SOBRE
|
|
var p codec.Encoder
|
|
l.encode(&p, pad)
|
|
out, err := p.Out()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(out)%Block != 0 {
|
|
return nil, fmt.Errorf("locator: internal error: %d bytes of plaintext", len(out))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Unmarshal reads the plaintext of a locator, checking its profile and the
|
|
// length that Marshal gives. 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 Usable leaves it out.
|
|
func Unmarshal(b []byte) (*Locator, error) {
|
|
var l Locator
|
|
pad := -1
|
|
decode := func(d *codec.Decoder) error {
|
|
pairs, err := d.Map(7)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var seen uint
|
|
for range pairs {
|
|
k, err := d.Key()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch k {
|
|
case 0:
|
|
err = decodeAddresses(d, &l)
|
|
case 1:
|
|
err = copyBstr(d, l.EnvelopeKey[:])
|
|
case 2:
|
|
l.EnvelopeHeader, err = d.Bstr(1, MaxHeaderLen)
|
|
case 3:
|
|
err = copyBstr(d, l.RestDigest[:])
|
|
case 4:
|
|
l.RestSize, err = d.Uint(codec.MaxSafeUint)
|
|
case 5:
|
|
err = copyBstr(d, l.CapsuleDigest[:])
|
|
case 6:
|
|
var z []byte
|
|
if z, err = d.Bstr(1, 1<<20); err == nil {
|
|
if len(bytes.Trim(z, "\x00")) != 0 {
|
|
return errors.New("the padding is not zeros")
|
|
}
|
|
pad = len(z)
|
|
}
|
|
default:
|
|
return fmt.Errorf("key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("key %d: %w", k, err)
|
|
}
|
|
seen |= 1 << k
|
|
}
|
|
if seen&0x3f != 0x3f {
|
|
return fmt.Errorf("a key from 0 to 5 is missing: %w", datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
return d.EndMap()
|
|
}
|
|
encode := func(e *codec.Encoder) { l.encode(e, pad) }
|
|
if err := codec.Unmarshal(b, decode, encode); err != nil {
|
|
return nil, fmt.Errorf("locator: %v", err)
|
|
}
|
|
if err := l.validateForm(); err != nil {
|
|
return nil, err
|
|
}
|
|
// The length is the one Marshal gives: nothing else is canonical.
|
|
want, err := l.marshal()
|
|
defer clear(want)
|
|
if err != nil || !bytes.Equal(want, b) {
|
|
return nil, fmt.Errorf("locator: the plaintext is not %d or the least multiple of %d that holds it", Block, Block)
|
|
}
|
|
return &l, nil
|
|
}
|
|
|
|
func copyBstr(d *codec.Decoder, dst []byte) error {
|
|
b, err := d.Bstr(len(dst), len(dst))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
copy(dst, b)
|
|
return nil
|
|
}
|
|
|
|
func decodeAddresses(d *codec.Decoder, l *Locator) error {
|
|
n, err := d.Array(MaxAddresses)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for range n {
|
|
pairs, err := d.Map(2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var a Address
|
|
var seen uint
|
|
for range pairs {
|
|
k, err := d.Key()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch k {
|
|
case 0:
|
|
a.URI, err = d.Text(MaxURILen)
|
|
case 1:
|
|
if a.Offset, err = d.Uint(codec.MaxSafeUint); err == nil && a.Offset == 0 {
|
|
err = fmt.Errorf("an offset of 0 is written by leaving it out: %w", datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
default:
|
|
return fmt.Errorf("address key %d is not defined: %w", k, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
seen |= 1 << k
|
|
}
|
|
if seen&1 == 0 {
|
|
return fmt.Errorf("an address without URI: %w", datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if err := d.EndMap(); err != nil {
|
|
return err
|
|
}
|
|
l.Addresses = append(l.Addresses, a)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Seal returns the locator as an age file with a single tlock stanza for the
|
|
// round of the DateKey (spec §44.1): nobody reads it before the date, the
|
|
// holder of the key included.
|
|
func Seal(p *profile.Profile, round uint64, l *Locator) ([]byte, error) {
|
|
plain, err := l.Marshal()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clear(plain)
|
|
r, err := agewrap.NewTimeRecipient(p, round)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var buf bytes.Buffer
|
|
w, err := age.Encrypt(&buf, r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := w.Write(plain); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// Open opens a sealed locator with the release of its round, and reads its
|
|
// plaintext. A locator for another round or another chain does not open: it
|
|
// is unusable (spec §44.1), and its errors carry no normative code.
|
|
func Open(p *profile.Profile, round uint64, release provider.Release, sealed []byte) (*Locator, error) {
|
|
id, err := agewrap.NewTimeIdentity(p, round, release)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("locator: %v", err)
|
|
}
|
|
r, err := age.Decrypt(bytes.NewReader(sealed), id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("locator: %v", err)
|
|
}
|
|
plain, err := io.ReadAll(io.LimitReader(r, maxSealed))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("locator: %v", err)
|
|
}
|
|
defer clear(plain)
|
|
return Unmarshal(plain)
|
|
}
|
|
|
|
// NewEnvelope encrypts the .dkc dkc with age for a new identity, I_SOBRE, and
|
|
// splits the age file: the locator it returns has the key, the header, the
|
|
// digests and the size of the rest, and no address yet; rest, with no mark,
|
|
// is what the person keeps outside. A caller adds the addresses where it
|
|
// stored rest, alone or inside another file, and then seals the locator.
|
|
func NewEnvelope(dkc []byte) (loc *Locator, rest []byte, err error) {
|
|
id, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
raw, err := agewrap.RawX25519Identity(id)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer clear(raw)
|
|
var buf bytes.Buffer
|
|
w, err := age.Encrypt(&buf, id.Recipient())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if _, err := w.Write(dkc); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
file := buf.Bytes()
|
|
end, err := headerEnd(file)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
loc = &Locator{EnvelopeHeader: bytes.Clone(file[:end]), RestDigest: sha256.Sum256(file[end:]), RestSize: uint64(len(file) - end), CapsuleDigest: sha256.Sum256(dkc)}
|
|
copy(loc.EnvelopeKey[:], raw)
|
|
return loc, bytes.Clone(file[end:]), nil
|
|
}
|
|
|
|
// headerEnd returns the length of the age header of file, up to and
|
|
// including the line feed after the MAC line: the 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 '-'.
|
|
func headerEnd(file []byte) (int, error) {
|
|
i := bytes.Index(file, []byte("\n--- "))
|
|
if i < 0 {
|
|
return 0, errors.New("locator: the age file has no MAC line")
|
|
}
|
|
j := bytes.IndexByte(file[i+1:], '\n')
|
|
if j < 0 {
|
|
return 0, errors.New("locator: the MAC line of the age file does not end")
|
|
}
|
|
return i + 1 + j + 1, nil
|
|
}
|
|
|
|
// OpenEnvelope joins the header of the locator and rest, which a reader got
|
|
// from an address, and decrypts the .dkc. It checks the size and the SHA-256
|
|
// of 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.
|
|
func (l *Locator) OpenEnvelope(rest []byte) ([]byte, error) {
|
|
if uint64(len(rest)) != l.RestSize {
|
|
return nil, fmt.Errorf("locator: the rest is %d bytes, not %d", len(rest), l.RestSize)
|
|
}
|
|
if sha256.Sum256(rest) != l.RestDigest {
|
|
return nil, errors.New("locator: the SHA-256 of the rest is not the one of the locator")
|
|
}
|
|
id, err := agewrap.X25519IdentityFromRaw(l.EnvelopeKey[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, err := age.Decrypt(io.MultiReader(bytes.NewReader(l.EnvelopeHeader), bytes.NewReader(rest)), id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("locator: the envelope: %w", err)
|
|
}
|
|
dkc, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("locator: the envelope: %w", err)
|
|
}
|
|
if sha256.Sum256(dkc) != l.CapsuleDigest {
|
|
return nil, errors.New("locator: the SHA-256 of the .dkc is not the capsule_digest of the locator")
|
|
}
|
|
return dkc, nil
|
|
}
|