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