You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							264 lines
						
					
					
						
							7.5 KiB
						
					
					
				
			
		
		
	
	
							264 lines
						
					
					
						
							7.5 KiB
						
					
					
				| // Copyright (c) 2014 - Gustavo Niemeyer <gustavo@niemeyer.net>
 | |
| //
 | |
| // All rights reserved.
 | |
| //
 | |
| // Redistribution and use in source and binary forms, with or without
 | |
| // modification, are permitted provided that the following conditions are met:
 | |
| //
 | |
| // 1. Redistributions of source code must retain the above copyright notice, this
 | |
| //    list of conditions and the following disclaimer.
 | |
| // 2. Redistributions in binary form must reproduce the above copyright notice,
 | |
| //    this list of conditions and the following disclaimer in the documentation
 | |
| //    and/or other materials provided with the distribution.
 | |
| //
 | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 | |
| // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 | |
| // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 | |
| // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 | |
| // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 | |
| // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 | |
| // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 | |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 | |
| // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | |
| 
 | |
| // Pacakage scram implements a SCRAM-{SHA-1,etc} client per RFC5802.
 | |
| //
 | |
| // http://tools.ietf.org/html/rfc5802
 | |
| //
 | |
| package scram
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"crypto/hmac"
 | |
| 	"crypto/rand"
 | |
| 	"encoding/base64"
 | |
| 	"fmt"
 | |
| 	"hash"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| )
 | |
| 
 | |
| // Client implements a SCRAM-* client (SCRAM-SHA-1, SCRAM-SHA-256, etc).
 | |
| //
 | |
| // A Client may be used within a SASL conversation with logic resembling:
 | |
| //
 | |
| //    var in []byte
 | |
| //    var client = scram.NewClient(sha1.New, user, pass)
 | |
| //    for client.Step(in) {
 | |
| //            out := client.Out()
 | |
| //            // send out to server
 | |
| //            in := serverOut
 | |
| //    }
 | |
| //    if client.Err() != nil {
 | |
| //            // auth failed
 | |
| //    }
 | |
| //
 | |
| type Client struct {
 | |
| 	newHash func() hash.Hash
 | |
| 
 | |
| 	user string
 | |
| 	pass string
 | |
| 	step int
 | |
| 	out  bytes.Buffer
 | |
| 	err  error
 | |
| 
 | |
| 	clientNonce []byte
 | |
| 	serverNonce []byte
 | |
| 	saltedPass  []byte
 | |
| 	authMsg     bytes.Buffer
 | |
| }
 | |
| 
 | |
| // NewClient returns a new SCRAM-* client with the provided hash algorithm.
 | |
| //
 | |
| // For SCRAM-SHA-256, for example, use:
 | |
| //
 | |
| //    client := scram.NewClient(sha256.New, user, pass)
 | |
| //
 | |
| func NewClient(newHash func() hash.Hash, user, pass string) *Client {
 | |
| 	c := &Client{
 | |
| 		newHash: newHash,
 | |
| 		user:    user,
 | |
| 		pass:    pass,
 | |
| 	}
 | |
| 	c.out.Grow(256)
 | |
| 	c.authMsg.Grow(256)
 | |
| 	return c
 | |
| }
 | |
| 
 | |
| // Out returns the data to be sent to the server in the current step.
 | |
| func (c *Client) Out() []byte {
 | |
| 	if c.out.Len() == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 	return c.out.Bytes()
 | |
| }
 | |
| 
 | |
| // Err returns the error that ocurred, or nil if there were no errors.
 | |
| func (c *Client) Err() error {
 | |
| 	return c.err
 | |
| }
 | |
| 
 | |
| // SetNonce sets the client nonce to the provided value.
 | |
| // If not set, the nonce is generated automatically out of crypto/rand on the first step.
 | |
| func (c *Client) SetNonce(nonce []byte) {
 | |
| 	c.clientNonce = nonce
 | |
| }
 | |
| 
 | |
