mirror of
https://github.com/hanzoai/mail.git
synced 2026-09-23 23:53:02 +00:00
Fork of github.com/mjl-/mox at 5656ca6d1b
(upstream v0.0.15 plus 55 commits).
Rename the module path in go.mod and the import paths in 268 Go files. Also
update the five scripts that "go install" the module (genwebsite.sh,
docker-release.sh, apidiff.sh, test-upgrade.sh, Dockerfile.release), which
would otherwise build upstream rather than this fork.
Add NOTICE recording the upstream project, author, license, and fork point,
and stating which license covers which files: MIT for the mox source and our
changes, MPL-2.0 for publicsuffix/public_suffix_list.txt alone, and the
vendored dependencies' own licenses under licenses/. Embed NOTICE so
"mail licenses" prints the lineage from the running binary. LICENSE.MIT,
LICENSE.MPLv2.0 and the licenses/ tree are preserved verbatim; no code is
relicensed.
The mox command name, config keys, data directory layout and website content
are unchanged. LLM.md records the fork point, the rename, what was left alone,
and the steps to stand the server up.
Co-authored-by: Hanzo Dev <[email protected]>
328 lines
10 KiB
Go
328 lines
10 KiB
Go
// Package SASL implements a client for Simple Authentication and Security Layer, RFC 4422.
|
|
//
|
|
// Supported authentication mechanisms:
|
|
//
|
|
// - EXTERNAL
|
|
// - SCRAM-SHA-256-PLUS
|
|
// - SCRAM-SHA-1-PLUS
|
|
// - SCRAM-SHA-256
|
|
// - SCRAM-SHA-1
|
|
// - CRAM-MD5
|
|
// - PLAIN
|
|
// - LOGIN
|
|
package sasl
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"hash"
|
|
"strings"
|
|
|
|
"golang.org/x/text/secure/precis"
|
|
|
|
"github.com/hanzoai/mail/scram"
|
|
)
|
|
|
|
// Client is a SASL client.
|
|
//
|
|
// A SASL client can be used for authentication in IMAP, SMTP and other protocols.
|
|
// A client and server exchange messages in step lock. In IMAP and SMTP, these
|
|
// messages are encoded with base64. Each SASL mechanism has predefined steps, but
|
|
// the transaction can be aborted by either side at any time. An IMAP or SMTP
|
|
// client must choose a SASL mechanism, instantiate a SASL client, and call Next
|
|
// with a nil parameter. The resulting data must be written to the server, properly
|
|
// encoded. The client must then read the response from the server and feed it to
|
|
// the SASL client, which will return more data to send, or an error.
|
|
type Client interface {
|
|
// Name as used in SMTP or IMAP authentication, e.g. PLAIN, CRAM-MD5,
|
|
// SCRAM-SHA-256. cleartextCredentials indicates if credentials are exchanged in
|
|
// clear text, which can be used to decide if the exchange is logged.
|
|
Info() (name string, cleartextCredentials bool)
|
|
|
|
// Next must be called for each step of the SASL transaction. The first call has a
|
|
// nil fromServer and serves to get a possible "initial response" from the client
|
|
// to the server. When last is true, the message from client to server is the last
|
|
// one, and the server must send a verdict. If err is set, the transaction must be
|
|
// aborted.
|
|
//
|
|
// For the first toServer ("initial response"), a nil toServer indicates there is
|
|
// no data, which is different from a non-nil zero-length toServer.
|
|
Next(fromServer []byte) (toServer []byte, last bool, err error)
|
|
}
|
|
|
|
type clientPlain struct {
|
|
Username, Password string
|
|
step int
|
|
}
|
|
|
|
var _ Client = (*clientPlain)(nil)
|
|
|
|
// NewClientPlain returns a client for SASL PLAIN authentication.
|
|
//
|
|
// PLAIN is specified in RFC 4616, The PLAIN Simple Authentication and Security
|
|
// Layer (SASL) Mechanism.
|
|
func NewClientPlain(username, password string) Client {
|
|
// No "precis" processing, remote can clean password up. ../rfc/8265:679
|
|
return &clientPlain{username, password, 0}
|
|
}
|
|
|
|
func (a *clientPlain) Info() (name string, hasCleartextCredentials bool) {
|
|
return "PLAIN", true
|
|
}
|
|
|
|
func (a *clientPlain) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
|
|
defer func() { a.step++ }()
|
|
switch a.step {
|
|
case 0:
|
|
return fmt.Appendf(nil, "\u0000%s\u0000%s", a.Username, a.Password), true, nil
|
|
default:
|
|
return nil, false, fmt.Errorf("invalid step %d", a.step)
|
|
}
|
|
}
|
|
|
|
type clientLogin struct {
|
|
Username, Password string
|
|
step int
|
|
}
|
|
|
|
var _ Client = (*clientLogin)(nil)
|
|
|
|
// NewClientLogin returns a client for the obsolete SASL LOGIN authentication.
|
|
//
|
|
// See https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
|
|
func NewClientLogin(username, password string) Client {
|
|
// No "precis" processing, remote can clean password up. ../rfc/8265:679
|
|
return &clientLogin{username, password, 0}
|
|
}
|
|
|
|
func (a *clientLogin) Info() (name string, hasCleartextCredentials bool) {
|
|
return "LOGIN", true
|
|
}
|
|
|
|
func (a *clientLogin) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
|
|
defer func() { a.step++ }()
|
|
switch a.step {
|
|
case 0:
|
|
return []byte(a.Username), false, nil
|
|
case 1:
|
|
return []byte(a.Password), true, nil
|
|
default:
|
|
return nil, false, fmt.Errorf("invalid step %d", a.step)
|
|
}
|
|
}
|
|
|
|
// Cleanup password with precis, like remote should have done. If the password
|
|
// appears invalid, we'll return the original, there is a chance the server also
|
|
// doesn't enforce requirements and accepts it. ../rfc/8265:679
|
|
func precisPassword(password string) string {
|
|
pw, err := precis.OpaqueString.String(password)
|
|
if err != nil {
|
|
return password
|
|
}
|
|
return pw
|
|
}
|
|
|
|
type clientCRAMMD5 struct {
|
|
Username, Password string
|
|
step int
|
|
}
|
|
|
|
var _ Client = (*clientCRAMMD5)(nil)
|
|
|
|
// NewClientCRAMMD5 returns a client for SASL CRAM-MD5 authentication.
|
|
//
|
|
// CRAM-MD5 is specified in RFC 2195, IMAP/POP AUTHorize Extension for Simple
|
|
// Challenge/Response.
|
|
func NewClientCRAMMD5(username, password string) Client {
|
|
password = precisPassword(password)
|
|
return &clientCRAMMD5{username, password, 0}
|
|
}
|
|
|
|
func (a *clientCRAMMD5) Info() (name string, hasCleartextCredentials bool) {
|
|
return "CRAM-MD5", false
|
|
}
|
|
|
|
func (a *clientCRAMMD5) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
|
|
defer func() { a.step++ }()
|
|
switch a.step {
|
|
case 0:
|
|
return nil, false, nil
|
|
case 1:
|
|
// Validate the challenge.
|
|
// ../rfc/2195:82
|
|
s := string(fromServer)
|
|
if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
|
|
return nil, false, fmt.Errorf("invalid challenge, missing angle brackets")
|
|
}
|
|
t := strings.SplitN(s, ".", 2)
|
|
if len(t) != 2 || t[0] == "" {
|
|
return nil, false, fmt.Errorf("invalid challenge, missing dot or random digits")
|
|
}
|
|
t = strings.Split(t[1], "@")
|
|
if len(t) == 1 || t[0] == "" || t[len(t)-1] == "" {
|
|
return nil, false, fmt.Errorf("invalid challenge, empty timestamp or empty hostname")
|
|
}
|
|
|
|
// ../rfc/2195:138
|
|
key := []byte(a.Password)
|
|
if len(key) > 64 {
|
|
t := md5.Sum(key)
|
|
key = t[:]
|
|
}
|
|
ipad := make([]byte, md5.BlockSize)
|
|
opad := make([]byte, md5.BlockSize)
|
|
copy(ipad, key)
|
|
copy(opad, key)
|
|
for i := range ipad {
|
|
ipad[i] ^= 0x36
|
|
opad[i] ^= 0x5c
|
|
}
|
|
ipadh := md5.New()
|
|
ipadh.Write(ipad)
|
|
ipadh.Write([]byte(fromServer))
|
|
|
|
opadh := md5.New()
|
|
opadh.Write(opad)
|
|
opadh.Write(ipadh.Sum(nil))
|
|
|
|
// ../rfc/2195:88
|
|
return fmt.Appendf(nil, "%s %x", a.Username, opadh.Sum(nil)), true, nil
|
|
|
|
default:
|
|
return nil, false, fmt.Errorf("invalid step %d", a.step)
|
|
}
|
|
}
|
|
|
|
type clientSCRAMSHA struct {
|
|
Username, Password string
|
|
|
|
hash func() hash.Hash
|
|
|
|
plus bool
|
|
cs tls.ConnectionState
|
|
|
|
// When not doing PLUS variant, this field indicates whether that is because the
|
|
// server doesn't support the PLUS variant. Used for detecting MitM attempts.
|
|
noServerPlus bool
|
|
|
|
name string
|
|
step int
|
|
scram *scram.Client
|
|
}
|
|
|
|
var _ Client = (*clientSCRAMSHA)(nil)
|
|
|
|
// NewClientSCRAMSHA1 returns a client for SASL SCRAM-SHA-1 authentication.
|
|
//
|
|
// Clients should prefer using the PLUS-variant with TLS channel binding, if
|
|
// supported by a server. If noServerPlus is set, this mechanism was chosen because
|
|
// the PLUS-variant was not supported by the server. If the server actually does
|
|
// implement the PLUS variant, this can indicate a MitM attempt, which is detected
|
|
// by the server and causes the authentication attempt to be aborted.
|
|
//
|
|
// SCRAM-SHA-1 is specified in RFC 5802, "Salted Challenge Response Authentication
|
|
// Mechanism (SCRAM) SASL and GSS-API Mechanisms".
|
|
func NewClientSCRAMSHA1(username, password string, noServerPlus bool) Client {
|
|
password = precisPassword(password)
|
|
return &clientSCRAMSHA{username, password, sha1.New, false, tls.ConnectionState{}, noServerPlus, "SCRAM-SHA-1", 0, nil}
|
|
}
|
|
|
|
// NewClientSCRAMSHA1PLUS returns a client for SASL SCRAM-SHA-1-PLUS authentication.
|
|
//
|
|
// The PLUS-variant binds the authentication exchange to the TLS connection,
|
|
// detecting any MitM attempt.
|
|
//
|
|
// SCRAM-SHA-1-PLUS is specified in RFC 5802, "Salted Challenge Response
|
|
// Authentication Mechanism (SCRAM) SASL and GSS-API Mechanisms".
|
|
func NewClientSCRAMSHA1PLUS(username, password string, cs tls.ConnectionState) Client {
|
|
password = precisPassword(password)
|
|
return &clientSCRAMSHA{username, password, sha1.New, true, cs, false, "SCRAM-SHA-1-PLUS", 0, nil}
|
|
}
|
|
|
|
// NewClientSCRAMSHA256 returns a client for SASL SCRAM-SHA-256 authentication.
|
|
//
|
|
// Clients should prefer using the PLUS-variant with TLS channel binding, if
|
|
// supported by a server. If noServerPlus is set, this mechanism was chosen because
|
|
// the PLUS-variant was not supported by the server. If the server actually does
|
|
// implement the PLUS variant, this can indicate a MitM attempt, which is detected
|
|
// by the server and causes the authentication attempt to be aborted.
|
|
//
|
|
// SCRAM-SHA-256 is specified in RFC 7677, "SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
|
|
// Simple Authentication and Security Layer (SASL) Mechanisms".
|
|
func NewClientSCRAMSHA256(username, password string, noServerPlus bool) Client {
|
|
password = precisPassword(password)
|
|
return &clientSCRAMSHA{username, password, sha256.New, false, tls.ConnectionState{}, noServerPlus, "SCRAM-SHA-256", 0, nil}
|
|
}
|
|
|
|
// NewClientSCRAMSHA256PLUS returns a client for SASL SCRAM-SHA-256-PLUS authentication.
|
|
//
|
|
// The PLUS-variant binds the authentication exchange to the TLS connection,
|
|
// detecting any MitM attempt.
|
|
//
|
|
// SCRAM-SHA-256-PLUS is specified in RFC 7677, "SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
|
|
// Simple Authentication and Security Layer (SASL) Mechanisms".
|
|
func NewClientSCRAMSHA256PLUS(username, password string, cs tls.ConnectionState) Client {
|
|
password = precisPassword(password)
|
|
return &clientSCRAMSHA{username, password, sha256.New, true, cs, false, "SCRAM-SHA-256-PLUS", 0, nil}
|
|
}
|
|
|
|
func (a *clientSCRAMSHA) Info() (name string, hasCleartextCredentials bool) {
|
|
return a.name, false
|
|
}
|
|
|
|
func (a *clientSCRAMSHA) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
|
|
defer func() { a.step++ }()
|
|
switch a.step {
|
|
case 0:
|
|
var cs *tls.ConnectionState
|
|
if a.plus {
|
|
cs = &a.cs
|
|
}
|
|
a.scram = scram.NewClient(a.hash, a.Username, "", a.noServerPlus, cs)
|
|
toserver, err := a.scram.ClientFirst()
|
|
return []byte(toserver), false, err
|
|
|
|
case 1:
|
|
clientFinal, err := a.scram.ServerFirst(fromServer, a.Password)
|
|
return []byte(clientFinal), false, err
|
|
|
|
case 2:
|
|
err := a.scram.ServerFinal(fromServer)
|
|
return nil, true, err
|
|
|
|
default:
|
|
return nil, false, fmt.Errorf("invalid step %d", a.step)
|
|
}
|
|
}
|
|
|
|
type clientExternal struct {
|
|
Username string
|
|
step int
|
|
}
|
|
|
|
var _ Client = (*clientExternal)(nil)
|
|
|
|
// NewClientExternal returns a client for SASL EXTERNAL authentication.
|
|
//
|
|
// Username is optional.
|
|
func NewClientExternal(username string) Client {
|
|
return &clientExternal{username, 0}
|
|
}
|
|
|
|
func (a *clientExternal) Info() (name string, hasCleartextCredentials bool) {
|
|
return "EXTERNAL", false
|
|
}
|
|
|
|
func (a *clientExternal) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
|
|
defer func() { a.step++ }()
|
|
switch a.step {
|
|
case 0:
|
|
return []byte(a.Username), true, nil
|
|
default:
|
|
return nil, false, fmt.Errorf("invalid step %d", a.step)
|
|
}
|
|
}
|