storage.go 11.6 KB
Newer Older
1
package irmaclient
2
3

import (
Ivar Derksen's avatar
Ivar Derksen committed
4
	"encoding/binary"
5
	"encoding/json"
6
	"path/filepath"
7
	"time"
8

9
10
	"github.com/go-errors/errors"

11
	"github.com/privacybydesign/gabi"
12
13
	"github.com/privacybydesign/gabi/revocation"
	irma "github.com/privacybydesign/irmago"
14
	"github.com/privacybydesign/irmago/internal/fs"
15
16

	"github.com/go-errors/errors"
17
	"go.etcd.io/bbolt"
18
19
)

Sietse Ringers's avatar
Sietse Ringers committed
20
21
22
// This file contains the storage struct and its methods,
// and some general filesystem functions.

23
// Storage provider for a Client
24
type storage struct {
25
	storagePath   string
Ivar Derksen's avatar
Ivar Derksen committed
26
	db            *bbolt.DB
27
	Configuration *irma.Configuration
28
29
}

30
31
32
33
type transaction struct {
	*bbolt.Tx
}

34
35
// Filenames
const databaseFile = "db"
36

Ivar Derksen's avatar
Ivar Derksen committed
37
38
// Bucketnames bbolt
const (
39
40
41
42
43
44
45
46
47
	userdataBucket = "userdata"    // Key/value: specified below
	skKey          = "sk"          // Value: *secretKey
	preferencesKey = "preferences" // Value: Preferences
	updatesKey     = "updates"     // Value: []update
	kssKey         = "kss"         // Value: map[irma.SchemeManagerIdentifier]*keyshareServer

	attributesBucket = "attrs" // Key: irma.CredentialIdentifier, value: []*irma.AttributeList
	logsBucket       = "logs"  // Key: (auto-increment index), value: *LogEntry
	signaturesBucket = "sigs"  // Key: credential.attrs.Hash, value: *gabi.CLSignature
Ivar Derksen's avatar
Ivar Derksen committed
48
49
)

50
func (s *storage) path(p string) string {
51
	return filepath.Join(s.storagePath, p)
52
53
}

Sietse Ringers's avatar
Sietse Ringers committed
54
// EnsureStorageExists initializes the credential storage folder,
Sietse Ringers's avatar
Sietse Ringers committed
55
56
57
// ensuring that it is in a usable state.
// Setting it up in a properly protected location (e.g., with automatic
// backups to iCloud/Google disabled) is the responsibility of the user.
Sietse Ringers's avatar
Sietse Ringers committed
58
func (s *storage) EnsureStorageExists() error {
59
60
	var err error
	if err = fs.AssertPathExists(s.storagePath); err != nil {
61
62
		return err
	}
Ivar Derksen's avatar
Ivar Derksen committed
63
	s.db, err = bbolt.Open(s.path(databaseFile), 0600, &bbolt.Options{Timeout: 1 * time.Second})
64
	return err
65
66
}

67
68
69
70
func (s *storage) Close() error {
	return s.db.Close()
}

71
func (s *storage) BucketExists(name []byte) bool {
Leon's avatar
Leon committed
72
	return s.db.View(func(tx *bbolt.Tx) error {
73
74
75
76
77
78
79
		if tx.Bucket(name) == nil {
			return bbolt.ErrBucketNotFound
		}
		return nil
	}) == nil
}

80
func (s *storage) txStore(tx *transaction, bucketName string, key string, value interface{}) error {
81
82
83
84
85
86
87
88
89
	b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
	if err != nil {
		return err
	}
	btsValue, err := json.Marshal(value)
	if err != nil {
		return err
	}

90
	return b.Put([]byte(key), btsValue)
91
92
}

93
func (s *storage) txDelete(tx *transaction, bucketName string, key string) error {
94
95
96
97
98
99
100
101
	b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
	if err != nil {
		return err
	}

	return b.Delete([]byte(key))
}

102
func (s *storage) txLoad(tx *transaction, bucketName string, key string, dest interface{}) (found bool, err error) {
103
104
105
106
	b := tx.Bucket([]byte(bucketName))
	if b == nil {
		return false, nil
	}
107
	bts := b.Get([]byte(key))
108
	b.Sequence()
109
110
111
112
113
114
	if bts == nil {
		return false, nil
	}
	return true, json.Unmarshal(bts, dest)
}

