#!/usr/bin/env python3
from __future__ import print_function
import sys
import argparse
from urllib.parse import unquote, urlencode
from urllib.request import urlopen
from base64 import b64decode, b32decode, b16decode, a85decode, b85decode
from string import ascii_lowercase as lowercase, ascii_uppercase as uppercase
import binascii
import re

# Check Python version
if sys.version_info[0] == 3:
    text_type = str
    binary_type = bytes
else:
    text_type = unicode
    binary_type = str

# Argument parser setup
parser = argparse.ArgumentParser()
parser.add_argument('string', help='String to be decoded')
parser.add_argument('-s', help='For sensitive data', dest='sensitive', action="store_true")
parser.add_argument('-rev', help='Reverse String', dest='rev', action="store_true")
parser.add_argument('-rot', help='Caesar Cipher Offset', dest='roter', type=int)
parser.add_argument('-q', help='Hides the banner', dest='quiet', action='store_true')

args = parser.parse_args()
string = args.string
sensitive = args.sensitive
decoded = []
loop = 0


def ensure_str(s, encoding='latin-1', errors='strict'):
    """Coerce *s* to `str` based on Python version."""
    if not isinstance(s, (text_type, binary_type, bytearray)):
        raise TypeError("not expecting type ''" % type(s))
    
    if isinstance(s, binary_type) or isinstance(s, bytearray):
        s = s.decode(encoding, errors)
    return s


def check_ascii(s):
    """Check if the string is ASCII."""
    return all(ord(c) < 128 for c in s)


def banner():
    if not args.quiet:
        msg = (r'''
              __                         __      
            |/  |                   | / /        
            |   | ___  ___  ___  ___|  (         
            |   )|___)|    |   )|   )| |___ \   )
            |__/ |__  |__  |__/ |__/ | |     \_/ 
                                              /  
            https://github.com/s0md3v/Decodify''')
        print(msg)


def decode(string):
    """Main decoding function."""
    string = ensure_str(string)

    # Check for various encodings
    if check_ascii(string):
        binary = re.search(r'^[01]+$', string)
        if binary:
            bin_to_ascii(string)

        sha2 = re.search(r'^[a-f0-9]{64}$', string)
        if sha2 and not sensitive:
            sha2_crack(string)

        sha1 = re.search(r'^[a-f0-9]{40}$', string)
        if sha1 and not sensitive:
            sha1_crack(string)

        md5 = re.search(r'^[a-f0-9]{32}$', string)
        if md5 and not sensitive:
            md5_crack(string)

        url = re.search(r'(%..)+', string)
        if url:
            url_decode(string)

        hexx = re.search(r'^(0x|0X)?[a-fA-F0-9]+$', string)
        if hexx:
            hex_decode(string)

        b64 = re.search(r'^[A-Za-z0-9+/]+={0,2}$', string)
        if len(string) % 4 == 0 and b64:
            base64_decode(string)

        b32 = re.search(r'^[A-Z2-7]+={0,6}$', string)
        if len(string) % 8 == 0 and b32:
            base32_decode(string)

        b16 = re.search(r'^[0-9A-Fa-f]+$', string)
        if len(string) % 2 == 0 and b16:
            base16_decode(string)

        b85 = re.search(r'^[!-u]+$', string)
        if b85:
            base85_decode(string)

        ipv6_base85 = re.search(r'^[0-9A-Za-z!#$%&()*+\-;<=>?@^_`{|}~]+$', string)  # Match any Base85-encoded string
        if ipv6_base85:
            decode_base85_ipv6(string)  # Call the IPv6 decoding function

        zmq_base85 = re.search(r'^[0-9a-zA-Z.\-:+=^!/*?&<>()\[\]{}@%$#]+$', string)  # Match any Base85-encoded string
        if zmq_base85:
            decode_base85_ipv6(string)  # Call the ZeroMQ decoding function

        deci = re.search(r'&#.*;+', string)
        if deci:
            decimal(string)

    else:
        print('\033[1;31m[-]\033[1;m Failed to detect the encoding.')
        quit()


def rot_decode(string, max_rot=1000):
    """Function to decode any ROT (Caesar Cipher) encoding up to ROT1000."""
    def rotate_char(char, rot):
        if char in lowercase:
            return lowercase[(lowercase.index(char) + rot) % 26]
        elif char in uppercase:
            return uppercase[(uppercase.index(char) + rot) % 26]
        else:
            return char  # Non-alphabet characters remain unchanged

    def rotate_string(s, rot):
        return ''.join(rotate_char(c, rot) for c in s)

    for rot in range(1, max_rot + 1):
        decoded_string = rotate_string(string, rot)
        if check_ascii(decoded_string):  # Ensure the decoded string is valid ASCII
            print(f"Decoded using ROT{rot}: {decoded_string}")
    return decoded_string


def decode_base85_ipv6(string, swap=False):
    """Decode a Base85-encoded IPv6 address."""
    try:
        # Decode the Base85 string
        decoded_bytes = b85decode(string.encode('utf-8'))
        # Convert the bytes to an IPv6 string
        try:
            decoded_string = decoded_bytes.decode()
        except:
            print(decoded_string) 
            return

        # Check if the decoded IPv6 address is already in the decoded list
        if decoded_string in decoded:
            return  # Exit if already decoded

        print(f'\033[1;32m[+]\033[1;m Decoded from Base85 IPv6 or ZeroMQ: {decoded_string}')
        decoded.append(decoded_string)

        # Optionally decode further
        decode(decoded_string)
    except:
        if not swap: decode_base85_ipv6(string.swapcase(), True)
        return


