client.go 24.1 KB
Newer Older
1
package irmaclient
2
3

import (
4
	"crypto/rand"
Sietse Ringers's avatar
Sietse Ringers committed
5
	"math/big"
Sietse Ringers's avatar
Sietse Ringers committed
6
	"sort"
7
	"time"
Sietse Ringers's avatar
Sietse Ringers committed
8

9
	"github.com/credentials/go-go-gadget-paillier"
10
	raven "github.com/getsentry/raven-go"
Sietse Ringers's avatar
Sietse Ringers committed
11
	"github.com/go-errors/errors"
12
	"github.com/mhe/gabi"
13
14
	"github.com/privacybydesign/irmago"
	"github.com/privacybydesign/irmago/internal/fs"
15
16
)

17
// This file contains most methods of the Client (c.f. session.go
Sietse Ringers's avatar
Sietse Ringers committed
18
19
// and updates.go).
//
Sietse Ringers's avatar
Sietse Ringers committed
20
21
22
23
24
25
26
27
// Clients are the main entry point into this package for the user of this package.
// The Client struct:
// - (De)serializes credentials and keyshare server information
// from storage, as well as logs of earlier IRMA sessions
// - it provides access to the attributes and all related information of its credentials,
// - it is the starting point for new IRMA sessions;
// - and it computes some of the messages in the client side of the IRMA protocol.
//
Sietse Ringers's avatar
Sietse Ringers committed
28
29
30
31
32
33
34
// The storage of credentials is split up in several parts:
//
// - The CL-signature of each credential is stored separately, so that we can
// load it on demand (i.e., during an IRMA session), instead of immediately
// at initialization.
//
// - The attributes of all credentials are stored together, as they all
35
// immediately need to be available anyway.
Sietse Ringers's avatar
Sietse Ringers committed
36
37
38
39
40
//
// - The secret key (the zeroth attribute of every credential), being the same
// across all credentials, is stored only once in a separate file (storing this
// in multiple places would be bad).

41
type Client struct {
Sietse Ringers's avatar
Sietse Ringers committed
42
	// Stuff we manage on disk
43
	secretkey        *secretKey
44
45
46
	attributes       map[irma.CredentialTypeIdentifier][]*irma.AttributeList
	credentials      map[irma.CredentialTypeIdentifier]map[int]*credential
	keyshareServers  map[irma.SchemeManagerIdentifier]*keyshareServer
47
	paillierKeyCache *paillierPrivateKey
Sietse Ringers's avatar
Sietse Ringers committed
48
	logs             []*LogEntry
Sietse Ringers's avatar
Sietse Ringers committed
49
	updates          []update
50

Sietse Ringers's avatar
Sietse Ringers committed
51
52
53
54
	// Where we store/load it to/from
	storage storage

	// Other state
55
	Preferences              Preferences
56
	Configuration            *irma.Configuration
57
	UnenrolledSchemeManagers []irma.SchemeManagerIdentifier
58
59
60
	irmaConfigurationPath    string
	androidStoragePath       string
	handler                  ClientHandler
61
	state                    *issuanceState
62
63
}

64
65
66
67
// SentryDSN should be set in the init() function
// Setting it to an empty string means no crash reports
var SentryDSN = ""

68
69
type Preferences struct {
	EnableCrashReporting bool
70
71
}

72
var defaultPreferences = Preferences{
73
	EnableCrashReporting: true,
74
75
}

76
// KeyshareHandler is used for asking the user for his email address and PIN,
77
// for enrolling at a keyshare server.
78
type KeyshareHandler interface {
Tomas's avatar
Tomas committed
79
	EnrollmentFailure(manager irma.SchemeManagerIdentifier, err error)
80
	EnrollmentSuccess(manager irma.SchemeManagerIdentifier)
81
82
}

Sietse Ringers's avatar
Sietse Ringers committed
83
84
// ClientHandler informs the user that the configuration or the list of attributes
// that this client uses has been updated.
85
86
87
type ClientHandler interface {
	KeyshareHandler

88
	UpdateConfiguration(new *irma.IrmaIdentifierSet)
89
	UpdateAttributes()
Sietse Ringers's avatar
Sietse Ringers committed
90
91
}

92
93
94
95
type secretKey struct {
	Key *big.Int
}

