Browse Source

Add email-based password reset (#1779)

* Add email-based password reset

Fixes #734

* rename SETPASS to RESETPASS

* review fixes

* abuse mitigations

* SENDPASS and RESETPASS should both touch the client login throttle
* Produce a logline and a sno on SENDPASS (since it actually sends an email)

* don't re-retrieve the settings value

* add email confirmation for NS SET EMAIL

* smtp: if require-tls is disabled, don't validate server cert

* review fixes

* remove cooldown for NS SET EMAIL

If you accidentally set the wrong address, the cooldown would prevent you
from fixing your mistake. Since we touch the registration throttle anyway,
this shouldn't present more of an abuse concern than registration itself.
tags/v2.8.0-rc1
Shivaram Lingamneni 2 years ago
parent
commit
8b2f6de3e0
No account linked to committer's email address
8 changed files with 525 additions and 58 deletions
  1. 7
    0
      default.yaml
  2. 244
    35
      irc/accounts.go
  3. 57
    1
      irc/database.go
  4. 21
    0
      irc/email/email.go
  5. 3
    1
      irc/import.go
  6. 178
    20
      irc/nickserv.go
  7. 8
    1
      irc/smtp/smtp.go
  8. 7
    0
      traditional.yaml

+ 7
- 0
default.yaml View File

414
             blacklist-regexes:
414
             blacklist-regexes:
415
             #    - ".*@mailinator.com"
415
             #    - ".*@mailinator.com"
416
             timeout: 60s
416
             timeout: 60s
417
+            # email-based password reset:
418
+            password-reset:
419
+                enabled: false
420
+                # time before we allow resending the email
421
+                cooldown: 1h
422
+                # time for which a password reset code is valid
423
+                timeout: 1d
417
 
424
 
418
     # throttle account login attempts (to prevent either password guessing, or DoS
425
     # throttle account login attempts (to prevent either password guessing, or DoS
419
     # attacks on the server aimed at forcing repeated expensive bcrypt computations)
426
     # attacks on the server aimed at forcing repeated expensive bcrypt computations)

+ 244
- 35
irc/accounts.go View File

4
 package irc
4
 package irc
5
 
5
 
6
 import (
6
 import (
7
-	"bytes"
8
 	"crypto/rand"
7
 	"crypto/rand"
9
 	"crypto/x509"
8
 	"crypto/x509"
10
 	"encoding/json"
9
 	"encoding/json"
32
 	keyAccountExists           = "account.exists %s"
31
 	keyAccountExists           = "account.exists %s"
33
 	keyAccountVerified         = "account.verified %s"
32
 	keyAccountVerified         = "account.verified %s"
34
 	keyAccountUnregistered     = "account.unregistered %s"
33
 	keyAccountUnregistered     = "account.unregistered %s"
35
-	keyAccountCallback         = "account.callback %s"
36
 	keyAccountVerificationCode = "account.verificationcode %s"
34
 	keyAccountVerificationCode = "account.verificationcode %s"
37
 	keyAccountName             = "account.name %s" // stores the 'preferred name' of the account, not casemapped
35
 	keyAccountName             = "account.name %s" // stores the 'preferred name' of the account, not casemapped
38
 	keyAccountRegTime          = "account.registered.time %s"
36
 	keyAccountRegTime          = "account.registered.time %s"
46
 	keyAccountModes            = "account.modes %s"     // user modes for the always-on client as a string
44
 	keyAccountModes            = "account.modes %s"     // user modes for the always-on client as a string
47
 	keyAccountRealname         = "account.realname %s"  // client realname stored as string
45
 	keyAccountRealname         = "account.realname %s"  // client realname stored as string
48
 	keyAccountSuspended        = "account.suspended %s" // client realname stored as string
46
 	keyAccountSuspended        = "account.suspended %s" // client realname stored as string
47
+	keyAccountPwReset          = "account.pwreset %s"
48
+	keyAccountEmailChange      = "account.emailchange %s"
49
 	// for an always-on client, a map of channel names they're in to their current modes
49
 	// for an always-on client, a map of channel names they're in to their current modes
50
 	// (not to be confused with their amodes, which a non-always-on client can have):
50
 	// (not to be confused with their amodes, which a non-always-on client can have):
51
 	keyAccountChannelToModes = "account.channeltomodes %s"
51
 	keyAccountChannelToModes = "account.channeltomodes %s"
391
 	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
391
 	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
392
 	unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
392
 	unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
393
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
393
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
394
-	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
395
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
394
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
396
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
395
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
397
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
396
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
397
+	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
398
 	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
398
 	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
399
 
399
 
400
 	var creds AccountCredentials
400
 	var creds AccountCredentials
409
 		return err
409
 		return err
410
 	}
410
 	}
411
 
411
 
412
+	var settingsStr string
413
+	if callbackNamespace == "mailto" {
414
+		settings := AccountSettings{Email: callbackValue}
415
+		j, err := json.Marshal(settings)
416
+		if err == nil {
417
+			settingsStr = string(j)
418
+		}
419
+	}
420
+
412
 	registeredTimeStr := strconv.FormatInt(time.Now().UnixNano(), 10)
421
 	registeredTimeStr := strconv.FormatInt(time.Now().UnixNano(), 10)
413
-	callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
414
 
422
 
415
 	var setOptions *buntdb.SetOptions
423
 	var setOptions *buntdb.SetOptions
416
 	ttl := time.Duration(config.Accounts.Registration.VerifyTimeout)
424
 	ttl := time.Duration(config.Accounts.Registration.VerifyTimeout)
449
 			tx.Set(accountNameKey, account, setOptions)
457
 			tx.Set(accountNameKey, account, setOptions)
450
 			tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
458
 			tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
451
 			tx.Set(credentialsKey, credStr, setOptions)
459
 			tx.Set(credentialsKey, credStr, setOptions)
452
-			tx.Set(callbackKey, callbackSpec, setOptions)
460
+			tx.Set(settingsKey, settingsStr, setOptions)
453
 			if certfp != "" {
461
 			if certfp != "" {
454
 				tx.Set(certFPKey, casefoldedAccount, setOptions)
462
 				tx.Set(certFPKey, casefoldedAccount, setOptions)
455
 			}
463
 			}
782
 		subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
790
 		subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
783
 	}
