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

import (
Sietse Ringers's avatar
Sietse Ringers committed
4
	"math/big"
5
	"strconv"
6
	"time"
Sietse Ringers's avatar
Sietse Ringers committed
7

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

15
// This file contains most methods of the Client (c.f. session.go
Sietse Ringers's avatar
Sietse Ringers committed
16
17
// and updates.go).
//
Sietse Ringers's avatar
Sietse Ringers committed
18
19
20
21
22
23
24
25
// 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
26
27
28
29
30
31
32
// 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
33
// immediately need to be available anyway.
Sietse Ringers's avatar
Sietse Ringers committed
34
35
36
37
38
//
// - 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).

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

Sietse Ringers's avatar
Sietse Ringers committed
48
49
50
51
	// Where we store/load it to/from
	storage storage

	// Other state
52
53
54
55
56
57
	Preferences           Preferences
	Configuration         *irma.Configuration
	irmaConfigurationPath string
	androidStoragePath    string
	handler               ClientHandler
	state                 *issuanceState
58
59
}

60
61
62
63
// SentryDSN should be set in the init() function
// Setting it to an empty string means no crash reports
var SentryDSN = ""

64
65
type Preferences struct {
	EnableCrashReporting bool
66
67
}

68
var defaultPreferences = Preferences{
69
	EnableCrashReporting: true,
70
71
}

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

79
80
81
type ChangePinHandler interface {
	ChangePinFailure(manager irma.SchemeManagerIdentifier, err error)
	ChangePinSuccess(manager irma.SchemeManagerIdentifier)
82
83
	ChangePinIncorrect(manager irma.SchemeManagerIdentifier, attempts int)
	ChangePinBlocked(manager irma.SchemeManagerIdentifier, timeout int)
84
85
}

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

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

96
97
98
99
type secretKey struct {
	Key *big.Int
}

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

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

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

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

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

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

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

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

175
	if len(cm.UnenrolledSchemeManagers()) > 1 {
176
177
178
		return nil, errors.New("Too many keyshare servers")
	}

179
	return cm, schemeMgrErr
180
181
}

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

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

Sietse Ringers's avatar
Sietse Ringers committed
197
198
199
	return list
}

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

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

	// If this is a singleton credential type, ensure we have at most one by removing any previous instance
218
219
220
221
	if !id.Empty() && cred.CredentialType().IsSingleton {
		for len(client.attrs(id)) != 0 {
			client.remove(id, 0, false)
		}
222
223
224
	}

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

234
	if err = client.storage.StoreSignature(cred); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
235
236
237
		return
	}
	if storeAttributes {
238
		err = client.storage.StoreAttributes(client.attributes)
Sietse Ringers's avatar
Sietse Ringers committed
239
240
241
242
243
244
245
246
247
248
249
250
251
252
	}
	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

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

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

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

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

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

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

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

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

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

334
// Attribute and credential getter methods
Sietse Ringers's avatar
Sietse Ringers committed
335

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

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

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

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

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

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

425
	return client.credentialsCache[id][counter], nil
426
427
}

Sietse Ringers's avatar
Sietse Ringers committed
428
// Methods used in the IRMA protocol
429

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

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

	return candidates
}

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

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

	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 {
503
504
			indices := make([]int, 1, 1)
			indices[0] = 1 // Always include metadata
505
506
507
508
509
510
			grouped[ici] = indices
		}

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

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

	return grouped, nil
}

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

531
	builders := gabi.ProofBuilderList([]gabi.ProofBuilder{})
532
	for id, list := range todisclose {
533
		cred, err := client.credentialByID(id)
534
535
536
537
538
		if err != nil {
			return nil, err
		}
		builders = append(builders, cred.Credential.CreateDisclosureProofBuilder(list))
	}
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556

	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
		}
	}

557
558
559
560
561
562
563
564
565
566
	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
	}

567
	return builders.BuildProofList(request.GetContext(), request.GetNonce(), issig), nil
Sietse Ringers's avatar
Sietse Ringers committed
568
569
}