96
// New creates a new Client that uses the directory
97
98
99
100
// 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),
101
// and handler is used for informing the user of new stuff, and when a
102
// enrollment to a keyshare server needs to happen.
103
// The client returned by this function has been fully deserialized
104
105
// and is ready for use.
//
Sietse Ringers's avatar
Sietse Ringers committed
106
107
// NOTE: It is the responsibility of the caller that there exists a (properly
// protected) directory at storagePath!
108
func New(
109
110
111
	storagePath string,
	irmaConfigurationPath string,
	androidStoragePath string,
112
	handler ClientHandler,
113
) (*Client, error) {
114
	var err error
115
	if err = fs.AssertPathExists(storagePath); err != nil {
116
117
		return nil, err
	}
118
	if err = fs.AssertPathExists(irmaConfigurationPath); err != nil {
119
120
121
		return nil, err
	}

122
	cm := &Client{
123
124
125
		credentials:           make(map[irma.CredentialTypeIdentifier]map[int]*credential),
		keyshareServers:       make(map[irma.SchemeManagerIdentifier]*keyshareServer),
		attributes:            make(map[irma.CredentialTypeIdentifier][]*irma.AttributeList),
126
127
		irmaConfigurationPath: irmaConfigurationPath,
		androidStoragePath:    androidStoragePath,
128
		handler:               handler,
129
130
	}

131
	cm.Configuration, err = irma.NewConfiguration(storagePath+"/irma_configuration", irmaConfigurationPath)
132
133
134
	if err != nil {
		return nil, err
	}
135

136
	schemeMgrErr := cm.Configuration.ParseOrRestoreFolder()
137
138
139
140
141
	// If schemMgrErr is of type SchemeManagerError, we continue and
	// return it at the end; otherwise bail out now
	_, isSchemeMgrErr := schemeMgrErr.(*irma.SchemeManagerError)
	if schemeMgrErr != nil && !isSchemeMgrErr {
		return nil, schemeMgrErr
142
	}
143
144

	// Ensure storage path exists, and populate it with necessary files
145
	cm.storage = storage{storagePath: storagePath, Configuration: cm.Configuration}
Sietse Ringers's avatar
Sietse Ringers committed
146
	if err = cm.storage.EnsureStorageExists(); err != nil {
147
148
149
		return nil, err
	}

150
	if cm.Preferences, err = cm.storage.LoadPreferences(); err != nil {
151
152
		return nil, err
	}
153
	cm.applyPreferences()
154

155
	// Perform new update functions from clientUpdates, if any
156
157
158
159
160
	if err = cm.update(); err != nil {
		return nil, err
	}

	// Load our stuff
Sietse Ringers's avatar
Sietse Ringers committed
161
	if cm.secretkey, err = cm.storage.LoadSecretKey(); err != nil {
162
163
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
164
	if cm.attributes, err = cm.storage.LoadAttributes(); err != nil {
165
166
		return nil, err
	}
167
	if cm.keyshareServers, err = cm.storage.LoadKeyshareServers(); err != nil {
168
169
		return nil, err
	}
170
	if cm.paillierKeyCache, err = cm.storage.LoadPaillierKeys(); err != nil {
171
172
		return nil, err
	}
173
174
175
	if cm.paillierKeyCache == nil {
		cm.paillierKey(false)
	}
176

177
178
	cm.UnenrolledSchemeManagers = cm.unenrolledSchemeManagers()
	if len(cm.UnenrolledSchemeManagers) > 1 {
179
180
181
		return nil, errors.New("Too many keyshare servers")
	}

182
	return cm, schemeMgrErr
183
184
}

185
// CredentialInfoList returns a list of information of all contained credentials.
186
187
func (client *Client) CredentialInfoList() irma.CredentialInfoList {
	list := irma.CredentialInfoList([]*irma.CredentialInfo{})
188

189
	for _, attrlistlist := range client.attributes {
190
191
		for index, attrlist := range attrlistlist {
			info := attrlist.Info()
192
193
194
			if info == nil {
				continue
			}
195
			info.Index = index
196
			list = append(list, info)
Sietse Ringers's avatar
Sietse Ringers committed
197
198
		}
	}
199

Sietse Ringers's avatar
Sietse Ringers committed
200
201
202
203
	sort.Sort(list)
	return list
}

204
// addCredential adds the specified credential to the Client, saving its signature
Sietse Ringers's avatar
Sietse Ringers committed
205
// imediately, and optionally cm.attributes as well.
206
func (client *Client) addCredential(cred *credential, storeAttributes bool) (err error) {
207
208
209
210
	id := irma.NewCredentialTypeIdentifier("")
	if cred.CredentialType() != nil {
		id = cred.CredentialType().Identifier()
	}
Sietse Ringers's avatar
Sietse Ringers committed
211

212
	// Don't add duplicate creds
213
	for _, attrlistlist := range client.attributes {
214
		for _, attrs := range attrlistlist {
215
			if attrs.Hash() == cred.AttributeList().Hash() {
216
217
218
219
220
221
				return nil
			}
		}
	}

	// If this is a singleton credential type, ensure we have at most one by removing any previous instance
222
	if !id.Empty() && cred.CredentialType().IsSingleton && len(client.creds(id)) > 0 {
223
		client.remove(id, 0, false) // Index is 0, because if we're here we have exactly one
224
225
226
	}

	// Append the new cred to our attributes and credentials
227
	client.attributes[id] = append(client.attrs(id), cred.AttributeList())
228
229
230
231
232
233
	if !id.Empty() {
		if _, exists := client.credentials[id]; !exists {
			client.credentials[id] = make(map[int]*credential)
		}
		counter := len(client.attributes[id]) - 1
		client.credentials[id][counter] = cred
Sietse Ringers's avatar
Sietse Ringers committed
234
235
	}

236
	if err = client.storage.StoreSignature(cred); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
237
238
239
		return
	}
	if storeAttributes {
240
		err = client.storage.StoreAttributes(client.attributes)
Sietse Ringers's avatar
Sietse Ringers committed
241
242
243
244
245
246
247
248
249
250
251
252
253
254
	}
	return
}

