scram.go (7659B)
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 package scram 29 30 import ( 31 "bytes" 32 "crypto/hmac" 33 "crypto/rand" 34 "encoding/base64" 35 "fmt" 36 "hash" 37 "strconv" 38 "strings" 39 ) 40 41 // Client implements a SCRAM-* client (SCRAM-SHA-1, SCRAM-SHA-256, etc). 42 // 43 // A Client may be used within a SASL conversation with logic resembling: 44 // 45 // var in []byte 46 // var client = scram.NewClient(sha1.New, user, pass) 47 // for client.Step(in) { 48 // out := client.Out() 49 // // send out to server 50 // in := serverOut 51 // } 52 // if client.Err() != nil { 53 // // auth failed 54 // } 55 type Client struct { 56 newHash func() hash.Hash 57 58 user string 59 pass string 60 step int 61 out bytes.Buffer 62 err error 63 64 clientNonce []byte 65 serverNonce []byte 66 saltedPass []byte 67 authMsg bytes.Buffer 68 } 69 70 // NewClient returns a new SCRAM-* client with the provided hash algorithm. 71 // 72 // For SCRAM-SHA-256, for example, use: 73 // 74 // client := scram.NewClient(sha256.New, user, pass) 75 func NewClient(newHash func() hash.Hash, user, pass string) *Client { 76 c := &Client{ 77 newHash: newHash, 78 user: user, 79 pass: pass, 80 } 81 c.out.Grow(256) 82 c.authMsg.Grow(256) 83 return c 84 } 85 86 // Out returns the data to be sent to the server in the current step. 87 func (c *Client) Out() []byte { 88 if c.out.Len() == 0 { 89 return nil 90 } 91 return c.out.Bytes() 92 } 93 94 // Err returns the error that occurred, or nil if there were no errors. 95 func (c *Client) Err() error { 96 return c.err 97 } 98 99 // SetNonce sets the client nonce to the provided value. 100 // If not set, the nonce is generated automatically out of crypto/rand on the first step. 101 func (c *Client) SetNonce(nonce []byte) { 102 c.clientNonce = nonce 103 } 104 105 var escaper = strings.NewReplacer("=", "=3D", ",", "=2C") 106 107 // Step processes the incoming data from the server and makes the 108 // next round of data for the server available via Client.Out. 109 // Step returns false if there are no errors and more data is 110 // still expected. 111 func (c *Client) Step(in []byte) bool { 112 c.out.Reset() 113 if c.step > 2 || c.err != nil { 114 return false 115 } 116 c.step++ 117 switch c.step { 118 case 1: 119 c.err = c.step1(in) 120 case 2: 121 c.err = c.step2(in) 122 case 3: 123 c.err = c.step3(in) 124 } 125 return c.step > 2 || c.err != nil 126 } 127 128 func (c *Client) step1(in []byte) error { 129 if len(c.clientNonce) == 0 { 130 const nonceLen = 16 131 buf := make([]byte, nonceLen+b64.EncodedLen(nonceLen)) 132 if _, err := rand.Read(buf[:nonceLen]); err != nil { 133 return fmt.Errorf("cannot read random SCRAM-SHA-256 nonce from operating system: %w", err) 134 } 135 c.clientNonce = buf[nonceLen:] 136 b64.Encode(c.clientNonce, buf[:nonceLen]) 137 } 138 c.authMsg.WriteString("n=") 139 escaper.WriteString(&c.authMsg, c.user) 140 c.authMsg.WriteString(",r=") 141 c.authMsg.Write(c.clientNonce) 142 143 c.out.WriteString("n,,") 144 c.out.Write(c.authMsg.Bytes()) 145 return nil 146 } 147 148 var b64 = base64.StdEncoding 149 150 func (c *Client) step2(in []byte) error { 151 c.authMsg.WriteByte(',') 152 c.authMsg.Write(in) 153 154 fields := bytes.Split(in, []byte(",")) 155 if len(fields) != 3 { 156 return fmt.Errorf("expected 3 fields in first SCRAM-SHA-256 server message, got %d: %q", len(fields), in) 157 } 158 if !bytes.HasPrefix(fields[0], []byte("r=")) || len(fields[0]) < 2 { 159 return fmt.Errorf("server sent an invalid SCRAM-SHA-256 nonce: %q", fields[0]) 160 } 161 if !bytes.HasPrefix(fields[1], []byte("s=")) || len(fields[1]) < 6 { 162 return fmt.Errorf("server sent an invalid SCRAM-SHA-256 salt: %q", fields[1]) 163 } 164 if !bytes.HasPrefix(fields[2], []byte("i=")) || len(fields[2]) < 6 { 165 return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2]) 166 } 167 168 c.serverNonce = fields[0][2:] 169 if !bytes.HasPrefix(c.serverNonce, c.clientNonce) { 170 return fmt.Errorf("server SCRAM-SHA-256 nonce is not prefixed by client nonce: got %q, want %q+\"...\"", c.serverNonce, c.clientNonce) 171 } 172 173 salt := make([]byte, b64.DecodedLen(len(fields[1][2:]))) 174 n, err := b64.Decode(salt, fields[1][2:]) 175 if err != nil { 176 return fmt.Errorf("cannot decode SCRAM-SHA-256 salt sent by server: %q", fields[1]) 177 } 178 salt = salt[:n] 179 iterCount, err := strconv.Atoi(string(fields[2][2:])) 180 if err != nil { 181 return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2]) 182 } 183 c.saltPassword(salt, iterCount) 184 185 c.authMsg.WriteString(",c=biws,r=") 186 c.authMsg.Write(c.serverNonce) 187 188 c.out.WriteString("c=biws,r=") 189 c.out.Write(c.serverNonce) 190 c.out.WriteString(",p=") 191 c.out.Write(c.clientProof()) 192 return nil 193 } 194 195 func (c *Client) step3(in []byte) error { 196 var isv, ise bool 197 var fields = bytes.Split(in, []byte(",")) 198 if len(fields) == 1 { 199 isv = bytes.HasPrefix(fields[0], []byte("v=")) 200 ise = bytes.HasPrefix(fields[0], []byte("e=")) 201 } 202 if ise { 203 return fmt.Errorf("SCRAM-SHA-256 authentication error: %s", fields[0][2:]) 204 } else if !isv { 205 return fmt.Errorf("unsupported SCRAM-SHA-256 final message from server: %q", in) 206 } 207 if !bytes.Equal(c.serverSignature(), fields[0][2:]) { 208 return fmt.Errorf("cannot authenticate SCRAM-SHA-256 server signature: %q", fields[0][2:]) 209 } 210 return nil 211 } 212 213 func (c *Client) saltPassword(salt []byte, iterCount int) { 214 mac := hmac.New(c.newHash, []byte(c.pass)) 215 mac.Write(salt) 216 mac.Write([]byte{0, 0, 0, 1}) 217 ui := mac.Sum(nil) 218 hi := make([]byte, len(ui)) 219 copy(hi, ui) 220 for i := 1; i < iterCount; i++ { 221 mac.Reset() 222 mac.Write(ui) 223 mac.Sum(ui[:0]) 224 for j, b := range ui { 225 hi[j] ^= b 226 } 227 } 228 c.saltedPass = hi 229 } 230 231 func (c *Client) clientProof() []byte { 232 mac := hmac.New(c.newHash, c.saltedPass) 233 mac.Write([]byte("Client Key")) 234 clientKey := mac.Sum(nil) 235 hash := c.newHash() 236 hash.Write(clientKey) 237 storedKey := hash.Sum(nil) 238 mac = hmac.New(c.newHash, storedKey) 239 mac.Write(c.authMsg.Bytes()) 240 clientProof := mac.Sum(nil) 241 for i, b := range clientKey { 242 clientProof[i] ^= b 243 } 244 clientProof64 := make([]byte, b64.EncodedLen(len(clientProof))) 245 b64.Encode(clientProof64, clientProof) 246 return clientProof64 247 } 248 249 func (c *Client) serverSignature() []byte { 250 mac := hmac.New(c.newHash, c.saltedPass) 251 mac.Write([]byte("Server Key")) 252 serverKey := mac.Sum(nil) 253 254 mac = hmac.New(c.newHash, serverKey) 255 mac.Write(c.authMsg.Bytes()) 256 serverSignature := mac.Sum(nil) 257 258 encoded := make([]byte, b64.EncodedLen(len(serverSignature))) 259 b64.Encode(encoded, serverSignature) 260 return encoded 261 }