570
571
// IssuanceProofBuilders constructs a list of proof builders in the issuance protocol
// for the future credentials as well as possibly any disclosed attributes.
572
func (client *Client) IssuanceProofBuilders(request *irma.IssuanceRequest) (gabi.ProofBuilderList, error) {
Sietse Ringers's avatar
Cleanup    
Sietse Ringers committed
573
	state, err := newIssuanceState()
Sietse Ringers's avatar
Sietse Ringers committed
574
575
576
	if err != nil {
		return nil, err
	}
577
	client.state = state
Sietse Ringers's avatar
Sietse Ringers committed
578

579
	proofBuilders := gabi.ProofBuilderList([]gabi.ProofBuilder{})
Sietse Ringers's avatar
Sietse Ringers committed
580
	for _, futurecred := range request.Credentials {
Sietse Ringers's avatar
Sietse Ringers committed
581
		var pk *gabi.PublicKey
582
		pk, err = client.Configuration.PublicKey(futurecred.CredentialTypeID.IssuerIdentifier(), futurecred.KeyCounter)
583
584
585
		if err != nil {
			return nil, err
		}
586
		credBuilder := gabi.NewCredentialBuilder(
587
			pk, request.GetContext(), client.secretkey.Key, state.nonce2)
588
		state.builders = append(state.builders, credBuilder)
Sietse Ringers's avatar
Sietse Ringers committed
589
590
		proofBuilders = append(proofBuilders, credBuilder)
	}
Sietse Ringers's avatar
Sietse Ringers committed
591

592
	disclosures, err := client.ProofBuilders(request.Choice, request, false)
Sietse Ringers's avatar
Sietse Ringers committed
593
594
595
	if err != nil {
		return nil, err
	}
Sietse Ringers's avatar
Sietse Ringers committed
596
	proofBuilders = append(disclosures, proofBuilders...)
Sietse Ringers's avatar
Sietse Ringers committed
597
598
	return proofBuilders, nil
}
Sietse Ringers's avatar
Sietse Ringers committed
599

Sietse Ringers's avatar
Sietse Ringers committed
600
601
// IssueCommitments computes issuance commitments, along with disclosure proofs
// specified by choice.
602
func (client *Client) IssueCommitments(request *irma.IssuanceRequest) (*gabi.IssueCommitmentMessage, error) {
603
	proofBuilders, err := client.IssuanceProofBuilders(request)
Sietse Ringers's avatar
Sietse Ringers committed
604
605
606
	if err != nil {
		return nil, err
	}
607
	list := proofBuilders.BuildProofList(request.GetContext(), request.GetNonce(), false)
608
	return &gabi.IssueCommitmentMessage{Proofs: list, Nonce2: client.state.nonce2}, nil
Sietse Ringers's avatar
Sietse Ringers committed
609
610
}

Sietse Ringers's avatar
Sietse Ringers committed
611
612
// ConstructCredentials constructs and saves new credentials
// using the specified issuance signature messages.
613
func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, request *irma.IssuanceRequest) error {
614
	if len(msg) != len(client.state.builders) {
Sietse Ringers's avatar
Sietse Ringers committed
615
616
617
		return errors.New("Received unexpected amount of signatures")
	}

618
619
	// 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
620
	gabicreds := []*gabi.Credential{}
Sietse Ringers's avatar
Sietse Ringers committed
621
	for i, sig := range msg {
622
		attrs, err := request.Credentials[i].AttributeList(client.Configuration, getMetadataVersion(request.GetVersion()))
Sietse Ringers's avatar
Sietse Ringers committed
623
624
625
		if err != nil {
			return err
		}
626
		cred, err := client.state.builders[i].ConstructCredential(sig, attrs.Ints)
Sietse Ringers's avatar
Sietse Ringers committed
627
628
629
		if err != nil {
			return err
		}
630
		gabicreds = append(gabicreds, cred)
Sietse Ringers's avatar
Sietse Ringers committed
631
632
	}

633
	for _, gabicred := range gabicreds {
634
		newcred, err := newCredential(gabicred, client.Configuration)
635
636
637
		if err != nil {
			return err
		}
638
		if err = client.addCredential(newcred, true); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
639
640
			return err
		}
Sietse Ringers's avatar
Sietse Ringers committed
641
	}
642

Sietse Ringers's avatar
Sietse Ringers committed
643
	return nil
Sietse Ringers's avatar
Sietse Ringers committed
644
}
645

Sietse Ringers's avatar
Sietse Ringers committed
646
647
// Keyshare server handling

