-
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
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
// @author Couchbase <info@couchbase.com>
// @copyright 2018 Couchbase, Inc.
//
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package scramsha provides implementation of client side SCRAM-SHA
// via Http according to https://tools.ietf.org/html/rfc7804
package scramsha
import (
"encoding/base64"
"github.com/pkg/errors"
"io"
"io/ioutil"
"net/http"
"strings"
)
// consts used to parse scramsha response from target
const (
WWWAuthenticate = "WWW-Authenticate"
AuthenticationInfo = "Authentication-Info"
Authorization = "Authorization"
DataPrefix = "data="
SidPrefix = "sid="
)
// Request provides implementation of http request that can be retried
type Request struct {
body io.ReadSeeker
// Embed an HTTP request directly. This makes a *Request act exactly
// like an *http.Request so that all meta methods are supported.
*http.Request
}
type lenReader interface {
Len() int
}
// NewRequest creates http request that can be retried
func NewRequest(method, url string, body io.ReadSeeker) (*Request, error) {
// Wrap the body in a noop ReadCloser if non-nil. This prevents the
// reader from being closed by the HTTP client.
var rcBody io.ReadCloser
if body != nil {
rcBody = ioutil.NopCloser(body)
}
// Make the request with the noop-closer for the body.
httpReq, err := http.NewRequest(method, url, rcBody)
if err != nil {
return nil, err
}
// Check if we can set the Content-Length automatically.
if lr, ok := body.(lenReader); ok {
httpReq.ContentLength = int64(lr.Len())
}
return &Request{body, httpReq}, nil
}
func encode(str string) string {
return base64.StdEncoding.EncodeToString([]byte(str))
}
func decode(str string) (string, error) {
bytes, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return "", errors.Errorf("Cannot base64 decode %s",
str)
}
return string(bytes), err
}
func trimPrefix(s, prefix string) (string, error) {
l := len(s)
trimmed := strings.TrimPrefix(s, prefix)
if l == len(trimmed) {
return trimmed, errors.Errorf("Prefix %s not found in %s",
prefix, s)
}
return trimmed, nil
}
func drainBody(resp *http.Response) {
defer resp.Body.Close()
io.Copy(ioutil.Discard, resp.Body)
}
// DoScramSha performs SCRAM-SHA handshake via Http
func DoScramSha(req *Request,
username string,
password string,
client *http.Client) (*http.Response, error) {
method := "SCRAM-SHA-512"
s, err := NewScramSha("SCRAM-SHA512")
if err != nil {
return nil, errors.Wrap(err,
"Unable to initialize SCRAM-SHA handler")
}
message, err := s.GetStartRequest(username)
if err != nil {
return nil, err
}
encodedMessage := method + " " + DataPrefix + encode(message)
req.Header.Set(Authorization, encodedMessage)
res, err := client.Do(req.Request)
if err != nil {
return nil, errors.Wrap(err, "Problem sending SCRAM-SHA start"+
"request")
}
if res.StatusCode != http.StatusUnauthorized {
return res, nil
}
authHeader := res.Header.Get(WWWAuthenticate)
if authHeader == "" {
drainBody(res)
return nil, errors.Errorf("Header %s is not populated in "+
"SCRAM-SHA start response", WWWAuthenticate)
}
authHeader, err = trimPrefix(authHeader, method+" ")
if err != nil {
if strings.HasPrefix(authHeader, "Basic ") {
// user not found
return res, nil
}
drainBody(res)
return nil, errors.Wrapf(err, "Error while parsing SCRAM-SHA "+
"start response %s", authHeader)
}
drainBody(res)
sid, response, err := parseSidAndData(authHeader)
if err != nil {
return nil, errors.Wrapf(err, "Error while parsing SCRAM-SHA "+
"start response %s", authHeader)
}
err = s.HandleStartResponse(response)
if err != nil {
return nil, errors.Wrapf(err, "Error parsing SCRAM-SHA start "+
"response %s", response)
}
message = s.GetFinalRequest(password)
encodedMessage = method + " " + SidPrefix + sid + "," + DataPrefix +
encode(message)
req.Header.Set(Authorization, encodedMessage)
// rewind request body so it can be resent again
if req.body != nil {
if _, err = req.body.Seek(0, 0); err != nil {
return nil, errors.Errorf("Failed to seek body: %v",
err)
}
}
res, err = client.Do(req.Request)
if err != nil {
return nil, errors.Wrap(err, "Problem sending SCRAM-SHA final"+
"request")
}
if res.StatusCode == http.StatusUnauthorized {
// TODO retrieve and return error
return res, nil
}
if res.StatusCode >= http.StatusInternalServerError {
// in this case we cannot expect server to set headers properly
return res, nil
}
authHeader = res.Header.Get(AuthenticationInfo)
if authHeader == "" {
drainBody(res)
return nil, errors.Errorf("Header %s is not populated in "+
"SCRAM-SHA final response", AuthenticationInfo)
}
finalSid, response, err := parseSidAndData(authHeader)
if err != nil {
drainBody(res)
return nil, errors.Wrapf(err, "Error while parsing SCRAM-SHA "+
"final response %s", authHeader)
}
if finalSid != sid {
drainBody(res)
return nil, errors.Errorf("Sid %s returned by server "+
"doesn't match the original sid %s", finalSid, sid)
}
err = s.HandleFinalResponse(response)
if err != nil {
drainBody(res)
return nil, errors.Wrapf(err,
"Error handling SCRAM-SHA final server response %s",
response)
}
return res, nil
}
func parseSidAndData(authHeader string) (string, string, error) {
sidIndex := strings.Index(authHeader, SidPrefix)
if sidIndex < 0 {
return "", "", errors.Errorf("Cannot find %s in %s",
SidPrefix, authHeader)
}
sidEndIndex := strings.Index(authHeader, ",")
if sidEndIndex < 0 {
return "", "", errors.Errorf("Cannot find ',' in %s",
authHeader)
}
sid := authHeader[sidIndex+len(SidPrefix) : sidEndIndex]
dataIndex := strings.Index(authHeader, DataPrefix)
if dataIndex < 0 {
return "", "", errors.Errorf("Cannot find %s in %s",
DataPrefix, authHeader)
}
data, err := decode(authHeader[dataIndex+len(DataPrefix):])
if err != nil {
return "", "", err
}
return sid, data, nil
}