791
 	}
784
 
792
 
785
-	var message bytes.Buffer
786
-	fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
787
-	fmt.Fprintf(&message, "To: %s\r\n", callbackValue)
788
-	if config.DKIM.Domain != "" {
789
-		fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), config.DKIM.Domain)
790
-	}
791
-	fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
792
-	fmt.Fprintf(&message, "Subject: %s\r\n", subject)
793
-	message.WriteString("\r\n") // blank line: end headers, begin message body
793
+	message := email.ComposeMail(config, callbackValue, subject)
794
 	fmt.Fprintf(&message, client.t("Account: %s"), account)
794
 	fmt.Fprintf(&message, client.t("Account: %s"), account)
795
 	message.WriteString("\r\n")
795
 	message.WriteString("\r\n")
796
 	fmt.Fprintf(&message, client.t("Verification code: %s"), code)
796
 	fmt.Fprintf(&message, client.t("Verification code: %s"), code)
823
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
823
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
824
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
824
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
825
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
825
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
826
-	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
827
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
826
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
827
+	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
828
 
828
 
829
 	var raw rawClientAccount
829
 	var raw rawClientAccount
830
 
830
 
892
 			tx.Set(accountKey, "1", nil)
892
 			tx.Set(accountKey, "1", nil)
893
 			tx.Set(accountNameKey, raw.Name, nil)
893
 			tx.Set(accountNameKey, raw.Name, nil)
894
 			tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
894
 			tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
895
-			tx.Set(callbackKey, raw.Callback, nil)
896
 			tx.Set(credentialsKey, raw.Credentials, nil)
895
 			tx.Set(credentialsKey, raw.Credentials, nil)
896
+			tx.Set(settingsKey, raw.Settings, nil)
897
 
897
 
898
 			var creds AccountCredentials
898
 			var creds AccountCredentials
899
 			// XXX we shouldn't do (de)serialization inside the txn,
899
 			// XXX we shouldn't do (de)serialization inside the txn,
955
 	return
955
 	return
956
 }
956
 }
957
 
957
 