| var escaper = strings.NewReplacer("=", "=3D", ",", "=2C")
 | |
| 
 | |
| // Step processes the incoming data from the server and makes the
 | |
| // next round of data for the server available via Client.Out.
 | |
| // Step returns false if there are no errors and more data is
 | |
| // still expected.
 | |
| func (c *Client) Step(in []byte) bool {
 | |
| 	c.out.Reset()
 | |
| 	if c.step > 2 || c.err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	c.step++
 | |
| 	switch c.step {
 | |
| 	case 1:
 | |
| 		c.err = c.step1(in)
 | |
| 	case 2:
 | |
| 		c.err = c.step2(in)
 | |
| 	case 3:
 | |
| 		c.err = c.step3(in)
 | |
| 	}
 | |
| 	return c.step > 2 || c.err != nil
 | |
| }
 | |
| 
 | |
| func (c *Client) step1(in []byte) error {
 | |
| 	if len(c.clientNonce) == 0 {
 | |
| 		const nonceLen = 16
 | |
| 		buf := make([]byte, nonceLen+b64.EncodedLen(nonceLen))
 | |
| 		if _, err := rand.Read(buf[:nonceLen]); err != nil {
 | |
| 			return fmt.Errorf("cannot read random SCRAM-SHA-256 nonce from operating system: %v", err)
 | |
| 		}
 | |
| 		c.clientNonce = buf[nonceLen:]
 | |
| 		b64.Encode(c.clientNonce, buf[:nonceLen])
 | |
| 	}
 | |
| 	c.authMsg.WriteString("n=")
 | |
| 	escaper.WriteString(&c.authMsg, c.user)
 | |
| 	c.authMsg.WriteString(",r=")
 | |
| 	c.authMsg.Write(c.clientNonce)
 | |
| 
 | |
| 	c.out.WriteString("n,,")
 | |
| 	c.out.Write(c.authMsg.Bytes())
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var b64 = base64.StdEncoding
 | |
| 
 | |
| func (c *Client) step2(in []byte) error {
 | |
| 	c.authMsg.WriteByte(',')
 | |
| 	c.authMsg.Write(in)
 | |
| 
 | |
| 	fields := bytes.Split(in, []byte(","))
 | |
| 	if len(fields) != 3 {
 | |
| 		return fmt.Errorf("expected 3 fields in first SCRAM-SHA-256 server message, got %d: %q", len(fields), in)
 | |
| 	}
 | |
| 	if !bytes.HasPrefix(fields[0], []byte("r=")) || len(fields[0]) < 2 {
 | |
| 		return fmt.Errorf("server sent an invalid SCRAM-SHA-256 nonce: %q", fields[0])
 | |
| 	}
 | |
| 	if !bytes.HasPrefix(fields[1], []byte("s=")) || len(fields[1]) < 6 {
 | |
| 		return fmt.Errorf("server sent an invalid SCRAM-SHA-256 salt: %q", fields[1])
 | |
| 	}
 | |
| 	if !bytes.HasPrefix(fields[2], []byte("i=")) || len(fields[2]) < 6 {
 | |
| 		return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2])
 | |
| 	}
 | |
| 
 | |
| 	c.serverNonce = fields[0][2:]
 | |
| 	if !bytes.HasPrefix(c.serverNonce, c.clientNonce) {
 | |
| 		return fmt.Errorf("server SCRAM-SHA-256 nonce is not prefixed by client nonce: got %q, want %q+\"...\"", c.serverNonce, c.clientNonce)
 | |
| 	}
 | |
| 
 | |
| 	salt := make([]byte, b64.DecodedLen(len(fields[1][2:])))
 | |
| 	n, err := b64.Decode(salt, fields[1][2:])
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("cannot decode SCRAM-SHA-256 salt sent by server: %q", fields[1])
 | |
| 	}
 | |
| 	salt = salt[:n]
 | |
| 	iterCount, err := strconv.Atoi(string(fields[2][2:]))
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2])
 | |
