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)