958
+type EmailChangeRecord struct {
959
+	TimeCreated time.Time
960
+	Code        string
961
+	Email       string
962
+}
963
+
964
+func (am *AccountManager) NsSetEmail(client *Client, emailAddr string) (err error) {
965
+	casefoldedAccount := client.Account()
966
+	if casefoldedAccount == "" {
967
+		return errAccountNotLoggedIn
968
+	}
969
+
970
+	if am.touchRegisterThrottle() {
971
+		am.server.logger.Warning("accounts", "global registration throttle exceeded by client changing email", client.Nick())
972
+		return errLimitExceeded
973
+	}
974
+
975
+	config := am.server.Config()
976
+	if !config.Accounts.Registration.EmailVerification.Enabled {
977
+		return errFeatureDisabled // redundant check, just in case
978
+	}
979
+	record := EmailChangeRecord{
980
+		TimeCreated: time.Now().UTC(),
981
+		Code:        utils.GenerateSecretToken(),
982
+		Email:       emailAddr,
983
+	}
984
+	recordKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
985
+	recordBytes, _ := json.Marshal(record)
986
+	recordVal := string(recordBytes)
987
+	am.server.store.Update(func(tx *buntdb.Tx) error {
988
+		tx.Set(recordKey, recordVal, nil)
989
+		return nil
990
+	})
991
+
992
+	if err != nil {
993
+		return err
994
+	}
995
+
996
+	message := email.ComposeMail(config.Accounts.Registration.EmailVerification,
997
+		emailAddr,
998
+		fmt.Sprintf(client.t("Verify your change of e-mail address on %s"), am.server.name))
999
+	message.WriteString(fmt.Sprintf(client.t("To confirm your change of e-mail address on %s, issue the following command:"), am.server.name))
1000
+	message.WriteString("\r\n")
1001
+	fmt.Fprintf(&message, "/MSG NickServ VERIFYEMAIL %s\r\n", record.Code)
1002
+
1003
+	err = email.SendMail(config.Accounts.Registration.EmailVerification, emailAddr, message.Bytes())
1004
+	if err == nil {
1005
+		am.server.logger.Info("services",
1006
+			fmt.Sprintf("email change verification sent for account %s", casefoldedAccount))
1007
+		return
1008
+	} else {
1009
+		am.server.logger.Error("internal", "Failed to dispatch e-mail change verification to", emailAddr, err.Error())
1010
+		return &registrationCallbackError{err}
1011
+	}
1012
+}
1013
+
1014
+func (am *AccountManager) NsVerifyEmail(client *Client, code string) (err error) {
1015
+	casefoldedAccount := client.Account()
1016
+	if casefoldedAccount == "" {
1017
+		return errAccountNotLoggedIn
1018
+	}
1019
+
1020
+	var record EmailChangeRecord
1021
+	success := false
1022
+	key := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
1023
+	ttl := time.Duration(am.server.Config().Accounts.Registration.VerifyTimeout)
1024
+	am.server.store.Update(func(tx *buntdb.Tx) error {
1025
+		rawStr, err := tx.Get(key)
1026
+		if err == nil && rawStr != "" {
1027
+			err := json.Unmarshal([]byte(rawStr), &record)
1028
+			if err == nil {
1029
+				if (ttl == 0 || time.Since(record.TimeCreated) < ttl) && utils.SecretTokensMatch(record.Code, code) {
1030
+					success = true
1031
+					tx.Delete(key)
1032
+				}
1033
+			}
1034
+		}
1035
+		return nil
1036
+	})
1037
+
1038
+	if !success {
1039
+		return errAccountVerificationInvalidCode
1040
+	}
1041
+
1042
+	munger := func(in AccountSettings) (out AccountSettings, err error) {
1043
+		out = in
1044
+		out.Email = record.Email
1045
+		return
1046
+	}
1047
+
1048
+	_, err = am.ModifyAccountSettings(casefoldedAccount, munger)
1049
+	return
1050
+}
1051
+
1052
+func (am *AccountManager) NsSendpass(client *Client, accountName string) (err error) {
1053
+	config := am.server.Config()
1054
+	if !(config.Accounts.Registration.EmailVerification.Enabled && config.Accounts.Registration.EmailVerification.PasswordReset.Enabled) {
1055
+		return errFeatureDisabled
1056
+	}
1057
+
1058
+	account, err := am.LoadAccount(accountName)
1059
+	if err != nil {
1060
+		return err
1061
+	}
1062
+	if !account.Verified {
1063
+		return errAccountUnverified
1064
+	}
1065
+	if account.Suspended != nil {
1066
+		return errAccountSuspended
1067
+	}
1068
+	if account.Settings.Email == "" {
1069
+		return errValidEmailRequired
1070
+	}
1071
+
1072
+	record := PasswordResetRecord{
1073
+		TimeCreated: time.Now().UTC(),
1074
+		Code:        utils.GenerateSecretToken(),
1075
+	}
1076
+	recordKey := fmt.Sprintf(keyAccountPwReset, account.NameCasefolded)
1077
+	recordBytes, _ := json.Marshal(record)
1078
+	recordVal := string(recordBytes)
1079
+
1080
+	am.server.store.Update(func(tx *buntdb.Tx) error {
1081
+		recStr, recErr := tx.Get(recordKey)
1082
+		if recErr == nil && recStr != "" {
1083
+			var existing PasswordResetRecord
1084
+			jErr := json.Unmarshal([]byte(recStr), &existing)
1085
+			cooldown := time.Duration(config.Accounts.Registration.EmailVerification.PasswordReset.Cooldown)
1086
+			if jErr == nil && time.Since(existing.TimeCreated) < cooldown {
1087
+				err = errLimitExceeded
1088
+				return nil
1089
+			}
1090
+		}
1091
+		tx.Set(recordKey, recordVal, &buntdb.SetOptions{
1092
+			Expires: true,
1093
+			TTL:     time.Duration(config.Accounts.Registration.EmailVerification.PasswordReset.Timeout),
1094
+		})
1095
+		return nil
1096
+	})
1097
+
1098
+	if err != nil {
1099
+		return
1100
+	}
1101
+
1102
+	subject := fmt.Sprintf(client.t("Reset your password on %s"), am.server.name)
1103
+	message := email.ComposeMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, subject)
1104
+	fmt.Fprintf(&message, client.t("We received a request to reset your password on %s for account: %s"), am.server.name, account.Name)
1105
+	message.WriteString("\r\n")
1106
+	fmt.Fprintf(&message, client.t("If you did not initiate this request, you can safely ignore this message."))
1107
+	message.WriteString("\r\n")
1108
+	message.WriteString("\r\n")
1109
+	message.WriteString(client.t("Otherwise, to reset your password, issue the following command (replace `new_password` with your desired password):"))
1110
+	message.WriteString("\r\n")
1111
+	fmt.Fprintf(&message, "/MSG NickServ RESETPASS %s %s new_password\r\n", account.Name, record.Code)
1112
+
1113
+	err = email.SendMail(config.Accounts.Registration.EmailVerification, account.Settings.Email, message.Bytes())
1114
+	if err == nil {
1115
+		am.server.logger.Info("services",
1116
+			fmt.Sprintf("client %s sent a password reset email for account %s", client.Nick(), account.Name))
1117
+	} else {
1118
+		am.server.logger.Error("internal", "Failed to dispatch e-mail to", account.Settings.Email, err.Error())
1119
+	}
1120
+	return
1121
+
1122
+}
1123
+
1124
+func (am *AccountManager) NsResetpass(client *Client, accountName, code, password string) (err error) {
1125
+	if validatePassphrase(password) != nil {
1126
+		return errAccountBadPassphrase
1127
+	}
1128
+	account, err := am.LoadAccount(accountName)
1129
+	if err != nil {
1130
+		return
1131
+	}
1132
+	if !account.Verified {
1133
+		return errAccountUnverified
1134
+	}
1135
+	if account.Suspended != nil {
1136
+		return errAccountSuspended
1137
+	}
1138
+
1139
+	success := false
1140
+	key := fmt.Sprintf(keyAccountPwReset, account.NameCasefolded)
1141
+	am.server.store.Update(func(tx *buntdb.Tx) error {
1142
+		rawStr, err := tx.Get(key)
1143
+		if err == nil && rawStr != "" {
1144
+			var record PasswordResetRecord
1145
+			err := json.Unmarshal([]byte(rawStr), &record)
1146
+			if err == nil && utils.SecretTokensMatch(record.Code, code) {
1147
+				success = true
1148
+				tx.Delete(key)
1149
+			}
1150
+		}
1151
+		return nil
1152
+	})
1153
+
1154
+	if success {
1155
+		return am.setPassword(accountName, password, true)
1156
+	} else {
1157
+		return errAccountInvalidCredentials
1158
+	}
1159
+}
1160
+
1161
+type PasswordResetRecord struct {
1162
+	TimeCreated time.Time
1163
+	Code        string
1164
+}
1165
+
958
 func marshalReservedNicks(nicks []string) string {
1166
 func marshalReservedNicks(nicks []string) string {
959
 	return strings.Join(nicks, ",")
1167
 	return strings.Join(nicks, ",")
960
 }
1168
 }
1294
 		return
1502
 		return
1295
 	}
1503
 	}
1296
 	result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
1504
 	result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
1297
-	if strings.HasPrefix(raw.Callback, "mailto:") {
1298
-		result.Email = strings.TrimPrefix(raw.Callback, "mailto:")
1299
-	}
1300
 	result.Verified = raw.Verified
1505
 	result.Verified = raw.Verified
