1# packet.py 2# 3# Copyright 2002-2005,2007 Wichert Akkerman <wichert@wiggy.net> 4# 5# A RADIUS packet as defined in RFC 2138 6 7from collections import OrderedDict 8import struct 9try: 10 import secrets 11 random_generator = secrets.SystemRandom() 12except ImportError: 13 import random 14 random_generator = random.SystemRandom() 15import hmac 16 17import sys 18if sys.version_info >= (3, 0): 19 hmac_new = lambda *x, **y: hmac.new(*x, digestmod='MD5', **y) 20else: 21 hmac_new = hmac.new 22 23try: 24 import hashlib 25 md5_constructor = hashlib.md5 26except ImportError: 27 # BBB for python 2.4 28 import md5 29 md5_constructor = md5.new 30import six 31from pyrad import tools 32 33# Packet codes 34AccessRequest = 1 35AccessAccept = 2 36AccessReject = 3 37AccountingRequest = 4 38AccountingResponse = 5 39AccessChallenge = 11 40StatusServer = 12 41StatusClient = 13 42DisconnectRequest = 40 43DisconnectACK = 41 44DisconnectNAK = 42 45CoARequest = 43 46CoAACK = 44 47CoANAK = 45 48 49# Current ID 50CurrentID = random_generator.randrange(1, 255) 51 52 53class PacketError(Exception): 54 pass 55 56 57class Packet(OrderedDict): 58 """Packet acts like a standard python map to provide simple access 59 to the RADIUS attributes. Since RADIUS allows for repeated 60 attributes the value will always be a sequence. pyrad makes sure 61 to preserve the ordering when encoding and decoding packets. 62 63 There are two ways to use the map intereface: if attribute 64 names are used pyrad take care of en-/decoding data. If 65 the attribute type number (or a vendor ID/attribute type 66 tuple for vendor attributes) is used you work with the 67 raw data. 68 69 Normally you will not use this class directly, but one of the 70 :obj:`AuthPacket` or :obj:`AcctPacket` classes. 71 """ 72 73 def __init__(self, code=0, id=None, secret=six.b(''), authenticator=None, 74 **attributes): 75 """Constructor 76 77 :param dict: RADIUS dictionary 78 :type dict: pyrad.dictionary.Dictionary class 79 :param secret: secret needed to communicate with a RADIUS server 80 :type secret: string 81 :param id: packet identification number 82 :type id: integer (8 bits) 83 :param code: packet type code 84 :type code: integer (8bits) 85 :param packet: raw packet to decode 86 :type packet: string 87 """ 88 OrderedDict.__init__(self) 89 self.code = code 90 if id is not None: 91 self.id = id 92 else: 93 self.id = CreateID() 94 if not isinstance(secret, six.binary_type): 95 raise TypeError('secret must be a binary string') 96 self.secret = secret 97 if authenticator is not None and \ 98 not isinstance(authenticator, six.binary_type): 99 raise TypeError('authenticator must be a binary string') 100 self.authenticator = authenticator 101 self.message_authenticator = None 102 self.raw_packet = None 103 104 if 'dict' in attributes: 105 self.dict = attributes['dict'] 106 107 if 'packet' in attributes: 108 self.raw_packet = attributes['packet'] 109 self.DecodePacket(self.raw_packet) 110 111 if 'message_authenticator' in attributes: 112 self.message_authenticator = attributes['message_authenticator'] 113 114 for (key, value) in attributes.items(): 115 if key in [ 116 'dict', 'fd', 'packet', 117 'message_authenticator', 118 ]: 119 continue 120 key = key.replace('_', '-') 121 self.AddAttribute(key, value) 122 123 def add_message_authenticator(self): 124 125 self.message_authenticator = True 126 # Maintain a zero octets content for md5 and hmac calculation. 127 self['Message-Authenticator'] = 16 * six.b('\00') 128 129 if self.id is None: 130 self.id = self.CreateID() 131 132 if self.authenticator is None and self.code == AccessRequest: 133 self.authenticator = self.CreateAuthenticator() 134 self._refresh_message_authenticator() 135 136 def get_message_authenticator(self): 137 self._refresh_message_authenticator() 138 return self.message_authenticator 139 140 def _refresh_message_authenticator(self): 141 hmac_constructor = hmac_new(self.secret) 142 143 # Maintain a zero octets content for md5 and hmac calculation. 144 self['Message-Authenticator'] = 16 * six.b('\00') 145 attr = self._PktEncodeAttributes() 146 147 header = struct.pack('!BBH', self.code, self.id, 148 (20 + len(attr))) 149 150 hmac_constructor.update(header[0:4]) 151 if self.code in (AccountingRequest, DisconnectRequest, 152 CoARequest, AccountingResponse): 153 hmac_constructor.update(16 * six.b('\00')) 154 else: 155 # NOTE: self.authenticator on reply packet is initialized 156 # with request authenticator by design. 157 # For AccessAccept, AccessReject and AccessChallenge 158 # it is needed use original Authenticator. 159 # For AccessAccept, AccessReject and AccessChallenge 160 # it is needed use original Authenticator. 161 if self.authenticator is None: 162 raise Exception('No authenticator found') 163 hmac_constructor.update(self.authenticator) 164 165 hmac_constructor.update(attr) 166 self['Message-Authenticator'] = hmac_constructor.digest() 167 168 def verify_message_authenticator(self, secret=None, 169 original_authenticator=None, 170 original_code=None): 171 """Verify packet Message-Authenticator. 172 173 :return: False if verification failed else True 174 :rtype: boolean 175 """ 176 if self.message_authenticator is None: 177 raise Exception('No Message-Authenticator AVP present') 178 179 prev_ma = self['Message-Authenticator'] 180 # Set zero bytes for Message-Authenticator for md5 calculation 181 if secret is None and self.secret is None: 182 raise Exception('Missing secret for HMAC/MD5 verification') 183 184 if secret: 185 key = secret 186 else: 187 key = self.secret 188 189 # If there's a raw packet, use that to calculate the expected 190 # Message-Authenticator. While the Packet class keeps multiple 191 # instances of an attribute grouped together in the attribute list, 192 # other applications may not. Using _PktEncodeAttributes to get 193 # the attributes could therefore end up changing the attribute order 194 # because of the grouping Packet does, which would cause 195 # Message-Authenticator verification to fail. Using the raw packet 196 # instead, if present, ensures the verification is done using the 197 # attributes exactly as sent. 198 if self.raw_packet: 199 attr = self.raw_packet[20:] 200 attr = attr.replace(prev_ma[0], 16 * six.b('\00')) 201 else: 202 self['Message-Authenticator'] = 16 * six.b('\00') 203 attr = self._PktEncodeAttributes() 204 205 header = struct.pack('!BBH', self.code, self.id, 206 (20 + len(attr))) 207 208 hmac_constructor = hmac_new(key) 209 hmac_constructor.update(header) 210 if self.code in (AccountingRequest, DisconnectRequest, 211 CoARequest, AccountingResponse): 212 if original_code is None or original_code != StatusServer: 213 # TODO: Handle Status-Server response correctly. 214 hmac_constructor.update(16 * six.b('\00')) 215 elif self.code in (AccessAccept, AccessChallenge, 216 AccessReject): 217 if original_authenticator is None: 218 if self.authenticator: 219 # NOTE: self.authenticator on reply packet is initialized 220 # with request authenticator by design. 221 original_authenticator = self.authenticator 222 else: 223 raise Exception('Missing original authenticator') 224 225 hmac_constructor.update(original_authenticator) 226 else: 227 # On Access-Request and Status-Server use dynamic authenticator 228 hmac_constructor.update(self.authenticator) 229 230 hmac_constructor.update(attr) 231 self['Message-Authenticator'] = prev_ma[0] 232 return prev_ma[0] == hmac_constructor.digest() 233 234 def CreateReply(self, **attributes): 235 """Create a new packet as a reply to this one. This method 236 makes sure the authenticator and secret are copied over 237 to the new instance. 238 """ 239 return Packet(id=self.id, secret=self.secret, 240 authenticator=self.authenticator, dict=self.dict, 241 **attributes) 242 243 def _DecodeValue(self, attr, value): 244 if attr.values.HasBackward(value): 245 return attr.values.GetBackward(value) 246 else: 247 return tools.DecodeAttr(attr.type, value) 248 249 def _EncodeValue(self, attr, value): 250 result = '' 251 if attr.values.HasForward(value): 252 result = attr.values.GetForward(value) 253 else: 254 result = tools.EncodeAttr(attr.type, value) 255 256 if attr.encrypt == 2: 257 # salt encrypt attribute 258 result = self.SaltCrypt(result) 259 260 return result 261 262 def _EncodeKeyValues(self, key, values): 263 if not isinstance(key, str): 264 return (key, values) 265 266 if not isinstance(values, (list, tuple)): 267 values = [values] 268 269 key, _, tag = key.partition(":") 270 attr = self.dict.attributes[key] 271 key = self._EncodeKey(key) 272 if tag: 273 tag = struct.pack('B', int(tag)) 274 if attr.type == "integer": 275 return (key, [tag + self._EncodeValue(attr, v)[1:] for v in values]) 276 else: 277 return (key, [tag + self._EncodeValue(attr, v) for v in values]) 278 else: 279 return (key, [self._EncodeValue(attr, v) for v in values]) 280 281 def _EncodeKey(self, key): 282 if not isinstance(key, str): 283 return key 284 285 attr = self.dict.attributes[key] 286 if attr.vendor and not attr.is_sub_attribute: #sub attribute keys don't need vendor 287 return (self.dict.vendors.GetForward(attr.vendor), attr.code) 288 else: 289 return attr.code 290 291 def _DecodeKey(self, key): 292 """Turn a key into a string if possible""" 293 294 if self.dict.attrindex.HasBackward(key): 295 return self.dict.attrindex.GetBackward(key) 296 return key 297 298 def AddAttribute(self, key, value): 299 """Add an attribute to the packet. 300 301 :param key: attribute name or identification 302 :type key: string, attribute code or (vendor code, attribute code) 303 tuple 304 :param value: value 305 :type value: depends on type of attribute 306 """ 307 attr = self.dict.attributes[key.partition(':')[0]] 308 309 (key, value) = self._EncodeKeyValues(key, value) 310 311 if attr.is_sub_attribute: 312 tlv = self.setdefault(self._EncodeKey(attr.parent.name), {}) 313 encoded = tlv.setdefault(key, []) 314 else: 315 encoded = self.setdefault(key, []) 316 317 encoded.extend(value) 318 319 def get(self, key, failobj=None): 320 try: 321 res = self.__getitem__(key) 322 except KeyError: 323 res = failobj 324 return res 325 326 def __getitem__(self, key): 327 if not isinstance(key, six.string_types): 328 return OrderedDict.__getitem__(self, key) 329 330 values = OrderedDict.__getitem__(self, self._EncodeKey(key)) 331 attr = self.dict.attributes[key] 332 if attr.type == 'tlv': # return map from sub attribute code to its values 333 res = {} 334 for (sub_attr_key, sub_attr_val) in values.items(): 335 sub_attr_name = attr.sub_attributes[sub_attr_key] 336 sub_attr = self.dict.attributes[sub_attr_name] 337 for v in sub_attr_val: 338 res.setdefault(sub_attr_name, []).append(self._DecodeValue(sub_attr, v)) 339 return res 340 else: 341 res = [] 342 for v in values: 343 res.append(self._DecodeValue(attr, v)) 344 return res 345 346 def __contains__(self, key): 347 try: 348 return OrderedDict.__contains__(self, self._EncodeKey(key)) 349 except KeyError: 350 return False 351 352 has_key = __contains__ 353 354 def __delitem__(self, key): 355 OrderedDict.__delitem__(self, self._EncodeKey(key)) 356 357 def __setitem__(self, key, item): 358 if isinstance(key, six.string_types): 359 (key, item) = self._EncodeKeyValues(key, item) 360 OrderedDict.__setitem__(self, key, item) 361 else: 362 OrderedDict.__setitem__(self, key, item) 363 364 def keys(self): 365 return [self._DecodeKey(key) for key in OrderedDict.keys(self)] 366 367 @staticmethod 368 def CreateAuthenticator(): 369 """Create a packet authenticator. All RADIUS packets contain a sixteen 370 byte authenticator which is used to authenticate replies from the 371 RADIUS server and in the password hiding algorithm. This function 372 returns a suitable random string that can be used as an authenticator. 373 374 :return: valid packet authenticator 375 :rtype: binary string 376 """ 377 378 data = [] 379 for _ in range(16): 380 data.append(random_generator.randrange(0, 256)) 381 if six.PY3: 382 return bytes(data) 383 else: 384 return ''.join(chr(b) for b in data) 385 386 def CreateID(self): 387 """Create a packet ID. All RADIUS requests have a ID which is used to 388 identify a request. This is used to detect retries and replay attacks. 389 This function returns a suitable random number that can be used as ID. 390 391 :return: ID number 392 :rtype: integer 393 394 """ 395 return random_generator.randrange(0, 256) 396 397 def ReplyPacket(self): 398 """Create a ready-to-transmit authentication reply packet. 399 Returns a RADIUS packet which can be directly transmitted 400 to a RADIUS server. This differs with Packet() in how 401 the authenticator is calculated. 402 403 :return: raw packet 404 :rtype: string 405 """ 406 assert(self.authenticator) 407 assert(self.secret is not None) 408 409 if self.message_authenticator: 410 self._refresh_message_authenticator() 411 412 attr = self._PktEncodeAttributes() 413 header = struct.pack('!BBH', self.code, self.id, (20 + len(attr))) 414 415 authenticator = md5_constructor(header[0:4] + self.authenticator 416 + attr + self.secret).digest() 417 418 return header + authenticator + attr 419 420 def VerifyReply(self, reply, rawreply=None): 421 if reply.id != self.id: 422 return False 423 424 if rawreply is None: 425 rawreply = reply.ReplyPacket() 426 427 attr = reply._PktEncodeAttributes() 428 # The Authenticator field in an Accounting-Response packet is called 429 # the Response Authenticator, and contains a one-way MD5 hash 430 # calculated over a stream of octets consisting of the Accounting 431 # Response Code, Identifier, Length, the Request Authenticator field 432 # from the Accounting-Request packet being replied to, and the 433 # response attributes if any, followed by the shared secret. The 434 # resulting 16 octet MD5 hash value is stored in the Authenticator 435 # field of the Accounting-Response packet. 436 hash = md5_constructor(rawreply[0:4] + self.authenticator + 437 rawreply[20:] + self.secret).digest() 438 439 if hash != rawreply[4:20]: 440 return False 441 return True 442 443 def _PktEncodeAttribute(self, key, value): 444 if isinstance(key, tuple): 445 value = struct.pack('!L', key[0]) + \ 446 self._PktEncodeAttribute(key[1], value) 447 key = 26 448 449 return struct.pack('!BB', key, (len(value) + 2)) + value 450 451 def _PktEncodeTlv(self, tlv_key, tlv_value): 452 tlv_attr = self.dict.attributes[self._DecodeKey(tlv_key)] 453 curr_avp = six.b('') 454 avps = [] 455 max_sub_attribute_len = max(map(lambda item: len(item[1]), tlv_value.items())) 456 for i in range(max_sub_attribute_len): 457 sub_attr_encoding = six.b('') 458 for (code, datalst) in tlv_value.items(): 459 if i < len(datalst): 460 sub_attr_encoding += self._PktEncodeAttribute(code, datalst[i]) 461 # split above 255. assuming len of one instance of all sub tlvs is lower than 255 462 if (len(sub_attr_encoding) + len(curr_avp)) < 245: 463 curr_avp += sub_attr_encoding 464 else: 465 avps.append(curr_avp) 466 curr_avp = sub_attr_encoding 467 avps.append(curr_avp) 468 tlv_avps = [] 469 for avp in avps: 470 value = struct.pack('!BB', tlv_attr.code, (len(avp) + 2)) + avp 471 tlv_avps.append(value) 472 if tlv_attr.vendor: 473 vendor_avps = six.b('') 474 for avp in tlv_avps: 475 vendor_avps += struct.pack( 476 '!BBL', 26, (len(avp) + 6), 477 self.dict.vendors.GetForward(tlv_attr.vendor) 478 ) + avp 479 return vendor_avps 480 else: 481 return b''.join(tlv_avps) 482 483 def _PktEncodeAttributes(self): 484 result = six.b('') 485 for (code, datalst) in self.items(): 486 attribute = self.dict.attributes.get(self._DecodeKey(code)) 487 if attribute and attribute.type == 'tlv': 488 result += self._PktEncodeTlv(code, datalst) 489 else: 490 for data in datalst: 491 result += self._PktEncodeAttribute(code, data) 492 return result 493 494 def _PktDecodeVendorAttribute(self, data): 495 # Check if this packet is long enough to be in the 496 # RFC2865 recommended form 497 if len(data) < 6: 498 return [(26, data)] 499 500 (vendor, atype, length) = struct.unpack('!LBB', data[:6])[0:3] 501 attribute = self.dict.attributes.get(self._DecodeKey((vendor, atype))) 502 try: 503 if attribute and attribute.type == 'tlv': 504 self._PktDecodeTlvAttribute((vendor, atype), data[6:length + 4]) 505 tlvs = [] # tlv is added to the packet inside _PktDecodeTlvAttribute 506 else: 507 tlvs = [((vendor, atype), data[6:length + 4])] 508 except: 509 return [(26, data)] 510 511 sumlength = 4 + length 512 while len(data) > sumlength: 513 try: 514 atype, length = struct.unpack('!BB', data[sumlength:sumlength+2])[0:2] 515 except: 516 return [(26, data)] 517 tlvs.append(((vendor, atype), data[sumlength+2:sumlength+length])) 518 sumlength += length 519 return tlvs 520 521 def _PktDecodeTlvAttribute(self, code, data): 522 sub_attributes = self.setdefault(code, {}) 523 loc = 0 524 525 while loc < len(data): 526 atype, length = struct.unpack('!BB', data[loc:loc+2])[0:2] 527 sub_attributes.setdefault(atype, []).append(data[loc+2:loc+length]) 528 loc += length 529 530 def DecodePacket(self, packet): 531 """Initialize the object from raw packet data. Decode a packet as 532 received from the network and decode it. 533 534 :param packet: raw packet 535 :type packet: string""" 536 537 try: 538 (self.code, self.id, length, self.authenticator) = \ 539 struct.unpack('!BBH16s', packet[0:20]) 540 541 except struct.error: 542 raise PacketError('Packet header is corrupt') 543 if len(packet) != length: 544 raise PacketError('Packet has invalid length') 545 if length > 8192: 546 raise PacketError('Packet length is too long (%d)' % length) 547 548 self.clear() 549 550 packet = packet[20:] 551 while packet: 552 try: 553 (key, attrlen) = struct.unpack('!BB', packet[0:2]) 554 except struct.error: 555 raise PacketError('Attribute header is corrupt') 556 557 if attrlen < 2: 558 raise PacketError( 559 'Attribute length is too small (%d)' % attrlen) 560 561 value = packet[2:attrlen] 562 attribute = self.dict.attributes.get(self._DecodeKey(key)) 563 if key == 26: 564 for (key, value) in self._PktDecodeVendorAttribute(value): 565 self.setdefault(key, []).append(value) 566 elif key == 80: 567 # POST: Message Authenticator AVP is present. 568 self.message_authenticator = True 569 self.setdefault(key, []).append(value) 570 elif attribute and attribute.type == 'tlv': 571 self._PktDecodeTlvAttribute(key,value) 572 else: 573 self.setdefault(key, []).append(value) 574 575 packet = packet[attrlen:] 576 577 def SaltCrypt(self, value): 578 """Salt Encryption 579 580 :param value: plaintext value 581 :type password: unicode string 582 :return: obfuscated version of the value 583 :rtype: binary string 584 """ 585 586 if isinstance(value, six.text_type): 587 value = value.encode('utf-8') 588 589 if self.authenticator is None: 590 # self.authenticator = self.CreateAuthenticator() 591 self.authenticator = 16 * six.b('\x00') 592 593 random_value = 32768 + random_generator.randrange(0, 32767) 594 if six.PY3: 595 salt_raw = struct.pack('!H', random_value ) 596 salt = chr(salt_raw[0]) + chr(salt_raw[1]) 597 else: 598 salt = struct.pack('!H', random_value ) 599 salt = chr(ord(salt[0]) | 1 << 7)+salt[1] 600 601 result = six.b(salt) 602 603 length = struct.pack("B", len(value)) 604 buf = length + value 605 if len(buf) % 16 != 0: 606 buf += six.b('\x00') * (16 - (len(buf) % 16)) 607 608 last = self.authenticator + six.b(salt) 609 while buf: 610 hash = md5_constructor(self.secret + last).digest() 611 if six.PY3: 612 for i in range(16): 613 result += bytes((hash[i] ^ buf[i],)) 614 else: 615 for i in range(16): 616 result += chr(ord(hash[i]) ^ ord(buf[i])) 617 618 last = result[-16:] 619 buf = buf[16:] 620 621 return result 622 623 624class AuthPacket(Packet): 625 def __init__(self, code=AccessRequest, id=None, secret=six.b(''), 626 authenticator=None, auth_type='pap', **attributes): 627 """Constructor 628 629 :param code: packet type code 630 :type code: integer (8bits) 631 :param id: packet identification number 632 :type id: integer (8 bits) 633 :param secret: secret needed to communicate with a RADIUS server 634 :type secret: string 635 636 :param dict: RADIUS dictionary 637 :type dict: pyrad.dictionary.Dictionary class 638 639 :param packet: raw packet to decode 640 :type packet: string 641 """ 642 643 Packet.__init__(self, code, id, secret, authenticator, **attributes) 644 self.auth_type = auth_type 645 646 def CreateReply(self, **attributes): 647 """Create a new packet as a reply to this one. This method 648 makes sure the authenticator and secret are copied over 649 to the new instance. 650 """ 651 return AuthPacket(AccessAccept, self.id, 652 self.secret, self.authenticator, dict=self.dict, 653 auth_type=self.auth_type, **attributes) 654 655 def RequestPacket(self): 656 """Create a ready-to-transmit authentication request packet. 657 Return a RADIUS packet which can be directly transmitted 658 to a RADIUS server. 659 660 :return: raw packet 661 :rtype: string 662 """ 663 if self.authenticator is None: 664 self.authenticator = self.CreateAuthenticator() 665 666 if self.id is None: 667 self.id = self.CreateID() 668 669 if self.message_authenticator: 670 self._refresh_message_authenticator() 671 672 attr = self._PktEncodeAttributes() 673 if self.auth_type == 'eap-md5': 674 header = struct.pack( 675 '!BBH16s', self.code, self.id, (20 + 18 + len(attr)), self.authenticator 676 ) 677 digest = hmac_new( 678 self.secret, 679 header 680 + attr 681 + struct.pack('!BB16s', 80, struct.calcsize('!BB16s'), b''), 682 ).digest() 683 return ( 684 header 685 + attr 686 + struct.pack('!BB16s', 80, struct.calcsize('!BB16s'), digest) 687 ) 688 689 header = struct.pack('!BBH16s', self.code, self.id, 690 (20 + len(attr)), self.authenticator) 691 692 return header + attr 693 694 def PwDecrypt(self, password): 695 """Obfuscate a RADIUS password. RADIUS hides passwords in packets by 696 using an algorithm based on the MD5 hash of the packet authenticator 697 and RADIUS secret. This function reverses the obfuscation process. 698 699 :param password: obfuscated form of password 700 :type password: binary string 701 :return: plaintext password 702 :rtype: unicode string 703 """ 704 buf = password 705 pw = six.b('') 706 707 last = self.authenticator 708 while buf: 709 hash = md5_constructor(self.secret + last).digest() 710 if six.PY3: 711 for i in range(16): 712 pw += bytes((hash[i] ^ buf[i],)) 713 else: 714 for i in range(16): 715 pw += chr(ord(hash[i]) ^ ord(buf[i])) 716 717 (last, buf) = (buf[:16], buf[16:]) 718 719 while pw.endswith(six.b('\x00')): 720 pw = pw[:-1] 721 722 return pw.decode('utf-8') 723 724 def PwCrypt(self, password): 725 """Obfuscate password. 726 RADIUS hides passwords in packets by using an algorithm 727 based on the MD5 hash of the packet authenticator and RADIUS 728 secret. If no authenticator has been set before calling PwCrypt 729 one is created automatically. Changing the authenticator after 730 setting a password that has been encrypted using this function 731 will not work. 732 733 :param password: plaintext password 734 :type password: unicode string 735 :return: obfuscated version of the password 736 :rtype: binary string 737 """ 738 if self.authenticator is None: 739 self.authenticator = self.CreateAuthenticator() 740 741 if isinstance(password, six.text_type): 742 password = password.encode('utf-8') 743 744 buf = password 745 if len(password) % 16 != 0: 746 buf += six.b('\x00') * (16 - (len(password) % 16)) 747 748 result = six.b('') 749 750 last = self.authenticator 751 while buf: 752 hash = md5_constructor(self.secret + last).digest() 753 if six.PY3: 754 for i in range(16): 755 result += bytes((hash[i] ^ buf[i],)) 756 else: 757 for i in range(16): 758 result += chr(ord(hash[i]) ^ ord(buf[i])) 759 760 last = result[-16:] 761 buf = buf[16:] 762 763 return result 764 765 def VerifyChapPasswd(self, userpwd): 766 """ Verify RADIUS ChapPasswd 767 768 :param userpwd: plaintext password 769 :type userpwd: str 770 :return: is verify ok 771 :rtype: bool 772 """ 773 774 if not self.authenticator: 775 self.authenticator = self.CreateAuthenticator() 776 777 if isinstance(userpwd, six.text_type): 778 userpwd = userpwd.strip().encode('utf-8') 779 780 chap_password = tools.DecodeOctets(self.get(3)[0]) 781 if len(chap_password) != 17: 782 return False 783 784 chapid = chap_password[0] 785 if six.PY3: 786 chapid = chr(chapid).encode('utf-8') 787 password = chap_password[1:] 788 789 challenge = self.authenticator 790 if 'CHAP-Challenge' in self: 791 challenge = self['CHAP-Challenge'][0] 792 return password == md5_constructor(chapid + userpwd + challenge).digest() 793 794 def VerifyAuthRequest(self): 795 """Verify request authenticator. 796 797 :return: True if verification failed else False 798 :rtype: boolean 799 """ 800 assert(self.raw_packet) 801 hash = md5_constructor(self.raw_packet[0:4] + 16 * six.b('\x00') + 802 self.raw_packet[20:] + self.secret).digest() 803 return hash == self.authenticator 804 805 806class AcctPacket(Packet): 807 """RADIUS accounting packets. This class is a specialization 808 of the generic :obj:`Packet` class for accounting packets. 809 """ 810 811 def __init__(self, code=AccountingRequest, id=None, secret=six.b(''), 812 authenticator=None, **attributes): 813 """Constructor 814 815 :param dict: RADIUS dictionary 816 :type dict: pyrad.dictionary.Dictionary class 817 :param secret: secret needed to communicate with a RADIUS server 818 :type secret: string 819 :param id: packet identification number 820 :type id: integer (8 bits) 821 :param code: packet type code 822 :type code: integer (8bits) 823 :param packet: raw packet to decode 824 :type packet: string 825 """ 826 Packet.__init__(self, code, id, secret, authenticator, **attributes) 827 828 def CreateReply(self, **attributes): 829 """Create a new packet as a reply to this one. This method 830 makes sure the authenticator and secret are copied over 831 to the new instance. 832 """ 833 return AcctPacket(AccountingResponse, self.id, 834 self.secret, self.authenticator, dict=self.dict, 835 **attributes) 836 837 def VerifyAcctRequest(self): 838 """Verify request authenticator. 839 840 :return: False if verification failed else True 841 :rtype: boolean 842 """ 843 assert(self.raw_packet) 844 845 hash = md5_constructor(self.raw_packet[0:4] + 16 * six.b('\x00') + 846 self.raw_packet[20:] + self.secret).digest() 847 848 return hash == self.authenticator 849 850 def RequestPacket(self): 851 """Create a ready-to-transmit authentication request packet. 852 Return a RADIUS packet which can be directly transmitted 853 to a RADIUS server. 854 855 :return: raw packet 856 :rtype: string 857 """ 858 859 if self.id is None: 860 self.id = self.CreateID() 861 862 if self.message_authenticator: 863 self._refresh_message_authenticator() 864 865 attr = self._PktEncodeAttributes() 866 header = struct.pack('!BBH', self.code, self.id, (20 + len(attr))) 867 self.authenticator = md5_constructor(header[0:4] + 16 * six.b('\x00') + 868 attr + self.secret).digest() 869 870 ans = header + self.authenticator + attr 871 872 return ans 873 874 875class CoAPacket(Packet): 876 """RADIUS CoA packets. This class is a specialization 877 of the generic :obj:`Packet` class for CoA packets. 878 """ 879 880 def __init__(self, code=CoARequest, id=None, secret=six.b(''), 881 authenticator=None, **attributes): 882 """Constructor 883 884 :param dict: RADIUS dictionary 885 :type dict: pyrad.dictionary.Dictionary class 886 :param secret: secret needed to communicate with a RADIUS server 887 :type secret: string 888 :param id: packet identification number 889 :type id: integer (8 bits) 890 :param code: packet type code 891 :type code: integer (8bits) 892 :param packet: raw packet to decode 893 :type packet: string 894 """ 895 Packet.__init__(self, code, id, secret, authenticator, **attributes) 896 897 def CreateReply(self, **attributes): 898 """Create a new packet as a reply to this one. This method 899 makes sure the authenticator and secret are copied over 900 to the new instance. 901 """ 902 return CoAPacket(CoAACK, self.id, 903 self.secret, self.authenticator, dict=self.dict, 904 **attributes) 905 906 def VerifyCoARequest(self): 907 """Verify request authenticator. 908 909 :return: False if verification failed else True 910 :rtype: boolean 911 """ 912 assert(self.raw_packet) 913 hash = md5_constructor(self.raw_packet[0:4] + 16 * six.b('\x00') + 914 self.raw_packet[20:] + self.secret).digest() 915 return hash == self.authenticator 916 917 def RequestPacket(self): 918 """Create a ready-to-transmit CoA request packet. 919 Return a RADIUS packet which can be directly transmitted 920 to a RADIUS server. 921 922 :return: raw packet 923 :rtype: string 924 """ 925 926 attr = self._PktEncodeAttributes() 927 928 if self.id is None: 929 self.id = self.CreateID() 930 931 header = struct.pack('!BBH', self.code, self.id, (20 + len(attr))) 932 self.authenticator = md5_constructor(header[0:4] + 16 * six.b('\x00') + 933 attr + self.secret).digest() 934 935 if self.message_authenticator: 936 self._refresh_message_authenticator() 937 attr = self._PktEncodeAttributes() 938 self.authenticator = md5_constructor(header[0:4] + 16 * six.b('\x00') + 939 attr + self.secret).digest() 940 941 return header + self.authenticator + attr 942 943 944def CreateID(): 945 """Generate a packet ID. 946 947 :return: packet ID 948 :rtype: 8 bit integer 949 """ 950 global CurrentID 951 952 CurrentID = (CurrentID + 1) % 256 953 return CurrentID 954