1 | // Copyright (c) 2014 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
---|
2 | //
|
---|
3 | // All rights reserved.
|
---|
4 | //
|
---|
5 | // Redistribution and use in source and binary forms, with or without
|
---|
6 | // modification, are permitted provided that the following conditions are met:
|
---|
7 | //
|
---|
8 | // 1. Redistributions of source code must retain the above copyright notice, this
|
---|
9 | // list of conditions and the following disclaimer.
|
---|
10 | // 2. Redistributions in binary form must reproduce the above copyright notice,
|
---|
11 | // this list of conditions and the following disclaimer in the documentation
|
---|
12 | // and/or other materials provided with the distribution.
|
---|
13 | //
|
---|
14 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
---|
15 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
---|
16 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
---|
17 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
---|
18 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
---|
19 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
---|
20 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
---|
21 | // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
---|
22 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
---|
23 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
---|
24 |
|
---|
25 | // Package scram implements a SCRAM-{SHA-1,etc} client per RFC5802.
|
---|
26 | //
|
---|
27 | // http://tools.ietf.org/html/rfc5802
|
---|
28 | //
|
---|
29 | package scram
|
---|
30 |
|
---|
31 | import (
|
---|
32 | "bytes"
|
---|
33 | "crypto/hmac"
|
---|
34 | "crypto/rand"
|
---|
35 | "encoding/base64"
|
---|
36 | "fmt"
|
---|
37 | "hash"
|
---|
38 | "strconv"
|
---|
39 | "strings"
|
---|
40 | )
|
---|
41 |
|
---|
42 | // Client implements a SCRAM-* client (SCRAM-SHA-1, SCRAM-SHA-256, etc).
|
---|
43 | //
|
---|
44 | // A Client may be used within a SASL conversation with logic resembling:
|
---|
45 | //
|
---|
46 | // var in []byte
|
---|
47 | // var client = scram.NewClient(sha1.New, user, pass)
|
---|
48 | // for client.Step(in) {
|
---|
49 | // out := client.Out()
|
---|
50 | // // send out to server
|
---|
51 | // in := serverOut
|
---|
52 | // }
|
---|
53 | // if client.Err() != nil {
|
---|
54 | // // auth failed
|
---|
55 | // }
|
---|
56 | //
|
---|
57 | type Client struct {
|
---|
58 | newHash func() hash.Hash
|
---|
59 |
|
---|
60 | user string
|
---|
61 | pass string
|
---|
62 | step int
|
---|
63 | out bytes.Buffer
|
---|
64 | err error
|
---|
65 |
|
---|
66 | clientNonce []byte
|
---|
67 | serverNonce []byte
|
---|
68 | saltedPass []byte
|
---|
69 | authMsg bytes.Buffer
|
---|
70 | }
|
---|
71 |
|
---|
72 | // NewClient returns a new SCRAM-* client with the provided hash algorithm.
|
---|
73 | //
|
---|
74 | // For SCRAM-SHA-256, for example, use:
|
---|
75 | //
|
---|
76 | // client := scram.NewClient(sha256.New, user, pass)
|
---|
77 | //
|
---|
78 | func NewClient(newHash func() hash.Hash, user, pass string) *Client {
|
---|
79 | c := &Client{
|
---|
80 | newHash: newHash,
|
---|
81 | user: user,
|
---|
82 | pass: pass,
|
---|
83 | }
|
---|
84 | c.out.Grow(256)
|
---|
85 | c.authMsg.Grow(256)
|
---|
86 | return c
|
---|
87 | }
|
---|
88 |
|
---|
89 | // Out returns the data to be sent to the server in the current step.
|
---|
90 | func (c *Client) Out() []byte {
|
---|
91 | if c.out.Len() == 0 {
|
---|
92 | return nil
|
---|
93 | }
|
---|
94 | return c.out.Bytes()
|
---|
95 | }
|
---|
96 |
|
---|
97 | // Err returns the error that occurred, or nil if there were no errors.
|
---|
98 | func (c *Client) Err() error {
|
---|
99 | return c.err
|
---|
100 | }
|
---|
101 |
|
---|
102 | // SetNonce sets the client nonce to the provided value.
|
---|
103 | // If not set, the nonce is generated automatically out of crypto/rand on the first step.
|
---|
104 | func (c *Client) SetNonce(nonce []byte) {
|
---|
105 | c.clientNonce = nonce
|
---|
106 | }
|
---|
107 |
|
---|
108 | var escaper = strings.NewReplacer("=", "=3D", ",", "=2C")
|
---|
109 |
|
---|
110 | // Step processes the incoming data from the server and makes the
|
---|
111 | // next round of data for the server available via Client.Out.
|
---|
112 | // Step returns false if there are no errors and more data is
|
---|
113 | // still expected.
|
---|
114 | func (c *Client) Step(in []byte) bool {
|
---|
115 | c.out.Reset()
|
---|
116 | if c.step > 2 || c.err != nil {
|
---|
117 | return false
|
---|
118 | }
|
---|
119 | c.step++
|
---|
120 | switch c.step {
|
---|
121 | case 1:
|
---|
122 | c.err = c.step1(in)
|
---|
123 | case 2:
|
---|
124 | c.err = c.step2(in)
|
---|
125 | case 3:
|
---|
126 | c.err = c.step3(in)
|
---|
127 | }
|
---|
128 | return c.step > 2 || c.err != nil
|
---|
129 | }
|
---|
130 |
|
---|
131 | func (c *Client) step1(in []byte) error {
|
---|
132 | if len(c.clientNonce) == 0 {
|
---|
133 | const nonceLen = 16
|
---|
134 | buf := make([]byte, nonceLen+b64.EncodedLen(nonceLen))
|
---|
135 | if _, err := rand.Read(buf[:nonceLen]); err != nil {
|
---|
136 | return fmt.Errorf("cannot read random SCRAM-SHA-256 nonce from operating system: %v", err)
|
---|
137 | }
|
---|
138 | c.clientNonce = buf[nonceLen:]
|
---|
139 | b64.Encode(c.clientNonce, buf[:nonceLen])
|
---|
140 | }
|
---|
141 | c.authMsg.WriteString("n=")
|
---|
142 | escaper.WriteString(&c.authMsg, c.user)
|
---|
143 | c.authMsg.WriteString(",r=")
|
---|
144 | c.authMsg.Write(c.clientNonce)
|
---|
145 |
|
---|
146 | c.out.WriteString("n,,")
|
---|
147 | c.out.Write(c.authMsg.Bytes())
|
---|
148 | return nil
|
---|
149 | }
|
---|
150 |
|
---|
151 | var b64 = base64.StdEncoding
|
---|
152 |
|
---|
153 | func (c *Client) step2(in []byte) error {
|
---|
154 | c.authMsg.WriteByte(',')
|
---|
155 | c.authMsg.Write(in)
|
---|
156 |
|
---|
157 | fields := bytes.Split(in, []byte(","))
|
---|
158 | if len(fields) != 3 {
|
---|
159 | return fmt.Errorf("expected 3 fields in first SCRAM-SHA-256 server message, got %d: %q", len(fields), in)
|
---|
160 | }
|
---|
161 | if !bytes.HasPrefix(fields[0], []byte("r=")) || len(fields[0]) < 2 {
|
---|
162 | return fmt.Errorf("server sent an invalid SCRAM-SHA-256 nonce: %q", fields[0])
|
---|
163 | }
|
---|
164 | if !bytes.HasPrefix(fields[1], []byte("s=")) || len(fields[1]) < 6 {
|
---|
165 | return fmt.Errorf("server sent an invalid SCRAM-SHA-256 salt: %q", fields[1])
|
---|
166 | }
|
---|
167 | if !bytes.HasPrefix(fields[2], []byte("i=")) || len(fields[2]) < 6 {
|
---|
168 | return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2])
|
---|
169 | }
|
---|
170 |
|
---|
171 | c.serverNonce = fields[0][2:]
|
---|
172 | if !bytes.HasPrefix(c.serverNonce, c.clientNonce) {
|
---|
173 | return fmt.Errorf("server SCRAM-SHA-256 nonce is not prefixed by client nonce: got %q, want %q+\"...\"", c.serverNonce, c.clientNonce)
|
---|
174 | }
|
---|
175 |
|
---|
176 | salt := make([]byte, b64.DecodedLen(len(fields[1][2:])))
|
---|
177 | n, err := b64.Decode(salt, fields[1][2:])
|
---|
178 | if err != nil {
|
---|
179 | return fmt.Errorf("cannot decode SCRAM-SHA-256 salt sent by server: %q", fields[1])
|
---|
180 | }
|
---|
181 | salt = salt[:n]
|
---|
182 | iterCount, err := strconv.Atoi(string(fields[2][2:]))
|
---|
183 | if err != nil {
|
---|
184 | return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2])
|
---|
185 | }
|
---|
186 | c.saltPassword(salt, iterCount)
|
---|
187 |
|
---|
188 | c.authMsg.WriteString(",c=biws,r=")
|
---|
189 | c.authMsg.Write(c.serverNonce)
|
---|
190 |
|
---|
191 | c.out.WriteString("c=biws,r=")
|
---|
192 | c.out.Write(c.serverNonce)
|
---|
193 | c.out.WriteString(",p=")
|
---|
194 | c.out.Write(c.clientProof())
|
---|
195 | return nil
|
---|
196 | }
|
---|
197 |
|
---|
198 | func (c *Client) step3(in []byte) error {
|
---|
199 | var isv, ise bool
|
---|
200 | var fields = bytes.Split(in, []byte(","))
|
---|
201 | if len(fields) == 1 {
|
---|
202 | isv = bytes.HasPrefix(fields[0], []byte("v="))
|
---|
203 | ise = bytes.HasPrefix(fields[0], []byte("e="))
|
---|
204 | }
|
---|
205 | if ise {
|
---|
206 | return fmt.Errorf("SCRAM-SHA-256 authentication error: %s", fields[0][2:])
|
---|
207 | } else if !isv {
|
---|
208 | return fmt.Errorf("unsupported SCRAM-SHA-256 final message from server: %q", in)
|
---|
209 | }
|
---|
210 | if !bytes.Equal(c.serverSignature(), fields[0][2:]) {
|
---|
211 | return fmt.Errorf("cannot authenticate SCRAM-SHA-256 server signature: %q", fields[0][2:])
|
---|
212 | }
|
---|
213 | return nil
|
---|
214 | }
|
---|
215 |
|
---|
216 | func (c *Client) saltPassword(salt []byte, iterCount int) {
|
---|
217 | mac := hmac.New(c.newHash, []byte(c.pass))
|
---|
218 | mac.Write(salt)
|
---|
219 | mac.Write([]byte{0, 0, 0, 1})
|
---|
220 | ui := mac.Sum(nil)
|
---|
221 | hi := make([]byte, len(ui))
|
---|
222 | copy(hi, ui)
|
---|
223 | for i := 1; i < iterCount; i++ {
|
---|
224 | mac.Reset()
|
---|
225 | mac.Write(ui)
|
---|
226 | mac.Sum(ui[:0])
|
---|
227 | for j, b := range ui {
|
---|
228 | hi[j] ^= b
|
---|
229 | }
|
---|
230 | }
|
---|
231 | c.saltedPass = hi
|
---|
232 | }
|
---|
233 |
|
---|
234 | func (c *Client) clientProof() []byte {
|
---|
235 | mac := hmac.New(c.newHash, c.saltedPass)
|
---|
236 | mac.Write([]byte("Client Key"))
|
---|
237 | clientKey := mac.Sum(nil)
|
---|
238 | hash := c.newHash()
|
---|
239 | hash.Write(clientKey)
|
---|
240 | storedKey := hash.Sum(nil)
|
---|
241 | mac = hmac.New(c.newHash, storedKey)
|
---|
242 | mac.Write(c.authMsg.Bytes())
|
---|
243 | clientProof := mac.Sum(nil)
|
---|
244 | for i, b := range clientKey {
|
---|
245 | clientProof[i] ^= b
|
---|
246 | }
|
---|
247 | clientProof64 := make([]byte, b64.EncodedLen(len(clientProof)))
|
---|
248 | b64.Encode(clientProof64, clientProof)
|
---|
249 | return clientProof64
|
---|
250 | }
|
---|
251 |
|
---|
252 | func (c *Client) serverSignature() []byte {
|
---|
253 | mac := hmac.New(c.newHash, c.saltedPass)
|
---|
254 | mac.Write([]byte("Server Key"))
|
---|
255 | serverKey := mac.Sum(nil)
|
---|
256 |
|
---|
257 | mac = hmac.New(c.newHash, serverKey)
|
---|
258 | mac.Write(c.authMsg.Bytes())
|
---|
259 | serverSignature := mac.Sum(nil)
|
---|
260 |
|
---|
261 | encoded := make([]byte, b64.EncodedLen(len(serverSignature)))
|
---|
262 | b64.Encode(encoded, serverSignature)
|
---|
263 | return encoded
|
---|
264 | }
|
---|