1301
 	if raw.VHost != "" {
1506
 	if raw.VHost != "" {
1302
 		e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
1507
 		e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
1329
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
1534
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
1330
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
1535
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
1331
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
1536
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
1332
-	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
1333
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
1537
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
1334
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
1538
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
1335
 	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
1539
 	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
1344
 	result.Name, _ = tx.Get(accountNameKey)
1548
 	result.Name, _ = tx.Get(accountNameKey)
1345
 	result.RegisteredAt, _ = tx.Get(registeredTimeKey)
1549
 	result.RegisteredAt, _ = tx.Get(registeredTimeKey)
1346
 	result.Credentials, _ = tx.Get(credentialsKey)
1550
 	result.Credentials, _ = tx.Get(credentialsKey)
1347
-	result.Callback, _ = tx.Get(callbackKey)
1348
 	result.AdditionalNicks, _ = tx.Get(nicksKey)
1551
 	result.AdditionalNicks, _ = tx.Get(nicksKey)
1349
 	result.VHost, _ = tx.Get(vhostKey)
1552
 	result.VHost, _ = tx.Get(vhostKey)
1350
 	result.Settings, _ = tx.Get(settingsKey)
1553
 	result.Settings, _ = tx.Get(settingsKey)
1524
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
1727
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
1525
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
1728
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
1526
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
1729
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
1527
-	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
1528
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
1730
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
1529
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
1731
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
1530
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
1732
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
1537
 	modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
1739
 	modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
1538
 	realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
1740
 	realnameKey := fmt.Sprintf(keyAccountRealname, casefoldedAccount)
1539
 	suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
1741
 	suspendedKey := fmt.Sprintf(keyAccountSuspended, casefoldedAccount)
1742
+	pwResetKey := fmt.Sprintf(keyAccountPwReset, casefoldedAccount)
1743
+	emailChangeKey := fmt.Sprintf(keyAccountEmailChange, casefoldedAccount)
1540
 
1744
 
1541
 	var clients []*Client
1745
 	var clients []*Client
1542
 	defer func() {
1746
 	defer func() {
1582
 		tx.Delete(accountNameKey)
1786
 		tx.Delete(accountNameKey)
1583
 		tx.Delete(verifiedKey)
1787
 		tx.Delete(verifiedKey)
1584
 		tx.Delete(registeredTimeKey)
1788
 		tx.Delete(registeredTimeKey)
1585
-		tx.Delete(callbackKey)
1586
 		tx.Delete(verificationCodeKey)
1789
 		tx.Delete(verificationCodeKey)
1587
 		tx.Delete(settingsKey)
1790
 		tx.Delete(settingsKey)
1588
 		rawNicks, _ = tx.Get(nicksKey)
1791
 		rawNicks, _ = tx.Get(nicksKey)
1597
 		tx.Delete(modesKey)
1800
 		tx.Delete(modesKey)
1598
 		tx.Delete(realnameKey)
1801
 		tx.Delete(realnameKey)
1599
 		tx.Delete(suspendedKey)
1802
 		tx.Delete(suspendedKey)
1803
+		tx.Delete(pwResetKey)
1804
+		tx.Delete(emailChangeKey)
1600
 
1805
 
1601
 		return nil
1806
 		return nil
1602
 	})
1807
 	})
1940
 	CredentialsAnope  = -2
2145
 	CredentialsAnope  = -2
1941
 )
2146
 )
1942
 
2147
 
2148
+type SCRAMCreds struct {
2149
+	Salt      []byte
2150
+	Iters     int
2151
+	StoredKey []byte
2152
+	ServerKey []byte
2153
+}
2154
+
1943
 // AccountCredentials stores the various methods for verifying accounts.
2155
 // AccountCredentials stores the various methods for verifying accounts.
1944
 type AccountCredentials struct {
2156
 type AccountCredentials struct {
1945
 	Version        CredentialsVersion
2157
 	Version        CredentialsVersion
1946
 	PassphraseHash []byte
2158
 	PassphraseHash []byte
1947
 	Certfps        []string
2159
 	Certfps        []string
1948
-	SCRAMCreds     struct {
1949
-		Salt      []byte
1950
-		Iters     int
1951
-		StoredKey []byte
1952
-		ServerKey []byte
1953
-	}
2160
+	SCRAMCreds
1954
 }
2161
 }
1955
 
2162
 
1956
 func (ac *AccountCredentials) Empty() bool {
2163
 func (ac *AccountCredentials) Empty() bool {
1970
 func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint) (err error) {
2177
 func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint) (err error) {
1971
 	if passphrase == "" {
2178
 	if passphrase == "" {
1972
 		ac.PassphraseHash = nil
2179
 		ac.PassphraseHash = nil
2180
+		ac.SCRAMCreds = SCRAMCreds{}
1973
 		return nil
2181
 		return nil
1974
 	}
2182
 	}
1975
 
2183
 
1994
 	// xdg-go/scram says: "Clients have a default minimum PBKDF2 iteration count of 4096."
2202
 	// xdg-go/scram says: "Clients have a default minimum PBKDF2 iteration count of 4096."
1995
 	minIters := 4096
2203
 	minIters := 4096
1996
 	scramCreds := scramClient.GetStoredCredentials(scram.KeyFactors{Salt: string(salt), Iters: minIters})
2204
 	scramCreds := scramClient.GetStoredCredentials(scram.KeyFactors{Salt: string(salt), Iters: minIters})
1997
-	ac.SCRAMCreds.Salt = salt
1998
-	ac.SCRAMCreds.Iters = minIters
1999
-	ac.SCRAMCreds.StoredKey = scramCreds.StoredKey
2000
-	ac.SCRAMCreds.ServerKey = scramCreds.ServerKey
2205
+	ac.SCRAMCreds = SCRAMCreds{
2206
+		Salt:      salt,
2207
+		Iters:     minIters,
2208
+		StoredKey: scramCreds.StoredKey,
2209
+		ServerKey: scramCreds.ServerKey,
2210
+	}
2001
 
2211
 
2002
 	return nil
2212
 	return nil
2003
 }
2213
 }
2112
 	AutoreplayMissed bool
2322
 	AutoreplayMissed bool
2113
 	DMHistory        HistoryStatus
2323
 	DMHistory        HistoryStatus
2114
 	AutoAway         PersistentStatus
2324
 	AutoAway         PersistentStatus
2325
+	Email            string
2115
 }
2326
 }
2116
 
2327
 
2117
 // ClientAccount represents a user account.
2328
 // ClientAccount represents a user account.
2120
 	Name            string
2331
 	Name            string
