# SPDX-License-Identifier: GPL-2.0-only
# This file is part of Scapy
# See https://scapy.net/ for more information
# Copyright (C) Philippe Biondi <phil@secdev.org>

"""
ISAKMP (Internet Security Association and Key Management Protocol).
"""

# Mostly based on https://tools.ietf.org/html/rfc2408

from __future__ import absolute_import
import struct
from scapy.config import conf
from scapy.packet import Packet, bind_bottom_up, bind_top_down
from scapy.compat import chb
from scapy.fields import ByteEnumField, ByteField, FieldLenField, FlagsField, \
    IntEnumField, IntField, PacketLenField, ShortEnumField, ShortField, \
    StrFixedLenField, StrLenField, XByteField
from scapy.layers.inet import IP, UDP
from scapy.sendrecv import sr
from scapy.volatile import RandString
from scapy.error import warning
from functools import reduce

# TODO: some ISAKMP payloads are not implemented,
# and inherit a default ISAKMP_payload


# see http://www.iana.org/assignments/ipsec-registry for details
ISAKMPAttributeTypes = {"Encryption": (1, {"DES-CBC": 1,
                                           "IDEA-CBC": 2,
                                           "Blowfish-CBC": 3,
                                           "RC5-R16-B64-CBC": 4,
                                           "3DES-CBC": 5,
                                           "CAST-CBC": 6,
                                           "AES-CBC": 7,
                                           "CAMELLIA-CBC": 8, }, 0),
                        "Hash": (2, {"MD5": 1,
                                     "SHA": 2,
                                     "Tiger": 3,
                                     "SHA2-256": 4,
                                     "SHA2-384": 5,
                                     "SHA2-512": 6, }, 0),
                        "Authentication": (3, {"PSK": 1,
                                               "DSS": 2,
                                               "RSA Sig": 3,
                                               "RSA Encryption": 4,
                                               "RSA Encryption Revised": 5,
                                               "ElGamal Encryption": 6,
                                               "ElGamal Encryption Revised": 7,
                                               "ECDSA Sig": 8,
                                               "HybridInitRSA": 64221,
                                               "HybridRespRSA": 64222,
                                               "HybridInitDSS": 64223,
                                               "HybridRespDSS": 64224,
                                               "XAUTHInitPreShared": 65001,
                                               "XAUTHRespPreShared": 65002,
                                               "XAUTHInitDSS": 65003,
                                               "XAUTHRespDSS": 65004,
                                               "XAUTHInitRSA": 65005,
                                               "XAUTHRespRSA": 65006,
                                               "XAUTHInitRSAEncryption": 65007,
                                               "XAUTHRespRSAEncryption": 65008,
                                               "XAUTHInitRSARevisedEncryption": 65009,  # noqa: E501
                                               "XAUTHRespRSARevisedEncryptio": 65010, }, 0),  # noqa: E501
                        "GroupDesc": (4, {"768MODPgr": 1,
                                          "1024MODPgr": 2,
                                          "EC2Ngr155": 3,
                                          "EC2Ngr185": 4,
                                          "1536MODPgr": 5,
                                          "2048MODPgr": 14,
                                          "3072MODPgr": 15,
                                          "4096MODPgr": 16,
                                          "6144MODPgr": 17,
                                          "8192MODPgr": 18, }, 0),
                        "GroupType": (5, {"MODP": 1,
                                          "ECP": 2,
                                          "EC2N": 3}, 0),
                        "GroupPrime": (6, {}, 1),
                        "GroupGenerator1": (7, {}, 1),
                        "GroupGenerator2": (8, {}, 1),
                        "GroupCurveA": (9, {}, 1),
                        "GroupCurveB": (10, {}, 1),
                        "LifeType": (11, {"Seconds": 1,
                                          "Kilobytes": 2}, 0),
                        "LifeDuration": (12, {}, 1),
                        "PRF": (13, {}, 0),
                        "KeyLength": (14, {}, 0),
                        "FieldSize": (15, {}, 0),
                        "GroupOrder": (16, {}, 1),
                        }

# the name 'ISAKMPTransformTypes' is actually a misnomer (since the table
# holds info for all ISAKMP Attribute types, not just transforms, but we'll
# keep it for backwards compatibility... for now at least
ISAKMPTransformTypes = ISAKMPAttributeTypes

ISAKMPTransformNum = {}
for n in ISAKMPTransformTypes:
    val = ISAKMPTransformTypes[n]
    tmp = {}
    for e in val[1]:
        tmp[val[1][e]] = e
    ISAKMPTransformNum[val[0]] = (n, tmp, val[2])
del n
del e
del tmp
del val