func generateSecretKey() (*secretKey, error) {
	key, err := gabi.RandomBigInt(gabi.DefaultSystemParameters[1024].Lm)
	if err != nil {
		return nil, err
	}
	return &secretKey{Key: key}, nil
}

// Removal methods

255
func (client *Client) remove(id irma.CredentialTypeIdentifier, index int, storenow bool) error {
256
	// Remove attributes
257
	list, exists := client.attributes[id]
258
259
260
261
	if !exists || index >= len(list) {
		return errors.Errorf("Can't remove credential %s-%d: no such credential", id.String(), index)
	}
	attrs := list[index]
262
	client.attributes[id] = append(list[:index], list[index+1:]...)
263
	if storenow {
264
		if err := client.storage.StoreAttributes(client.attributes); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
265
266
			return err
		}
267
268
269
	}

	// Remove credential
270
	if creds, exists := client.credentials[id]; exists {
271
		if _, exists := creds[index]; exists {
Sietse Ringers's avatar
Sietse Ringers committed
272
			delete(creds, index)
273
			client.credentials[id] = creds
274
275
276
277
		}
	}

	// Remove signature from storage
278
	if err := client.storage.DeleteSignature(attrs); err != nil {
279
280
281
		return err
	}

282
	removed := map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
283
284
285
	removed[id] = attrs.Strings()

	if storenow {
286
		return client.addLogEntry(&LogEntry{
287
			Type:    actionRemoval,
288
			Time:    irma.Timestamp(time.Now()),
289
290
291
292
			Removed: removed,
		})
	}
	return nil
293
294
}

Sietse Ringers's avatar
Sietse Ringers committed
295
// RemoveCredential removes the specified credential.
296
func (client *Client) RemoveCredential(id irma.CredentialTypeIdentifier, index int) error {
297
	return client.remove(id, index, true)
298
299
}

Sietse Ringers's avatar
Sietse Ringers committed
300
// RemoveCredentialByHash removes the specified credential.
301
302
func (client *Client) RemoveCredentialByHash(hash string) error {
	cred, index, err := client.credentialByHash(hash)
303
304
305
	if err != nil {
		return err
	}
306
	return client.RemoveCredential(cred.CredentialType().Identifier(), index)
307
308
}

Sietse Ringers's avatar
Sietse Ringers committed
309
// RemoveAllCredentials removes all credentials.
310
func (client *Client) RemoveAllCredentials() error {
311
	removed := map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
312
	for _, attrlistlist := range client.attributes {
Sietse Ringers's avatar
Sietse Ringers committed
313
314
315
316
		for _, attrs := range attrlistlist {
			if attrs.CredentialType() != nil {
				removed[attrs.CredentialType().Identifier()] = attrs.Strings()
			}
317
			client.storage.DeleteSignature(attrs)
318
319
		}
	}
320
	client.attributes = map[irma.CredentialTypeIdentifier][]*irma.AttributeList{}
321
	if err := client.storage.StoreAttributes(client.attributes); err != nil {
322
323
		return err
	}
324
325
326

	logentry := &LogEntry{
		Type:    actionRemoval,
327
		Time:    irma.Timestamp(time.Now()),
328
329
		Removed: removed,
	}
330
	if err := client.addLogEntry(logentry); err != nil {
331
332
		return err
	}
333
	return client.storage.StoreLogs(client.logs)
334
335
}