2121
 	NameCasefolded  string
2332
 	NameCasefolded  string
2122
 	RegisteredAt    time.Time
2333
 	RegisteredAt    time.Time
2123
-	Email           string
2124
 	Credentials     AccountCredentials
2334
 	Credentials     AccountCredentials
2125
 	Verified        bool
2335
 	Verified        bool
2126
 	Suspended       *AccountSuspension
2336
 	Suspended       *AccountSuspension
2134
 	Name            string
2344
 	Name            string
2135
 	RegisteredAt    string
2345
 	RegisteredAt    string
2136
 	Credentials     string
2346
 	Credentials     string
2137
-	Callback        string
2138
 	Verified        bool
2347
 	Verified        bool
2139
 	AdditionalNicks string
2348
 	AdditionalNicks string
2140
 	VHost           string
2349
 	VHost           string

+ 57
- 1
irc/database.go View File

24
 	// 'version' of the database schema
24
 	// 'version' of the database schema
25
 	keySchemaVersion = "db.version"
25
 	keySchemaVersion = "db.version"
26
 	// latest schema of the db
26
 	// latest schema of the db
27
-	latestDbSchema = 20
27
+	latestDbSchema = 21
28
 
28
 
29
 	keyCloakSecret = "crypto.cloak_secret"
29
 	keyCloakSecret = "crypto.cloak_secret"
30
 )
30
 )
1008
 	return nil
1008
 	return nil
1009
 }
1009
 }
1010
 
1010
 
1011
+// #734: move the email address into the settings object,
1012
+// giving people a way to change it
1013
+func schemaChangeV20To21(config *Config, tx *buntdb.Tx) error {
1014
+	type accountSettingsv21 struct {
1015
+		AutoreplayLines  *int
1016
+		NickEnforcement  NickEnforcementMethod
1017
+		AllowBouncer     MulticlientAllowedSetting
1018
+		ReplayJoins      ReplayJoinsSetting
1019
+		AlwaysOn         PersistentStatus
1020
+		AutoreplayMissed bool
1021
+		DMHistory        HistoryStatus
1022
+		AutoAway         PersistentStatus
1023
+		Email            string
1024
+	}
1025
+	var accounts []string
1026
+	var emails []string
1027
+	callbackPrefix := "account.callback "
1028
+	tx.AscendGreaterOrEqual("", callbackPrefix, func(key, value string) bool {
1029
+		if !strings.HasPrefix(key, callbackPrefix) {
1030
+			return false
1031
+		}
1032
+		account := strings.TrimPrefix(key, callbackPrefix)
1033
+		if _, err := tx.Get("account.verified " + account); err != nil {
1034
+			return true
1035
+		}
1036
+		if strings.HasPrefix(value, "mailto:") {
1037
+			accounts = append(accounts, account)
1038
+			emails = append(emails, strings.TrimPrefix(value, "mailto:"))
1039
+		}
1040
+		return true
1041
+	})
1042
+	for i, account := range accounts {
1043
+		var settings accountSettingsv21
1044
+		email := emails[i]
1045
+		settingsKey := "account.settings " + account
1046
+		settingsStr, err := tx.Get(settingsKey)
1047
+		if err == nil && settingsStr != "" {
1048
+			json.Unmarshal([]byte(settingsStr), &settings)
1049
+		}
1050
+		settings.Email = email
1051
+		settingsBytes, err := json.Marshal(settings)
1052
+		if err != nil {
1053
+			log.Printf("couldn't marshal settings for %s: %v\n", account, err)
1054
+		} else {
1055
+			tx.Set(settingsKey, string(settingsBytes), nil)
1056
+		}
1057
+		tx.Delete(callbackPrefix + account)
1058
+	}
1059
+	return nil
1060
+}
1061
+
1011
 func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
1062
 func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
1012
 	for _, change := range allChanges {
1063
 	for _, change := range allChanges {
1013
 		if initialVersion == change.InitialVersion {
1064
 		if initialVersion == change.InitialVersion {
1113
 		TargetVersion:  20,
1164
 		TargetVersion:  20,
1114
 		Changer:        schemaChangeV19To20,
1165
 		Changer:        schemaChangeV19To20,
1115
 	},
1166
 	},
1167
+	{
1168
+		InitialVersion: 20,
1169
+		TargetVersion:  21,
1170
+		Changer:        schemaChangeV20To21,
1171
+	},
1116
 }
1172
 }

+ 21
- 0
irc/email/email.go View File

4
 package email
4
 package email
5
 
5
 
6
 import (
6
 import (
7
+	"bytes"
7
 	"errors"
8
 	"errors"
8
 	"fmt"
9
 	"fmt"
9
 	"net"
10
 	"net"
11
 	"strings"
12
 	"strings"
12
 	"time"
13
 	"time"
13
 
14
 
15
+	"github.com/ergochat/ergo/irc/custime"
14
 	"github.com/ergochat/ergo/irc/smtp"
16
 	"github.com/ergochat/ergo/irc/smtp"
17
+	"github.com/ergochat/ergo/irc/utils"
15
 )
18
 )
16
 
19
 
17
 var (
20
 var (
42
 	BlacklistRegexes     []string  `yaml:"blacklist-regexes"`
45
 	BlacklistRegexes     []string  `yaml:"blacklist-regexes"`
43
 	blacklistRegexes     []*regexp.Regexp
46
 	blacklistRegexes     []*regexp.Regexp
44
 	Timeout              time.Duration
47
 	Timeout              time.Duration
48
+	PasswordReset        struct {
49
+		Enabled  bool
50
+		Cooldown custime.Duration
51
+		Timeout  custime.Duration
52
+	} `yaml:"password-reset"`
45
 }
53
 }
46
 
54
 
47
 func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
55
 func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
95
 	return
103
 	return
96
 }
104
 }
97
 
105
 
106
+func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.Buffer) {
107
+	fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
108
+	fmt.Fprintf(&message, "To: %s\r\n", recipient)
109
+	dkimDomain := config.DKIM.Domain
110
+	if dkimDomain != "" {
111
+		fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
112
+	}
113
+	fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
114
+	fmt.Fprintf(&message, "Subject: %s\r\n", subject)
115
+	message.WriteString("\r\n") // blank line: end headers, begin message body
116
+	return message
117
+}
118
+
98
 func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
