storage.go 10.9 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/irmago"
	"github.com/privacybydesign/irmago/internal/fs"
14
	"go.etcd.io/bbolt"
15
16
)

Sietse Ringers's avatar
Sietse Ringers committed
17
18
19
// This file contains the storage struct and its methods,
// and some general filesystem functions.

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

27
28
29
30
type transaction struct {
	*bbolt.Tx
}

31
32
// Filenames
const databaseFile = "db"
33

Ivar Derksen's avatar
Ivar Derksen committed
34
35
// Bucketnames bbolt
const (
36
37
38
39
40
41
42
43
44
	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
45
46
)

47
func (s *storage) path(p string) string {
48
	return filepath.Join(s.storagePath, p)
49
50
}

Sietse Ringers's avatar
Sietse Ringers committed
51
// EnsureStorageExists initializes the credential storage folder,
Sietse Ringers's avatar
Sietse Ringers committed
52
53
54
// 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
55
func (s *storage) EnsureStorageExists() error {
56
57
	var err error
	if err = fs.AssertPathExists(s.storagePath); err != nil {
58
59
		return err
	}
Ivar Derksen's avatar
Ivar Derksen committed
60
	s.db, err = bbolt.Open(s.path(databaseFile), 0600, &bbolt.Options{Timeout: 1 * time.Second})
61
	return err
62
63
}

64
65
66
67
func (s *storage) Close() error {
	return s.db.Close()
}

68
69
70
71
72
73
74
75
76
func (s *storage) BucketExists(name []byte) bool {
	return s.Transaction(func(tx *transaction) error {
		if tx.Bucket(name) == nil {
			return bbolt.ErrBucketNotFound
		}
		return nil
	}) == nil
}

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

87
	return b.Put([]byte(key), btsValue)
88
89
}

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

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

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

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

120
func (s *storage) Transaction(f func(*transaction) error) error {
121
	return s.db.Update(func(tx *bbolt.Tx) error {
122
		return f(&transaction{tx})
123
124
125
	})
}

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

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

func (s *storage) TxStoreSignature(tx *transaction, cred *credential) error {
	return s.TxStoreCLSignature(tx, cred.AttributeList().Hash(), cred.Signature)
136
137
}

138
func (s *storage) TxStoreCLSignature(tx *transaction, credHash string, sig *gabi.CLSignature) error {
139
140
141
142
	// 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
143
	// so keeping one of them suffices.
144
	return s.txStore(tx, signaturesBucket, credHash, sig)
145
146
}

Sietse Ringers's avatar
Sietse Ringers committed
147
func (s *storage) StoreSecretKey(sk *secretKey) error {
148
	return s.Transaction(func(tx *transaction) error {
149
150
151
152
		return s.TxStoreSecretKey(tx, sk)
	})
}

153
154
func (s *storage) TxStoreSecretKey(tx *transaction, sk *secretKey) error {
	return s.txStore(tx, userdataBucket, skKey, sk)
Sietse Ringers's avatar
Sietse Ringers committed
155
156
}

157
func (s *storage) StoreAttributes(credTypeID irma.CredentialTypeIdentifier, attrlistlist []*irma.AttributeList) error {
158
	return s.Transaction(func(tx *transaction) error {
159
		return s.TxStoreAttributes(tx, credTypeID, attrlistlist)
160
161
162
	})
}

163
func (s *storage) TxStoreAttributes(tx *transaction, credTypeID irma.CredentialTypeIdentifier,
164
165
	attrlistlist []*irma.AttributeList) error {

166
167
	// If no credentials are left of a certain type, the full entry can be deleted.
	if len(attrlistlist) == 0 {
168
		return s.txDelete(tx, attributesBucket, credTypeID.String())
169
	}
170
	return s.txStore(tx, attributesBucket, credTypeID.String(), attrlistlist)
171
172
}

173
174
func (s *storage) TxDeleteAllAttributes(tx *transaction) error {
	return tx.DeleteBucket([]byte(attributesBucket))
175
176
}

