storage.go 10 KB
Newer Older
1
2
3
4
5
package irmago

import (
	"encoding/json"
	"errors"
6
	"io"
7
8
9
10
	"io/ioutil"
	"os"
	"strconv"

Sietse Ringers's avatar
Sietse Ringers committed
11
12
13
14
	"crypto/rand"
	"encoding/hex"
	"math/big"
	"path"
15

16
17
	"time"

18
	"github.com/mhe/gabi"
19
20
21
22
23
24
)

// Filenames in which we store stuff
const (
	skFile         = "sk"
	attributesFile = "attrs"
25
26
	kssFile        = "kss"
	paillierFile   = "paillier"
27
	updatesFile    = "updates"
28
29
30
	signaturesDir  = "sigs"
)

Sietse Ringers's avatar
Sietse Ringers committed
31
// PathExists checks if the specified path exists.
32
func PathExists(path string) (bool, error) {
33
34
35
36
37
38
39
40
41
42
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return true, err
}

43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func ensureDirectoryExists(path string) error {
	exists, err := PathExists(path)
	if err != nil {
		return err
	}
	if exists {
		return nil
	}
	return os.Mkdir(path, 0700)
}

// writeFile writes the contents of reader to a new or truncated file at dest.
func writeFile(reader io.Reader, dest string) error {
	destfile, err := os.Create(dest)
	if err != nil {
		return err
	}
	if _, err := io.Copy(destfile, reader); err != nil {
		destfile.Close()
		return err
	}
	return destfile.Close()
}