119
 func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
99
 	for _, reg := range config.blacklistRegexes {
120
 	for _, reg := range config.blacklistRegexes {
100
 		if reg.MatchString(recipient) {
121
 		if reg.MatchString(recipient) {

+ 3
- 1
irc/import.go View File

121
 		tx.Set(fmt.Sprintf(keyAccountExists, cfUsername), "1", nil)
121
 		tx.Set(fmt.Sprintf(keyAccountExists, cfUsername), "1", nil)
122
 		tx.Set(fmt.Sprintf(keyAccountVerified, cfUsername), "1", nil)
122
 		tx.Set(fmt.Sprintf(keyAccountVerified, cfUsername), "1", nil)
123
 		tx.Set(fmt.Sprintf(keyAccountName, cfUsername), userInfo.Name, nil)
123
 		tx.Set(fmt.Sprintf(keyAccountName, cfUsername), userInfo.Name, nil)
124
-		tx.Set(fmt.Sprintf(keyAccountCallback, cfUsername), "mailto:"+userInfo.Email, nil)
124
+		settings := AccountSettings{Email: userInfo.Email}
125
+		settingsBytes, _ := json.Marshal(settings)
126
+		tx.Set(fmt.Sprintf(keyAccountSettings, cfUsername), string(settingsBytes), nil)
125
 		tx.Set(fmt.Sprintf(keyAccountCredentials, cfUsername), string(marshaledCredentials), nil)
127
 		tx.Set(fmt.Sprintf(keyAccountCredentials, cfUsername), string(marshaledCredentials), nil)
126
 		tx.Set(fmt.Sprintf(keyAccountRegTime, cfUsername), strconv.FormatInt(userInfo.RegisteredAt, 10), nil)
128
 		tx.Set(fmt.Sprintf(keyAccountRegTime, cfUsername), strconv.FormatInt(userInfo.RegisteredAt, 10), nil)
127
 		if userInfo.Vhost != "" {
129
 		if userInfo.Vhost != "" {

+ 178
- 20
irc/nickserv.go View File

36
 	return config.Accounts.Multiclient.Enabled
36
 	return config.Accounts.Multiclient.Enabled
37
 }
37
 }
38
 
38
 
39
+func servCmdRequiresEmailReset(config *Config) bool {
40
+	return config.Accounts.Registration.EmailVerification.Enabled &&
41
+		config.Accounts.Registration.EmailVerification.PasswordReset.Enabled
42
+}
43
+
39
 const nickservHelp = `NickServ lets you register, log in to, and manage an account.`
44
 const nickservHelp = `NickServ lets you register, log in to, and manage an account.`
40
 
45
 
41
 var (
46
 var (
302
 'auto-away' is only effective for always-on clients. If enabled, you will
307
 'auto-away' is only effective for always-on clients. If enabled, you will
303
 automatically be marked away when all your sessions are disconnected, and
308
 automatically be marked away when all your sessions are disconnected, and
304
 automatically return from away when you connect again.`,
309
 automatically return from away when you connect again.`,
310
+				`$bEMAIL$b
311
+'email' controls the e-mail address associated with your account (if the
312
+server operator allows it, this address can be used for password resets).
313
+As an additional security measure, if you have a password set, you must
314
+provide it as an additional argument to $bSET$b, for example,
315
+SET EMAIL test@example.com hunter2`,
305
 			},
316
 			},
306
 			authRequired: true,
317
 			authRequired: true,
307
 			enabled:      servCmdRequiresAuthEnabled,
318
 			enabled:      servCmdRequiresAuthEnabled,
318
 			minParams: 3,
329
 			minParams: 3,
319
 			capabs:    []string{"accreg"},
330
 			capabs:    []string{"accreg"},
320
 		},
331
 		},
332
+		"sendpass": {
333
+			handler: nsSendpassHandler,
334
+			help: `Syntax: $bSENDPASS <account>$b
335
+
336
+SENDPASS sends a password reset email to the email address associated with
337
+the target account. The reset code in the email can then be used with the
338
+$bRESETPASS$b command.`,
339
+			helpShort: `$bSENDPASS$b initiates an email-based password reset`,
340
+			enabled:   servCmdRequiresEmailReset,
341
+			minParams: 1,
342
+		},
343
+		"resetpass": {
344
+			handler: nsResetpassHandler,
345
+			help: `Syntax: $bRESETPASS <account> <code> <password>$b
346
+
347
+RESETPASS resets an account password, using a reset code that was emailed as
348
+the result of a previous $bSENDPASS$b command.`,
349
+			helpShort: `$bRESETPASS$b completes an email-based password reset`,
350
+			enabled:   servCmdRequiresEmailReset,
351
+			minParams: 3,
352
+		},
321
 		"cert": {
353
 		"cert": {
322
 			handler: nsCertHandler,
354
 			handler: nsCertHandler,
323
 			help: `Syntax: $bCERT <LIST | ADD | DEL> [account] [certfp]$b
355
 			help: `Syntax: $bCERT <LIST | ADD | DEL> [account] [certfp]$b
357
 			minParams: 2,
389
 			minParams: 2,
358
 			capabs:    []string{"accreg"},
390
 			capabs:    []string{"accreg"},
359
 		},
391
 		},
392
+		"verifyemail": {
393
+			handler:      nsVerifyEmailHandler,
394
+			authRequired: true,
395
+			minParams:    1,
396
+			hidden:       true,
397
+		},
360
 	}
398
 	}
361
 )
399
 )
362
 
400
 
459
 		effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
497
 		effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
460
 		service.Notice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
498
 		service.Notice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
461
 		service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
499
 		service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
462
-
500
+	case "email":
501
+		if settings.Email != "" {
502
+			service.Notice(rb, fmt.Sprintf(client.t("Your stored e-mail address is: %s"), settings.Email))
503
+		} else {
504
+			service.Notice(rb, client.t("You have no stored e-mail address"))
505
+		}
463
 	default:
506
 	default:
464
 		service.Notice(rb, client.t("No such setting"))
