support cram-md5 authentication for imap and smtp

and change thunderbird autoconfiguration to use it.

unfortunately, for microsoft autodiscover, there appears to be no way to
request secure password negotiation. so it will default to plain text auth.

cram-md5 is less secure than scram-sha-*, but thunderbird does not yet support
scram auth. it currently chooses "plain", sending the literal password over the
connection (which is TLS-protected, but we don't want to receive clear text
passwords). in short, cram-md5 is better than nothing...

for cram-md5 to work, a new set of derived credentials need to be stored in the
database. so you need to save your password again to make it work. this was
also the case with the scram-sha-1 addition, but i forgot to mention it then.
This commit is contained in:
Mechiel Lukkien
2023-02-05 16:29:03 +01:00
parent f83fe79f96
commit e52c9d36a6
9 changed files with 306 additions and 22 deletions

View File

@ -39,6 +39,7 @@ import (
"bufio"
"bytes"
"context"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/tls"
@ -118,8 +119,9 @@ var (
// ID: ../rfc/2971
// AUTH=SCRAM-SHA-256: ../rfc/7677 ../rfc/5802
// AUTH=SCRAM-SHA-1: ../rfc/5802
// AUTH=CRAM-MD5: ../rfc/2195
// APPENDLIMIT, we support the max possible size, 1<<63 - 1: ../rfc/7889:129
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 ID APPENDLIMIT=9223372036854775807"
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807"
type conn struct {
cid int64
@ -1392,6 +1394,71 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
c.username = authc
authResult = "ok"
case "CRAM-MD5":
authVariant = strings.ToLower(authType)
// ../rfc/9051:1462
p.xempty()
// ../rfc/2195:82
chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII)
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal)))
resp := xreadContinuation()
t := strings.Split(string(resp), " ")
if len(t) != 2 || len(t[1]) != 2*md5.Size {
xsyntaxErrorf("malformed cram-md5 response")
}
addr := t[0]
c.log.Info("cram-md5 auth", mlog.Field("address", addr))
acc, _, err := store.OpenEmail(addr)
if err != nil {
if errors.Is(err, store.ErrUnknownCredentials) {
xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
}
xserverErrorf("looking up address: %v", err)
}
defer func() {
if acc != nil {
err := acc.Close()
c.xsanity(err, "close account")
}
}()
var ipadhash, opadhash hash.Hash
acc.WithRLock(func() {
err := acc.DB.Read(func(tx *bstore.Tx) error {
password, err := bstore.QueryTx[store.Password](tx).Get()
if err == bstore.ErrAbsent {
xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
}
if err != nil {
return err
}
ipadhash = password.CRAMMD5.Ipad
opadhash = password.CRAMMD5.Opad
return nil
})
xcheckf(err, "tx read")
})
if ipadhash == nil || opadhash == nil {
c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", addr))
xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
}
// ../rfc/2195:138 ../rfc/2104:142
ipadhash.Write([]byte(chal))
opadhash.Write(ipadhash.Sum(nil))
digest := fmt.Sprintf("%x", opadhash.Sum(nil))
if digest != t[1] {
xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
}
c.account = acc
acc = nil // Cancel cleanup.
c.username = addr
authResult = "ok"
case "SCRAM-SHA-1", "SCRAM-SHA-256":
// todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error?
// todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
@ -1438,6 +1505,7 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
xscram = password.SCRAMSHA256
}
if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", ss.Authentication))
xuserErrorf("scram not possible")
}
xcheckf(err, "fetching credentials")