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

import (
4
	"strconv"
5
	"time"
Sietse Ringers's avatar
Sietse Ringers committed
6

7
	"github.com/getsentry/raven-go"
Sietse Ringers's avatar
Sietse Ringers committed
8
	"github.com/go-errors/errors"
9
10
	"github.com/privacybydesign/gabi"
	"github.com/privacybydesign/gabi/big"
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
	Preferences           Preferences
	Configuration         *irma.Configuration
	irmaConfigurationPath string
	androidStoragePath    string
	handler               ClientHandler
57
58
}

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

63
64
type Preferences struct {
	EnableCrashReporting bool
65
66
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

178
	return cm, schemeMgrErr
179
180
}

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

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

Sietse Ringers's avatar
Sietse Ringers committed
195
196
197
	return list
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

332
// Attribute and credential getter methods
Sietse Ringers's avatar
Sietse Ringers committed
333

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

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

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

363
func (client *Client) attributesByHash(hash string) (*irma.AttributeList, int) {
364
	for _, attrlistlist := range client.attributes {
365
		for index, attrs := range attrlistlist {
366
			if attrs.Hash() == hash {
367
				return attrs, index
368
369
370
			}
		}
	}
371
372
373
374
375
376
377
378
379
	return nil, 0
}

func (client *Client) credentialByHash(hash string) (*credential, int, error) {
	attrs, index := client.attributesByHash(hash)
	if attrs != nil {
		cred, err := client.credential(attrs.CredentialType().Identifier(), index)
		return cred, index, err
	}
380
381
382
	return nil, 0, nil
}

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

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

431
	return client.credentialsCache[id][counter], nil
432
433
}

Sietse Ringers's avatar
Sietse Ringers committed
434
// Methods used in the IRMA protocol
435

436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
// credCandidates returns a list containing a list of candidate credential instances for each item
// in the conjunction. (A credential instance from the client is a candidate it it contains
// attributes required in this conjunction). If one credential type occurs multiple times in the
// conjunction it is not added twice.
func (client *Client) credCandidates(con irma.AttributeCon) credCandidateSet {
	var candidates [][]*irma.CredentialIdentifier
	for _, credtype := range con.CredentialTypes() {
		creds := client.attributes[credtype]
		if len(creds) == 0 {
			return nil // we'll need at least one instance of each credtype in this conjunction
		}
		var c []*irma.CredentialIdentifier
		for _, cred := range creds {
			if !cred.IsValid() {
				continue
			}
			c = append(c, &irma.CredentialIdentifier{Type: credtype, Hash: cred.Hash()})
		}
		candidates = append(candidates, c)
	}
	return candidates
}

type credCandidateSet [][]*irma.CredentialIdentifier
460

461
462
463
464
465
func (set credCandidateSet) multiply(candidates []*irma.CredentialIdentifier) credCandidateSet {
	result := make(credCandidateSet, 0, len(set)*len(candidates))
	for _, cred := range candidates {
		for _, toDisclose := range set {
			result = append(result, append(toDisclose, cred))
466
		}
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
	}
	return result
}

func (set credCandidateSet) expand(client *Client, con irma.AttributeCon) [][]*irma.AttributeIdentifier {
	var result [][]*irma.AttributeIdentifier

outer:
	for _, s := range set {
		var candidateSet []*irma.AttributeIdentifier
		for _, cred := range s {
			for _, attr := range con {
				if attr.Type.CredentialTypeIdentifier() == cred.Type {
					id := &irma.AttributeIdentifier{Type: attr.Type, CredentialHash: cred.Hash}
					if attr.Value != nil {
						attrs, _ := client.attributesByHash(cred.Hash)
						val := attrs.UntranslatedAttribute(id.Type)
						if val == nil || *attr.Value != *val {
							continue outer
						}
					}
					candidateSet = append(candidateSet, id)
				}
			}
		}
		result = append(result, candidateSet)
	}

	return result
}