67
68
69
70
71
72
73
74
75
76
77
78
// NewCredentialManager creates a new CredentialManager that uses the directory
// specified by storagePath for (de)serializing itself. irmaConfigurationPath
// is the path to a (possibly readonly) folder containing irma_configuration;
// androidStoragePath is an optional path to the files of the old android app
// (specify "" if you do not want to parse the old android app files),
// and keyshareHandler is used for when a registration to a keyshare server needs
// to happen.
// The credential manager returned by this function has been fully deserialized
// and is ready for use.
//
// NOTE: It is the responsibility of the caller that there exists a directory
// at storagePath!
79
func NewCredentialManager(
80
81
	storagePath string,
	irmaConfigurationPath string,
82
	androidStoragePath string,
83
	keyshareHandler KeyshareHandler,
84
85
86
) (*CredentialManager, error) {
	var err error
	cm := &CredentialManager{
87
88
		credentials:           make(map[CredentialTypeIdentifier]map[int]*credential),
		keyshareServers:       make(map[SchemeManagerIdentifier]*keyshareServer),
89
		attributes:            make(map[CredentialTypeIdentifier][]*AttributeList),
90
		irmaConfigurationPath: irmaConfigurationPath,
91
92
		androidStoragePath:    androidStoragePath,
		Store:                 NewConfigurationStore(storagePath + "/irma_configuration"),
93
94
	}

95
	// Ensure storage path exists, and populate it with necessary files
96
97
	cm.storagePath = storagePath
	if err = cm.ensureStorageExists(); err != nil {
98
		return nil, err
99
	}
100
	if err = cm.Store.ParseFolder(); err != nil {
101
		return nil, err
102
	}
103

104
105
106
107
108
109
	// Perform new update functions from credentialManagerUpdates, if any
	if err = cm.update(); err != nil {
		return nil, err
	}

	// Load our stuff
110
	if cm.secretkey, err = cm.loadSecretKey(); err != nil {
111
		return nil, err
112
	}
113
	if cm.attributes, err = cm.loadAttributes(); err != nil {
114
		return nil, err
115
	}
116
	if cm.paillierKeyCache, err = cm.loadPaillierKeys(); err != nil {
117
		return nil, err
Sietse Ringers's avatar
Sietse Ringers committed
118
	}
119
	if cm.keyshareServers, err = cm.loadKeyshareServers(); err != nil {
120
		return nil, err
121
	}
Sietse Ringers's avatar
Sietse Ringers committed
122
123
124

	unenrolled := cm.unenrolledKeyshareServers()
	switch len(unenrolled) {
125
	case 0: // nop
Sietse Ringers's avatar
Sietse Ringers committed
126
	case 1:
Sietse Ringers's avatar
Sietse Ringers committed
127
		if keyshareHandler == nil {
128
			return nil, errors.New("Keyshare server found but no KeyshareHandler was given")
Sietse Ringers's avatar
Sietse Ringers committed
129
		}
130
		keyshareHandler.StartRegistration(unenrolled[0], func(email, pin string) {
Sietse Ringers's avatar
Sietse Ringers committed
131
132
			cm.KeyshareEnroll(unenrolled[0].Identifier(), email, pin)
		})
Sietse Ringers's avatar
Sietse Ringers committed
133
	default:
134
		return nil, errors.New("Too many keyshare servers")
Sietse Ringers's avatar
Sietse Ringers committed
135
136
	}

137
	return cm, nil
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
// update performs any function from credentialManagerUpdates that has not
// already been executed in the past, keeping track of previously executed updates
// in the file at updatesFile.
func (cm *CredentialManager) update() error {
	// Load and parse file containing info about already performed updates
	exists, err := PathExists(cm.path(updatesFile))
	if err != nil {
		return err
	}
	if !exists {
		cm.updates = []update{}
	} else {
		bytes, err := ioutil.ReadFile(cm.path(updatesFile))
		if err != nil {
			return err
		}
		if err = json.Unmarshal(bytes, &cm.updates); err != nil {
			return err
		}
	}

	// Perform all new updates
	for i := len(cm.updates); i < len(credentialManagerUpdates); i++ {
		if err := credentialManagerUpdates[i](cm); err != nil {
			return err
		}
		cm.updates = append(cm.updates,
			update{
				When:   Timestamp(time.Now()),
				Number: i,
			},
		)
	}

	// Save updates file
	bytes, err := json.Marshal(cm.updates)
	if err != nil {
		return err
	}
	cm.saveFile(cm.path(updatesFile), bytes)

	return nil
}

184
185
186
187
func (cm *CredentialManager) path(file string) string {
	return cm.storagePath + "/" + file
}

Sietse Ringers's avatar
Sietse Ringers committed
188
189
190
191
192
193
194
195
196
func (cm *CredentialManager) signatureFilename(id string, counter int) string {
	return cm.path(signaturesDir) + "/" + id + "-" + strconv.Itoa(counter)
}

// ensureStorageExists initializes the credential storage folder,
// ensuring that it is in a usable state.
// NOTE: we do not create the folder if it does not exist!
// Setting it up in a properly protected location (e.g., with automatic
// backups to iCloud/Google disabled) is the responsibility of the user.
197
func (cm *CredentialManager) ensureStorageExists() error {
198
	exist, err := PathExists(cm.storagePath)
199
	if err != nil {
200
		return err
201
202
203
204
205
	}
	if !exist {
		return errors.New("credential storage path does not exist")
	}

206
207
208
209
210
211
212
213
214
215
216
217
	if exist, err = PathExists(cm.Store.path); err != nil {
		return err
	}
	if !exist {
		if err = ensureDirectoryExists(cm.Store.path); err != nil {
			return err
		}
		cm.Store.Copy(cm.irmaConfigurationPath, false)
		if err = cm.Store.ParseFolder(); err != nil {
			return err
		}
	}
218
	return ensureDirectoryExists(cm.path(signaturesDir))
219
220
}

Sietse Ringers's avatar
Sietse Ringers committed
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
func (cm *CredentialManager) storeSecretKey(sk *big.Int) error {
	return ioutil.WriteFile(cm.path(skFile), sk.Bytes(), 0600)
}

// Save the filecontents at the specified path atomically:
// - first save the content in a temp file with a random filename in the same dir
// - then rename the temp file to the specified filepath, overwriting the old file
func (cm *CredentialManager) saveFile(filepath string, content []byte) (err error) {
	dir := path.Dir(filepath)

	// Read random data for filename and convert to hex
	randBytes := make([]byte, 16)
	_, err = rand.Read(randBytes)
	if err != nil {
		return
	}
	tempfilename := hex.EncodeToString(randBytes)

	// Create temp file
	err = ioutil.WriteFile(dir+"/"+tempfilename, content, 0600)
	if err != nil {
		return
	}

	// Rename, overwriting old file
	return os.Rename(dir+"/"+tempfilename, filepath)
247
248
}

Sietse Ringers's avatar
Sietse Ringers committed
249
func (cm *CredentialManager) storeSignature(cred *credential, counter int) (err error) {
250
251
252
253
254
255
256
257
258
259
	if cred.CredentialType() == nil {
		return errors.New("cannot add unknown credential type")
	}

	credbytes, err := json.Marshal(cred.Signature)
	if err != nil {
		return err
	}

	// TODO existence check
260
	filename := cm.signatureFilename(cred.CredentialType().Identifier().String(), counter)
261
262
263
264
265
	err = ioutil.WriteFile(filename, credbytes, 0600)
	return
}

func (cm *CredentialManager) storeAttributes() (err error) {
266
	attrbytes, err := json.Marshal(cm.attributes)
267
	if err != nil {
268
		return err
269
270
271
272
273
274
	}

	// TODO existence check
	err = ioutil.WriteFile(cm.path(attributesFile), attrbytes, 0600)
	return
}
Sietse Ringers's avatar
Sietse Ringers committed
275

276
func (cm *CredentialManager) storeKeyshareServers() (err error) {
277
	bts, err := json.Marshal(cm.keyshareServers)
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
	if err != nil {
		return
	}
	err = ioutil.WriteFile(cm.path(kssFile), bts, 0600)
	return
}

func (cm *CredentialManager) storePaillierKeys() (err error) {
	bts, err := json.Marshal(cm.paillierKeyCache)
	if err != nil {
		return
	}
	err = ioutil.WriteFile(cm.path(paillierFile), bts, 0600)
	return
}

294
func (cm *CredentialManager) loadSignature(id CredentialTypeIdentifier, counter int) (signature *gabi.CLSignature, err error) {
Sietse Ringers's avatar
Cleanup    
Sietse Ringers committed
295
296
	sigpath := cm.signatureFilename(id.String(), counter)
	exists, err := PathExists(sigpath)
297
	if err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
298
299
		return
	}
300
301
302
	if !exists {
		return nil, errors.New("Signature file not found")
	}
Sietse Ringers's avatar
Cleanup    
Sietse Ringers committed
303
	bytes, err := ioutil.ReadFile(sigpath)
304
305
306
	if err != nil {
		return
	}
Sietse Ringers's avatar
Sietse Ringers committed
307
308
309
310
311
312
313
314
	signature = new(gabi.CLSignature)
	err = json.Unmarshal(bytes, signature)
	return
}