177
func (s *storage) StoreKeyshareServers(keyshareServers map[irma.SchemeManagerIdentifier]*keyshareServer) error {
178
	return s.Transaction(func(tx *transaction) error {
179
180
		return s.TxStoreKeyshareServers(tx, keyshareServers)
	})
181
182
}

183
184
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
185
186
}

187
func (s *storage) AddLogEntry(entry *LogEntry) error {
Ivar Derksen's avatar
Ivar Derksen committed
188
	return s.db.Update(func(tx *bbolt.Tx) error {
189
		return s.TxAddLogEntry(&transaction{tx}, entry)
Ivar Derksen's avatar
Ivar Derksen committed
190
191
192
	})
}

193
func (s *storage) TxAddLogEntry(tx *transaction, entry *LogEntry) error {
Ivar Derksen's avatar
Ivar Derksen committed
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
	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
213
214
}

215
func (s *storage) StorePreferences(prefs Preferences) error {
216
	return s.Transaction(func(tx *transaction) error {
217
218
219
220
		return s.TxStorePreferences(tx, prefs)
	})
}

221
222
func (s *storage) TxStorePreferences(tx *transaction, prefs Preferences) error {
	return s.txStore(tx, userdataBucket, preferencesKey, prefs)
223
224
}

Sietse Ringers's avatar
Sietse Ringers committed
225
func (s *storage) StoreUpdates(updates []update) (err error) {
226
	return s.Transaction(func(tx *transaction) error {
227
228
		return s.TxStoreUpdates(tx, updates)
	})
229
230
}

231
232
func (s *storage) TxStoreUpdates(tx *transaction, updates []update) error {
	return s.txStore(tx, userdataBucket, updatesKey, updates)
233
234
235
236
}

func (s *storage) LoadSignature(attrs *irma.AttributeList) (signature *gabi.CLSignature, err error) {
	signature = new(gabi.CLSignature)
237
	found, err := s.load(signaturesBucket, attrs.Hash(), signature)
238
239
240
241
242
243
244
245
246
247
248
249
	if err != nil {
		return nil, err
	} else if !found {
		return nil, errors.Errorf("Signature of credential with hash %s cannot be found", attrs.Hash())
	}
	return
}

// 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{}
250
	found, err := s.load(userdataBucket, skKey, sk)
251
252
253
254
255
256
	if err != nil {
		return nil, err
	}
	if found {
		return sk, nil
	}
257

Sietse Ringers's avatar
Sietse Ringers committed
258
	if sk, err = generateSecretKey(); err != nil {
259
260
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
261
	if err = s.StoreSecretKey(sk); err != nil {
262
263
264
		return nil, err
	}
	return sk, nil
Sietse Ringers's avatar
Sietse Ringers committed
265
266
}

267
268
269
270
271
272
273
274
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 {
275
276
277
			credTypeID := irma.NewCredentialTypeIdentifier(string(key))

			var attrlistlist []*irma.AttributeList
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
			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)
296
	_, err = s.load(userdataBucket, kssKey, &ksses)
297
	return
298
299
}

300
// Returns all logs stored before log with ID 'index' sorted from new to old with
301
// a maximum result length of 'max'.
302
303
304
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))
305
306
307
308
309
310
		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) {
311
	return s.loadLogs(max, func(c *bbolt.Cursor) (key, value []byte) {
312
313
314
315
316
317
318
		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.
319
320
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
321
322
	return logs, s.db.View(func(tx *bbolt.Tx) error {
		bucket := tx.Bucket([]byte(logsBucket))
323
324
325
326
327
		if bucket == nil {
			return nil
		}
		c := bucket.Cursor()

328
		for k, v := startAt(c); k != nil && len(logs) < max; k, v = c.Prev() {
329
330
331
332
333
334
335
336
337
			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
338
}
339

340
341
func (s *storage) LoadUpdates() (updates []update, err error) {
	updates = []update{}
342
	_, err = s.load(userdataBucket, updatesKey, &updates)
343
344
345
	return
}

346
347
func (s *storage) LoadPreferences() (Preferences, error) {
	config := defaultPreferences
348
	_, err := s.load(userdataBucket, preferencesKey, &config)
349
	return config, err
350
}
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374

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
}