client.go 25.7 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
}

83
84
85
86
87
type ChangepinHandler interface {
	ChangepinFailure(manager irma.SchemeManagerIdentifier, err error)
	ChangepinSuccess(manager irma.SchemeManagerIdentifier)
}

Sietse Ringers's avatar
Sietse Ringers committed
88
89
// ClientHandler informs the user that the configuration or the list of attributes
// that this client uses has been updated.
90
91
type ClientHandler interface {
	KeyshareHandler
92
	ChangepinHandler
93

94
	UpdateConfiguration(new *irma.IrmaIdentifierSet)
95
	UpdateAttributes()
Sietse Ringers's avatar
Sietse Ringers committed
96
97
}

98
99
100
101
type secretKey struct {
	Key *big.Int
}

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

128
	cm := &Client{
129
130
131
		credentials:           make(map[irma.CredentialTypeIdentifier]map[int]*credential),
		keyshareServers:       make(map[irma.SchemeManagerIdentifier]*keyshareServer),
		attributes:            make(map[irma.CredentialTypeIdentifier][]*irma.AttributeList),
132
133
		irmaConfigurationPath: irmaConfigurationPath,
		androidStoragePath:    androidStoragePath,
134
		handler:               handler,
135
136
	}

137
	cm.Configuration, err = irma.NewConfiguration(storagePath+"/irma_configuration", irmaConfigurationPath)
138
139
140
	if err != nil {
		return nil, err
	}
141

142
	schemeMgrErr := cm.Configuration.ParseOrRestoreFolder()
143
144
145
146
147
	// 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
148
	}
149
150

	// Ensure storage path exists, and populate it with necessary files
151
	cm.storage = storage{storagePath: storagePath, Configuration: cm.Configuration}
Sietse Ringers's avatar
Sietse Ringers committed
152
	if err = cm.storage.EnsureStorageExists(); err != nil {
153
154
155
		return nil, err
	}

156
	if cm.Preferences, err = cm.storage.LoadPreferences(); err != nil {
157
158
		return nil, err
	}
159
	cm.applyPreferences()
160

161
	// Perform new update functions from clientUpdates, if any
162
163
164
165
166
	if err = cm.update(); err != nil {
		return nil, err
	}

	// Load our stuff
Sietse Ringers's avatar
Sietse Ringers committed
167
	if cm.secretkey, err = cm.storage.LoadSecretKey(); err != nil {
168
169
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
170
	if cm.attributes, err = cm.storage.LoadAttributes(); err != nil {
171
172
		return nil, err
	}
173
	if cm.keyshareServers, err = cm.storage.LoadKeyshareServers(); err != nil {
174
175
		return nil, err
	}
176
	if cm.paillierKeyCache, err = cm.storage.LoadPaillierKeys(); err != nil {
177
178
		return nil, err
	}
179
180
181
	if cm.paillierKeyCache == nil {
		cm.paillierKey(false)
	}
182

183
184
	cm.UnenrolledSchemeManagers = cm.unenrolledSchemeManagers()
	if len(cm.UnenrolledSchemeManagers) > 1 {
185
186
187
		return nil, errors.New("Too many keyshare servers")
	}

188
	return cm, schemeMgrErr
189
190
}

191
// CredentialInfoList returns a list of information of all contained credentials.
192
193
func (client *Client) CredentialInfoList() irma.CredentialInfoList {
	list := irma.CredentialInfoList([]*irma.CredentialInfo{})
194

195
	for _, attrlistlist := range client.attributes {
196
197
		for index, attrlist := range attrlistlist {
			info := attrlist.Info()
198
199
200
			if info == nil {
				continue
			}
201
			info.Index = index
202
			list = append(list, info)
Sietse Ringers's avatar
Sietse Ringers committed
203
204
		}
	}
205

Sietse Ringers's avatar
Sietse Ringers committed
206
207
208
209
	sort.Sort(list)
	return list
}

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

218
	// Don't add duplicate creds
219
	for _, attrlistlist := range client.attributes {
220
		for _, attrs := range attrlistlist {
221
			if attrs.Hash() == cred.AttributeList().Hash() {
222
223
224
225
226
227
				return nil
			}
		}
	}

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

	// Append the new cred to our attributes and credentials
233
	client.attributes[id] = append(client.attrs(id), cred.AttributeList())
234
235
236
237
238
239
	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
240
241
	}

242
	if err = client.storage.StoreSignature(cred); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
243
244
245
		return
	}
	if storeAttributes {
246
		err = client.storage.StoreAttributes(client.attributes)
Sietse Ringers's avatar
Sietse Ringers committed
247
248
249
250
251
252
253
254
255
256
257
258
259
260
	}
	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

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

	// Remove credential
