// 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" "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") } 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 { st, err := agewrap.Stanzas(bytes.NewReader(i.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 nil, fmt.Errorf("locator: the locator is not an age file with one tlock stanza for round %d, the one of its DateKey: %w", d.Round, datekeys.ErrExtensionDataInvalid) } } return &i, nil } // 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, "[") { a, err := netip.ParseAddr(strings.Trim(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< 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 }