func cartesianProduct(candidates [][]*irma.CredentialIdentifier) credCandidateSet {
	set := credCandidateSet{[]*irma.CredentialIdentifier{}} // Unit element for this multiplication
	for _, c := range candidates {
		set = set.multiply(c)
	}
	return set
}

// Candidates returns attributes present in this client that satisfy the specified attribute
// disjunction. It returns a list of candidate attribute sets, each of which would satisfy the
// specified disjunction. If the disjunction cannot be satisfied by the attributes that the client
// currently posesses (ie. len(candidates) == 0), then the second return parameter lists the missing
// attributes that would be necessary to satisfy the disjunction.
func (client *Client) Candidates(discon irma.AttributeDisCon) (
	candidates [][]*irma.AttributeIdentifier, missing map[int]irma.AttributeCon,
) {
	candidates = [][]*irma.AttributeIdentifier{}

	for _, con := range discon {
		// Build a list containing, for each attribute in this conjunction, a list of credential
		// instances containing the attribute. Writing schematically a sample conjunction of three
		// attribute types as [ a.a.a.a, a.a.a.b, a.a.b.x ], we map this to:
		// [ [ a.a.a #1, a.a.a #2] , [ a.a.b #1 ] ]
		// assuming the client has 2 instances of a.a.a and 1 instance of a.a.b.
		c := client.credCandidates(con)
		if len(c) == 0 {
524
525
			continue
		}
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557

		// The cartesian product of the list of lists constructed above results in a list of which
		// each item is a list of credentials containing attributes that together will satisfy the
		// current conjunction
		// [ [ a.a.a #1, a.a.b #1 ], [ a.a.a #2, a.a.b #1 ] ]
		c = cartesianProduct(c)

		// Expand each credential instance to those attribute instances within it that the con
		// is asking for, resulting in attribute sets each of which would satisfy the conjunction,
		// and therefore the containing disjunction
		// [ [ a.a.a.a #1, a.a.a.b #1, a.a.b.x #1 ], [ a.a.a.a #2, a.a.a.b #2, a.a.b.x #1 ] ]
		candidates = append(candidates, c.expand(client, con)...)
	}

	if len(candidates) == 0 {
		missing = client.missingAttributes(discon)
	}

	return
}

// missingAttributes returns for each of the conjunctions in the specified disjunction
// a list of attributes that the client does not posess but which would be required to
// satisfy the conjunction.
func (client *Client) missingAttributes(discon irma.AttributeDisCon) map[int]irma.AttributeCon {
	missing := map[int]irma.AttributeCon{}

	for i, con := range discon {
		for _, attr := range con {
			creds := client.attributes[attr.Type.CredentialTypeIdentifier()]
			if len(creds) == 0 {
				missing[i] = append(missing[i], attr)
558
559
				continue
			}
560
561
			for _, cred := range creds {
				if attr.Satisfy(attr.Type, cred.UntranslatedAttribute(attr.Type)) {
562
563
564
					continue
				}
			}
565
			missing[i] = append(missing[i], attr)
566
567
568
		}
	}

569
	return missing
570
571
}

572
// CheckSatisfiability checks if this client has the required attributes
Sietse Ringers's avatar
Sietse Ringers committed
573
574
// to satisfy the specifed disjunction list. If not, the unsatisfiable disjunctions
// are returned.
575
576
577
578
579
580
581
582
583
func (client *Client) CheckSatisfiability(condiscon irma.AttributeConDisCon) (
	candidates [][][]*irma.AttributeIdentifier, missing map[int]map[int]irma.AttributeCon,
) {
	candidates = make([][][]*irma.AttributeIdentifier, len(condiscon))
	missing = map[int]map[int]irma.AttributeCon{}

	for i, discon := range condiscon {
		var m map[int]irma.AttributeCon
		candidates[i], m = client.Candidates(discon)
584
		if len(candidates[i]) == 0 {
585
			missing[i] = m
586
587
		}
	}
588
589

	return
590
}
591

