forked from sqrldev/server-go-ssp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cli_response.go
308 lines (268 loc) · 8.19 KB
/
cli_response.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
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
package ssp
import (
"bytes"
"fmt"
"log"
"strconv"
"strings"
)
//
// TIF bitflags
const (
// the identity has been seen before
TIFIDMatch = 0x1
// the previous identity is a known identity
TIFPreviousIDMatch = 0x2
// the IP address of the current request and the original Nut request match
TIFIPMatched = 0x4
// the SQRL account is disabled
TIFSQRLDisabled = 0x8
// the ClientBody.Cmd is not recognized
TIFFunctionNotSupported = 0x10
// used for all the random server errors like failures to connect to datastores
TIFTransientError = 0x20
// the specific ClientBody.Cmd could not be completed for any reason
TIFCommandFailed = 0x40
// the client sent bad or unrecognized data or signature validation failed
TIFClientFailure = 0x80
// The owner of the Nut doesn't match this request
TIFBadIDAssociation = 0x100
// The IDK has been rekeyed to a newer one
TIFIdentitySuperseded = 0x200
)
// TIFDesc description of the TIF bits
var TIFDesc = map[uint32]string{
TIFIDMatch: "ID Matched",
TIFPreviousIDMatch: "Previous ID Matched",
TIFIPMatched: "IP Matched",
TIFSQRLDisabled: "Identity disabled",
TIFFunctionNotSupported: "Command not recognized",
TIFTransientError: "Server Error",
TIFCommandFailed: "Command failed",
TIFClientFailure: "Bad client request",
TIFBadIDAssociation: "Mismatch of nut to idk",
TIFIdentitySuperseded: "Identity superseded by newer one",
}
// CliResponse encodes a response to the SQRL client
// As specified https://www.grc.com/sqrl/semantics.htm
type CliResponse struct {
Version []int
Nut Nut
TIF uint32
Qry string
URL string
Sin string
Suk string
Ask *Ask
Can string
// HoardCache is not serialized but the encoded response is saved here
// so we can check it in the next request
HoardCache *HoardCache
}
// Ask holds optional response to queries that
// will be shown to the user from the SQRL client
type Ask struct {
Message string `json:"message"`
Button1 string `json:"button1,omitempty"`
URL1 string `json:"url1,omitempty"`
Button2 string `json:"button2,omitempty"`
URL2 string `json:"url2,omitempty"`
}
// ParseAsk parses the special Ask format
func ParseAsk(askString string) *Ask {
encparts := strings.Split(askString, "~")
parts := make([]string, len(encparts))
for i, e := range encparts {
b, _ := Sqrl64.DecodeString(e)
parts[i] = string(b)
}
ask := &Ask{
Message: parts[0],
}
if len(parts) > 1 {
ask.Button1, ask.URL1 = splitButton(parts[1])
}
if len(parts) > 2 {
ask.Button2, ask.URL2 = splitButton(parts[2])
}
return ask
}
func splitButton(buttonString string) (string, string) {
semi := strings.Index(buttonString, ";")
if semi == -1 {
return buttonString, ""
}
button := buttonString[0:semi]
url := buttonString[semi+1:]
return button, url
}
// Encode creates the tilde and semicolon separated ask format
func (a *Ask) Encode() string {
delimited := make([]string, 1)
delimited[0] = Sqrl64.EncodeToString([]byte(a.Message))
button := encodeButton(a.Button1, a.URL1)
if button != "" {
delimited = append(delimited, button)
}
button = encodeButton(a.Button2, a.URL2)
if button != "" {
delimited = append(delimited, button)
}
return strings.Join(delimited, "~")
}
func encodeButton(button, url string) string {
if button != "" {
urlappend := ""
if url != "" {
urlappend = fmt.Sprintf(";%v", url)
}
return Sqrl64.EncodeToString([]byte(fmt.Sprintf("%s%s", removeSemi(button), urlappend)))
}
return ""
}
func removeTilde(v string) string {
return strings.Replace(v, "~", "", -1)
}
func removeSemi(v string) string {
return strings.Replace(v, ";", "", -1)
}
// NewCliResponse creates a minimal valid CliResponse object
func NewCliResponse(nut Nut, qry string) *CliResponse {
return &CliResponse{
Version: []int{1},
Nut: nut,
Qry: qry,
}
}
// WithIDMatch set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithIDMatch() *CliResponse {
cr.TIF |= TIFIDMatch
return cr
}
// ClearIDMatch set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) ClearIDMatch() *CliResponse {
cr.TIF = cr.TIF &^ TIFIDMatch
return cr
}
// WithPreviousIDMatch set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithPreviousIDMatch() *CliResponse {
cr.TIF |= TIFPreviousIDMatch
return cr
}
// ClearPreviousIDMatch clears the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) ClearPreviousIDMatch() *CliResponse {
cr.TIF = cr.TIF &^ TIFPreviousIDMatch
return cr
}
// WithIPMatch set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithIPMatch() *CliResponse {
cr.TIF |= TIFIPMatched
return cr
}
// WithSQRLDisabled set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithSQRLDisabled() *CliResponse {
cr.TIF |= TIFSQRLDisabled
return cr
}
// WithFunctionNotSupported set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithFunctionNotSupported() *CliResponse {
cr.TIF |= TIFFunctionNotSupported
return cr
}
// WithTransientError set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithTransientError() *CliResponse {
cr.TIF |= TIFTransientError
return cr
}
// WithClientFailure set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithClientFailure() *CliResponse {
cr.TIF |= TIFClientFailure
return cr
}
// WithCommandFailed set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithCommandFailed() *CliResponse {
cr.TIF |= TIFCommandFailed
return cr
}
// WithBadIDAssociation set the appropriate TIF bits on this response.
// Returns the object for easier chaining (not immutability).
func (cr *CliResponse) WithBadIDAssociation() *CliResponse {
cr.TIF |= TIFBadIDAssociation
return cr
}
func (cr *CliResponse) WithIdentitySuperseded() *CliResponse {
cr.TIF |= TIFIdentitySuperseded
return cr
}
// Encode writes the response as the CRNL format and
// encodes it using Sqrl64 encoding.
func (cr *CliResponse) Encode() []byte {
var b bytes.Buffer
// TODO be less lazy and support ranges
b.WriteString("ver=")
for i, v := range cr.Version {
if i > 0 {
b.WriteString(",")
}
b.WriteString(strconv.Itoa(v))
}
b.WriteString("\r\n")
b.WriteString(fmt.Sprintf("nut=%v\r\n", cr.Nut))
b.WriteString(fmt.Sprintf("tif=%x\r\n", cr.TIF))
b.WriteString(fmt.Sprintf("qry=%v\r\n", cr.Qry))
if cr.URL != "" {
b.WriteString(fmt.Sprintf("url=%v\r\n", cr.URL))
}
if cr.Sin != "" {
b.WriteString(fmt.Sprintf("sin=%v\r\n", cr.Sin))
}
if cr.Suk != "" {
b.WriteString(fmt.Sprintf("suk=%v\r\n", cr.Suk))
}
if cr.Ask != nil {
b.WriteString(fmt.Sprintf("ask=%v\r\n", cr.Ask.Encode()))
}
if cr.Can != "" {
b.WriteString(fmt.Sprintf("can=%v\r\n", cr.Can))
}
encoded := Sqrl64.EncodeToString(b.Bytes())
log.Printf("Encoded response: <%v>", encoded)
return []byte(encoded)
}
// ParseCliResponse parses a server response
func ParseCliResponse(body []byte) (*CliResponse, error) {
decoded := make([]byte, Sqrl64.DecodedLen(len(body)))
_, err := Sqrl64.Decode(decoded, body)
if err != nil {
return nil, fmt.Errorf("error decoding response: %v", err)
}
params, err := ParseSqrlQuery(string(decoded))
if err != nil {
return nil, fmt.Errorf("couldn't parse response: %v", err)
}
tifbig, err := strconv.ParseUint(params["tif"], 16, 32)
if err != nil {
return nil, fmt.Errorf("can't parse tif: %v", err)
}
return &CliResponse{
Version: []int{1},
Nut: Nut(params["nut"]),
TIF: uint32(tifbig),
Qry: params["qry"],
URL: params["url"],
Sin: params["sin"],
Suk: params["suk"],
Ask: ParseAsk(params["ask"]),
Can: params["can"],
}, nil
}