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.
|
|
|
|
// Package 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 occurred, 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
|
|
}
|
|
|