Skip to content

Commit 51e2106

Browse files
raz-varrenmaddyblue
authored andcommitted
Add SCRAM-SHA-256 authentication to this library (#833)
Add SCRAM-SHA-256 authentication to this library
1 parent d6156e1 commit 51e2106

File tree

2 files changed

+315
-1
lines changed

2 files changed

+315
-1
lines changed

conn.go

+51-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"context"
66
"crypto/md5"
7+
"crypto/sha256"
78
"database/sql"
89
"database/sql/driver"
910
"encoding/binary"
@@ -21,6 +22,7 @@ import (
2122
"unicode"
2223

2324
"github.com/lib/pq/oid"
25+
"github.com/lib/pq/scram"
2426
)
2527

2628
// Common error types
@@ -958,7 +960,6 @@ func (cn *conn) recv() (t byte, r *readBuf) {
958960
if err != nil {
959961
panic(err)
960962
}
961-
962963
switch t {
963964
case 'E':
964965
panic(parseError(r))
@@ -1129,6 +1130,55 @@ func (cn *conn) auth(r *readBuf, o values) {
11291130
if r.int32() != 0 {
11301131
errorf("unexpected authentication response: %q", t)
11311132
}
1133+
case 10:
1134+
sc := scram.NewClient(sha256.New, o["user"], o["password"])
1135+
sc.Step(nil)
1136+
if sc.Err() != nil {
1137+
errorf("SCRAM-SHA-256 error: %s", sc.Err().Error())
1138+
}
1139+
scOut := sc.Out()
1140+
1141+
w := cn.writeBuf('p')
1142+
w.string("SCRAM-SHA-256")
1143+
w.int32(len(scOut))
1144+
w.bytes(scOut)
1145+
cn.send(w)
1146+
1147+
t, r := cn.recv()
1148+
if t != 'R' {
1149+
errorf("unexpected password response: %q", t)
1150+
}
1151+
1152+
if r.int32() != 11 {
1153+
errorf("unexpected authentication response: %q", t)
1154+
}
1155+
1156+
nextStep := r.next(len(*r))
1157+
sc.Step(nextStep)
1158+
if sc.Err() != nil {
1159+
errorf("SCRAM-SHA-256 error: %s", sc.Err().Error())
1160+
}
1161+
1162+
scOut = sc.Out()
1163+
w = cn.writeBuf('p')
1164+
w.bytes(scOut)
1165+
cn.send(w)
1166+
1167+
t, r = cn.recv()
1168+
if t != 'R' {
1169+
errorf("unexpected password response: %q", t)
1170+
}
1171+
1172+
if r.int32() != 12 {
1173+
errorf("unexpected authentication response: %q", t)
1174+
}
1175+
1176+
nextStep = r.next(len(*r))
1177+
sc.Step(nextStep)
1178+
if sc.Err() != nil {
1179+
errorf("SCRAM-SHA-256 error: %s", sc.Err().Error())
1180+
}
1181+
11321182
default:
11331183
errorf("unknown authentication response: %d", code)
11341184
}

scram/scram.go

+264
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
// Copyright (c) 2014 - Gustavo Niemeyer <[email protected]>
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+
// Pacakage 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 ocurred, 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+
}

0 commit comments

Comments
 (0)