mirror of
https://github.com/mjl-/mox.git
synced 2024-12-27 08:53:48 +03:00
91b7d3dda8
so microsoft outlook "new" can login. that's the "new" email client that logs in from cloud servers.
205 lines
5.5 KiB
Go
205 lines
5.5 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 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 {
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|