def sha2_crack(string):
    html = urlopen("http://md5decrypt.net/Api/api.php?hash=" + string + "&hash_type=sha256&email=deanna_abshire@proxymail.eu&code=1152464b80a61728")
    decoded_string = ensure_str(html.read())
    if decoded_string:
        if decoded_string in decoded:
            return  # Exit if already decoded
        
        print(f'\033[1;32m[+]\033[1;m Cracked SHA2 Hash: {decoded_string}')
        decoded.append(decoded_string)
        decode(decoded_string)
    else:
        print('\033[1;31m[-]\033[1;m Its a SHA2 Hash but I failed to crack it.')


def decimal(string):
    """Decode a string from decimal character references."""
    # Ensure the input string is properly formatted
    string = ensure_str(string)
    
    # Remove HTML entity format (e.g., &#1234;)
    string = string.replace('&#', '').replace(';', ' ')
    
    # Split the string into individual decimal values
    str_list = string.split()
    
    # Initialize a list to hold the decoded characters
    calculated = []
    
    for i in str_list:
        if i.strip():  # Check if the string is not empty
            try:
                # Convert the decimal value to an integer and then to a character
                char = chr(int(i))
                calculated.append(char)
            except ValueError:
                # If conversion fails, skip to the next value
                continue

    # Join the list of characters into a single string
    decoded_string = ''.join(calculated).encode('utf-8')
    
    # Check if the decoded string is already in the decoded list
    if decoded_string in decoded:
        return  # Exit if it's already decoded
    
    try:
        print(f'\033[1;32m[+]\033[1;m Decoded from Decimal: {decoded_string.decode()}')
    except:
        return
    decoded.append(decoded_string)
    decode(decoded_string)
    

def sha1_crack(string):
    data = urlencode({"auth": "8272hgt", "hash": string, "string": "", "Submit": "Submit"})
    html = urlopen("http://hashcrack.com/index.php", data)
    match = re.search(r'<span class=hervorheb2>[^<]*</span></div></TD>', html.read())
    if match:
        decoded_string = match.group().split('hervorheb2>')[1][:-18]

        if decoded_string in decoded:
            return  # Exit if already decoded
        
        print(f'\033[1;32m[+]\033[1;m Cracked SHA1: {decoded_string}')
        decoded.append(decoded_string)
        decode(decoded_string)
    else:
        print('\033[1;31m[-]\033[1;m Its a SHA1 Hash but I failed to crack it.')


def md5_crack(string):
    url = "http://www.nitrxgen.net/md5db/" + string
    decoded_str = ensure_str(urlopen(url).read())
    if decoded_str:
        if decoded_str in decoded:
            return  # Exit if already decoded
        
        print(f'\033[1;32m[+]\033[1;m Cracked MD5 Hash: {decoded_str}')
        decoded.append(decoded_str)
        decode(decoded_str)
    else:
        print('\033[1;31m[-]\033[1;m Its a MD5 Hash but I failed to crack it.')


def url_decode(string):
    decoded_str = unquote(string)

    if decoded_str in decoded:
            return  # Exit if already decoded
    
    print(f'\033[1;32m[+]\033[1;m Decoded from URL encoding: {decoded_str}')
    decoded.append(decoded_str)
    decode(decoded_str)


def hex_decode(string):
    try:
        decoded_str = int(string, 16)

        if decoded_str in decoded:
            return  # Exit if already decoded
        
        print(f'\033[1;32m[+]\033[1;m Decoded from Hex: {decoded_str}')
        decoded.append(decoded_str)
        decode(decoded_str)
    except:
        return


def base64_decode(string):
    try:
        decoded_str = b64decode(string).decode()

        if decoded_str in decoded:
            return  # Exit if already decoded
        
        print(f'\033[1;32m[+]\033[1;m Decoded from Base64: {decoded_str}')
        decoded.append(decoded_str)
        decode(decoded_str)
    except:
        return


def base32_decode(string):
    try:
        decoded_str = b32decode(string, casefold=True).decode()

        if decoded_str in decoded:
            return  # Exit if already decoded
        
        print(f'\033[1;32m[+]\033[1;m Decoded from Base32: {decoded_str}')
        decoded.append(decoded_str)
        decode(decoded_str)
    except:
        return


def base16_decode(string):
    try:
        decoded_str = b16decode(string, casefold=True).decode()

        if decoded_str in decoded:
            return  # Exit if already decoded
        
        print(f'\033[1;32m[+]\033[1;m Decoded from Base16: {decoded_str}')
        decoded.append(decoded_str)
        decode(decoded_str)
    except:
        return


def base85_decode(string):
    try:
        decoded_str = a85decode(string.encode('utf-8')).decode()

        if decoded_str in decoded:
            return  # Exit if already decoded
        
        print(f'\033[1;32m[+]\033[1;m Decoded from Base85: {decoded_str}')
        decoded.append(decoded_str)
        decode(decoded_str)
    except:
        return


def bin_to_ascii(string):
    n = int(string, 2)
    decoded_str = binascii.unhexlify('%x' % n).decode()

    if decoded_str in decoded:
            return  # Exit if already decoded
    
    print(f'\033[1;32m[+]\033[1;m Decoded from Binary: {decoded_str}')
    decoded.append(decoded_str)
    decode(decoded_str)


def main():
    global string
    banner()
    if args.roter:
        string = rot_decode(args.string, args.roter)
        return
    if args.rev:
        string = string[::-1]
        print(f'\033[1;32m[+]\033[1;m Reversed String: {string}')
    decode(string)


if __name__ == "__main__":
    main()
