Shivaram Lingamneni 4 лет назад
Родитель
Сommit
f920d3b79f
16 измененных файлов: 447 добавлений и 298 удалений
  1. 2
    8
      gencapdefs.py
  2. 187
    44
      irc/accounts.go
  3. 3
    8
      irc/caps/defs.go
  4. 1
    0
      irc/chanserv.go
  5. 2
    1
      irc/client.go
  6. 0
    5
      irc/commands.go
  7. 6
    1
      irc/config.go
  8. 57
    1
      irc/database.go
  9. 4
    0
      irc/errors.go
  10. 6
    2
      irc/gateways.go
  11. 2
    190
      irc/handlers.go
  12. 1
    1
      irc/legacy.go
  13. 145
    14
      irc/nickserv.go
  14. 11
    9
      irc/utils/crypto.go
  15. 17
    11
      irc/utils/crypto_test.go
  16. 3
    3
      oragono.yaml

+ 2
- 8
gencapdefs.py Просмотреть файл

@@ -15,12 +15,6 @@ from collections import namedtuple
15 15
 CapDef = namedtuple("CapDef", ['identifier', 'name', 'url', 'standard'])
16 16
 
17 17
 CAPDEFS = [
18
-    CapDef(
19
-        identifier="Acc",
20
-        name="draft/acc",
21
-        url="https://github.com/ircv3/ircv3-specifications/pull/276",
22
-        standard="proposed IRCv3",
23
-    ),
24 18
     CapDef(
25 19
         identifier="AccountNotify",
26 20
         name="account-notify",
@@ -163,7 +157,7 @@ CAPDEFS = [
163 157
         identifier="EventPlayback",
164 158
         name="draft/event-playback",
165 159
         url="https://github.com/ircv3/ircv3-specifications/pull/362",
166
-        standard="Proposed IRCv3",
160
+        standard="proposed IRCv3",
167 161
     ),
168 162
     CapDef(
169 163
         identifier="ZNCPlayback",
@@ -181,7 +175,7 @@ CAPDEFS = [
181 175
         identifier="Multiline",
182 176
         name="draft/multiline",
183 177
         url="https://github.com/ircv3/ircv3-specifications/pull/398",
184
-        standard="Proposed IRCv3",
178
+        standard="proposed IRCv3",
185 179
     ),
186 180
 ]
187 181
 

+ 187
- 44
irc/accounts.go Просмотреть файл

@@ -36,6 +36,8 @@ const (
36 36
 
37 37
 	keyVHostQueueAcctToId = "vhostQueue %s"
38 38
 	vhostRequestIdx       = "vhostQueue"
39
+
40
+	maxCertfpsPerAccount = 5
39 41
 )
40 42
 
41 43
 // everything about accounts is persistent; therefore, the database is the authoritative
@@ -327,7 +329,14 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
327 329
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
328 330
 	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
329 331
 
330
-	credStr, err := am.serializeCredentials(passphrase, certfp)
332
+	var creds AccountCredentials
333
+	creds.Version = 1
334
+	err = creds.SetPassphrase(passphrase, am.server.Config().Accounts.Registration.BcryptCost)
335
+	if err != nil {
336
+		return err
337
+	}
338
+	creds.AddCertfp(certfp)
339
+	credStr, err := creds.Serialize()
331 340
 	if err != nil {
332 341
 		return err
333 342
 	}
@@ -411,58 +420,124 @@ func validatePassphrase(passphrase string) error {
411 420
 	return nil
412 421
 }
413 422
 
414
-// helper to assemble the serialized JSON for an account's credentials
415
-func (am *AccountManager) serializeCredentials(passphrase string, certfp string) (result string, err error) {
423
+// changes the password for an account
424
+func (am *AccountManager) setPassword(account string, password string, hasPrivs bool) (err error) {
425
+	cfAccount, err := CasefoldName(account)
426
+	if err != nil {
427
+		return errAccountDoesNotExist
428
+	}
429
+
430
+	credKey := fmt.Sprintf(keyAccountCredentials, cfAccount)
431
+	var credStr string
432
+	am.server.store.View(func(tx *buntdb.Tx) error {
433
+		// no need to check verification status here or below;
434
+		// you either need to be auth'ed to the account or be an oper to do this
435
+		credStr, err = tx.Get(credKey)
436
+		return nil
437
+	})
438
+
439
+	if err != nil {
440
+		return errAccountDoesNotExist
441
+	}
442
+
416 443
 	var creds AccountCredentials
417
-	creds.Version = 1
418
-	// we need at least one of passphrase and certfp:
419
-	if passphrase == "" && certfp == "" {
420
-		return "", errAccountBadPassphrase
421
-	}
422
-	// but if we have one, it's fine if the other is missing, it just means no
423
-	// credential of that type will be accepted.
424
-	creds.Certificate = certfp
425
-	if passphrase != "" {
426
-		if validatePassphrase(passphrase) != nil {
427
-			return "", errAccountBadPassphrase
428
-		}
429
-		bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost)
430
-		creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
431
-		if err != nil {
432
-			am.server.logger.Error("internal", "could not hash password", err.Error())
433
-			return "", errAccountCreation
434
-		}
444
+	err = json.Unmarshal([]byte(credStr), &creds)
445
+	if err != nil {
446
+		return err
435 447
 	}
436 448
 
437
-	credText, err := json.Marshal(creds)
449
+	err = creds.SetPassphrase(password, am.server.Config().Accounts.Registration.BcryptCost)
438 450
 	if err != nil {
439
-		am.server.logger.Error("internal", "could not marshal credentials", err.Error())
440
-		return "", errAccountCreation
451
+		return err
441 452
 	}
442
-	return string(credText), nil
453
+
454
+	if creds.Empty() && !hasPrivs {
455
+		return errEmptyCredentials
456
+	}
457
+
458
+	newCredStr, err := creds.Serialize()
459
+	if err != nil {
460
+		return err
461
+	}
462
+
463
+	err = am.server.store.Update(func(tx *buntdb.Tx) error {
464
+		curCredStr, err := tx.Get(credKey)
465
+		if credStr != curCredStr {
466
+			return errCASFailed
467
+		}
468
+		_, _, err = tx.Set(credKey, newCredStr, nil)
469
+		return err
470
+	})
471
+
472
+	return err
443 473
 }
444 474
 
445
-// changes the password for an account
446
-func (am *AccountManager) setPassword(account string, password string) (err error) {
447
-	casefoldedAccount, err := CasefoldName(account)
475
+func (am *AccountManager) addRemoveCertfp(account, certfp string, add bool, hasPrivs bool) (err error) {
476
+	certfp, err = utils.NormalizeCertfp(certfp)
477
+	if err != nil {
478
+		return err
479
+	}
480
+
481
+	cfAccount, err := CasefoldName(account)
482
+	if err != nil {
483
+		return errAccountDoesNotExist
484
+	}
485
+
486
+	credKey := fmt.Sprintf(keyAccountCredentials, cfAccount)
487
+	var credStr string
488
+	am.server.store.View(func(tx *buntdb.Tx) error {
489
+		credStr, err = tx.Get(credKey)
490
+		return nil
491
+	})
492
+
493
+	if err != nil {
494
+		return errAccountDoesNotExist
495
+	}
496
+
497
+	var creds AccountCredentials
498
+	err = json.Unmarshal([]byte(credStr), &creds)
448 499
 	if err != nil {
449 500
 		return err
450 501
 	}
451
-	act, err := am.LoadAccount(casefoldedAccount)
502
+
503
+	if add {
504
+		err = creds.AddCertfp(certfp)
505
+	} else {
506
+		err = creds.RemoveCertfp(certfp)
507
+	}
452 508
 	if err != nil {
453 509
 		return err
454 510
 	}
455 511
 
456
-	credStr, err := am.serializeCredentials(password, act.Credentials.Certificate)
512
+	if creds.Empty() && !hasPrivs {
513
+		return errEmptyCredentials
514
+	}
515
+
516
+	newCredStr, err := creds.Serialize()
457 517
 	if err != nil {
458 518
 		return err
459 519
 	}
460 520
 
461
-	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
462
-	return am.server.store.Update(func(tx *buntdb.Tx) error {
463
-		_, _, err := tx.Set(credentialsKey, credStr, nil)
521
+	certfpKey := fmt.Sprintf(keyCertToAccount, certfp)
522
+	err = am.server.store.Update(func(tx *buntdb.Tx) error {
523
+		curCredStr, err := tx.Get(credKey)
524
+		if credStr != curCredStr {
525
+			return errCASFailed
526
+		}
527
+		if add {
528
+			_, err = tx.Get(certfpKey)
529
+			if err != buntdb.ErrNotFound {
530
+				return errCertfpAlreadyExists
531
+			}
532
+			tx.Set(certfpKey, cfAccount, nil)
533
+		} else {
534
+			tx.Delete(certfpKey)
535
+		}
536
+		_, _, err = tx.Set(credKey, newCredStr, nil)
464 537
 		return err
465 538
 	})
539
+
540
+	return err
466 541
 }
467 542
 
468 543
 func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
@@ -574,8 +649,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
574 649
 			// XXX we shouldn't do (de)serialization inside the txn,
575 650
 			// but this is like 2 usec on my system
576 651
 			json.Unmarshal([]byte(raw.Credentials), &creds)
577
-			if creds.Certificate != "" {
578
-				certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
652
+			for _, cert := range creds.Certfps {
653
+				certFPKey := fmt.Sprintf(keyCertToAccount, cert)
579 654
 				tx.Set(certFPKey, casefoldedAccount, nil)
580 655
 			}
581 656
 
@@ -906,14 +981,16 @@ func (am *AccountManager) Unregister(account string) error {
906 981
 
907 982
 	if err == nil {
908 983
 		var creds AccountCredentials
909
-		if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" {
910
-			certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
911
-			am.server.store.Update(func(tx *buntdb.Tx) error {
912
-				if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
913
-					tx.Delete(certFPKey)
914
-				}
915
-				return nil
916
-			})
984
+		if err = json.Unmarshal([]byte(credText), &creds); err == nil {
985
+			for _, cert := range creds.Certfps {
986
+				certFPKey := fmt.Sprintf(keyCertToAccount, cert)
987
+				am.server.store.Update(func(tx *buntdb.Tx) error {
988
+					if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
989
+						tx.Delete(certFPKey)
990
+					}
991
+					return nil
992
+				})
993
+			}
917 994
 		}
918 995
 	}
919 996
 
@@ -1326,7 +1403,73 @@ type AccountCredentials struct {
1326 1403
 	Version        uint
1327 1404
 	PassphraseSalt []byte // legacy field, not used by v1 and later
1328 1405
 	PassphraseHash []byte
1329
-	Certificate    string // fingerprint
1406
+	Certfps        []string
1407
+}
1408
+
1409
+func (ac *AccountCredentials) Empty() bool {
1410
+	return len(ac.PassphraseHash) == 0 && len(ac.Certfps) == 0
1411
+}
1412
+
1413
+// helper to assemble the serialized JSON for an account's credentials
1414
+func (ac *AccountCredentials) Serialize() (result string, err error) {
1415
+	ac.Version = 1
1416
+	credText, err := json.Marshal(*ac)
1417
+	if err != nil {
1418
+		return "", err
1419
+	}
1420
+	return string(credText), nil
1421
+}
1422
+
1423
+func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint) (err error) {
1424
+	if passphrase == "" {
1425
+		ac.PassphraseHash = nil
1426
+		return nil
1427
+	}
1428
+
1429
+	if validatePassphrase(passphrase) != nil {
1430
+		return errAccountBadPassphrase
1431
+	}
1432
+
1433
+	ac.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), int(bcryptCost))
1434
+	if err != nil {
1435
+		return errAccountBadPassphrase
1436
+	}
1437
+
1438
+	return nil
1439
+}
1440
+
1441
+func (ac *AccountCredentials) AddCertfp(certfp string) (err error) {
1442
+	for _, current := range ac.Certfps {
1443
+		if certfp == current {
1444
+			return errNoop
1445
+		}
1446
+	}
1447
+
1448
+	if maxCertfpsPerAccount <= len(ac.Certfps) {
1449
+		return errLimitExceeded
1450
+	}
1451
+
1452
+	ac.Certfps = append(ac.Certfps, certfp)
1453
+	return nil
1454
+}
1455
+
1456
+func (ac *AccountCredentials) RemoveCertfp(certfp string) (err error) {
1457
+	found := false
1458
+	newList := make([]string, 0, len(ac.Certfps))
1459
+	for _, current := range ac.Certfps {
1460
+		if current == certfp {
1461
+			found = true
1462
+		} else {
1463
+			newList = append(newList, current)
1464
+		}
1465
+	}
1466
+	if !found {
1467
+		// this is important because it prevents you from deleting someone else's
1468
+		// fingerprint record
1469
+		return errNoop
1470
+	}
1471
+	ac.Certfps = newList
1472
+	return nil
1330 1473
 }
1331 1474
 
1332 1475
 type BouncerAllowedSetting int

+ 3
- 8
irc/caps/defs.go Просмотреть файл

@@ -7,7 +7,7 @@ package caps
7 7
 
8 8
 const (
9 9
 	// number of recognized capabilities:
10
-	numCapabs = 28
10
+	numCapabs = 27
11 11
 	// length of the uint64 array that represents the bitset:
12 12
 	bitsetLen = 1
13 13
 )
@@ -37,11 +37,7 @@ const (
37 37
 	// https://ircv3.net/specs/extensions/chghost-3.2.html
38 38
 	ChgHost Capability = iota
39 39
 
40
-	// Acc is the proposed IRCv3 capability named "draft/acc":
41
-	// https://github.com/ircv3/ircv3-specifications/pull/276
42
-	Acc Capability = iota
43
-
44
-	// EventPlayback is the Proposed IRCv3 capability named "draft/event-playback":
40
+	// EventPlayback is the proposed IRCv3 capability named "draft/event-playback":
45 41
 	// https://github.com/ircv3/ircv3-specifications/pull/362
46 42
 	EventPlayback Capability = iota
47 43
 
@@ -53,7 +49,7 @@ const (
53 49
 	// https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6
54 50
 	Languages Capability = iota
55 51
 
56
-	// Multiline is the Proposed IRCv3 capability named "draft/multiline":
52
+	// Multiline is the proposed IRCv3 capability named "draft/multiline":
57 53
 	// https://github.com/ircv3/ircv3-specifications/pull/398
58 54
 	Multiline Capability = iota
59 55
 
@@ -135,7 +131,6 @@ var (
135 131
 		"batch",
136 132
 		"cap-notify",
137 133
 		"chghost",
138
-		"draft/acc",
139 134
 		"draft/event-playback",
140 135
 		"draft/labeled-response-0.2",
141 136
 		"draft/languages",

+ 1
- 0
irc/chanserv.go Просмотреть файл

@@ -134,6 +134,7 @@ set using PURGE.`,
134 134
 
135 135
 INFO displays info about a registered channel.`,
136 136
 			helpShort: `$bINFO$b displays info about a registered channel.`,
137
+			enabled:   chanregEnabled,
137 138
 			minParams: 1,
138 139
 		},
139 140
 	}

+ 2
- 1
irc/client.go Просмотреть файл

@@ -1419,10 +1419,11 @@ func (client *Client) attemptAutoOper(session *Session) {
1419 1419
 		return
1420 1420
 	}
1421 1421
 	for _, oper := range client.server.Config().operators {
1422
-		if oper.Auto && oper.Pass == nil && utils.CertfpsMatch(oper.Fingerprint, client.certfp) {
1422
+		if oper.Auto && oper.Pass == nil && oper.Fingerprint != "" && oper.Fingerprint == client.certfp {
1423 1423
 			rb := NewResponseBuffer(session)
1424 1424
 			applyOper(client, oper, rb)
1425 1425
 			rb.Send(true)
1426
+			return
1426 1427
 		}
1427 1428
 	}
1428 1429
 }

+ 0
- 5
irc/commands.go Просмотреть файл

@@ -80,11 +80,6 @@ var Commands map[string]Command
80 80
 
81 81
 func init() {
82 82
 	Commands = map[string]Command{
83
-		"ACC": {
84
-			handler:      accHandler,
85
-			usablePreReg: true,
86
-			minParams:    1,
87
-		},
88 83
 		"AMBIANCE": {
89 84
 			handler:   sceneHandler,
90 85
 			minParams: 2,

+ 6
- 1
irc/config.go Просмотреть файл

@@ -511,7 +511,12 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
511 511
 				return nil, fmt.Errorf("Oper %s has an invalid password hash: %s", oper.Name, err.Error())
512 512
 			}
513 513
 		}
514
-		oper.Fingerprint = opConf.Fingerprint
514
+		if opConf.Fingerprint != "" {
515
+			oper.Fingerprint, err = utils.NormalizeCertfp(opConf.Fingerprint)
516
+			if err != nil {
517
+				return nil, fmt.Errorf("Oper %s has an invalid fingerprint: %s", oper.Name, err.Error())
518
+			}
519
+		}
515 520
 		oper.Auto = opConf.Auto
516 521
 
517 522
 		if oper.Pass == nil && oper.Fingerprint == "" {

+ 57
- 1
irc/database.go Просмотреть файл

@@ -22,7 +22,7 @@ const (
22 22
 	// 'version' of the database schema
23 23
 	keySchemaVersion = "db.version"
24 24
 	// latest schema of the db
25
-	latestDbSchema = "8"
25
+	latestDbSchema = "9"
26 26
 )
27 27
 
28 28
 type SchemaChanger func(*Config, *buntdb.Tx) error
@@ -553,6 +553,57 @@ func schemaChangeV7ToV8(config *Config, tx *buntdb.Tx) error {
553 553
 	return nil
554 554
 }
555 555
 
556
+type accountCredsLegacyV8 struct {
557
+	Version        uint
558
+	PassphraseSalt []byte // legacy field, not used by v1 and later
559
+	PassphraseHash []byte
560
+	Certificate    string
561
+}
562
+
563
+type accountCredsLegacyV9 struct {
564
+	Version        uint
565
+	PassphraseSalt []byte // legacy field, not used by v1 and later
566
+	PassphraseHash []byte
567
+	Certfps        []string
568
+}
569
+
570
+// #530: support multiple client certificate fingerprints
571
+func schemaChangeV8ToV9(config *Config, tx *buntdb.Tx) error {
572
+	prefix := "account.credentials "
573
+	var accounts, blobs []string
574
+	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
575
+		var legacy accountCredsLegacyV8
576
+		var current accountCredsLegacyV9
577
+		if !strings.HasPrefix(key, prefix) {
578
+			return false
579
+		}
580
+		account := strings.TrimPrefix(key, prefix)
581
+		err := json.Unmarshal([]byte(value), &legacy)
582
+		if err != nil {
583
+			log.Printf("corrupt record for %s: %v\n", account, err)
584
+			return true
585
+		}
586
+		current.Version = legacy.Version
587
+		current.PassphraseSalt = legacy.PassphraseSalt // ugh can't get rid of this
588
+		current.PassphraseHash = legacy.PassphraseHash
589
+		if legacy.Certificate != "" {
590
+			current.Certfps = []string{legacy.Certificate}
591
+		}
592
+		blob, err := json.Marshal(current)
593
+		if err != nil {
594
+			log.Printf("could not marshal record for %s: %v\n", account, err)
595
+			return true
596
+		}
597
+		accounts = append(accounts, account)
598
+		blobs = append(blobs, string(blob))
599
+		return true
600
+	})
601
+	for i, account := range accounts {
602
+		tx.Set(prefix+account, blobs[i], nil)
603
+	}
604
+	return nil
605
+}
606
+
556 607
 func init() {
557 608
 	allChanges := []SchemaChange{
558 609
 		{
@@ -590,6 +641,11 @@ func init() {
590 641
 			TargetVersion:  "8",
591 642
 			Changer:        schemaChangeV7ToV8,
592 643
 		},
644
+		{
645
+			InitialVersion: "8",
646
+			TargetVersion:  "9",
647
+			Changer:        schemaChangeV8ToV9,
648
+		},
593 649
 	}
594 650
 
595 651
 	// build the index

+ 4
- 0
irc/errors.go Просмотреть файл

@@ -50,6 +50,10 @@ var (
50 50
 	errBanned                         = errors.New("IP or nickmask banned")
51 51
 	errInvalidParams                  = utils.ErrInvalidParams
52 52
 	errNoVhost                        = errors.New(`You do not have an approved vhost`)
53
+	errLimitExceeded                  = errors.New("Limit exceeded")
54
+	errNoop                           = errors.New("Action was a no-op")
55
+	errCASFailed                      = errors.New("Compare-and-swap update of database value failed")
56
+	errEmptyCredentials               = errors.New("No more credentials are approved")
53 57
 )
54 58
 
55 59
 // Socket Errors

+ 6
- 2
irc/gateways.go Просмотреть файл

@@ -39,13 +39,17 @@ type webircConfig struct {
39 39
 // Populate fills out our password or fingerprint.
40 40
 func (wc *webircConfig) Populate() (err error) {
41 41
 	if wc.Fingerprint == "" && wc.PasswordString == "" {
42
-		return ErrNoFingerprintOrPassword
42
+		err = ErrNoFingerprintOrPassword
43 43
 	}
44 44
 
45
-	if wc.PasswordString != "" {
45
+	if err == nil && wc.PasswordString != "" {
46 46
 		wc.Password, err = decodeLegacyPasswordHash(wc.PasswordString)
47 47
 	}
48 48
 
49
+	if err == nil && wc.Fingerprint != "" {
50
+		wc.Fingerprint, err = utils.NormalizeCertfp(wc.Fingerprint)
51
+	}
52
+
49 53
 	if err == nil {
50 54
 		wc.allowedNets, err = utils.ParseNetList(wc.Hosts)
51 55
 	}

+ 2
- 190
irc/handlers.go Просмотреть файл

@@ -31,52 +31,6 @@ import (
31 31
 	"golang.org/x/crypto/bcrypt"
32 32
 )
33 33
 
34
-// ACC [LS|REGISTER|VERIFY] ...
35
-func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
36
-	subcommand := strings.ToLower(msg.Params[0])
37
-
38
-	if subcommand == "ls" {
39
-		config := server.Config().Accounts
40
-
41
-		rb.Add(nil, server.name, "ACC", "LS", "SUBCOMMANDS", "LS REGISTER VERIFY")
42
-
43
-		// this list is sorted by the config loader, yay
44
-		rb.Add(nil, server.name, "ACC", "LS", "CALLBACKS", strings.Join(config.Registration.EnabledCallbacks, " "))
45
-
46
-		rb.Add(nil, server.name, "ACC", "LS", "CREDTYPES", "passphrase certfp")
47
-
48
-		flags := []string{"nospaces"}
49
-		if config.NickReservation.Enabled {
50
-			flags = append(flags, "regnick")
51
-		}
52
-		sort.Strings(flags)
53
-		rb.Add(nil, server.name, "ACC", "LS", "FLAGS", strings.Join(flags, " "))
54
-		return false
55
-	}
56
-
57
-	// disallow account stuff before connection registration has completed, for now
58
-	if !client.Registered() {
59
-		client.Send(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command"))
60
-		return false
61
-	}
62
-
63
-	// make sure reg is enabled
64
-	if !server.AccountConfig().Registration.Enabled {
65
-		rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNAVAILABLE", client.t("Account registration is disabled"))
66
-		return false
67
-	}
68
-
69
-	if subcommand == "register" {
70
-		return accRegisterHandler(server, client, msg, rb)
71
-	} else if subcommand == "verify" {
72
-		return accVerifyHandler(server, client, msg, rb)
73
-	} else {
74
-		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
75
-	}
76
-
77
-	return false
78
-}
79
-
80 34
 // helper function to parse ACC callbacks, e.g., mailto:person@example.com, tel:16505551234
81 35
 func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) {
82 36
 	callback := strings.ToLower(spec)
@@ -103,113 +57,6 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
103 57
 	return
104 58
 }
105 59
 
106
-// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
107
-func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
108
-	nick := client.Nick()
109
-
110
-	if len(msg.Params) < 4 {
111
-		rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
112
-		return false
113
-	}
114
-
115
-	account := msg.Params[1]
116
-
117
-	// check for account name of *
118
-	if account == "*" {
119
-		account = nick
120
-	} else {
121
-		if server.Config().Accounts.NickReservation.Enabled {
122
-			rb.Add(nil, server.name, "FAIL", "ACC", "REG_MUST_USE_REGNICK", account, client.t("Must register with current nickname instead of separate account name"))
123
-			return false
124
-		}
125
-	}
126
-
127
-	// clients can't reg new accounts if they're already logged in
128
-	if client.LoggedIntoAccount() {
129
-		rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNSPECIFIED_ERROR", account, client.t("You're already logged into an account"))
130
-		return false
131
-	}
132
-
133
-	// sanitise account name
134
-	casefoldedAccount, err := CasefoldName(account)
135
-	if err != nil {
136
-		rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_ACCOUNT_NAME", account, client.t("Account name is not valid"))
137
-		return false
138
-	}
139
-
140
-	callbackSpec := msg.Params[2]
141
-	callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
142
-
143
-	if callbackNamespace == "" {
144
-		rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CALLBACK", account, callbackSpec, client.t("Cannot send verification code there"))
145
-		return false
146
-	}
147
-
148
-	// get credential type/value
149
-	var credentialType, credentialValue string
150
-
151
-	if len(msg.Params) > 4 {
152
-		credentialType = strings.ToLower(msg.Params[3])
153
-		credentialValue = msg.Params[4]
154
-	} else {
155
-		// exactly 4 params
156
-		credentialType = "passphrase" // default from the spec
157
-		credentialValue = msg.Params[3]
158
-	}
159
-
160
-	// ensure the credential type is valid
161
-	var credentialValid bool
162
-	for _, name := range server.AccountConfig().Registration.EnabledCredentialTypes {
163
-		if credentialType == name {
164
-			credentialValid = true
165
-		}
166
-	}
167
-	if credentialType == "certfp" && client.certfp == "" {
168
-		rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CREDENTIAL", account, client.t("You must connect with a TLS client certificate to use certfp"))
169
-		return false
170
-	}
171
-
172
-	if !credentialValid {
173
-		rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CRED_TYPE", account, credentialType, client.t("Credential type is not supported"))
174
-		return false
175
-	}
176
-
177
-	var passphrase, certfp string
178
-	if credentialType == "certfp" {
179
-		certfp = client.certfp
180
-	} else if credentialType == "passphrase" {
181
-		passphrase = credentialValue
182
-	}
183
-
184
-	throttled, remainingTime := client.loginThrottle.Touch()
185
-	if throttled {
186
-		rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNSPECIFIED_ERROR", account, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
187
-		return false
188
-	}
189
-
190
-	err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
191
-	if err != nil {
192
-		msg, code := registrationErrorToMessageAndCode(err)
193
-		rb.Add(nil, server.name, "FAIL", "ACC", code, account, client.t(msg))
194
-		return false
195
-	}
196
-
197
-	// automatically complete registration
198
-	if callbackNamespace == "*" {
199
-		err := server.accounts.Verify(client, casefoldedAccount, "")
200
-		if err != nil {
201
-			return false
202
-		}
203
-		sendSuccessfulRegResponse(client, rb, false)
204
-	} else {
205
-		messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s")
206
-		message := fmt.Sprintf(messageTemplate, fmt.Sprintf("%s:%s", callbackNamespace, callbackValue))
207
-		rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, nick, casefoldedAccount, message)
208
-	}
209
-
210
-	return false
211
-}
212
-
213 60
 func registrationErrorToMessageAndCode(err error) (message, code string) {
214 61
 	// default responses: let's be risk-averse about displaying internal errors
215 62
 	// to the clients, especially for something as sensitive as accounts
@@ -263,41 +110,6 @@ func sendSuccessfulAccountAuth(client *Client, rb *ResponseBuffer, forNS, forSAS
263 110
 	client.server.logger.Info("accounts", "client", details.nick, "logged into account", details.accountName)
264 111
 }
265 112
 
266
-// ACC VERIFY <accountname> <auth_code>
267
-func accVerifyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
268
-	account := strings.TrimSpace(msg.Params[1])
269
-
270
-	if len(msg.Params) < 3 {
271
-		rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), msg.Command, client.t("Not enough parameters"))
272
-		return false
273
-	}
274
-
275
-	err := server.accounts.Verify(client, account, msg.Params[2])
276
-
277
-	var code string
278
-	var message string
279
-
280
-	if err == errAccountVerificationInvalidCode {
281
-		code = "ACCOUNT_INVALID_VERIFY_CODE"
282
-		message = err.Error()
283
-	} else if err == errAccountAlreadyVerified {
284
-		code = "ACCOUNT_ALREADY_VERIFIED"
285
-		message = err.Error()
286
-	} else if err != nil {
287
-		code = "VERIFY_UNSPECIFIED_ERROR"
288
-		message = errAccountVerificationFailed.Error()
289
-	}
290
-
291
-	if err == nil {
292
-		rb.Add(nil, server.name, RPL_VERIFY_SUCCESS, client.Nick(), account, client.t("Account verification successful"))
293
-		sendSuccessfulAccountAuth(client, rb, false, false)
294
-	} else {
295
-		rb.Add(nil, server.name, "FAIL", "ACC", code, account, client.t(message))
296
-	}
297
-
298
-	return false
299
-}
300
-
301 113
 // AUTHENTICATE [<mechanism>|<data>|*]
302 114
 func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
303 115
 	config := server.Config()
@@ -2300,7 +2112,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2300 2112
 	oper := server.GetOperator(msg.Params[0])
2301 2113
 	if oper != nil {
2302 2114
 		if oper.Fingerprint != "" {
2303
-			if utils.CertfpsMatch(oper.Fingerprint, client.certfp) {
2115
+			if oper.Fingerprint == client.certfp {
2304 2116
 				checkPassed = true
2305 2117
 			} else {
2306 2118
 				checkFailed = true
@@ -2772,7 +2584,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2772 2584
 			if 0 < len(info.Password) && bcrypt.CompareHashAndPassword(info.Password, givenPassword) != nil {
2773 2585
 				continue
2774 2586
 			}
2775
-			if 0 < len(info.Fingerprint) && !utils.CertfpsMatch(info.Fingerprint, client.certfp) {
2587
+			if info.Fingerprint != "" && info.Fingerprint != client.certfp {
2776 2588
 				continue
2777 2589
 			}
2778 2590
 

+ 1
- 1
irc/legacy.go Просмотреть файл

@@ -63,7 +63,7 @@ func handleLegacyPasswordV0(server *Server, account string, credentials AccountC
63 63
 	}
64 64
 
65 65
 	// upgrade credentials
66
-	err = server.accounts.setPassword(account, passphrase)
66
+	err = server.accounts.setPassword(account, passphrase, true)
67 67
 	if err != nil {
68 68
 		server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err))
69 69
 	}

+ 145
- 14
irc/nickserv.go Просмотреть файл

@@ -12,6 +12,7 @@ import (
12 12
 	"github.com/goshuirc/irc-go/ircfmt"
13 13
 
14 14
 	"github.com/oragono/oragono/irc/modes"
15
+	"github.com/oragono/oragono/irc/passwd"
15 16
 	"github.com/oragono/oragono/irc/sno"
16 17
 	"github.com/oragono/oragono/irc/utils"
17 18
 )
@@ -72,6 +73,7 @@ entry for $bSET$b for more information.`,
72 73
 GHOST disconnects the given user from the network if they're logged in with the
73 74
 same user account, letting you reclaim your nickname.`,
74 75
 			helpShort:    `$bGHOST$b reclaims your nickname.`,
76
+			enabled:      servCmdRequiresAuthEnabled,
75 77
 			authRequired: true,
76 78
 			minParams:    1,
77 79
 		},
@@ -92,6 +94,7 @@ will not be able to use it.`,
92 94
 IDENTIFY lets you login to the given username using either password auth, or
93 95
 certfp (your client certificate) if a password is not given.`,
94 96
 			helpShort: `$bIDENTIFY$b lets you login to your account.`,
97
+			enabled:   servCmdRequiresAuthEnabled,
95 98
 			minParams: 1,
96 99
 		},
97 100
 		"info": {
@@ -179,7 +182,8 @@ Or:     $bPASSWD <username> <new>$b
179 182
 PASSWD lets you change your account password. You must supply your current
180 183
 password and confirm the new one by typing it twice. If you're an IRC operator
181 184
 with the correct permissions, you can use PASSWD to reset someone else's
182
-password by supplying their username and then the desired password.`,
185
+password by supplying their username and then the desired password. To
186
+indicate an empty password, use * instead.`,
183 187
 			helpShort: `$bPASSWD$b lets you change your password.`,
184 188
 			enabled:   servCmdRequiresAuthEnabled,
185 189
 			minParams: 2,
@@ -259,6 +263,20 @@ information on the settings and their possible values, see HELP SET.`,
259 263
 			minParams: 3,
260 264
 			capabs:    []string{"accreg"},
261 265
 		},
266
+		"cert": {
267
+			handler: nsCertHandler,
268
+			help: `Syntax: $bCERT <LIST | ADD | DEL> [account] [certfp]$b
269
+
270
+CERT examines or modifies the TLS certificate fingerprints that can be used to
271
+log into an account. Specifically, $bCERT LIST$b lists the authorized
272
+fingerprints, $bCERT ADD <fingerprint>$b adds a new fingerprint, and
273
+$bCERT DEL <fingerprint>$b removes a fingerprint. If you're an IRC operator
274
+with the correct permissions, you can act on another user's account, for
275
+example with $bCERT ADD <account> <fingerprint>$b.`,
276
+			helpShort: `$bCERT$b controls a user account's certificate fingerprints`,
277
+			enabled:   servCmdRequiresAuthEnabled,
278
+			minParams: 1,
279
+		},
262 280
 	}
263 281
 )
264 282
 
@@ -548,6 +566,11 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
548 566
 }
549 567
 
550 568
 func nsInfoHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
569
+	if !server.Config().Accounts.AuthenticationEnabled && !client.HasRoleCapabs("accreg") {
570
+		nsNotice(rb, client.t("This command has been disabled by the server administrators"))
571
+		return
572
+	}
573
+
551 574
 	var accountName string
552 575
 	if len(params) > 0 {
553 576
 		nick := params[0]
@@ -659,6 +682,9 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []
659 682
 
660 683
 func nsSaregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
661 684
 	account, passphrase := params[0], params[1]
685
+	if passphrase == "*" {
686
+		passphrase = ""
687
+	}
662 688
 	err := server.accounts.Register(nil, account, "admin", "", passphrase, "")
663 689
 	if err == nil {
664 690
 		err = server.accounts.Verify(nil, account, "")
@@ -753,30 +779,40 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
753 779
 	var errorMessage string
754 780
 
755 781
 	hasPrivs := client.HasRoleCapabs("accreg")
756
-	if !hasPrivs && !nsLoginThrottleCheck(client, rb) {
757
-		return
758
-	}
759 782
 
760 783
 	switch len(params) {
761 784
 	case 2:
762 785
 		if !hasPrivs {
763
-			errorMessage = "Insufficient privileges"
786
+			errorMessage = `Insufficient privileges`
764 787
 		} else {
765 788
 			target, newPassword = params[0], params[1]
789
+			if newPassword == "*" {
790
+				newPassword = ""
791
+			}
766 792
 		}
767 793
 	case 3:
768 794
 		target = client.Account()
769 795
 		if target == "" {
770
-			errorMessage = "You're not logged into an account"
796
+			errorMessage = `You're not logged into an account`
771 797
 		} else if params[1] != params[2] {
772
-			errorMessage = "Passwords do not match"
798
+			errorMessage = `Passwords do not match`
773 799
 		} else {
774
-			// check that they correctly supplied the preexisting password
775
-			_, err := server.accounts.checkPassphrase(target, params[0])
800
+			if !nsLoginThrottleCheck(client, rb) {
801
+				return
802
+			}
803
+			accountData, err := server.accounts.LoadAccount(target)
776 804
 			if err != nil {
777
-				errorMessage = "Password incorrect"
805
+				errorMessage = `You're not logged into an account`
778 806
 			} else {
779
-				newPassword = params[1]
807
+				hash := accountData.Credentials.PassphraseHash
808
+				if hash != nil && passwd.CompareHashAndPassword(hash, []byte(params[0])) != nil {
809
+					errorMessage = `Password incorrect`
810
+				} else {
811
+					newPassword = params[1]
812
+					if newPassword == "*" {
813
+						newPassword = ""
814
+					}
815
+				}
780 816
 			}
781 817
 		}
782 818
 	default:
@@ -788,10 +824,15 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
788 824
 		return
789 825
 	}
790 826
 
791
-	err := server.accounts.setPassword(target, newPassword)
792
-	if err == nil {
827
+	err := server.accounts.setPassword(target, newPassword, hasPrivs)
828
+	switch err {
829
+	case nil:
793 830
 		nsNotice(rb, client.t("Password changed"))
794
-	} else {
831
+	case errEmptyCredentials:
832
+		nsNotice(rb, client.t("You can't delete your password unless you add a certificate fingerprint"))
833
+	case errCASFailed:
834
+		nsNotice(rb, client.t("Try again later"))
835
+	default:
795 836
 		server.logger.Error("internal", "could not upgrade user password:", err.Error())
796 837
 		nsNotice(rb, client.t("Password could not be changed due to server error"))
797 838
 	}
@@ -837,3 +878,93 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
837 878
 		nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(time.RFC1123)))
838 879
 	}
839 880
 }
881
+
882
+func nsCertHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
883
+	verb := strings.ToLower(params[0])
884
+	params = params[1:]
885
+	var target, certfp string
886
+
887
+	switch verb {
888
+	case "list":
889
+		if 1 <= len(params) {
890
+			target = params[0]
891
+		}
892
+	case "add", "del":
893
+		if 2 <= len(params) {
894
+			target, certfp = params[0], params[1]
895
+		} else if len(params) == 1 {
896
+			certfp = params[0]
897
+		} else {
898
+			nsNotice(rb, client.t("Invalid parameters"))
899
+			return
900
+		}
901
+	default:
902
+		nsNotice(rb, client.t("Invalid parameters"))
903
+		return
904
+	}
905
+
906
+	hasPrivs := client.HasRoleCapabs("accreg")
907
+	if target != "" && !hasPrivs {
908
+		nsNotice(rb, client.t("Insufficient privileges"))
909
+		return
910
+	} else if target == "" {
911
+		target = client.Account()
912
+		if target == "" {
913
+			nsNotice(rb, client.t("You're not logged into an account"))
914
+			return
915
+		}
916
+	}
917
+
918
+	var err error
919
+	switch verb {
920
+	case "list":
921
+		accountData, err := server.accounts.LoadAccount(target)
922
+		if err == errAccountDoesNotExist {
923
+			nsNotice(rb, client.t("Account does not exist"))
924
+			return
925
+		} else if err != nil {
926
+			nsNotice(rb, client.t("An error occurred"))
927
+			return
928
+		}
929
+		certfps := accountData.Credentials.Certfps
930
+		nsNotice(rb, fmt.Sprintf(client.t("There are %d certificate fingerprint(s) authorized for account %s."), len(certfps), accountData.Name))
931
+		for i, certfp := range certfps {
932
+			nsNotice(rb, fmt.Sprintf("%d: %s", i+1, certfp))
933
+		}
934
+		return
935
+	case "add":
936
+		err = server.accounts.addRemoveCertfp(target, certfp, true, hasPrivs)
937
+	case "del":
938
+		err = server.accounts.addRemoveCertfp(target, certfp, false, hasPrivs)
939
+	}
940
+
941
+	switch err {
942
+	case nil:
943
+		if verb == "add" {
944
+			nsNotice(rb, client.t("Certificate fingerprint successfully added"))
945
+		} else {
946
+			nsNotice(rb, client.t("Certificate fingerprint successfully removed"))
947
+		}
948
+	case errNoop:
949
+		if verb == "add" {
950
+			nsNotice(rb, client.t("That certificate fingerprint was already authorized"))
951
+		} else {
952
+			nsNotice(rb, client.t("Certificate fingerprint not found"))
953
+		}
954
+	case errAccountDoesNotExist:
955
+		nsNotice(rb, client.t("Account does not exist"))
956
+	case errLimitExceeded:
957
+		nsNotice(rb, client.t("You already have too many certificate fingerprints"))
958
+	case utils.ErrInvalidCertfp:
959
+		nsNotice(rb, client.t("Invalid certificate fingerprint"))
960
+	case errCertfpAlreadyExists:
961
+		nsNotice(rb, client.t("That certificate fingerprint is already associated with another account"))
962
+	case errEmptyCredentials:
963
+		nsNotice(rb, client.t("You can't remove all your certificate fingerprints unless you add a password"))
964
+	case errCASFailed:
965
+		nsNotice(rb, client.t("Try again later"))
966
+	default:
967
+		server.logger.Error("internal", "could not modify certificates:", err.Error())
968
+		nsNotice(rb, client.t("An error occurred"))
969
+	}
970
+}

