render.gno
6.49 Kb · 186 lines
1package wesh
2
3import (
4 "chain/runtime"
5 "encoding/hex"
6 "strconv"
7 "strings"
8 "time"
9
10 "gno.land/p/moul/x/wesh/v0"
11)
12
13// Render serves the directory index at "" and one identity card at "<name>".
14//
15// The index deliberately does not compute a Berty link per row: base58 over a
16// 70-byte payload is big-integer work, and it belongs on the page that a human
17// actually asked for.
18func Render(path string) string {
19 path = strings.Trim(path, "/")
20 if path == "" {
21 return renderIndex()
22 }
23 return renderIdentity(path)
24}
25
26func renderIndex() string {
27 var b strings.Builder
28 b.WriteString("# Wesh directory\n\n")
29 b.WriteString("Resolvable, self-signed [Wesh protocol](https://berty.tech/docs/protocol/) ")
30 b.WriteString("identities, on top of [`p/moul/x/wesh`](/p/moul/x/wesh/v0).\n\n")
31 b.WriteString("A Berty identity normally travels out of band as a QR code, cannot be ")
32 b.WriteString("looked up, cannot announce a seed rotation, and can never revoke a device. ")
33 b.WriteString("This realm addresses those three, and stores no secret of any kind.\n\n")
34
35 if byName.Size() == 0 {
36 b.WriteString("_No identity registered yet._\n")
37 return b.String()
38 }
39
40 b.WriteString("| name | owner | rev | mode | devices |\n")
41 b.WriteString("|---|---|---|---|---|\n")
42 byName.Iterate("", "", func(key string, value any) bool {
43 id := value.(*identity)
44 b.WriteString("| [" + id.name + "](/r/moul/x/wesh/v0:" + id.name + ") ")
45 b.WriteString("| `" + id.owner.String() + "` ")
46 b.WriteString("| " + strconv.Itoa(id.revision) + " ")
47 b.WriteString("| " + modeLabel(id) + " ")
48 b.WriteString("| " + strconv.Itoa(activeDevices(id)) + " active / " + strconv.Itoa(len(id.devices)) + " entries |\n")
49 return false
50 })
51 n := byName.Size()
52 if n == 1 {
53 b.WriteString("\n1 identity.\n")
54 } else {
55 b.WriteString("\n" + strconv.Itoa(n) + " identities.\n")
56 }
57 return b.String()
58}
59
60func renderIdentity(name string) string {
61 id := lookup(name)
62 if id == nil {
63 return "# Not found\n\nNo identity named `" + name + "`. [Back to the directory](/r/moul/x/wesh/v0).\n"
64 }
65
66 var b strings.Builder
67 b.WriteString("# " + id.name + "\n\n")
68 if id.displayName != "" {
69 b.WriteString("_" + id.displayName + "_\n\n")
70 }
71 b.WriteString("| field | value |\n|---|---|\n")
72 b.WriteString("| owner | `" + id.owner.String() + "` |\n")
73 b.WriteString("| account key | `" + hex.EncodeToString(id.accountPK) + "` |\n")
74 b.WriteString("| mode | " + modeLabel(id) + " |\n")
75 b.WriteString("| revision | " + strconv.Itoa(id.revision) + " |\n")
76 b.WriteString("| registered | block " + strconv.FormatInt(id.history[0].height, 10) + " |\n")
77
78 b.WriteString("\n## Contact\n\n")
79 if id.committed {
80 b.WriteString("This identity publishes a **commitment**, `")
81 b.WriteString(hex.EncodeToString(id.payload))
82 b.WriteString("`, not its rendezvous seed.\n\n")
83 b.WriteString("The chain attests that this account key claimed that commitment from ")
84 b.WriteString("that gno address. The seed itself is shared out of band, and whoever ")
85 b.WriteString("receives it can check it here with `H(seed || salt)` before using it. ")
86 b.WriteString("The rendezvous point stays off chain.\n")
87 } else {
88 link, err := contactOf(id).WebLink()
89 if err != nil {
90 b.WriteString("_Contact is unrenderable: " + err.Error() + "_\n")
91 } else {
92 b.WriteString("Scan or open this in Berty to send a contact request:\n\n")
93 b.WriteString("```\n" + link + "\n```\n\n")
94 b.WriteString("| field | value |\n|---|---|\n")
95 b.WriteString("| rendezvous seed | `" + hex.EncodeToString(id.payload) + "` |\n")
96 now := time.Now().Unix()
97 period := wesh.RoundPeriod(now, wesh.DefaultRotationInterval)
98 point, _ := contactOf(id).RendezvousPointAt(now, wesh.DefaultRotationInterval)
99 b.WriteString("| rendezvous point, period " + strconv.FormatInt(period, 10) + " | `" + hex.EncodeToString(point) + "` |\n")
100 b.WriteString("\nThe point is `HMAC-SHA256(accountPK || seed, be64(period))`, derived ")
101 b.WriteString("exactly as weshnet derives it, and it rotates every 24h. Anyone can ")
102 b.WriteString("recompute it and check the DHT, so this entry is verifiable rather ")
103 b.WriteString("than merely asserted.\n")
104 }
105 }
106
107 b.WriteString("\n## Devices\n\n")
108 if len(id.devices) == 0 {
109 b.WriteString("No sigchain entry yet. Head is the genesis digest `")
110 b.WriteString(hex.EncodeToString(id.head) + "`.\n")
111 } else {
112 b.WriteString("Every entry below was signed by the account key and verified on chain, ")
113 b.WriteString("and chains to the digest of the one before it.\n\n")
114 b.WriteString("| seq | op | device | block | digest |\n|---|---|---|---|---|\n")
115 for _, e := range id.devices {
116 b.WriteString("| " + strconv.Itoa(e.seq) + " ")
117 b.WriteString("| " + e.op + " ")
118 b.WriteString("| `" + short(hex.EncodeToString(e.devicePK)) + "` ")
119 b.WriteString("| " + strconv.FormatInt(e.height, 10) + " ")
120 b.WriteString("| `" + short(hex.EncodeToString(e.digest)) + "` |\n")
121 }
122 b.WriteString("\nHead: `" + hex.EncodeToString(id.head) + "`\n")
123 }
124
125 if len(id.history) > 1 {
126 b.WriteString("\n## Rotation history\n\n")
127 b.WriteString("Weshnet has no way to announce that a seed was reset, so every link ")
128 b.WriteString("ever shared dies silently. These are the superseded values, kept so a ")
129 b.WriteString("stale link can be recognised as stale.\n\n")
130 b.WriteString("| rev | value | block |\n|---|---|---|\n")
131 for _, r := range id.history {
132 mark := ""
133 if r.revision == id.revision {
134 mark = " (current)"
135 }
136 b.WriteString("| " + strconv.Itoa(r.revision) + mark + " ")
137 b.WriteString("| `" + short(hex.EncodeToString(r.payload)) + "` ")
138 b.WriteString("| " + strconv.FormatInt(r.height, 10) + " |\n")
139 }
140 }
141
142 b.WriteString("\n[Back to the directory](/r/moul/x/wesh/v0) · chain `" + runtime.ChainID() + "`\n")
143 return b.String()
144}
145
146func modeLabel(id *identity) string {
147 if id.committed {
148 return "committed"
149 }
150 return "published"
151}
152
153// activeDevices counts the distinct devices whose latest sigchain entry is an
154// add. A device can be added, revoked and added again, so only the last entry
155// for each key decides.
156func activeDevices(id *identity) int {
157 seen := []string{}
158 for _, e := range id.devices {
159 key := string(e.devicePK)
160 known := false
161 for _, s := range seen {
162 if s == key {
163 known = true
164 break
165 }
166 }
167 if !known {
168 seen = append(seen, key)
169 }
170 }
171 n := 0
172 for _, k := range seen {
173 if deviceIsActive(id, []byte(k)) {
174 n++
175 }
176 }
177 return n
178}
179
180// short abbreviates a long hex string for table cells.
181func short(s string) string {
182 if len(s) <= 16 {
183 return s
184 }
185 return s[:8] + "…" + s[len(s)-8:]
186}