276
	if creds, exists := client.credentials[id]; exists {
277
		if _, exists := creds[index]; exists {
Sietse Ringers's avatar
Sietse Ringers committed
278
			delete(creds, index)
279
			client.credentials[id] = creds
280
281
282
283
		}
	}

	// Remove signature from storage
284
	if err := client.storage.DeleteSignature(attrs); err != nil {
285
286
287
		return err
	}

288
	removed := map[irma.CredentialTypeIdentifier][]irma.TranslatedString{}
289
290
291
	removed[id] = attrs.Strings()

	if storenow {
292
		return client.addLogEntry(&LogEntry{
293
			Type:    actionRemoval,
294
			Time:    irma.Timestamp(time.Now()),
295
296
297
298
			Removed: removed,
		})
	}
	return nil
299
300
}

Sietse Ringers's avatar
Sietse Ringers committed
301
// RemoveCredential removes the specified credential.
302
func (client *Client) RemoveCredential(id irma.CredentialTypeIdentifier, index int) error {
303
	return client.remove(id, index, true)
304
305
}

Sietse Ringers's avatar
Sietse Ringers committed
306
// RemoveCredentialByHash removes the specified credential.
307
308
func (client *Client) RemoveCredentialByHash(hash string) error {
	cred, index, err := client.credentialByHash(hash)
309
310
311
	if err != nil {
		return err
	}
312
	return client.RemoveCredential(cred.CredentialType().Identifier(), index)
313
314
}

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

	logentry := &LogEntry{
		Type:    actionRemoval,
333
		Time:    irma.Timestamp(time.Now()),
334
335
		Removed: removed,
	}
336
	if err := client.addLogEntry(logentry); err != nil {
337
338
		return err
	}
339
	return client.storage.StoreLogs(client.logs)
340
341
}

342
// Attribute and credential getter methods
Sietse Ringers's avatar
Sietse Ringers committed
343

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

// creds returns cm.credentials[id], initializing it to an empty map if neccesary
355
func (client *Client) creds(id irma.CredentialTypeIdentifier) map[int]*credential {
356
	list, exists := client.credentials[id]
Sietse Ringers's avatar
Sietse Ringers committed
357
	if !exists {
Sietse Ringers's avatar
Sietse Ringers committed
358
		list = make(map[int]*credential)
359
		client.credentials[id] = list
Sietse Ringers's avatar
Sietse Ringers committed
360
361
362
363
	}
	return list
}

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

373
374
func (client *Client) credentialByHash(hash string) (*credential, int, error) {
	for _, attrlistlist := range client.attributes {
375
		for index, attrs := range attrlistlist {
376
			if attrs.Hash() == hash {
377
				cred, err := client.credential(attrs.CredentialType().Identifier(), index)
378
379
380
381
382
383
384
				return cred, index, err
			}
		}
	}
	return nil, 0, nil
}

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

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

433
	return client.credentials[id][counter], nil
434
435
}

Sietse Ringers's avatar
Sietse Ringers committed
436
// Methods used in the IRMA protocol
437

438
// Candidates returns a list of attributes present in this client
Sietse Ringers's avatar
Sietse Ringers committed
439
// that satisfy the specified attribute disjunction.
440
441
func (client *Client) Candidates(disjunction *irma.AttributeDisjunction) []*irma.AttributeIdentifier {
	candidates := make([]*irma.AttributeIdentifier, 0, 10)
442
443

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

	return candidates
}

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

498
499
func (client *Client) groupCredentials(choice *irma.DisclosureChoice) (map[irma.CredentialIdentifier][]int, error) {
	grouped := make(map[irma.CredentialIdentifier][]int)
500
501
502
	if choice == nil || choice.Attributes == nil {
		return grouped, nil
	}
503
504
505
506
507
508
509
510

	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 {
511
512
			indices := make([]int, 1, 1)
			indices[0] = 1 // Always include metadata
513
514
515
516
517
518
			grouped[ici] = indices
		}

		if identifier.IsCredential() {
			continue // In this case we only disclose the metadata attribute, which is already handled
		}
519
		index, err := client.Configuration.CredentialTypes[identifier.CredentialTypeIdentifier()].IndexOf(identifier)
520
521
522
523
		if err != nil {
			return nil, err
		}

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

	return grouped, nil
}

