#!/usr/bin/env python
# mojicrypt — aes-256-gcm encryption with unicode-encoded output
# turns your secrets into a wall of emojis and symbols
# works on text, binary files, images, whatever you throw at it
import sys
import os
import argparse
import secrets
import time
import uuid
try:
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import scrypt
from Crypto.Random import get_random_bytes
except ModuleNotFoundError:
from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import scrypt
from Cryptodome.Random import get_random_bytes
APP_NAME = "mojicrypt"
VERSION = "20260303"
KEYFILE_LEN = 8192 # characters of hex entropy written to auto-generated keyfiles
# params
SCRYPT_N = 2**18 # cpu/memory cost
SCRYPT_R = 8 # block size
SCRYPT_P = 1 # parallelization
SALT_LEN = 16 # 128-bit salt
KEY_LEN = 32 # 256-bit key
NONCE_LEN = 12 # 96-bit nonce (gcm standard)
TAG_LEN = 16 # 128-bit auth tag
HEADER_LEN = SALT_LEN + NONCE_LEN + TAG_LEN # 44 bytes overhead
# glyphs
# 256 unique non-alphanumeric unicode symbols
# each byte (0x00-0xFF) maps to one glyph — simple 1:1 encoding
# curated from: emoji faces, nature, animals, food, misc symbols,
# arrows, geometric shapes, and math operators
def _build_glyph_table():
"""construct the 256-char glyph table from curated unicode ranges"""
codepoints = []
# emoji faces — smileys and expressions (64)
codepoints.extend(range(0x1F600, 0x1F640))
# weather and nature — cyclone through shooting star (32)
codepoints.extend(range(0x1F300, 0x1F320))
# animals — rat through blowfish (32)
codepoints.extend(range(0x1F400, 0x1F420))
# food — tomato through shrimp (32)
codepoints.extend(range(0x1F345, 0x1F365))
# misc symbols — sun through pointing fingers (32)
codepoints.extend(range(0x2600, 0x2620))
# arrows — left through rightwards paired (16)
codepoints.extend(range(0x2190, 0x21A0))
# geometric shapes — black square through white vertical rectangle (16)
codepoints.extend(range(0x25A0, 0x25B0))
# mathematical operators — for all through right angle (32)
codepoints.extend(range(0x2200, 0x2220))
table = "".join(chr(cp) for cp in codepoints)
# sanity checks
assert len(table) == 256, f"glyph table has {len(table)} entries, expected 256"
assert len(set(table)) == 256, "duplicate glyphs detected in table"
for g in table:
assert not g.isalnum(), f"alphanumeric char found: {g} (U+{ord(g):04X})"
assert not g.isspace(), f"whitespace char found: {g} (U+{ord(g):04X})"
# no joiners (ZWJ, ZWNJ, BOM)
assert ord(g) not in (0x200D, 0x200C, 0xFEFF), \
f"joiner/bom found: U+{ord(g):04X}"
return table
GLYPH_TABLE = _build_glyph_table()
GLYPH_TO_BYTE = {g: i for i, g in enumerate(GLYPH_TABLE)}
# encode/decode
def bytes_to_glyphs(data: bytes) -> str:
"""encode raw bytes as unicode glyphs"""
return "".join(GLYPH_TABLE[b] for b in data)
def glyphs_to_bytes(text: str) -> bytes:
"""decode unicode glyphs back to raw bytes"""
result = []
for g in text:
if g not in GLYPH_TO_BYTE:
print(
f"error: invalid glyph in ciphertext: {g} (U+{ord(g):04X})",
file=sys.stderr,
)
sys.exit(1)
result.append(GLYPH_TO_BYTE[g])
return bytes(result)
# crypto operations
def derive_key(passphrase: str, salt: bytes) -> bytes:
"""derive a 256-bit key from passphrase using scrypt"""
return scrypt(
passphrase.encode("utf-8"),
salt,
key_len=KEY_LEN,
N=SCRYPT_N,
r=SCRYPT_R,
p=SCRYPT_P,
)
def encrypt_bytes(data: bytes, passphrase: str) -> str:
"""encrypt raw bytes with aes-256-gcm, return unicode-encoded ciphertext"""
salt = get_random_bytes(SALT_LEN)
key = derive_key(passphrase, salt)
nonce = get_random_bytes(NONCE_LEN)
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(data)
# wire format: salt(16) || nonce(12) || tag(16) || ciphertext(N)
packed = salt + nonce + tag + ciphertext
return bytes_to_glyphs(packed)
def decrypt_bytes(encoded: str, passphrase: str) -> bytes:
"""decode unicode glyphs and decrypt with aes-256-gcm, return raw bytes"""
raw = glyphs_to_bytes(encoded)
if len(raw) < HEADER_LEN:
print("error: ciphertext too short to be valid", file=sys.stderr)
sys.exit(1)
# unpack: salt(16) || nonce(12) || tag(16) || ciphertext(N)
salt = raw[:SALT_LEN]
nonce = raw[SALT_LEN:SALT_LEN + NONCE_LEN]
tag = raw[SALT_LEN + NONCE_LEN:HEADER_LEN]
ciphertext = raw[HEADER_LEN:]
key = derive_key(passphrase, salt)
try:
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
return cipher.decrypt_and_verify(ciphertext, tag)
except (ValueError, KeyError):
print(
"error: decryption failed — wrong passphrase or corrupted data",
file=sys.stderr,
)
sys.exit(1)
# text convenience wrappers
def encrypt_text(plaintext: str, passphrase: str) -> str:
"""encode text to utf-8, encrypt, return glyph string"""
return encrypt_bytes(plaintext.encode("utf-8"), passphrase)
def decrypt_text(encoded: str, passphrase: str) -> str:
"""decrypt and decode result as utf-8 text"""
raw = decrypt_bytes(encoded, passphrase)
try:
return raw.decode("utf-8")
except UnicodeDecodeError:
print(
"error: decrypted data is not valid utf-8 — "
"did you mean to use --file/-f to write to a binary output?",
file=sys.stderr,
)
sys.exit(1)
# keyfile helpers
def generate_keyfile() -> tuple[str, str]:
"""generate an 8192-char hex key, write it to a .ukey file, return (key, path)"""
key = secrets.token_hex(KEYFILE_LEN // 2) # token_hex(N) gives 2*N hex chars
epoch = int(time.time())
uid = uuid.uuid4()
filename = f"mojicrypt-key{epoch}_{uid}.ukey"
with open(filename, "w", encoding="utf-8") as fh:
fh.write(key)
# lock it down — readable only by the owner
os.chmod(filename, 0o600)
return key, filename
def load_keyfile(path: str) -> str:
"""read a .ukey file and return its contents as the passphrase"""
if not os.path.isfile(path):
print(f"error: keyfile not found: {path}", file=sys.stderr)
sys.exit(1)
with open(path, "r", encoding="utf-8") as fh:
key = fh.read().strip()
if not key:
print(f"error: keyfile is empty: {path}", file=sys.stderr)
sys.exit(1)
return key
# cli
def main():
parser = argparse.ArgumentParser(
prog=APP_NAME,
description="aes-256-gcm encryption with unicode-encoded output",
)
parser.add_argument(
"-v", "--version",
action="version",
version=f"{APP_NAME} {VERSION}",
)
parser.add_argument(
"mode",
choices=["encrypt", "decrypt"],
help="operation to perform",
)
parser.add_argument(
"text",
nargs="?",
help="text to encrypt/decrypt (reads stdin if omitted, ignored when -f is used)",
)
parser.add_argument(
"-p", "--passphrase",
help="passphrase (prompted securely if omitted)",
)
parser.add_argument(
"-f", "--file",
metavar="PATH",
help="input file to encrypt/decrypt",
)
parser.add_argument(
"-o", "--output",
metavar="PATH",
help=(
"output file path "
"(encrypt default: .uc | decrypt default: strips .uc or appends .dec)"
),
)
parser.add_argument(
"-k", "--keyfile",
metavar="PATH",
help="load passphrase from a .ukey file instead of -p",
)
args = parser.parse_args()
# resolve passphrase — priority: -p > -k > auto-generate (encrypt) / error (decrypt)
if args.passphrase:
passphrase = args.passphrase
elif args.keyfile:
passphrase = load_keyfile(args.keyfile)
print(f"using keyfile: {args.keyfile}", file=sys.stderr)
elif args.mode == "encrypt":
passphrase, kf_path = generate_keyfile()
print(f"no passphrase given - generated keyfile: {kf_path}", file=sys.stderr)
print("keep that file safe — you'll need it to decrypt", file=sys.stderr)
else:
print(
"error: no passphrase provided for decryption.\n"
" use -p or -k ",
file=sys.stderr,
)
sys.exit(1)
# file mode
if args.file:
in_path = args.file
if not os.path.isfile(in_path):
print(f"error: file not found: {in_path}", file=sys.stderr)
sys.exit(1)
if args.mode == "encrypt":
# default output: .uc
out_path = args.output or (in_path + ".uc")
with open(in_path, "rb") as fh:
data = fh.read()
glyphs = encrypt_bytes(data, passphrase)
with open(out_path, "w", encoding="utf-8") as fh:
fh.write(glyphs)
print(f"encrypted → {out_path}", file=sys.stderr)
else: # decrypt
# default output: strip .uc if present, else append .dec
if args.output:
out_path = args.output
elif in_path.endswith(".uc"):
out_path = in_path[:-3]
else:
out_path = in_path + ".dec"
with open(in_path, "r", encoding="utf-8") as fh:
encoded = fh.read().strip()
raw = decrypt_bytes(encoded, passphrase)
with open(out_path, "wb") as fh:
fh.write(raw)
print(f"decrypted → {out_path}", file=sys.stderr)
return
# ── text / stdin mode ──────────────────────────────────────
if args.text:
text = args.text
elif not sys.stdin.isatty():
text = sys.stdin.read()
else:
if args.mode == "encrypt":
print("enter plaintext (ctrl+d to finish):", file=sys.stderr)
else:
print("paste ciphertext (ctrl+d to finish):", file=sys.stderr)
text = sys.stdin.read()
if not text:
print("error: no input provided", file=sys.stderr)
sys.exit(1)
if args.mode == "encrypt":
result = encrypt_text(text, passphrase)
print(result)
else:
# strip whitespace from pasted ciphertext (none of our glyphs are whitespace)
result = decrypt_text(text.strip(), passphrase)
print(result)
if __name__ == "__main__":
main()