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

import (
Ivar Derksen's avatar
Ivar Derksen committed
4
	"encoding/binary"
5
	"encoding/json"
6
	"github.com/go-errors/errors"
7
	"path/filepath"
8
	"time"
9

10
	"github.com/privacybydesign/gabi"
11
12
	"github.com/privacybydesign/irmago"
	"github.com/privacybydesign/irmago/internal/fs"
13
	"go.etcd.io/bbolt"
14
15
)

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

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

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

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

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

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

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

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

67
func (s *storage) txStore(tx *transaction, bucketName string, key string, value interface{}) error {
68
69
70
71
72
73
74
75
76
	b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
	if err != nil {
		return err
	}
	btsValue, err := json.Marshal(value)
	if err != nil {
		return err
	}

77
	return b.Put([]byte(key), btsValue)
78
79
}

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

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

89
func (s *storage) txLoad(tx *transaction, bucketName string, key string, dest interface{}) (found bool, err error) {
90
91
92
93
	b := tx.Bucket([]byte(bucketName))
	if b == nil {
		return false, nil
	}
94
	bts := b.Get([]byte(key))
95
	b.Sequence()
96
97
98
99
100
101
	if bts == nil {
		return false, nil
	}
	return true, json.Unmarshal(bts, dest)
}

102
func (s *storage) load(bucketName string, key string, dest interface{}) (found bool, err error) {
103
	err = s.db.View(func(tx *bbolt.Tx) error {
104
		found, err = s.txLoad(&transaction{tx}, bucketName, key, dest)
105
106
107
108
109
		return err
	})
	return
}

110
func (s *storage) DoStoreTransaction(f func(*transaction) error) error {
111
	return s.db.Update(func(tx *bbolt.Tx) error {
112
		return f(&transaction{tx})
113
114
115
	})
}

116
func (s *storage) TxDeleteSignature(tx *transaction, attrs *irma.AttributeList) error {
117
	return s.txDelete(tx, signaturesBucket, attrs.Hash())
Sietse Ringers's avatar
Sietse Ringers committed
118
}
119

120
121
122
123
124
125
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)
126
127
}

128
func (s *storage) TxStoreCLSignature(tx *transaction, credHash string, sig *gabi.CLSignature) error {
129
130
131
132
	// 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
133
	// so keeping one of them suffices.
134
	return s.txStore(tx, signaturesBucket, credHash, sig)
135
136
}

Sietse Ringers's avatar
Sietse Ringers committed
137
func (s *storage) StoreSecretKey(sk *secretKey) error {
138
	return s.DoStoreTransaction(func(tx *transaction) error {
139
140
141
142
		return s.TxStoreSecretKey(tx, sk)
	})
}

143
144
func (s *storage) TxStoreSecretKey(tx *transaction, sk *secretKey) error {
	return s.txStore(tx, userdataBucket, skKey, sk)
Sietse Ringers's avatar
Sietse Ringers committed
145
146
}

147
func (s *storage) StoreAttributes(credTypeID irma.CredentialTypeIdentifier, attrlistlist []*irma.AttributeList) error {
148
	return s.DoStoreTransaction(func(tx *transaction) error {
149
		return s.TxStoreAttributes(tx, credTypeID, attrlistlist)
150
151
152
	})
}

153
func (s *storage) TxStoreAttributes(tx *transaction, credTypeID irma.CredentialTypeIdentifier,
154
155
	attrlistlist []*irma.AttributeList) error {

156
157
	// If no credentials are left of a certain type, the full entry can be deleted.
	if len(attrlistlist) == 0 {
158
		return s.txDelete(tx, attributesBucket, credTypeID.String())
159
	}
160
	return s.txStore(tx, attributesBucket, credTypeID.String(), attrlistlist)
161
162
}

163
164
func (s *storage) TxDeleteAllAttributes(tx *transaction) error {
	return tx.DeleteBucket([]byte(attributesBucket))
165
166
}

