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.
Files changed (53) hide show
  1. docs/Makefile +20 -0
  2. docs/make.bat +36 -0
  3. docs/source/_static/logo.png +0 -0
  4. docs/source/api/client.rst +10 -0
  5. docs/source/api/dictionary.rst +10 -0
  6. docs/source/api/host.rst +7 -0
  7. docs/source/api/packet.rst +48 -0
  8. docs/source/api/proxy.rst +7 -0
  9. docs/source/api/server.rst +13 -0
  10. docs/source/conf.py +158 -0
  11. docs/source/index.rst +75 -0
  12. example/acct.py +41 -0
  13. example/auth.py +37 -0
  14. example/auth_async.py +164 -0
  15. example/client-coa.py +61 -0
  16. example/coa.py +40 -0
  17. example/dictionary +405 -0
  18. example/dictionary.freeradius +91 -0
  19. example/pyrad.log +0 -0
  20. example/server.py +68 -0
  21. example/server_async.py +117 -0
  22. example/status.py +26 -0
  23. pyrad/__init__.py +3 -3
  24. pyrad/client.py +54 -9
  25. pyrad/client_async.py +22 -14
  26. pyrad/dictfile.py +2 -5
  27. pyrad/dictionary.py +12 -1
  28. pyrad/host.py +1 -1
  29. pyrad/packet.py +208 -133
  30. pyrad/proxy.py +2 -2
  31. pyrad/server.py +3 -7
  32. pyrad/server_async.py +4 -5
  33. pyrad/tests/__init__.py +2 -2
  34. pyrad/tests/mock.py +5 -1
  35. pyrad/tests/{testBidict.py → test_bidict.py} +2 -2
  36. pyrad/tests/{testClient.py → test_client.py} +28 -30
  37. pyrad/tests/{testDictionary.py → test_dictionary.py} +38 -21
  38. pyrad/tests/{testHost.py → test_host.py} +10 -10
  39. pyrad/tests/test_packet.py +679 -0
  40. pyrad/tests/{testProxy.py → test_proxy.py} +11 -11
  41. pyrad/tests/{testServer.py → test_server.py} +35 -33
  42. pyrad/tests/test_tools.py +126 -0
  43. pyrad/tools.py +254 -158
  44. {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info}/METADATA +44 -20
  45. pyrad-2.5.0.dist-info/RECORD +51 -0
  46. {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info}/WHEEL +1 -1
  47. {pyrad-2.3.dist-info → pyrad-2.5.0.dist-info/licenses}/LICENSE.txt +2 -1
  48. pyrad-2.5.0.dist-info/top_level.txt +3 -0
  49. pyrad/tests/testPacket.py +0 -530
  50. pyrad/tests/testTools.py +0 -122
  51. pyrad-2.3.dist-info/RECORD +0 -29
  52. pyrad-2.3.dist-info/top_level.txt +0 -1
  53. {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 struct
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 intereface: if attribute
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=six.b(''), authenticator=None,
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, six.binary_type):
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, six.binary_type):
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.DecodePacket(attributes['packet'])
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 * six.b('\00')
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 = hmac.new(self.secret)
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 * six.b('\00')
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 * six.b('\00'))
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
- self['Message-Authenticator'] = 16 * six.b('\00')
180
- attr = self._PktEncodeAttributes()
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 = hmac.new(key)
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 * six.b('\00'))
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
- return self.__getitem__(key) or failobj
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, six.string_types):
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, six.string_types):
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
- data = []
352
- for _ in range(16):
353
- data.append(random_generator.randrange(0, 256))
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
- attr = reply._PktEncodeAttributes()
401
- # The Authenticator field in an Accounting-Response packet is called
402
- # the Response Authenticator, and contains a one-way MD5 hash
403
- # calculated over a stream of octets consisting of the Accounting
404
- # Response Code, Identifier, Length, the Request Authenticator field
405
- # from the Accounting-Request packet being replied to, and the
406
- # response attributes if any, followed by the shared secret. The
407
- # resulting 16 octet MD5 hash value is stored in the Authenticator
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
- attr + self.secret).digest()
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 = six.b('')
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 = six.b('')
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 = six.b('')
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 = six.b('')
498
+ result = b''
458
499
  for (code, datalst) in self.items():
459
- attribute = self.dict.attributes.get(self._DecodeKey(code))
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 attribute and attribute.type == 'tlv':
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 attribute and attribute.type == 'tlv':
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
- """Salt Encryption
607
+ """SaltEncrypt
552
608
 
553
609
  :param value: plaintext value
554
- :type password: unicode string
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, six.text_type):
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 * six.b('\x00')
620
+ self.authenticator = 16 * b'\x00'
565
621
 
566
- salt = struct.pack('!H', random_generator.randrange(0, 65535))
567
- salt = chr(ord(salt[0]) | 1 << 7)+salt[1]
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
- buf = length + value
571
- if len(buf) % 16 != 0:
572
- buf += six.b('\x00') * (16 - (len(buf) % 16))
628
+ value = length + value
573
629
 
574
- result = six.b(salt)
630
+ # zero padding
631
+ if len(value) % 16 != 0:
632
+ value += b'\x00' * (16 - (len(value) % 16))
575
633
 
576
- last = self.authenticator + salt
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
- last = result[-16:]
587
- buf = buf[16:]
636
+ def SaltDecrypt(self, value):
637
+ """ SaltDecrypt
588
638
 
589
- return result
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=six.b(''),
594
- authenticator=None, **attributes):
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
- if 'packet' in attributes:
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 = six.b('')
742
+ pw = b''
658
743
 
659
744
  last = self.authenticator
660
745
  while buf:
661
746
  hash = md5_constructor(self.secret + last).digest()
662
- if six.PY3:
663
- for i in range(16):
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
- while pw.endswith(six.b('\x00')):
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
- return pw.decode('utf-8')
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, six.text_type):
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 += six.b('\x00') * (16 - (len(password) % 16))
784
+ buf += b'\x00' * (16 - (len(password) % 16))
699
785
 
700
- result = six.b('')
786
+ result = b''
701
787
 
702
788
  last = self.authenticator
703
789
  while buf:
704
790
  hash = md5_constructor(self.secret + last).digest()
705
- if six.PY3:
706
- for i in range(16):
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, six.text_type):
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[0]
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 failed else False
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 * six.b('\x00') +
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=six.b(''),
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: False if verification failed else True
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 * six.b('\x00') +
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 * six.b('\x00') +
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=six.b(''),
835
- authenticator=None, **attributes):
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: False if verification failed else True
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 * six.b('\x00') +
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 * six.b('\x00') +
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 * six.b('\x00') +
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