1// Package SASL implements Simple Authentication and Security Layer, RFC 4422.
12 "github.com/mjl-/mox/scram"
15// Client is a SASL client
16type Client interface {
17 // Name as used in SMTP AUTH, e.g. PLAIN, CRAM-MD5, SCRAM-SHA-256.
18 // cleartextCredentials indicates if credentials are exchanged in clear text, which influences whether they are logged.
19 Info() (name string, cleartextCredentials bool)
21 // Next is called for each step of the SASL communication. The first call has a nil
22 // fromServer and serves to get a possible "initial response" from the client. If
23 // the client sends its final message it indicates so with last. Returning an error
24 // aborts the authentication attempt.
25 // For the first toServer ("initial response"), a nil toServer indicates there is
26 // no data, which is different from a non-nil zero-length toServer.
27 Next(fromServer []byte) (toServer []byte, last bool, err error)
30type clientPlain struct {
31 Username, Password string
35var _ Client = (*clientPlain)(nil)
37// NewClientPlain returns a client for SASL PLAIN authentication.
38func NewClientPlain(username, password string) Client {
39 return &clientPlain{username, password, 0}
42func (a *clientPlain) Info() (name string, hasCleartextCredentials bool) {
46func (a *clientPlain) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
47 defer func() { a.step++ }()
50 return []byte(fmt.Sprintf("\u0000%s\u0000%s", a.Username, a.Password)), true, nil
52 return nil, false, fmt.Errorf("invalid step %d", a.step)
56type clientLogin struct {
57 Username, Password string
61var _ Client = (*clientLogin)(nil)
63// NewClientLogin returns a client for the obsolete SASL LOGIN authentication.
64// See https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
65func NewClientLogin(username, password string) Client {
66 return &clientLogin{username, password, 0}
69func (a *clientLogin) Info() (name string, hasCleartextCredentials bool) {
73func (a *clientLogin) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
74 defer func() { a.step++ }()
77 return []byte(a.Username), false, nil
79 return []byte(a.Password), true, nil
81 return nil, false, fmt.Errorf("invalid step %d", a.step)
85type clientCRAMMD5 struct {
86 Username, Password string
90var _ Client = (*clientCRAMMD5)(nil)
92// NewClientCRAMMD5 returns a client for SASL CRAM-MD5 authentication.
93func NewClientCRAMMD5(username, password string) Client {
94 return &clientCRAMMD5{username, password, 0}
97func (a *clientCRAMMD5) Info() (name string, hasCleartextCredentials bool) {
98 return "CRAM-MD5", false
101func (a *clientCRAMMD5) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
102 defer func() { a.step++ }()
105 return nil, false, nil
107 // Validate the challenge.
109 s := string(fromServer)
110 if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
111 return nil, false, fmt.Errorf("invalid challenge, missing angle brackets")
113 t := strings.SplitN(s, ".", 2)
114 if len(t) != 2 || t[0] == "" {
115 return nil, false, fmt.Errorf("invalid challenge, missing dot or random digits")
117 t = strings.Split(t[1], "@")
118 if len(t) == 1 || t[0] == "" || t[len(t)-1] == "" {
119 return nil, false, fmt.Errorf("invalid challenge, empty timestamp or empty hostname")
123 key := []byte(a.Password)
128 ipad := make([]byte, md5.BlockSize)
129 opad := make([]byte, md5.BlockSize)
132 for i := range ipad {
138 ipadh.Write([]byte(fromServer))
142 opadh.Write(ipadh.Sum(nil))
145 return []byte(fmt.Sprintf("%s %x", a.Username, opadh.Sum(nil))), true, nil
148 return nil, false, fmt.Errorf("invalid step %d", a.step)
152type clientSCRAMSHA struct {
153 Username, Password string
160var _ Client = (*clientSCRAMSHA)(nil)
162// NewClientSCRAMSHA1 returns a client for SASL SCRAM-SHA-1 authentication.
163func NewClientSCRAMSHA1(username, password string) Client {
164 return &clientSCRAMSHA{username, password, "SCRAM-SHA-1", 0, nil}
167// NewClientSCRAMSHA256 returns a client for SASL SCRAM-SHA-256 authentication.
168func NewClientSCRAMSHA256(username, password string) Client {
169 return &clientSCRAMSHA{username, password, "SCRAM-SHA-256", 0, nil}
172func (a *clientSCRAMSHA) Info() (name string, hasCleartextCredentials bool) {
176func (a *clientSCRAMSHA) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
177 defer func() { a.step++ }()
180 var h func() hash.Hash
184 case "SCRAM-SHA-256":
187 return nil, false, fmt.Errorf("invalid SCRAM-SHA variant %q", a.name)
190 a.scram = scram.NewClient(h, a.Username, "")
191 toserver, err := a.scram.ClientFirst()
192 return []byte(toserver), false, err
195 clientFinal, err := a.scram.ServerFirst(fromServer, a.Password)
196 return []byte(clientFinal), false, err
199 err := a.scram.ServerFinal(fromServer)
200 return nil, true, err
203 return nil, false, fmt.Errorf("invalid step %d", a.step)