taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

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 }