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