336
// Attribute and credential getter methods
Sietse Ringers's avatar
Sietse Ringers committed
337

Sietse Ringers's avatar
Sietse Ringers committed
338
// attrs returns cm.attributes[id], initializing it to an empty slice if neccesary
339
func (client *Client) attrs(id irma.CredentialTypeIdentifier) []*irma.AttributeList {
340
	list, exists := client.attributes[id]
Sietse Ringers's avatar
Sietse Ringers committed
341
	if !exists {
342
		list = make([]*irma.AttributeList, 0, 1)
343
		client.attributes[id] = list
Sietse Ringers's avatar
Sietse Ringers committed
344
345
346
347
348
	}
	return list
}

// creds returns cm.credentials[id], initializing it to an empty map if neccesary
349
func (client *Client) creds(id irma.CredentialTypeIdentifier) map[int]*credential {
350
	list, exists := client.credentials[id]
Sietse Ringers's avatar
Sietse Ringers committed
351
	if !exists {
Sietse Ringers's avatar
Sietse Ringers committed
352
		list = make(map[int]*credential)
353
		client.credentials[id] = list
Sietse Ringers's avatar
Sietse Ringers committed
354
355
356
357
	}
	return list
}

Sietse Ringers's avatar
Sietse Ringers committed
358
// Attributes returns the attribute list of the requested credential, or nil if we do not have it.
359
func (client *Client) Attributes(id irma.CredentialTypeIdentifier, counter int) (attributes *irma.AttributeList) {
360
	list := client.attrs(id)
Sietse Ringers's avatar
Sietse Ringers committed
361
	if len(list) <= counter {
Sietse Ringers's avatar
Sietse Ringers committed
362
363
364
365
366
		return
	}
	return list[counter]
}

367
368
func (client *Client) credentialByHash(hash string) (*credential, int, error) {
	for _, attrlistlist := range client.attributes {
369
		for index, attrs := range attrlistlist {
370
			if attrs.Hash() == hash {
371
				cred, err := client.credential(attrs.CredentialType().Identifier(), index)
372
373
374
375
376
377
378
				return cred, index, err
			}
		}
	}
	return nil, 0, nil
}

379
func (client *Client) credentialByID(id irma.CredentialIdentifier) (*credential, error) {
380
	if _, exists := client.attributes[id.Type]; !exists {
Sietse Ringers's avatar
Sietse Ringers committed
381
382
		return nil, nil
	}
383
	for index, attrs := range client.attributes[id.Type] {
384
		if attrs.Hash() == id.Hash {
385
			return client.credential(attrs.CredentialType().Identifier(), index)
Sietse Ringers's avatar
Sietse Ringers committed
386
387
388
		}
	}
	return nil, nil
389
390
}

Sietse Ringers's avatar
Sietse Ringers committed
391
// credential returns the requested credential, or nil if we do not have it.
392
func (client *Client) credential(id irma.CredentialTypeIdentifier, counter int) (cred *credential, err error) {
Sietse Ringers's avatar
Sietse Ringers committed
393
	// If the requested credential is not in credential map, we check if its attributes were
394
	// deserialized during New(). If so, there should be a corresponding signature file,
Sietse Ringers's avatar
Sietse Ringers committed
395
	// so we read that, construct the credential, and add it to the credential map
396
397
	if _, exists := client.creds(id)[counter]; !exists {
		attrs := client.Attributes(id, counter)
Sietse Ringers's avatar
Sietse Ringers committed
398
399
400
		if attrs == nil { // We do not have the requested cred
			return
		}
401
		sig, err := client.storage.LoadSignature(attrs)
Sietse Ringers's avatar
Sietse Ringers committed
402
403
404
405
406
407
408
		if err != nil {
			return nil, err
		}
		if sig == nil {
			err = errors.New("signature file not found")
			return nil, err
		}
409
		pk, err := attrs.PublicKey()
410
411
412
		if err != nil {
			return nil, err
		}
413
414
415
		if pk == nil {
			return nil, errors.New("unknown public key")
		}
416
		cred, err := newCredential(&gabi.Credential{
417
			Attributes: append([]*big.Int{client.secretkey.Key}, attrs.Ints...),
418
			Signature:  sig,
419
			Pk:         pk,
420
		}, client.Configuration)
421
422
423
		if err != nil {
			return nil, err
		}
424
		client.credentials[id][counter] = cred
Sietse Ringers's avatar
Sietse Ringers committed
425
426
	}

427
	return client.credentials[id][counter], nil
428
429
}