592
593
594
595
596
597
598
599
600
601
// attributeGroup points to a credential and some of its attributes which are to be disclosed
type attributeGroup struct {
	cred  irma.CredentialIdentifier
	attrs []int
}

// Given the user's choice of attributes to be disclosed, group them per credential out of which they
// are to be disclosed
func (client *Client) groupCredentials(choice *irma.DisclosureChoice) (
	[]attributeGroup, irma.DisclosedAttributeIndices, error,
602
) {
603
	todisclose := make([]attributeGroup, 0, 10)
604
	if choice == nil || choice.Attributes == nil {
605
		return todisclose, irma.DisclosedAttributeIndices{}, nil
606
	}
607

608
609
	// maps an irma.CredentialIdentifier to its index in the final ProofList
	credIndices := make(map[irma.CredentialIdentifier]int)
610
	attributeIndices := make(irma.DisclosedAttributeIndices, len(choice.Attributes))
611
612
613
614
615
616
617
618
619
620
621
622
623
624
	for i, attributeset := range choice.Attributes {
		attributeIndices[i] = []*irma.DisclosedAttributeIndex{}
		for _, attribute := range attributeset {
			var credIndex int
			ici := attribute.CredentialIdentifier()
			if _, present := credIndices[ici]; !present {
				credIndex = len(todisclose)
				credIndices[ici] = credIndex
				todisclose = append(todisclose, attributeGroup{
					cred: ici, attrs: []int{1}, // Always disclose metadata
				})
			} else {
				credIndex = credIndices[ici]
			}
625

626
627
628
629
			identifier := attribute.Type
			if identifier.IsCredential() {
				attributeIndices[i] = append(attributeIndices[i], &irma.DisclosedAttributeIndex{CredentialIndex: credIndex, AttributeIndex: 1, Identifier: ici})
				continue // In this case we only disclose the metadata attribute, which is already handled above
630
			}
631

632
633
634
635
636
637
638
639
			attrIndex, err := client.Configuration.CredentialTypes[identifier.CredentialTypeIdentifier()].IndexOf(identifier)
			if err != nil {
				return nil, nil, err
			}
			// These attribute indices will be used in the []*big.Int at gabi.credential.Attributes,
			// which doesn't know about the secret key and metadata attribute, so +2
			attributeIndices[i] = append(attributeIndices[i], &irma.DisclosedAttributeIndex{CredentialIndex: credIndex, AttributeIndex: attrIndex + 2, Identifier: ici})
			todisclose[credIndex].attrs = append(todisclose[credIndex].attrs, attrIndex+2)
640
641
642
		}
	}

643
	return todisclose, attributeIndices, nil
644
645
}

646
// ProofBuilders constructs a list of proof builders for the specified attribute choice.
647
648
func (client *Client) ProofBuilders(choice *irma.DisclosureChoice, request irma.SessionRequest, issig bool,
) (gabi.ProofBuilderList, irma.DisclosedAttributeIndices, error) {
649
	todisclose, attributeIndices, err := client.groupCredentials(choice)
650
	if err != nil {
651
		return nil, nil, err
652
653
	}

654
	builders := gabi.ProofBuilderList([]gabi.ProofBuilder{})
655
656
	for _, grp := range todisclose {
		cred, err := client.credentialByID(grp.cred)
657
		if err != nil {
658
			return nil, nil, err
659
		}
660
		builders = append(builders, cred.Credential.CreateDisclosureProofBuilder(grp.attrs))
661
	}
662
663
664
665
666
667
668
669
670
671
672
673
674
675

	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 {
676
			return nil, nil, err
677
678
679
		}
	}

680
	return builders, attributeIndices, nil
681
682
683
}

