# This file is part of Scapy
# Scapy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# any later version.
#
# Scapy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Scapy. If not, see <http://www.gnu.org/licenses/>.

# Copyright (C) 2017 Francois Contat <francois.contat@ssi.gouv.fr>

# Based on tacacs+ v6 draft https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06

# scapy.contrib.description = TACACS+ Protocol
# scapy.contrib.status = loads

import struct
import hashlib

from scapy.packet import Packet, bind_layers
from scapy.fields import ByteEnumField, ByteField, IntField
from scapy.fields import FieldListField
from scapy.fields import FieldLenField, ConditionalField, StrLenField
from scapy.layers.inet import TCP
from scapy.compat import chb, orb
from scapy.config import conf
from scapy.modules.six.moves import range

SECRET = 'test'

def obfuscate(pay, secret, session_id, version, seq):

    '''

    Obfuscation methodology from section 3.7
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.7

    '''

    pad = b""
    curr_pad = b""

    # pad length must equal the payload to obfuscate.
    # pad = {MD5_1 [,MD5_2 [ ... ,MD5_n]]}

    while len(pad) < len(pay):

        msg = hashlib.md5()
        msg.update(struct.pack('!I', session_id))
        msg.update(secret.encode())
        msg.update(struct.pack('!BB', version, seq))
        msg.update(curr_pad)
        curr_pad = msg.digest()
        pad += curr_pad

    # Obf/Unobfuscation via XOR operation between plaintext and pad

    return b"".join(chb(orb(pad[i]) ^ orb(pay[i])) for i in range(len(pay)))

TACACSPRIVLEVEL = {15:'Root',
                   1:'User',
                   0:'Minimum'}

##########################
# Authentication Packets #
##########################

TACACSVERSION = {1:'Tacacs',
                 192:'Tacacs+'}

TACACSTYPE = {1:'Authentication',
              2:'Authorization',
              3:'Accounting'}

TACACSFLAGS = {1:'Unencrypted',
               4:'Single Connection'}

TACACSAUTHENACTION = {1:'Login',
                      2:'Change Pass',
                      4:'Send Authentication'}

TACACSAUTHENTYPE = {1:'ASCII',
                    2:'PAP',
                    3:'CHAP',
                    4:'ARAP', #Deprecated
                    5:'MSCHAP',
                    6:'MSCHAPv2'}

TACACSAUTHENSERVICE = {0:'None',
                       1:'Login',
                       2:'Enable',
                       3:'PPP',
                       4:'ARAP',
                       5:'PT',
                       6:'RCMD',
                       7:'X25',
                       8:'NASI',
                       9:'FwProxy'}

TACACSREPLYPASS = {1:'PASS',
                   2:'FAIL',
                   3:'GETDATA',
                   4:'GETUSER',
                   5:'GETPASS',
                   6:'RESTART',
                   7:'ERROR',
                   21:'FOLLOW'}

TACACSREPLYFLAGS = {1:'NOECHO'}

TACACSCONTINUEFLAGS = {1:'ABORT'}


class TacacsAuthenticationStart(Packet):

    '''

    Tacacs authentication start body from section 4.1
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.1

    '''

    name = 'Tacacs Authentication Start Body'
    fields_desc = [ByteEnumField('action', 1, TACACSAUTHENACTION),
                   ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
                   ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
                   ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
                   FieldLenField('user_len', None, fmt='!B', length_of='user'),
                   FieldLenField('port_len', None, fmt='!B', length_of='port'),
                   FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'),
                   FieldLenField('data_len', None, fmt='!B', length_of='data'),
                   ConditionalField(StrLenField('user', '', length_from=lambda x: x.user_len),
                                    lambda x: x != ''),
                   StrLenField('port', '', length_from=lambda x: x.port_len),
                   StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len),
                   StrLenField('data', '', length_from=lambda x: x.data_len)]

class TacacsAuthenticationReply(Packet):

    '''

    Tacacs authentication reply body from section 4.2
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.2

    '''

    name = 'Tacacs Authentication Reply Body'
    fields_desc = [ByteEnumField('status', 1, TACACSREPLYPASS),
                   ByteEnumField('flags', 0, TACACSREPLYFLAGS),
                   FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'),
                   FieldLenField('data_len', None, fmt='!H', length_of='data'),
                   StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len),
                   StrLenField('data', '', length_from=lambda x: x.data_len)]