Sietse Ringers's avatar
Sietse Ringers committed
430
// Methods used in the IRMA protocol
431

432
// Candidates returns a list of attributes present in this client
Sietse Ringers's avatar
Sietse Ringers committed
433
// that satisfy the specified attribute disjunction.
434
435
func (client *Client) Candidates(disjunction *irma.AttributeDisjunction) []*irma.AttributeIdentifier {
	candidates := make([]*irma.AttributeIdentifier, 0, 10)
436
437

	for _, attribute := range disjunction.Attributes {
Sietse Ringers's avatar
Sietse Ringers committed
438
		credID := attribute.CredentialTypeIdentifier()
439
		if !client.Configuration.Contains(credID) {
440
441
			continue
		}
442
		creds := client.attributes[credID]
443
444
445
446
		count := len(creds)
		if count == 0 {
			continue
		}
447
		for _, attrs := range creds {
448
449
450
			if !attrs.IsValid() {
				continue
			}
451
			id := &irma.AttributeIdentifier{Type: attribute, CredentialHash: attrs.Hash()}
452
453
454
			if attribute.IsCredential() {
				candidates = append(candidates, id)
			} else {
455
				val := attrs.UntranslatedAttribute(attribute)
456
				if val == nil {
457
458
					continue
				}
459
				if !disjunction.HasValues() {
460
					candidates = append(candidates, id)
461
462
463
464
465
				} else {
					requiredValue, present := disjunction.Values[attribute]
					if !present || requiredValue == nil || *val == *requiredValue {
						candidates = append(candidates, id)
					}
466
467
468
469
470
471
472
473
				}
			}
		}
	}

	return candidates
}

474
// CheckSatisfiability checks if this client has the required attributes
Sietse Ringers's avatar
Sietse Ringers committed
475
476
// to satisfy the specifed disjunction list. If not, the unsatisfiable disjunctions
// are returned.
477
func (client *Client) CheckSatisfiability(
478
479
480
481
	disjunctions irma.AttributeDisjunctionList,
) ([][]*irma.AttributeIdentifier, irma.AttributeDisjunctionList) {
	candidates := [][]*irma.AttributeIdentifier{}
	missing := irma.AttributeDisjunctionList{}
482
	for i, disjunction := range disjunctions {
483
		candidates = append(candidates, []*irma.AttributeIdentifier{})
484
		candidates[i] = client.Candidates(disjunction)
485
		if len(candidates[i]) == 0 {
486
487
488
			missing = append(missing, disjunction)
		}
	}
489
	return candidates, missing
490
}
491

492
493
func (client *Client) groupCredentials(choice *irma.DisclosureChoice) (map[irma.CredentialIdentifier][]int, error) {
	grouped := make(map[irma.CredentialIdentifier][]int)
494
495
496
	if choice == nil || choice.Attributes == nil {
		return grouped, nil
	}
497
498
499
500
501
502
503
504

	for _, attribute := range choice.Attributes {
		identifier := attribute.Type
		ici := attribute.CredentialIdentifier()

		// If this is the first attribute of its credential type that we encounter
		// in the disclosure choice, then there is no slice yet at grouped[ici]
		if _, present := grouped[ici]; !present {
505
506
			indices := make([]int, 1, 1)
			indices[0] = 1 // Always include metadata
507
508
509
510
511
512
			grouped[ici] = indices
		}

		if identifier.IsCredential() {
			continue // In this case we only disclose the metadata attribute, which is already handled
		}
513
		index, err := client.Configuration.CredentialTypes[identifier.CredentialTypeIdentifier()].IndexOf(identifier)
514
515
516
517
		if err != nil {
			return nil, err
		}

Sietse Ringers's avatar
Sietse Ringers committed
518
		// These indices will be used in the []*big.Int at gabi.credential.Attributes,
519
		// which doesn't know about the secret key and metadata attribute, so +2
520
		grouped[ici] = append(grouped[ici], index+2)
521
522
523
524
525
	}

	return grouped, nil
}