115
func (s *storage) load(bucketName string, key string, dest interface{}) (found bool, err error) {
116
	err = s.db.View(func(tx *bbolt.Tx) error {
117
		found, err = s.txLoad(&transaction{tx}, bucketName, key, dest)
118
119
120
121
122
		return err
	})
	return
}

123
func (s *storage) Transaction(f func(*transaction) error) error {
124
	return s.db.Update(func(tx *bbolt.Tx) error {
125
		return f(&transaction{tx})
126
127
128
	})
}

129
func (s *storage) TxDeleteSignature(tx *transaction, attrs *irma.AttributeList) error {
130
	return s.txDelete(tx, signaturesBucket, attrs.Hash())
Sietse Ringers's avatar
Sietse Ringers committed
131
}
132

133
134
135
136
func (s *storage) TxDeleteAllSignatures(tx *transaction) error {
	return tx.DeleteBucket([]byte(signaturesBucket))
}

137
138
139
140
141
type clSignatureWitness struct {
	*gabi.CLSignature
	Witness *revocation.Witness
}

142
func (s *storage) TxStoreSignature(tx *transaction, cred *credential) error {
143
144
145
146
	return s.TxStoreCLSignature(tx, cred.AttributeList().Hash(), &clSignatureWitness{
		CLSignature: cred.Signature,
		Witness:     cred.NonRevocationWitness,
	})
147
148
}

149
func (s *storage) TxStoreCLSignature(tx *transaction, credHash string, sig *clSignatureWitness) error {
150
151
152
153
	// We take the SHA256 hash over all attributes as the bucket key for the signature.
	// This means that of the signatures of two credentials that have identical attributes
	// only one gets stored, one overwriting the other - but that doesn't
	// matter, because either one of the signatures is valid over both attribute lists,
Ivar Derksen's avatar
Ivar Derksen committed
154
	// so keeping one of them suffices.
155
	return s.txStore(tx, signaturesBucket, credHash, sig)
156
157
}

Sietse Ringers's avatar
Sietse Ringers committed
158
func (s *storage) StoreSecretKey(sk *secretKey) error {
159
	return s.Transaction(func(tx *transaction) error {
160
161
162
163
		return s.TxStoreSecretKey(tx, sk)
	})
}

164
165
func (s *storage) TxStoreSecretKey(tx *transaction, sk *secretKey) error {
	return s.txStore(tx, userdataBucket, skKey, sk)
Sietse Ringers's avatar
Sietse Ringers committed
166
167
}

168
func (s *storage) StoreAttributes(credTypeID irma.CredentialTypeIdentifier, attrlistlist []*irma.AttributeList) error {
169
	return s.Transaction(func(tx *transaction) error {
170
		return s.TxStoreAttributes(tx, credTypeID, attrlistlist)
171
172
173
	})
}

174
func (s *storage) TxStoreAttributes(tx *transaction, credTypeID irma.CredentialTypeIdentifier,
175
176
	attrlistlist []*irma.AttributeList) error {

177
178
	// If no credentials are left of a certain type, the full entry can be deleted.
	if len(attrlistlist) == 0 {
179
		return s.txDelete(tx, attributesBucket, credTypeID.String())
180
	}
181
	return s.txStore(tx, attributesBucket, credTypeID.String(), attrlistlist)
182
183
}

184
185
func (s *storage) TxDeleteAllAttributes(tx *transaction) error {
	return tx.DeleteBucket([]byte(attributesBucket))
186
187
}

188
func (s *storage) StoreKeyshareServers(keyshareServers map[irma.SchemeManagerIdentifier]*keyshareServer) error {
189
	return s.Transaction(func(tx *transaction) error {
190
191
		return s.TxStoreKeyshareServers(tx, keyshareServers)
	})
192
193
}

194
195
func (s *storage) TxStoreKeyshareServers(tx *transaction, keyshareServers map[irma.SchemeManagerIdentifier]*keyshareServer) error {
	return s.txStore(tx, userdataBucket, kssKey, keyshareServers)
Sietse Ringers's avatar
Sietse Ringers committed
196
197
}

