1// Package SASL implements Simple Authentication and Security Layer, RFC 4422.
2package sasl
3
4import (
5 "crypto/md5"
6 "crypto/sha1"
7 "crypto/sha256"
8 "fmt"
9 "hash"
10 "strings"
11
12 "github.com/mjl-/mox/scram"
13)
14
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)
20
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)
28}
29
30type clientPlain struct {
31 Username, Password string
32 step int
33}
34
35var _ Client = (*clientPlain)(nil)
36
37// NewClientPlain returns a client for SASL PLAIN authentication.
38func NewClientPlain(username, password string) Client {
39 return &clientPlain{username, password, 0}
40}
41
42func (a *clientPlain) Info() (name string, hasCleartextCredentials bool) {
43 return "PLAIN", true
44}
45
46func (a *clientPlain) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
47 defer func() { a.step++ }()
48 switch a.step {
49 case 0:
50 return []byte(fmt.Sprintf("\u0000%s\u0000%s", a.Username, a.Password)), true, nil
51 default:
52 return nil, false, fmt.Errorf("invalid step %d", a.step)
53 }
54}
55
56type clientLogin struct {
57 Username, Password string
58 step int
59}
60
61var _ Client = (*clientLogin)(nil)
62
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}
67}
68
69func (a *clientLogin) Info() (name string, hasCleartextCredentials bool) {
70 return "LOGIN", true
71}
72
73func (a *clientLogin) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
74 defer func() { a.step++ }()
75 switch a.step {
76 case 0:
77 return []byte(a.Username), false, nil
78 case 1:
79 return []byte(a.Password), true, nil
80 default:
81 return nil, false, fmt.Errorf("invalid step %d", a.step)
82 }
83}
84
85type clientCRAMMD5 struct {
86 Username, Password string
87 step int
88}
89
90var _ Client = (*clientCRAMMD5)(nil)
91
92// NewClientCRAMMD5 returns a client for SASL CRAM-MD5 authentication.
93func NewClientCRAMMD5(username, password string) Client {
94 return &clientCRAMMD5{username, password, 0}
95}
96
97func (a *clientCRAMMD5) Info() (name string, hasCleartextCredentials bool) {
98 return "CRAM-MD5", false
99}
100
101func (a *clientCRAMMD5) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
102 defer func() { a.step++ }()
103 switch a.step {
104 case 0:
105 return nil, false, nil
106 case 1:
107 // Validate the challenge.
108 // ../rfc/2195:82
109 s := string(fromServer)
110 if !strings.HasPrefix(s, "<") || !strings.HasSuffix(s, ">") {
111 return nil, false, fmt.Errorf("invalid challenge, missing angle brackets")
112 }
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")
116 }
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")
120 }
121
122 // ../rfc/2195:138
123 key := []byte(a.Password)
124 if len(key) > 64 {
125 t := md5.Sum(key)
126 key = t[:]
127 }
128 ipad := make([]byte, md5.BlockSize)
129 opad := make([]byte, md5.BlockSize)
130 copy(ipad, key)
131 copy(opad, key)
132 for i := range ipad {
133 ipad[i] ^= 0x36
134 opad[i] ^= 0x5c
135 }
136 ipadh := md5.New()
137 ipadh.Write(ipad)
138 ipadh.Write([]byte(fromServer))
139
140 opadh := md5.New()
141 opadh.Write(opad)
142 opadh.Write(ipadh.Sum(nil))
143
144 // ../rfc/2195:88
145 return []byte(fmt.Sprintf("%s %x", a.Username, opadh.Sum(nil))), true, nil
146
147 default:
148 return nil, false, fmt.Errorf("invalid step %d", a.step)
149 }
150}
151
152type clientSCRAMSHA struct {
153 Username, Password string
154
155 name string
156 step int
157 scram *scram.Client
158}
159
160var _ Client = (*clientSCRAMSHA)(nil)
161
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}
165}
166
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}
170}
171
172func (a *clientSCRAMSHA) Info() (name string, hasCleartextCredentials bool) {
173 return a.name, false
174}
175
176func (a *clientSCRAMSHA) Next(fromServer []byte) (toServer []byte, last bool, rerr error) {
177 defer func() { a.step++ }()
178 switch a.step {
179 case 0:
180 var h func() hash.Hash
181 switch a.name {
182 case "SCRAM-SHA-1":
183 h = sha1.New
184 case "SCRAM-SHA-256":
185 h = sha256.New
186 default:
187 return nil, false, fmt.Errorf("invalid SCRAM-SHA variant %q", a.name)
188 }
189
190 a.scram = scram.NewClient(h, a.Username, "")
191 toserver, err := a.scram.ClientFirst()
192 return []byte(toserver), false, err
193
194 case 1:
195 clientFinal, err := a.scram.ServerFirst(fromServer, a.Password)
196 return []byte(clientFinal), false, err
197
198 case 2:
199 err := a.scram.ServerFinal(fromServer)
200 return nil, true, err
201
202 default:
203 return nil, false, fmt.Errorf("invalid step %d", a.step)
204 }
205}
206