526
// ProofBuilders constructs a list of proof builders for the specified attribute choice.
527
func (client *Client) ProofBuilders(choice *irma.DisclosureChoice) (gabi.ProofBuilderList, error) {
528
	todisclose, err := client.groupCredentials(choice)
529
530
531
532
	if err != nil {
		return nil, err
	}

533
	builders := gabi.ProofBuilderList([]gabi.ProofBuilder{})
534
	for id, list := range todisclose {
535
		cred, err := client.credentialByID(id)
536
537
538
539
540
		if err != nil {
			return nil, err
		}
		builders = append(builders, cred.Credential.CreateDisclosureProofBuilder(list))
	}
Sietse Ringers's avatar
Sietse Ringers committed
541
	return builders, nil
542
}
Sietse Ringers's avatar
Sietse Ringers committed
543

Sietse Ringers's avatar
Sietse Ringers committed
544
// Proofs computes disclosure proofs containing the attributes specified by choice.
545
func (client *Client) Proofs(choice *irma.DisclosureChoice, request irma.IrmaSession, issig bool) (gabi.ProofList, error) {
546
	builders, err := client.ProofBuilders(choice)
Sietse Ringers's avatar
Sietse Ringers committed
547
548
549
	if err != nil {
		return nil, err
	}
550
	return builders.BuildProofList(request.GetContext(), request.GetNonce(), issig), nil
Sietse Ringers's avatar
Sietse Ringers committed
551
552
}

553
554
// IssuanceProofBuilders constructs a list of proof builders in the issuance protocol
// for the future credentials as well as possibly any disclosed attributes.
555
func (client *Client) IssuanceProofBuilders(request *irma.IssuanceRequest) (gabi.ProofBuilderList, error) {
Sietse Ringers's avatar
Cleanup    
Sietse Ringers committed
556
	state, err := newIssuanceState()
Sietse Ringers's avatar
Sietse Ringers committed
557
558
559
	if err != nil {
		return nil, err
	}
560
	client.state = state
Sietse Ringers's avatar
Sietse Ringers committed
561

562
	proofBuilders := gabi.ProofBuilderList([]gabi.ProofBuilder{})
Sietse Ringers's avatar
Sietse Ringers committed
563
	for _, futurecred := range request.Credentials {
Sietse Ringers's avatar
Sietse Ringers committed
564
		var pk *gabi.PublicKey
565
		pk, err = client.Configuration.PublicKey(futurecred.CredentialTypeID.IssuerIdentifier(), futurecred.KeyCounter)
566
567
568
		if err != nil {
			return nil, err
		}
569
		credBuilder := gabi.NewCredentialBuilder(
570
			pk, request.GetContext(), client.secretkey.Key, state.nonce2)
571
		state.builders = append(state.builders, credBuilder)
Sietse Ringers's avatar
Sietse Ringers committed
572
573
		proofBuilders = append(proofBuilders, credBuilder)
	}
Sietse Ringers's avatar
Sietse Ringers committed
574

575
	disclosures, err := client.ProofBuilders(request.Choice)
Sietse Ringers's avatar
Sietse Ringers committed
576
577
578
	if err != nil {
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
579
	proofBuilders = append(disclosures, proofBuilders...)
Sietse Ringers's avatar
Sietse Ringers committed
580
581
	return proofBuilders, nil
}
Sietse Ringers's avatar
Sietse Ringers committed
582

Sietse Ringers's avatar
Sietse Ringers committed
583
584
// IssueCommitments computes issuance commitments, along with disclosure proofs
// specified by choice.
585
func (client *Client) IssueCommitments(request *irma.IssuanceRequest) (*gabi.IssueCommitmentMessage, error) {
586
	proofBuilders, err := client.IssuanceProofBuilders(request)
Sietse Ringers's avatar
Sietse Ringers committed
587
588
589
	if err != nil {
		return nil, err
	}
590
	list := proofBuilders.BuildProofList(request.GetContext(), request.GetNonce(), false)
591
	return &gabi.IssueCommitmentMessage{Proofs: list, Nonce2: client.state.nonce2}, nil
Sietse Ringers's avatar
Sietse Ringers committed
592
593
}

Sietse Ringers's avatar
Sietse Ringers committed
594
595
// ConstructCredentials constructs and saves new credentials
// using the specified issuance signature messages.
596
func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, request *irma.IssuanceRequest) error {
597
	if len(msg) != len(client.state.builders) {
Sietse Ringers's avatar
Sietse Ringers committed
598
599
600
		return errors.New("Received unexpected amount of signatures")
	}

601
602
	// First collect all credentials in a slice, so that if one of them induces an error,
	// we save none of them to fail the session cleanly
603
	gabicreds := []*gabi.Credential{}
Sietse Ringers's avatar
Sietse Ringers committed
604
	for i, sig := range msg {
605
		attrs, err := request.Credentials[i].AttributeList(client.Configuration, getMetadataVersion(request.GetVersion()))
Sietse Ringers's avatar
Sietse Ringers committed
606
607
608
		if err != nil {
			return err
		}
609
		cred, err := client.state.builders[i].ConstructCredential(sig, attrs.Ints)
Sietse Ringers's avatar
Sietse Ringers committed
610
611
612
		if err != nil {
			return err
		}
613
		gabicreds = append(gabicreds, cred)
Sietse Ringers's avatar
Sietse Ringers committed
614
615
	}

616
	for _, gabicred := range gabicreds {
617
		newcred, err := newCredential(gabicred, client.Configuration)
618
619
620
		if err != nil {
			return err
		}
621
		if err = client.addCredential(newcred, true); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
622
623
			return err
		}
Sietse Ringers's avatar
Sietse Ringers committed
624
	}
