Upload 16 files
Browse files- .dockerignore +13 -0
- .env +2 -0
- Decipher/__init__.py +1 -0
- Decipher/cisco_pwd.py +34 -0
- Decipher/cmac.py +58 -0
- Decipher/ctr.py +33 -0
- Decipher/eax.py +53 -0
- Decipher/extract.py +299 -0
- Decipher/generator.py +83 -0
- Decipher/models.py +85 -0
- Decipher/pt_crypto.py +52 -0
- Decipher/twofish.py +393 -0
- Decipher/validator.py +65 -0
- Dockerfile +25 -0
- main.py +80 -0
- requirements.txt +8 -0
.dockerignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
.env
|
| 5 |
+
.git/
|
| 6 |
+
.vscode/
|
| 7 |
+
.idea/
|
| 8 |
+
*.pkt
|
| 9 |
+
*.pka
|
| 10 |
+
test_*.py
|
| 11 |
+
Dockerfile
|
| 12 |
+
.dockerignore
|
| 13 |
+
requirements.txt
|
.env
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GHAYMAH_BASE_URL=https://genai.ghaymah.systems
|
| 2 |
+
GHAYMAH_GENERATE_AI_KEY=sk-iKzhUlwdPKlGDhKDAi53Wg
|
Decipher/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Decipher package
|
Decipher/cisco_pwd.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def decrypt_cisco_type7(encrypted_hex):
|
| 2 |
+
"""
|
| 3 |
+
Decrypts Cisco Type 7 passwords.
|
| 4 |
+
"""
|
| 5 |
+
if not encrypted_hex or len(encrypted_hex) < 4:
|
| 6 |
+
return None
|
| 7 |
+
|
| 8 |
+
# Cisco Type 7 key
|
| 9 |
+
key = [
|
| 10 |
+
0x64, 0x73, 0x66, 0x64, 0x3b, 0x6b, 0x66, 0x6f,
|
| 11 |
+
0x41, 0x2c, 0x2e, 0x69, 0x79, 0x65, 0x77, 0x72,
|
| 12 |
+
0x6b, 0x6c, 0x64, 0x4a, 0x4b, 0x44, 0x48, 0x53,
|
| 13 |
+
0x55, 0x42
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
# First two digits are the index into the key
|
| 18 |
+
index = int(encrypted_hex[:2])
|
| 19 |
+
encrypted_data = encrypted_hex[2:]
|
| 20 |
+
|
| 21 |
+
decrypted = ""
|
| 22 |
+
for i in range(0, len(encrypted_data), 2):
|
| 23 |
+
# Get the next byte from the hex string
|
| 24 |
+
char_hex = encrypted_data[i:i+2]
|
| 25 |
+
char_code = int(char_hex, 16)
|
| 26 |
+
|
| 27 |
+
# XOR with the key and advance index
|
| 28 |
+
decrypted_char = chr(char_code ^ key[index % len(key)])
|
| 29 |
+
decrypted += decrypted_char
|
| 30 |
+
index += 1
|
| 31 |
+
|
| 32 |
+
return decrypted
|
| 33 |
+
except:
|
| 34 |
+
return None
|
Decipher/cmac.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Callable
|
| 2 |
+
|
| 3 |
+
BLOCK_SIZE = 16 # 128 bit for Twofish
|
| 4 |
+
|
| 5 |
+
def xor_bytes(a: bytes, b: bytes) -> bytes:
|
| 6 |
+
return bytes(x ^ y for x, y in zip(a, b))
|
| 7 |
+
|
| 8 |
+
def left_shift_one(bitstring: bytes) -> bytes:
|
| 9 |
+
out = bytearray(len(bitstring))
|
| 10 |
+
carry = 0
|
| 11 |
+
for i in reversed(range(len(bitstring))):
|
| 12 |
+
new = (bitstring[i] << 1) & 0xFF
|
| 13 |
+
out[i] = new | carry
|
| 14 |
+
carry = (bitstring[i] & 0x80) >> 7
|
| 15 |
+
return bytes(out)
|
| 16 |
+
|
| 17 |
+
def generate_subkeys(encrypt_block: Callable[[bytes], bytes]):
|
| 18 |
+
const_rb = 0x87
|
| 19 |
+
zero = bytes(BLOCK_SIZE)
|
| 20 |
+
L = encrypt_block(zero)
|
| 21 |
+
|
| 22 |
+
K1 = left_shift_one(L)
|
| 23 |
+
if L[0] & 0x80:
|
| 24 |
+
K1 = xor_bytes(K1, b'\x00' * 15 + bytes([const_rb]))
|
| 25 |
+
|
| 26 |
+
K2 = left_shift_one(K1)
|
| 27 |
+
if K1[0] & 0x80:
|
| 28 |
+
K2 = xor_bytes(K2, b'\x00' * 15 + bytes([const_rb]))
|
| 29 |
+
|
| 30 |
+
return K1, K2
|
| 31 |
+
|
| 32 |
+
def pad(block: bytes) -> bytes:
|
| 33 |
+
padded = block + b'\x80'
|
| 34 |
+
return padded + b'\x00' * (BLOCK_SIZE - len(padded))
|
| 35 |
+
|
| 36 |
+
class CMAC:
|
| 37 |
+
def __init__(self, encrypt_block: Callable[[bytes], bytes]):
|
| 38 |
+
self.encrypt_block = encrypt_block
|
| 39 |
+
self.K1, self.K2 = generate_subkeys(encrypt_block)
|
| 40 |
+
|
| 41 |
+
def digest(self, data: bytes) -> bytes:
|
| 42 |
+
if len(data) == 0:
|
| 43 |
+
last = xor_bytes(pad(b''), self.K2)
|
| 44 |
+
blocks = []
|
| 45 |
+
else:
|
| 46 |
+
blocks = [data[i:i+BLOCK_SIZE] for i in range(0, len(data), BLOCK_SIZE)]
|
| 47 |
+
if len(blocks[-1]) == BLOCK_SIZE:
|
| 48 |
+
last = xor_bytes(blocks[-1], self.K1)
|
| 49 |
+
blocks = blocks[:-1]
|
| 50 |
+
else:
|
| 51 |
+
last = xor_bytes(pad(blocks[-1]), self.K2)
|
| 52 |
+
blocks = blocks[:-1]
|
| 53 |
+
|
| 54 |
+
X = bytes(BLOCK_SIZE)
|
| 55 |
+
for block in blocks:
|
| 56 |
+
X = self.encrypt_block(xor_bytes(X, block))
|
| 57 |
+
|
| 58 |
+
return self.encrypt_block(xor_bytes(X, last))
|
Decipher/ctr.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Callable
|
| 2 |
+
|
| 3 |
+
BLOCK_SIZE = 16 # 128 bit
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def inc_counter_be(counter: bytearray):
|
| 7 |
+
"""Incrementa un contatore big-endian a 128 bit (come Crypto++)."""
|
| 8 |
+
for i in range(BLOCK_SIZE - 1, -1, -1):
|
| 9 |
+
counter[i] = (counter[i] + 1) & 0xFF
|
| 10 |
+
if counter[i] != 0:
|
| 11 |
+
break
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class CTR:
|
| 15 |
+
def __init__(self, encrypt_block: Callable[[bytes], bytes], initial_counter: bytes):
|
| 16 |
+
assert len(initial_counter) == BLOCK_SIZE
|
| 17 |
+
self.encrypt_block = encrypt_block
|
| 18 |
+
self.counter = bytearray(initial_counter)
|
| 19 |
+
|
| 20 |
+
def process(self, data: bytes) -> bytes:
|
| 21 |
+
out = bytearray()
|
| 22 |
+
offset = 0
|
| 23 |
+
|
| 24 |
+
while offset < len(data):
|
| 25 |
+
keystream = self.encrypt_block(bytes(self.counter))
|
| 26 |
+
inc_counter_be(self.counter)
|
| 27 |
+
|
| 28 |
+
block = data[offset:offset + BLOCK_SIZE]
|
| 29 |
+
ks = keystream[:len(block)]
|
| 30 |
+
out.extend(b ^ k for b, k in zip(block, ks))
|
| 31 |
+
offset += BLOCK_SIZE
|
| 32 |
+
|
| 33 |
+
return bytes(out)
|
Decipher/eax.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Callable
|
| 2 |
+
from Decipher.cmac import CMAC, xor_bytes, BLOCK_SIZE
|
| 3 |
+
from Decipher.ctr import CTR
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def _omac_with_prefix(cmac: CMAC, prefix: int, data: bytes) -> bytes:
|
| 7 |
+
# Prefisso di 16 byte: [0, 0, ..., prefix]
|
| 8 |
+
P = b'\x00' * (BLOCK_SIZE - 1) + bytes([prefix])
|
| 9 |
+
return cmac.digest(P + data)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class EAX:
|
| 13 |
+
def __init__(self, encrypt_block: Callable[[bytes], bytes]):
|
| 14 |
+
self.encrypt_block = encrypt_block
|
| 15 |
+
self.cmac = CMAC(encrypt_block)
|
| 16 |
+
|
| 17 |
+
def encrypt(self, nonce: bytes, plaintext: bytes, aad: bytes = b''):
|
| 18 |
+
# OMAC_0 = CMAC(0x00 || nonce)
|
| 19 |
+
n_tag = _omac_with_prefix(self.cmac, 0x00, nonce)
|
| 20 |
+
|
| 21 |
+
# OMAC_1 = CMAC(0x01 || aad)
|
| 22 |
+
h_tag = _omac_with_prefix(self.cmac, 0x01, aad)
|
| 23 |
+
|
| 24 |
+
# CTR parte da n_tag
|
| 25 |
+
ctr = CTR(self.encrypt_block, n_tag)
|
| 26 |
+
ciphertext = ctr.process(plaintext)
|
| 27 |
+
|
| 28 |
+
# OMAC_2 = CMAC(0x02 || ciphertext)
|
| 29 |
+
c_tag = _omac_with_prefix(self.cmac, 0x02, ciphertext)
|
| 30 |
+
|
| 31 |
+
# TAG finale = n_tag ⊕ h_tag ⊕ c_tag
|
| 32 |
+
tag = xor_bytes(xor_bytes(n_tag, h_tag), c_tag)
|
| 33 |
+
|
| 34 |
+
return ciphertext, tag
|
| 35 |
+
|
| 36 |
+
def decrypt(self, nonce: bytes, ciphertext: bytes, tag: bytes, aad: bytes = b''):
|
| 37 |
+
# Ricalcolo OMAC_0
|
| 38 |
+
n_tag = _omac_with_prefix(self.cmac, 0x00, nonce)
|
| 39 |
+
|
| 40 |
+
# CTR
|
| 41 |
+
ctr = CTR(self.encrypt_block, n_tag)
|
| 42 |
+
plaintext = ctr.process(ciphertext)
|
| 43 |
+
|
| 44 |
+
# Ricalcolo OMAC_1 e OMAC_2
|
| 45 |
+
h_tag = _omac_with_prefix(self.cmac, 0x01, aad)
|
| 46 |
+
c_tag = _omac_with_prefix(self.cmac, 0x02, ciphertext)
|
| 47 |
+
|
| 48 |
+
# Verifica TAG
|
| 49 |
+
expected_tag = xor_bytes(xor_bytes(n_tag, h_tag), c_tag)
|
| 50 |
+
if expected_tag != tag:
|
| 51 |
+
raise ValueError("EAX authentication failed")
|
| 52 |
+
|
| 53 |
+
return plaintext
|
Decipher/extract.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import xml.etree.ElementTree as ET
|
| 2 |
+
import re
|
| 3 |
+
import base64
|
| 4 |
+
from ciscoconfparse import CiscoConfParse
|
| 5 |
+
from Decipher.cisco_pwd import decrypt_cisco_type7
|
| 6 |
+
|
| 7 |
+
def sanitize_xml_content(xml_bytes):
|
| 8 |
+
"""
|
| 9 |
+
Sanitizes XML content by properly handling problematic binary data in attributes
|
| 10 |
+
instead of just deleting them.
|
| 11 |
+
"""
|
| 12 |
+
# Robust decoding
|
| 13 |
+
xml_content = xml_bytes.decode('utf-8', errors='ignore')
|
| 14 |
+
|
| 15 |
+
def encode_binary_attr(match):
|
| 16 |
+
attr_name = match.group(1)
|
| 17 |
+
attr_value = match.group(2)
|
| 18 |
+
# Check if value contains non-XML friendly characters
|
| 19 |
+
if any(ord(c) < 32 and c not in '\n\r\t' for c in attr_value):
|
| 20 |
+
encoded = base64.b64encode(attr_value.encode('utf-8', errors='ignore')).decode('utf-8')
|
| 21 |
+
return f'{attr_name}="base64:{encoded}"'
|
| 22 |
+
return match.group(0)
|
| 23 |
+
|
| 24 |
+
# Find attributes that might contain binary data (like VALUE="...")
|
| 25 |
+
xml_content = re.sub(r'(\b\w+)="([^"]*)"', encode_binary_attr, xml_content)
|
| 26 |
+
|
| 27 |
+
# Ensure root tag is closed if it's a snippet
|
| 28 |
+
if "</PACKETTRACER5>" not in xml_content and "<PACKETTRACER5>" in xml_content:
|
| 29 |
+
xml_content += "</PACKETTRACER5>"
|
| 30 |
+
elif "</PACKETTRACER5_ACTIVITY>" not in xml_content and "<PACKETTRACER5_ACTIVITY>" in xml_content:
|
| 31 |
+
xml_content += "</PACKETTRACER5_ACTIVITY>"
|
| 32 |
+
|
| 33 |
+
return xml_content
|
| 34 |
+
|
| 35 |
+
def extract_structured_data(xml_path):
|
| 36 |
+
try:
|
| 37 |
+
with open(xml_path, 'rb') as f:
|
| 38 |
+
xml_bytes = f.read()
|
| 39 |
+
|
| 40 |
+
xml_content = sanitize_xml_content(xml_bytes)
|
| 41 |
+
root = ET.fromstring(xml_content)
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"[-] Error parsing {xml_path}: {e}")
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
data = {
|
| 47 |
+
"version": root.findtext(".//VERSION"),
|
| 48 |
+
"devices": [],
|
| 49 |
+
"physical_links": [],
|
| 50 |
+
"wireless_connections": [],
|
| 51 |
+
"vlans": {},
|
| 52 |
+
"instructions": ""
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# 1. Extract Devices and their properties
|
| 56 |
+
devices_by_id = {}
|
| 57 |
+
ssids_servers = {} # ssid -> device_name
|
| 58 |
+
|
| 59 |
+
for device in root.findall(".//DEVICE"):
|
| 60 |
+
engine = device.find("ENGINE")
|
| 61 |
+
if engine is None:
|
| 62 |
+
continue
|
| 63 |
+
|
| 64 |
+
name = engine.findtext("NAME")
|
| 65 |
+
dev_type = engine.findtext("TYPE")
|
| 66 |
+
model = engine.find("TYPE").get("model") if engine.find("TYPE") is not None else ""
|
| 67 |
+
ref_id = engine.findtext("SAVE_REF_ID")
|
| 68 |
+
|
| 69 |
+
dev_info = {
|
| 70 |
+
"name": name,
|
| 71 |
+
"type": dev_type,
|
| 72 |
+
"model": model,
|
| 73 |
+
"interfaces": [],
|
| 74 |
+
"wireless_info": None,
|
| 75 |
+
"vlans": []
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
if ref_id:
|
| 79 |
+
devices_by_id[ref_id] = name
|
| 80 |
+
|
| 81 |
+
# Extract Interfaces & IP info
|
| 82 |
+
for port in engine.findall(".//PORT"):
|
| 83 |
+
port_name = port.findtext("PORT_NAME") or port.findtext("NAME")
|
| 84 |
+
if not port_name:
|
| 85 |
+
# Try to find in parent SLOT/MODULE
|
| 86 |
+
parent_module = port.find("..")
|
| 87 |
+
if parent_module is not None:
|
| 88 |
+
port_type = port.findtext("TYPE")
|
| 89 |
+
if port_type:
|
| 90 |
+
port_name = port_type
|
| 91 |
+
|
| 92 |
+
if port_name:
|
| 93 |
+
ip = port.findtext("IP")
|
| 94 |
+
subnet = port.findtext("SUBNET")
|
| 95 |
+
ipv6 = port.findtext("IPV6_LINK_LOCAL")
|
| 96 |
+
|
| 97 |
+
if ip or ipv6 or port.findtext("PORT_DHCP_ENABLE") == "true":
|
| 98 |
+
dev_info["interfaces"].append({
|
| 99 |
+
"name": port_name,
|
| 100 |
+
"ip": ip if ip else "DHCP" if port.findtext("PORT_DHCP_ENABLE") == "true" else None,
|
| 101 |
+
"subnet": subnet,
|
| 102 |
+
"ipv6": ipv6
|
| 103 |
+
})
|
| 104 |
+
|
| 105 |
+
# Extract Wireless Server info (Access Points)
|
| 106 |
+
wireless_server = engine.find("WIRELESS_SERVER")
|
| 107 |
+
if wireless_server is not None:
|
| 108 |
+
common = wireless_server.find("WIRELESS_COMMON")
|
| 109 |
+
if common is not None:
|
| 110 |
+
ssid = common.findtext("SSID")
|
| 111 |
+
dev_info["wireless_info"] = {
|
| 112 |
+
"role": "server",
|
| 113 |
+
"ssid": ssid,
|
| 114 |
+
"encryption": common.findtext("ENCRYPT_TYPE"),
|
| 115 |
+
"key": common.find(".//KEY").text if common.find(".//KEY") is not None else None
|
| 116 |
+
}
|
| 117 |
+
if ssid:
|
| 118 |
+
ssids_servers[ssid] = name
|
| 119 |
+
|
| 120 |
+
# Extract Wireless Client info (PC/Laptop/Phone)
|
| 121 |
+
wireless_client = engine.find("WIRELESS_CLIENT")
|
| 122 |
+
if wireless_client is not None:
|
| 123 |
+
common = wireless_client.find("WIRELESS_COMMON")
|
| 124 |
+
if common is not None:
|
| 125 |
+
ssid = common.findtext("SSID")
|
| 126 |
+
dev_info["wireless_info"] = {
|
| 127 |
+
"role": "client",
|
| 128 |
+
"ssid": ssid,
|
| 129 |
+
"encryption": common.findtext("ENCRYPT_TYPE")
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
# Extract VLANs from device-specific storage (Switches)
|
| 133 |
+
vlan_storage = engine.findall(".//VLANS/VLAN")
|
| 134 |
+
for v in vlan_storage:
|
| 135 |
+
v_num = v.get("number")
|
| 136 |
+
v_name = v.get("name")
|
| 137 |
+
if v_num and v_num not in data["vlans"]:
|
| 138 |
+
data["vlans"][v_num] = v_name
|
| 139 |
+
dev_info["vlans"].append({"id": v_num, "name": v_name})
|
| 140 |
+
|
| 141 |
+
# Parse RUNNINGCONFIG using CiscoConfParse for deeper analysis
|
| 142 |
+
config = engine.findtext("RUNNINGCONFIG")
|
| 143 |
+
if config:
|
| 144 |
+
parse = CiscoConfParse(config.splitlines(), factory=True)
|
| 145 |
+
|
| 146 |
+
# 1. Improved VLAN assignments
|
| 147 |
+
for intf_obj in parse.find_objects(r'^interface'):
|
| 148 |
+
intf_name = intf_obj.text.replace('interface ', '').strip()
|
| 149 |
+
vlan_id = None
|
| 150 |
+
|
| 151 |
+
# Check for access vlan
|
| 152 |
+
access_vlan = intf_obj.re_search_children(r'switchport access vlan (\d+)')
|
| 153 |
+
if access_vlan:
|
| 154 |
+
vlan_id = access_vlan[0].re_match_typed(r'switchport access vlan (\d+)')
|
| 155 |
+
|
| 156 |
+
if vlan_id:
|
| 157 |
+
# Update or add interface info
|
| 158 |
+
found = False
|
| 159 |
+
for i in dev_info["interfaces"]:
|
| 160 |
+
if i["name"] == intf_name:
|
| 161 |
+
i["vlan"] = vlan_id
|
| 162 |
+
found = True
|
| 163 |
+
break
|
| 164 |
+
if not found:
|
| 165 |
+
dev_info["interfaces"].append({"name": intf_name, "vlan": vlan_id})
|
| 166 |
+
|
| 167 |
+
# 2. L3/L4 Support: Routing Protocols (Static, OSPF)
|
| 168 |
+
dev_info["routing"] = {
|
| 169 |
+
"static_routes": [],
|
| 170 |
+
"ospf": [],
|
| 171 |
+
"nat": [],
|
| 172 |
+
"dhcp_pools": []
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
# Static Routes
|
| 176 |
+
static_routes = parse.find_objects(r'^ip route')
|
| 177 |
+
for route in static_routes:
|
| 178 |
+
dev_info["routing"]["static_routes"].append(route.text)
|
| 179 |
+
|
| 180 |
+
# OSPF
|
| 181 |
+
ospf_processes = parse.find_objects(r'^router ospf')
|
| 182 |
+
for ospf in ospf_processes:
|
| 183 |
+
ospf_info = {"process_id": ospf.text.split()[-1], "networks": []}
|
| 184 |
+
for network in ospf.re_search_children(r'network'):
|
| 185 |
+
ospf_info["networks"].append(network.text.strip())
|
| 186 |
+
dev_info["routing"]["ospf"].append(ospf_info)
|
| 187 |
+
|
| 188 |
+
# NAT
|
| 189 |
+
nat_rules = parse.find_objects(r'^ip nat')
|
| 190 |
+
for rule in nat_rules:
|
| 191 |
+
dev_info["routing"]["nat"].append(rule.text)
|
| 192 |
+
|
| 193 |
+
# DHCP Pools
|
| 194 |
+
dhcp_pools = parse.find_objects(r'^ip dhcp pool')
|
| 195 |
+
for pool in dhcp_pools:
|
| 196 |
+
pool_info = {
|
| 197 |
+
"name": pool.text.replace('ip dhcp pool ', '').strip(),
|
| 198 |
+
"details": [child.text.strip() for child in pool.children]
|
| 199 |
+
}
|
| 200 |
+
dev_info["routing"]["dhcp_pools"].append(pool_info)
|
| 201 |
+
|
| 202 |
+
# 3. Security Audit & ACLs
|
| 203 |
+
dev_info["security"] = {
|
| 204 |
+
"acls": [],
|
| 205 |
+
"vulnerabilities": [],
|
| 206 |
+
"decrypted_passwords": []
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
# Decrypt Type 7 passwords
|
| 210 |
+
type7_pwds = re.findall(r'password 7 ([0-9A-Fa-f]+)|enable password 7 ([0-9A-Fa-f]+)', config)
|
| 211 |
+
for pwd_match in type7_pwds:
|
| 212 |
+
encrypted = pwd_match[0] or pwd_match[1]
|
| 213 |
+
decrypted = decrypt_cisco_type7(encrypted)
|
| 214 |
+
if decrypted:
|
| 215 |
+
dev_info["security"]["decrypted_passwords"].append({
|
| 216 |
+
"encrypted": encrypted,
|
| 217 |
+
"decrypted": decrypted,
|
| 218 |
+
"type": "Cisco Type 7"
|
| 219 |
+
})
|
| 220 |
+
dev_info["security"]["vulnerabilities"].append(f"Weak encryption found: Decrypted password '{decrypted}'")
|
| 221 |
+
|
| 222 |
+
# Standard and Extended ACLs
|
| 223 |
+
acls = parse.find_objects(r'^access-list|ip access-list')
|
| 224 |
+
for acl in acls:
|
| 225 |
+
acl_info = {"name": acl.text.strip(), "rules": []}
|
| 226 |
+
if acl.children:
|
| 227 |
+
acl_info["rules"] = [child.text.strip() for child in acl.children]
|
| 228 |
+
dev_info["security"]["acls"].append(acl_info)
|
| 229 |
+
|
| 230 |
+
# Security Audit: Check for plaintext passwords or weak services
|
| 231 |
+
if parse.find_objects(r'^no service password-encryption'):
|
| 232 |
+
dev_info["security"]["vulnerabilities"].append("Plaintext password storage enabled (no service password-encryption)")
|
| 233 |
+
|
| 234 |
+
if parse.find_objects(r'^line vty'):
|
| 235 |
+
vty_lines = parse.find_objects(r'^line vty')
|
| 236 |
+
for vty in vty_lines:
|
| 237 |
+
if not vty.re_search_children(r'transport input ssh'):
|
| 238 |
+
dev_info["security"]["vulnerabilities"].append(f"Insecure remote access (Telnet allowed on {vty.text.strip()})")
|
| 239 |
+
|
| 240 |
+
# 4. Server Services (if applicable)
|
| 241 |
+
dev_info["services"] = []
|
| 242 |
+
services_node = engine.find("SERVICES")
|
| 243 |
+
if services_node is not None:
|
| 244 |
+
# HTTP Service
|
| 245 |
+
http = services_node.find("HTTP")
|
| 246 |
+
if http is not None and http.get("enabled") == "true":
|
| 247 |
+
dev_info["services"].append({"type": "HTTP", "port": 80})
|
| 248 |
+
|
| 249 |
+
# DNS Service
|
| 250 |
+
dns = services_node.find("DNS")
|
| 251 |
+
if dns is not None and dns.get("enabled") == "true":
|
| 252 |
+
dns_info = {"type": "DNS", "records": []}
|
| 253 |
+
for record in dns.findall("RECORD"):
|
| 254 |
+
dns_info["records"].append({
|
| 255 |
+
"name": record.get("name"),
|
| 256 |
+
"type": record.get("type"),
|
| 257 |
+
"value": record.get("value")
|
| 258 |
+
})
|
| 259 |
+
dev_info["services"].append(dns_info)
|
| 260 |
+
|
| 261 |
+
data["devices"].append(dev_info)
|
| 262 |
+
|
| 263 |
+
# 2. Extract Physical Links
|
| 264 |
+
for link in root.findall(".//LINK"):
|
| 265 |
+
cable = link.find("CABLE")
|
| 266 |
+
if cable is not None:
|
| 267 |
+
ports = cable.findall("PORT")
|
| 268 |
+
from_id = cable.findtext("FROM")
|
| 269 |
+
to_id = cable.findtext("TO")
|
| 270 |
+
|
| 271 |
+
link_info = {
|
| 272 |
+
"type": link.findtext("TYPE"),
|
| 273 |
+
"from_device": devices_by_id.get(from_id, from_id),
|
| 274 |
+
"to_device": devices_by_id.get(to_id, to_id),
|
| 275 |
+
}
|
| 276 |
+
if len(ports) >= 2:
|
| 277 |
+
link_info["from_port"] = ports[0].text
|
| 278 |
+
link_info["to_port"] = ports[1].text
|
| 279 |
+
|
| 280 |
+
data["physical_links"].append(link_info)
|
| 281 |
+
|
| 282 |
+
# 3. Establish Wireless Connections
|
| 283 |
+
for dev in data["devices"]:
|
| 284 |
+
if dev["wireless_info"] and dev["wireless_info"]["role"] == "client":
|
| 285 |
+
ssid = dev["wireless_info"]["ssid"]
|
| 286 |
+
if ssid in ssids_servers:
|
| 287 |
+
data["wireless_connections"].append({
|
| 288 |
+
"from_device": dev["name"],
|
| 289 |
+
"to_device": ssids_servers[ssid],
|
| 290 |
+
"ssid": ssid,
|
| 291 |
+
"type": "wireless"
|
| 292 |
+
})
|
| 293 |
+
|
| 294 |
+
# 4. Extract Instructions
|
| 295 |
+
instr_page = root.find(".//INSTRUCTIONS/PAGE")
|
| 296 |
+
if instr_page is not None:
|
| 297 |
+
data["instructions"] = instr_page.text
|
| 298 |
+
|
| 299 |
+
return data
|
Decipher/generator.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from Decipher.models import NetworkTopology
|
| 6 |
+
from Decipher.validator import validate_topology
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
client = OpenAI(
|
| 11 |
+
api_key=os.getenv("GHAYMAH_GENERATE_AI_KEY"),
|
| 12 |
+
base_url=os.getenv("GHAYMAH_BASE_URL"),
|
| 13 |
+
timeout=60.0 # Increase timeout to 60 seconds
|
| 14 |
+
)
|
| 15 |
+
SYSTEM_PROMPT = """
|
| 16 |
+
You are a Senior Network Architect. Your task is to generate a highly accurate and valid network topology JSON based on a user's description.
|
| 17 |
+
The output MUST strictly follow the provided JSON schema.
|
| 18 |
+
|
| 19 |
+
Principles for accuracy:
|
| 20 |
+
1. IP Addressing: Ensure devices in the same segment have unique IPs in the same subnet.
|
| 21 |
+
2. Connectivity: Define physical links correctly between existing devices and ports.
|
| 22 |
+
3. Routing: If OSPF or static routes are requested, ensure the configuration is logically sound.
|
| 23 |
+
4. VLANs: Ensure ports are assigned to correct VLANs if specified.
|
| 24 |
+
5. Device Types: Use realistic device types (Router, Switch, PC, Server, etc.) and models (e.g., 2911, 2960).
|
| 25 |
+
|
| 26 |
+
You must respond ONLY with the valid JSON object. No markdown, no conversational text.
|
| 27 |
+
Ensure all relevant arrays (devices, physical_links) are populated based on the user's request. Do not return empty arrays if the user asked for specific components.
|
| 28 |
+
|
| 29 |
+
Example Output Structure:
|
| 30 |
+
{
|
| 31 |
+
"version": "1.0",
|
| 32 |
+
"devices": [
|
| 33 |
+
{
|
| 34 |
+
"name": "Router1",
|
| 35 |
+
"type": "Router",
|
| 36 |
+
"model": "2911",
|
| 37 |
+
"interfaces": [{"name": "GigabitEthernet0/0", "ip": "192.168.1.1", "subnet": "255.255.255.0"}],
|
| 38 |
+
"routing": {"static_routes": [], "ospf": [{"process_id": "1", "networks": ["192.168.1.0 0.0.0.255 area 0"]}]}
|
| 39 |
+
}
|
| 40 |
+
],
|
| 41 |
+
"physical_links": [
|
| 42 |
+
{"type": "Copper", "from_device": "Router1", "to_device": "Switch1", "from_port": "GigabitEthernet0/0", "to_port": "FastEthernet0/1"}
|
| 43 |
+
],
|
| 44 |
+
"vlans": {"10": "Sales", "20": "HR"},
|
| 45 |
+
"instructions": "Brief summary of the network..."
|
| 46 |
+
}
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def generate_network(prompt: str) -> NetworkTopology:
|
| 51 |
+
try:
|
| 52 |
+
response = client.chat.completions.create(
|
| 53 |
+
model="DeepSeek-V3-0324", # Using available model
|
| 54 |
+
messages=[
|
| 55 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 56 |
+
{"role": "user", "content": f"Generate a network topology for: {prompt}"}
|
| 57 |
+
],
|
| 58 |
+
response_format={"type": "json_object"}
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
content = response.choices[0].message.content
|
| 62 |
+
print(f"--- RAW LLM RESPONSE ---\n{content}\n-----------------------")
|
| 63 |
+
data = json.loads(content)
|
| 64 |
+
|
| 65 |
+
if not data.get("devices") and not data.get("physical_links"):
|
| 66 |
+
# If AI returned an empty-ish object, it might have failed to understand.
|
| 67 |
+
# We can try to raise an error so the API doesn't return an empty success.
|
| 68 |
+
raise ValueError("AI generated an empty network topology. Please try a more descriptive prompt.")
|
| 69 |
+
|
| 70 |
+
# Validate and parse into Pydantic model
|
| 71 |
+
topology = NetworkTopology(**data)
|
| 72 |
+
|
| 73 |
+
# Intelligent Validation Layer
|
| 74 |
+
validation_issues = validate_topology(topology)
|
| 75 |
+
if validation_issues:
|
| 76 |
+
# Append issues to instructions so user is aware of logical warnings
|
| 77 |
+
issue_text = "\n\n[Accuracy Layer Warnings]:\n- " + "\n- ".join(validation_issues)
|
| 78 |
+
topology.instructions += issue_text
|
| 79 |
+
|
| 80 |
+
return topology
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"Error generating network: {e}")
|
| 83 |
+
raise e
|
Decipher/models.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List, Optional, Dict, Any
|
| 3 |
+
|
| 4 |
+
class Interface(BaseModel):
|
| 5 |
+
name: str
|
| 6 |
+
ip: Optional[str] = None
|
| 7 |
+
subnet: Optional[str] = None
|
| 8 |
+
ipv6: Optional[str] = None
|
| 9 |
+
vlan: Optional[str] = None
|
| 10 |
+
|
| 11 |
+
class WirelessInfo(BaseModel):
|
| 12 |
+
role: str # "server" or "client"
|
| 13 |
+
ssid: Optional[str] = None
|
| 14 |
+
encryption: Optional[str] = None
|
| 15 |
+
key: Optional[str] = None
|
| 16 |
+
|
| 17 |
+
class OSPFNetwork(BaseModel):
|
| 18 |
+
network: str
|
| 19 |
+
|
| 20 |
+
class OSPFProcess(BaseModel):
|
| 21 |
+
process_id: str
|
| 22 |
+
networks: List[str]
|
| 23 |
+
|
| 24 |
+
class Routing(BaseModel):
|
| 25 |
+
static_routes: List[str] = []
|
| 26 |
+
ospf: List[OSPFProcess] = []
|
| 27 |
+
nat: List[str] = []
|
| 28 |
+
dhcp_pools: List[Dict[str, Any]] = []
|
| 29 |
+
|
| 30 |
+
class Security(BaseModel):
|
| 31 |
+
acls: List[Dict[str, Any]] = []
|
| 32 |
+
vulnerabilities: List[str] = []
|
| 33 |
+
decrypted_passwords: List[Dict[str, Any]] = []
|
| 34 |
+
|
| 35 |
+
class Device(BaseModel):
|
| 36 |
+
name: str
|
| 37 |
+
type: str
|
| 38 |
+
model: Optional[str] = ""
|
| 39 |
+
interfaces: List[Interface] = []
|
| 40 |
+
wireless_info: Optional[WirelessInfo] = None
|
| 41 |
+
vlans: List[Dict[str, str]] = []
|
| 42 |
+
routing: Optional[Routing] = None
|
| 43 |
+
security: Optional[Security] = None
|
| 44 |
+
services: List[Dict[str, Any]] = []
|
| 45 |
+
|
| 46 |
+
class PhysicalLink(BaseModel):
|
| 47 |
+
type: str
|
| 48 |
+
from_device: str
|
| 49 |
+
to_device: str
|
| 50 |
+
from_port: Optional[str] = None
|
| 51 |
+
to_port: Optional[str] = None
|
| 52 |
+
|
| 53 |
+
class WirelessConnection(BaseModel):
|
| 54 |
+
from_device: str
|
| 55 |
+
to_device: str
|
| 56 |
+
ssid: str
|
| 57 |
+
type: str = "wireless"
|
| 58 |
+
|
| 59 |
+
class NetworkTopology(BaseModel):
|
| 60 |
+
version: Optional[str] = "1.0"
|
| 61 |
+
devices: List[Device] = []
|
| 62 |
+
physical_links: List[PhysicalLink] = []
|
| 63 |
+
wireless_connections: List[WirelessConnection] = []
|
| 64 |
+
vlans: Dict[str, str] = {}
|
| 65 |
+
instructions: str = ""
|
| 66 |
+
|
| 67 |
+
model_config = {
|
| 68 |
+
"json_schema_extra": {
|
| 69 |
+
"example": {
|
| 70 |
+
"version": "1.0",
|
| 71 |
+
"devices": [
|
| 72 |
+
{
|
| 73 |
+
"name": "Router-ISP",
|
| 74 |
+
"type": "Router",
|
| 75 |
+
"model": "2911",
|
| 76 |
+
"interfaces": [
|
| 77 |
+
{"name": "GigabitEthernet0/0", "ip": "10.0.0.1", "subnet": "255.255.255.0"}
|
| 78 |
+
]
|
| 79 |
+
}
|
| 80 |
+
],
|
| 81 |
+
"physical_links": [],
|
| 82 |
+
"instructions": "This is an example topology."
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
Decipher/pt_crypto.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from Decipher.eax import EAX
|
| 2 |
+
from Decipher.twofish import Twofish
|
| 3 |
+
import zlib
|
| 4 |
+
import struct
|
| 5 |
+
|
| 6 |
+
def deobf_stage1(data: bytes) -> bytes:
|
| 7 |
+
L = len(data)
|
| 8 |
+
res = bytearray(L)
|
| 9 |
+
for i in range(L):
|
| 10 |
+
res[i] = data[L-1-i] ^ (L - i*L & 0xFF)
|
| 11 |
+
return bytes(res)
|
| 12 |
+
|
| 13 |
+
def deobf_stage2(data: bytes) -> bytes:
|
| 14 |
+
L = len(data)
|
| 15 |
+
res = bytearray(L)
|
| 16 |
+
for i, b in enumerate(data):
|
| 17 |
+
res[i] = b ^ (L - i & 0xFF)
|
| 18 |
+
return bytes(res)
|
| 19 |
+
|
| 20 |
+
def uncompress_qt(blob: bytes) -> bytes:
|
| 21 |
+
size = struct.unpack(">I", blob[:4])[0]
|
| 22 |
+
return zlib.decompress(blob[4:])[:size]
|
| 23 |
+
|
| 24 |
+
def decrypt_pkt(pkt: bytes) -> bytes:
|
| 25 |
+
# Stage 1 deobfuscation
|
| 26 |
+
stage1 = deobf_stage1(pkt)
|
| 27 |
+
|
| 28 |
+
# Chiave e IV per i file .pkt
|
| 29 |
+
key = bytes([137])*16
|
| 30 |
+
iv = bytes([16])*16
|
| 31 |
+
|
| 32 |
+
# Twofish block cipher
|
| 33 |
+
tf = Twofish(key)
|
| 34 |
+
encrypt_block = tf.encrypt
|
| 35 |
+
|
| 36 |
+
# EAX con nonce = iv
|
| 37 |
+
eax = EAX(encrypt_block)
|
| 38 |
+
|
| 39 |
+
# Supponiamo che negli .pkt il tag sia alla fine
|
| 40 |
+
ciphertext = stage1[:-16]
|
| 41 |
+
tag = stage1[-16:]
|
| 42 |
+
|
| 43 |
+
# Decrypt usando nonce fisso
|
| 44 |
+
decrypted = eax.decrypt(nonce=iv, ciphertext=ciphertext, tag=tag)
|
| 45 |
+
|
| 46 |
+
# Stage 2 deobfuscation
|
| 47 |
+
stage2 = deobf_stage2(decrypted)
|
| 48 |
+
|
| 49 |
+
# Decompressione
|
| 50 |
+
xml = uncompress_qt(stage2)
|
| 51 |
+
|
| 52 |
+
return xml
|
Decipher/twofish.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## twofish.py - pure Python implementation of the Twofish algorithm.
|
| 2 |
+
## Bjorn Edstrom <be@bjrn.se> 13 december 2007.
|
| 3 |
+
##
|
| 4 |
+
## Copyrights
|
| 5 |
+
## ==========
|
| 6 |
+
##
|
| 7 |
+
## This code is a derived from an implementation by Dr Brian Gladman
|
| 8 |
+
## (gladman@seven77.demon.co.uk) which is subject to the following license.
|
| 9 |
+
## This Python implementation is not subject to any other license.
|
| 10 |
+
##
|
| 11 |
+
##/* This is an independent implementation of the encryption algorithm: */
|
| 12 |
+
##/* */
|
| 13 |
+
##/* Twofish by Bruce Schneier and colleagues */
|
| 14 |
+
##/* */
|
| 15 |
+
##/* which is a candidate algorithm in the Advanced Encryption Standard */
|
| 16 |
+
##/* programme of the US National Institute of Standards and Technology. */
|
| 17 |
+
##/* */
|
| 18 |
+
##/* Copyright in this implementation is held by Dr B R Gladman but I */
|
| 19 |
+
##/* hereby give permission for its free direct or derivative use subject */
|
| 20 |
+
##/* to acknowledgment of its origin and compliance with any conditions */
|
| 21 |
+
##/* that the originators of t he algorithm place on its exploitation. */
|
| 22 |
+
##/* */
|
| 23 |
+
##/* My thanks to Doug Whiting and Niels Ferguson for comments that led */
|
| 24 |
+
##/* to improvements in this implementation. */
|
| 25 |
+
##/* */
|
| 26 |
+
##/* Dr Brian Gladman (gladman@seven77.demon.co.uk) 14th January 1999 */
|
| 27 |
+
##
|
| 28 |
+
## The above copyright notice must not be removed.
|
| 29 |
+
##
|
| 30 |
+
## Information
|
| 31 |
+
## ===========
|
| 32 |
+
##
|
| 33 |
+
## Anyone thinking of using this code should reconsider. It's slow.
|
| 34 |
+
## Try python-mcrypt instead. In case a faster library is not installed
|
| 35 |
+
## on the target system, this code can be used as a portable fallback.
|
| 36 |
+
|
| 37 |
+
# pylint: disable-all
|
| 38 |
+
|
| 39 |
+
block_size = 16
|
| 40 |
+
key_size = 32
|
| 41 |
+
|
| 42 |
+
class Twofish:
|
| 43 |
+
|
| 44 |
+
def __init__(self, key=None):
|
| 45 |
+
"""Twofish."""
|
| 46 |
+
|
| 47 |
+
if key:
|
| 48 |
+
self.set_key(key)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def set_key(self, key):
|
| 52 |
+
"""Init."""
|
| 53 |
+
|
| 54 |
+
key_len = len(key)
|
| 55 |
+
if key_len not in [16, 24, 32]:
|
| 56 |
+
# XXX: add padding?
|
| 57 |
+
raise KeyError("key must be 16, 24 or 32 bytes")
|
| 58 |
+
if key_len % 4:
|
| 59 |
+
# XXX: add padding?
|
| 60 |
+
raise KeyError("key not a multiple of 4")
|
| 61 |
+
if key_len > 32:
|
| 62 |
+
# XXX: prune?
|
| 63 |
+
raise KeyError("key_len > 32")
|
| 64 |
+
|
| 65 |
+
self.context = TWI()
|
| 66 |
+
|
| 67 |
+
key_word32 = [0] * 32
|
| 68 |
+
i = 0
|
| 69 |
+
while key:
|
| 70 |
+
key_word32[i] = struct.unpack("<L", key[0:4])[0]
|
| 71 |
+
key = key[4:]
|
| 72 |
+
i += 1
|
| 73 |
+
|
| 74 |
+
set_key(self.context, key_word32, key_len)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def decrypt(self, block):
|
| 78 |
+
"""Decrypt blocks."""
|
| 79 |
+
|
| 80 |
+
if len(block) % 16:
|
| 81 |
+
raise ValueError("block size must be a multiple of 16")
|
| 82 |
+
|
| 83 |
+
plaintext = b''
|
| 84 |
+
|
| 85 |
+
while block:
|
| 86 |
+
a, b, c, d = struct.unpack("<4L", block[:16])
|
| 87 |
+
temp = [a, b, c, d]
|
| 88 |
+
decrypt(self.context, temp)
|
| 89 |
+
plaintext += struct.pack("<4L", *temp)
|
| 90 |
+
block = block[16:]
|
| 91 |
+
|
| 92 |
+
return plaintext
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def encrypt(self, block):
|
| 96 |
+
"""Encrypt blocks."""
|
| 97 |
+
|
| 98 |
+
if len(block) % 16:
|
| 99 |
+
raise ValueError("block size must be a multiple of 16")
|
| 100 |
+
|
| 101 |
+
ciphertext = b''
|
| 102 |
+
|
| 103 |
+
while block:
|
| 104 |
+
a, b, c, d = struct.unpack("<4L", block[0:16])
|
| 105 |
+
temp = [a, b, c, d]
|
| 106 |
+
encrypt(self.context, temp)
|
| 107 |
+
ciphertext += struct.pack("<4L", *temp)
|
| 108 |
+
block = block[16:]
|
| 109 |
+
|
| 110 |
+
return ciphertext
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def get_name(self):
|
| 114 |
+
"""Return the name of the cipher."""
|
| 115 |
+
|
| 116 |
+
return "Twofish"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def get_block_size(self):
|
| 120 |
+
"""Get cipher block size in bytes."""
|
| 121 |
+
|
| 122 |
+
return 16
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def get_key_size(self):
|
| 126 |
+
"""Get cipher key size in bytes."""
|
| 127 |
+
|
| 128 |
+
return 32
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
#
|
| 132 |
+
# Private.
|
| 133 |
+
#
|
| 134 |
+
|
| 135 |
+
import struct
|
| 136 |
+
import sys
|
| 137 |
+
|
| 138 |
+
WORD_BIGENDIAN = 0
|
| 139 |
+
if sys.byteorder == 'big':
|
| 140 |
+
WORD_BIGENDIAN = 1
|
| 141 |
+
|
| 142 |
+
def rotr32(x, n):
|
| 143 |
+
return (x >> n) | ((x << (32 - n)) & 0xFFFFFFFF)
|
| 144 |
+
|
| 145 |
+
def rotl32(x, n):
|
| 146 |
+
return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))
|
| 147 |
+
|
| 148 |
+
def byteswap32(x):
|
| 149 |
+
return ((x & 0xff) << 24) | (((x >> 8) & 0xff) << 16) | \
|
| 150 |
+
(((x >> 16) & 0xff) << 8) | ((x >> 24) & 0xff)
|
| 151 |
+
|
| 152 |
+
class TWI:
|
| 153 |
+
def __init__(self):
|
| 154 |
+
self.k_len = 0 # word32
|
| 155 |
+
self.l_key = [0]*40 # word32
|
| 156 |
+
self.s_key = [0]*4 # word32
|
| 157 |
+
self.qt_gen = 0 # word32
|
| 158 |
+
self.q_tab = [[0]*256, [0]*256] # byte
|
| 159 |
+
self.mt_gen = 0 # word32
|
| 160 |
+
self.m_tab = [[0]*256, [0]*256, [0]*256, [0]*256] # word32
|
| 161 |
+
self.mk_tab = [[0]*256, [0]*256, [0]*256, [0]*256] # word32
|
| 162 |
+
|
| 163 |
+
def byte(x, n):
|
| 164 |
+
return (x >> (8 * n)) & 0xff
|
| 165 |
+
|
| 166 |
+
tab_5b = [0, 90, 180, 238]
|
| 167 |
+
tab_ef = [0, 238, 180, 90]
|
| 168 |
+
ror4 = [0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15]
|
| 169 |
+
ashx = [0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 5, 14, 7]
|
| 170 |
+
qt0 = [[8, 1, 7, 13, 6, 15, 3, 2, 0, 11, 5, 9, 14, 12, 10, 4],
|
| 171 |
+
[2, 8, 11, 13, 15, 7, 6, 14, 3, 1, 9, 4, 0, 10, 12, 5]]
|
| 172 |
+
qt1 = [[14, 12, 11, 8, 1, 2, 3, 5, 15, 4, 10, 6, 7, 0, 9, 13],
|
| 173 |
+
[1, 14, 2, 11, 4, 12, 3, 7, 6, 13, 10, 5, 15, 9, 0, 8]]
|
| 174 |
+
qt2 = [[11, 10, 5, 14, 6, 13, 9, 0, 12, 8, 15, 3, 2, 4, 7, 1],
|
| 175 |
+
[4, 12, 7, 5, 1, 6, 9, 10, 0, 14, 13, 8, 2, 11, 3, 15]]
|
| 176 |
+
qt3 = [[13, 7, 15, 4, 1, 2, 6, 14, 9, 11, 3, 0, 8, 5, 12, 10],
|
| 177 |
+
[11, 9, 5, 1, 12, 3, 13, 14, 6, 4, 7, 15, 2, 0, 8, 10]]
|
| 178 |
+
|
| 179 |
+
def qp(n, x): # word32, byte
|
| 180 |
+
n %= 0x100000000
|
| 181 |
+
x %= 0x100
|
| 182 |
+
a0 = x >> 4;
|
| 183 |
+
b0 = x & 15;
|
| 184 |
+
a1 = a0 ^ b0;
|
| 185 |
+
b1 = ror4[b0] ^ ashx[a0];
|
| 186 |
+
a2 = qt0[n][a1];
|
| 187 |
+
b2 = qt1[n][b1];
|
| 188 |
+
a3 = a2 ^ b2;
|
| 189 |
+
b3 = ror4[b2] ^ ashx[a2];
|
| 190 |
+
a4 = qt2[n][a3];
|
| 191 |
+
b4 = qt3[n][b3];
|
| 192 |
+
return (b4 << 4) | a4;
|
| 193 |
+
|
| 194 |
+
def gen_qtab(pkey):
|
| 195 |
+
for i in range(256):
|
| 196 |
+
pkey.q_tab[0][i] = qp(0, i)
|
| 197 |
+
pkey.q_tab[1][i] = qp(1, i)
|
| 198 |
+
|
| 199 |
+
def gen_mtab(pkey):
|
| 200 |
+
for i in range(256):
|
| 201 |
+
f01 = pkey.q_tab[1][i]
|
| 202 |
+
f01 = pkey.q_tab[1][i];
|
| 203 |
+
f5b = ((f01) ^ ((f01) >> 2) ^ tab_5b[(f01) & 3]);
|
| 204 |
+
fef = ((f01) ^ ((f01) >> 1) ^ ((f01) >> 2) ^ tab_ef[(f01) & 3]);
|
| 205 |
+
pkey.m_tab[0][i] = f01 + (f5b << 8) + (fef << 16) + (fef << 24);
|
| 206 |
+
pkey.m_tab[2][i] = f5b + (fef << 8) + (f01 << 16) + (fef << 24);
|
| 207 |
+
|
| 208 |
+
f01 = pkey.q_tab[0][i];
|
| 209 |
+
f5b = ((f01) ^ ((f01) >> 2) ^ tab_5b[(f01) & 3]);
|
| 210 |
+
fef = ((f01) ^ ((f01) >> 1) ^ ((f01) >> 2) ^ tab_ef[(f01) & 3]);
|
| 211 |
+
pkey.m_tab[1][i] = fef + (fef << 8) + (f5b << 16) + (f01 << 24);
|
| 212 |
+
pkey.m_tab[3][i] = f5b + (f01 << 8) + (fef << 16) + (f5b << 24);
|
| 213 |
+
|
| 214 |
+
def gen_mk_tab(pkey, key):
|
| 215 |
+
if pkey.k_len == 2:
|
| 216 |
+
for i in range(256):
|
| 217 |
+
by = i % 0x100
|
| 218 |
+
pkey.mk_tab[0][i] = pkey.m_tab[0][pkey.q_tab[0][pkey.q_tab[0][by] ^ byte(key[1],0)] ^ byte(key[0],0)];
|
| 219 |
+
pkey.mk_tab[1][i] = pkey.m_tab[1][pkey.q_tab[0][pkey.q_tab[1][by] ^ byte(key[1],1)] ^ byte(key[0],1)];
|
| 220 |
+
pkey.mk_tab[2][i] = pkey.m_tab[2][pkey.q_tab[1][pkey.q_tab[0][by] ^ byte(key[1],2)] ^ byte(key[0],2)];
|
| 221 |
+
pkey.mk_tab[3][i] = pkey.m_tab[3][pkey.q_tab[1][pkey.q_tab[1][by] ^ byte(key[1],3)] ^ byte(key[0],3)];
|
| 222 |
+
if pkey.k_len == 3:
|
| 223 |
+
for i in range(256):
|
| 224 |
+
by = i % 0x100
|
| 225 |
+
pkey.mk_tab[0][i] = pkey.m_tab[0][pkey.q_tab[0][pkey.q_tab[0][pkey.q_tab[1][by] ^ byte(key[2], 0)] ^ byte(key[1], 0)] ^ byte(key[0], 0)];
|
| 226 |
+
pkey.mk_tab[1][i] = pkey.m_tab[1][pkey.q_tab[0][pkey.q_tab[1][pkey.q_tab[1][by] ^ byte(key[2], 1)] ^ byte(key[1], 1)] ^ byte(key[0], 1)];
|
| 227 |
+
pkey.mk_tab[2][i] = pkey.m_tab[2][pkey.q_tab[1][pkey.q_tab[0][pkey.q_tab[0][by] ^ byte(key[2], 2)] ^ byte(key[1], 2)] ^ byte(key[0], 2)];
|
| 228 |
+
pkey.mk_tab[3][i] = pkey.m_tab[3][pkey.q_tab[1][pkey.q_tab[1][pkey.q_tab[0][by] ^ byte(key[2], 3)] ^ byte(key[1], 3)] ^ byte(key[0], 3)];
|
| 229 |
+
if pkey.k_len == 4:
|
| 230 |
+
for i in range(256):
|
| 231 |
+
by = i % 0x100
|
| 232 |
+
pkey.mk_tab[0][i] = pkey.m_tab[0][pkey.q_tab[0][pkey.q_tab[0][pkey.q_tab[1][pkey.q_tab[1][by] ^ byte(key[3], 0)] ^ byte(key[2], 0)] ^ byte(key[1], 0)] ^ byte(key[0], 0)];
|
| 233 |
+
pkey.mk_tab[1][i] = pkey.m_tab[1][pkey.q_tab[0][pkey.q_tab[1][pkey.q_tab[1][pkey.q_tab[0][by] ^ byte(key[3], 1)] ^ byte(key[2], 1)] ^ byte(key[1], 1)] ^ byte(key[0], 1)];
|
| 234 |
+
pkey.mk_tab[2][i] = pkey.m_tab[2][pkey.q_tab[1][pkey.q_tab[0][pkey.q_tab[0][pkey.q_tab[0][by] ^ byte(key[3], 2)] ^ byte(key[2], 2)] ^ byte(key[1], 2)] ^ byte(key[0], 2)];
|
| 235 |
+
pkey.mk_tab[3][i] = pkey.m_tab[3][pkey.q_tab[1][pkey.q_tab[1][pkey.q_tab[0][pkey.q_tab[1][by] ^ byte(key[3], 3)] ^ byte(key[2], 3)] ^ byte(key[1], 3)] ^ byte(key[0], 3)];
|
| 236 |
+
|
| 237 |
+
def h_fun(pkey, x, key):
|
| 238 |
+
b0 = byte(x, 0);
|
| 239 |
+
b1 = byte(x, 1);
|
| 240 |
+
b2 = byte(x, 2);
|
| 241 |
+
b3 = byte(x, 3);
|
| 242 |
+
if pkey.k_len >= 4:
|
| 243 |
+
b0 = pkey.q_tab[1][b0] ^ byte(key[3], 0);
|
| 244 |
+
b1 = pkey.q_tab[0][b1] ^ byte(key[3], 1);
|
| 245 |
+
b2 = pkey.q_tab[0][b2] ^ byte(key[3], 2);
|
| 246 |
+
b3 = pkey.q_tab[1][b3] ^ byte(key[3], 3);
|
| 247 |
+
if pkey.k_len >= 3:
|
| 248 |
+
b0 = pkey.q_tab[1][b0] ^ byte(key[2], 0);
|
| 249 |
+
b1 = pkey.q_tab[1][b1] ^ byte(key[2], 1);
|
| 250 |
+
b2 = pkey.q_tab[0][b2] ^ byte(key[2], 2);
|
| 251 |
+
b3 = pkey.q_tab[0][b3] ^ byte(key[2], 3);
|
| 252 |
+
if pkey.k_len >= 2:
|
| 253 |
+
b0 = pkey.q_tab[0][pkey.q_tab[0][b0] ^ byte(key[1], 0)] ^ byte(key[0], 0);
|
| 254 |
+
b1 = pkey.q_tab[0][pkey.q_tab[1][b1] ^ byte(key[1], 1)] ^ byte(key[0], 1);
|
| 255 |
+
b2 = pkey.q_tab[1][pkey.q_tab[0][b2] ^ byte(key[1], 2)] ^ byte(key[0], 2);
|
| 256 |
+
b3 = pkey.q_tab[1][pkey.q_tab[1][b3] ^ byte(key[1], 3)] ^ byte(key[0], 3);
|
| 257 |
+
return pkey.m_tab[0][b0] ^ pkey.m_tab[1][b1] ^ pkey.m_tab[2][b2] ^ pkey.m_tab[3][b3];
|
| 258 |
+
|
| 259 |
+
def mds_rem(p0, p1):
|
| 260 |
+
i, t, u = 0, 0, 0
|
| 261 |
+
for i in range(8):
|
| 262 |
+
t = p1 >> 24
|
| 263 |
+
p1 = ((p1 << 8) & 0xffffffff) | (p0 >> 24)
|
| 264 |
+
p0 = (p0 << 8) & 0xffffffff
|
| 265 |
+
u = (t << 1) & 0xffffffff
|
| 266 |
+
if t & 0x80:
|
| 267 |
+
u ^= 0x0000014d
|
| 268 |
+
p1 ^= t ^ ((u << 16) & 0xffffffff)
|
| 269 |
+
u ^= (t >> 1)
|
| 270 |
+
if t & 0x01:
|
| 271 |
+
u ^= 0x0000014d >> 1
|
| 272 |
+
p1 ^= ((u << 24) & 0xffffffff) | ((u << 8) & 0xffffffff)
|
| 273 |
+
return p1
|
| 274 |
+
|
| 275 |
+
def set_key(pkey, in_key, key_len):
|
| 276 |
+
pkey.qt_gen = 0
|
| 277 |
+
if not pkey.qt_gen:
|
| 278 |
+
gen_qtab(pkey)
|
| 279 |
+
pkey.qt_gen = 1
|
| 280 |
+
pkey.mt_gen = 0
|
| 281 |
+
if not pkey.mt_gen:
|
| 282 |
+
gen_mtab(pkey)
|
| 283 |
+
pkey.mt_gen = 1
|
| 284 |
+
pkey.k_len = (key_len * 8) // 64
|
| 285 |
+
|
| 286 |
+
a = 0
|
| 287 |
+
b = 0
|
| 288 |
+
me_key = [0,0,0,0]
|
| 289 |
+
mo_key = [0,0,0,0]
|
| 290 |
+
for i in range(pkey.k_len):
|
| 291 |
+
if WORD_BIGENDIAN:
|
| 292 |
+
a = byteswap32(in_key[i + 1])
|
| 293 |
+
me_key[i] = a
|
| 294 |
+
b = byteswap32(in_key[i + i + 1])
|
| 295 |
+
else:
|
| 296 |
+
a = in_key[i + i]
|
| 297 |
+
me_key[i] = a
|
| 298 |
+
b = in_key[i + i + 1]
|
| 299 |
+
mo_key[i] = b
|
| 300 |
+
pkey.s_key[pkey.k_len - i - 1] = mds_rem(a, b);
|
| 301 |
+
for i in range(0, 40, 2):
|
| 302 |
+
a = (0x01010101 * i) % 0x100000000;
|
| 303 |
+
b = (a + 0x01010101) % 0x100000000;
|
| 304 |
+
a = h_fun(pkey, a, me_key);
|
| 305 |
+
b = rotl32(h_fun(pkey, b, mo_key), 8);
|
| 306 |
+
pkey.l_key[i] = (a + b) % 0x100000000;
|
| 307 |
+
pkey.l_key[i + 1] = rotl32((a + 2 * b) % 0x100000000, 9);
|
| 308 |
+
gen_mk_tab(pkey, pkey.s_key)
|
| 309 |
+
|
| 310 |
+
def encrypt(pkey, in_blk):
|
| 311 |
+
blk = [0, 0, 0, 0]
|
| 312 |
+
|
| 313 |
+
if WORD_BIGENDIAN:
|
| 314 |
+
blk[0] = byteswap32(in_blk[0]) ^ pkey.l_key[0];
|
| 315 |
+
blk[1] = byteswap32(in_blk[1]) ^ pkey.l_key[1];
|
| 316 |
+
blk[2] = byteswap32(in_blk[2]) ^ pkey.l_key[2];
|
| 317 |
+
blk[3] = byteswap32(in_blk[3]) ^ pkey.l_key[3];
|
| 318 |
+
else:
|
| 319 |
+
blk[0] = in_blk[0] ^ pkey.l_key[0];
|
| 320 |
+
blk[1] = in_blk[1] ^ pkey.l_key[1];
|
| 321 |
+
blk[2] = in_blk[2] ^ pkey.l_key[2];
|
| 322 |
+
blk[3] = in_blk[3] ^ pkey.l_key[3];
|
| 323 |
+
|
| 324 |
+
for i in range(8):
|
| 325 |
+
t1 = ( pkey.mk_tab[0][byte(blk[1],3)] ^ pkey.mk_tab[1][byte(blk[1],0)] ^ pkey.mk_tab[2][byte(blk[1],1)] ^ pkey.mk_tab[3][byte(blk[1],2)] );
|
| 326 |
+
t0 = ( pkey.mk_tab[0][byte(blk[0],0)] ^ pkey.mk_tab[1][byte(blk[0],1)] ^ pkey.mk_tab[2][byte(blk[0],2)] ^ pkey.mk_tab[3][byte(blk[0],3)] );
|
| 327 |
+
|
| 328 |
+
blk[2] = rotr32(blk[2] ^ ((t0 + t1 + pkey.l_key[4 * (i) + 8]) % 0x100000000), 1);
|
| 329 |
+
blk[3] = rotl32(blk[3], 1) ^ ((t0 + 2 * t1 + pkey.l_key[4 * (i) + 9]) % 0x100000000);
|
| 330 |
+
|
| 331 |
+
t1 = ( pkey.mk_tab[0][byte(blk[3],3)] ^ pkey.mk_tab[1][byte(blk[3],0)] ^ pkey.mk_tab[2][byte(blk[3],1)] ^ pkey.mk_tab[3][byte(blk[3],2)] );
|
| 332 |
+
t0 = ( pkey.mk_tab[0][byte(blk[2],0)] ^ pkey.mk_tab[1][byte(blk[2],1)] ^ pkey.mk_tab[2][byte(blk[2],2)] ^ pkey.mk_tab[3][byte(blk[2],3)] );
|
| 333 |
+
|
| 334 |
+
blk[0] = rotr32(blk[0] ^ ((t0 + t1 + pkey.l_key[4 * (i) + 10]) % 0x100000000), 1);
|
| 335 |
+
blk[1] = rotl32(blk[1], 1) ^ ((t0 + 2 * t1 + pkey.l_key[4 * (i) + 11]) % 0x100000000);
|
| 336 |
+
|
| 337 |
+
if WORD_BIGENDIAN:
|
| 338 |
+
in_blk[0] = byteswap32(blk[2] ^ pkey.l_key[4]);
|
| 339 |
+
in_blk[1] = byteswap32(blk[3] ^ pkey.l_key[5]);
|
| 340 |
+
in_blk[2] = byteswap32(blk[0] ^ pkey.l_key[6]);
|
| 341 |
+
in_blk[3] = byteswap32(blk[1] ^ pkey.l_key[7]);
|
| 342 |
+
else:
|
| 343 |
+
in_blk[0] = blk[2] ^ pkey.l_key[4];
|
| 344 |
+
in_blk[1] = blk[3] ^ pkey.l_key[5];
|
| 345 |
+
in_blk[2] = blk[0] ^ pkey.l_key[6];
|
| 346 |
+
in_blk[3] = blk[1] ^ pkey.l_key[7];
|
| 347 |
+
|
| 348 |
+
return
|
| 349 |
+
|
| 350 |
+
def decrypt(pkey, in_blk):
|
| 351 |
+
blk = [0, 0, 0, 0]
|
| 352 |
+
|
| 353 |
+
if WORD_BIGENDIAN:
|
| 354 |
+
blk[0] = byteswap32(in_blk[0]) ^ pkey.l_key[4];
|
| 355 |
+
blk[1] = byteswap32(in_blk[1]) ^ pkey.l_key[5];
|
| 356 |
+
blk[2] = byteswap32(in_blk[2]) ^ pkey.l_key[6];
|
| 357 |
+
blk[3] = byteswap32(in_blk[3]) ^ pkey.l_key[7];
|
| 358 |
+
else:
|
| 359 |
+
blk[0] = in_blk[0] ^ pkey.l_key[4];
|
| 360 |
+
blk[1] = in_blk[1] ^ pkey.l_key[5];
|
| 361 |
+
blk[2] = in_blk[2] ^ pkey.l_key[6];
|
| 362 |
+
blk[3] = in_blk[3] ^ pkey.l_key[7];
|
| 363 |
+
|
| 364 |
+
for i in range(7, -1, -1):
|
| 365 |
+
t1 = ( pkey.mk_tab[0][byte(blk[1],3)] ^ pkey.mk_tab[1][byte(blk[1],0)] ^ pkey.mk_tab[2][byte(blk[1],1)] ^ pkey.mk_tab[3][byte(blk[1],2)] )
|
| 366 |
+
t0 = ( pkey.mk_tab[0][byte(blk[0],0)] ^ pkey.mk_tab[1][byte(blk[0],1)] ^ pkey.mk_tab[2][byte(blk[0],2)] ^ pkey.mk_tab[3][byte(blk[0],3)] )
|
| 367 |
+
|
| 368 |
+
blk[2] = rotl32(blk[2], 1) ^ ((t0 + t1 + pkey.l_key[4 * (i) + 10]) % 0x100000000)
|
| 369 |
+
blk[3] = rotr32(blk[3] ^ ((t0 + 2 * t1 + pkey.l_key[4 * (i) + 11]) % 0x100000000), 1)
|
| 370 |
+
|
| 371 |
+
t1 = ( pkey.mk_tab[0][byte(blk[3],3)] ^ pkey.mk_tab[1][byte(blk[3],0)] ^ pkey.mk_tab[2][byte(blk[3],1)] ^ pkey.mk_tab[3][byte(blk[3],2)] )
|
| 372 |
+
t0 = ( pkey.mk_tab[0][byte(blk[2],0)] ^ pkey.mk_tab[1][byte(blk[2],1)] ^ pkey.mk_tab[2][byte(blk[2],2)] ^ pkey.mk_tab[3][byte(blk[2],3)] )
|
| 373 |
+
|
| 374 |
+
blk[0] = rotl32(blk[0], 1) ^ ((t0 + t1 + pkey.l_key[4 * (i) + 8]) % 0x100000000)
|
| 375 |
+
blk[1] = rotr32(blk[1] ^ ((t0 + 2 * t1 + pkey.l_key[4 * (i) + 9]) % 0x100000000), 1)
|
| 376 |
+
|
| 377 |
+
if WORD_BIGENDIAN:
|
| 378 |
+
in_blk[0] = byteswap32(blk[2] ^ pkey.l_key[0]);
|
| 379 |
+
in_blk[1] = byteswap32(blk[3] ^ pkey.l_key[1]);
|
| 380 |
+
in_blk[2] = byteswap32(blk[0] ^ pkey.l_key[2]);
|
| 381 |
+
in_blk[3] = byteswap32(blk[1] ^ pkey.l_key[3]);
|
| 382 |
+
else:
|
| 383 |
+
in_blk[0] = blk[2] ^ pkey.l_key[0];
|
| 384 |
+
in_blk[1] = blk[3] ^ pkey.l_key[1];
|
| 385 |
+
in_blk[2] = blk[0] ^ pkey.l_key[2];
|
| 386 |
+
in_blk[3] = blk[1] ^ pkey.l_key[3];
|
| 387 |
+
return
|
| 388 |
+
|
| 389 |
+
__testkey = b'\xD4\x3B\xB7\x55\x6E\xA3\x2E\x46\xF2\xA2\x82\xB7\xD4\x5B\x4E\x0D\x57\xFF\x73\x9D\x4D\xC9\x2C\x1B\xD7\xFC\x01\x70\x0C\xC8\x21\x6F'
|
| 390 |
+
__testdat = b'\x90\xAF\xE9\x1B\xB2\x88\x54\x4F\x2C\x32\xDC\x23\x9B\x26\x35\xE6'
|
| 391 |
+
assert b'l\xb4V\x1c@\xbf\n\x97\x05\x93\x1c\xb6\xd4\x08\xe7\xfa' == Twofish(__testkey).encrypt(__testdat)
|
| 392 |
+
assert __testdat == Twofish(__testkey).decrypt(b'l\xb4V\x1c@\xbf\n\x97\x05\x93\x1c\xb6\xd4\x08\xe7\xfa')
|
| 393 |
+
|
Decipher/validator.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ipaddress
|
| 2 |
+
from typing import List, Dict, Set
|
| 3 |
+
from Decipher.models import NetworkTopology
|
| 4 |
+
|
| 5 |
+
def validate_topology(topology: NetworkTopology) -> List[str]:
|
| 6 |
+
"""
|
| 7 |
+
Performs logical validation on the network topology and returns a list of warnings/errors.
|
| 8 |
+
"""
|
| 9 |
+
issues = []
|
| 10 |
+
device_names = {d.name for d in topology.devices}
|
| 11 |
+
ip_to_interface = {} # ip -> (device, interface)
|
| 12 |
+
|
| 13 |
+
# 1. Check for IP Conflicts and basic interface sanity
|
| 14 |
+
for device in topology.devices:
|
| 15 |
+
for intf in device.interfaces:
|
| 16 |
+
if intf.ip:
|
| 17 |
+
# Check for duplicate IPs
|
| 18 |
+
if intf.ip in ip_to_interface:
|
| 19 |
+
other_dev, other_intf = ip_to_interface[intf.ip]
|
| 20 |
+
issues.append(f"IP Conflict: {intf.ip} is assigned to both {device.name} ({intf.name}) and {other_dev} ({other_intf})")
|
| 21 |
+
else:
|
| 22 |
+
ip_to_interface[intf.ip] = (device.name, intf.name)
|
| 23 |
+
|
| 24 |
+
# Basic IP format validation
|
| 25 |
+
try:
|
| 26 |
+
if "/" in intf.ip:
|
| 27 |
+
ipaddress.ip_interface(intf.ip)
|
| 28 |
+
else:
|
| 29 |
+
ipaddress.ip_address(intf.ip)
|
| 30 |
+
except ValueError:
|
| 31 |
+
issues.append(f"Invalid IP Format: '{intf.ip}' on {device.name} ({intf.name})")
|
| 32 |
+
|
| 33 |
+
# 2. Ensure Link Endpoints Exist
|
| 34 |
+
for link in topology.physical_links:
|
| 35 |
+
if link.from_device not in device_names:
|
| 36 |
+
issues.append(f"Broken Link: Source device '{link.from_device}' does not exist.")
|
| 37 |
+
if link.to_device not in device_names:
|
| 38 |
+
issues.append(f"Broken Link: Destination device '{link.to_device}' does not exist.")
|
| 39 |
+
|
| 40 |
+
# 3. Verify Routing Protocols (Basic Sanity)
|
| 41 |
+
for device in topology.devices:
|
| 42 |
+
if device.routing and device.routing.ospf:
|
| 43 |
+
device_ips = [intf.ip for intf in device.interfaces if intf.ip]
|
| 44 |
+
for ospf in device.routing.ospf:
|
| 45 |
+
for net_statement in ospf.networks:
|
| 46 |
+
# Check if the OSPF network statement matches at least one interface IP segment
|
| 47 |
+
try:
|
| 48 |
+
# network statement is usually "network 10.0.0.0 0.0.0.255 area 0"
|
| 49 |
+
# we extract the network and wildcard mask
|
| 50 |
+
parts = net_statement.split()
|
| 51 |
+
if "network" in parts:
|
| 52 |
+
net_addr = parts[parts.index("network") + 1]
|
| 53 |
+
# Simple check: does any device IP start with the same prefix?
|
| 54 |
+
# This is a heuristic check
|
| 55 |
+
found_match = False
|
| 56 |
+
for ip in device_ips:
|
| 57 |
+
if ip.split('.')[0:2] == net_addr.split('.')[0:2]:
|
| 58 |
+
found_match = True
|
| 59 |
+
break
|
| 60 |
+
if not found_match:
|
| 61 |
+
issues.append(f"OSPF Warning: Network statement '{net_statement}' on {device.name} doesn't seem to match any local interface IPs.")
|
| 62 |
+
except Exception:
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
return issues
|
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Python runtime as a parent image
|
| 2 |
+
FROM python:3.13-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory in the container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system dependencies if any (none needed for now)
|
| 8 |
+
# RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
+
# build-essential \
|
| 10 |
+
# && rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Copy the requirements file into the container
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
|
| 15 |
+
# Install any needed packages specified in requirements.txt
|
| 16 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 17 |
+
|
| 18 |
+
# Copy the current directory contents into the container at /app
|
| 19 |
+
COPY . .
|
| 20 |
+
|
| 21 |
+
# Hugging Face Spaces uses port 7860 by default
|
| 22 |
+
ENV PORT=7860
|
| 23 |
+
|
| 24 |
+
# Run the application
|
| 25 |
+
CMD ["python", "main.py"]
|
main.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
import os
|
| 4 |
+
import shutil
|
| 5 |
+
import tempfile
|
| 6 |
+
import xml.etree.ElementTree as ET
|
| 7 |
+
from Decipher.pt_crypto import decrypt_pkt
|
| 8 |
+
from Decipher.extract import extract_structured_data
|
| 9 |
+
from Decipher.generator import generate_network
|
| 10 |
+
from Decipher.models import NetworkTopology
|
| 11 |
+
from pydantic import Field, BaseModel
|
| 12 |
+
|
| 13 |
+
class PromptRequest(BaseModel):
|
| 14 |
+
prompt: str = Field(..., description="The description of the network to generate", json_schema_extra={"example": "A network with 1 router and 2 PCs"})
|
| 15 |
+
|
| 16 |
+
app = FastAPI(title="TopoScan API", description="API for decrypting and extracting data from Cisco Packet Tracer files")
|
| 17 |
+
|
| 18 |
+
# Enable CORS
|
| 19 |
+
app.add_middleware(
|
| 20 |
+
CORSMiddleware,
|
| 21 |
+
allow_origins=["*"],
|
| 22 |
+
allow_credentials=True,
|
| 23 |
+
allow_methods=["*"],
|
| 24 |
+
allow_headers=["*"],
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
@app.get("/")
|
| 28 |
+
async def root():
|
| 29 |
+
return {"message": "TopoScan API is running"}
|
| 30 |
+
|
| 31 |
+
@app.post("/analyze-pkt")
|
| 32 |
+
async def analyze_pkt(file: UploadFile = File(...)):
|
| 33 |
+
if not file.filename.endswith(".pkt") and not file.filename.endswith(".pka"):
|
| 34 |
+
raise HTTPException(status_code=400, detail="Only .pkt or .pka files are supported")
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
# Read the uploaded file content
|
| 38 |
+
content = await file.read()
|
| 39 |
+
|
| 40 |
+
# Decrypt the file
|
| 41 |
+
try:
|
| 42 |
+
xml_data = decrypt_pkt(content)
|
| 43 |
+
except Exception as e:
|
| 44 |
+
raise HTTPException(status_code=500, detail=f"Decryption failed: {str(e)}")
|
| 45 |
+
|
| 46 |
+
# Create a temporary file to store the XML for extraction
|
| 47 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".xml") as temp_xml:
|
| 48 |
+
temp_xml.write(xml_data)
|
| 49 |
+
temp_xml_path = temp_xml.name
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
# Extract structured data using existing logic
|
| 53 |
+
structured_data = extract_structured_data(temp_xml_path)
|
| 54 |
+
|
| 55 |
+
if structured_data is None:
|
| 56 |
+
raise HTTPException(status_code=500, detail="Failed to extract structured data from decrypted XML")
|
| 57 |
+
|
| 58 |
+
return structured_data
|
| 59 |
+
|
| 60 |
+
finally:
|
| 61 |
+
# Clean up the temporary file
|
| 62 |
+
if os.path.exists(temp_xml_path):
|
| 63 |
+
os.remove(temp_xml_path)
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
raise HTTPException(status_code=500, detail=f"An error occurred: {str(e)}")
|
| 67 |
+
|
| 68 |
+
@app.post("/generate-network", response_model=NetworkTopology)
|
| 69 |
+
async def api_generate_network(request: PromptRequest):
|
| 70 |
+
try:
|
| 71 |
+
topology = generate_network(request.prompt)
|
| 72 |
+
return topology
|
| 73 |
+
except Exception as e:
|
| 74 |
+
raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
import uvicorn
|
| 78 |
+
# Hugging Face Spaces and other cloud providers often use the PORT environment variable
|
| 79 |
+
port = int(os.environ.get("PORT", 8000))
|
| 80 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.128.3
|
| 2 |
+
uvicorn==0.40.0
|
| 3 |
+
pydantic==2.12.5
|
| 4 |
+
python-dotenv==1.2.1
|
| 5 |
+
openai==2.36.0
|
| 6 |
+
ciscoconfparse==1.9.52
|
| 7 |
+
python-multipart==0.0.22
|
| 8 |
+
requests==2.32.5
|