// Proofs computes disclosure proofs containing the attributes specified by choice.
684
685
func (client *Client) Proofs(choice *irma.DisclosureChoice, request irma.SessionRequest, issig bool) (*irma.Disclosure, error) {
	builders, choices, err := client.ProofBuilders(choice, request, issig)
686
687
688
689
	if err != nil {
		return nil, err
	}

690
	return &irma.Disclosure{
691
		Proofs:  builders.BuildProofList(request.Base().GetContext(), request.GetNonce(), issig),
692
693
		Indices: choices,
	}, nil
Sietse Ringers's avatar
Sietse Ringers committed
694
695
}

696
697
698
699
700
// generateIssuerProofNonce generates a nonce which the issuer must use in its gabi.ProofS.
func generateIssuerProofNonce() (*big.Int, error) {
	return gabi.RandomBigInt(gabi.DefaultSystemParameters[4096].Lstatzk)
}

701
// IssuanceProofBuilders constructs a list of proof builders in the issuance protocol
702
703
// for the future credentials as well as possibly any disclosed attributes, and generates
// a nonce against which the issuer's proof of knowledge must verify.
704
705
func (client *Client) IssuanceProofBuilders(request *irma.IssuanceRequest,
) (gabi.ProofBuilderList, irma.DisclosedAttributeIndices, *big.Int, error) {
706
	issuerProofNonce, err := generateIssuerProofNonce()
Sietse Ringers's avatar
Sietse Ringers committed
707
	if err != nil {
708
		return nil, nil, nil, err
Sietse Ringers's avatar
Sietse Ringers committed
709
	}
710
	builders := gabi.ProofBuilderList([]gabi.ProofBuilder{})
Sietse Ringers's avatar
Sietse Ringers committed
711
	for _, futurecred := range request.Credentials {
Sietse Ringers's avatar
Sietse Ringers committed
712
		var pk *gabi.PublicKey
713
		pk, err = client.Configuration.PublicKey(futurecred.CredentialTypeID.IssuerIdentifier(), futurecred.KeyCounter)
714
		if err != nil {
715
			return nil, nil, nil, err
716
		}
717
		credBuilder := gabi.NewCredentialBuilder(
718
719
			pk, request.GetContext(), client.secretkey.Key, issuerProofNonce)
		builders = append(builders, credBuilder)
Sietse Ringers's avatar
Sietse Ringers committed
720
	}
Sietse Ringers's avatar
Sietse Ringers committed
721

722
	disclosures, choices, err := client.ProofBuilders(request.Choice, request, false)
Sietse Ringers's avatar
Sietse Ringers committed
723
	if err != nil {
724
		return nil, nil, nil, err
Sietse Ringers's avatar
Sietse Ringers committed
725
	}
726
	builders = append(disclosures, builders...)
727
	return builders, choices, issuerProofNonce, nil
Sietse Ringers's avatar
Sietse Ringers committed
728
}
Sietse Ringers's avatar
Sietse Ringers committed
729

730
731
// IssueCommitments computes issuance commitments, along with disclosure proofs specified by choice,
// and also returns the credential builders which will become the new credentials upon combination with the issuer's signature.
732
733
734
func (client *Client) IssueCommitments(request *irma.IssuanceRequest,
) (*irma.IssueCommitmentMessage, gabi.ProofBuilderList, error) {
	builders, choices, issuerProofNonce, err := client.IssuanceProofBuilders(request)
Sietse Ringers's avatar
Sietse Ringers committed
735
	if err != nil {
736
		return nil, nil, err
Sietse Ringers's avatar
Sietse Ringers committed
737
	}
738
739
740
741
742
743
744
	return &irma.IssueCommitmentMessage{
		IssueCommitmentMessage: &gabi.IssueCommitmentMessage{
			Proofs: builders.BuildProofList(request.GetContext(), request.GetNonce(), false),
			Nonce2: issuerProofNonce,
		},
		Indices: choices,
	}, builders, nil
Sietse Ringers's avatar
Sietse Ringers committed
745
746
}

