when auth fails due to missing derived secrets, don't hold it against connection

smtp & imap can only indicate which mechanisms the server software supports.
individual accounts may not have derived secrets for all those mechanisms. imap
& smtp cannot indicate that a client should try another (specific) mechanism.
but at least we shouldn't slow the connection down due to failed auth attempts
in that case.

heard from ben that this is a common source for trouble when setting up email
accounts.
This commit is contained in:
Mechiel Lukkien 2024-03-05 10:40:40 +01:00
parent caa4931d35
commit 5738d9e7b8
No known key found for this signature in database
2 changed files with 39 additions and 12 deletions

View file

@ -1495,8 +1495,17 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
if c.authFailed > 3 && authFailDelay > 0 { if c.authFailed > 3 && authFailDelay > 0 {
mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay) mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
} }
// If authentication fails due to missing derived secrets, we don't hold it against
// the connection. There is no way to indicate server support for an authentication
// mechanism, but that a mechanism won't work for an account.
var missingDerivedSecrets bool
c.authFailed++ // Compensated on success. c.authFailed++ // Compensated on success.
defer func() { defer func() {
if missingDerivedSecrets {
c.authFailed--
}
// On the 3rd failed authentication, start responding slowly. Successful auth will // On the 3rd failed authentication, start responding slowly. Successful auth will
// cause fast responses again. // cause fast responses again.
if c.authFailed >= 3 { if c.authFailed >= 3 {
@ -1508,10 +1517,9 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
authResult := "error" authResult := "error"
defer func() { defer func() {
metrics.AuthenticationInc("imap", authVariant, authResult) metrics.AuthenticationInc("imap", authVariant, authResult)
switch authResult { if authResult == "ok" {
case "ok":
mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now()) mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
default: } else if !missingDerivedSecrets {
mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1) mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
} }
}() }()
@ -1648,6 +1656,7 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
if ipadhash == nil || opadhash == nil { if ipadhash == nil || opadhash == nil {
c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr)) c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP)) c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
missingDerivedSecrets = true
xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials") xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
} }
@ -1716,6 +1725,11 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
acc.WithRLock(func() { acc.WithRLock(func() {
err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error { err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
password, err := bstore.QueryTx[store.Password](tx).Get() password, err := bstore.QueryTx[store.Password](tx).Get()
if err == bstore.ErrAbsent {
c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
}
xcheckf(err, "fetching credentials")
switch authVariant { switch authVariant {
case "scram-sha-1", "scram-sha-1-plus": case "scram-sha-1", "scram-sha-1-plus":
xscram = password.SCRAMSHA1 xscram = password.SCRAMSHA1
@ -1724,12 +1738,12 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
default: default:
xserverErrorf("missing case for scram credentials") xserverErrorf("missing case for scram credentials")
} }
if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) { if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
missingDerivedSecrets = true
c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication)) c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication))
xuserErrorf("scram not possible") xuserErrorf("scram not possible")
} }
xcheckf(err, "fetching credentials") return nil
return err
}) })
xcheckf(err, "read tx") xcheckf(err, "read tx")
}) })

View file

@ -959,6 +959,11 @@ func (c *conn) cmdAuth(p *parser) {
// todo future: we may want to normalize usernames and passwords, see stringprep in ../rfc/4013:38 and possibly newer mechanisms (though they are opt-in and that may not have happened yet). // todo future: we may want to normalize usernames and passwords, see stringprep in ../rfc/4013:38 and possibly newer mechanisms (though they are opt-in and that may not have happened yet).
// If authentication fails due to missing derived secrets, we don't hold it against
// the connection. There is no way to indicate server support for an authentication
// mechanism, but that a mechanism won't work for an account.
var missingDerivedSecrets bool
// For many failed auth attempts, slow down verification attempts. // For many failed auth attempts, slow down verification attempts.
// Dropping the connection could also work, but more so when we have a connection rate limiter. // Dropping the connection could also work, but more so when we have a connection rate limiter.
// ../rfc/4954:770 // ../rfc/4954:770
@ -968,6 +973,9 @@ func (c *conn) cmdAuth(p *parser) {
} }
c.authFailed++ // Compensated on success. c.authFailed++ // Compensated on success.
defer func() { defer func() {
if missingDerivedSecrets {
c.authFailed--
}
// On the 3rd failed authentication, start responding slowly. Successful auth will // On the 3rd failed authentication, start responding slowly. Successful auth will
// cause fast responses again. // cause fast responses again.
if c.authFailed >= 3 { if c.authFailed >= 3 {
@ -979,10 +987,9 @@ func (c *conn) cmdAuth(p *parser) {
authResult := "error" authResult := "error"
defer func() { defer func() {
metrics.AuthenticationInc("submission", authVariant, authResult) metrics.AuthenticationInc("submission", authVariant, authResult)
switch authResult { if authResult == "ok" {
case "ok":
mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now()) mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
default: } else if !missingDerivedSecrets {
mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1) mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
} }
}() }()
@ -1179,6 +1186,7 @@ func (c *conn) cmdAuth(p *parser) {
xcheckf(err, "tx read") xcheckf(err, "tx read")
}) })
if ipadhash == nil || opadhash == nil { if ipadhash == nil || opadhash == nil {
missingDerivedSecrets = true
c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr)) c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP)) c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass") xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
@ -1254,6 +1262,11 @@ func (c *conn) cmdAuth(p *parser) {
acc.WithRLock(func() { acc.WithRLock(func() {
err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error { err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
password, err := bstore.QueryTx[store.Password](tx).Get() password, err := bstore.QueryTx[store.Password](tx).Get()
if err == bstore.ErrAbsent {
c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
}
xcheckf(err, "fetching credentials")
switch authVariant { switch authVariant {
case "scram-sha-1", "scram-sha-1-plus": case "scram-sha-1", "scram-sha-1-plus":
xscram = password.SCRAMSHA1 xscram = password.SCRAMSHA1
@ -1262,13 +1275,13 @@ func (c *conn) cmdAuth(p *parser) {
default: default:
xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case") xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
} }
if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) { if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
missingDerivedSecrets = true
c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication)) c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication))
c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP)) c.log.Info("failed authentication attempt", slog.String("username", ss.Authentication), slog.Any("remote", c.remoteIP))
xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible") xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
} }
xcheckf(err, "fetching credentials") return nil
return err
}) })
xcheckf(err, "read tx") xcheckf(err, "read tx")
}) })