#!/usr/bin/env python3 # cypher - a text transformation utility supporting ROT13/18/47, reverse, # binary and your own custom alphanumeric substitution cipher. # Copyright 2026 640kb.neocities.org # Blue Oak Model License 1.0.0 # # 11aug2026: Initial release import sys import argparse if sys.version_info < (3, 6): sys.exit("Error: This script requires Python 3.6 or newer.") # =========================================================================== # CONFIGURATION # =========================================================================== # Custom cipher settings (used with the -secret flag) # Keyword for alphabet substitution. It must be a string with non-repeating # letters. Case insensitive. Default: DEBUG CUSTOM_KEYWORD = "DEBUG" # Custom digit mapping. Must contain all 10 numbers. Default: 2148675309 CUSTOM_DIGITS = "2148675309" # =========================================================================== # END CONFIGURATION # =========================================================================== def validate_config(): if len(CUSTOM_KEYWORD) != len(set(CUSTOM_KEYWORD.upper())): sys.exit("Error: Keyword must be a word with non-repeating letters") if not CUSTOM_KEYWORD.isalpha(): sys.exit("Error: Keyword must contain only letters (A-Z)") if sorted(CUSTOM_DIGITS) != list("0123456789"): sys.exit("Error: Digits must contain all 10 numbers, exactly once") def generate_cipher_mappings(): keyword = CUSTOM_KEYWORD.upper() unique_keyword = '' for char in keyword: if char not in unique_keyword: unique_keyword += char upper_cipher = unique_keyword for char in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": if char not in upper_cipher: upper_cipher += char lower_cipher = '' for char in "abcdefghijklmnopqrstuvwxyz": if char not in keyword.lower(): lower_cipher += char lower_cipher += keyword.lower() return upper_cipher, lower_cipher def custom_cipher(text): validate_config() upper_cipher, lower_cipher = generate_cipher_mappings() trans = str.maketrans({ **dict(zip("ABCDEFGHIJKLMNOPQRSTUVWXYZ", upper_cipher)), **dict(zip("abcdefghijklmnopqrstuvwxyz", lower_cipher)), **dict(zip("0123456789", CUSTOM_DIGITS)) }) return text.translate(trans) def show_help(): print(""" cypher: text transformation (ROT13/18/47, reverse, binary, custom cipher) Reads from files or stdin. Not secure - just for fun. USAGE: cypher {-r13 | -r18 | -r47 | -rev | -bin | -secret} [file] FLAGS: -r13 ROT13 - rotates letters by 13 positions -r18 ROT18 - rotates letters by 13 and digits by 5 -r47 ROT47 - rotates letters, numbers, punctuation by 47 -rev reverse - reverses the order of text -bin binary - converts text to/from binary (0s and 1s) -secret custom, user-configurable substitution cipher --map explain the substitution mapping used with -secret -h Show this help EXAMPLES: # encode/decode a local text file cypher -r13 file.txt > out.txt # read remote files as stdin finger user@domain.com | cypher -r13 curl -s http://example.com/file.txt | cypher -bin # reverse a string using echo echo "hello world" | cypher -rev # custom cipher using configurable keyword and digit mapping echo "Peace and long life. 867-5309" | cypher -secret # chaining transformations (reverse the order of flags to decode) cat file.txt | cypher -r47 | cypher -rev > encoded.txt cat encoded.txt | cypher -rev | cypher -r47 > original.txt CONFIGURATION: To customize the -secret cipher, edit CUSTOM_KEYWORD and CUSTOM_DIGITS at the top of the script. """) def show_cyphermap(): print(""" Default Configuration Keyword: DEBUG (unique letters: D, E, B, U, G) Digits: 2148675309 1. Uppercase Mapping (Keyword at START) Plain: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Cipher: D E B U G A C F H I J K L M N O P Q R S T V W X Y Z 2. Lowercase Mapping (Keyword at END) Plain: a b c d e f g h i j k l m n o p q r s t u v w x y z Cipher: a c f h i j k l m n o p q r s t v w x y z d e b u g 3. Digit Mapping Plain: 0 1 2 3 4 5 6 7 8 9 Cipher: 2 1 4 8 6 7 5 3 0 9 Example: $ echo "Peace and long life. 867-5309" | cypher -secret Result: Oiafi arh psrk pmji. 053-7829 """) def rot13(text): result = [] for char in text: if 'a' <= char <= 'z': result.append(chr((ord(char) - ord('a') + 13) % 26 + ord('a'))) elif 'A' <= char <= 'Z': result.append(chr((ord(char) - ord('A') + 13) % 26 + ord('A'))) else: result.append(char) return ''.join(result) def rot18(text): result = [] for char in text: if 'a' <= char <= 'z': result.append(chr((ord(char) - ord('a') + 13) % 26 + ord('a'))) elif 'A' <= char <= 'Z': result.append(chr((ord(char) - ord('A') + 13) % 26 + ord('A'))) elif '0' <= char <= '9': result.append(chr((ord(char) - ord('0') + 5) % 10 + ord('0'))) else: result.append(char) return ''.join(result) def rot47(text): result = [] for char in text: if 33 <= ord(char) <= 126: result.append(chr(33 + ((ord(char) - 33 + 47) % 94))) else: result.append(char) return ''.join(result) def reverse_text(text): return text[::-1] def text_to_binary(text): return ' '.join(format(ord(char), '08b') for char in text) def binary_to_text(text): text = ''.join(text.split()) result = [] for i in range(0, len(text), 8): byte = text[i:i+8] if len(byte) == 8: result.append(chr(int(byte, 2))) return ''.join(result) def is_binary(text): stripped = ''.join(text.split()) return len(stripped) > 0 and all(c in '01' for c in stripped) def main(): if len(sys.argv) == 1 or sys.argv[1] in ("-h", "--help"): show_help() sys.exit(0) if len(sys.argv) >= 2 and sys.argv[1] == "--map": show_cyphermap() sys.exit(0) parser = argparse.ArgumentParser(description='ROT cipher encoder/decoder', add_help=False) parser.add_argument('file', nargs='?', help='Input file (optional, reads from stdin if not provided)') group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-r13', action='store_true', help='ROT13 encode/decode') group.add_argument('-r18', action='store_true', help='ROT18 encode/decode') group.add_argument('-r47', action='store_true', help='ROT47 encode/decode') group.add_argument('-rev', action='store_true', help='Reverse text') group.add_argument('-bin', action='store_true', help='Convert to/from binary') group.add_argument('-secret', action='store_true', help='Custom substitution cipher') args = parser.parse_args() if args.file: try: with open(args.file, 'r', encoding='utf-8') as f: text = f.read() except FileNotFoundError: print(f"Error: File '{args.file}' not found", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Error reading file: {e}", file=sys.stderr) sys.exit(1) else: if sys.stdin.isatty(): show_help() sys.exit(1) text = sys.stdin.read() if args.r13: output = rot13(text) elif args.r18: output = rot18(text) elif args.r47: output = rot47(text) elif args.rev: output = reverse_text(text) elif args.bin: if is_binary(text): output = binary_to_text(text) else: output = text_to_binary(text) elif args.secret: output = custom_cipher(text) sys.stdout.write(output) if __name__ == '__main__': main()