| 	}
 | |
| 	c.saltPassword(salt, iterCount)
 | |
| 
 | |
| 	c.authMsg.WriteString(",c=biws,r=")
 | |
| 	c.authMsg.Write(c.serverNonce)
 | |
| 
 | |
| 	c.out.WriteString("c=biws,r=")
 | |
| 	c.out.Write(c.serverNonce)
 | |
| 	c.out.WriteString(",p=")
 | |
| 	c.out.Write(c.clientProof())
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (c *Client) step3(in []byte) error {
 | |
| 	var isv, ise bool
 | |
| 	var fields = bytes.Split(in, []byte(","))
 | |
| 	if len(fields) == 1 {
 | |
| 		isv = bytes.HasPrefix(fields[0], []byte("v="))
 | |
| 		ise = bytes.HasPrefix(fields[0], []byte("e="))
 | |
| 	}
 | |
| 	if ise {
 | |
| 		return fmt.Errorf("SCRAM-SHA-256 authentication error: %s", fields[0][2:])
 | |
| 	} else if !isv {
 | |
| 		return fmt.Errorf("unsupported SCRAM-SHA-256 final message from server: %q", in)
 | |
| 	}
 | |
| 	if !bytes.Equal(c.serverSignature(), fields[0][2:]) {
 | |
| 		return fmt.Errorf("cannot authenticate SCRAM-SHA-256 server signature: %q", fields[0][2:])
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (c *Client) saltPassword(salt []byte, iterCount int) {
 | |
| 	mac := hmac.New(c.newHash, []byte(c.pass))
 | |
| 	mac.Write(salt)
 | |
| 	mac.Write([]byte{0, 0, 0, 1})
 | |
| 	ui := mac.Sum(nil)
 | |
| 	hi := make([]byte, len(ui))
 | |
| 	copy(hi, ui)
 | |
| 	for i := 1; i < iterCount; i++ {
 | |
| 		mac.Reset()
 | |
| 		mac.Write(ui)
 | |
| 		mac.Sum(ui[:0])
 | |
| 		for j, b := range ui {
 | |
| 			hi[j] ^= b
 | |
| 		}
 | |
| 	}
 | |
| 	c.saltedPass = hi
 | |
| }
 | |
| 
 | |
| func (c *Client) clientProof() []byte {
 | |
| 	mac := hmac.New(c.newHash, c.saltedPass)
 | |
| 	mac.Write([]byte("Client Key"))
 | |
| 	clientKey := mac.Sum(nil)
 | |
| 	hash := c.newHash()
 | |
| 	hash.Write(clientKey)
 | |
| 	storedKey := hash.Sum(nil)
 | |
| 	mac = hmac.New(c.newHash, storedKey)
 | |
| 	mac.Write(c.authMsg.Bytes())
 | |
| 	clientProof := mac.Sum(nil)
 | |
| 	for i, b := range clientKey {
 | |
| 		clientProof[i] ^= b
 | |
| 	}
 | |
| 	clientProof64 := make([]byte, b64.EncodedLen(len(clientProof)))
 | |
| 	b64.Encode(clientProof64, clientProof)
 | |
| 	return clientProof64
 | |
| }
 | |
| 
 | |
| func (c *Client) serverSignature() []byte {
 | |
| 	mac := hmac.New(c.newHash, c.saltedPass)
 | |
| 	mac.Write([]byte("Server Key"))
 | |
| 	serverKey := mac.Sum(nil)
 | |
| 
 | |
| 	mac = hmac.New(c.newHash, serverKey)
 | |
| 	mac.Write(c.authMsg.Bytes())
 | |
| 	serverSignature := mac.Sum(nil)
 | |
| 
 | |
| 	encoded := make([]byte, b64.EncodedLen(len(serverSignature)))
 | |
| 	b64.Encode(encoded, serverSignature)
 | |
| 	return encoded
 | |
| }
 | |
| 
 |