532
// ProofBuilders constructs a list of proof builders for the specified attribute choice.
533
func (client *Client) ProofBuilders(choice *irma.DisclosureChoice, request irma.IrmaSession, issig bool) (gabi.ProofBuilderList, error) {
534
	todisclose, err := client.groupCredentials(choice)
535
536
537
538
	if err != nil {
		return nil, err
	}

539
	builders := gabi.ProofBuilderList([]gabi.ProofBuilder{})
540
	for id, list := range todisclose {
541
		cred, err := client.credentialByID(id)
542
543
544
545
546
		if err != nil {
			return nil, err
		}
		builders = append(builders, cred.Credential.CreateDisclosureProofBuilder(list))
	}
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564

	if issig {
		var sigs []*big.Int
		var disclosed [][]*big.Int
		var s *big.Int
		var d []*big.Int
		for _, builder := range builders {
			s, d = builder.(*gabi.DisclosureProofBuilder).TimestampRequestContributions()
			sigs = append(sigs, s)
			disclosed = append(disclosed, d)
		}
		r := request.(*irma.SignatureRequest)
		r.Timestamp, err = irma.GetTimestamp(r.Message, sigs, disclosed)
		if err != nil {
			return nil, err
		}
	}

565
566
567
568
569
570
571
572
573
574
	return builders, nil
}

// Proofs computes disclosure proofs containing the attributes specified by choice.
func (client *Client) Proofs(choice *irma.DisclosureChoice, request irma.IrmaSession, issig bool) (gabi.ProofList, error) {
	builders, err := client.ProofBuilders(choice, request, issig)
	if err != nil {
		return nil, err
	}

575
	return builders.BuildProofList(request.GetContext(), request.GetNonce(), issig), nil
Sietse Ringers's avatar
Sietse Ringers committed
576
577
}

578
579
// IssuanceProofBuilders constructs a list of proof builders in the issuance protocol
// for the future credentials as well as possibly any disclosed attributes.
580
func (client *Client) IssuanceProofBuilders(request *irma.IssuanceRequest) (gabi.ProofBuilderList, error) {
Sietse Ringers's avatar
Cleanup    
Sietse Ringers committed
581
	state, err := newIssuanceState()
Sietse Ringers's avatar
Sietse Ringers committed
582
583
584
	if err != nil {
		return nil, err
	}
585
	client.state = state
Sietse Ringers's avatar
Sietse Ringers committed
586

587
	proofBuilders := gabi.ProofBuilderList([]gabi.ProofBuilder{})
Sietse Ringers's avatar
Sietse Ringers committed
588
	for _, futurecred := range request.Credentials {
Sietse Ringers's avatar
Sietse Ringers committed
589
		var pk *gabi.PublicKey
590
		pk, err = client.Configuration.PublicKey(futurecred.CredentialTypeID.IssuerIdentifier(), futurecred.KeyCounter)
591
592
593
		if err != nil {
			return nil, err
		}
594
		credBuilder := gabi.NewCredentialBuilder(
595
			pk, request.GetContext(), client.secretkey.Key, state.nonce2)
596
		state.builders = append(state.builders, credBuilder)
Sietse Ringers's avatar
Sietse Ringers committed
597
598
		proofBuilders = append(proofBuilders, credBuilder)
	}
Sietse Ringers's avatar
Sietse Ringers committed
599

600
	disclosures, err := client.ProofBuilders(request.Choice, request, false)
Sietse Ringers's avatar
Sietse Ringers committed
601
602
603
	if err != nil {
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
604
	proofBuilders = append(disclosures, proofBuilders...)
Sietse Ringers's avatar
Sietse Ringers committed
605
606
	return proofBuilders, nil
}
Sietse Ringers's avatar
Sietse Ringers committed
607

Sietse Ringers's avatar
Sietse Ringers committed
608
609
// IssueCommitments computes issuance commitments, along with disclosure proofs
// specified by choice.
610
func (client *Client) IssueCommitments(request *irma.IssuanceRequest) (*gabi.IssueCommitmentMessage, error) {
611
	proofBuilders, err := client.IssuanceProofBuilders(request)
Sietse Ringers's avatar
Sietse Ringers committed
612
613
614
	if err != nil {
		return nil, err
	}
615
	list := proofBuilders.BuildProofList(request.GetContext(), request.GetNonce(), false)
616
	return &gabi.IssueCommitmentMessage{Proofs: list, Nonce2: client.state.nonce2}, nil
Sietse Ringers's avatar
Sietse Ringers committed
617
618
}

Sietse Ringers's avatar
Sietse Ringers committed
619
620
// ConstructCredentials constructs and saves new credentials
// using the specified issuance signature messages.
621
func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, request *irma.IssuanceRequest) error {
622
	if len(msg) != len(client.state.builders) {
Sietse Ringers's avatar
Sietse Ringers committed
623
624
625
		return errors.New("Received unexpected amount of signatures")
	}

626
627
	// 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
628
	gabicreds := []*gabi.Credential{}
Sietse Ringers's avatar
Sietse Ringers committed
629
	for i, sig := range msg {
630
		attrs, err := request.Credentials[i].AttributeList(client.Configuration, getMetadataVersion(request.GetVersion()))
Sietse Ringers's avatar
Sietse Ringers committed
631
632
633
		if err != nil {
			return err
		}
634
		cred, err := client.state.builders[i].ConstructCredential(sig, attrs.Ints)
Sietse Ringers's avatar
Sietse Ringers committed
635
636
637
		if err != nil {
			return err
		}
638
		gabicreds = append(gabicreds, cred)
Sietse Ringers's avatar
Sietse Ringers committed
639
640
	}

641
	for _, gabicred := range gabicreds {
642
		newcred, err := newCredential(gabicred, client.Configuration)
643
644
645
		if err != nil {
			return err
		}
646
		if err = client.addCredential(newcred, true); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
647
648
			return err
		}
Sietse Ringers's avatar
Sietse Ringers committed
649
	}
