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

backup.gno

4.94 Kb · 158 lines
  1package memba_notes_v1
  2
  3import (
  4	"bytes"
  5	"chain/runtime"
  6	"crypto/bech32"
  7	"crypto/sha256"
  8	"encoding/binary"
  9	"encoding/hex"
 10	"gno.land/p/samcrew/avl"
 11	keys "gno.land/r/samcrew/enckeys_v1"
 12	"strconv"
 13	"strings"
 14)
 15
 16const MaxBackupBytes = 365
 17
 18type backupRecord struct {
 19	Record               []byte
 20	Mode                 uint8
 21	Generation, Revision uint64
 22	PublicKeyHash        []byte
 23	Last                 receipt
 24}
 25
 26var backupLatest = avl.NewTree()
 27var backups = avl.NewTree()
 28
 29// A wallet-protected historical backup cannot become Vault-safe by relabelling.
 30var standardBackupKeys = avl.NewTree()
 31
 32func backupKey(who address, generation uint64) string {
 33	s := strconv.FormatUint(generation, 10)
 34	return string(who) + ":" + strings.Repeat("0", 20-len(s)) + s
 35}
 36func backupText(record []byte, pos, cap int) (string, int) {
 37	if pos+4 > len(record) {
 38		panic("notes: backup truncated")
 39	}
 40	size := int(binary.BigEndian.Uint32(record[pos : pos+4]))
 41	pos += 4
 42	if size < 1 || size > cap || pos+size > len(record) {
 43		panic("notes: backup text length")
 44	}
 45	for _, b := range record[pos : pos+size] {
 46		if b < 33 || b > 126 {
 47			panic("notes: backup ascii")
 48		}
 49	}
 50	return string(record[pos : pos+size]), pos + size
 51}
 52func validateBackup(who address, record []byte) (uint8, uint64, []byte) {
 53	if len(record) < 185 || len(record) > MaxBackupBytes || string(record[:4]) != "MNKB" || record[4] != 1 || record[5] != 1 {
 54		panic("notes: backup format")
 55	}
 56	mode := record[6]
 57	kdf := record[7]
 58	memory := binary.BigEndian.Uint32(record[8:12])
 59	passes := binary.BigEndian.Uint32(record[12:16])
 60	lanes := record[16]
 61	if !(mode == 1 && kdf == 1 && memory == 0 && passes == 0 && lanes == 0 || mode == 2 && kdf == 2 && memory == 65536 && passes == 3 && lanes == 1) {
 62		panic("notes: backup kdf profile")
 63	}
 64	chain, pos := backupText(record, 33, 64)
 65	realmPath, pos := backupText(record, pos, 128)
 66	if chain != runtime.ChainID() || realmPath != RealmPath || len(record) != pos+132 {
 67		panic("notes: backup network/length")
 68	}
 69	encoded, err := bech32.EncodeFromBase256("g", record[pos:pos+20])
 70	if err != nil || address(encoded) != who {
 71		panic("notes: backup address")
 72	}
 73	pos += 20
 74	generation := binary.BigEndian.Uint64(record[pos : pos+8])
 75	pos += 8
 76	hash := record[pos : pos+32]
 77	pk, gen, active := keys.GetKey(who, keys.SuiteXWingV1)
 78	h := sha256.Sum256(pk)
 79	if generation == 0 || generation != gen || !active || !bytes.Equal(hash, h[:]) {
 80		panic("notes: backup registry identity")
 81	}
 82	return mode, generation, copied(hash)
 83}
 84func SetSeedBackup(cur realm, baseRevision uint64, record []byte, opID [16]byte) {
 85	who := enter(cur, false)
 86	defer leave()
 87	if !cur.Previous().IsUserCall() {
 88		panic("notes: backup directly from account")
 89	}
 90	if opID == ([16]byte{}) || baseRevision == ^uint64(0) {
 91		panic("notes: invalid backup operation")
 92	}
 93	var revision uint64
 94	if v, ok := backupLatest.Get(string(who)); ok {
 95		r := v.(*backupRecord)
 96		revision = r.Revision
 97		if r.Last.Operation == opID {
 98			panic("notes: operation id reused")
 99		}
100	}
101	if revision != baseRevision {
102		panic("notes: stale backup revision")
103	}
104	mode, generation, hash := validateBackup(who, record)
105	exposureKey := string(who) + ":" + hex.EncodeToString(hash)
106	if mode == 2 {
107		if _, seen := standardBackupKeys.Get(exposureKey); seen {
108			panic("notes: vault requires independent identity")
109		}
110	} else {
111		standardBackupKeys.Set(exposureKey, true)
112	}
113	r := &backupRecord{copied(record), mode, generation, revision + 1, hash, receipt{opID, who, runtime.ChainHeight()}}
114	backups.Set(backupKey(who, generation), r)
115	backupLatest.Set(string(who), r)
116}
117func backupJSON(r *backupRecord, withRecord bool) string {
118	s := `{"mode":` + strconv.Itoa(int(r.Mode)) + `,"suite":"xwing-v1","generation":` + u(r.Generation) + `,"revision":` + u(r.Revision) + `,"public_key_hash":` + blob(r.PublicKeyHash) + `,"op_id":` + q(idText(r.Last.Operation)) + `,"actor":` + q(string(r.Last.Actor)) + `,"height":` + q(strconv.FormatInt(r.Last.Height, 10))
119	if withRecord {
120		s += `,"record":` + blob(r.Record)
121	}
122	return s + `}`
123}
124func SeedBackupJSON(who address) string {
125	v, ok := backupLatest.Get(string(keys.CanonicalAddress(who)))
126	if !ok {
127		return "null"
128	}
129	return backupJSON(v.(*backupRecord), true)
130}
131func SeedBackupAtJSON(who address, generation uint64) string {
132	v, ok := backups.Get(backupKey(keys.CanonicalAddress(who), generation))
133	if !ok {
134		return "null"
135	}
136	return backupJSON(v.(*backupRecord), true)
137}
138func SeedBackupsJSON(who address, cursor uint64, limit int) string {
139	who = keys.CanonicalAddress(who)
140	if limit < 1 || limit > MaxPage {
141		panic("notes: page limit")
142	}
143	items := []string{}
144	var next uint64
145	backups.Iterate(backupKey(who, cursor), string(who)+":\xff", func(k string, v interface{}) bool {
146		r := v.(*backupRecord)
147		if r.Generation <= cursor {
148			return false
149		}
150		items = append(items, backupJSON(r, false))
151		next = r.Generation
152		return len(items) == limit
153	})
154	if len(items) < limit {
155		next = 0
156	}
157	return `{"items":[` + strings.Join(items, ",") + `],"next_cursor":` + u(next) + `}`
158}