import argparse
import random
import sys
from typing import NamedTuple

# The key used in all cisco's devices is publicly known
KEY = b"dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87"


class Cisco7Error(Exception):
    pass


def encrypt(secret: bytes, key: bytes, force_init: int | None = None) -> str:
    """Encrypt a password using Cisco's type 7 algorithm."""

    # Start with a random offset if it is not forced
    if force_init is not None:
        if force_init < 0 or force_init >= len(key):
            raise Cisco7Error("Invalid initial offset")
        offset = force_init
    else:
        offset = random.randint(0, min(len(key) - 1, 255))

    # Store the initial offset
    initial_offset = format(offset, "02d")

    # Encrypt
    ciphertext = []
    for byte in secret:
        ciphertext.append(byte ^ key[offset])
        offset = (offset + 1) % len(key)

    return initial_offset + bytes(ciphertext).hex()


def decrypt(ciphertext: str, key: bytes) -> bytes:
    """Decrypt Cisco type 7 passwords"""

    # Get the initial offset from the first two characters
    if len(ciphertext) < 2:
        raise Cisco7Error("Invalid Cisco type 7 password: too short")
    offset = int(ciphertext[:2])
    if offset >= len(key):
        raise Cisco7Error("Invalid initial offset in Cisco type 7 password")

    # Decrypt the password
    secret = []
    for byte in bytes.fromhex(ciphertext[2:]):
        secret.append(byte ^ key[offset])
        offset = (offset + 1) % len(key)

    return bytes(secret)


Args = NamedTuple(
    "Args",
    encrypt=str | None,
    decrypt=str | None,
    key=str | None,
)


def parse_args() -> Args:
    parser = argparse.ArgumentParser(
        description="Cisco type 7 password encryption/decryption",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""note:
    Encryption is non-deterministic.

examples:
    %(prog)s -e 'My Sup3r S3cr3t P4ssw0rd'
    %(prog)s -d '07362E590E1B1C041B1E124C0A2F2E206832752E1A01134D'""",
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument(
        "-e",
        "--encrypt",
        nargs=1,
        metavar="SECRET",
        action="store",
    )
    group.add_argument(
        "-d",
        "--decrypt",
        nargs=1,
        metavar="CIPHERTEXT",
        action="store",
    )
    parser.add_argument(
        "-k",
        "--key",
        nargs=1,
        help="use this encryption/decryption key (in hex format), "
        "defaults to the publicly known Cisco key",
    )

    args = parser.parse_args()
    return Args(
        encrypt=args.encrypt[0] if args.encrypt else None,
        decrypt=args.decrypt[0] if args.decrypt else None,
        key=args.key[0] if args.key else None,
    )


def warn(msg: str) -> None:
    print(f"[WARNING] {msg}", file=sys.stderr)


def error(msg: str, rc: int) -> None:
    print(f"[ERROR] {msg}", file=sys.stderr)
    exit(rc)


def interpret_argument(arg: str) -> str:
    # If arg is -, read from stdin
    if arg == "-":
        arg = sys.stdin.readline()
        # Remove trailing newline if present
        if arg and arg[-1] == "\n":
            arg = arg[:-1]
    return arg


if __name__ == "__main__":
    args = parse_args()

    key = args.key or KEY
    if args.encrypt:
        secret = interpret_argument(args.encrypt)
        # At the theoretical level, nothing prevents us from accepting
        # arbitrary utf-8 strings as passwords.
        # But I'm not sure IOS handles non-ascii characters.
        if not secret.isascii():
            warn("Secret contains non-ascii characters")
        try:
            print(encrypt(secret.encode("utf-8"), key))
        except Cisco7Error as exn:
            error(exn.args[0], 1)
    else:
        ciphertext = interpret_argument(args.decrypt)
        try:
            print(decrypt(ciphertext, key).decode("utf-8"))
        except Cisco7Error as exn:
            error(exn.args[0], 1)