650

Sietse Ringers's avatar
Sietse Ringers committed
651
	return nil
Sietse Ringers's avatar
Sietse Ringers committed
652
}
653

Sietse Ringers's avatar
Sietse Ringers committed
654
655
// Keyshare server handling

656
// PaillierKey returns a new Paillier key (and generates a new one in a goroutine).
657
658
func (client *Client) paillierKey(wait bool) *paillierPrivateKey {
	cached := client.paillierKeyCache
Sietse Ringers's avatar
Sietse Ringers committed
659
	ch := make(chan bool)
660

661
662
	// 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
663
	// the if-clause below match.
664
	go client.paillierKeyWorker(cached == nil && wait, ch)
665
	if cached == nil && wait {
Sietse Ringers's avatar
Sietse Ringers committed
666
		<-ch
667
		// generate yet another one for future calls, but no need to wait now
668
		go client.paillierKeyWorker(false, ch)
Sietse Ringers's avatar
Sietse Ringers committed
669
	}
670
	return client.paillierKeyCache
671
}
Sietse Ringers's avatar
Sietse Ringers committed
672

673
func (client *Client) paillierKeyWorker(wait bool, ch chan bool) {
674
	newkey, _ := paillier.GenerateKey(rand.Reader, 2048)
675
676
	client.paillierKeyCache = (*paillierPrivateKey)(newkey)
	client.storage.StorePaillierKeys(client.paillierKeyCache)
677
678
679
680
681
	if wait {
		ch <- true
	}
}

682
683
func (client *Client) unenrolledSchemeManagers() []irma.SchemeManagerIdentifier {
	list := []irma.SchemeManagerIdentifier{}
684
	for name, manager := range client.Configuration.SchemeManagers {
685
		if _, contains := client.keyshareServers[name]; manager.Distributed() && !contains {
Sietse Ringers's avatar
Sietse Ringers committed
686
			list = append(list, manager.Identifier())
Sietse Ringers's avatar
Sietse Ringers committed
687
688
689
690
		}
	}
	return list
}
Sietse Ringers's avatar
Sietse Ringers committed
691

692
// KeyshareEnroll attempts to enroll at the keyshare server of the specified scheme manager.
693
func (client *Client) KeyshareEnroll(manager irma.SchemeManagerIdentifier, email *string, pin string, lang string) {
Sietse Ringers's avatar
Sietse Ringers committed
694
	go func() {
695
		err := client.keyshareEnrollWorker(manager, email, pin, lang)
Sietse Ringers's avatar
Sietse Ringers committed
696
		if err != nil {
Tomas's avatar
Tomas committed
697
			client.handler.EnrollmentFailure(manager, err)
Sietse Ringers's avatar
Sietse Ringers committed
698
699
		}
	}()
700
701
}

702
func (client *Client) keyshareEnrollWorker(managerID irma.SchemeManagerIdentifier, email *string, pin string, lang string) error {
703
	manager, ok := client.Configuration.SchemeManagers[managerID]
Sietse Ringers's avatar
Sietse Ringers committed
704
705
706
707
708
709
710
711
712
713
	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")
	}

714
	transport := irma.NewHTTPTransport(manager.KeyshareServer)
715
	kss, err := newKeyshareServer(managerID, client.paillierKey(true), manager.KeyshareServer)
Sietse Ringers's avatar
Sietse Ringers committed
716
717
718
	if err != nil {
		return err
	}
