blob: 9c8838c38aea61e36a9f54ee36db0f2c40a6477f [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 "crypto/hmac"
11 "encoding/base64"
12 "errors"
13 "fmt"
14)
15
16type serverState int
17
18const (
19 serverFirst serverState = iota
20 serverFinal
21 serverDone
22)
23
24// ServerConversation implements the server-side of an authentication
25// conversation with a client. A new conversation must be created for
26// each authentication attempt.
27type ServerConversation struct {
28 nonceGen NonceGeneratorFcn
29 hashGen HashGeneratorFcn
30 credentialCB CredentialLookup
31 state serverState
32 credential StoredCredentials
33 valid bool
34 gs2Header string
35 username string
36 authzID string
37 nonce string
38 c1b string
39 s1 string
40}
41
42// Step takes a string provided from a client and attempts to move the
43// authentication conversation forward. It returns a string to be sent to the
44// client or an error if the client message is invalid. Calling Step after a
45// conversation completes is also an error.
46func (sc *ServerConversation) Step(challenge string) (response string, err error) {
47 switch sc.state {
48 case serverFirst:
49 sc.state = serverFinal
50 response, err = sc.firstMsg(challenge)
51 case serverFinal:
52 sc.state = serverDone
53 response, err = sc.finalMsg(challenge)
54 default:
55 response, err = "", errors.New("Conversation already completed")
56 }
57 return
58}
59
60// Done returns true if the conversation is completed or has errored.
61func (sc *ServerConversation) Done() bool {
62 return sc.state == serverDone
63}
64
65// Valid returns true if the conversation successfully authenticated the
66// client.
67func (sc *ServerConversation) Valid() bool {
68 return sc.valid
69}
70
71// Username returns the client-provided username. This is valid to call
72// if the first conversation Step() is successful.
73func (sc *ServerConversation) Username() string {
74 return sc.username
75}
76
77// AuthzID returns the (optional) client-provided authorization identity, if
78// any. If one was not provided, it returns the empty string. This is valid
79// to call if the first conversation Step() is successful.
80func (sc *ServerConversation) AuthzID() string {
81 return sc.authzID
82}
83
84func (sc *ServerConversation) firstMsg(c1 string) (string, error) {
85 msg, err := parseClientFirst(c1)
86 if err != nil {
87 sc.state = serverDone
88 return "", err
89 }
90
91 sc.gs2Header = msg.gs2Header
92 sc.username = msg.username
93 sc.authzID = msg.authzID
94
95 sc.credential, err = sc.credentialCB(msg.username)
96 if err != nil {
97 sc.state = serverDone
98 return "e=unknown-user", err
99 }
100
101 sc.nonce = msg.nonce + sc.nonceGen()
102 sc.c1b = msg.c1b
103 sc.s1 = fmt.Sprintf("r=%s,s=%s,i=%d",
104 sc.nonce,
105 base64.StdEncoding.EncodeToString([]byte(sc.credential.Salt)),
106 sc.credential.Iters,
107 )
108
109 return sc.s1, nil
110}
111
112// For errors, returns server error message as well as non-nil error. Callers
113// can choose whether to send server error or not.
114func (sc *ServerConversation) finalMsg(c2 string) (string, error) {
115 msg, err := parseClientFinal(c2)
116 if err != nil {
117 return "", err
118 }
119
120 // Check channel binding matches what we expect; in this case, we expect
121 // just the gs2 header we received as we don't support channel binding
122 // with a data payload. If we add binding, we need to independently
123 // compute the header to match here.
124 if string(msg.cbind) != sc.gs2Header {
125 return "e=channel-bindings-dont-match", fmt.Errorf("channel binding received '%s' doesn't match expected '%s'", msg.cbind, sc.gs2Header)
126 }
127
128 // Check nonce received matches what we sent
129 if msg.nonce != sc.nonce {
130 return "e=other-error", errors.New("nonce received did not match nonce sent")
131 }
132
133 // Create auth message
134 authMsg := sc.c1b + "," + sc.s1 + "," + msg.c2wop
135
136 // Retrieve ClientKey from proof and verify it
137 clientSignature := computeHMAC(sc.hashGen, sc.credential.StoredKey, []byte(authMsg))
138 clientKey := xorBytes([]byte(msg.proof), clientSignature)
139 storedKey := computeHash(sc.hashGen, clientKey)
140
141 // Compare with constant-time function
142 if !hmac.Equal(storedKey, sc.credential.StoredKey) {
143 return "e=invalid-proof", errors.New("challenge proof invalid")
144 }
145
146 sc.valid = true
147
148 // Compute and return server verifier
149 serverSignature := computeHMAC(sc.hashGen, sc.credential.ServerKey, []byte(authMsg))
150 return "v=" + base64.StdEncoding.EncodeToString(serverSignature), nil
151}