625

Sietse Ringers's avatar
Sietse Ringers committed
626
	return nil
Sietse Ringers's avatar
Sietse Ringers committed
627
}
628

Sietse Ringers's avatar
Sietse Ringers committed
629
630
// Keyshare server handling

631
// PaillierKey returns a new Paillier key (and generates a new one in a goroutine).
632
633
func (client *Client) paillierKey(wait bool) *paillierPrivateKey {
	cached := client.paillierKeyCache
Sietse Ringers's avatar
Sietse Ringers committed
634
	ch := make(chan bool)
635

636
637
	// Would just write client.paillierKeyCache instead of cached here, but the worker
	// modifies client.paillierKeyCache, and we must be sure that the boolean here and
638
	// the if-clause below match.
639
	go client.paillierKeyWorker(cached == nil && wait, ch)
640
	if cached == nil && wait {
Sietse Ringers's avatar
Sietse Ringers committed
641
		<-ch
642
		// generate yet another one for future calls, but no need to wait now
643
		go client.paillierKeyWorker(false, ch)
Sietse Ringers's avatar
Sietse Ringers committed
644
	}
645
	return client.paillierKeyCache
646
}
Sietse Ringers's avatar
Sietse Ringers committed
647

648
func (client *Client) paillierKeyWorker(wait bool, ch chan bool) {
649
	newkey, _ := paillier.GenerateKey(rand.Reader, 2048)
650
651
	client.paillierKeyCache = (*paillierPrivateKey)(newkey)
	client.storage.StorePaillierKeys(client.paillierKeyCache)
652
653
654
655
656
	if wait {
		ch <- true
	}
}

657
658
func (client *Client) unenrolledSchemeManagers() []irma.SchemeManagerIdentifier {
	list := []irma.SchemeManagerIdentifier{}
659
	for name, manager := range client.Configuration.SchemeManagers {
660
		if _, contains := client.keyshareServers[name]; manager.Distributed() && !contains {
Sietse Ringers's avatar
Sietse Ringers committed
661
			list = append(list, manager.Identifier())
Sietse Ringers's avatar
Sietse Ringers committed
662
663
664
665
		}
	}
	return list
}
Sietse Ringers's avatar
Sietse Ringers committed
666

667
// KeyshareEnroll attempts to enroll at the keyshare server of the specified scheme manager.
668
func (client *Client) KeyshareEnroll(manager irma.SchemeManagerIdentifier, email *string, pin string, lang string) {
Sietse Ringers's avatar
Sietse Ringers committed
669
	go func() {
670
		err := client.keyshareEnrollWorker(manager, email, pin, lang)
Sietse Ringers's avatar
Sietse Ringers committed
671
		if err != nil {
Tomas's avatar
Tomas committed
672
			client.handler.EnrollmentFailure(manager, err)
Sietse Ringers's avatar
Sietse Ringers committed
673
674
		}
	}()
675
676
}