719
	message := keyshareEnrollment{
720
		Email:     email,
Sietse Ringers's avatar
Sietse Ringers committed
721
		Pin:       kss.HashedPin(pin),
722
		Language:  lang,
Sietse Ringers's avatar
Sietse Ringers committed
723
724
725
		PublicKey: (*paillierPublicKey)(&kss.PrivateKey.PublicKey),
	}

726
727
	qr := &irma.Qr{}
	err = transport.Post("client/register", qr, message)
Sietse Ringers's avatar
Sietse Ringers committed
728
729
730
731
	if err != nil {
		return err
	}

732
733
734
735
736
	// 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.
737
	client.keyshareServers[managerID] = kss
738
739
740
741
742
743
744
	client.NewSession(qr, &keyshareEnrollmentHandler{
		client: client,
		pin:    pin,
		kss:    kss,
	})

	return nil
Sietse Ringers's avatar
Sietse Ringers committed
745
746
}

747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
func (client *Client) KeyshareChangepin(manager irma.SchemeManagerIdentifier, oldpin string, newpin string) {
	go func() {
		err := client.keyshareChangepinWorker(manager, oldpin, newpin)
		if err != nil {
			client.handler.ChangepinFailure(manager, err)
		} else {
			client.handler.ChangepinSuccess(manager)
		}
	}()
}

func (client *Client) keyshareChangepinWorker(managerID irma.SchemeManagerIdentifier, oldpin string, newpin string) error {
	kss, ok := client.keyshareServers[managerID]
	if !ok {
		return errors.New("Unknown keyshare server")
	}

	transport := irma.NewHTTPTransport(kss.URL)
	message := keysharePinchange{
		Username: kss.Username,
		Oldpin: kss.HashedPin(oldpin),
		Newpin: kss.HashedPin(newpin),
	}

	res := &keysharePinStatus{}
	err := transport.Post("users/change/pin", res, message)
	if err != nil {
		return err
	}
	if res.Status != kssPinSuccess {
		return errors.New("Pin change rejected")
	}
	return nil
}

782
// KeyshareRemove unenrolls the keyshare server of the specified scheme manager.
783
func (client *Client) KeyshareRemove(manager irma.SchemeManagerIdentifier) error {
784
	if _, contains := client.keyshareServers[manager]; !contains {
Sietse Ringers's avatar
Sietse Ringers committed
785
786
		return errors.New("Can't uninstall unknown keyshare server")
	}
787
788
	delete(client.keyshareServers, manager)
	return client.storage.StoreKeyshareServers(client.keyshareServers)
Sietse Ringers's avatar
Sietse Ringers committed
789
}
Sietse Ringers's avatar
Sietse Ringers committed
790

Sietse Ringers's avatar
Sietse Ringers committed
791
// KeyshareRemoveAll removes all keyshare server registrations.
792
func (client *Client) KeyshareRemoveAll() error {
793
	client.keyshareServers = map[irma.SchemeManagerIdentifier]*keyshareServer{}
794
795
	client.UnenrolledSchemeManagers = client.unenrolledSchemeManagers()
	return client.storage.StoreKeyshareServers(client.keyshareServers)
796
797
}

Sietse Ringers's avatar
Sietse Ringers committed
798
799
// Add, load and store log entries

800
801
802
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
803
804
}

Sietse Ringers's avatar
Sietse Ringers committed
805
// Logs returns the log entries of past events.
806
807
func (client *Client) Logs() ([]*LogEntry, error) {
	if client.logs == nil || len(client.logs) == 0 {
Sietse Ringers's avatar
Sietse Ringers committed
808
		var err error
809
		client.logs, err = client.storage.LoadLogs()
Sietse Ringers's avatar
Sietse Ringers committed
810
811
812
813
		if err != nil {
			return nil, err
		}
	}
814
	return client.logs, nil
Sietse Ringers's avatar
Sietse Ringers committed
815
}
816

817
// SetCrashReportingPreference toggles whether or not crash reports should be sent to Sentry.
Sietse Ringers's avatar
Sietse Ringers committed
818
// Has effect only after restarting.
819
820
821
822
func (client *Client) SetCrashReportingPreference(enable bool) {
	client.Preferences.EnableCrashReporting = enable
	_ = client.storage.StorePreferences(client.Preferences)
	client.applyPreferences()
823
824
}

825
826
func (client *Client) applyPreferences() {
	if client.Preferences.EnableCrashReporting {
827
		raven.SetDSN(SentryDSN)
828
829
	} else {
		raven.SetDSN("")
830
831
	}
}