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

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}