pyrad 2.3__py3-none-any.whl → 2.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/Makefile +20 -0
- docs/make.bat +36 -0
- docs/source/_static/logo.png +0 -0
- docs/source/api/client.rst +10 -0
- docs/source/api/dictionary.rst +10 -0
- docs/source/api/host.rst +7 -0
- docs/source/api/packet.rst +48 -0
- docs/source/api/proxy.rst +7 -0
- docs/source/api/server.rst +13 -0
- docs/source/conf.py +158 -0
- docs/source/index.rst +75 -0
- example/acct.py +41 -0
- example/auth.py +37 -0
- example/auth_async.py +164 -0
- example/client-coa.py +61 -0
- example/coa.py +40 -0
- example/dictionary +405 -0
- example/dictionary.freeradius +91 -0
- example/pyrad.log +0 -0
- example/server.py +68 -0
- example/server_async.py +117 -0
- example/status.py +26 -0
- pyrad/__init__.py +3 -3
- pyrad/client.py +54 -9
- pyrad/client_async.py +22 -14
- pyrad/dictfile.py +2 -5
- pyrad/dictionary.py +12 -1
- pyrad/host.py +1 -1
- pyrad/packet.py +208 -133
- pyrad/proxy.py +2 -2
- pyrad/server.py +3 -7
- pyrad/server_async.py +4 -5
- pyrad/tests/__init__.py +2 -2
- pyrad/tests/mock.py +5 -1
- pyrad/tests/{testBidict.py → test_bidict.py} +2 -2
- pyrad/tests/{testClient.py → test_client.py} +28 -30
- pyrad/tests/{testDictionary.py → test_dictionary.py} +38 -21
- pyrad/tests/{testHost.py → test_host.py} +10 -10
- pyrad/tests/test_packet.py +679 -0
- pyrad/tests/{testProxy.py → test_proxy.py} +11 -11
- pyrad/tests/{testServer.py → test_server.py} +35 -33
- pyrad/tests/test_tools.py +126 -0
- pyrad/tools.py +254 -158
- {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info}/METADATA +44 -20
- pyrad-2.5.0.dist-info/RECORD +51 -0
- {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info}/WHEEL +1 -1
- {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info/licenses}/LICENSE.txt +2 -1
- pyrad-2.5.0.dist-info/top_level.txt +3 -0
- pyrad/tests/testPacket.py +0 -530
- pyrad/tests/testTools.py +0 -122
- pyrad-2.3.dist-info/RECORD +0 -29
- pyrad-2.3.dist-info/top_level.txt +0 -1
- {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info}/zip-safe +0 -0
pyrad/packet.py
CHANGED
|
@@ -5,10 +5,29 @@
|
|
|
5
5
|
# A RADIUS packet as defined in RFC 2138
|
|
6
6
|
|
|
7
7
|
from collections import OrderedDict
|
|
8
|
-
import
|
|
9
|
-
import random
|
|
10
|
-
# Hmac needed for Message-Authenticator
|
|
8
|
+
from pyrad import tools
|
|
11
9
|
import hmac
|
|
10
|
+
import struct
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import secrets
|
|
15
|
+
random_generator = secrets.SystemRandom()
|
|
16
|
+
except ImportError:
|
|
17
|
+
import random
|
|
18
|
+
random_generator = random.SystemRandom()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _hmac_md5(*args, **kwargs):
|
|
22
|
+
"""Py3 hmac.new() wrapper with explicit MD5 digestmod."""
|
|
23
|
+
return hmac.new(*args, digestmod='MD5', **kwargs)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if sys.version_info >= (3, 0):
|
|
27
|
+
hmac_new = _hmac_md5
|
|
28
|
+
else:
|
|
29
|
+
hmac_new = hmac.new
|
|
30
|
+
|
|
12
31
|
try:
|
|
13
32
|
import hashlib
|
|
14
33
|
md5_constructor = hashlib.md5
|
|
@@ -16,8 +35,6 @@ except ImportError:
|
|
|
16
35
|
# BBB for python 2.4
|
|
17
36
|
import md5
|
|
18
37
|
md5_constructor = md5.new
|
|
19
|
-
import six
|
|
20
|
-
from pyrad import tools
|
|
21
38
|
|
|
22
39
|
# Packet codes
|
|
23
40
|
AccessRequest = 1
|
|
@@ -35,9 +52,6 @@ CoARequest = 43
|
|
|
35
52
|
CoAACK = 44
|
|
36
53
|
CoANAK = 45
|
|
37
54
|
|
|
38
|
-
# Use cryptographic-safe random generator as provided by the OS.
|
|
39
|
-
random_generator = random.SystemRandom()
|
|
40
|
-
|
|
41
55
|
# Current ID
|
|
42
56
|
CurrentID = random_generator.randrange(1, 255)
|
|
43
57
|
|
|
@@ -52,7 +66,7 @@ class Packet(OrderedDict):
|
|
|
52
66
|
attributes the value will always be a sequence. pyrad makes sure
|
|
53
67
|
to preserve the ordering when encoding and decoding packets.
|
|
54
68
|
|
|
55
|
-
There are two ways to use the map
|
|
69
|
+
There are two ways to use the map interface: if attribute
|
|
56
70
|
names are used pyrad take care of en-/decoding data. If
|
|
57
71
|
the attribute type number (or a vendor ID/attribute type
|
|
58
72
|
tuple for vendor attributes) is used you work with the
|
|
@@ -62,7 +76,7 @@ class Packet(OrderedDict):
|
|
|
62
76
|
:obj:`AuthPacket` or :obj:`AcctPacket` classes.
|
|
63
77
|
"""
|
|
64
78
|
|
|
65
|
-
def __init__(self, code=0, id=None, secret=
|
|
79
|
+
def __init__(self, code=0, id=None, secret=b'', authenticator=None,
|
|
66
80
|
**attributes):
|
|
67
81
|
"""Constructor
|
|
68
82
|
|
|
@@ -83,20 +97,23 @@ class Packet(OrderedDict):
|
|
|
83
97
|
self.id = id
|
|
84
98
|
else:
|
|
85
99
|
self.id = CreateID()
|
|
86
|
-
if not isinstance(secret,
|
|
100
|
+
if not isinstance(secret, bytes):
|
|
87
101
|
raise TypeError('secret must be a binary string')
|
|
88
102
|
self.secret = secret
|
|
89
103
|
if authenticator is not None and \
|
|
90
|
-
not isinstance(authenticator,
|
|
104
|
+
not isinstance(authenticator, bytes):
|
|
91
105
|
raise TypeError('authenticator must be a binary string')
|
|
92
106
|
self.authenticator = authenticator
|
|
107
|
+
self.request_authenticator = None # used for storing request authenticator in reply packets
|
|
93
108
|
self.message_authenticator = None
|
|
109
|
+
self.raw_packet = None
|
|
94
110
|
|
|
95
111
|
if 'dict' in attributes:
|
|
96
112
|
self.dict = attributes['dict']
|
|
97
113
|
|
|
98
114
|
if 'packet' in attributes:
|
|
99
|
-
self.
|
|
115
|
+
self.raw_packet = attributes['packet']
|
|
116
|
+
self.DecodePacket(self.raw_packet)
|
|
100
117
|
|
|
101
118
|
if 'message_authenticator' in attributes:
|
|
102
119
|
self.message_authenticator = attributes['message_authenticator']
|
|
@@ -114,7 +131,7 @@ class Packet(OrderedDict):
|
|
|
114
131
|
|
|
115
132
|
self.message_authenticator = True
|
|
116
133
|
# Maintain a zero octets content for md5 and hmac calculation.
|
|
117
|
-
self['Message-Authenticator'] = 16 *
|
|
134
|
+
self['Message-Authenticator'] = 16 * b'\00'
|
|
118
135
|
|
|
119
136
|
if self.id is None:
|
|
120
137
|
self.id = self.CreateID()
|
|
@@ -128,10 +145,10 @@ class Packet(OrderedDict):
|
|
|
128
145
|
return self.message_authenticator
|
|
129
146
|
|
|
130
147
|
def _refresh_message_authenticator(self):
|
|
131
|
-
hmac_constructor =
|
|
148
|
+
hmac_constructor = hmac_new(self.secret)
|
|
132
149
|
|
|
133
150
|
# Maintain a zero octets content for md5 and hmac calculation.
|
|
134
|
-
self['Message-Authenticator'] = 16 *
|
|
151
|
+
self['Message-Authenticator'] = 16 * b'\00'
|
|
135
152
|
attr = self._PktEncodeAttributes()
|
|
136
153
|
|
|
137
154
|
header = struct.pack('!BBH', self.code, self.id,
|
|
@@ -140,7 +157,7 @@ class Packet(OrderedDict):
|
|
|
140
157
|
hmac_constructor.update(header[0:4])
|
|
141
158
|
if self.code in (AccountingRequest, DisconnectRequest,
|
|
142
159
|
CoARequest, AccountingResponse):
|
|
143
|
-
hmac_constructor.update(16 *
|
|
160
|
+
hmac_constructor.update(16 * b'\00')
|
|
144
161
|
else:
|
|
145
162
|
# NOTE: self.authenticator on reply packet is initialized
|
|
146
163
|
# with request authenticator by design.
|
|
@@ -176,19 +193,32 @@ class Packet(OrderedDict):
|
|
|
176
193
|
else:
|
|
177
194
|
key = self.secret
|
|
178
195
|
|
|
179
|
-
|
|
180
|
-
|
|
196
|
+
# If there's a raw packet, use that to calculate the expected
|
|
197
|
+
# Message-Authenticator. While the Packet class keeps multiple
|
|
198
|
+
# instances of an attribute grouped together in the attribute list,
|
|
199
|
+
# other applications may not. Using _PktEncodeAttributes to get
|
|
200
|
+
# the attributes could therefore end up changing the attribute order
|
|
201
|
+
# because of the grouping Packet does, which would cause
|
|
202
|
+
# Message-Authenticator verification to fail. Using the raw packet
|
|
203
|
+
# instead, if present, ensures the verification is done using the
|
|
204
|
+
# attributes exactly as sent.
|
|
205
|
+
if self.raw_packet:
|
|
206
|
+
attr = self.raw_packet[20:]
|
|
207
|
+
attr = attr.replace(prev_ma[0], 16 * b'\00')
|
|
208
|
+
else:
|
|
209
|
+
self['Message-Authenticator'] = 16 * b'\00'
|
|
210
|
+
attr = self._PktEncodeAttributes()
|
|
181
211
|
|
|
182
212
|
header = struct.pack('!BBH', self.code, self.id,
|
|
183
213
|
(20 + len(attr)))
|
|
184
214
|
|
|
185
|
-
hmac_constructor =
|
|
215
|
+
hmac_constructor = hmac_new(key)
|
|
186
216
|
hmac_constructor.update(header)
|
|
187
217
|
if self.code in (AccountingRequest, DisconnectRequest,
|
|
188
218
|
CoARequest, AccountingResponse):
|
|
189
219
|
if original_code is None or original_code != StatusServer:
|
|
190
220
|
# TODO: Handle Status-Server response correctly.
|
|
191
|
-
hmac_constructor.update(16 *
|
|
221
|
+
hmac_constructor.update(16 * b'\00')
|
|
192
222
|
elif self.code in (AccessAccept, AccessChallenge,
|
|
193
223
|
AccessReject):
|
|
194
224
|
if original_authenticator is None:
|
|
@@ -218,6 +248,10 @@ class Packet(OrderedDict):
|
|
|
218
248
|
**attributes)
|
|
219
249
|
|
|
220
250
|
def _DecodeValue(self, attr, value):
|
|
251
|
+
if attr.encrypt == 2:
|
|
252
|
+
# salt decrypt attribute
|
|
253
|
+
value = self.SaltDecrypt(value)
|
|
254
|
+
|
|
221
255
|
if attr.values.HasBackward(value):
|
|
222
256
|
return attr.values.GetBackward(value)
|
|
223
257
|
else:
|
|
@@ -239,7 +273,7 @@ class Packet(OrderedDict):
|
|
|
239
273
|
def _EncodeKeyValues(self, key, values):
|
|
240
274
|
if not isinstance(key, str):
|
|
241
275
|
return (key, values)
|
|
242
|
-
|
|
276
|
+
|
|
243
277
|
if not isinstance(values, (list, tuple)):
|
|
244
278
|
values = [values]
|
|
245
279
|
|
|
@@ -260,7 +294,7 @@ class Packet(OrderedDict):
|
|
|
260
294
|
return key
|
|
261
295
|
|
|
262
296
|
attr = self.dict.attributes[key]
|
|
263
|
-
if attr.vendor and not attr.is_sub_attribute: #sub attribute keys don't need vendor
|
|
297
|
+
if attr.vendor and not attr.is_sub_attribute: # sub attribute keys don't need vendor
|
|
264
298
|
return (self.dict.vendors.GetForward(attr.vendor), attr.code)
|
|
265
299
|
else:
|
|
266
300
|
return attr.code
|
|
@@ -281,7 +315,7 @@ class Packet(OrderedDict):
|
|
|
281
315
|
:param value: value
|
|
282
316
|
:type value: depends on type of attribute
|
|
283
317
|
"""
|
|
284
|
-
attr = self.dict.attributes[key]
|
|
318
|
+
attr = self.dict.attributes[key.partition(':')[0]]
|
|
285
319
|
|
|
286
320
|
(key, value) = self._EncodeKeyValues(key, value)
|
|
287
321
|
|
|
@@ -294,10 +328,14 @@ class Packet(OrderedDict):
|
|
|
294
328
|
encoded.extend(value)
|
|
295
329
|
|
|
296
330
|
def get(self, key, failobj=None):
|
|
297
|
-
|
|
331
|
+
try:
|
|
332
|
+
res = self.__getitem__(key)
|
|
333
|
+
except KeyError:
|
|
334
|
+
res = failobj
|
|
335
|
+
return res
|
|
298
336
|
|
|
299
337
|
def __getitem__(self, key):
|
|
300
|
-
if not isinstance(key,
|
|
338
|
+
if not isinstance(key, str):
|
|
301
339
|
return OrderedDict.__getitem__(self, key)
|
|
302
340
|
|
|
303
341
|
values = OrderedDict.__getitem__(self, self._EncodeKey(key))
|
|
@@ -328,7 +366,7 @@ class Packet(OrderedDict):
|
|
|
328
366
|
OrderedDict.__delitem__(self, self._EncodeKey(key))
|
|
329
367
|
|
|
330
368
|
def __setitem__(self, key, item):
|
|
331
|
-
if isinstance(key,
|
|
369
|
+
if isinstance(key, str):
|
|
332
370
|
(key, item) = self._EncodeKeyValues(key, item)
|
|
333
371
|
OrderedDict.__setitem__(self, key, item)
|
|
334
372
|
else:
|
|
@@ -347,14 +385,10 @@ class Packet(OrderedDict):
|
|
|
347
385
|
:return: valid packet authenticator
|
|
348
386
|
:rtype: binary string
|
|
349
387
|
"""
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if six.PY3:
|
|
355
|
-
return bytes(data)
|
|
356
|
-
else:
|
|
357
|
-
return ''.join(chr(b) for b in data)
|
|
388
|
+
return bytes(
|
|
389
|
+
random_generator.randrange(0, 256)
|
|
390
|
+
for _ in range(16)
|
|
391
|
+
)
|
|
358
392
|
|
|
359
393
|
def CreateID(self):
|
|
360
394
|
"""Create a packet ID. All RADIUS requests have a ID which is used to
|
|
@@ -376,8 +410,8 @@ class Packet(OrderedDict):
|
|
|
376
410
|
:return: raw packet
|
|
377
411
|
:rtype: string
|
|
378
412
|
"""
|
|
379
|
-
assert(self.authenticator)
|
|
380
|
-
assert(self.secret is not None)
|
|
413
|
+
assert (self.authenticator)
|
|
414
|
+
assert (self.secret is not None)
|
|
381
415
|
|
|
382
416
|
if self.message_authenticator:
|
|
383
417
|
self._refresh_message_authenticator()
|
|
@@ -390,27 +424,34 @@ class Packet(OrderedDict):
|
|
|
390
424
|
|
|
391
425
|
return header + authenticator + attr
|
|
392
426
|
|
|
393
|
-
def VerifyReply(self, reply, rawreply=None):
|
|
427
|
+
def VerifyReply(self, reply, rawreply=None, enforce_ma=False):
|
|
394
428
|
if reply.id != self.id:
|
|
395
429
|
return False
|
|
396
430
|
|
|
397
431
|
if rawreply is None:
|
|
398
432
|
rawreply = reply.ReplyPacket()
|
|
399
433
|
|
|
400
|
-
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
434
|
+
_ = reply._PktEncodeAttributes()
|
|
435
|
+
# The Authenticator field in an Accounting-Response packet is called
|
|
436
|
+
# the Response Authenticator, and contains a one-way MD5 hash
|
|
437
|
+
# calculated over a stream of octets consisting of the Accounting
|
|
438
|
+
# Response Code, Identifier, Length, the Request Authenticator field
|
|
439
|
+
# from the Accounting-Request packet being replied to, and the
|
|
440
|
+
# response attributes if any, followed by the shared secret. The
|
|
441
|
+
# resulting 16 octet MD5 hash value is stored in the Authenticator
|
|
408
442
|
# field of the Accounting-Response packet.
|
|
409
443
|
hash = md5_constructor(rawreply[0:4] + self.authenticator +
|
|
410
|
-
|
|
444
|
+
rawreply[20:] + self.secret).digest()
|
|
411
445
|
|
|
412
446
|
if hash != rawreply[4:20]:
|
|
413
447
|
return False
|
|
448
|
+
|
|
449
|
+
if enforce_ma:
|
|
450
|
+
if self.message_authenticator is None:
|
|
451
|
+
return False
|
|
452
|
+
if not self.verify_message_authenticator():
|
|
453
|
+
return False
|
|
454
|
+
|
|
414
455
|
return True
|
|
415
456
|
|
|
416
457
|
def _PktEncodeAttribute(self, key, value):
|
|
@@ -423,11 +464,11 @@ class Packet(OrderedDict):
|
|
|
423
464
|
|
|
424
465
|
def _PktEncodeTlv(self, tlv_key, tlv_value):
|
|
425
466
|
tlv_attr = self.dict.attributes[self._DecodeKey(tlv_key)]
|
|
426
|
-
curr_avp =
|
|
467
|
+
curr_avp = b''
|
|
427
468
|
avps = []
|
|
428
469
|
max_sub_attribute_len = max(map(lambda item: len(item[1]), tlv_value.items()))
|
|
429
470
|
for i in range(max_sub_attribute_len):
|
|
430
|
-
sub_attr_encoding =
|
|
471
|
+
sub_attr_encoding = b''
|
|
431
472
|
for (code, datalst) in tlv_value.items():
|
|
432
473
|
if i < len(datalst):
|
|
433
474
|
sub_attr_encoding += self._PktEncodeAttribute(code, datalst[i])
|
|
@@ -443,7 +484,7 @@ class Packet(OrderedDict):
|
|
|
443
484
|
value = struct.pack('!BB', tlv_attr.code, (len(avp) + 2)) + avp
|
|
444
485
|
tlv_avps.append(value)
|
|
445
486
|
if tlv_attr.vendor:
|
|
446
|
-
vendor_avps =
|
|
487
|
+
vendor_avps = b''
|
|
447
488
|
for avp in tlv_avps:
|
|
448
489
|
vendor_avps += struct.pack(
|
|
449
490
|
'!BBL', 26, (len(avp) + 6),
|
|
@@ -454,10 +495,9 @@ class Packet(OrderedDict):
|
|
|
454
495
|
return b''.join(tlv_avps)
|
|
455
496
|
|
|
456
497
|
def _PktEncodeAttributes(self):
|
|
457
|
-
result =
|
|
498
|
+
result = b''
|
|
458
499
|
for (code, datalst) in self.items():
|
|
459
|
-
|
|
460
|
-
if attribute and attribute.type == 'tlv':
|
|
500
|
+
if self._PktIsTlvAttribute(code):
|
|
461
501
|
result += self._PktEncodeTlv(code, datalst)
|
|
462
502
|
else:
|
|
463
503
|
for data in datalst:
|
|
@@ -471,21 +511,20 @@ class Packet(OrderedDict):
|
|
|
471
511
|
return [(26, data)]
|
|
472
512
|
|
|
473
513
|
(vendor, atype, length) = struct.unpack('!LBB', data[:6])[0:3]
|
|
474
|
-
attribute = self.dict.attributes.get(self._DecodeKey((vendor, atype)))
|
|
475
514
|
try:
|
|
476
|
-
if
|
|
515
|
+
if self._PktIsTlvAttribute((vendor, atype)):
|
|
477
516
|
self._PktDecodeTlvAttribute((vendor, atype), data[6:length + 4])
|
|
478
517
|
tlvs = [] # tlv is added to the packet inside _PktDecodeTlvAttribute
|
|
479
518
|
else:
|
|
480
519
|
tlvs = [((vendor, atype), data[6:length + 4])]
|
|
481
|
-
except:
|
|
520
|
+
except Exception:
|
|
482
521
|
return [(26, data)]
|
|
483
522
|
|
|
484
523
|
sumlength = 4 + length
|
|
485
524
|
while len(data) > sumlength:
|
|
486
525
|
try:
|
|
487
526
|
atype, length = struct.unpack('!BB', data[sumlength:sumlength+2])[0:2]
|
|
488
|
-
except:
|
|
527
|
+
except Exception:
|
|
489
528
|
return [(26, data)]
|
|
490
529
|
tlvs.append(((vendor, atype), data[sumlength+2:sumlength+length]))
|
|
491
530
|
sumlength += length
|
|
@@ -500,6 +539,10 @@ class Packet(OrderedDict):
|
|
|
500
539
|
sub_attributes.setdefault(atype, []).append(data[loc+2:loc+length])
|
|
501
540
|
loc += length
|
|
502
541
|
|
|
542
|
+
def _PktIsTlvAttribute(self, code):
|
|
543
|
+
attr = self.dict.attributes.get(self._DecodeKey(code))
|
|
544
|
+
return (attr is not None and attr.type == 'tlv')
|
|
545
|
+
|
|
503
546
|
def DecodePacket(self, packet):
|
|
504
547
|
"""Initialize the object from raw packet data. Decode a packet as
|
|
505
548
|
received from the network and decode it.
|
|
@@ -528,11 +571,9 @@ class Packet(OrderedDict):
|
|
|
528
571
|
raise PacketError('Attribute header is corrupt')
|
|
529
572
|
|
|
530
573
|
if attrlen < 2:
|
|
531
|
-
raise PacketError(
|
|
532
|
-
'Attribute length is too small (%d)' % attrlen)
|
|
574
|
+
raise PacketError('Attribute length is too small (%d)' % attrlen)
|
|
533
575
|
|
|
534
576
|
value = packet[2:attrlen]
|
|
535
|
-
attribute = self.dict.attributes.get(self._DecodeKey(key))
|
|
536
577
|
if key == 26:
|
|
537
578
|
for (key, value) in self._PktDecodeVendorAttribute(value):
|
|
538
579
|
self.setdefault(key, []).append(value)
|
|
@@ -540,58 +581,82 @@ class Packet(OrderedDict):
|
|
|
540
581
|
# POST: Message Authenticator AVP is present.
|
|
541
582
|
self.message_authenticator = True
|
|
542
583
|
self.setdefault(key, []).append(value)
|
|
543
|
-
elif
|
|
544
|
-
self._PktDecodeTlvAttribute(key,value)
|
|
584
|
+
elif self._PktIsTlvAttribute(key):
|
|
585
|
+
self._PktDecodeTlvAttribute(key, value)
|
|
545
586
|
else:
|
|
546
587
|
self.setdefault(key, []).append(value)
|
|
547
588
|
|
|
548
589
|
packet = packet[attrlen:]
|
|
549
590
|
|
|
591
|
+
def _salt_en_decrypt(self, data, salt):
|
|
592
|
+
result = b''
|
|
593
|
+
if self.request_authenticator is not None:
|
|
594
|
+
last = self.request_authenticator + salt
|
|
595
|
+
else:
|
|
596
|
+
last = self.authenticator + salt
|
|
597
|
+
while data:
|
|
598
|
+
hash = md5_constructor(self.secret + last).digest()
|
|
599
|
+
for i in range(16):
|
|
600
|
+
result += bytes((hash[i] ^ data[i],))
|
|
601
|
+
|
|
602
|
+
last = result[-16:]
|
|
603
|
+
data = data[16:]
|
|
604
|
+
return result
|
|
605
|
+
|
|
550
606
|
def SaltCrypt(self, value):
|
|
551
|
-
"""
|
|
607
|
+
"""SaltEncrypt
|
|
552
608
|
|
|
553
609
|
:param value: plaintext value
|
|
554
|
-
:type
|
|
610
|
+
:type: unicode string
|
|
555
611
|
:return: obfuscated version of the value
|
|
556
612
|
:rtype: binary string
|
|
557
613
|
"""
|
|
558
614
|
|
|
559
|
-
if isinstance(value,
|
|
615
|
+
if isinstance(value, str):
|
|
560
616
|
value = value.encode('utf-8')
|
|
561
617
|
|
|
562
618
|
if self.authenticator is None:
|
|
563
619
|
# self.authenticator = self.CreateAuthenticator()
|
|
564
|
-
self.authenticator = 16 *
|
|
620
|
+
self.authenticator = 16 * b'\x00'
|
|
565
621
|
|
|
566
|
-
|
|
567
|
-
|
|
622
|
+
# create salt
|
|
623
|
+
random_value = 32768 + random_generator.randrange(0, 32767)
|
|
624
|
+
salt_raw = struct.pack('!H', random_value)
|
|
568
625
|
|
|
626
|
+
# length prefixing
|
|
569
627
|
length = struct.pack("B", len(value))
|
|
570
|
-
|
|
571
|
-
if len(buf) % 16 != 0:
|
|
572
|
-
buf += six.b('\x00') * (16 - (len(buf) % 16))
|
|
628
|
+
value = length + value
|
|
573
629
|
|
|
574
|
-
|
|
630
|
+
# zero padding
|
|
631
|
+
if len(value) % 16 != 0:
|
|
632
|
+
value += b'\x00' * (16 - (len(value) % 16))
|
|
575
633
|
|
|
576
|
-
|
|
577
|
-
while buf:
|
|
578
|
-
hash = md5_constructor(self.secret + last).digest()
|
|
579
|
-
if six.PY3:
|
|
580
|
-
for i in range(16):
|
|
581
|
-
result += bytes((hash[i] ^ buf[i],))
|
|
582
|
-
else:
|
|
583
|
-
for i in range(16):
|
|
584
|
-
result += chr(ord(hash[i]) ^ ord(buf[i]))
|
|
634
|
+
return salt_raw + self._salt_en_decrypt(value, salt_raw)
|
|
585
635
|
|
|
586
|
-
|
|
587
|
-
|
|
636
|
+
def SaltDecrypt(self, value):
|
|
637
|
+
""" SaltDecrypt
|
|
588
638
|
|
|
589
|
-
|
|
639
|
+
:param value: encrypted value including salt
|
|
640
|
+
:type: binary string
|
|
641
|
+
:return: decrypted plaintext string
|
|
642
|
+
:rtype: unicode string
|
|
643
|
+
"""
|
|
644
|
+
# extract salt
|
|
645
|
+
salt = value[:2]
|
|
646
|
+
|
|
647
|
+
# decrypt
|
|
648
|
+
value = self._salt_en_decrypt(value[2:], salt)
|
|
649
|
+
|
|
650
|
+
# remove padding
|
|
651
|
+
length = value[0]
|
|
652
|
+
value = value[1:length+1]
|
|
653
|
+
|
|
654
|
+
return value
|
|
590
655
|
|
|
591
656
|
|
|
592
657
|
class AuthPacket(Packet):
|
|
593
|
-
def __init__(self, code=AccessRequest, id=None, secret=
|
|
594
|
-
|
|
658
|
+
def __init__(self, code=AccessRequest, id=None, secret=b'',
|
|
659
|
+
authenticator=None, auth_type='pap', **attributes):
|
|
595
660
|
"""Constructor
|
|
596
661
|
|
|
597
662
|
:param code: packet type code
|
|
@@ -607,9 +672,9 @@ class AuthPacket(Packet):
|
|
|
607
672
|
:param packet: raw packet to decode
|
|
608
673
|
:type packet: string
|
|
609
674
|
"""
|
|
675
|
+
|
|
610
676
|
Packet.__init__(self, code, id, secret, authenticator, **attributes)
|
|
611
|
-
|
|
612
|
-
self.raw_packet = attributes['packet']
|
|
677
|
+
self.auth_type = auth_type
|
|
613
678
|
|
|
614
679
|
def CreateReply(self, **attributes):
|
|
615
680
|
"""Create a new packet as a reply to this one. This method
|
|
@@ -618,7 +683,7 @@ class AuthPacket(Packet):
|
|
|
618
683
|
"""
|
|
619
684
|
return AuthPacket(AccessAccept, self.id,
|
|
620
685
|
self.secret, self.authenticator, dict=self.dict,
|
|
621
|
-
**attributes)
|
|
686
|
+
auth_type=self.auth_type, **attributes)
|
|
622
687
|
|
|
623
688
|
def RequestPacket(self):
|
|
624
689
|
"""Create a ready-to-transmit authentication request packet.
|
|
@@ -638,40 +703,61 @@ class AuthPacket(Packet):
|
|
|
638
703
|
self._refresh_message_authenticator()
|
|
639
704
|
|
|
640
705
|
attr = self._PktEncodeAttributes()
|
|
706
|
+
if self.auth_type == 'eap-md5':
|
|
707
|
+
header = struct.pack(
|
|
708
|
+
'!BBH16s', self.code, self.id, (20 + 18 + len(attr)), self.authenticator
|
|
709
|
+
)
|
|
710
|
+
digest = hmac_new(
|
|
711
|
+
self.secret,
|
|
712
|
+
header
|
|
713
|
+
+ attr
|
|
714
|
+
+ struct.pack('!BB16s', 80, struct.calcsize('!BB16s'), b''),
|
|
715
|
+
).digest()
|
|
716
|
+
return (
|
|
717
|
+
header
|
|
718
|
+
+ attr
|
|
719
|
+
+ struct.pack('!BB16s', 80, struct.calcsize('!BB16s'), digest)
|
|
720
|
+
)
|
|
721
|
+
|
|
641
722
|
header = struct.pack('!BBH16s', self.code, self.id,
|
|
642
723
|
(20 + len(attr)), self.authenticator)
|
|
643
724
|
|
|
644
725
|
return header + attr
|
|
645
726
|
|
|
646
727
|
def PwDecrypt(self, password):
|
|
647
|
-
"""Obfuscate a RADIUS password. RADIUS hides passwords in packets by
|
|
728
|
+
"""De-Obfuscate a RADIUS password. RADIUS hides passwords in packets by
|
|
648
729
|
using an algorithm based on the MD5 hash of the packet authenticator
|
|
649
730
|
and RADIUS secret. This function reverses the obfuscation process.
|
|
650
731
|
|
|
732
|
+
Although RFC2865 does not explicitly state UTF-8 for the password field,
|
|
733
|
+
the rest of RFC2865 defines UTF-8 as the encoding expected for the decrypted password.
|
|
734
|
+
|
|
735
|
+
|
|
651
736
|
:param password: obfuscated form of password
|
|
652
737
|
:type password: binary string
|
|
653
738
|
:return: plaintext password
|
|
654
739
|
:rtype: unicode string
|
|
655
740
|
"""
|
|
656
741
|
buf = password
|
|
657
|
-
pw =
|
|
742
|
+
pw = b''
|
|
658
743
|
|
|
659
744
|
last = self.authenticator
|
|
660
745
|
while buf:
|
|
661
746
|
hash = md5_constructor(self.secret + last).digest()
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
pw += bytes((hash[i] ^ buf[i],))
|
|
665
|
-
else:
|
|
666
|
-
for i in range(16):
|
|
667
|
-
pw += chr(ord(hash[i]) ^ ord(buf[i]))
|
|
668
|
-
|
|
747
|
+
for i in range(16):
|
|
748
|
+
pw += bytes((hash[i] ^ buf[i],))
|
|
669
749
|
(last, buf) = (buf[:16], buf[16:])
|
|
670
750
|
|
|
671
|
-
|
|
751
|
+
# This is safe even with UTF-8 encoding since no valid encoding of UTF-8
|
|
752
|
+
# (other than encoding U+0000 NULL) will produce a bytestream containing 0x00 byte.
|
|
753
|
+
while pw.endswith(b'\x00'):
|
|
672
754
|
pw = pw[:-1]
|
|
673
755
|
|
|
674
|
-
|
|
756
|
+
# If the shared secret with the client is not the same, then de-obfuscating the password
|
|
757
|
+
# field may yield illegal UTF-8 bytes. Therefore, in order not to provoke an Exception here
|
|
758
|
+
# (which would be not consistently generated since this will depend on the random data
|
|
759
|
+
# chosen by the client) we simply ignore un-parsable UTF-8 sequences.
|
|
760
|
+
return pw.decode('utf-8', errors="ignore")
|
|
675
761
|
|
|
676
762
|
def PwCrypt(self, password):
|
|
677
763
|
"""Obfuscate password.
|
|
@@ -690,25 +776,20 @@ class AuthPacket(Packet):
|
|
|
690
776
|
if self.authenticator is None:
|
|
691
777
|
self.authenticator = self.CreateAuthenticator()
|
|
692
778
|
|
|
693
|
-
if isinstance(password,
|
|
779
|
+
if isinstance(password, str):
|
|
694
780
|
password = password.encode('utf-8')
|
|
695
781
|
|
|
696
782
|
buf = password
|
|
697
783
|
if len(password) % 16 != 0:
|
|
698
|
-
buf +=
|
|
784
|
+
buf += b'\x00' * (16 - (len(password) % 16))
|
|
699
785
|
|
|
700
|
-
result =
|
|
786
|
+
result = b''
|
|
701
787
|
|
|
702
788
|
last = self.authenticator
|
|
703
789
|
while buf:
|
|
704
790
|
hash = md5_constructor(self.secret + last).digest()
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
result += bytes((hash[i] ^ buf[i],))
|
|
708
|
-
else:
|
|
709
|
-
for i in range(16):
|
|
710
|
-
result += chr(ord(hash[i]) ^ ord(buf[i]))
|
|
711
|
-
|
|
791
|
+
for i in range(16):
|
|
792
|
+
result += bytes((hash[i] ^ buf[i],))
|
|
712
793
|
last = result[-16:]
|
|
713
794
|
buf = buf[16:]
|
|
714
795
|
|
|
@@ -726,16 +807,14 @@ class AuthPacket(Packet):
|
|
|
726
807
|
if not self.authenticator:
|
|
727
808
|
self.authenticator = self.CreateAuthenticator()
|
|
728
809
|
|
|
729
|
-
if isinstance(userpwd,
|
|
810
|
+
if isinstance(userpwd, str):
|
|
730
811
|
userpwd = userpwd.strip().encode('utf-8')
|
|
731
812
|
|
|
732
813
|
chap_password = tools.DecodeOctets(self.get(3)[0])
|
|
733
814
|
if len(chap_password) != 17:
|
|
734
815
|
return False
|
|
735
816
|
|
|
736
|
-
chapid = chap_password[
|
|
737
|
-
if six.PY3:
|
|
738
|
-
chapid = chr(chapid).encode('utf-8')
|
|
817
|
+
chapid = chap_password[:1]
|
|
739
818
|
password = chap_password[1:]
|
|
740
819
|
|
|
741
820
|
challenge = self.authenticator
|
|
@@ -746,11 +825,11 @@ class AuthPacket(Packet):
|
|
|
746
825
|
def VerifyAuthRequest(self):
|
|
747
826
|
"""Verify request authenticator.
|
|
748
827
|
|
|
749
|
-
:return: True if verification
|
|
828
|
+
:return: True if verification passed else False
|
|
750
829
|
:rtype: boolean
|
|
751
830
|
"""
|
|
752
|
-
assert(self.raw_packet)
|
|
753
|
-
hash = md5_constructor(self.raw_packet[0:4] + 16 *
|
|
831
|
+
assert (self.raw_packet)
|
|
832
|
+
hash = md5_constructor(self.raw_packet[0:4] + 16 * b'\x00' +
|
|
754
833
|
self.raw_packet[20:] + self.secret).digest()
|
|
755
834
|
return hash == self.authenticator
|
|
756
835
|
|
|
@@ -760,7 +839,7 @@ class AcctPacket(Packet):
|
|
|
760
839
|
of the generic :obj:`Packet` class for accounting packets.
|
|
761
840
|
"""
|
|
762
841
|
|
|
763
|
-
def __init__(self, code=AccountingRequest, id=None, secret=
|
|
842
|
+
def __init__(self, code=AccountingRequest, id=None, secret=b'',
|
|
764
843
|
authenticator=None, **attributes):
|
|
765
844
|
"""Constructor
|
|
766
845
|
|
|
@@ -776,8 +855,6 @@ class AcctPacket(Packet):
|
|
|
776
855
|
:type packet: string
|
|
777
856
|
"""
|
|
778
857
|
Packet.__init__(self, code, id, secret, authenticator, **attributes)
|
|
779
|
-
if 'packet' in attributes:
|
|
780
|
-
self.raw_packet = attributes['packet']
|
|
781
858
|
|
|
782
859
|
def CreateReply(self, **attributes):
|
|
783
860
|
"""Create a new packet as a reply to this one. This method
|
|
@@ -791,12 +868,12 @@ class AcctPacket(Packet):
|
|
|
791
868
|
def VerifyAcctRequest(self):
|
|
792
869
|
"""Verify request authenticator.
|
|
793
870
|
|
|
794
|
-
:return:
|
|
871
|
+
:return: True if verification passed else False
|
|
795
872
|
:rtype: boolean
|
|
796
873
|
"""
|
|
797
|
-
assert(self.raw_packet)
|
|
874
|
+
assert (self.raw_packet)
|
|
798
875
|
|
|
799
|
-
hash = md5_constructor(self.raw_packet[0:4] + 16 *
|
|
876
|
+
hash = md5_constructor(self.raw_packet[0:4] + 16 * b'\x00' +
|
|
800
877
|
self.raw_packet[20:] + self.secret).digest()
|
|
801
878
|
|
|
802
879
|
return hash == self.authenticator
|
|
@@ -818,7 +895,7 @@ class AcctPacket(Packet):
|
|
|
818
895
|
|
|
819
896
|
attr = self._PktEncodeAttributes()
|
|
820
897
|
header = struct.pack('!BBH', self.code, self.id, (20 + len(attr)))
|
|
821
|
-
self.authenticator = md5_constructor(header[0:4] + 16 *
|
|
898
|
+
self.authenticator = md5_constructor(header[0:4] + 16 * b'\x00' +
|
|
822
899
|
attr + self.secret).digest()
|
|
823
900
|
|
|
824
901
|
ans = header + self.authenticator + attr
|
|
@@ -831,8 +908,8 @@ class CoAPacket(Packet):
|
|
|
831
908
|
of the generic :obj:`Packet` class for CoA packets.
|
|
832
909
|
"""
|
|
833
910
|
|
|
834
|
-
def __init__(self, code=CoARequest, id=None, secret=
|
|
835
|
-
|
|
911
|
+
def __init__(self, code=CoARequest, id=None, secret=b'',
|
|
912
|
+
authenticator=None, **attributes):
|
|
836
913
|
"""Constructor
|
|
837
914
|
|
|
838
915
|
:param dict: RADIUS dictionary
|
|
@@ -847,8 +924,6 @@ class CoAPacket(Packet):
|
|
|
847
924
|
:type packet: string
|
|
848
925
|
"""
|
|
849
926
|
Packet.__init__(self, code, id, secret, authenticator, **attributes)
|
|
850
|
-
if 'packet' in attributes:
|
|
851
|
-
self.raw_packet = attributes['packet']
|
|
852
927
|
|
|
853
928
|
def CreateReply(self, **attributes):
|
|
854
929
|
"""Create a new packet as a reply to this one. This method
|
|
@@ -862,11 +937,11 @@ class CoAPacket(Packet):
|
|
|
862
937
|
def VerifyCoARequest(self):
|
|
863
938
|
"""Verify request authenticator.
|
|
864
939
|
|
|
865
|
-
:return:
|
|
940
|
+
:return: True if verification passed else False
|
|
866
941
|
:rtype: boolean
|
|
867
942
|
"""
|
|
868
|
-
assert(self.raw_packet)
|
|
869
|
-
hash = md5_constructor(self.raw_packet[0:4] + 16 *
|
|
943
|
+
assert (self.raw_packet)
|
|
944
|
+
hash = md5_constructor(self.raw_packet[0:4] + 16 * b'\x00' +
|
|
870
945
|
self.raw_packet[20:] + self.secret).digest()
|
|
871
946
|
return hash == self.authenticator
|
|
872
947
|
|
|
@@ -885,13 +960,13 @@ class CoAPacket(Packet):
|
|
|
885
960
|
self.id = self.CreateID()
|
|
886
961
|
|
|
887
962
|
header = struct.pack('!BBH', self.code, self.id, (20 + len(attr)))
|
|
888
|
-
self.authenticator = md5_constructor(header[0:4] + 16 *
|
|
963
|
+
self.authenticator = md5_constructor(header[0:4] + 16 * b'\x00' +
|
|
889
964
|
attr + self.secret).digest()
|
|
890
965
|
|
|
891
966
|
if self.message_authenticator:
|
|
892
967
|
self._refresh_message_authenticator()
|
|
893
968
|
attr = self._PktEncodeAttributes()
|
|
894
|
-
self.authenticator = md5_constructor(header[0:4] + 16 *
|
|
969
|
+
self.authenticator = md5_constructor(header[0:4] + 16 * b'\x00' +
|
|
895
970
|
attr + self.secret).digest()
|
|
896
971
|
|
|
897
972
|
return header + self.authenticator + attr
|