167
func (s *storage) StoreKeyshareServers(keyshareServers map[irma.SchemeManagerIdentifier]*keyshareServer) error {
168
	return s.DoStoreTransaction(func(tx *transaction) error {
169
170
		return s.TxStoreKeyshareServers(tx, keyshareServers)
	})
171
172
}

173
174
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
175
176
}

177
func (s *storage) AddLogEntry(entry *LogEntry) error {
Ivar Derksen's avatar
Ivar Derksen committed
178
	return s.db.Update(func(tx *bbolt.Tx) error {
179
		return s.TxAddLogEntry(&transaction{tx}, entry)
Ivar Derksen's avatar
Ivar Derksen committed
180
181
182
	})
}

183
func (s *storage) TxAddLogEntry(tx *transaction, entry *LogEntry) error {
Ivar Derksen's avatar
Ivar Derksen committed
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
	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
203
204
}

205
func (s *storage) StorePreferences(prefs Preferences) error {
206
	return s.DoStoreTransaction(func(tx *transaction) error {
207
208
209
210
		return s.TxStorePreferences(tx, prefs)
	})
}

211
212
func (s *storage) TxStorePreferences(tx *transaction, prefs Preferences) error {
	return s.txStore(tx, userdataBucket, preferencesKey, prefs)
213
214
}

Sietse Ringers's avatar
Sietse Ringers committed
215
func (s *storage) StoreUpdates(updates []update) (err error) {
216
	return s.DoStoreTransaction(func(tx *transaction) error {
217
218
		return s.TxStoreUpdates(tx, updates)
	})
219
220
}

221
222
func (s *storage) TxStoreUpdates(tx *transaction, updates []update) error {
	return s.txStore(tx, userdataBucket, updatesKey, updates)
223
224
225
226
}

func (s *storage) LoadSignature(attrs *irma.AttributeList) (signature *gabi.CLSignature, err error) {
	signature = new(gabi.CLSignature)
227
	found, err := s.load(signaturesBucket, attrs.Hash(), signature)
228
229
230
231
232
233
234
235
236
237
238
239
	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{}
240
	found, err := s.load(userdataBucket, skKey, sk)
241
242
243
244
245
246
	if err != nil {
		return nil, err
	}
	if found {
		return sk, nil
	}
247

Sietse Ringers's avatar
Sietse Ringers committed
248
	if sk, err = generateSecretKey(); err != nil {
249
250
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
251
	if err = s.StoreSecretKey(sk); err != nil {
252
253
254
		return nil, err
	}
	return sk, nil
Sietse Ringers's avatar
Sietse Ringers committed
255
256
}

257
258
259
260
261
262
263
264
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 {
265
266
267
			credTypeID := irma.NewCredentialTypeIdentifier(string(key))

			var attrlistlist []*irma.AttributeList
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
			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)
286
	_, err = s.load(userdataBucket, kssKey, &ksses)
287
	return
288
289
}

290
// Returns all logs stored before log with ID 'index' sorted from new to old with
291
// a maximum result length of 'max'.
292
293
294
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))
295
296
297
298
299
300
		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) {
301
	return s.loadLogs(max, func(c *bbolt.Cursor) (key, value []byte) {
302
303
304
305
306
307
308
		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.
309
310
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
311
312
	return logs, s.db.View(func(tx *bbolt.Tx) error {
		bucket := tx.Bucket([]byte(logsBucket))
313
314
315
316
317
		if bucket == nil {
			return nil
		}
		c := bucket.Cursor()

318
		for k, v := startAt(c); k != nil && len(logs) < max; k, v = c.Prev() {
319
320
321
322
323
324
325
326
327
			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
328
}
329

330
331
func (s *storage) LoadUpdates() (updates []update, err error) {
	updates = []update{}
332
	_, err = s.load(userdataBucket, updatesKey, &updates)
333
334
335
	return
}

336
337
func (s *storage) LoadPreferences() (Preferences, error) {
	config := defaultPreferences
338
	_, err := s.load(userdataBucket, preferencesKey, &config)
339
	return config, err
340
}