// loadSecretKey retrieves and returns the secret key from storage, or if no secret key
// was found in storage, it generates, saves, and returns a new secret key.
func (cm *CredentialManager) loadSecretKey() (*big.Int, error) {
315
	exists, err := PathExists(cm.path(skFile))
Sietse Ringers's avatar
Sietse Ringers committed
316
317
318
319
	if err != nil {
		return nil, err
	}
	if exists {
320
321
322
		var bytes []byte
		if bytes, err = ioutil.ReadFile(cm.path(skFile)); err == nil {
			return new(big.Int).SetBytes(bytes), nil
Sietse Ringers's avatar
Sietse Ringers committed
323
		}
324
		return nil, err
Sietse Ringers's avatar
Sietse Ringers committed
325
	}
326
327
328
329
330
331
332
333
334
335

	sk, err := cm.generateSecretKey()
	if err != nil {
		return nil, err
	}
	err = cm.storeSecretKey(sk)
	if err != nil {
		return nil, err
	}
	return sk, nil
Sietse Ringers's avatar
Sietse Ringers committed
336
337
}

338
339
func (cm *CredentialManager) loadAttributes() (list map[CredentialTypeIdentifier][]*AttributeList, err error) {
	list = make(map[CredentialTypeIdentifier][]*AttributeList)
340
	exists, err := PathExists(cm.path(attributesFile))
Sietse Ringers's avatar
Sietse Ringers committed
341
342
343
344
345
346
347
	if err != nil || !exists {
		return
	}
	bytes, err := ioutil.ReadFile(cm.path(attributesFile))
	if err != nil {
		return nil, err
	}
348
	err = json.Unmarshal(bytes, &list)
349
350
351
	if err != nil {
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
352

353
354
	for _, attrlistlist := range list {
		for _, attrlist := range attrlistlist {
355
			attrlist.MetadataAttribute = MetadataFromInt(attrlist.Ints[0], cm.Store)
356
357
358
		}
	}

359
	return list, nil
Sietse Ringers's avatar
Sietse Ringers committed
360
}
361
362
363
364
365
366
367
368
369
370
371

func (cm *CredentialManager) loadKeyshareServers() (ksses map[SchemeManagerIdentifier]*keyshareServer, err error) {
	ksses = make(map[SchemeManagerIdentifier]*keyshareServer)
	exists, err := PathExists(cm.path(kssFile))
	if err != nil || !exists {
		return
	}
	bytes, err := ioutil.ReadFile(cm.path(kssFile))
	if err != nil {
		return nil, err
	}
372
	err = json.Unmarshal(bytes, &ksses)
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
	if err != nil {
		return nil, err
	}
	return
}

func (cm *CredentialManager) loadPaillierKeys() (key *paillierPrivateKey, err error) {
	exists, err := PathExists(cm.path(paillierFile))
	if err != nil || !exists {
		return
	}
	bytes, err := ioutil.ReadFile(cm.path(paillierFile))
	if err != nil {
		return nil, err
	}
	key = new(paillierPrivateKey)
	err = json.Unmarshal(bytes, key)
	if err != nil {
		return nil, err
	}
	return
}