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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

link.gno

5.52 Kb · 194 lines
  1package wesh
  2
  3import (
  4	"encoding/binary"
  5	"errors"
  6	"net/url"
  7	"strings"
  8
  9	"gno.land/p/nym-moultest123/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}