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}