747
748
749
750
// ConstructCredentials constructs and saves new credentials using the specified issuance signature messages
// and credential builders.
func (client *Client) ConstructCredentials(msg []*gabi.IssueSignatureMessage, request *irma.IssuanceRequest, builders gabi.ProofBuilderList) error {
	if len(msg) > len(builders) {
Sietse Ringers's avatar
Sietse Ringers committed
751
752
753
		return errors.New("Received unexpected amount of signatures")
	}

754
755
	// 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
756
	gabicreds := []*gabi.Credential{}
757
758
759
760
761
762
763
764
	offset := 0
	for i, builder := range builders {
		credbuilder, ok := builder.(*gabi.CredentialBuilder)
		if !ok { // Skip builders of disclosure proofs
			offset++
			continue
		}
		sig := msg[i-offset]
765
		attrs, err := request.Credentials[i-offset].AttributeList(client.Configuration, irma.GetMetadataVersion(request.Base().Version))
Sietse Ringers's avatar
Sietse Ringers committed
766
767
768
		if err != nil {
			return err
		}
769
		cred, err := credbuilder.ConstructCredential(sig, attrs.Ints)
Sietse Ringers's avatar
Sietse Ringers committed
770
771
772
		if err != nil {
			return err
		}
773
		gabicreds = append(gabicreds, cred)
Sietse Ringers's avatar
Sietse Ringers committed
774
775
	}

776
	for _, gabicred := range gabicreds {
777
		newcred, err := newCredential(gabicred, client.Configuration)
778
779
780
		if err != nil {
			return err
		}
781
		if err = client.addCredential(newcred, true); err != nil {
Sietse Ringers's avatar
Sietse Ringers committed
782
783
			return err
		}
Sietse Ringers's avatar
Sietse Ringers committed
784
	}
785

Sietse Ringers's avatar
Sietse Ringers committed
786
	return nil
Sietse Ringers's avatar
Sietse Ringers committed
787
}
788

Sietse Ringers's avatar
Sietse Ringers committed
789
790
// Keyshare server handling

791
func (client *Client) genSchemeManagersList(enrolled bool) []irma.SchemeManagerIdentifier {
792
	list := []irma.SchemeManagerIdentifier{}
793
	for name, manager := range client.Configuration.SchemeManagers {
794
		if _, contains := client.keyshareServers[name]; manager.Distributed() && contains == enrolled {
Sietse Ringers's avatar
Sietse Ringers committed
795
			list = append(list, manager.Identifier())
Sietse Ringers's avatar
Sietse Ringers committed
796
797
798
799
		}
	}
	return list
}
Sietse Ringers's avatar
Sietse Ringers committed
800

801
802
803
804
func (client *Client) UnenrolledSchemeManagers() []irma.SchemeManagerIdentifier {
	return client.genSchemeManagersList(false)
}

805
func (client *Client) EnrolledSchemeManagers() []irma.SchemeManagerIdentifier {
806
	return client.genSchemeManagersList(true)
807
808
}

809
// KeyshareEnroll attempts to enroll at the keyshare server of the specified scheme manager.
810
func (client *Client) KeyshareEnroll(manager irma.SchemeManagerIdentifier, email *string, pin string, lang string) {
Sietse Ringers's avatar
Sietse Ringers committed
811
	go func() {
812
		err := client.keyshareEnrollWorker(manager, email, pin, lang)
Sietse Ringers's avatar
Sietse Ringers committed
813
		if err != nil {
Tomas's avatar
Tomas committed
814
			client.handler.EnrollmentFailure(manager, err)
Sietse Ringers's avatar
Sietse Ringers committed
815
816
		}
	}()
817
818
}

819
func (client *Client) keyshareEnrollWorker(managerID irma.SchemeManagerIdentifier, email *string, pin string, lang string) error {
820
	manager, ok := client.Configuration.SchemeManagers[managerID]
Sietse Ringers's avatar
Sietse Ringers committed
821
822
823
824
825
826
827
828
829
830
	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")
	}

831
	transport := irma.NewHTTPTransport(manager.KeyshareServer)
832
	kss, err := newKeyshareServer(managerID, manager.KeyshareServer)
