Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

link.gno

5.51 Kb · 194 lines
  1package wesh
  2
  3import (
  4	"encoding/binary"
  5	"errors"
  6	"net/url"
  7	"strings"
  8
  9	"gno.land/p/moul/x/daily/b58/v0"
 10)
 11
 12// WebLinkPrefix is the prefix of a shareable Berty web link. The fragment
 13// marker is part of it on purpose: everything after '#' stays in the browser
 14// and is never sent to berty.tech, so the web host never learns the identity
 15// being shared.
 16const WebLinkPrefix = "https://berty.tech/id#"
 17
 18// contactKind is the human-readable link kind for a contact invite,
 19// berty's BertyLink_ContactInviteV1Kind.
 20const contactKind = "contact"
 21
 22// Protobuf field numbers, from berty's api/messengertypes/messengertypes.proto
 23// and weshnet's api/protocol/protocoltypes.proto. They are wire-format
 24// constants: changing one silently produces a link no Berty client can read.
 25const (
 26	fieldBertyLinkBertyID = 2 // BertyLink.berty_id
 27	fieldBertyIDSeed      = 1 // BertyID.public_rendezvous_seed
 28	fieldBertyIDAccountPK = 2 // BertyID.account_pk
 29)
 30
 31var (
 32	ErrNotAWebLink    = errors.New("wesh: not a berty web link")
 33	ErrNotAContactLink = errors.New("wesh: link is not a contact invite")
 34	ErrBadLinkPayload = errors.New("wesh: link payload is not a valid contact")
 35)
 36
 37// Blob returns the protobuf-encoded machine payload of a contact invite link:
 38// a BertyLink carrying only a BertyID with the rendezvous seed and account key.
 39//
 40// The kind and the display name are deliberately absent. berty's MarshalLink
 41// puts the kind in the human-readable path segment and the display name in the
 42// query string, so a blob that carried them would not match a link produced by
 43// a real Berty client. The encoding here is verified against berty's own
 44// golden test vector; see link_test.gno.
 45func (c Contact) Blob() ([]byte, error) {
 46	if err := c.Validate(); err != nil {
 47		return nil, err
 48	}
 49	id := appendBytesField(nil, fieldBertyIDSeed, c.Seed)
 50	id = appendBytesField(id, fieldBertyIDAccountPK, c.AccountPK)
 51	return appendBytesField(nil, fieldBertyLinkBertyID, id), nil
 52}
 53
 54// WebLink returns the shareable https://berty.tech/id# link for the contact.
 55// Scanning or opening it in Berty starts a contact request.
 56func (c Contact) WebLink() (string, error) {
 57	blob, err := c.Blob()
 58	if err != nil {
 59		return "", err
 60	}
 61	link := WebLinkPrefix + contactKind + "/" + b58.Encode(blob)
 62	if c.DisplayName != "" {
 63		link += "/name=" + url.QueryEscape(c.DisplayName)
 64	}
 65	return link, nil
 66}
 67
 68// ParseWebLink decodes a Berty contact web link back into a [Contact].
 69//
 70// It accepts links with or without a trailing query segment, and ignores query
 71// keys other than "name", matching berty's own UnmarshalLink leniency.
 72func ParseWebLink(link string) (Contact, error) {
 73	var c Contact
 74
 75	if !strings.HasPrefix(link, WebLinkPrefix) {
 76		return c, ErrNotAWebLink
 77	}
 78	parts := strings.Split(strings.TrimPrefix(link, WebLinkPrefix), "/")
 79	if len(parts) < 2 {
 80		return c, ErrNotAWebLink
 81	}
 82	if parts[0] != contactKind {
 83		return c, ErrNotAContactLink
 84	}
 85	if !b58.IsValid(parts[1]) {
 86		return c, ErrBadLinkPayload
 87	}
 88
 89	inner, ok := lookupBytesField(b58.Decode(parts[1]), fieldBertyLinkBertyID)
 90	if !ok {
 91		return c, ErrBadLinkPayload
 92	}
 93	seed, ok := lookupBytesField(inner, fieldBertyIDSeed)
 94	if !ok {
 95		return c, ErrBadLinkPayload
 96	}
 97	pk, ok := lookupBytesField(inner, fieldBertyIDAccountPK)
 98	if !ok {
 99		return c, ErrBadLinkPayload
100	}
101	c.AccountPK = pk
102	c.Seed = seed
103
104	if len(parts) > 2 {
105		c.DisplayName = queryName(parts[2])
106	}
107	if err := c.Validate(); err != nil {
108		return Contact{}, err
109	}
110	return c, nil
111}
112
113// queryName pulls the "name" key out of an encoded query segment, returning ""
114// when it is absent or malformed. A bad display name must never fail a link
115// that is otherwise valid: the name is decoration, the keys are the payload.
116func queryName(q string) string {
117	for _, pair := range strings.Split(q, "&") {
118		if !strings.HasPrefix(pair, "name=") {
119			continue
120		}
121		name, err := url.QueryUnescape(strings.TrimPrefix(pair, "name="))
122		if err != nil || len(name) > MaxDisplayNameLen {
123			return ""
124		}
125		return name
126	}
127	return ""
128}
129
130// appendBytesField appends a length-delimited (wire type 2) protobuf field.
131// An empty value encodes to nothing, which is what proto3 does for a zero
132// bytes field, and is why an invalid contact can never produce a short-but-
133// plausible blob: Validate has already rejected it.
134func appendBytesField(dst []byte, num int, val []byte) []byte {
135	if len(val) == 0 {
136		return dst
137	}
138	dst = append(dst, byte(num<<3|2))
139	dst = binary.AppendUvarint(dst, uint64(len(val)))
140	return append(dst, val...)
141}
142
143// lookupBytesField scans a protobuf message for the first length-delimited
144// field with the given number, skipping every other field whatever its wire
145// type. Unknown fields are skipped rather than rejected so a link produced by
146// a newer Berty client still parses.
147func lookupBytesField(buf []byte, num int) ([]byte, bool) {
148	i := 0
149	for i < len(buf) {
150		tag, n := binary.Uvarint(buf[i:])
151		if n <= 0 {
152			return nil, false
153		}
154		i += n
155		fieldNum := int(tag >> 3)
156		wireType := int(tag & 0x7)
157
158		switch wireType {
159		case 0: // varint
160			_, n := binary.Uvarint(buf[i:])
161			if n <= 0 {
162				return nil, false
163			}
164			i += n
165		case 1: // 64-bit
166			if i+8 > len(buf) {
167				return nil, false
168			}
169			i += 8
170		case 2: // length-delimited
171			l, n := binary.Uvarint(buf[i:])
172			if n <= 0 {
173				return nil, false
174			}
175			i += n
176			end := i + int(l)
177			if end < i || end > len(buf) {
178				return nil, false
179			}
180			if fieldNum == num {
181				return buf[i:end], true
182			}
183			i = end
184		case 5: // 32-bit
185			if i+4 > len(buf) {
186				return nil, false
187			}
188			i += 4
189		default:
190			return nil, false
191		}
192	}
193	return nil, false
194}