507
 		service.Notice(rb, client.t("No such setting"))
465
 	}
508
 	}
475
 }
518
 }
476
 
519
 
477
 func nsSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
520
 func nsSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
521
+	var privileged bool
478
 	var account string
522
 	var account string
479
 	if command == "saset" {
523
 	if command == "saset" {
524
+		privileged = true
480
 		account = params[0]
525
 		account = params[0]
481
 		params = params[1:]
526
 		params = params[1:]
482
 	} else {
527
 	} else {
483
 		account = client.Account()
528
 		account = client.Account()
484
 	}
529
 	}
485
 
530
 
531
+	key := strings.ToLower(params[0])
532
+	// unprivileged NS SET EMAIL is different because it requires a confirmation
533
+	if !privileged && key == "email" {
534
+		nsSetEmailHandler(service, client, params, rb)
535
+		return
536
+	}
537
+
486
 	var munger settingsMunger
538
 	var munger settingsMunger
487
 	var finalSettings AccountSettings
539
 	var finalSettings AccountSettings
488
 	var err error
540
 	var err error
489
-	switch strings.ToLower(params[0]) {
541
+	switch key {
490
 	case "pass", "password":
542
 	case "pass", "password":
491
 		service.Notice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD"))
543
 		service.Notice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD"))
492
 		return
544
 		return
603
 				return
655
 				return
604
 			}
656
 			}
605
 		}
657
 		}
658
+	case "email":
659
+		newValue := params[1]
660
+		munger = func(in AccountSettings) (out AccountSettings, err error) {
661
+			out = in
662
+			out.Email = newValue
663
+			return
664
+		}
606
 	default:
665
 	default:
607
 		err = errInvalidParams
666
 		err = errInvalidParams
608
 	}
667
 	}
614
 	switch err {
673
 	switch err {
615
 	case nil:
674
 	case nil:
616
 		service.Notice(rb, client.t("Successfully changed your account settings"))
675
 		service.Notice(rb, client.t("Successfully changed your account settings"))
617
-		displaySetting(service, params[0], finalSettings, client, rb)
676
+		displaySetting(service, key, finalSettings, client, rb)
618
 	case errInvalidParams, errAccountDoesNotExist, errFeatureDisabled, errAccountUnverified, errAccountUpdateFailed:
677
 	case errInvalidParams, errAccountDoesNotExist, errFeatureDisabled, errAccountUnverified, errAccountUpdateFailed:
619
 		service.Notice(rb, client.t(err.Error()))
678
 		service.Notice(rb, client.t(err.Error()))
620
 	case errNickAccountMismatch:
679
 	case errNickAccountMismatch:
625
 	}
684
 	}
626
 }
685
 }
627
 
686
 
687
+// handle unprivileged NS SET EMAIL, which sends a confirmation code
688
+func nsSetEmailHandler(service *ircService, client *Client, params []string, rb *ResponseBuffer) {
689
+	config := client.server.Config()
690
+	if !config.Accounts.Registration.EmailVerification.Enabled {
691
+		rb.Notice(client.t("E-mail verification is disabled"))
692
+		return
693
+	}
694
+	if !nsLoginThrottleCheck(service, client, rb) {
695
+		return
696
+	}
697
+	var password string
698
+	if len(params) > 2 {
699
+		password = params[2]
700
+	}
701
+	account := client.Account()
702
+	errorMessage := nsConfirmPassword(client.server, account, password)
703
+	if errorMessage != "" {
704
+		service.Notice(rb, client.t(errorMessage))
705
+		return
706
+	}
707
+	err := client.server.accounts.NsSetEmail(client, params[1])
708
+	switch err {
709
+	case nil:
710
+		service.Notice(rb, client.t("Check your e-mail for instructions on how to confirm your change of address"))
711
+	case errLimitExceeded:
712
+		service.Notice(rb, client.t("Try again later"))
713
+	default:
714
+		// if appropriate, show the client the error from the attempted email sending
715
+		if rErr := registrationCallbackErrorText(config, client, err); rErr != "" {
716
+			service.Notice(rb, rErr)
717
+		} else {
718
+			service.Notice(rb, client.t("An error occurred"))
719
+		}
720
+	}
721
+}
722
+
723
+func nsVerifyEmailHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
724
+	err := server.accounts.NsVerifyEmail(client, params[0])
725
+	switch err {
726
+	case nil:
727
+		service.Notice(rb, client.t("Successfully changed your account settings"))
728
+		displaySetting(service, "email", client.AccountSettings(), client, rb)
729
+	case errAccountVerificationInvalidCode:
730
+		service.Notice(rb, client.t(err.Error()))
731
+	default:
732
+		service.Notice(rb, client.t("An error occurred"))
733
+	}
734
+}
735
+
628
 func nsDropHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
736
 func nsDropHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
629
 	sadrop := command == "sadrop"
737
 	sadrop := command == "sadrop"
630
 	var nick string
738
 	var nick string
815
 	service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), registeredAt))
923
 	service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), registeredAt))
816
 
924
 
817
 	if account.Name == client.AccountName() || client.HasRoleCapabs("accreg") {
925
 	if account.Name == client.AccountName() || client.HasRoleCapabs("accreg") {
818
-		if account.Email != "" {
819
-			service.Notice(rb, fmt.Sprintf(client.t("Email address: %s"), account.Email))
926
+		if account.Settings.Email != "" {
927
+			service.Notice(rb, fmt.Sprintf(client.t("Email address: %s"), account.Settings.Email))
820
 		}
928
 		}
821
 	}
929
 	}
822
 
930
 
1018
 	}
1126
 	}
1019
 }
1127
 }
1020
 
1128
 
1129
+func nsConfirmPassword(server *Server, account, passphrase string) (errorMessage string) {
1130
+	accountData, err := server.accounts.LoadAccount(account)
1131
+	if err != nil {
1132
+		errorMessage = `You're not logged into an account`
1133
+	} else {
1134
+		hash := accountData.Credentials.PassphraseHash
1135
+		if hash != nil && passwd.CompareHashAndPassword(hash, []byte(passphrase)) != nil {
1136
+			errorMessage = `Password incorrect`
1137
+		}
1138
+	}
1139
+	return
1140
+}
1141
+
1021
 func nsPasswdHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1142
 func nsPasswdHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1022
 	var target string
