forked from facebookincubator/tacquito
-
Notifications
You must be signed in to change notification settings - Fork 0
/
crypt.go
329 lines (296 loc) · 8.83 KB
/
crypt.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/*
Copyright (c) Facebook, Inc. and its affiliates.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
package tacquito
import (
"bufio"
"crypto/md5"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"github.com/facebookincubator/tacquito/proxy"
)
/* crypt calculates the psuedo-random pad for a TACACs+ packet and performs xor ops
See https://datatracker.ietf.org/doc/html/rfc8907#section-4.5 for more info
The packet body is obfuscated by XOR-ing it byte-wise with a pseudo-random pad.
ENCRYPTED {data} = data ^ pseudo_pad
The packet body can then be de-obfuscated by XOR-ing it byte-wise with a pseudo random pad.
data = ENCRYPTED {data} ^ pseudo_pad
The pad is generated by concatenating a series of MD5 hashes (each 16 bytes long) and truncating it to the length of the input data.
pseudo_pad = {MD5_1 [,MD5_2 [ ... ,MD5_n]]} truncated to len(data)
The first MD5 hash is generated by concatenating the session_id, the
secret key, the version number and the sequence number and then
running MD5 over that stream.
Subsequent hashes are generated by using the same input stream, but
concatenating the previous hash value at the end of the input stream.
MD5_1 = MD5{session_id, key, version, seq_no} MD5_2 = MD5{session_id, key, version, seq_no, MD5_1} .... MD5_n = MD5{session_id, key, version, seq_no, MD5_n-1}
WARNING: Per the RFC, this is not 'real' encryption. This algorithm does not meet modern standards, but like The Mandalorian says, "This Is The Way".
*/
func crypt(secret []byte, p *Packet) error {
if p.Header.Flags.Has(UnencryptedFlag) {
return nil
}
sessionID, err := p.Header.SessionID.MarshalBinary()
if err != nil {
return err
}
version, err := p.Header.Version.MarshalBinary()
if err != nil {
return err
}
headerLen := int(p.Header.Length)
seqNo := []byte{byte(p.Header.SeqNo)}
lastHash := make([]byte, 0, 16)
pad := make([]byte, 0, 80)
h := md5.New()
for len(pad) < headerLen {
h.Reset()
h.Write(sessionID)
h.Write(secret)
h.Write(version)
h.Write(seqNo)
h.Write(lastHash)
lastHash = h.Sum(nil)
pad = append(pad, lastHash[:]...)
// truncate to length of body
if len(pad) > headerLen {
pad = pad[:headerLen]
}
}
// perform xor ops
for i, b := range p.Body {
p.Body[i] = b ^ pad[i]
}
return nil
}
// newCrypter makes a new crypter
func newCrypter(secret []byte, c net.Conn, proxy bool) *crypter {
return &crypter{secret: secret, Conn: c, Reader: bufio.NewReaderSize(c, 107), proxy: proxy}
}
// crypter wraps the net.Conn and performs reads and writes and crypt ops
// if the incoming packet is not crypted via the no crypt flag, nothing will happen
// to the underlying bytes. However, if that flag is missing and the keys mismatch,
// the corresponding response from the server will appear to be malformed to the client
// since we'll still send a crypted reply for the secret the client should be using.
type crypter struct {
net.Conn
*bufio.Reader
// secret is the tacacs psk used in crypt ops
secret []byte
// proxy if set, will strip the ha-proxy style ascii header
proxy bool
}
// read will read a packet from the underlying net.Conn and decyrpt it
func (c *crypter) read() (*Packet, error) {
// strip proxy header and record metrics
if c.proxy {
line, err := c.ReadBytes('\000') // octal null byte
if err != nil {
if err == io.EOF {
return nil, err
}
crypterReadError.Inc()
return nil, fmt.Errorf("unable to read header proxy line; %w", err)
}
p := proxy.NewHeader(c.LocalAddr(), c.RemoteAddr())
if _, err := p.Write(line); err != nil {
crypterReadError.Inc()
return nil, fmt.Errorf("unable to extract proxy header; %w", err)
}
// TODO add metrics for reporting in next diff
}
// allocate a tacacs header
h := make([]byte, MaxHeaderLength)
if _, err := io.ReadFull(c.Reader, h); err != nil {
if err != io.EOF {
crypterReadError.Inc()
}
return nil, err
}
// read the length field from the bytes of the header to know how many more bytes we need to get
s := int(binary.BigEndian.Uint32(h[8:]))
if s > int(MaxBodyLength) {
return nil, fmt.Errorf("max header length exceeded in crypt read, aborting")
}
b := make([]byte, s)
if _, err := io.ReadFull(c.Reader, b); err != nil {
crypterReadError.Inc()
return nil, err
}
var p Packet
err := Unmarshal(append(h, b...), &p)
if err != nil {
crypterUnmarshalError.Inc()
return nil, err
}
// run crypt first before we look for bad secrets
if err := crypt(c.secret, &p); err != nil {
crypterCryptError.Inc()
return nil, err
}
// if err is != nil, we hit a bug
// if reply is != nil, we found a bad secret.
// if both are non nil, we only inspect the error as that
// is a higher error condition in the server than a bad secret is
if reply, err := c.detectBadSecret(&p); err != nil {
return nil, err
} else if reply != nil {
if _, err := c.write(reply); err != nil {
return nil, fmt.Errorf("bad secret, crypt write fail for ip [%s]: %v", c.RemoteAddr().String(), err)
}
return nil, fmt.Errorf("bad secret detected for ip [%s]", c.RemoteAddr().String())
}
crypterRead.Inc()
return &p, nil
}
// write takes a packet, marshals and crypts it
func (c *crypter) write(p *Packet) (int, error) {
if p == nil {
return 0, fmt.Errorf("handler error, packet cannot be nil")
}
if p.Body == nil {
return 0, fmt.Errorf("handler error, packet.Body cannot be nil")
}
p.Header.Length = uint32(len(p.Body))
if err := crypt(c.secret, p); err != nil {
crypterCryptError.Inc()
return 0, err
}
b, err := p.MarshalBinary()
if err != nil {
crypterMarshalError.Inc()
return 0, err
}
n, err := c.Write(b)
if err != nil {
crypterWriteError.Inc()
return 0, err
}
crypterWrite.Inc()
return n, nil
}
// detectBadSecret is "a way" to detect a potential bad secret. tacacs doesn't give
// us enough information to know what body to expect from a given header, so we
// have to go to great lengths to guess
func (c crypter) detectBadSecret(p *Packet) (*Packet, error) {
if p.Header.Flags.Has(UnencryptedFlag) {
return nil, nil
}
var badSecret *BadSecretErr
switch p.Header.Type {
case Authenticate:
errCnt := 0
var as AuthenStart
if err := Unmarshal(p.Body, &as); errors.As(err, &badSecret) {
errCnt++
}
var ac AuthenContinue
if err := Unmarshal(p.Body, &ac); errors.As(err, &badSecret) {
errCnt++
}
var ar AuthenReply
if err := Unmarshal(p.Body, &ar); errors.As(err, &badSecret) {
errCnt++
}
if errCnt == 3 {
crypterBadSecret.Inc()
// all packet types failed, most likley a bad secret
return c.badSecretReply(p.Header)
}
case Authorize:
errCnt := 0
var ar AuthorRequest
if err := Unmarshal(p.Body, &ar); errors.As(err, &badSecret) {
errCnt++
}
var arr AuthorReply
if err := Unmarshal(p.Body, &arr); errors.As(err, &badSecret) {
errCnt++
}
if errCnt == 2 {
crypterBadSecret.Inc()
// all packet types failed, most likley a bad secret
return c.badSecretReply(p.Header)
}
case Accounting:
errCnt := 0
var ar AcctRequest
if err := Unmarshal(p.Body, &ar); errors.As(err, &badSecret) {
errCnt++
}
var arr AcctReply
if err := Unmarshal(p.Body, &arr); errors.As(err, &badSecret) {
errCnt++
}
if errCnt == 2 {
crypterBadSecret.Inc()
// all packet types failed, most likley a bad secret
return c.badSecretReply(p.Header)
}
}
return nil, nil
}
func (c crypter) badSecretReply(h *Header) (*Packet, error) {
var b []byte
var err error
switch h.Type {
case Authenticate:
b, err = NewAuthenReply(
SetAuthenReplyStatus(AuthenStatusError),
SetAuthenReplyServerMsg("bad secret"),
).MarshalBinary()
if err != nil {
crypterMarshalError.Inc()
return nil, err
}
case Authorize:
b, err = NewAuthorReply(
SetAuthorReplyStatus(AuthorStatusError),
SetAuthorReplyServerMsg("bad secret"),
).MarshalBinary()
if err != nil {
crypterMarshalError.Inc()
return nil, err
}
case Accounting:
b, err = NewAcctReply(
SetAcctReplyStatus(AcctReplyStatusError),
SetAcctReplyServerMsg("bad secret"),
).MarshalBinary()
if err != nil {
crypterMarshalError.Inc()
return nil, err
}
default:
return nil, fmt.Errorf("unknown header type [%v]", h.Type)
}
// reset some flags and state for this error reply.
// under error conditions it can be common in the rfc to reset the sequence to 1
// if the error is particularly egregious. a bad secret seems like it fits and
// the rfc is unclear for this particular condition on what to do
h.SeqNo = SequenceNumber(1)
p := NewPacket(
SetPacketHeader(h),
SetPacketBody(b),
)
if err != nil {
return nil, err
}
return p, nil
}
// BadSecretErr ...
type BadSecretErr struct {
msg string
}
// NewBadSecretErr ...
func NewBadSecretErr(msg string) *BadSecretErr {
return &BadSecretErr{msg: msg}
}
// Error ...
func (b BadSecretErr) Error() string {
return b.msg
}