class ISAKMPTransformSetField(StrLenField):
    islist = 1

    @staticmethod
    def type2num(type_val_tuple):
        typ, val = type_val_tuple
        type_val, enc_dict, tlv = ISAKMPTransformTypes.get(typ, (typ, {}, 0))
        val = enc_dict.get(val, val)
        s = b""
        if (val & ~0xffff):
            if not tlv:
                warning("%r should not be TLV but is too big => using TLV encoding" % typ)  # noqa: E501
            n = 0
            while val:
                s = chb(val & 0xff) + s
                val >>= 8
                n += 1
            val = n
        else:
            type_val |= 0x8000
        return struct.pack("!HH", type_val, val) + s

    @staticmethod
    def num2type(typ, enc):
        val = ISAKMPTransformNum.get(typ, (typ, {}))
        enc = val[1].get(enc, enc)
        return (val[0], enc)

    def i2m(self, pkt, i):
        if i is None:
            return b""
        i = [ISAKMPTransformSetField.type2num(e) for e in i]
        return b"".join(i)

    def m2i(self, pkt, m):
        # I try to ensure that we don't read off the end of our packet based
        # on bad length fields we're provided in the packet. There are still
        # conditions where struct.unpack() may not get enough packet data, but
        # worst case that should result in broken attributes (which would
        # be expected). (wam)
        lst = []
        while len(m) >= 4:
            trans_type, = struct.unpack("!H", m[:2])
            is_tlv = not (trans_type & 0x8000)
            if is_tlv:
                # We should probably check to make sure the attribute type we
                # are looking at is allowed to have a TLV format and issue a
                # warning if we're given an TLV on a basic attribute.
                value_len, = struct.unpack("!H", m[2:4])
                if value_len + 4 > len(m):
                    warning("Bad length for ISAKMP transform type=%#6x" % trans_type)  # noqa: E501
                value = m[4:4 + value_len]
                value = reduce(lambda x, y: (x << 8) | y, struct.unpack("!%s" % ("B" * len(value),), value), 0)  # noqa: E501
            else:
                trans_type &= 0x7fff
                value_len = 0
                value, = struct.unpack("!H", m[2:4])
            m = m[4 + value_len:]
            lst.append(ISAKMPTransformSetField.num2type(trans_type, value))
        if len(m) > 0:
            warning("Extra bytes after ISAKMP transform dissection [%r]" % m)
        return lst


ISAKMP_payload_type = ["None", "SA", "Proposal", "Transform", "KE", "ID",
                       "CERT", "CR", "Hash", "SIG", "Nonce", "Notification",
                       "Delete", "VendorID"]

ISAKMP_exchange_type = ["None", "base", "identity prot.",
                        "auth only", "aggressive", "info"]


class ISAKMP_class(Packet):
    def guess_payload_class(self, payload):
        np = self.next_payload
        if np == 0:
            return conf.raw_layer
        elif np < len(ISAKMP_payload_type):
            pt = ISAKMP_payload_type[np]
            return globals().get("ISAKMP_payload_%s" % pt, ISAKMP_payload)
        else:
            return ISAKMP_payload


class ISAKMP(ISAKMP_class):  # rfc2408
    name = "ISAKMP"
    fields_desc = [
        StrFixedLenField("init_cookie", "", 8),
        StrFixedLenField("resp_cookie", "", 8),
        ByteEnumField("next_payload", 0, ISAKMP_payload_type),
        XByteField("version", 0x10),
        ByteEnumField("exch_type", 0, ISAKMP_exchange_type),
        FlagsField("flags", 0, 8, ["encryption", "commit", "auth_only", "res3", "res4", "res5", "res6", "res7"]),  # XXX use a Flag field  # noqa: E501
        IntField("id", 0),
        IntField("length", None)
    ]

    def guess_payload_class(self, payload):
        if self.flags & 1:
            return conf.raw_layer
        return ISAKMP_class.guess_payload_class(self, payload)

    def answers(self, other):
        if isinstance(other, ISAKMP):
            if other.init_cookie == self.init_cookie:
                return 1
        return 0

    def post_build(self, p, pay):
        p += pay
        if self.length is None:
            p = p[:24] + struct.pack("!I", len(p)) + p[28:]
        return p


class ISAKMP_payload_Transform(ISAKMP_class):
    name = "IKE Transform"
    fields_desc = [
        ByteEnumField("next_payload", None, ISAKMP_payload_type),
        ByteField("res", 0),
        #        ShortField("len",None),
        ShortField("length", None),
        ByteField("num", None),
        ByteEnumField("id", 1, {1: "KEY_IKE"}),
        ShortField("res2", 0),
        ISAKMPTransformSetField("transforms", None, length_from=lambda x: x.length - 8)  # noqa: E501
        #        XIntField("enc",0x80010005L),
        #        XIntField("hash",0x80020002L),
        #        XIntField("auth",0x80030001L),
        #        XIntField("group",0x80040002L),
        #        XIntField("life_type",0x800b0001L),
        #        XIntField("durationh",0x000c0004L),
        #        XIntField("durationl",0x00007080L),
    ]

    def post_build(self, p, pay):
        if self.length is None:
            tmp_len = len(p)
            tmp_pay = p[:2] + chb((tmp_len >> 8) & 0xff)
            p = tmp_pay + chb(tmp_len & 0xff) + p[4:]
        p += pay
        return p