+ 11
- 9
irc/utils/crypto.go Просмотреть файл

@@ -8,12 +8,16 @@ import (
8 8
 	"crypto/subtle"
9 9
 	"encoding/base32"
10 10
 	"encoding/base64"
11
+	"encoding/hex"
12
+	"errors"
11 13
 	"strings"
12 14
 )
13 15
 
14 16
 var (
15 17
 	// slingamn's own private b32 alphabet, removing 1, l, o, and 0
16 18
 	B32Encoder = base32.NewEncoding("abcdefghijkmnpqrstuvwxyz23456789").WithPadding(base32.NoPadding)
19
+
20
+	ErrInvalidCertfp = errors.New("Invalid certfp")
17 21
 )
18 22
 
19 23
 const (
@@ -70,14 +74,12 @@ func GenerateSecretKey() string {
70 74
 	return base64.RawURLEncoding.EncodeToString(buf[:])
71 75
 }
72 76
 
73
-func normalizeCertfp(certfp string) string {
74
-	return strings.ToLower(strings.Replace(certfp, ":", "", -1))
75
-}
76
-
77
-// Convenience to compare certfps as returned by different tools, e.g., openssl vs. oragono
78
-func CertfpsMatch(storedCertfp, suppliedCertfp string) bool {
79
-	if storedCertfp == "" {
80
-		return false
77
+// Normalize openssl-formatted certfp's to oragono's format
78
+func NormalizeCertfp(certfp string) (result string, err error) {
79
+	result = strings.ToLower(strings.Replace(certfp, ":", "", -1))
80
+	decoded, err := hex.DecodeString(result)
81
+	if err != nil || len(decoded) != 32 {
82
+		return "", ErrInvalidCertfp
81 83
 	}
82
-	return normalizeCertfp(storedCertfp) == normalizeCertfp(suppliedCertfp)
84
+	return
83 85
 }

+ 17
- 11
irc/utils/crypto_test.go Просмотреть файл

@@ -85,17 +85,23 @@ func BenchmarkMungeSecretToken(b *testing.B) {
85 85
 func TestCertfpComparisons(t *testing.T) {
86 86
 	opensslFP := "3D:6B:11:BF:B4:05:C3:F8:4B:38:CD:30:38:FB:EC:01:71:D5:03:54:79:04:07:88:4C:A5:5D:23:41:85:66:C9"
87 87
 	oragonoFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c9"
88
-	badFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c8"
89
-	if !CertfpsMatch(opensslFP, oragonoFP) {
90
-		t.Error("these certs should match")
91
-	}
92
-	if !CertfpsMatch(oragonoFP, opensslFP) {
93
-		t.Error("these certs should match")
94
-	}
95
-	if CertfpsMatch("", "") {
96
-		t.Error("empty stored certfp should not match empty provided certfp")
88
+	badFP := "3d6b11bfb405c3f84b38cd3038fbec0171d50354790407884ca55d23418566c"
89
+	badFP2 := "*"
90
+
91
+	normalizedOpenssl, err := NormalizeCertfp(opensslFP)
92
+	assertEqual(err, nil, t)
93
+	assertEqual(normalizedOpenssl, oragonoFP, t)
94
+
95
+	normalizedOragono, err := NormalizeCertfp(oragonoFP)
96
+	assertEqual(err, nil, t)
97
+	assertEqual(normalizedOragono, oragonoFP, t)
98
+
99
+	_, err = NormalizeCertfp(badFP)
100
+	if err == nil {
101
+		t.Errorf("corrupt fp should fail normalization")
97 102
 	}
98
-	if CertfpsMatch(opensslFP, badFP) {
99
-		t.Error("these certs should not match")
103
+	_, err = NormalizeCertfp(badFP2)
104
+	if err == nil {
105
+		t.Errorf("corrupt fp should fail normalization")
100 106
 	}
101 107
 }

+ 3
- 3
oragono.yaml Просмотреть файл

@@ -244,6 +244,9 @@ server:
244 244
 
245 245
 # account options
246 246
 accounts:
247
+    # is account authentication enabled, i.e., can users log into existing accounts?
248
+    authentication-enabled: true
249
+
247 250
     # account registration
248 251
     registration:
249 252
         # can users register new accounts for themselves? if this is false, operators with
@@ -271,9 +274,6 @@ accounts:
271 274
         #         password: ""
272 275
         #         sender: "admin@my.network"
273 276
 
274
-    # is account authentication enabled?
275
-    authentication-enabled: true
276
-
277 277
     # throttle account login attempts (to prevent either password guessing, or DoS
278 278
     # attacks on the server aimed at forcing repeated expensive bcrypt computations)
279 279
     login-throttling:

Загрузка…
Отмена
Сохранить