1143
 	var target string
1023
 	var newPassword string
1144
 	var newPassword string
1041
 		}
1162
 		}
1042
 	case 3:
1163
 	case 3:
1043
 		target = client.Account()
1164
 		target = client.Account()
1165
+		newPassword = params[1]
1166
+		if newPassword == "*" {
1167
+			newPassword = ""
1168
+		}
1044
 		if target == "" {
1169
 		if target == "" {
1045
 			errorMessage = `You're not logged into an account`
1170
 			errorMessage = `You're not logged into an account`
1046
-		} else if params[1] != params[2] {
1171
+		} else if newPassword != params[2] {
1047
 			errorMessage = `Passwords do not match`
1172
 			errorMessage = `Passwords do not match`
1048
 		} else {
1173
 		} else {
1049
 			if !nsLoginThrottleCheck(service, client, rb) {
1174
 			if !nsLoginThrottleCheck(service, client, rb) {
1050
 				return
1175
 				return
1051
 			}
1176
 			}
1052
-			accountData, err := server.accounts.LoadAccount(target)
1053
-			if err != nil {
1054
-				errorMessage = `You're not logged into an account`
1055
-			} else {
1056
-				hash := accountData.Credentials.PassphraseHash
1057
-				if hash != nil && passwd.CompareHashAndPassword(hash, []byte(params[0])) != nil {
1058
-					errorMessage = `Password incorrect`
1059
-				} else {
1060
-					newPassword = params[1]
1061
-					if newPassword == "*" {
1062
-						newPassword = ""
1063
-					}
1064
-				}
1065
-			}
1177
+			errorMessage = nsConfirmPassword(server, target, params[0])
1066
 		}
1178
 		}
1067
 	default:
1179
 	default:
1068
 		errorMessage = `Invalid parameters`
1180
 		errorMessage = `Invalid parameters`
1422
 	return fmt.Sprintf(client.t("Account %[1]s suspended at %[2]s. Duration: %[3]s. %[4]s"), suspension.AccountName, ts, duration, reason)
1534
 	return fmt.Sprintf(client.t("Account %[1]s suspended at %[2]s. Duration: %[3]s. %[4]s"), suspension.AccountName, ts, duration, reason)
1423
 }
1535
 }
1424
 
1536
 
1537
+func nsSendpassHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1538
+	if !nsLoginThrottleCheck(service, client, rb) {
1539
+		return
1540
+	}
1541
+
1542
+	account := params[0]
1543
+	var message string
1544
+	err := server.accounts.NsSendpass(client, account)
1545
+	switch err {
1546
+	case nil:
1547
+		server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf("Client %s sent a password reset for account %s", client.Nick(), account))
1548
+		message = `Successfully sent password reset email`
1549
+	case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
1550
+		message = err.Error()
1551
+	case errValidEmailRequired:
1552
+		message = `That account is not associated with an email address`
1553
+	case errLimitExceeded:
1554
+		message = `Try again later`
1555
+	default:
1556
+		server.logger.Error("services", "error in NS SENDPASS", err.Error())
1557
+		message = `An error occurred`
1558
+	}
1559
+	rb.Notice(client.t(message))
1560
+}
1561
+
1562
+func nsResetpassHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1563
+	if !nsLoginThrottleCheck(service, client, rb) {
1564
+		return
1565
+	}
1566
+
1567
+	var message string
1568
+	err := server.accounts.NsResetpass(client, params[0], params[1], params[2])
1569
+	switch err {
1570
+	case nil:
1571
+		message = `Successfully reset account password`
1572
+	case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended, errAccountBadPassphrase:
1573
+		message = err.Error()
1574
+	case errAccountInvalidCredentials:
1575
+		message = `Code did not match`
1576
+	default:
1577
+		server.logger.Error("services", "error in NS RESETPASS", err.Error())
1578
+		message = `An error occurred`
1579
+	}
1580
+	rb.Notice(client.t(message))
1581
+}
1582
+
1425
 func nsRenameHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1583
 func nsRenameHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
1426
 	oldName, newName := params[0], params[1]
1584
 	oldName, newName := params[0], params[1]
1427
 	err := server.accounts.Rename(oldName, newName)
1585
 	err := server.accounts.Rename(oldName, newName)

+ 8
- 1
irc/smtp/smtp.go View File

354
 		return err
354
 		return err
355
 	}
355
 	}
356
 	if ok, _ := c.Extension("STARTTLS"); ok {
356
 	if ok, _ := c.Extension("STARTTLS"); ok {
357
-		config := &tls.Config{ServerName: c.serverName}
357
+		var config *tls.Config
358
+		if requireTLS {
359
+			config = &tls.Config{ServerName: c.serverName}
360
+		} else {
361
+			// if TLS isn't a hard requirement, don't verify the certificate either,
362
+			// since a MITM attacker could just remove the STARTTLS advertisement
363
+			config = &tls.Config{InsecureSkipVerify: true}
364
+		}
358
 		if testHookStartTLS != nil {
365
 		if testHookStartTLS != nil {
359
 			testHookStartTLS(config)
366
 			testHookStartTLS(config)
360
 		}
367
 		}

+ 7
- 0
traditional.yaml View File

387
             blacklist-regexes:
387
             blacklist-regexes:
388
             #    - ".*@mailinator.com"
388
             #    - ".*@mailinator.com"
389
             timeout: 60s
389
             timeout: 60s
390
+            # email-based password reset:
391
+            password-reset:
392
+                enabled: false
393
+                # time before we allow resending the email
394
+                cooldown: 1h
395
+                # time for which a password reset code is valid
396
+                timeout: 1d
390
 
397
 
391
     # throttle account login attempts (to prevent either password guessing, or DoS
398
     # throttle account login attempts (to prevent either password guessing, or DoS
392
     # attacks on the server aimed at forcing repeated expensive bcrypt computations)
399
     # attacks on the server aimed at forcing repeated expensive bcrypt computations)

Loading…
Cancel
Save