Complete Python 3 implementation of the five-stave Vernier cipher system. Run with --demo for an automatic walkthrough. Press F in the menu to generate a printable field card with encoding tables, codebook, mod-10 tables, and pre-keyed pad sheets with worksheets. No dependencies beyond the standard library.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Vernier Field Cipher System
kmsp42.com // rev 2.0
Interactive encoder/decoder implementing the five-stave hybrid OTP system.
TRNG from /dev/random mixed with timing jitter and user-sourced entropy.
Usage: python3 vernier.py
"""
import os
import sys
import time
import hashlib
import struct
import platform
import threading
from typing import List, Dict, Tuple, Optional
# ═══════════════════════════════════════════════════════════════════════════
# TERMINAL / COLOR
# ═══════════════════════════════════════════════════════════════════════════
USE_COLOR = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
# Enable ANSI on Windows if possible
if platform.system() == 'Windows':
try:
import ctypes
kernel = ctypes.windll.kernel32
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
USE_COLOR = True
except Exception:
USE_COLOR = False
def _c(code: str, text: str) -> str:
return f'\033[{code}m{text}\033[0m' if USE_COLOR else text
def gold(t): return _c('38;5;179', t)
def dim(t): return _c('2', t)
def bright(t): return _c('1', t)
def orange(t): return _c('38;5;208', t)
def blue(t): return _c('38;5;75', t)
def green(t): return _c('38;5;77', t)
def red(t): return _c('38;5;196', t)
def purple(t): return _c('38;5;141', t)
def gray(t): return _c('38;5;240', t)
def cyan(t): return _c('38;5;87', t)
# ═══════════════════════════════════════════════════════════════════════════
# Vernier ENCODING TABLES
# ═══════════════════════════════════════════════════════════════════════════
# Primary tier: 8 most frequent English letters -- single digit codes 0-7
PRIMARY: Dict[str, int] = {
'E': 0, 'T': 1, 'A': 2, 'O': 3,
'I': 4, 'N': 5, 'S': 6, 'H': 7,
}
# Secondary tier: next 10 letters -- two digit codes 80-89 (prefix 8)
SECONDARY: Dict[str, int] = {
'R': 80, 'D': 81, 'L': 82, 'C': 83, 'U': 84,
'M': 85, 'W': 86, 'F': 87, 'G': 88, 'Y': 89,
}
# Tertiary tier: remaining rare letters + period -- two digit codes 90-98 (prefix 9, second 0-8)
TERTIARY: Dict[str, int] = {
'P': 90, 'B': 91, 'V': 92, 'K': 93, 'J': 94,
'X': 95, 'Q': 96, 'Z': 97, '.': 98,
}
# Numerals: digit d -> [9, 9, d] (prefix 99, third digit is numeral)
# Spaces are stripped before encoding -- no code needed
# Reverse lookup tables
_REV_PRIMARY = {v: k for k, v in PRIMARY.items()}
_REV_SECONDARY = {v: k for k, v in SECONDARY.items()}
_REV_TERTIARY = {v: k for k, v in TERTIARY.items()}
# All encodable chars
ALL_ENCODABLE = set(PRIMARY) | set(SECONDARY) | set(TERTIARY) | set('0123456789. ')
# ZM sequence in digits (end-of-message marker)
# Z=97=[9,7], M=85=[8,5] --> [9,7,8,5,3,1]
ZM_DIGITS = [9, 7, 8, 5, 3, 1] # 6-digit marker (1-in-1M false match)
# ═══════════════════════════════════════════════════════════════════════════
# CODEBOOK
# ═══════════════════════════════════════════════════════════════════════════
# Full codebook (all 26 codes — backward compatible)
CODEBOOK: Dict[str, str] = {
'ZA': 'CONFIRMED',
'ZB': 'ABORT',
'ZC': 'PROCEED',
'ZD': 'DANGER',
'ZE': 'DELAY',
'ZF': 'COMPLETE',
'ZG': 'CONFIRM LAST',
'ZH': 'EMERGENCY',
'ZI': 'NOTHING TO REPORT',
'ZJ': 'AS PLANNED',
'ZK': 'PRIMARY LOCATION',
'ZL': 'ALTERNATE LOCATION',
'ZM': 'END OF MESSAGE', # RESERVED -- never reassign
'ZN': 'MEET',
'ZO': 'TOMORROW',
'ZP': 'TODAY',
'ZQ': 'LOCATION FOLLOWS',
'ZR': 'TIME FOLLOWS',
'ZS': 'SWITCH TO ALTERNATE',
'ZT': 'DEAD DROP',
'ZU': 'SURVEILLANCE DETECTED',
'ZV': 'CLEAR',
'ZW': 'RECEIVED AND UNDERSTOOD',
'ZX': 'DOCUMENT FOLLOWS',
'ZY': 'COORDINATES FOLLOW',
'ZZ': 'NAME FOLLOWS',
}
# ── FIELD ESSENTIAL: 10 codes to memorize. The rest are on the card. ──
FIELD_ESSENTIAL = {
'ZA': 'CONFIRMED',
'ZB': 'ABORT',
'ZD': 'DANGER',
'ZH': 'EMERGENCY',
'ZI': 'NOTHING TO REPORT',
'ZM': 'END OF MESSAGE',
'ZN': 'MEET',
'ZO': 'TOMORROW',
'ZP': 'TODAY',
'ZW': 'RECEIVED AND UNDERSTOOD',
}
# Reverse: phrase -> code (for suggestion matching)
_CB_REVERSE: Dict[str, str] = {v: k for k, v in CODEBOOK.items() if k != 'ZM'}
# Routine circuit codes -- most useful to memorize
ROUTINE_CODES = {'ZI', 'ZW', 'ZA'}
# ═══════════════════════════════════════════════════════════════════════════
# KEYWORD DIGIT TABLE — print on pad sheet, one glance per letter
# ═══════════════════════════════════════════════════════════════════════════
KEYWORD_TABLE: Dict[str, int] = {chr(i): (i - ord('A') + 1) % 10 for i in range(ord('A'), ord('Z') + 1)}
# A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8 I=9 J=0 K=1 L=2 M=3 N=4 O=5 P=6 Q=7 R=8 S=9 T=0 U=1 V=2 W=3 X=4 Y=5 Z=6
# ═══════════════════════════════════════════════════════════════════════════
# BLOCK CLASSES
# ═══════════════════════════════════════════════════════════════════════════
BLOCK_CLASSES: Dict[str, Dict] = {
'S': {'prefix': [0, 0], 'size': 50, 'groups': 10, 'max_content': 44},
'M': {'prefix': [0, 1], 'size': 100, 'groups': 20, 'max_content': 94},
'L': {'prefix': [0, 2], 'size': 200, 'groups': 40, 'max_content': 194},
}
def select_block_class(content_digit_count: int) -> str:
"""Select block class. Default to M (100 digits) — one fewer decision under stress.
Only use S for very short messages, L only when M overflows."""
if content_digit_count <= 94:
return 'M' # Default: 100 digits. Simpler than choosing.
elif content_digit_count <= 194:
return 'L'
return 'SPLIT'
# ═══════════════════════════════════════════════════════════════════════════
# TRNG -- TRUE RANDOM NUMBER GENERATION
# ═══════════════════════════════════════════════════════════════════════════
class EntropyPool:
"""
Collects entropy from multiple sources.
Sources (combined via SHA-256):
/dev/random -- Linux hardware entropy pool (blocks until sufficient)
/dev/urandom -- macOS CSPRNG (cryptographically equivalent on modern kernels)
timing jitter -- nanosecond deltas between user events
os.urandom -- OS CSPRNG as mixing baseline
"""
def __init__(self):
self._pool: bytearray = bytearray()
self._lock = threading.Lock()
self._last_ns: int = time.time_ns()
self.events: int = 0
self.dev_bytes: int = 0
def add_bytes(self, data: bytes) -> None:
with self._lock:
self._pool.extend(data)
def add_timing(self) -> None:
"""Record a timing event. Each call adds 8 bytes of jitter entropy."""
now = time.time_ns()
with self._lock:
delta = now - self._last_ns
# Pack nanosecond delta + current timestamp for maximum jitter capture
self._pool.extend(struct.pack('<QQ', delta, now))
self._last_ns = now
self.events += 1
def size(self) -> int:
with self._lock:
return len(self._pool)
def derive(self, n_bytes: int) -> bytes:
"""
Derive n_bytes from the pool using a SHA-256 chain.
Output is deterministically derived from the pool contents.
Pool must contain genuine entropy for output to be secure.
"""
with self._lock:
seed = bytes(self._pool)
# Final mix: hash the pool with os.urandom for defense-in-depth
seed = hashlib.sha256(seed + os.urandom(32)).digest()
result = b''
ctr = 0
while len(result) < n_bytes:
block = hashlib.sha256(seed + struct.pack('<I', ctr)).digest()
result += block
ctr += 1
return result[:n_bytes]
def _dev_random_reader(pool: EntropyPool, target: int, stop: threading.Event) -> None:
"""Background thread: drain /dev/random into the entropy pool."""
try:
# /dev/random on Linux blocks until the kernel entropy pool has enough bits.
# On macOS, /dev/urandom is the correct choice (equivalent quality).
# On Windows, fall through to os.urandom below.
if platform.system() == 'Linux':
dev_path = '/dev/random'
elif platform.system() == 'Darwin':
dev_path = '/dev/urandom'
else:
raise OSError('No device path on this platform')
collected = 0
with open(dev_path, 'rb') as f:
while collected < target and not stop.is_set():
want = min(32, target - collected)
chunk = f.read(want)
if chunk:
pool.add_bytes(chunk)
pool.dev_bytes += len(chunk)
collected += len(chunk)
except OSError:
pass
finally:
# Always supplement with os.urandom regardless of device availability
pool.add_bytes(os.urandom(target))
stop.set()
def bytes_to_uniform_digits(raw: bytes) -> List[int]:
"""
Convert raw bytes to uniformly distributed digits 0-9.
Uses rejection sampling: accept byte b only if b < 250.
250 = 25 x 10, so b % 10 is exactly uniform for b in 0-249.
Rejection rate: 6/256 = ~2.3%.
"""
return [b % 10 for b in raw if b < 250]
def collect_entropy(n_digits: int, silent: bool = False) -> List[int]:
"""
Collect n_digits of cryptographically random digits (0-9).
Blocks until sufficient entropy is gathered from all sources.
Interactive: prompts user to type/move mouse for timing entropy.
"""
pool = EntropyPool()
stop_event = threading.Event()
# We need ~1.025x bytes to account for rejection sampling
target_bytes = int(n_digits * 1.05) + 96
# Start /dev/random reader in background
reader = threading.Thread(
target=_dev_random_reader,
args=(pool, target_bytes, stop_event),
daemon=True
)
reader.start()
if not silent:
_rule()
print(f'\n {gold("// ENTROPY COLLECTION")}')
_rule()
print(f'\n Generating {orange(str(n_digits))} random digits.')
print(f'\n {dim("Sources:")}')
dev = '/dev/random' if platform.system() == 'Linux' else '/dev/urandom'
print(f' {dim(" ")} {green(dev):<22} {dim("-- hardware entropy pool")}')
print(f' {dim(" ")} {green("timing jitter"):<22} {dim("-- nanosecond keystroke intervals")}')
print(f' {dim(" ")} {green("os.urandom"):<22} {dim("-- CSPRNG baseline mixing")}')
print(f'\n {orange("While gathering:")} type random characters, vary speed and rhythm.')
print(f' {dim("Each keystroke timing adds entropy to the pool.")}')
print(f' {dim("Press")} {gold("ENTER")} {dim("when done (minimum 5 keystrokes recommended).")}')
print()
# Collect timing entropy interactively
_collect_keystroke_entropy(pool, silent)
# Wait for /dev/random reader (timeout 8s -- don't block forever)
reader.join(timeout=8.0)
# Final timing sample + os.urandom mixing
pool.add_timing()
pool.add_bytes(os.urandom(32))
if not silent:
print(f'\n {dim("Pool size:")} {gold(str(pool.size()))} bytes '
f'{dim("Events:")} {gold(str(pool.events))} '
f'{dim("Device bytes:")} {gold(str(pool.dev_bytes))}')
# Generate digits with overflow headroom
raw = pool.derive(target_bytes * 3)
digits = bytes_to_uniform_digits(raw)
# Should always have enough, but safety loop just in case
while len(digits) < n_digits:
extra = pool.derive(64)
digits.extend(bytes_to_uniform_digits(extra))
result = digits[:n_digits]
if not silent:
check = _uniformity_check(result)
print(f' {dim("Uniformity:")} {check}')
return result
def _collect_keystroke_entropy(pool: EntropyPool, silent: bool) -> None:
"""Read characters one at a time to capture keystroke timing."""
if not silent:
print(f' {gray(">")} ', end='', flush=True)
try:
import termios
import tty
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
while True:
ch = sys.stdin.read(1)
pool.add_timing()
pool.add_bytes(bytes([ord(ch) & 0xFF]))
if ch in ('\r', '\n'):
break
elif ch == '\x03': # Ctrl-C
termios.tcsetattr(fd, termios.TCSADRAIN, old)
raise KeyboardInterrupt
elif ch == '\x7f': # Backspace -- still good entropy
if not silent:
print(dim('<'), end='', flush=True)
else:
if not silent:
print(dim('*'), end='', flush=True)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
except (ImportError, Exception):
# Windows or non-tty fallback
try:
line = input()
pool.add_timing()
pool.add_bytes(line.encode())
except (EOFError, KeyboardInterrupt):
pass
if not silent:
print()
def _uniformity_check(digits: List[int]) -> str:
"""Simple chi-squared-style uniformity test on digit distribution."""
if not digits:
return red('NO DATA')
counts = [digits.count(i) for i in range(10)]
expected = len(digits) / 10.0
max_dev = max(abs(c - expected) / expected * 100 for c in counts)
if max_dev < 15.0:
return green(f'PASS (max deviation {max_dev:.1f}%)')
elif max_dev < 30.0:
return orange(f'MARGINAL (max deviation {max_dev:.1f}%)')
else:
return red(f'FAIL (max deviation {max_dev:.1f}% -- regenerate)')
# ═══════════════════════════════════════════════════════════════════════════
# Vernier ENCODE / DECODE
# ═══════════════════════════════════════════════════════════════════════════
def encode_char(ch: str) -> List[int]:
"""Encode a single character to a list of Vernier digits."""
ch = ch.upper()
if ch in PRIMARY:
return [PRIMARY[ch]]
if ch in SECONDARY:
v = SECONDARY[ch]
return [v // 10, v % 10] # e.g. R=80 -> [8, 0]
if ch in TERTIARY:
v = TERTIARY[ch]
return [v // 10, v % 10] # e.g. Z=97 -> [9, 7]
if ch.isdigit():
return [9, 9, int(ch)] # e.g. '6' -> [9, 9, 6]
return [] # space and unknowns stripped
def encode_string(text: str) -> List[int]:
"""Encode a full string to Vernier digit stream. Spaces are stripped."""
digits: List[int] = []
for ch in text.upper():
if ch != ' ':
digits.extend(encode_char(ch))
return digits
def decode_digits(digits: List[int]) -> Tuple[str, int]:
"""
Decode a Vernier digit stream to a string.
Returns (decoded_text, zm_position) where zm_position is the index
of the first ZM marker in the digit stream, or -1 if not found.
Decoding stops at ZM.
"""
result: List[str] = []
i = 0
zm_pos = -1
while i < len(digits):
# Detect ZM = [9,7,8,5,3,1]
if (i + 3 < len(digits) and
digits[i] == 9 and digits[i+1] == 7 and
digits[i+2] == 8 and digits[i+3] == 5):
zm_pos = i
break
d = digits[i]
if 0 <= d <= 7:
# Primary: single digit
result.append(_REV_PRIMARY.get(d, '?'))
i += 1
elif d == 8:
# Secondary: two digits
if i + 1 < len(digits):
code = 80 + digits[i + 1]
result.append(_REV_SECONDARY.get(code, '?'))
i += 2
else:
i += 1 # truncated -- skip
elif d == 9:
if i + 1 >= len(digits):
i += 1
continue
d2 = digits[i + 1]
if d2 == 9:
# Numeral: three digits [9, 9, x]
if i + 2 < len(digits):
result.append(str(digits[i + 2]))
i += 3
else:
i += 2
else:
# Tertiary: two digits [9, x] where x in 0-8
code = 90 + d2
result.append(_REV_TERTIARY.get(code, '?'))
i += 2
else:
i += 1 # should not occur (digits are 0-9)
return ''.join(result), zm_pos
def expand_codebook(text: str) -> str:
"""Expand Z-code pairs in decoded text to their full phrases."""
out: List[str] = []
i = 0
while i < len(text):
if text[i] == 'Z' and i + 1 < len(text):
code = 'Z' + text[i + 1]
if code in CODEBOOK and code != 'ZM':
out.append(f'[{CODEBOOK[code]}]')
i += 2
continue
out.append(text[i])
i += 1
return ''.join(out)
# ═══════════════════════════════════════════════════════════════════════════
# CODEBOOK INTERACTIVE APPLICATION
# ═══════════════════════════════════════════════════════════════════════════
def apply_codebook_interactive(text: str) -> str:
"""
Suggest codebook substitutions and apply with user confirmation.
Returns modified text with Z-codes substituted where accepted.
"""
print(f'\n {gold("// CODEBOOK APPLICATION")}')
_rule()
print(f' Original: {bright(text)}\n')
working = text.upper()
# Sort by phrase length descending so longer matches take priority
candidates = sorted(
[(phrase, code) for code, phrase in CODEBOOK.items() if code != 'ZM'],
key=lambda x: len(x[0]),
reverse=True
)
applied: List[str] = []
skipped: List[str] = []
for phrase, code in candidates:
if phrase in working:
tag = cyan('(routine)') if code in ROUTINE_CODES else ''
digits = encode_string('Z' + code[1]) # show encoded cost
cost = f'{dim(str(len(digits)))} digits'
print(f' {gold("Match:")} {orange(phrase):<32} {green(code)} {tag}')
print(f' {dim("Encoded as:")} {dim(str(digits))} ({cost})')
ans = input(f' {dim("Apply? [Y/n]")} ').strip().lower()
if ans != 'n':
working = working.replace(phrase, code, 1)
applied.append(f'{code}={phrase}')
print(f' {green("Applied.")}')
else:
skipped.append(phrase)
print()
# Clean up spacing
working = ' '.join(working.split())
if applied:
print(f' {green("Applied:")} {", ".join(applied)}')
else:
print(f' {dim("No substitutions applied.")}')
if skipped:
print(f' {dim("Skipped:")} {", ".join(skipped)}')
print(f' {dim("Working text:")} {bright(working)}')
return working
# ═══════════════════════════════════════════════════════════════════════════
# MOD-10 ARITHMETIC
# ═══════════════════════════════════════════════════════════════════════════
def add_mod10(p: int, k: int) -> int:
return (p + k) % 10
def sub_mod10(c: int, k: int) -> int:
return (c - k + 10) % 10
def encrypt_stream(plain: List[int], key: List[int]) -> List[int]:
"""Apply (P+K) mod 10 across parallel digit streams."""
return [add_mod10(p, k) for p, k in zip(plain, key)]
def decrypt_stream(cipher: List[int], key: List[int]) -> List[int]:
"""Apply (C-K+10) mod 10 across parallel digit streams."""
return [sub_mod10(c, k) for c, k in zip(cipher, key)]
# ═══════════════════════════════════════════════════════════════════════════
# KEYWORD LAYER (Stave 5)
# ═══════════════════════════════════════════════════════════════════════════
def keyword_to_stream(keyword: str, length: int) -> List[int]:
"""
Convert keyword to a cycling digit stream.
Conversion: (alphabet position) mod 10
A=1, B=2, ... J=0(10%10), K=1, ... Z=6(26%10)
"""
stream: List[int] = []
for ch in keyword.upper():
if ch.isalpha():
pos = ord(ch) - ord('A') + 1 # A=1 ... Z=26
stream.append(pos % 10)
if not stream:
return [0] * length
result: List[int] = []
while len(result) < length:
result.extend(stream)
return result[:length]
def keyword_stream_preview(keyword: str) -> str:
"""Show the digit stream a keyword produces (first 20 digits, cycling)."""
stream = keyword_to_stream(keyword, 20)
return ' '.join(str(d) for d in stream) + ' ...'
# ═══════════════════════════════════════════════════════════════════════════
# DISPLAY HELPERS
# ═══════════════════════════════════════════════════════════════════════════
def _clear():
os.system('clear' if platform.system() != 'Windows' else 'cls')
def _rule(width: int = 48):
print(dim(' ' + '\u2500' * width))
def _header(subtitle: str = ''):
print()
print(gold(' \u2554' + '\u2550' * 46 + '\u2557'))
print(gold(' \u2551') + bright(' Vernier FIELD CIPHER SYSTEM') + ' ' * 16 + gold('\u2551'))
print(gold(' \u2551') + dim(' kmsp42.com // rev 2.0') + ' ' * 24 + gold('\u2551'))
if subtitle:
pad = 46 - len(subtitle) - 2
print(gold(' \u2551') + orange(f' {subtitle}') + ' ' * max(0, pad) + gold('\u2551'))
print(gold(' \u255a' + '\u2550' * 46 + '\u255d'))
print()
def _groups(digits: List[int], group_sz: int = 5, line_width: int = 10) -> str:
"""Format digit list into grouped lines (line_width groups per line)."""
flat = ''.join(str(d) for d in digits)
groups = [flat[i:i+group_sz] for i in range(0, len(flat), group_sz)]
lines = []
for i in range(0, len(groups), line_width):
lines.append(' '.join(groups[i:i+line_width]))
return '\n '.join(lines)
def _print_row(label: str, digits: List[int], color_fn=None, max_digits: int = 0):
"""Print a labeled row of digit groups."""
cf = color_fn or (lambda x: x)
show = digits[:max_digits] if max_digits else digits
continuation = dim(' ...') if max_digits and len(digits) > max_digits else ''
print(f' {gold(label + ":")}')
print(f' {cf(_groups(show))}' + continuation)
def _wait(msg: str = 'Press ENTER to continue...'):
input(f'\n {dim(msg)}')
# ═══════════════════════════════════════════════════════════════════════════
# REFERENCE SCREENS
# ═══════════════════════════════════════════════════════════════════════════
def show_encoding_table():
_clear()
_header('RADIX ENCODING TABLE')
print(f' {gold("PRIMARY")} {dim("(0-7, single digit)")}')
print(f' {dim("Mnemonic: Eat Tangerines And Oranges In Nice Spanish Hotels")}')
print()
row = ' '
for ch, d in PRIMARY.items():
row += f'{bright(ch)}{dim("=")}{green(str(d))} '
print(row)
print(f'\n {gold("SECONDARY")} {dim("(prefix 8, codes 80-89)")}')
print(f' {dim("Mnemonic: Really Dull Lessons Can Undermine Many Willing Fields, Gaining Yesterday")}')
print()
row = ' '
for ch, d in SECONDARY.items():
row += f'{bright(ch)}{dim("=")}{blue(str(d))} '
print(row)
print(f'\n {gold("TERTIARY")} {dim("(prefix 9, codes 90-98)")}')
print(f' {dim("Mnemonic: Please Be Very Kind, Just eXercise Quietly, Zealously.")}')
print()
row = ' '
for ch, d in TERTIARY.items():
row += f'{bright(ch)}{dim("=")}{orange(str(d))} '
print(row)
print(f'\n {gold("NUMERALS")} {dim("(prefix 99, codes 990-999)")}')
print(f' {dim("Rule: 990 + digit")}')
print()
row = ' '
for i in range(10):
row += f'{bright(str(i))}{dim("=")}{purple(str(990+i))} '
print(row)
print(f'\n {gold("ZM END MARKER")} {dim("= [9,7,8,5,3,1]")} {dim("(Z=97, M=85)")}')
_rule()
print(f'\n {gold("PARSING RULE:")}')
print(f' {dim("d in 0-7")} {dim("-->")} complete. {green("One digit.")}')
print(f' {dim("d = 8")} {dim("-->")} read one more. {blue("8x secondary table.")}')
print(f' {dim("d = 9, x in 0-8")} {dim("-->")} two digits total. {orange("9x tertiary table.")}')
print(f' {dim("d = 9, x = 9")} {dim("-->")} read one more. {purple("Numeral = that digit.")}')
print()
print(f' {cyan("No lookahead required at any position. Each decision is final.")}')
_wait()
def show_codebook():
_clear()
_header('CODEBOOK // Z-PREFIX SYSTEM')
print(f' {dim("Z encodes as [9,7]. Any Z+letter pair is a codebook entry.")}')
print(f' {dim("ZM is reserved as end-of-message. Never reassign.")}')
print(f' {gold("★")} {dim("= FIELD ESSENTIAL (memorize these 10)")}')
_rule()
print()
items = [(k, v) for k, v in CODEBOOK.items()]
for i in range(0, len(items), 2):
lk, lv = items[i]
tag_l = gold(' ★') if lk in FIELD_ESSENTIAL else ''
ls = f' {gold(lk)} {dim(lv):<34}{tag_l}'
if i + 1 < len(items):
rk, rv = items[i + 1]
tag_r = gold(' ★') if rk in FIELD_ESSENTIAL else ''
rs = f'{gold(rk)} {dim(rv)}{tag_r}'
print(f'{ls:<52} {rs}')
else:
print(ls)
_rule()
print(f'\n {gold("Routine circuit")} {dim("(all fit in Short block -- 50 digits):")}')
print(f' {green("ZI")} {dim("ZM")} = nothing to report')
print(f' {green("ZW")} {dim("ZM")} = received and understood')
print(f' {green("ZG")} {dim("ZM")} = confirm last message received correctly')
print()
print(f' {dim("Encoding cost per Z-code entry:")}')
for code in ['ZA', 'ZD', 'ZN', 'ZU', 'ZI', 'ZH']:
digs = encode_string('Z' + code[1])
meaning = CODEBOOK[code]
print(f' {gold(code)} {dim("=>")} {dim(str(digs))} {dim(f"({len(digs)} digits) [{meaning}]")}')
_wait()
def show_block_classes():
_clear()
_header('BLOCK CLASSES')
print(f' {"Class":<8} {"Prefix":<8} {"Block":<8} {"Max Content":<14} {"Groups":<8} Dice Rolls')
_rule()
print(f' {green("S SHORT"):<8} {dim("00"):<8} {dim("50"):<8} {dim("44 digits"):<14} {dim("10"):<8} {dim("50")}')
print(f' {gold("M MEDIUM"):<8} {dim("01"):<8} {dim("100"):<8} {dim("94 digits"):<14} {dim("20"):<8} {dim("100")}')
print(f' {orange("L LONG"):<8} {dim("02"):<8} {dim("200"):<8} {dim("194 digits"):<14} {dim("40"):<8} {dim("200")}')
_rule()
print(f'\n {gold("Structure:")}')
print(f' {dim("[OFFSET 2][SALT 4][RANDOM FILL][CONTENT][ZM 6][PADDING] = block size (25, 50, or 100)")}')
print(f'\n {gold("Decision rule:")}')
print(f' {dim("Encode message fully. Count digits. Select class. Then roll dice.")}')
print(f' {dim("Key block size matches class (50 / 100 / 200 rolls).")}')
print(f'\n {gold("Typical messages:")}')
print(f' {green("ZI ZM")} = 8 digits --> {green("Short")}')
print(f' {green("ZH ZB ZM")} = 11 digits --> {green("Short")}')
print(f' {gold("ZN ZP ZK ZA ZM")} = 16 digits --> {gold("Short")}')
print(f' {gold("Mixed report ~60 digits")} --> {gold("Medium")}')
print(f' {orange("Coordinates + instructions ~150 d")} --> {orange("Long")}')
print(f'\n {dim("Over 194 digits: split across two pad sheets.")}')
_wait()
def show_mod_table():
_clear()
_header('MOD-10 CIPHER TABLE')
print(f' {dim("Row = plaintext digit (P). Column = key digit (K). Cell = cipher digit (C).")}')
print(f' {dim("Decrypt: find cipher value in key row, column header = plaintext.")}')
print()
# Header row
hdr = f' {dim("P\\K"):>5}'
for j in range(10):
hdr += f' {blue(str(j)):>4}'
print(hdr)
_rule(52)
for i in range(10):
row_str = f' {gold(str(i)):>5}'
for j in range(10):
val = (i + j) % 10
cell = green(str(val)) if i == j else dim(str(val))
row_str += f' {cell:>4}'
print(row_str)
_rule(52)
print(f'\n {gold("ENCRYPT:")} C = (P + K) mod 10')
print(f' {gold("DECRYPT:")} P = (C - K + 10) mod 10')
print(f'\n {dim("Shaded diagonal (green): P = K. Cipher equals plaintext. Key digit is zero.")}')
print(f' {dim("Zero is equally likely from a random source -- not a security flaw.")}')
_wait()
# ═══════════════════════════════════════════════════════════════════════════
# ENCRYPTION WORKFLOW
# ═══════════════════════════════════════════════════════════════════════════
def encrypt_workflow():
_clear()
_header('ENCRYPT MESSAGE')
# ── Step 1: Plaintext input ──────────────────────────────────────────
print(f' {orange("STEP 1")} {dim("/")} Input plaintext')
_rule()
print(f' {dim("Type or paste your message. Press ENTER when done.")}')
print(f' {dim("Use plain English. Codebook suggestions follow.")}')
print()
plaintext = input(f' {gold("> ")}').strip()
if not plaintext:
print(red('\n No input. Cancelling.'))
time.sleep(1)
return
# ── Step 2: Codebook ─────────────────────────────────────────────────
print(f'\n {orange("STEP 2")} {dim("/")} Apply codebook')
_rule()
coded = apply_codebook_interactive(plaintext)
# ── Step 3: RADIX encode ─────────────────────────────────────────────
print(f'\n {orange("STEP 3")} {dim("/")} Vernier encode')
_rule()
# Show per-character encoding
stripped = coded.replace(' ', '')
print(f' {dim("Encoding:")} {bright(stripped)}\n')
encoded: List[int] = []
char_table: List[Tuple[str, List[int]]] = []
for ch in stripped.upper():
d = encode_char(ch) if ch != ' ' else []
encoded.extend(d)
char_table.append((ch, d))
# Print encoding table (compact)
line = ' '
for ch, d in char_table:
entry = f'{bright(ch)}{dim("->")}{cyan("".join(str(x) for x in d))} '
if len(line) + len(entry) > 72:
print(line)
line = ' '
line += entry
if line.strip():
print(line)
# Append ZM — ALWAYS
content = encoded + ZM_DIGITS
print(f'\n {gold(">>> APPEND ZM END-OF-MESSAGE: 9 7 8 5 <<<")}')
print(f' {dim("Content + ZM:")}')
_print_row('ENCODED', content, blue)
print(f' {dim("Content digit count:")} {gold(str(len(content)))}')
# ── Step 4: Block class selection ───────────────────────────────────
print(f'\n {orange("STEP 4")} {dim("/")} Select block class')
_rule()
cls = select_block_class(len(content))
if cls == 'SPLIT':
print(f' {red("Message too long!")} {len(content)} digits exceeds 194 digit maximum.')
print(f' {dim("Split into two messages on separate pad sheets.")}')
_wait()
return
bc = BLOCK_CLASSES[cls]
bsize = bc['size']
prefix = bc['prefix']
null_n = bsize - 2 - len(content)
cls_color = {
'S': green, 'M': gold, 'L': orange
}.get(cls, gold)
print(f' Content: {gold(str(len(content)))} digits --> class {cls_color(cls)}')
print(f' Block size: {dim(str(bsize))} digits | Prefix: {dim(str(prefix))} '
f'| Groups: {dim(str(bc["groups"]))} | Null fill: {dim(str(null_n))}')
# ── Step 5: Optional keyword layer ──────────────────────────────────
print(f'\n {orange("STEP 5")} {dim("/")} Keyword layer (optional)')
_rule()
print(f' {dim("Apply a memorized keyword for defense against pad physical compromise.")}')
print(f' {dim("Leave blank to skip Stave 5.")}')
print(f'\n {dim("KEYWORD DIGIT TABLE (one glance per letter):")}')
print(f' {blue("A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8 I=9 J=0")}')
print(f' {blue("K=1 L=2 M=3 N=4 O=5 P=6 Q=7 R=8 S=9 T=0")}')
print(f' {blue("U=1 V=2 W=3 X=4 Y=5 Z=6")}')
print()
kw_raw = input(f' {gold("Keyword (ENTER to skip): ")}').strip()
use_kw = bool(kw_raw)
if use_kw:
preview = keyword_stream_preview(kw_raw)
print(f' {dim("Keyword digit stream:")} {blue(preview)}')
print(f' {green("Stave 5 active.")}')
else:
print(f' {dim("Stave 5 skipped.")}')
# ── Step 6: Key generation ───────────────────────────────────────────
print(f'\n {orange("STEP 6")} {dim("/")} Generate key material ({bsize} digits)')
_rule()
key_digits = collect_entropy(bsize)
_print_row('KEY', key_digits, blue)
# ── Step 7: Build full plaintext stream and encrypt ──────────────────
print(f'\n {orange("STEP 7")} {dim("/")} Encrypt')
_rule()
# Full plaintext = prefix + encoded_content (ZM included)
plain_active = prefix + content
# Apply keyword layer before OTP if requested
if use_kw:
kw_stream = keyword_to_stream(kw_raw, len(plain_active))
plain_kw = [add_mod10(p, k) for p, k in zip(plain_active, kw_stream)]
print(f' {dim("After keyword layer:")}')
_print_row('P+KW', plain_kw[:30], purple, max_digits=30)
else:
plain_kw = plain_active
# OTP encryption
key_active = key_digits[:len(plain_kw)]
cipher_active = encrypt_stream(plain_kw, key_active)
# Null fill: plaintext = 0, so cipher = key digit
null_key = key_digits[len(plain_kw):]
cipher_null = null_key[:] # (0 + k) mod 10 = k
cipher_full = cipher_active + cipher_null
# Display
_print_row('PLAIN', plain_active[:20], dim, max_digits=20)
_print_row('KEY', key_digits[:20], blue, max_digits=20)
_print_row('CIPHER', cipher_full, green)
verify = cipher_full[:5]
# ── Step 8: Output ───────────────────────────────────────────────────
print(f'\n {orange("STEP 8")} {dim("/")} Transmit')
_rule()
print(f'\n {gold("CIPHERTEXT:")}')
print(f' {green(_groups(cipher_full))}')
print(f'\n {gold("VERIFY STRIP")} {dim("(first 5 cipher digits):")} '
f'{orange(_groups(verify))}')
print(f'\n {dim("Class:")} {cls_color(cls)} '
f'{dim("Digits:")} {gold(str(len(cipher_full)))} '
f'{dim("Groups:")} {gold(str(bc["groups"]))}')
# ── Step 9: Save option ──────────────────────────────────────────────
_rule()
print(f'\n {gold("Save key block to file?")}')
print(f' {red("Key file is sensitive. Share only via physical exchange.")}')
print(f' {dim("The receiver needs this to decrypt.")}')
print()
save = input(f' {gold("Save? [y/N] ")}').strip().lower()
if save == 'y':
fname = input(f' {gold("Filename [vernier_pad.txt]: ")}').strip() or 'vernier_pad.txt'
try:
with open(fname, 'w') as f:
f.write('Vernier PAD SHEET\n')
f.write('kmsp42.com // rev 2.0\n')
f.write('=' * 48 + '\n')
f.write(f'BLOCK CLASS: {cls}\n')
f.write(f'BLOCK SIZE: {bsize}\n')
f.write(f'GROUPS: {bc["groups"]}\n')
f.write(f'VERIFY STRIP: {"".join(str(d) for d in verify)}\n')
f.write(f'KEYWORD: {"YES (memorized only -- not stored)" if use_kw else "NO"}\n')
f.write('KEY DIGITS:\n')
f.write(_groups(key_digits) + '\n')
f.write('=' * 48 + '\n')
f.write('DESTROY THIS FILE AFTER COUNTERPART HAS THE KEY.\n')
print(f' {green("Saved:")} {gold(fname)}')
print(f' {red("Destroy file once key is physically exchanged.")}')
except OSError as e:
print(f' {red("Save failed:")} {e}')
_wait()
# ═══════════════════════════════════════════════════════════════════════════
# DECRYPTION WORKFLOW
# ═══════════════════════════════════════════════════════════════════════════
def decrypt_workflow():
_clear()
_header('DECRYPT MESSAGE')
# ── Step 1: Ciphertext ───────────────────────────────────────────────
print(f' {orange("STEP 1")} {dim("/")} Enter ciphertext')
_rule()
print(f' {dim("Paste received digit groups. Spaces/newlines are ignored.")}')
print()
raw = input(f' {gold("Ciphertext: ")}').strip()
cipher_digits = [int(c) for c in raw if c.isdigit()]
if not cipher_digits:
print(red('\n No digits found. Cancelling.'))
time.sleep(1)
return
# Detect block class
detected_cls = None
for cls, bc in BLOCK_CLASSES.items():
if len(cipher_digits) == bc['size']:
detected_cls = cls
break
if detected_cls:
cls_color = {'S': green, 'M': gold, 'L': orange}[detected_cls]
print(f' {dim("Block class detected:")} {cls_color(detected_cls)} '
f'{dim(f"({len(cipher_digits)} digits)")}')
else:
print(f' {orange("Non-standard block size:")} {len(cipher_digits)} digits')
_print_row('CIPHER', cipher_digits, green)
# ── Step 2: Key digits ───────────────────────────────────────────────
print(f'\n {orange("STEP 2")} {dim("/")} Enter key digits')
_rule()
load = input(f' {gold("Load from file? [y/N] ")}').strip().lower()
key_digits: List[int] = []
if load == 'y':
fname = input(f' {gold("Filename: ")}').strip()
try:
in_key = False
raw_key = ''
with open(fname, 'r') as f:
for line in f:
if line.startswith('KEY DIGITS:'):
in_key = True
continue
if in_key:
raw_key += line
if line.startswith('='):
break
key_digits = [int(c) for c in raw_key if c.isdigit()]
print(f' {green("Loaded")} {gold(str(len(key_digits)))} key digits from {gold(fname)}')
except OSError as e:
print(f' {red("Load failed:")} {e}')
if not key_digits:
print(f' {dim("Enter key digits (spaces are ignored):")}')
raw_key = input(f' {gold("Key: ")}').strip()
key_digits = [int(c) for c in raw_key if c.isdigit()]
if not key_digits:
print(red('\n No key digits. Cancelling.'))
time.sleep(1)
return
if len(key_digits) < len(cipher_digits):
print(f' {orange("Warning:")} key ({len(key_digits)}) shorter than ciphertext ({len(cipher_digits)})')
key_digits += [0] * (len(cipher_digits) - len(key_digits))
_print_row('KEY', key_digits[:len(cipher_digits)], blue)
# ── Step 3: Keyword layer ────────────────────────────────────────────
print(f'\n {orange("STEP 3")} {dim("/")} Keyword layer')
_rule()
print(f' {dim("Enter keyword if Stave 5 was active during encryption.")}')
kw_raw = input(f' {gold("Keyword (ENTER to skip): ")}').strip()
use_kw = bool(kw_raw)
# ── Step 4: Decrypt ──────────────────────────────────────────────────
print(f'\n {orange("STEP 4")} {dim("/")} Decrypt')
_rule()
key_slice = key_digits[:len(cipher_digits)]
plain_raw = decrypt_stream(cipher_digits, key_slice)
# Remove keyword layer if active
if use_kw:
kw_stream = keyword_to_stream(kw_raw, len(plain_raw))
plain_raw = [sub_mod10(p, k) for p, k in zip(plain_raw, kw_stream)]
print(f' {dim("Keyword layer removed.")}')
_print_row('PLAIN RAW', plain_raw[:40], dim, max_digits=40)
# Read block class prefix (first two digits)
if len(plain_raw) >= 2:
p_val = plain_raw[0] * 10 + plain_raw[1]
prefix_map = {0: 'S', 1: 'M', 2: 'L'}
if p_val in prefix_map:
pfx_cls = prefix_map[p_val]
pfx_color = {'S': green, 'M': gold, 'L': orange}[pfx_cls]
print(f' {dim("Prefix:")} {dim(str(plain_raw[:2]))} --> class {pfx_color(pfx_cls)}')
content_digits = plain_raw[2:]
else:
print(f' {orange("Unexpected prefix digits:")} {plain_raw[:2]}')
content_digits = plain_raw
else:
content_digits = plain_raw
# ── Step 5: RADIX decode ─────────────────────────────────────────────
print(f'\n {orange("STEP 5")} {dim("/")} RADIX decode')
_rule()
decoded, zm_pos = decode_digits(content_digits)
if zm_pos >= 0:
print(f' {dim("ZM marker found at content position")} {gold(str(zm_pos))} '
f'{dim("-- truncating null fill.")}')
else:
print(f' {orange("ZM marker not found.")} '
f'{dim("Wrong key, wrong block class, or transmission error.")}')
expanded = expand_codebook(decoded)
print()
print(f' {gold("DECODED RAW:")}')
print(f' {blue(decoded)}')
print()
print(f' {gold("PLAINTEXT:")}')
print(f' {bright(expanded)}')
# Show codebook expansions detail
if any(f'Z{c}' in decoded for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'):
_rule()
print(f'\n {gold("Codebook expansions:")}')
i = 0
while i < len(decoded):
if decoded[i] == 'Z' and i + 1 < len(decoded):
code = 'Z' + decoded[i+1]
if code in CODEBOOK:
print(f' {green(code)} --> {CODEBOOK[code]}')
i += 2
else:
i += 1
_wait()
# ═══════════════════════════════════════════════════════════════════════════
# QUICK ENCODE / DECODE UTILITIES
# ═══════════════════════════════════════════════════════════════════════════
def quick_encode():
"""Encode a string to RADIX digits without encryption."""
_clear()
_header('QUICK ENCODE -- No Encryption')
print(f' {dim("Encodes text to Vernier digits only. For practice and testing.")}')
_rule()
text = input(f'\n {gold("Text: ")}').strip()
if not text:
return
enc = encode_string(text)
print(f'\n {gold("Input:")}'
f' {bright(text.upper())}')
print(f'\n {gold("RADIX digits:")}')
_print_row('OUT', enc, green)
print(f'\n {dim("Count:")} {gold(str(len(enc)))} digits')
# Also show block class this would require
cls = select_block_class(len(enc) + 4) # +4 for ZM
print(f' {dim("Block class (with ZM):")} {gold(cls)}')
_wait()
def quick_decode():
"""Decode a RADIX digit string without decryption."""
_clear()
_header('QUICK DECODE -- No Decryption')
print(f' {dim("Decodes Vernier digit stream only. For practice and testing.")}')
_rule()
raw = input(f'\n {gold("Digits: ")}').strip()
digits = [int(c) for c in raw if c.isdigit()]
if not digits:
return
decoded, zm_pos = decode_digits(digits)
expanded = expand_codebook(decoded)
print(f'\n {gold("Decoded raw:")} {blue(decoded)}')
print(f'\n {gold("Plaintext:")} {bright(expanded)}')
if zm_pos >= 0:
print(f'\n {dim("ZM at position")} {gold(str(zm_pos))}')
_wait()
def keyword_tool():
"""Preview keyword digit stream."""
_clear()
_header('KEYWORD STREAM PREVIEW')
print(f' {dim("Converts a keyword to its cycling mod-10 digit stream.")}')
print(f' {dim("Use this to verify both parties have the same stream before operation.")}')
_rule()
kw = input(f'\n {gold("Keyword: ")}').strip()
if not kw:
return
stream = keyword_to_stream(kw, 50)
print(f'\n {dim("Keyword:")} {bright(kw.upper())}')
print()
chars = list(kw.upper())
detail = ' '
for ch in chars:
if ch.isalpha():
pos = ord(ch) - ord('A') + 1
d = pos % 10
detail += f'{bright(ch)}{dim("->")}{blue(str(d))} '
print(detail)
print(f'\n {dim("Cycling stream (50 digits):")}')
_print_row('STREAM', stream, blue)
check = _uniformity_check(stream)
print(f'\n {dim("Distribution:")} {check}')
_wait()
# ═══════════════════════════════════════════════════════════════════════════
# PRINTABLE FIELD CARD + WORKSHEET GENERATOR
# ═══════════════════════════════════════════════════════════════════════════
def generate_field_card():
"""Generate a printable text field card with everything needed for hand operation."""
_clear()
_header('GENERATE FIELD CARD')
print(f' {dim("Generates a printable text file with:")}')
print(f' {dim(" - RADIX encoding table")}')
print(f' {dim(" - Essential codebook (10 codes)")}')
print(f' {dim(" - Keyword digit table")}')
print(f' {dim(" - Mod-10 addition/subtraction tables")}')
print(f' {dim(" - Pre-ruled encryption worksheet with key + null digits")}')
print()
sheets = 1
raw = input(f' {gold("How many pad sheets? [1] ")}').strip()
if raw.isdigit() and int(raw) > 0:
sheets = min(int(raw), 10)
fname = input(f' {gold("Filename [vernier_field_card.txt]: ")}').strip() or 'vernier_field_card.txt'
import random as _rng
try:
with open(fname, 'w') as f:
w = f.write
# ── Header ──
w('=' * 60 + '\n')
w(' Vernier FIELD CARD // kmsp42.com rev 2.0\n')
w(' PRINT THIS. LAMINATE THE CARD. DESTROY PAD SHEETS AFTER USE.\n')
w('=' * 60 + '\n\n')
# ── RADIX Encoding Table ──
w('─── RADIX ENCODING TABLE ───────────────────────────────\n\n')
w(' SINGLE DIGIT (0-7): most common letters\n')
w(' ┌───┬───┬───┬───┬───┬───┬───┬───┐\n')
w(' │ E │ T │ A │ O │ I │ N │ S │ H │\n')
w(' │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │\n')
w(' └───┴───┴───┴───┴───┴───┴───┴───┘\n\n')
w(' TWO DIGIT (8x): medium frequency\n')
w(' ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐\n')
w(' │ R │ D │ L │ C │ U │ M │ W │ F │ G │ Y │\n')
w(' │ 80 │ 81 │ 82 │ 83 │ 84 │ 85 │ 86 │ 87 │ 88 │ 89 │\n')
w(' └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘\n\n')
w(' TWO DIGIT (9x): rare letters + period\n')
w(' ┌────┬────┬────┬────┬────┬────┬────┬────┬────┐\n')
w(' │ P │ B │ V │ K │ J │ X │ Q │ Z │ . │\n')
w(' │ 90 │ 91 │ 92 │ 93 │ 94 │ 95 │ 96 │ 97 │ 98 │\n')
w(' └────┴────┴────┴────┴────┴────┴────┴────┴────┘\n\n')
w(' NUMERALS: digit d → 99d (e.g. 6 → 996)\n\n')
w(' PARSING: 0-7=done | 8=read 1 more | 9(0-8)=done | 99=read 1 more\n')
w(' END OF MESSAGE: ALWAYS APPEND 9 7 8 5 (ZM)\n\n')
# ── Essential Codebook ──
w('─── ESSENTIAL CODEBOOK (memorize these 10) ──────────────\n\n')
for code, phrase in FIELD_ESSENTIAL.items():
w(f' {code} {phrase}\n')
w('\n')
# ── Keyword Digit Table ──
w('─── KEYWORD DIGIT TABLE ─────────────────────────────────\n\n')
w(' A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8 I=9 J=0\n')
w(' K=1 L=2 M=3 N=4 O=5 P=6 Q=7 R=8 S=9 T=0\n')
w(' U=1 V=2 W=3 X=4 Y=5 Z=6\n\n')
# ── Mod-10 Tables ──
w('─── MOD-10 ADDITION TABLE ───────────────────────────────\n')
w(' ENCRYPT: find P row, K column, read cipher digit\n\n')
w(' + │ 0 1 2 3 4 5 6 7 8 9\n')
w(' ────┼──────────────────────────────\n')
for r in range(10):
row = ' '.join(str((r + c) % 10) for c in range(10))
w(f' {r} │ {row}\n')
w('\n')
w('─── MOD-10 SUBTRACTION TABLE ────────────────────────────\n')
w(' DECRYPT: find C row, K column, read plain digit\n\n')
w(' - │ 0 1 2 3 4 5 6 7 8 9\n')
w(' ────┼──────────────────────────────\n')
for r in range(10):
row = ' '.join(str((r - c + 10) % 10) for c in range(10))
w(f' {r} │ {row}\n')
w('\n')
# ── Pad Sheets ──
for sheet_num in range(1, sheets + 1):
w('=' * 60 + '\n')
w(f' PAD SHEET {sheet_num:02d} // 100 DIGITS // SINGLE USE\n')
w(' DESTROY AFTER ENCRYPTING OR DECRYPTING ONE MESSAGE.\n')
w('=' * 60 + '\n\n')
# Generate 100 truly random key digits
pool = EntropyPool()
pool.collect_urandom(200)
key = pool.extract_digits(100)
# Pre-generate 100 null digits too
pool2 = EntropyPool()
pool2.collect_urandom(200)
nulls = pool2.extract_digits(100)
w(' KEY DIGITS (100):\n')
flat = ''.join(str(d) for d in key)
for i in range(0, 100, 50):
groups = [flat[j:j+5] for j in range(i, min(i+50, 100), 5)]
w(' ' + ' '.join(groups) + '\n')
w('\n')
w(' NULL FILL DIGITS (use to pad short messages):\n')
flat_n = ''.join(str(d) for d in nulls)
for i in range(0, 100, 50):
groups = [flat_n[j:j+5] for j in range(i, min(i+50, 100), 5)]
w(' ' + ' '.join(groups) + '\n')
w('\n')
# Pre-ruled worksheet
w(' ─── ENCRYPTION WORKSHEET ───\n\n')
w(' PLAINTEXT: _____ _____ _____ _____ _____ _____ _____ _____ _____ _____\n')
w(' _____ _____ _____ _____ _____ _____ _____ _____ _____ _____\n\n')
w(' + KEYWORD: _____ _____ _____ _____ _____ _____ _____ _____ _____ _____\n')
w(' _____ _____ _____ _____ _____ _____ _____ _____ _____ _____\n\n')
w(' = P+KW: _____ _____ _____ _____ _____ _____ _____ _____ _____ _____\n')
w(' _____ _____ _____ _____ _____ _____ _____ _____ _____ _____\n\n')
w(' + KEY: (copy from KEY DIGITS above)\n\n')
w(' = CIPHER: _____ _____ _____ _____ _____ _____ _____ _____ _____ _____\n')
w(' _____ _____ _____ _____ _____ _____ _____ _____ _____ _____\n\n')
w(' VERIFY STRIP (first 5 cipher digits): _____\n')
w(' Remember: APPEND 9 7 8 5 (ZM) TO YOUR ENCODED TEXT BEFORE ENCRYPTING\n\n')
print(f'\n {green("Saved:")} {gold(fname)}')
print(f' {dim(f"Contains: encoding table, codebook, keyword table, mod-10 tables, {sheets} pad sheet(s) with worksheets")}')
print(f' {red("Print the card. Laminate it. Destroy pad sheets after single use.")}')
except OSError as e:
print(f' {red("Save failed:")} {e}')
_wait()
# ═══════════════════════════════════════════════════════════════════════════
# AUTOMATIC DEMO — FULL ENCRYPT/DECRYPT CYCLE
# ═══════════════════════════════════════════════════════════════════════════
def demo_workflow():
"""Run the complete five-stave encrypt/decrypt cycle automatically with narration."""
_clear()
_header('AUTOMATIC DEMO — SOUP TO NUTS')
# ── Setup ────────────────────────────────────────────────────────────
sample = 'MEET AT DEAD DROP TOMORROW'
keyword = 'CIPHER'
print(f' {gold("This demo encrypts a message through all five staves,")}')
print(f' {gold("then decrypts it back, showing every intermediate step.")}')
print()
print(f' {dim("Plaintext:")} {bright(sample)}')
print(f' {dim("Keyword:")} {bright(keyword)}')
print(f' {dim("Codebook:")} active')
print(f' {dim("Block size:")} auto-selected')
_rule()
input(f'\n {dim("Press ENTER to begin...")}')
# ══════════════════════════════════════════════════════════════════════
# ENCRYPTION
# ══════════════════════════════════════════════════════════════════════
print(f'\n {orange("═══ ENCRYPTION ═══")}')
# ── Stave 1: Codebook ────────────────────────────────────────────────
print(f'\n {orange("STAVE 1")} {dim("/")} Codebook compression')
_rule()
print(f' {dim("Original:")} {bright(sample)}')
# Apply codebook manually: MEET -> ZN, DEAD DROP -> ZT, TOMORROW -> ZO
coded = 'ZN AT ZT ZO'
# Build the mapping display
print(f' {dim("MEET")} {dim("->")} {cyan("ZN")} {dim("(codebook)")}' )
print(f' {dim("DEAD DROP")} {dim("->")} {cyan("ZT")} {dim("(codebook)")}')
print(f' {dim("TOMORROW")} {dim("->")} {cyan("ZO")} {dim("(codebook)")}')
print(f' {dim("After codebook:")} {bright(coded)}')
print(f' {dim("Compression:")} {green(f"{len(sample)} chars -> {len(coded.replace(chr(32), str()))} chars")}')
input(f'\n {dim("ENTER to continue...")}')
# ── Stave 2: Vernier Encoding ───────────────────────────────────────
print(f'\n {orange("STAVE 2")} {dim("/")} Vernier variable-length encoding')
_rule()
stripped = coded.replace(' ', '')
print(f' {dim("Input:")} {bright(stripped)}')
print()
# Show per-character encoding
encoded: List[int] = []
for ch in stripped.upper():
d = encode_char(ch)
encoded.extend(d)
code_str = ''.join(str(x) for x in d)
length_desc = {1: 'single', 2: 'double', 3: 'triple'}.get(len(d), '?')
tier = ''
if ch in PRIMARY: tier = 'primary (0-7)'
elif ch in SECONDARY: tier = 'secondary (8x)'
elif ch in TERTIARY: tier = 'tertiary (9x)'
print(f' {bright(ch)} {dim("->")} {cyan(code_str):>5} '
f'{dim(f"[{length_desc}, {tier}]")}')
# Append end-of-message marker
content = encoded + ZM_DIGITS
print(f'\n {dim("End-of-message marker (ZM):")} {cyan("978531")}')
print(f'\n {dim("Full digit stream:")}')
_print_row('ENCODED', content, cyan)
print(f' {dim("Digit count:")} {gold(str(len(content)))}')
print(f'\n {dim("Parsing rule:")} {green("0-7 = 1 digit | 8x = 2 digits | 9(0-8) = 2 digits | 99x = 3 digits")}')
input(f'\n {dim("ENTER to continue...")}')
# ── Block class selection ────────────────────────────────────────────
print(f'\n {orange("BLOCK")} {dim("/")} Select block class')
_rule()
cls = select_block_class(len(content))
bc = BLOCK_CLASSES[cls]
bsize = bc['size']
prefix = bc['prefix']
null_n = bsize - 2 - len(content)
print(f' {dim("Content:")} {gold(str(len(content)))} digits')
print(f' {dim("Block class:")} {green(cls)} ({bsize} digits, {bc["groups"]} groups)')
print(f' {dim("Prefix:")} {cyan("".join(str(d) for d in prefix))}')
print(f' {dim("Null fill:")} {gold(str(null_n))} random digits')
# Build full plaintext
plain_active = prefix + content
# ── Stave 5: Keyword ─────────────────────────────────────────────────
print(f'\n {orange("STAVE 5")} {dim("/")} Keyword defense')
_rule()
print(f' {dim("Keyword:")} {bright(keyword)}')
kw_stream = keyword_to_stream(keyword, len(plain_active))
print(f' {dim("Keyword -> digit stream:")}')
kw_preview = ' '.join(str(d) for d in kw_stream[:20])
print(f' {blue(kw_preview)} {dim("... (lagged Fibonacci, non-repeating)")}')
print()
print(f' {dim("Before keyword:")}')
_print_row('PLAIN', plain_active, dim)
print(f' {dim("Keyword stream:")}')
_print_row('KW', kw_stream[:len(plain_active)], blue)
plain_kw = [add_mod10(p, k) for p, k in zip(plain_active, kw_stream)]
print(f' {dim("After keyword (P + KW) mod 10:")}')
_print_row('P+KW', plain_kw, purple)
input(f'\n {dim("ENTER to continue...")}')
# ── Stave 3: One-Time Pad ────────────────────────────────────────────
print(f'\n {orange("STAVE 3")} {dim("/")} One-time pad encryption')
_rule()
print(f' {dim("Generating")} {gold(str(bsize))} {dim("truly random key digits...")}')
key_digits = collect_entropy(bsize)
print()
# Active encryption
key_active = key_digits[:len(plain_kw)]
cipher_active = encrypt_stream(plain_kw, key_active)
# Null fill (random)
null_key = key_digits[len(plain_kw):]
cipher_null = null_key[:] # (0 + k) mod 10 = k
cipher_full = cipher_active + cipher_null
print(f' {dim("P+KW digits:")}')
_print_row('P+KW', plain_kw, purple)
print(f' {dim("OTP key:")}')
_print_row('KEY', key_digits, blue)
print(f' {dim("Cipher (P+KW + KEY) mod 10:")}')
_print_row('CIPHER', cipher_full, green)
print()
print(f' {dim("Null-filled:")} {gold(str(null_n))} {dim("digits at end are indistinguishable from ciphertext")}')
input(f'\n {dim("ENTER to continue...")}')
# ── Stave 4: Block formatting ────────────────────────────────────────
print(f'\n {orange("STAVE 4")} {dim("/")} Fixed-size block discipline')
_rule()
print(f' {dim("All messages are")} {gold(str(bsize))} {dim("digits regardless of content length.")}')
print(f' {dim("An eavesdropper cannot determine message length.")}')
print()
print(f' {gold("CIPHERTEXT FOR TRANSMISSION:")}')
print()
groups_str = _groups(cipher_full)
print(f' {green(groups_str)}')
print()
verify = cipher_full[:5]
print(f' {gold("VERIFY STRIP")} {dim("(first 5):")} {orange(_groups(verify))}')
print(f' {dim("Class:")} {green(cls)} {dim("Digits:")} {gold(str(len(cipher_full)))} '
f'{dim("Groups:")} {gold(str(bc["groups"]))}')
_rule()
print(f'\n {bright("Encryption complete. All 5 layers applied.")}')
print(f' {dim("The ciphertext above is what gets transmitted.")}')
input(f'\n {dim("ENTER for decryption...")}')
# ══════════════════════════════════════════════════════════════════════
# DECRYPTION — reverse all layers
# ══════════════════════════════════════════════════════════════════════
_clear()
_header('AUTOMATIC DEMO — DECRYPTION')
print(f' {orange("═══ DECRYPTION ═══")}')
print(f'\n {dim("Received ciphertext:")}')
print(f' {green(groups_str)}')
print(f' {dim("Key material (from pad sheet):")}')
_print_row('KEY', key_digits, blue)
# ── Strip OTP ────────────────────────────────────────────────────────
print(f'\n {orange("STAVE 3")} {dim("/")} Remove one-time pad')
_rule()
decrypted_full = decrypt_stream(cipher_full, key_digits)
print(f' {dim("(CIPHER - KEY + 10) mod 10:")}')
_print_row('DEC', decrypted_full, purple)
# ── Strip keyword ────────────────────────────────────────────────────
print(f'\n {orange("STAVE 5")} {dim("/")} Remove keyword')
_rule()
print(f' {dim("Keyword:")} {bright(keyword)}')
kw_full = keyword_to_stream(keyword, len(decrypted_full))
plain_recovered = [sub_mod10(d, k) for d, k in zip(decrypted_full, kw_full)]
print(f' {dim("(DEC - KW + 10) mod 10:")}')
_print_row('PLAIN', plain_recovered, cyan)
# ── Parse prefix and content ─────────────────────────────────────────
print(f'\n {orange("BLOCK")} {dim("/")} Parse block prefix')
_rule()
pfx = plain_recovered[:2]
body = plain_recovered[2:]
pfx_str = ''.join(str(d) for d in pfx)
detected_cls = None
for k, v in BLOCK_CLASSES.items():
if v['prefix'] == pfx:
detected_cls = k
break
print(f' {dim("Prefix:")} {cyan(pfx_str)} {dim("-> class")} {green(detected_cls or "?")}')
# ── RADIX decode ─────────────────────────────────────────────────────
print(f'\n {orange("STAVE 2")} {dim("/")} Vernier decode')
_rule()
decoded, consumed = decode_digits(body)
print(f' {dim("Digit stream -> characters:")}')
# Show the parsing process
pos = 0
parse_body = body[:consumed] if consumed > 0 else body
while pos < len(parse_body):
d0 = parse_body[pos]
if d0 <= 7:
ch = _REV_PRIMARY.get(d0, '?')
print(f' {cyan(str(d0)):>8} {dim("->")} {bright(ch)} {dim("[single digit, primary]")}')
pos += 1
elif d0 == 8:
if pos + 1 < len(parse_body):
code = d0 * 10 + parse_body[pos + 1]
ch = _REV_SECONDARY.get(code, '?')
print(f' {cyan(str(d0) + str(parse_body[pos+1])):>8} {dim("->")} {bright(ch)} {dim("[two digit, secondary]")}')
pos += 2
else:
break
elif d0 == 9:
if pos + 1 < len(parse_body):
d1 = parse_body[pos + 1]
if d1 <= 8:
code = d0 * 10 + d1
ch = _REV_TERTIARY.get(code, '?')
if ch == '?' and code == 97 and pos + 2 < len(parse_body) and parse_body[pos+2] == 8 and pos + 3 < len(parse_body) and parse_body[pos+3] == 5:
# ZM marker
print(f' {cyan("978531"):>8} {dim("->")} {gold("ZM")} {dim("[end-of-message]")}')
break
print(f' {cyan(str(d0) + str(d1)):>8} {dim("->")} {bright(ch)} {dim("[two digit, tertiary]")}')
pos += 2
elif d1 == 9:
if pos + 2 < len(parse_body):
numeral = parse_body[pos + 2]
print(f' {cyan(str(d0) + str(d1) + str(numeral)):>8} {dim("->")} {bright(str(numeral))} {dim("[three digit, numeral]")}')
pos += 3
else:
break
else:
break
else:
pos += 1
print(f'\n {dim("Decoded text:")} {bright(decoded)}')
# ── Codebook expansion ───────────────────────────────────────────────
print(f'\n {orange("STAVE 1")} {dim("/")} Codebook expansion')
_rule()
expanded = decoded
for code, phrase in CODEBOOK.items():
if code in expanded and code != 'ZM':
expanded = expanded.replace(code, phrase)
print(f' {cyan(code)} {dim("->")} {bright(phrase)}')
print(f'\n {dim("Final plaintext:")} {bright(expanded)}')
# ── Verify ───────────────────────────────────────────────────────────
_rule()
original_clean = sample.replace(' ', '').upper()
recovered_clean = expanded.replace(' ', '').upper()
if original_clean == recovered_clean:
print(f'\n {green("✓ INTEGRITY VERIFIED")} — decrypted plaintext matches original')
else:
print(f'\n {red("✗ MISMATCH")}')
print(f' {dim("Original:")} {sample}')
print(f' {dim("Recovered:")} {expanded}')
print(f'\n {dim("All 5 layers reversed successfully.")}')
print(f' {dim("The cipher is information-theoretically secure: no computation,")}')
print(f' {dim("classical or quantum, can break it without the key material.")}')
_wait()
# ═══════════════════════════════════════════════════════════════════════════
# MAIN MENU
# ═══════════════════════════════════════════════════════════════════════════
MENU_ITEMS = [
('1', 'Encrypt message', encrypt_workflow, bright),
('2', 'Decrypt message', decrypt_workflow, bright),
(None, None, None, None),
('3', 'Quick encode (no OTP)', quick_encode, dim),
('4', 'Quick decode (no OTP)', quick_decode, dim),
('5', 'Keyword stream preview', keyword_tool, dim),
(None, None, None, None),
('6', 'Encoding table', show_encoding_table, dim),
('7', 'Codebook', show_codebook, dim),
('8', 'Block classes', show_block_classes, dim),
('9', 'Mod-10 cipher table', show_mod_table, dim),
(None, None, None, None),
('D', 'Demo (full cycle)', demo_workflow, gold),
('F', 'Field card + pad sheets', generate_field_card, gold),
('Q', 'Quit', None, gray),
]
def main():
try:
while True:
_clear()
_header()
for key, label, _, color_fn in MENU_ITEMS:
if key is None:
print(dim(' ' + '\u2500' * 32))
else:
print(f' {gold(key)} {color_fn(label)}')
print()
choice = input(f' {gold("> ")}').strip().lower()
for key, _, fn, _ in MENU_ITEMS:
if key and choice == key.lower():
if fn:
fn()
break
else:
if choice in ('exit', 'quit', 'q'):
break
except KeyboardInterrupt:
pass
finally:
_clear()
print(f'\n {dim("Vernier // session ended cleanly")}\n')
sys.exit(0)
if __name__ == '__main__':
if '--demo' in sys.argv:
demo_workflow()
else:
main()
A synthesis of VIC cipher straddling checkerboard efficiency, SOE Worked Out Key direct-letter philosophy, mod-10 arithmetic simplicity, and operational codebook compression. Designed from first principles around three criteria: unambiguous parsing under stress, minimum mental operations per character, and layered physical and cryptographic security. Full information-theoretic security. No electronics required.
Every existing hand cipher system trades one problem for another. This document derives a synthesis by identifying the specific failure modes of each approach and taking only what each does best.
| System | What It Does Best | Its Failure Mode |
|---|---|---|
| Soviet VIC / Mod-10 | Simple arithmetic (one 10x10 table) | Encoding step (A=01) is slow and error-prone |
| SOE WOK / Mod-26 | No encoding step -- letters in, letters out | 26x26 Vigenere table is large and slow under stress |
| Straddling Checkerboard | Frequency-weighted single-digit codes | Ambiguous parsing when prefix digits overlap with single-digit codes |
| LC4 | Stateful -- no key material consumed | Complex operations, high error rate under stress |
| Solitaire | Key is a deck of cards -- innocuous | ~8 operations per character, extreme error rate |
| Previous hybrid (this doc series) | Checkerboard + mod-10 combined | Row 0 digits (1,2,3) overlap with row prefix digits (1,2,3) -- ambiguous |
The previous system in this series had a subtle but serious flaw: Row 0 used single digits 1, 2, 3 for E, T, A -- but 1, 2, 3 were also row prefixes for two-digit codes. Decoding required lookahead: "is this 1 the letter E, or the start of a code like 15=M?" Under stress, that decision is a source of error.
Vernier solves this with a single rule that requires no lookahead and no decision-making:
Each layer addresses a different threat. Layers 1 and 4 address operational efficiency and traffic analysis. Stave 2 addresses speed and error rate. Stave 3 provides cryptographic security. Stave 5 provides physical security when key material may be at risk. Layers 3 through 5 together mean an adversary must simultaneously defeat a cryptographic proof, recover physical key material, and extract a memorized secret to read traffic.
The codebook operates before encoding. It replaces standard operational phrases with short letter codes, which then go through normal RADIX encoding. This is a human judgment step, not an automatic substitution. The encoder decides when a phrase matches a codebook entry closely enough to apply it.
Previous iterations used digit codes (9001, 9002, etc.) for the codebook. Vernier uses two-letter codes instead. This eliminates the need for a separate codebook prefix digit and allows codebook entries to flow through the same RADIX encoding as the rest of the message. The two-letter codes are chosen from rare letter pairs that do not appear in normal English text.
The letter pair Z followed by any letter signals a codebook entry. Z is the rarest letter in English (0.07% frequency). ZZ, ZA, ZB, etc. are essentially nonexistent in operational vocabulary. After decoding, the receiver scans for any Z and treats Z+letter as a codebook lookup.
| Code | Meaning | Code | Meaning |
|---|---|---|---|
| ZA | CONFIRMED / YES | ZN | MEET |
| ZB | ABORT / CANCEL | ZO | TOMORROW |
| ZC | PROCEED | ZP | TODAY |
| ZD | DANGER / COMPROMISED | ZQ | LOCATION FOLLOWS |
| ZE | DELAY | ZR | TIME FOLLOWS |
| ZF | COMPLETE / DONE | ZS | SWITCH TO ALTERNATE |
| ZG | CONFIRM LAST -- previous msg received correctly | ZT | DEAD DROP |
| ZH | EMERGENCY | ZU | SURVEILLANCE DETECTED |
| ZI | NOTHING TO REPORT / NO CHANGE | ZV | CLEAR / ALL CLEAR |
| ZJ | AS PLANNED | ZW | RECEIVED AND UNDERSTOOD |
| ZK | PRIMARY LOCATION | ZX | DOCUMENT FOLLOWS |
| ZL | ALTERNATE LOCATION | ZY | COORDINATES FOLLOW |
| ZM | END OF MESSAGE (reserved -- never reassign) | ZZ | NAME FOLLOWS |
| ZG / ZI / ZW are revised from previous version. ZG=CONFIRM LAST enables check-in circuit. ZI=NOTHING TO REPORT covers routine nil-report messages. ZW=RECEIVED AND UNDERSTOOD disambiguates from ZA=CONFIRMED (which confirms an instruction, not a receipt). | |||
Z is encoded as 97 (tertiary position 7). The following letter uses its normal RADIX code. A two-letter codebook entry costs 3-4 digits versus 10-40 digits for the full phrase.
ZA (CONFIRMED): Z=97 A=2 --> 3 digits ZD (DANGER): Z=97 D=81 --> 4 digits ZU (SURV DET): Z=97 U=84 --> 4 digits ZN (MEET): Z=97 N=5 --> 3 digits Example message using only codebook: "SURVEILLANCE DETECTED ABORT SWITCH TO ALTERNATE MEET TOMORROW" = ZU ZB ZS ZN ZO = 97 84 | 97 91 | 97 S=6 | 97 5 | 97 O=3 = 9784 9791 976 975 973 = 19 digits Fully spelled out (no codebook): = ~130 digits Savings: ~85%
ZM (Z=97, M=85) marks the end of real content. Everything after ZM in the block is null padding. Receiver stops decoding at ZM. This replaces the previous 9000 convention with a letter-space marker that flows naturally through the encoding system.
Three codebook entries cover the entire class of routine check-in messages, enabling a nil-report or acknowledgment circuit that costs one Short block (50 digits) regardless of operational status:
Sender: nothing to report --> ZI ZM (encrypted, Short block, 50 digits) Receiver confirms receipt --> ZW ZM (encrypted, Short block, 50 digits) Sender asks: did you get it? --> ZG ZM (encrypted, Short block, 50 digits) A pure check-in: ZI ZM = 97 4 97 85 = 8 digits content, padded to 50 An acknowledgment: ZW ZM = 97 86 97 85 = 8 digits content, padded to 50 An adversary observing traffic sees two 50-digit Short block transmissions. They cannot distinguish ZI (nothing to report) from ZH (emergency) by length. Both fit in a Short block. Both look identical.
Encode the following operational intent using codebook entries (refer to the table above). Then determine how many digits the codebook version produces versus the spelled-out version.
Intent: Danger. Abort. Proceed to alternate location. Confirmed.
Codebook version: DANGER = ZD ABORT = ZB PROCEED = ZC ALTERNATE LOCATION = ZL CONFIRMED = ZA END = ZM Pre-encoding string: ZD ZB ZC ZL ZA ZM RADIX encoding: Z=97 D=81 --> 9781 Z=97 B=91 --> 9791 Z=97 C=83 --> 9783 Z=97 L=82 --> 9782 Z=97 A=2 --> 972 Z=97 M=85 --> 978531 Digit string: 9781 9791 9783 9782 972 978531 = 23 digits Fully spelled (DANGERABORTPROCEEDTOALTERNATELOCATIONCONFIRMED): D=81 A=2 N=5 G=88 E=0 R=80 = 6 codes... ~50 chars x ~1.5 avg = ~75 digits Savings: ~70%
RADIX encoding converts plaintext characters to a digit stream using a three-tier frequency-weighted system. The critical innovation over all previous approaches: the digit that begins any code uniquely determines how many more digits to read. Zero decision-making required.
RADIX strips spaces before encoding. Words run together in the digit stream. The receiver infers word boundaries from context after decoding -- this was standard practice in telegraph and cipher communications for over a century. Removing space encoding eliminates one code type entirely and reduces average digit length.
"MEET AT DAWN" --> strip spaces --> "MEETATDAWN" --> encode
| PRIMARY -- digits 0-7 -- one digit each -- top 8 English letters | |||||||
|---|---|---|---|---|---|---|---|
| E 0 | T 1 | A 2 | O 3 |
I 4 | N 5 | S 6 | H 7 |
| SECONDARY -- prefix 8 -- two digits each (80-89) | |||||||||
|---|---|---|---|---|---|---|---|---|---|
| R 80 | D 81 | L 82 | C 83 |
U 84 | M 85 | W 86 | F 87 |
G 88 | Y 89 |
| TERTIARY -- prefix 9 -- two or three digits (90-99) | |||||||||
|---|---|---|---|---|---|---|---|---|---|
| P 90 | B 91 | V 92 | K 93 |
J 94 | X 95 | Q 96 | Z 97 |
. 98 | NUM 99+ |
| NUMERALS -- prefix 99 -- three digits each (990-999) | |||||||||
|---|---|---|---|---|---|---|---|---|---|
| 0 990 | 1 991 | 2 992 |
3 993 | 4 994 | 5 995 |
6 996 | 7 997 | 8 998 |
9 999 |
See digit d:
d in {0,1,2,3,4,5,6,7} ──> DONE. Single-digit code.
E T A O I N S H
d = 8 ──> Read one more digit x.
Look up 8x in secondary table.
R D L C U M W F G Y
d = 9, next digit 0-8 ──> Two digits 9x.
Look up in tertiary table.
P B V K J X Q Z .
d = 9, next digit = 9 ──> Read one MORE digit x.
That digit x IS the numeral.
990=0 991=1 ... 999=9
PRIMARY (0-7): E T A O I N S H "Eat Tangerines And Oranges In Nice Spanish Hotels" The classic English frequency sequence. Already memorized by most cipher students. SECONDARY (8x, 80-89): R D L C U M W F G Y "Really Dull Lessons Can Undermine Many Willing Field Graduates, Yes" Next 10 letters by frequency. R is 9th most common in English. TERTIARY (9x, 90-98): P B V K J X Q Z . "Please Be Very Kind, Just eXercise Quietly, Zealously." Rare letters in rough frequency order. Period at the end. NUMERALS (99x): 990-999 = digits 0-9. Just add 990. "Ninety-nine plus the digit."
English letter frequencies in typical text:
Primary (ETAOINSH): ~57% of letters x 1 digit = 0.57 avg digits
Secondary (RDLCUMWFGY): ~26% of letters x 2 digits = 0.52 avg digits
Tertiary (PBVKJXQZ.): ~17% of letters x 2 digits = 0.34 avg digits
Average (letter-only text): ~1.43 digits per character
Compare:
A=01 simple encoding: 2.00 digits/char
Previous row system: 1.35 digits/char (but ambiguous parsing)
RADIX (letter only): 1.43 digits/char (unambiguous parsing)
RADIX with codebook: ~0.4 digits/char for codebook-heavy messages
RADIX trades 0.08 digits/char for complete parsing unambiguity.
That is the right trade for stress operation.
Encode the following words using only primary-tier codes (all letters should be from ETAOINSH). Time yourself. Target: under 5 seconds per word after practice.
INTENSE ANTIHERO NOTATION SENTHRONE NINETEEN
INTENSE: I=4 N=5 T=1 E=0 N=5 S=6 E=0 --> 4510560 (7 digits, 7 chars) ANTIHERO: A=2 N=5 T=1 I=4 H=7 E=0 R=80 O=3 --> 25147 0803 (9 digits, 8 chars) (R is secondary -- 80) NOTATION: N=5 O=3 T=1 A=2 T=1 I=4 O=3 N=5 --> 53121435 (8 digits, 8 chars) SENTHRONE: S=6 E=0 N=5 T=1 H=7 R=80 O=3 N=5 E=0 --> 60517 8035 0 (10 digits, 9 chars) NINETEEN: N=5 I=4 N=5 E=0 T=1 E=0 E=0 N=5 --> 54501005 (8 digits, 8 chars) Note: ANTIHERO and SENTHRONE contain R (secondary). Average efficiency for all-primary words: 1.0 digit/char.
Encode the following phrase. Strip spaces first. Identify which tier each letter falls into before writing its code.
REPORT CONFIRMED
Strip spaces: REPORTCONFIRMED R = 80 (secondary) E = 0 (primary) P = 90 (tertiary) O = 3 (primary) R = 80 (secondary) T = 1 (primary) C = 83 (secondary) O = 3 (primary) N = 5 (primary) F = 87 (secondary) I = 4 (primary) R = 80 (secondary) M = 85 (secondary) E = 0 (primary) D = 81 (secondary) Digit string: 80 0 90 3 80 1 83 3 5 87 4 80 85 0 81 Grouped by 5: 80090 38013 33587 48085 081 15 characters --> 23 digits Average: 1.53 digits/char (slightly above baseline due to R appearing 3x)
Decode the following digit stream back to plaintext. Apply the parsing rule at each digit: 0-7 = done; 8 = read one more; 9 then 0-8 = two digits; 99 = read one more for numeral. Insert word breaks by inference.
6 0 5 81 80 3 90 4 5 1 0 81
6 --> S (primary) 0 --> E (primary) 5 --> N (primary) 81 --> D (secondary: 8, then 1 = position 1 = D) 80 --> R (secondary: 8, then 0 = position 0 = R) 3 --> O (primary) 90 --> P (tertiary: 9, then 0 = position 0 = P) 4 --> I (primary) 5 --> N (primary) 1 --> T (primary) 0 --> E (primary) 81 --> D (secondary) Decoded: SENDROPPINTED? Reparse for word breaks: SEND REPORTED? S E N D R O P I N T E D Plaintext: SEND ROINTED Wait -- SENDR + OPIN + TED? Let's try: S-E-N-D | R-O-P-I-N-T-E-D = SEND ROINTED Or: S-E-N-D-R-O-P-I-N-T-E-D = SENDROPIN TED? Most natural: SEND REPORTED... but P not R at position 6. Actual sequence: S E N D R O P I N T E D = "SEND ROPIN TED" -- likely "SEND DROP INTENDED" with some letters elided or simply "SENDROPINTED" decoded exactly as above, word breaks from context.
Encode the following string including numerals. Confirm each numeral costs exactly 3 digits (99 + the digit).
NORTH AT 0600
Strip spaces: NORTHAT0600 N=5 O=3 R=80 T=1 H=7 A=2 T=1 0=990 6=996 0=990 0=990 Digit string: 5 3 80 1 7 2 1 990 996 990 990 Grouped: 53801 72199 09969 9099 0 11 characters --> 18 digits Letters only (7 chars): 9 digits = 1.29 digits/char Numerals (4 chars): 12 digits = 3 digits/char Combined: 18/11 = 1.64 digits/char Numerals inflate the average significantly. Use codebook entries for times where possible (ZR = TIME FOLLOWS, then spell only the actual time in numerals).
The cipher operation is unchanged from the Soviet standard: modular addition in base 10, no carries. The key material is a truly random digit stream generated by d10 dice and recorded on a one-time pad sheet. This layer provides information-theoretic security -- not computational security. No amount of compute can break it if the key is random and used once.
ENCRYPT: C = (P + K) mod 10 DECRYPT: P = (C - K + 10) mod 10
| P\K | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |
| 2 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 |
| 3 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 |
| 4 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 |
| 5 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 4 |
| 6 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 4 | 5 |
| 7 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 8 | 8 | 9 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 9 | 9 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Row = plaintext digit, column = key digit, cell = cipher digit. The shaded diagonal (P=K) marks zero-key positions where ciphertext equals plaintext. This is not a flaw -- a zero key digit is as probable as any other, and its occurrence is not visible in the ciphertext stream.
Given: C=3, K=7 Find row K=7: 7 8 9 0 1 2 3 4 5 6 Locate C=3 in that row: position 6 Column header = 6 = plaintext digit Verify: (6+7) mod 10 = 13 mod 10 = 3. Correct. The table is its own inverse. Encryption and decryption use identical lookup mechanics -- find key row, find value in row, read column header.
Requirements: d10 dice (precision casino-grade preferred) One die = one digit per roll Record immediately, do not group mentally first Generate full block (200 digits) before writing message Never reject a valid roll for any reason Both parties generate identical copies (carbon / simultaneous) Verify digit-for-digit after generation
Using only the table (no mental arithmetic), encrypt and decrypt the following digit pairs. Do not count -- look up. Time yourself. Target: under 2 seconds per pair.
Encrypt: P=3, K=8 --> C=? Encrypt: P=7, K=7 --> C=? Encrypt: P=0, K=9 --> C=? Encrypt: P=9, K=1 --> C=? Decrypt: C=2, K=5 --> P=? Decrypt: C=0, K=3 --> P=? Decrypt: C=5, K=9 --> P=?
Encrypt P=3, K=8: row 3, col 8 = 1 | verify: (3+8)%10=1 ✓ Encrypt P=7, K=7: row 7, col 7 = 4 | verify: (7+7)%10=4 ✓ Encrypt P=0, K=9: row 0, col 9 = 9 | verify: (0+9)%10=9 ✓ Encrypt P=9, K=1: row 9, col 1 = 0 | verify: (9+1)%10=0 ✓ Decrypt C=2, K=5: row 5, find 2 --> col 7 = P=7 | verify: (7+5)%10=2 ✓ Decrypt C=0, K=3: row 3, find 0 --> col 7 = P=7 | verify: (7+3)%10=0 ✓ Decrypt C=5, K=9: row 9, find 5 --> col 6 = P=6 | verify: (6+9)%10=5 ✓
Fixed 200-digit blocks are blunt. A 3-digit codebook message padded to 200 wastes 194 digits of key material and 194 digits of transmission time. Vernier uses three agreed block classes, selected by the encoder based on message length. The class is signaled by a two-digit encrypted prefix at the start of every transmission.
| Class | Prefix (plaintext) | Block Size | Max Content | Dice Rolls | Groups |
|---|---|---|---|---|---|
| S -- SHORT | 00 | 50 digits | 44 digits (50 - 2 prefix - 4 ZM) | 50 | 10 |
| M -- MEDIUM | 01 | 100 digits | 94 digits (100 - 2 prefix - 4 ZM) | 100 | 20 |
| L -- LONG | 02 | 200 digits | 194 digits (200 - 2 prefix - 4 ZM) | 200 | 40 |
[OFFSET 2][SALT 4][RANDOM FILL][CONTENT][ZM 978531][PADDING] = BLOCK SIZE (25, 50, or 100) Short: 00 [content up to 44 digits] [978531] [null fill] = 50 digits Medium: 01 [content up to 94 digits] [978531] [null fill] = 100 digits Long: 02 [content up to 194 digits][978531] [null fill] = 200 digits The prefix is encrypted as part of the block. The receiver decrypts digits 1-2, reads the class, then decrypts the remainder of that class size.
Encode the message content first (without prefix). Count the digits. Then select the class. This means encoding happens before key generation -- you need the block size before you know how many dice to roll.
Content 0 - 44 digits --> Short (50 rolls) Content 45 - 94 digits --> Medium (100 rolls) Content 95 - 194 digits --> Long (200 rolls) Over 194 digits --> Split across two pad sheets
SHORT (most common with codebook): Routine nil-report: ZI ZM = 8 digits Short Single acknowledgment: ZW ZM = 8 digits Short Emergency abort: ZH ZB ZM = 11 digits Short Confirmed meet: ZN ZA ZM = 10 digits Short Full check-in: ZN ZP ZK ZA ZM = 16 digits Short MEDIUM (mixed codebook + spelled): Meet tomorrow north gate at 0800: ~30 digits Short Report with two location references: ~55 digits Medium Message with names/callsigns: ~70 digits Medium LONG (heavily spelled or numeric): Detailed coordinates + instructions: ~120 digits Long Multi-part report with numerals: ~150 digits Long
The null fill after ZM is the continuation of the actual key digit stream. The receiver decrypts it and gets noise, but stops at ZM. Null digits cost key material but are already generated as part of the block roll -- no extra work.
If operating against an adversary with deep traffic analysis capability who could correlate message class against known events, revert to a single fixed 200-digit block. Security always beats efficiency at the top of the threat model. The variable class system is the default for most operational scenarios.
If an adversary photographs or copies your pad sheet without your knowledge, the OTP layer alone no longer provides security -- they have the key. Stave 5 adds a memorized keyword stream that is applied before the OTP addition. The keyword lives only in memory, never on any physical medium.
Convert any agreed word to a cycling digit stream using position in the alphabet mod 10. This is done once, memorized, and the paper is destroyed.
Conversion: position in alphabet mod 10 A=1 B=2 C=3 D=4 E=5 F=6 G=7 H=8 I=9 J=0 K=1 L=2 M=3 N=4 O=5 P=6 Q=7 R=8 S=9 T=0 U=1 V=2 W=3 X=4 Y=5 Z=6 Example keyword: SEDATION S=9 E=5 D=4 A=1 T=0 I=9 O=5 N=4 Keyword digit stream: 9 5 4 1 0 9 5 4 | 9 5 4 1 0 9 5 4 | ... (cycles from the beginning when exhausted)
ENCRYPT: Step 1: Encode plaintext --> digit stream P Step 2: Add keyword stream K1 mod-10 --> intermediate I Step 3: Add OTP key stream K2 mod-10 --> ciphertext C C = (P + K1 + K2) mod 10 DECRYPT: Step 1: Subtract OTP key stream K2 mod-10 --> intermediate I Step 2: Subtract keyword stream K1 mod-10 --> plaintext P P = (C - K2 - K1 + 20) mod 10 (The +20 ensures no negative values before mod)
Must: Be memorizable under stress Produce non-obvious digit stream (avoid AAAA, ABCD patterns) Be 6-12 characters (longer cycles are better) Contain no repeated letters (optional but cleaner) Avoid: Names, dates, places associated with you or the operation Words appearing in any document you carry Common words that an adversary might guess systematically Good candidates: SEDATION, OUTSHINE, HOARIEST, EARTHLINGS Test: convert to digit stream, check for obvious runs (0000, 1234)
Convert the keyword OUTSHINE to its digit stream. Then write out the first 20 digits of the cycling stream.
Keyword: OUTSHINE
O=5 U=1 T=0 S=9 H=8 I=9 N=4 E=5 Keyword stream: 5 1 0 9 8 9 4 5 Cycling 20: 5 1 0 9 8 9 4 5 | 5 1 0 9 8 9 4 5 | 5 1 0 9 Check for runs: no four identical consecutive digits. Good. Entropy appears distributed. Acceptable keyword.
Confirm pad. Verify pad sheet number with counterpart. Confirm verify strip from previous exchange matches. Write pad ID in Q4 reference quadrant. A mismatch now costs seconds. Discovered after full encryption it costs everything.
Apply codebook (Q2 upper). Write the raw plaintext in Q2. Identify phrases with codebook entries. Replace them with Z-codes. Remaining text stays as written.
Strip spaces and encode (Q2 lower). Remove all spaces. Encode character by character using the RADIX table. Apply the parsing rule. Write the digit stream in Q2 lower, in groups of five. Append ZM (978531) at the end. Count the total digits.
Select block class. Count content digits (including ZM). 0-44 digits: Short (prefix 00, roll 50 key digits). 45-94 digits: Medium (prefix 01, roll 100 key digits). 95-194 digits: Long (prefix 02, roll 200 key digits). Over 194: split into two messages on two pad sheets. Write the prefix as the first two digits of Q2 lower, before the content.
Generate key block (Q1). Roll d10 dice, record exactly the number of digits required by the selected class (50, 100, or 200) in Q1 in groups of five. This is your OTP key stream K2. Do not begin until the full encoding in Q2 is complete.
Apply keyword stream if using Stave 5 (Q2 lower, in place). Add the cycling keyword digit stream K1 to each encoded digit mod-10. Overwrite Q2 digits with the result, or use a separate working row.
Apply OTP key stream (Q3). Add Q2 (or Stave 5 result) digit-by-digit to Q1 key stream using the mod-10 table. Write results in Q3. Continue writing key digits as null fill until Q3 contains the full block size.
Record verify strip. Write the first 5 cipher digits of Q3 at the bottom of Q3. Both parties will compare these next exchange.
Transmit. Read Q3 in groups of five to block size (10, 20, or 40 groups). Ciphertext only. Never the pad ID, class, key, or plaintext via the same channel.
Destroy immediately. Water method first, flame backup. Hard non-porous writing surface, no impressions on surfaces below. Confirm no legible fragment before leaving.
Confirm pad and verify strip. Confirm pad sheet number. Confirm that the sender's verify strip (first 5 cipher digits) matches the first 5 digits of what you received. Mismatch = wrong pad sheet. Stop and resolve before proceeding.
Write received ciphertext in Q3. Copy exactly as received. Do not correct suspected errors.
Subtract OTP key stream mod-10 (Q2).
Apply I = (C - K2 + 10) mod 10 digit by digit.
Use table: find K2's row, locate C in that row, read column header.
Write results in Q2.
Subtract keyword stream if using Stave 5 (Q2, in place).
Apply P = (I - K1 + 10) mod 10 digit by digit.
Overwrite Q2 digits.
Decode digit stream to characters. Apply the RADIX parsing rule. Stop at ZM (97 85). Write the decoded characters in Q2 upper. Expand any Z-codes using the codebook.
Infer word breaks. Insert spaces based on context. Operational vocabulary is constrained -- ambiguous parses are usually resolvable. If genuinely ambiguous, request retransmission of the specific group.
Record verify strip and destroy. Write the first 5 cipher digits of this message in your verify strip. Destroy pad sheet immediately.
Encrypt the following message using Layers 1-4 (no keyword layer). Use the key digits provided. Apply codebook where applicable. Produce ciphertext groups and a verify strip.
Message: CONFIRMED PROCEED TO PRIMARY LOCATION TOMORROW Key digits (first 40): 7 3 2 8 4 1 9 0 5 6 3 8 2 1 4 7 0 9 3 5 1 4 7 2 8 3 5 0 6 1 4 9 2 7 3 8 0 5 1 6
Step 1 -- Codebook: CONFIRMED = ZA PROCEED = ZC PRIMARY LOCATION = ZK TOMORROW = ZO End = ZM Pre-encoded: ZA ZC ZK ZO ZM Step 2 -- RADIX encode: ZA: 97 2 ZC: 97 83 ZK: 97 93 ZO: 97 3 ZM: 97 85 Digit string: 9 7 2 9 7 83 9 7 93 9 7 3 9 7 85 = 9 7 2 9 7 8 3 9 7 9 3 9 7 3 9 7 8 5 = 18 digits of content + 182 null Step 3 -- Add key mod-10: Plain: 9 7 2 9 7 8 3 9 7 9 3 9 7 3 9 7 8 5 Key: 7 3 2 8 4 1 9 0 5 6 3 8 2 1 4 7 0 9 9+7=6 | 7+3=0 | 2+2=4 | 9+8=7 | 7+4=1 | 8+1=9 | 3+9=2 | 9+0=9 7+5=2 | 9+6=5 | 3+3=6 | 9+8=7 | 7+2=9 | 3+1=4 | 9+4=3 | 7+7=4 8+0=8 | 5+9=4 Cipher (content): 6 0 4 7 1 9 2 9 2 5 6 7 9 4 3 4 8 4 [Continue: key digits 19-200 written as null fill into Q3] Groups of 5: 60471 92925 67943 484__ [null fill groups] Verify strip: 60471
Decrypt the following ciphertext using the key provided. Recover the full plaintext including codebook expansion. This is the output of Exercise 11.1.
Received: 60471 92925 67943 484[null] Key digits: 7 3 2 8 4 1 9 0 5 6 3 8 2 1 4 7 0 9 ...
Step 1 -- Subtract key mod-10:
Cipher: 6 0 4 7 1 9 2 9 2 5 6 7 9 4 3 4 8 4
Key: 7 3 2 8 4 1 9 0 5 6 3 8 2 1 4 7 0 9
6-7+10=9 | 0-3+10=7 | 4-2=2 | 7-8+10=9 | 1-4+10=7 | 9-1=8 | 2-9+10=3
9-0=9 | 2-5+10=7 | 5-6+10=9 | 6-3=3 | 7-8+10=9 | 9-2=7 | 4-1=3
3-4+10=9 | 4-7+10=7 | 8-0=8 | 4-9+10=5
Decoded digits: 9 7 2 9 7 8 3 9 7 9 3 9 7 3 9 7 8 5
Step 2 -- RADIX decode:
97 --> Z (tertiary: 9, pos 7)
2 --> A (primary)
ZA = CONFIRMED
97 --> Z
83 --> C (secondary: 8, pos 3)
ZC = PROCEED
97 --> Z
93 --> K (tertiary: 9, pos 3)
ZK = PRIMARY LOCATION
97 --> Z
3 --> O (primary)
ZO = TOMORROW
97 --> Z
85 --> M (secondary: 8, pos 5)
ZM = END OF MESSAGE
Plaintext: CONFIRMED PROCEED TO PRIMARY LOCATION TOMORROW
Encrypt the following message using all five staves including the keyword defense layer. Use keyword SEDATION (digit stream: 9 5 4 1 0 9 5 4, cycling).
Message: ABORT DANGER EMERGENCY SWITCH TO ALTERNATE Key digits (K2, OTP, first 30): 8 2 4 7 3 1 0 9 5 6 2 4 8 3 7 0 5 1 6 2 4 7 8 3 0 5 9 2 1 4
Step 1 -- Codebook: ABORT = ZB DANGER = ZD EMERGENCY = ZH SWITCH TO ALTERNATE = ZS End = ZM Pre-encoded: ZB ZD ZH ZS ZM Step 2 -- RADIX encode: ZB: 97 91 ZD: 97 81 ZH: 97 7 ZS: 97 6 ZM: 97 85 Digit stream (P): 9 7 9 1 9 7 8 1 9 7 7 9 7 6 9 7 8 5 Step 3 -- Stave 5: Add keyword K1 (SEDATION = 9 5 4 1 0 9 5 4, cycling) K1 cycling for 18 digits: 9 5 4 1 0 9 5 4 | 9 5 4 1 0 9 5 4 | 9 5 P: 9 7 9 1 9 7 8 1 9 7 7 9 7 6 9 7 8 5 K1: 9 5 4 1 0 9 5 4 9 5 4 1 0 9 5 4 9 5 After Stave 5 (I = (P+K1) mod 10): 9+9=8 | 7+5=2 | 9+4=3 | 1+1=2 | 9+0=9 | 7+9=6 | 8+5=3 | 1+4=5 9+9=8 | 7+5=2 | 7+4=1 | 9+1=0 | 7+0=7 | 6+9=5 | 9+5=4 | 7+4=1 8+9=7 | 5+5=0 Intermediate (I): 8 2 3 2 9 6 3 5 8 2 1 0 7 5 4 1 7 0 Step 4 -- Add OTP key K2: I: 8 2 3 2 9 6 3 5 8 2 1 0 7 5 4 1 7 0 K2: 8 2 4 7 3 1 0 9 5 6 2 4 8 3 7 0 5 1 8+8=6 | 2+2=4 | 3+4=7 | 2+7=9 | 9+3=2 | 6+1=7 | 3+0=3 | 5+9=4 8+5=3 | 2+6=8 | 1+2=3 | 0+4=4 | 7+8=5 | 5+3=8 | 4+7=1 | 1+0=1 7+5=2 | 0+1=1 Ciphertext: 6 4 7 9 2 7 3 4 3 8 3 4 5 8 1 1 2 1 [Continue null fill to 200] Groups: 64792 73438 34581 121__ [null fill] Verify: 64792
Complete a full encrypt cycle (Layers 1-4) on the following message, rolling your own key digits. Time yourself from first roll to verify strip.
Message: MEET TODAY PRIMARY LOCATION ZERO EIGHT HUNDRED CONFIRMED
Benchmark targets:
First attempt: under 20 minutes After 2 weeks: under 10 minutes Operational minimum: under 6 minutes
Note which step consumes most time. That step determines your next practice priority. Most operators find RADIX encoding is fastest after 1-2 weeks, and the mod-10 addition is the remaining bottleneck -- which means the table lookup needs more drilling.
Complete a full encrypt-decrypt cycle with a partner using two independently prepared pad sheets. The sender encrypts the message below and transmits only the ciphertext groups. The receiver decrypts and recovers the plaintext. Compare results. Any discrepancy indicates a transcription error -- locate it using the group where decryption first produces nonsense.
Message (sender composes, does not reveal to receiver until after): [Choose any operational message using codebook and spelled content, 10-50 characters, including at least one numeral]
After successful completion, swap roles and repeat with a new pad sheet and a receiver-composed message.
Vernier with mod-10 OTP has highly localized error propagation. A single corrupted digit in the ciphertext corrupts exactly the characters whose RADIX codes include that digit position. Because many characters encode as 1-digit primary codes, a single error often corrupts only one or two characters.
This is significantly better than shift-register ciphers where one error corrupts the entire subsequent message. Error localization is one of the practical arguments for mod-10 OTP over more complex stateful systems.
During decryption, errors manifest as: 1. Invalid RADIX codes A sequence that violates the parsing rule. Example: 8 followed by nothing (truncated group). Indicates: transmission error in that 5-digit group. 2. Nonsense decoded text Valid RADIX but incoherent plaintext. Example: QXZJV -- unlikely in operational vocabulary. Indicates: error has produced valid-looking but wrong codes. 3. ZM never appearing Null fill continues indefinitely without end marker. Indicates: error before or at ZM position, or wrong pad sheet.
1. Note which 5-digit group first produces nonsense. 2. Request retransmission of that group and the two following. 3. If retransmission is not possible: -- Try decoding the group with +1 and -1 on each digit in sequence -- Single-digit errors in 1-digit primary codes are likely -- The context of surrounding decoded text constrains possibilities 4. If more than one error: request full retransmission. 5. Never send "did not understand" -- send "request group XX and following."
PRIMARY (single digit, 0-7): E=0 T=1 A=2 O=3 I=4 N=5 S=6 H=7 "Eat Tangerines And Oranges In Nice Spanish Hotels" SECONDARY (prefix 8, two digits, 80-89): R=80 D=81 L=82 C=83 U=84 M=85 W=86 F=87 G=88 Y=89 "Really Dull Lessons Can Undermine Many Willing Fields, Gaining Yesterday" TERTIARY (prefix 9, two digits, 90-98): P=90 B=91 V=92 K=93 J=94 X=95 Q=96 Z=97 .=98 NUMERALS (prefix 99, three digits, 990-999): 0=990 1=991 2=992 3=993 4=994 5=995 6=996 7=997 8=998 9=999 Rule: 990 + digit
d in 0-7 --> complete. One digit. d = 8 --> read one more. 8x table. d = 9, x 0-8 --> two digits. 9x table. d = 9, x = 9 --> read one more. Numeral = that digit.
ZA=CONFIRMED ZB=ABORT ZC=PROCEED ZD=DANGER ZE=DELAY ZF=COMPLETE ZG=CONFIRM LAST ZH=EMERGENCY ZI=NO CHANGE ZJ=AS PLANNED ZK=PRI LOC ZL=ALT LOC ZM=END MSG* ZN=MEET ZO=TOMORROW ZP=TODAY ZQ=LOC FLWS ZR=TIME FLWS ZS=SW ALT ZT=DEAD DROP ZU=SURV DET ZV=CLEAR ZW=RCVD+UNDSTD ZX=DOC FLWS ZY=COORD FLWS ZZ=NAME FLWS * ZM is reserved as end-of-message marker. Never reassign. Routine circuit: ZI ZM = nothing to report ZW ZM = received and understood ZG ZM = confirm last message received correctly
Class S -- SHORT prefix 00 50 digits (10 groups) 0-44 content digits Class M -- MEDIUM prefix 01 100 digits (20 groups) 45-94 content digits Class L -- LONG prefix 02 200 digits (40 groups) 95-194 content digits Structure: [OFFSET 2][SALT 4][FILL][CONTENT][ZM 6][PADDING] = 25, 50, or 100 Prefix is encrypted as part of the block. Decision: encode full message first, count digits, select class, then roll.
ENCRYPT C = (P + K) mod 10 DECRYPT P = (C - K + 10) mod 10 WITH KEYWORD: ENCRYPT I = (P + K1) mod 10 --> C = (I + K2) mod 10 DECRYPT I = (C - K2 + 10) mod 10 --> P = (I - K1 + 10) mod 10
BEFORE: [ ] Pad ID confirmed with counterpart [ ] Verify strip from last exchange matches [ ] Hard surface prepared, no paper below [ ] Water container nearby ENCRYPT: [ ] Codebook applied, raw text in Q2 upper [ ] RADIX encoded in Q2 lower, ZM appended [ ] Digit count done, block class selected (S/M/L) [ ] Class prefix (00/01/02) prepended in Q2 [ ] Key block rolled for class size (50/100/200) in Q1 [ ] Keyword applied if using Stave 5 [ ] Mod-10 addition to Q3, null fill to block size [ ] Verify strip recorded AFTER: [ ] Destroy pad sheet immediately [ ] No legible fragment confirmed