# https://tools.ietf.org/html/rfc2408#section-3.5
class ISAKMP_payload_Proposal(ISAKMP_class):
    name = "IKE proposal"
#    ISAKMP_payload_type = 0
    fields_desc = [
        ByteEnumField("next_payload", None, ISAKMP_payload_type),
        ByteField("res", 0),
        FieldLenField("length", None, "trans", "H", adjust=lambda pkt, x:x + 8),  # noqa: E501
        ByteField("proposal", 1),
        ByteEnumField("proto", 1, {1: "ISAKMP"}),
        FieldLenField("SPIsize", None, "SPI", "B"),
        ByteField("trans_nb", None),
        StrLenField("SPI", "", length_from=lambda x: x.SPIsize),
        PacketLenField("trans", conf.raw_layer(), ISAKMP_payload_Transform, length_from=lambda x: x.length - 8),  # noqa: E501
    ]


class ISAKMP_payload(ISAKMP_class):
    name = "ISAKMP payload"
    fields_desc = [
        ByteEnumField("next_payload", None, ISAKMP_payload_type),
        ByteField("res", 0),
        FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 4),
        StrLenField("load", "", length_from=lambda x:x.length - 4),
    ]


class ISAKMP_payload_VendorID(ISAKMP_payload):
    name = "ISAKMP Vendor ID"


class ISAKMP_payload_SA(ISAKMP_class):
    name = "ISAKMP SA"
    fields_desc = [
        ByteEnumField("next_payload", None, ISAKMP_payload_type),
        ByteField("res", 0),
        FieldLenField("length", None, "prop", "H", adjust=lambda pkt, x:x + 12),  # noqa: E501
        IntEnumField("DOI", 1, {1: "IPSEC"}),
        IntEnumField("situation", 1, {1: "identity"}),
        PacketLenField("prop", conf.raw_layer(), ISAKMP_payload_Proposal, length_from=lambda x: x.length - 12),  # noqa: E501
    ]


class ISAKMP_payload_Nonce(ISAKMP_payload):
    name = "ISAKMP Nonce"


class ISAKMP_payload_KE(ISAKMP_payload):
    name = "ISAKMP Key Exchange"


class ISAKMP_payload_ID(ISAKMP_class):
    name = "ISAKMP Identification"
    fields_desc = [
        ByteEnumField("next_payload", None, ISAKMP_payload_type),
        ByteField("res", 0),
        FieldLenField("length", None, "load", "H", adjust=lambda pkt, x:x + 8),
        ByteEnumField("IDtype", 1, {1: "IPv4_addr", 11: "Key"}),
        ByteEnumField("ProtoID", 0, {0: "Unused"}),
        ShortEnumField("Port", 0, {0: "Unused"}),
        #        IPField("IdentData","127.0.0.1"),
        StrLenField("load", "", length_from=lambda x: x.length - 8),
    ]


class ISAKMP_payload_Hash(ISAKMP_payload):
    name = "ISAKMP Hash"


bind_bottom_up(UDP, ISAKMP, dport=500)
bind_bottom_up(UDP, ISAKMP, sport=500)
bind_top_down(UDP, ISAKMP, dport=500, sport=500)

# Add building bindings
# (Dissection bindings are located in ISAKMP_class.guess_payload_class)
bind_top_down(ISAKMP_class, ISAKMP_payload, next_payload=0)
bind_top_down(ISAKMP_class, ISAKMP_payload_SA, next_payload=1)
bind_top_down(ISAKMP_class, ISAKMP_payload_Proposal, next_payload=2)
bind_top_down(ISAKMP_class, ISAKMP_payload_Transform, next_payload=3)
bind_top_down(ISAKMP_class, ISAKMP_payload_KE, next_payload=4)
bind_top_down(ISAKMP_class, ISAKMP_payload_ID, next_payload=5)
# bind_top_down(ISAKMP_class, ISAKMP_payload_CERT, next_payload=6)
# bind_top_down(ISAKMP_class, ISAKMP_payload_CR, next_payload=7)
bind_top_down(ISAKMP_class, ISAKMP_payload_Hash, next_payload=8)
# bind_top_down(ISAKMP_class, ISAKMP_payload_SIG, next_payload=9)
bind_top_down(ISAKMP_class, ISAKMP_payload_Nonce, next_payload=10)
# bind_top_down(ISAKMP_class, ISAKMP_payload_Notification, next_payload=11)
# bind_top_down(ISAKMP_class, ISAKMP_payload_Delete, next_payload=12)
bind_top_down(ISAKMP_class, ISAKMP_payload_VendorID, next_payload=13)


def ikescan(ip):
    """Sends/receives a ISAMPK payload SA with payload proposal"""
    pkt = IP(dst=ip)
    pkt /= UDP()
    pkt /= ISAKMP(init_cookie=RandString(8), exch_type=2)
    pkt /= ISAKMP_payload_SA(prop=ISAKMP_payload_Proposal())
    return sr(pkt)