class TacacsAuthenticationContinue(Packet):

    '''

    Tacacs authentication continue body from section 4.3
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-4.3

    '''

    name = 'Tacacs Authentication Continue Body'
    fields_desc = [FieldLenField('user_msg_len', None, fmt='!H', length_of='user_msg'),
                   FieldLenField('data_len', None, fmt='!H', length_of='data'),
                   ByteEnumField('flags', 1, TACACSCONTINUEFLAGS),
                   StrLenField('user_msg', '', length_from=lambda x: x.user_msg_len),
                   StrLenField('data', '', length_from=lambda x: x.data_len)]

#########################
# Authorization Packets #
#########################

TACACSAUTHORTYPE = {0:'Not Set',
                    1:'None',
                    2:'Kerberos 5',
                    3:'Line',
                    4:'Enable',
                    5:'Local',
                    6:'Tacacs+',
                    8:'Guest',
                    16:'Radius',
                    17:'Kerberos 4',
                    32:'RCMD'}

TACACSAUTHORSTATUS = {1:'Pass Add',
                      2:'Pass repl',
                      16:'Fail',
                      17:'Error',
                      33:'Follow'}

class TacacsAuthorizationRequest(Packet):

    '''

    Tacacs authorization request body from section 5.1
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-5.1

    '''

    name = 'Tacacs Authorization Request Body'
    fields_desc = [ByteEnumField('authen_method', 0, TACACSAUTHORTYPE),
                   ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
                   ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
                   ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
                   FieldLenField('user_len', None, fmt='!B', length_of='user'),
                   FieldLenField('port_len', None, fmt='!B', length_of='port'),
                   FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'),
                   FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'),
                   FieldListField('arg_len_list', [], ByteField('', 0),
                                  length_from=lambda pkt: pkt.arg_cnt),
                   StrLenField('user', '', length_from=lambda x: x.user_len),
                   StrLenField('port', '', length_from=lambda x: x.port_len),
                   StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len)]

    def guess_payload_class(self, pay):
        if self.arg_cnt > 0:
            return TacacsPacketArguments
        return conf.padding_layer

class TacacsAuthorizationReply(Packet):

    '''

    Tacacs authorization reply body from section 5.2
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-5.2

    '''

    name = 'Tacacs Authorization Reply Body'
    fields_desc = [ByteEnumField('status', 0, TACACSAUTHORSTATUS),
                   FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'),
                   FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'),
                   FieldLenField('data_len', None, fmt='!H', length_of='data'),
                   FieldListField('arg_len_list', [], ByteField('', 0),
                                  length_from=lambda pkt: pkt.arg_cnt),
                   StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len),
                   StrLenField('data', '', length_from=lambda x: x.data_len)]

    def guess_payload_class(self, pay):
        if self.arg_cnt > 0:
            return TacacsPacketArguments
        return conf.padding_layer


######################
# Accounting Packets #
######################

TACACSACNTFLAGS = {2:'Start',
                   4:'Stop',
                   8:'Watchdog'}

TACACSACNTSTATUS = {1:'Success',
                    2:'Error',
                    33:'Follow'}

class TacacsAccountingRequest(Packet):

    '''

    Tacacs accounting request body from section 6.1
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-6.1

    '''

    name = 'Tacacs Accounting Request Body'
    fields_desc = [ByteEnumField('flags', 0, TACACSACNTFLAGS),
                   ByteEnumField('authen_method', 0, TACACSAUTHORTYPE),
                   ByteEnumField('priv_lvl', 1, TACACSPRIVLEVEL),
                   ByteEnumField('authen_type', 1, TACACSAUTHENTYPE),
                   ByteEnumField('authen_service', 1, TACACSAUTHENSERVICE),
                   FieldLenField('user_len', None, fmt='!B', length_of='user'),
                   FieldLenField('port_len', None, fmt='!B', length_of='port'),
                   FieldLenField('rem_addr_len', None, fmt='!B', length_of='rem_addr'),
                   FieldLenField('arg_cnt', None, fmt='!B', count_of='arg_len_list'),
                   FieldListField('arg_len_list', [], ByteField('', 0),
                                  length_from=lambda pkt: pkt.arg_cnt),
                   StrLenField('user', '', length_from=lambda x: x.user_len),
                   StrLenField('port', '', length_from=lambda x: x.port_len),
                   StrLenField('rem_addr', '', length_from=lambda x: x.rem_addr_len)]

    def guess_payload_class(self, pay):
        if self.arg_cnt > 0:
            return TacacsPacketArguments
        return conf.padding_layer

