mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
8096441f67
the default transport is still just "direct delivery", where we connect to the destination domain's MX servers. other transports are: - regular smtp without authentication, this is relaying to a smarthost. - submission with authentication, e.g. to a third party email sending service. - direct delivery, but with with connections going through a socks proxy. this can be helpful if your ip is blocked, you need to get email out, and you have another IP that isn't blocked. keep in mind that for all of the above, appropriate SPF/DKIM settings have to be configured. the "dnscheck" for a domain does a check for any SOCKS IP in the SPF record. SPF for smtp/submission (ranges? includes?) and any DKIM requirements cannot really be checked. which transport is used can be configured through routes. routes can be set on an account, a domain, or globally. the routes are evaluated in that order, with the first match selecting the transport. these routes are evaluated for each delivery attempt. common selection criteria are recipient domain and sender domain, but also which delivery attempt this is. you could configured mox to attempt sending through a 3rd party from the 4th attempt onwards. routes and transports are optional. if no route matches, or an empty/zero transport is selected, normal direct delivery is done. we could already "submit" emails with 3rd party accounts with "sendmail". but we now support more SASL authentication mechanisms with SMTP (not only PLAIN, but also SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5), which sendmail now also supports. sendmail will use the most secure mechanism supported by the server, or the explicitly configured mechanism. for issue #36 by dmikushin. also based on earlier discussion on hackernews.
176 lines
4.7 KiB
Go
176 lines
4.7 KiB
Go
// Package SASL implements Simple Authentication and Security Layer, RFC 4422.
|
|
package sasl
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"hash"
|
|
"strings"
|
|
|
|
"github.com/mjl-/mox/scram"
|
|
)
|
|
|
|
// Client is a SASL client
|
|
type Client interface {
|
|
// Name as used in SMTP AUTH, e.g. PLAIN, CRAM-MD5, SCRAM-SHA-256.
|
|
// cleartextCredentials indicates if credentials are exchanged in clear text, which influences whether they are logged.
|
|
Info() (name string, cleartextCredentials bool)
|
|
|
|
// Next is called for each step of the SASL communication. The first call has a nil
|
|
// fromServer and serves to get a possible "initial response" from the client. If
|
|
// the client sends its final message it indicates so with last. Returning an error
|
|
// aborts the authentication attempt.
|
|
// 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.
|
|
func NewClientPlain(username, password string) Client {
|
|
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 []byte(fmt.Sprintf("\u0000%s\u0000%s", a.Username, a.Password)), true, nil
|
|
default:
|
|
return nil, false, fmt.Errorf("invalid step %d", a.step)
|
|
}
|
|
}
|
|
|
|
type clientCRAMMD5 struct {
|
|
Username, Password string
|
|
step int
|
|
}
|
|
|
|
var _ Client = (*clientCRAMMD5)(nil)
|
|
|
|
// NewClientCRAMMD5 returns a client for SASL CRAM-MD5 authentication.
|
|
func NewClientCRAMMD5(username, password string) Client {
|
|
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 []byte(fmt.Sprintf("%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
|
|
|
|
name string
|
|
step int
|
|
scram *scram.Client
|
|
}
|
|
|
|
var _ Client = (*clientSCRAMSHA)(nil)
|
|
|
|
// NewClientSCRAMSHA1 returns a client for SASL SCRAM-SHA-1 authentication.
|
|
func NewClientSCRAMSHA1(username, password string) Client {
|
|
return &clientSCRAMSHA{username, password, "SCRAM-SHA-1", 0, nil}
|
|
}
|
|
|
|
// NewClientSCRAMSHA256 returns a client for SASL SCRAM-SHA-256 authentication.
|
|
func NewClientSCRAMSHA256(username, password string) Client {
|
|
return &clientSCRAMSHA{username, password, "SCRAM-SHA-256", 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 h func() hash.Hash
|
|
switch a.name {
|
|
case "SCRAM-SHA-1":
|
|
h = sha1.New
|
|
case "SCRAM-SHA-256":
|
|
h = sha256.New
|
|
default:
|
|
return nil, false, fmt.Errorf("invalid SCRAM-SHA variant %q", a.name)
|
|
}
|
|
|
|
a.scram = scram.NewClient(h, a.Username, "")
|
|
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)
|
|
}
|
|
}
|