storage.go 9.9 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
// Filenames
const databaseFile = "db"
28

Ivar Derksen's avatar
Ivar Derksen committed
29 30
// Bucketnames bbolt
const (
31 32 33 34 35 36 37 38 39
	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
40 41
)

42
func (s *storage) path(p string) string {
43
	return filepath.Join(s.storagePath, p)
44 45
}

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

59 60 61 62
func (s *storage) Close() error {
	return s.db.Close()
}

63
func (s *storage) txStore(tx *bbolt.Tx, key string, value interface{}, bucketName string) error {
64 65 66 67 68 69 70 71 72
	b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
	if err != nil {
		return err
	}
	btsValue, err := json.Marshal(value)
	if err != nil {
		return err
	}

73
	return b.Put([]byte(key), btsValue)
74 75
}

76 77 78 79 80 81 82 83 84
func (s *storage) txDelete(tx *bbolt.Tx, key string, bucketName string) error {
	b, err := tx.CreateBucketIfNotExists([]byte(bucketName))
	if err != nil {
		return err
	}

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

85
func (s *storage) txLoad(tx *bbolt.Tx, key string, dest interface{}, bucketName string) (found bool, err error) {
86 87 88 89
	b := tx.Bucket([]byte(bucketName))
	if b == nil {
		return false, nil
	}
90
	bts := b.Get([]byte(key))
91 92 93 94 95 96
	if bts == nil {
		return false, nil
	}
	return true, json.Unmarshal(bts, dest)
}

97
func (s *storage) load(key string, dest interface{}, bucketName string) (found bool, err error) {
98 99 100 101 102 103 104
	err = s.db.View(func(tx *bbolt.Tx) error {
		found, err = s.txLoad(tx, key, dest, bucketName)
		return err
	})
	return
}

105
func (s *storage) DeleteSignature(attrs *irma.AttributeList) error {
106
	return s.db.Update(func(tx *bbolt.Tx) error {
107 108 109 110 111 112 113
		return s.txDelete(tx, attrs.Hash(), signaturesBucket)
	})
}

func (s *storage) DeleteAllSignatures() error {
	return s.db.Update(func(tx *bbolt.Tx) error {
		return tx.DeleteBucket([]byte(signaturesBucket))
114
	})
Sietse Ringers's avatar
Sietse Ringers committed
115
}
116

Sietse Ringers's avatar
Sietse Ringers committed
117
func (s *storage) StoreSignature(cred *credential) error {
118 119 120 121 122 123 124 125 126 127
	return s.db.Update(func(tx *bbolt.Tx) error {
		return s.TxStoreSignature(tx, cred.attrs.Hash(), cred.Signature)
	})
}

func (s *storage) TxStoreSignature(tx *bbolt.Tx, credHash string, sig *gabi.CLSignature) error {
	// 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
128
	// so keeping one of them suffices.
129
	return s.txStore(tx, credHash, sig, signaturesBucket)
130 131
}

Sietse Ringers's avatar
Sietse Ringers committed
132
func (s *storage) StoreSecretKey(sk *secretKey) error {
133 134 135 136 137 138 139
	return s.db.Update(func(tx *bbolt.Tx) error {
		return s.TxStoreSecretKey(tx, sk)
	})
}

func (s *storage) TxStoreSecretKey(tx *bbolt.Tx, sk *secretKey) error {
	return s.txStore(tx, skKey, sk, userdataBucket)
Sietse Ringers's avatar
Sietse Ringers committed
140 141
}

142
func (s *storage) StoreAttributes(credTypeID irma.CredentialTypeIdentifier, attrlistlist []*irma.AttributeList) error {
143
	return s.db.Update(func(tx *bbolt.Tx) error {
144
		return s.TxStoreAttributes(tx, credTypeID, attrlistlist)
145 146 147
	})
}

148
func (s *storage) TxStoreAttributes(tx *bbolt.Tx, credTypeID irma.CredentialTypeIdentifier,
149 150
	attrlistlist []*irma.AttributeList) error {

151 152 153 154
	// If no credentials are left of a certain type, the full entry can be deleted.
	if len(attrlistlist) == 0 {
		return s.txDelete(tx, credTypeID.String(), attributesBucket)
	}
155 156 157
	return s.txStore(tx, credTypeID.String(), attrlistlist, attributesBucket)
}

158
func (s *storage) DeleteAllAttributes() error {
159
	return s.db.Update(func(tx *bbolt.Tx) error {
160
		return tx.DeleteBucket([]byte(attributesBucket))
161 162 163
	})
}

164
func (s *storage) StoreKeyshareServers(keyshareServers map[irma.SchemeManagerIdentifier]*keyshareServer) error {
165 166 167
	return s.db.Update(func(tx *bbolt.Tx) error {
		return s.TxStoreKeyshareServers(tx, keyshareServers)
	})
168 169
}

170
func (s *storage) TxStoreKeyshareServers(tx *bbolt.Tx, keyshareServers map[irma.SchemeManagerIdentifier]*keyshareServer) error {
171
	return s.txStore(tx, kssKey, keyshareServers, userdataBucket)
Sietse Ringers's avatar
Sietse Ringers committed
172 173
}

174
func (s *storage) AddLogEntry(entry *LogEntry) error {
Ivar Derksen's avatar
Ivar Derksen committed
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
	return s.db.Update(func(tx *bbolt.Tx) error {
		return s.TxAddLogEntry(tx, entry)
	})
}

func (s *storage) TxAddLogEntry(tx *bbolt.Tx, entry *LogEntry) error {
	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
200 201
}

202
func (s *storage) StorePreferences(prefs Preferences) error {
203 204 205 206 207 208 209
	return s.db.Update(func(tx *bbolt.Tx) error {
		return s.TxStorePreferences(tx, prefs)
	})
}

func (s *storage) TxStorePreferences(tx *bbolt.Tx, prefs Preferences) error {
	return s.txStore(tx, preferencesKey, prefs, userdataBucket)
210 211
}

Sietse Ringers's avatar
Sietse Ringers committed
212
func (s *storage) StoreUpdates(updates []update) (err error) {
213 214 215
	return s.db.Update(func(tx *bbolt.Tx) error {
		return s.TxStoreUpdates(tx, updates)
	})
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
func (s *storage) TxStoreUpdates(tx *bbolt.Tx, updates []update) error {
	return s.txStore(tx, updatesKey, updates, userdataBucket)
}

func (s *storage) LoadSignature(attrs *irma.AttributeList) (signature *gabi.CLSignature, err error) {
	signature = new(gabi.CLSignature)
	found, err := s.load(attrs.Hash(), signature, signaturesBucket)
	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{}
	found, err := s.load(skKey, sk, userdataBucket)
	if err != nil {
		return nil, err
	}
	if found {
		return sk, nil
	}
244

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

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

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

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

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

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

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