677
func (client *Client) keyshareEnrollWorker(managerID irma.SchemeManagerIdentifier, email *string, pin string, lang string) error {
678
	manager, ok := client.Configuration.SchemeManagers[managerID]
Sietse Ringers's avatar
Sietse Ringers committed
679
680
681
682
683
684
685
686
687
688
	if !ok {
		return errors.New("Unknown scheme manager")
	}
	if len(manager.KeyshareServer) == 0 {
		return errors.New("Scheme manager has no keyshare server")
	}
	if len(pin) < 5 {
		return errors.New("PIN too short, must be at least 5 characters")
	}

689
	transport := irma.NewHTTPTransport(manager.KeyshareServer)
690
	kss, err := newKeyshareServer(managerID, client.paillierKey(true), manager.KeyshareServer)
Sietse Ringers's avatar
Sietse Ringers committed
691
692
693
	if err != nil {
		return err
	}
694
	message := keyshareEnrollment{
695
		Email:     email,
Sietse Ringers's avatar
Sietse Ringers committed
696
		Pin:       kss.HashedPin(pin),
697
		Language:  lang,
Sietse Ringers's avatar
Sietse Ringers committed
698
699
700
		PublicKey: (*paillierPublicKey)(&kss.PrivateKey.PublicKey),
	}

701
702
	qr := &irma.Qr{}
	err = transport.Post("client/register", qr, message)
Sietse Ringers's avatar
Sietse Ringers committed
703
704
705
706
	if err != nil {
		return err
	}

707
708
709
710
711
	// We add the new keyshare server to the client here, without saving it to disk,
	// and start the issuance session for the keyshare server login attribute -
	// keyshare.go needs the relevant keyshare server to be present in the client.
	// If the session succeeds or fails, the keyshare server is stored to disk or
	// removed from the client by the keyshareEnrollmentHandler.
712
	client.keyshareServers[managerID] = kss
713
714
715
716
717
718
719
	client.NewSession(qr, &keyshareEnrollmentHandler{
		client: client,
		pin:    pin,
		kss:    kss,
	})

	return nil
Sietse Ringers's avatar
Sietse Ringers committed
720
721
}

722
// KeyshareRemove unenrolls the keyshare server of the specified scheme manager.
723
func (client *Client) KeyshareRemove(manager irma.SchemeManagerIdentifier) error {
724
	if _, contains := client.keyshareServers[manager]; !contains {
Sietse Ringers's avatar
Sietse Ringers committed
725
726
		return errors.New("Can't uninstall unknown keyshare server")
	}
727
728
	delete(client.keyshareServers, manager)
	return client.storage.StoreKeyshareServers(client.keyshareServers)
Sietse Ringers's avatar
Sietse Ringers committed
729
}
Sietse Ringers's avatar
Sietse Ringers committed
730

Sietse Ringers's avatar
Sietse Ringers committed
731
// KeyshareRemoveAll removes all keyshare server registrations.
732
func (client *Client) KeyshareRemoveAll() error {
733
	client.keyshareServers = map[irma.SchemeManagerIdentifier]*keyshareServer{}
734
735
	client.UnenrolledSchemeManagers = client.unenrolledSchemeManagers()
	return client.storage.StoreKeyshareServers(client.keyshareServers)
736
737
}

Sietse Ringers's avatar
Sietse Ringers committed
738
739
// Add, load and store log entries

740
741
742
func (client *Client) addLogEntry(entry *LogEntry) error {
	client.logs = append(client.logs, entry)
	return client.storage.StoreLogs(client.logs)
Sietse Ringers's avatar
Sietse Ringers committed
743
744
}

Sietse Ringers's avatar
Sietse Ringers committed
745
// Logs returns the log entries of past events.
746
747
func (client *Client) Logs() ([]*LogEntry, error) {
	if client.logs == nil || len(client.logs) == 0 {
Sietse Ringers's avatar
Sietse Ringers committed
748
		var err error
749
		client.logs, err = client.storage.LoadLogs()
Sietse Ringers's avatar
Sietse Ringers committed
750
751
752
753
		if err != nil {
			return nil, err
		}
	}
754
	return client.logs, nil
Sietse Ringers's avatar
Sietse Ringers committed
755
}
756

757
// SetCrashReportingPreference toggles whether or not crash reports should be sent to Sentry.
Sietse Ringers's avatar
Sietse Ringers committed
758
// Has effect only after restarting.
759
760
761
762
func (client *Client) SetCrashReportingPreference(enable bool) {
	client.Preferences.EnableCrashReporting = enable
	_ = client.storage.StorePreferences(client.Preferences)
	client.applyPreferences()
763
764
}

765
766
func (client *Client) applyPreferences() {
	if client.Preferences.EnableCrashReporting {
767
		raven.SetDSN(SentryDSN)
768
769
	} else {
		raven.SetDSN("")
770
771
	}
}