blob: ca0c4c711c6ca4631b55823c2e90b10ab108cac6 [file] [log] [blame]
Don Newton379ae252019-04-01 12:17:06 -04001// Copyright 2018 by David A. Golden. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may
4// not use this file except in compliance with the License. You may obtain
5// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
7package scram
8
9import (
10 "sync"
11
12 "golang.org/x/crypto/pbkdf2"
13)
14
15// Client implements the client side of SCRAM authentication. It holds
16// configuration values needed to initialize new client-side conversations for
17// a specific username, password and authorization ID tuple. Client caches
18// the computationally-expensive parts of a SCRAM conversation as described in
19// RFC-5802. If repeated authentication conversations may be required for a
20// user (e.g. disconnect/reconnect), the user's Client should be preserved.
21//
22// For security reasons, Clients have a default minimum PBKDF2 iteration count
23// of 4096. If a server requests a smaller iteration count, an authentication
24// conversation will error.
25//
26// A Client can also be used by a server application to construct the hashed
27// authentication values to be stored for a new user. See StoredCredentials()
28// for more.
29type Client struct {
30 sync.RWMutex
31 username string
32 password string
33 authzID string
34 minIters int
35 nonceGen NonceGeneratorFcn
36 hashGen HashGeneratorFcn
37 cache map[KeyFactors]derivedKeys
38}
39
40func newClient(username, password, authzID string, fcn HashGeneratorFcn) *Client {
41 return &Client{
42 username: username,
43 password: password,
44 authzID: authzID,
45 minIters: 4096,
46 nonceGen: defaultNonceGenerator,
47 hashGen: fcn,
48 cache: make(map[KeyFactors]derivedKeys),
49 }
50}
51
52// WithMinIterations changes minimum required PBKDF2 iteration count.
53func (c *Client) WithMinIterations(n int) *Client {
54 c.Lock()
55 defer c.Unlock()
56 c.minIters = n
57 return c
58}
59
60// WithNonceGenerator replaces the default nonce generator (base64 encoding of
61// 24 bytes from crypto/rand) with a custom generator. This is provided for
62// testing or for users with custom nonce requirements.
63func (c *Client) WithNonceGenerator(ng NonceGeneratorFcn) *Client {
64 c.Lock()
65 defer c.Unlock()
66 c.nonceGen = ng
67 return c
68}
69
70// NewConversation constructs a client-side authentication conversation.
71// Conversations cannot be reused, so this must be called for each new
72// authentication attempt.
73func (c *Client) NewConversation() *ClientConversation {
74 c.RLock()
75 defer c.RUnlock()
76 return &ClientConversation{
77 client: c,
78 nonceGen: c.nonceGen,
79 hashGen: c.hashGen,
80 minIters: c.minIters,
81 }
82}
83
84func (c *Client) getDerivedKeys(kf KeyFactors) derivedKeys {
85 dk, ok := c.getCache(kf)
86 if !ok {
87 dk = c.computeKeys(kf)
88 c.setCache(kf, dk)
89 }
90 return dk
91}
92
93// GetStoredCredentials takes a salt and iteration count structure and
94// provides the values that must be stored by a server to authentication a
95// user. These values are what the Server credential lookup function must
96// return for a given username.
97func (c *Client) GetStoredCredentials(kf KeyFactors) StoredCredentials {
98 dk := c.getDerivedKeys(kf)
99 return StoredCredentials{
100 KeyFactors: kf,
101 StoredKey: dk.StoredKey,
102 ServerKey: dk.ServerKey,
103 }
104}
105
106func (c *Client) computeKeys(kf KeyFactors) derivedKeys {
107 h := c.hashGen()
108 saltedPassword := pbkdf2.Key([]byte(c.password), []byte(kf.Salt), kf.Iters, h.Size(), c.hashGen)
109 clientKey := computeHMAC(c.hashGen, saltedPassword, []byte("Client Key"))
110
111 return derivedKeys{
112 ClientKey: clientKey,
113 StoredKey: computeHash(c.hashGen, clientKey),
114 ServerKey: computeHMAC(c.hashGen, saltedPassword, []byte("Server Key")),
115 }
116}
117
118func (c *Client) getCache(kf KeyFactors) (derivedKeys, bool) {
119 c.RLock()
120 defer c.RUnlock()
121 dk, ok := c.cache[kf]
122 return dk, ok
123}
124
125func (c *Client) setCache(kf KeyFactors, dk derivedKeys) {
126 c.Lock()
127 defer c.Unlock()
128 c.cache[kf] = dk
129 return
130}