class TacacsAccountingReply(Packet):

    '''

    Tacacs accounting reply body from section 6.2
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-6.2

    '''

    name = 'Tacacs Accounting Reply Body'
    fields_desc = [FieldLenField('server_msg_len', None, fmt='!H', length_of='server_msg'),
                   FieldLenField('data_len', None, fmt='!H', length_of='data'),
                   ByteEnumField('status', None, TACACSACNTSTATUS),
                   StrLenField('server_msg', '', length_from=lambda x: x.server_msg_len),
                   StrLenField('data', '', length_from=lambda x: x.data_len)]

class TacacsPacketArguments(Packet):

    '''

    Class defined to handle the arguments listed at the end of tacacs+
    Authorization and Accounting packets.

    '''

    __slots__ = ['_len']
    name = 'Arguments in Tacacs+ packet'
    fields_desc = [StrLenField('data', '', length_from=lambda pkt: pkt._len)]

    def pre_dissect(self, s):
        cur = self.underlayer
        i = 0

        # Searching the position in layer in order to get its length

        while isinstance(cur, TacacsPacketArguments):
            cur = cur.underlayer
            i += 1
        self._len = cur.arg_len_list[i]
        return s

    def guess_payload_class(self, pay):
        cur = self.underlayer
        i = 0

        # Guessing if Argument packet. Nothing in encapsulated via tacacs+

        while isinstance(cur, TacacsPacketArguments):
            cur = cur.underlayer
            i += 1
        if i+1 < cur.arg_cnt:
            return TacacsPacketArguments
        return conf.padding_layer



class TacacsClientPacket(Packet):

    '''

    Super class for tacacs packet in order to get them uncrypted
    Obfuscation methodology from section 3.7
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.7

    '''
    def post_dissect(self, pay):

        if self.flags == 0:
            pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq)
            return pay

class TacacsHeader(TacacsClientPacket):

    '''

    Tacacs Header packet from section 3.8
    https://tools.ietf.org/html/draft-ietf-opsawg-tacacs-06#section-3.8

    '''

    name = 'Tacacs Header'
    fields_desc = [ByteEnumField('version', 192, TACACSVERSION),
                   ByteEnumField('type', 1, TACACSTYPE),
                   ByteField('seq', 1),
                   ByteEnumField('flags', 0, TACACSFLAGS),
                   IntField('session_id', 0),
                   IntField('length', None)]

    def guess_payload_class(self, payload):

        # Guessing packet type from type and seq values

        # Authentication packet - type 1

        if self.type == 1:
            if self.seq % 2 == 0:
                return TacacsAuthenticationReply
            if sum(struct.unpack('bbbb', payload[4:8])) == len(payload[8:]):
                return TacacsAuthenticationStart
            elif sum(struct.unpack('!hh', payload[:4])) == len(payload[5:]):
                return TacacsAuthenticationContinue

        # Authorization packet - type 2

        if self.type == 2:
            if self.seq % 2 == 0:
                return TacacsAuthorizationReply
            return TacacsAuthorizationRequest

        # Accounting packet - type 3

        if self.type == 3:
            if self.seq % 2 == 0:
                return TacacsAccountingReply
            return TacacsAccountingRequest

        return conf.raw_layer

    def post_build(self, p, pay):

        # Setting length of packet to obfuscate if not filled by user

        if self.length is None and pay:
            p = p[:-4] + struct.pack('!I', len(pay))


        if self.flags == 0:

            pay = obfuscate(pay, SECRET, self.session_id, self.version, self.seq)
            return p + pay

        return p

    def hashret(self):
        return struct.pack('I', self.session_id)

    def answers(self, other):
        return (isinstance(other, TacacsHeader) and
                self.seq == other.seq + 1 and
                self.type == other.type and
                self.session_id == other.session_id)


bind_layers(TCP, TacacsHeader, dport=49)
bind_layers(TCP, TacacsHeader, sport=49)
bind_layers(TacacsHeader, TacacsAuthenticationStart, type=1, dport=49)
bind_layers(TacacsHeader, TacacsAuthenticationReply, type=1, sport=49)

if __name__ == '__main__':
    from scapy.main import interact
    interact(mydict=globals(), mybanner='tacacs+')
