enckeys.gno
5.02 Kb · 153 lines
1// Package enckeys_v1 is an ownerless public encryption-key directory.
2// Keys are announcements by accounts, not attestations of identity, custody,
3// recovery strength or successful decryption. There is no fee, admin or pause.
4package enckeys_v1
5
6import (
7 "chain"
8 "chain/runtime"
9 "crypto/bech32"
10 "encoding/base64"
11 "encoding/hex"
12 "strconv"
13
14 "gno.land/p/samcrew/avl"
15)
16
17const SuiteXWingV1 = "xwing-v1"
18const PublicKeyBytes = 1216
19
20type keyRecord struct {
21 Generation uint64
22 PublicKey []byte
23 Operation [16]byte
24 Height int64
25 Revoked bool
26}
27
28var keys = avl.NewTree()
29
30// RegisterKey replaces the caller's announcement using an optimistic lock.
31// expectedGeneration is zero only for first registration. A revoked generation
32// is retained forever; re-registering cannot reuse an old identity generation.
33func RegisterKey(cur realm, suite string, expectedGeneration uint64, publicKey []byte, opID [16]byte) uint64 {
34 caller := user(cur)
35 checkSuite(suite)
36 if len(publicKey) != PublicKeyBytes {
37 panic("enckeys: bad public key length")
38 }
39 old := current(caller)
40 checkUpdate(old, expectedGeneration, opID)
41 rec := &keyRecord{Generation: expectedGeneration + 1, PublicKey: clone(publicKey), Operation: opID, Height: runtime.ChainHeight()}
42 keys.Set(string(caller), rec)
43 emit(caller, rec, "registered")
44 return rec.Generation
45}
46
47// RevokeKey removes the active key but retains the generation and receipt.
48// Historical announcements and wraps remain public chain history.
49func RevokeKey(cur realm, suite string, expectedGeneration uint64, opID [16]byte) uint64 {
50 caller := user(cur)
51 checkSuite(suite)
52 old := current(caller)
53 checkUpdate(old, expectedGeneration, opID)
54 if old == nil || old.Revoked {
55 panic("enckeys: no active key")
56 }
57 rec := &keyRecord{Generation: expectedGeneration + 1, Operation: opID, Height: runtime.ChainHeight(), Revoked: true}
58 keys.Set(string(caller), rec)
59 emit(caller, rec, "revoked")
60 return rec.Generation
61}
62
63// GetKey returns a defensive copy; generation is still returned after revoke.
64func GetKey(addr address, suite string) (publicKey []byte, generation uint64, active bool) {
65 checkSuite(suite)
66 rec := current(CanonicalAddress(addr))
67 if rec == nil {
68 return nil, 0, false
69 }
70 return clone(rec.PublicKey), rec.Generation, !rec.Revoked
71}
72
73// KeyJSON serializes all chain integers as decimal strings. op_id and height
74// identify the last successful mutation for recovery from an unknown outcome.
75func KeyJSON(addr address, suite string) string {
76 checkSuite(suite)
77 who := CanonicalAddress(addr)
78 rec := current(who)
79 prefix := `{"address":` + strconv.Quote(string(who)) + `,"suite":"xwing-v1"`
80 if rec == nil {
81 return prefix + `,"generation":"0","active":false,"public_key":"","op_id":"","height":"0"}`
82 }
83 return prefix + `,"generation":` + strconv.Quote(strconv.FormatUint(rec.Generation, 10)) + `,"active":` + strconv.FormatBool(!rec.Revoked) + `,"public_key":` + strconv.Quote(base64.StdEncoding.EncodeToString(rec.PublicKey)) + `,"op_id":` + strconv.Quote(hex.EncodeToString(rec.Operation[:])) + `,"height":` + strconv.Quote(strconv.FormatInt(rec.Height, 10)) + `}`
84}
85
86func Render(path string) string {
87 if path == "" {
88 return "# Encryption key directory\n\nAccount announcements for the xwing-v1 suite. Revocation does not erase chain history.\n"
89 }
90 return "````json\n" + KeyJSON(address(path), SuiteXWingV1) + "\n````"
91}
92
93// CanonicalAddress accepts the chain's four checksum/case spellings but always
94// returns the lowercase original bech32 spelling that realm callers use.
95func CanonicalAddress(addr address) address {
96 if len(string(addr)) != 40 {
97 panic("enckeys: invalid address")
98 }
99 hrp, raw, err := bech32.DecodeToBase256(string(addr))
100 if err != nil || hrp != "g" || len(raw) != 20 {
101 panic("enckeys: invalid address")
102 }
103 out, err := bech32.EncodeFromBase256("g", raw)
104 if err != nil {
105 panic("enckeys: invalid address")
106 }
107 return address(out)
108}
109
110func user(cur realm) address {
111 if !cur.IsCurrent() {
112 panic("enckeys: stale realm")
113 }
114 if !cur.Previous().IsUserCall() {
115 panic("enckeys: call directly from your account")
116 }
117 assertNoCoins()
118 return cur.Previous().Address()
119}
120func checkSuite(suite string) {
121 if suite != SuiteXWingV1 {
122 panic("enckeys: unknown suite")
123 }
124}
125func current(addr address) *keyRecord {
126 v, ok := keys.Get(string(addr))
127 if !ok {
128 return nil
129 }
130 return v.(*keyRecord)
131}
132func checkUpdate(old *keyRecord, expected uint64, op [16]byte) {
133 generation := uint64(0)
134 if old != nil {
135 generation = old.Generation
136 }
137 if generation != expected {
138 panic("enckeys: stale generation")
139 }
140 if expected == ^uint64(0) {
141 panic("enckeys: generation exhausted")
142 }
143 if op == ([16]byte{}) {
144 panic("enckeys: empty operation id")
145 }
146 if old != nil && old.Operation == op {
147 panic("enckeys: operation id reused")
148 }
149}
150func clone(b []byte) []byte { return append([]byte(nil), b...) }
151func emit(addr address, rec *keyRecord, action string) {
152 chain.Emit("EncryptionKey", "address", string(addr), "suite", SuiteXWingV1, "generation", strconv.FormatUint(rec.Generation, 10), "action", action, "op_id", hex.EncodeToString(rec.Operation[:]))
153}