Sietse Ringers's avatar
Sietse Ringers committed
833
834
835
	if err != nil {
		return err
	}
836
	message := keyshareEnrollment{
837
838
839
		Email:    email,
		Pin:      kss.HashedPin(pin),
		Language: lang,
Sietse Ringers's avatar
Sietse Ringers committed
840
841
	}

842
843
	qr := &irma.Qr{}
	err = transport.Post("client/register", qr, message)
Sietse Ringers's avatar
Sietse Ringers committed
844
845
846
847
	if err != nil {
		return err
	}

848
849
850
851
852
	// 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.
853
	client.keyshareServers[managerID] = kss
854
	client.newQrSession(qr, &keyshareEnrollmentHandler{
855
856
857
858
859
860
		client: client,
		pin:    pin,
		kss:    kss,
	})

	return nil
Sietse Ringers's avatar
Sietse Ringers committed
861
862
}

863
864
865
// KeyshareVerifyPin verifies the specified PIN at the keyshare server, returning if it succeeded;
// if not, how many tries are left, or for how long the user is blocked. If an error is returned
// it is of type *irma.SessionError.
866
867
868
func (client *Client) KeyshareVerifyPin(pin string, schemeid irma.SchemeManagerIdentifier) (bool, int, int, error) {
	scheme := client.Configuration.SchemeManagers[schemeid]
	if scheme == nil || !scheme.Distributed() {
869
870
871
872
873
		return false, 0, 0, &irma.SessionError{
			Err:       errors.Errorf("Can't verify pin of scheme %s", schemeid.String()),
			ErrorType: irma.ErrorUnknownSchemeManager,
			Info:      schemeid.String(),
		}
874
875
876
877
878
	}
	kss := client.keyshareServers[schemeid]
	return verifyPinWorker(pin, kss, irma.NewHTTPTransport(kss.URL))
}

879
func (client *Client) KeyshareChangePin(manager irma.SchemeManagerIdentifier, oldPin string, newPin string) {
880
	go func() {
881
		err := client.keyshareChangePinWorker(manager, oldPin, newPin)
882
		if err != nil {
883
			client.handler.ChangePinFailure(manager, err)
884
885
886
887
		}
	}()
}

888
func (client *Client) keyshareChangePinWorker(managerID irma.SchemeManagerIdentifier, oldPin string, newPin string) error {
889
890
891
892
893
894
	kss, ok := client.keyshareServers[managerID]
	if !ok {
		return errors.New("Unknown keyshare server")
	}

	transport := irma.NewHTTPTransport(kss.URL)
895
	message := keyshareChangepin{
896
		Username: kss.Username,
897
898
		OldPin:   kss.HashedPin(oldPin),
		NewPin:   kss.HashedPin(newPin),
899
900
901
902
903
904
905
	}

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

907
908
	switch res.Status {
	case kssPinSuccess:
909
		client.handler.ChangePinSuccess(managerID)
910
	case kssPinFailure:
911
912
913
914
915
		attempts, err := strconv.Atoi(res.Message)
		if err != nil {
			return err
		}
		client.handler.ChangePinIncorrect(managerID, attempts)
916
	case kssPinError:
917
918
919
920
921
		timeout, err := strconv.Atoi(res.Message)
		if err != nil {
			return err
		}
		client.handler.ChangePinBlocked(managerID, timeout)
922
923
	default:
		return errors.New("Unknown keyshare response")
924
	}
925

926
927
928
	return nil
}

929
// KeyshareRemove unenrolls the keyshare server of the specified scheme manager.
930
func (client *Client) KeyshareRemove(manager irma.SchemeManagerIdentifier) error {
931
	if _, contains := client.keyshareServers[manager]; !contains {
Sietse Ringers's avatar
Sietse Ringers committed
932
933
		return errors.New("Can't uninstall unknown keyshare server")
	}
934
935
	delete(client.keyshareServers, manager)
	return client.storage.StoreKeyshareServers(client.keyshareServers)