648
func (client *Client) genSchemeManagersList(enrolled bool) []irma.SchemeManagerIdentifier {
649
	list := []irma.SchemeManagerIdentifier{}
650
	for name, manager := range client.Configuration.SchemeManagers {
651
		if _, contains := client.keyshareServers[name]; manager.Distributed() && contains == enrolled {
Sietse Ringers's avatar
Sietse Ringers committed
652
			list = append(list, manager.Identifier())
Sietse Ringers's avatar
Sietse Ringers committed
653
654
655
656
		}
	}
	return list
}
Sietse Ringers's avatar
Sietse Ringers committed
657

658
659
660
661
func (client *Client) UnenrolledSchemeManagers() []irma.SchemeManagerIdentifier {
	return client.genSchemeManagersList(false)
}

662
func (client *Client) EnrolledSchemeManagers() []irma.SchemeManagerIdentifier {
663
	return client.genSchemeManagersList(true)
664
665
}

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

676
func (client *Client) keyshareEnrollWorker(managerID irma.SchemeManagerIdentifier, email *string, pin string, lang string) error {
677
	manager, ok := client.Configuration.SchemeManagers[managerID]
Sietse Ringers's avatar
Sietse Ringers committed
678
679
680
681
682
683
684
685
686
687
	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")
	}

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

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

705
706
707
708
709
	// 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.
710
	client.keyshareServers[managerID] = kss
711
712
713
714
715
716
717
	client.NewSession(qr, &keyshareEnrollmentHandler{
		client: client,
		pin:    pin,
		kss:    kss,
	})

	return nil
Sietse Ringers's avatar
Sietse Ringers committed
718
719
}

720
721
722
723
724
725
726
727
728
func (client *Client) KeyshareVerifyPin(pin string, schemeid irma.SchemeManagerIdentifier) (bool, int, int, error) {
	scheme := client.Configuration.SchemeManagers[schemeid]
	if scheme == nil || !scheme.Distributed() {
		return false, 0, 0, errors.Errorf("Can't verify pin of scheme %s", schemeid.String())
	}
	kss := client.keyshareServers[schemeid]
	return verifyPinWorker(pin, kss, irma.NewHTTPTransport(kss.URL))
}

729
func (client *Client) KeyshareChangePin(manager irma.SchemeManagerIdentifier, oldPin string, newPin string) {
730
	go func() {
731
		err := client.keyshareChangePinWorker(manager, oldPin, newPin)
732
		if err != nil {
733
			client.handler.ChangePinFailure(manager, err)
734
735
736
737
		}
	}()
}

738
func (client *Client) keyshareChangePinWorker(managerID irma.SchemeManagerIdentifier, oldPin string, newPin string) error {
739
740
741
742
743
744
	kss, ok := client.keyshareServers[managerID]
	if !ok {
		return errors.New("Unknown keyshare server")
	}

	transport := irma.NewHTTPTransport(kss.URL)
745
	message := keyshareChangepin{
746
		Username: kss.Username,
747
748
		OldPin:   kss.HashedPin(oldPin),
		NewPin:   kss.HashedPin(newPin),
749
750
751
752
753
754
755
	}

	res := &keysharePinStatus{}
	err := transport.Post("users/change/pin", res, message)
	if err != nil {
		return err
	}
756

757
758
	switch res.Status {
	case kssPinSuccess:
759
		client.handler.ChangePinSuccess(managerID)
760
	case kssPinFailure:
761
762
763
764
765
		attempts, err := strconv.Atoi(res.Message)
		if err != nil {
			return err
		}
		client.handler.ChangePinIncorrect(managerID, attempts)
766
	case kssPinError:
767
768
769
770
771
		timeout, err := strconv.Atoi(res.Message)
		if err != nil {
			return err
		}
		client.handler.ChangePinBlocked(managerID, timeout)
772
773
	default:
		return errors.New("Unknown keyshare response")
774
	}
775

776
777
778
	return nil
}

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

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

Sietse Ringers's avatar
Sietse Ringers committed
794
795
// Add, load and store log entries

796
797
798
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
799
800
}

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

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

821
822
func (client *Client) applyPreferences() {
	if client.Preferences.EnableCrashReporting {
823
		raven.SetDSN(SentryDSN)
824
825
	} else {
		raven.SetDSN("")
826
827
	}
}