-
Notifications
You must be signed in to change notification settings - Fork 0
/
store.go
345 lines (292 loc) · 9.35 KB
/
store.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
// Copyright 2017 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.
package merklestore
import (
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"sync"
"time"
"github.com/keybase/client/go/libkb"
"github.com/keybase/client/go/protocol/keybase1"
)
type MerkleStoreError struct {
msg string
}
func (e MerkleStoreError) Error() string {
return fmt.Sprintf("MerkleStore: %s", e.msg)
}
func NewMerkleStoreError(msgf string, a ...interface{}) MerkleStoreError {
return MerkleStoreError{msg: fmt.Sprintf(msgf, a...)}
}
// Bump this to ignore existing cache entries.
const dbVersion = 1
type dbKit struct {
DBVersion int
Hash keybase1.MerkleStoreKitHash
Kit keybase1.MerkleStoreKit
}
// MerkleStore is the way verify data stored on the server matches the hash
// which is published in the merkle root. This allows an auditable trail for
// data the clients fetch from the server and use for proof or other
// validation.
// Talks to MerkleClient
// Has an in-memory and LocalDB cache.
type MerkleStoreImpl struct {
libkb.Contextified
sync.Mutex
// human readable tag for logs/error reporting
tag string
// server endpoint to fetch stored data
endpoint string
// latest supported version
supportedVersion keybase1.MerkleStoreSupportedVersion
// getter for merkle hash we want to verify against
getHash func(libkb.MerkleRoot) string
// path to load kit from a file while debugging, if present this will be
// used instead of requesting data from the server, helpful for debugging.
kitFilename string
mem *dbKit
}
var _ libkb.MerkleStore = (*MerkleStoreImpl)(nil)
func NewMerkleStore(g *libkb.GlobalContext, tag, endpoint, kitFilename string, supportedVersion keybase1.MerkleStoreSupportedVersion,
getHash func(root libkb.MerkleRoot) string) libkb.MerkleStore {
return &MerkleStoreImpl{
Contextified: libkb.NewContextified(g),
tag: tag,
endpoint: endpoint,
kitFilename: kitFilename,
supportedVersion: supportedVersion,
getHash: getHash,
}
}
type merkleStoreKitT struct {
KitVersion int `json:"kit_version"`
Ctime int `json:"ctime"`
// Versioned entries of the store
Tab map[int]json.RawMessage `json:"tab"`
}
// GetLatestEntry returns the latest (active) entry for the given MerkleStore
func (s *MerkleStoreImpl) GetLatestEntry(m libkb.MetaContext) (ret keybase1.MerkleStoreEntry, err error) {
kitJSON, hash, err := s.getKitString(m)
if err != nil {
return ret, err
}
var kit merkleStoreKitT
if err = json.Unmarshal([]byte(kitJSON), &kit); err != nil {
return ret, NewMerkleStoreError("unmarshalling kit: %s", err)
}
sub, ok := kit.Tab[int(s.supportedVersion)]
if !ok {
return ret, NewMerkleStoreError("missing %s for version: %d", s.tag, s.supportedVersion)
}
if len(sub) == 0 {
return ret, NewMerkleStoreError("empty %s for version: %d", s.tag, s.supportedVersion)
}
return keybase1.MerkleStoreEntry{
Hash: hash,
Entry: keybase1.MerkleStoreEntryString(sub),
}, nil
}
// Get stored kit as a string. First it makes sure that the merkle root is
// recent enough. Using the hash from that, it fetches from in-memory falling
// back to db falling back to server.
func (s *MerkleStoreImpl) getKitString(m libkb.MetaContext) (
keybase1.MerkleStoreKit, keybase1.MerkleStoreKitHash, error) {
// Use a file instead if specified.
if len(s.kitFilename) > 0 {
m.CDebugf("MerkleStore: using kit file: %s", s.kitFilename)
return s.readFile(s.kitFilename)
}
mc := m.G().GetMerkleClient()
if mc == nil {
return "", "", NewMerkleStoreError("no MerkleClient available")
}
s.Lock()
defer s.Unlock()
root := mc.LastRoot()
// The time that the root was fetched is used rather than when the
// root was published so that we can continue to operate even if
// the root has not been published in a long time.
if (root == nil) || s.pastDue(m, root.Fetched(), libkb.MerkleStoreShouldRefresh) {
m.CDebugf("MerkleStore: merkle root should refresh")
// Attempt a refresh if the root is old or nil.
err := s.refreshRoot(m)
if err != nil {
m.CDebugf("MerkleStore: could not refresh merkle root: %s", err)
} else {
root = mc.LastRoot()
}
}
if root == nil {
return "", "", NewMerkleStoreError("no merkle root")
}
if s.pastDue(m, root.Fetched(), libkb.MerkleStoreRequireRefresh) {
// The root is still too old, even after an attempted refresh.
m.CDebugf("MerkleStore: merkle root too old")
return "", "", NewMerkleStoreError("merkle root too old: %v %s", seqnoWrap(root.Seqno()), root.Fetched())
}
// This is the hash we are being instructed to use.
hash := keybase1.MerkleStoreKitHash(s.getHash(*root))
if hash == "" {
return "", "", NewMerkleStoreError("merkle root has empty %s hash: %v", s.tag, seqnoWrap(root.Seqno()))
}
// Use in-memory cache if it matches
if fromMem := s.memGet(hash); fromMem != nil {
m.VLogf(libkb.VLog0, "MerkleStore: mem cache hit %s, using hash: %s", s.tag, hash)
return *fromMem, hash, nil
}
// Use db cache if it matches
if fromDB := s.dbGet(m, hash); fromDB != nil {
m.CDebugf("MerkleStore: db cache hit")
// Store to memory
s.memSet(hash, *fromDB)
m.CDebugf("MerkleStore: using hash: %s", hash)
return *fromDB, hash, nil
}
// Fetch from the server
// This validates the hash
kitJSON, err := s.fetch(m, hash)
if err != nil {
return "", "", err
}
// Store to memory
s.memSet(hash, kitJSON)
// db write
s.dbSet(m.BackgroundWithLogTags(), hash, kitJSON)
m.CDebugf("MerkleStore: using hash: %s", hash)
return kitJSON, hash, nil
}
type merkleStoreServerRes struct {
Status libkb.AppStatus `json:"status"`
KitJSON keybase1.MerkleStoreKit `json:"kit_json"`
}
func (r *merkleStoreServerRes) GetAppStatus() *libkb.AppStatus {
return &r.Status
}
// Fetch data and check the hash.
func (s *MerkleStoreImpl) fetch(m libkb.MetaContext, hash keybase1.MerkleStoreKitHash) (keybase1.MerkleStoreKit, error) {
m.CDebugf("MerkleStore: fetching from server: %s", hash)
var res merkleStoreServerRes
err := m.G().API.GetDecode(libkb.APIArg{
Endpoint: s.endpoint,
SessionType: libkb.APISessionTypeNONE,
MetaContext: m,
Args: libkb.HTTPArgs{
"hash": libkb.S{Val: string(hash)},
},
}, &res)
if err != nil {
return "", NewMerkleStoreError(err.Error())
}
if res.KitJSON == "" {
return "", NewMerkleStoreError("server returned empty kit for %s", s.tag)
}
if s.hash(res.KitJSON) != hash {
m.CDebugf("%s hash mismatch: got:%s expected:%s", s.tag, s.hash(res.KitJSON), hash)
return "", NewMerkleStoreError("server returned wrong kit for %s", s.tag)
}
return res.KitJSON, nil
}
// updateRoot kicks MerkleClient to update its merkle root
// by doing a LookupUser on some arbitrary user.
func (s *MerkleStoreImpl) refreshRoot(m libkb.MetaContext) error {
q := libkb.NewHTTPArgs()
// The user lookup here is unnecessary. It is done because that is what is
// easy with MerkleClient. The user looked up is you if known, otherwise
// arbitrarily t_alice. If t_alice is removed, this path will break.
uid := s.G().GetMyUID()
if len(uid) == 0 {
// Use t_alice's uid.
uid = libkb.TAliceUID
}
q.Add("uid", libkb.UIDArg(uid))
_, err := s.G().MerkleClient.LookupUser(m, q, nil)
return err
}
func (s *MerkleStoreImpl) memGet(hash keybase1.MerkleStoreKitHash) *keybase1.MerkleStoreKit {
if s.mem != nil {
if s.mem.Hash == hash {
ret := s.mem.Kit
return &ret
}
}
return nil
}
func (s *MerkleStoreImpl) memSet(hash keybase1.MerkleStoreKitHash, kitJSON keybase1.MerkleStoreKit) {
s.mem = &dbKit{
DBVersion: dbVersion,
Hash: hash,
Kit: kitJSON,
}
}
// Get from local db. Can return nil.
func (s *MerkleStoreImpl) dbGet(m libkb.MetaContext, hash keybase1.MerkleStoreKitHash) *keybase1.MerkleStoreKit {
db := m.G().LocalDb
if db == nil {
return nil
}
var entry dbKit
if found, err := db.GetInto(&entry, s.dbKey()); err != nil {
m.CDebugf("MerkleStore: error reading from db: %s", err)
return nil
} else if !found {
return nil
}
if entry.DBVersion != dbVersion {
return nil
}
if entry.Hash == hash {
return &entry.Kit
}
return nil
}
// Logs errors.
func (s *MerkleStoreImpl) dbSet(m libkb.MetaContext, hash keybase1.MerkleStoreKitHash, kitJSON keybase1.MerkleStoreKit) {
db := m.G().LocalDb
if db == nil {
m.CDebugf("dbSet: no db")
return
}
entry := dbKit{
DBVersion: dbVersion,
Hash: hash,
Kit: kitJSON,
}
if err := db.PutObj(s.dbKey(), nil, entry); err != nil {
m.CDebugf("dbSet: %s", err)
}
}
// hex of sha512
func (s *MerkleStoreImpl) hash(in keybase1.MerkleStoreKit) keybase1.MerkleStoreKitHash {
buf := sha512.Sum512([]byte(in))
out := hex.EncodeToString(buf[:])
return keybase1.MerkleStoreKitHash(out)
}
func (s *MerkleStoreImpl) pastDue(m libkb.MetaContext, event time.Time, limit time.Duration) bool {
diff := m.G().Clock().Now().Sub(event)
isOverdue := diff > limit
if isOverdue {
m.CDebugf("MerkleStore: pastDue diff:(%s) t1:(%s) limit:(%s)", diff, event, limit)
}
return isOverdue
}
func (s *MerkleStoreImpl) readFile(path string) (keybase1.MerkleStoreKit, keybase1.MerkleStoreKitHash, error) {
buf, err := ioutil.ReadFile(path)
kitJSON := keybase1.MerkleStoreKit(string(buf))
return kitJSON, s.hash(kitJSON), err
}
func (s *MerkleStoreImpl) dbKey() libkb.DbKey {
return libkb.DbKey{
Typ: libkb.DBMerkleStore,
Key: s.tag,
}
}
func seqnoWrap(x *keybase1.Seqno) int64 {
if x == nil {
return 0
}
return int64(*x)
}