198
func (s *storage) AddLogEntry(entry *LogEntry) error {
Ivar Derksen's avatar
Ivar Derksen committed
199
	return s.db.Update(func(tx *bbolt.Tx) error {
200
		return s.TxAddLogEntry(&transaction{tx}, entry)
Ivar Derksen's avatar
Ivar Derksen committed
201
202
203
	})
}

204
func (s *storage) TxAddLogEntry(tx *transaction, entry *LogEntry) error {
Ivar Derksen's avatar
Ivar Derksen committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
	b, err := tx.CreateBucketIfNotExists([]byte(logsBucket))
	if err != nil {
		return err
	}

	entry.ID, err = b.NextSequence()
	if err != nil {
		return err
	}
	k := s.logEntryKeyToBytes(entry.ID)
	v, err := json.Marshal(entry)

	return b.Put(k, v)
}

func (s *storage) logEntryKeyToBytes(id uint64) []byte {
	k := make([]byte, 8)
	binary.BigEndian.PutUint64(k, id)
	return k
224
225
}

226
func (s *storage) StorePreferences(prefs Preferences) error {
227
	return s.Transaction(func(tx *transaction) error {
228
229
230
231
		return s.TxStorePreferences(tx, prefs)
	})
}

232
233
func (s *storage) TxStorePreferences(tx *transaction, prefs Preferences) error {
	return s.txStore(tx, userdataBucket, preferencesKey, prefs)
234
235
}

Sietse Ringers's avatar
Sietse Ringers committed
236
func (s *storage) StoreUpdates(updates []update) (err error) {
237
	return s.Transaction(func(tx *transaction) error {
238
239
		return s.TxStoreUpdates(tx, updates)
	})
240
241
}

242
243
func (s *storage) TxStoreUpdates(tx *transaction, updates []update) error {
	return s.txStore(tx, userdataBucket, updatesKey, updates)
244
245
}

246
247
248
func (s *storage) LoadSignature(attrs *irma.AttributeList) (*gabi.CLSignature, *revocation.Witness, error) {
	sig := new(clSignatureWitness)
	found, err := s.load(signaturesBucket, attrs.Hash(), sig)
249
	if err != nil {
250
		return nil, nil, err
251
	} else if !found {
252
		return nil, nil, errors.Errorf("Signature of credential with hash %s cannot be found", attrs.Hash())
253
	}
254
	if sig.Witness != nil {
255
		pk, err := s.Configuration.Revocation.Keys.PublicKey(
256
			attrs.CredentialType().IssuerIdentifier(),
257
			sig.Witness.SignedAccumulator.PKIndex,
258
259
260
261
262
263
264
265
		)
		if err != nil {
			return nil, nil, err
		}
		if err = sig.Witness.Verify(pk); err != nil {
			return nil, nil, err
		}
	}
266
	return sig.CLSignature, sig.Witness, nil
267
268
269
270
271
272
}

// LoadSecretKey retrieves and returns the secret key from bbolt storage, or if no secret key
// was found in storage, it generates, saves, and returns a new secret key.
func (s *storage) LoadSecretKey() (*secretKey, error) {
	sk := &secretKey{}
273
	found, err := s.load(userdataBucket, skKey, sk)
274
275
276
277
278
279
	if err != nil {
		return nil, err
	}
	if found {
		return sk, nil
	}
280

Sietse Ringers's avatar
Sietse Ringers committed
281
	if sk, err = generateSecretKey(); err != nil {
282
283
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
284
	if err = s.StoreSecretKey(sk); err != nil {
285
286
287
		return nil, err
	}
	return sk, nil
Sietse Ringers's avatar
Sietse Ringers committed
288
289
}

290
291
292
293
294
295
296
297
func (s *storage) LoadAttributes() (list map[irma.CredentialTypeIdentifier][]*irma.AttributeList, err error) {
	list = make(map[irma.CredentialTypeIdentifier][]*irma.AttributeList)
	return list, s.db.View(func(tx *bbolt.Tx) error {
		b := tx.Bucket([]byte(attributesBucket))
		if b == nil {
			return nil
		}
		return b.ForEach(func(key, value []byte) error {
298
299
300
			credTypeID := irma.NewCredentialTypeIdentifier(string(key))

			var attrlistlist []*irma.AttributeList
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
			err = json.Unmarshal(value, &attrlistlist)
			if err != nil {
				return err
			}

			// Initialize metadata attributes
			for _, attrlist := range attrlistlist {
				attrlist.MetadataAttribute = irma.MetadataFromInt(attrlist.Ints[0], s.Configuration)
			}

			list[credTypeID] = attrlistlist
			return nil
		})
	})
}

func (s *storage) LoadKeyshareServers() (ksses map[irma.SchemeManagerIdentifier]*keyshareServer, err error) {
	ksses = make(map[irma.SchemeManagerIdentifier]*keyshareServer)
319
	_, err = s.load(userdataBucket, kssKey, &ksses)
320
	return
321
322
}

323
// Returns all logs stored before log with ID 'index' sorted from new to old with
324
// a maximum result length of 'max'.
325
326
327
func (s *storage) LoadLogsBefore(index uint64, max int) ([]*LogEntry, error) {
	return s.loadLogs(max, func(c *bbolt.Cursor) (key, value []byte) {
		c.Seek(s.logEntryKeyToBytes(index))
328
329
330
331
332
333
		return c.Prev()
	})
}

// Returns the latest logs stored sorted from new to old with a maximum result length of 'max'
func (s *storage) LoadNewestLogs(max int) ([]*LogEntry, error) {
334
	return s.loadLogs(max, func(c *bbolt.Cursor) (key, value []byte) {
335
336
337
338
339
340
341
		return c.Last()
	})
}

// Returns the logs stored sorted from new to old with a maximum result length of 'max' where the starting position
// of the bbolt cursor can be manipulated by the anonymous function 'startAt'. 'startAt' should return
// the key and the value of the first element from the bbolt database that should be loaded.
342
343
func (s *storage) loadLogs(max int, startAt func(*bbolt.Cursor) (key, value []byte)) ([]*LogEntry, error) {
	logs := make([]*LogEntry, 0, max)
Ivar Derksen's avatar
Ivar Derksen committed
344
345
	return logs, s.db.View(func(tx *bbolt.Tx) error {
		bucket := tx.Bucket([]byte(logsBucket))
346
347
348
349
350
		if bucket == nil {
			return nil
		}
		c := bucket.Cursor()

351
		for k, v := startAt(c); k != nil && len(logs) < max; k, v = c.Prev() {
352
353
354
355
356
357
358
359
360
			var log LogEntry
			if err := json.Unmarshal(v, &log); err != nil {
				return err
			}

			logs = append(logs, &log)
		}
		return nil
	})
Sietse Ringers's avatar
Sietse Ringers committed
361
}
362

363
364
func (s *storage) LoadUpdates() (updates []update, err error) {
	updates = []update{}
365
	_, err = s.load(userdataBucket, updatesKey, &updates)
366
367
368
	return
}

369
370
func (s *storage) LoadPreferences() (Preferences, error) {
	config := defaultPreferences
371
	_, err := s.load(userdataBucket, preferencesKey, &config)
372
	return config, err
373
}
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397

func (s *storage) TxDeleteUserdata(tx *transaction) error {
	return tx.DeleteBucket([]byte(userdataBucket))
}

func (s *storage) TxDeleteLogs(tx *transaction) error {
	return tx.DeleteBucket([]byte(logsBucket))
}

func (s *storage) TxDeleteAll(tx *transaction) error {
	if err := s.TxDeleteAllAttributes(tx); err != nil && err != bbolt.ErrBucketNotFound {
		return err
	}
	if err := s.TxDeleteAllSignatures(tx); err != nil && err != bbolt.ErrBucketNotFound {
		return err
	}
	if err := s.TxDeleteUserdata(tx); err != nil && err != bbolt.ErrBucketNotFound {
		return err
	}
	if err := s.TxDeleteLogs(tx); err != nil && err != bbolt.ErrBucketNotFound {
		return err
	}
	return nil
}
Leon's avatar
Leon committed
398
399
400
401
402
403

func (s *storage) DeleteAll() error {
	return s.Transaction(func(tx *transaction) error {
		return s.TxDeleteAll(tx)
	})
}