24字符外星密码体系(v ↔ U)——从手写防呆到8字节无损压缩的全栈设计
摘要
16 字符体系与机器同构,但对人类手写并不友好。本文提出 24 字符外星密码体系:剔除所有易混淆字形(0/O、1/I/L、2/Z、5/S、6/G 之一、8/B 之一、V),并引入关键约定 v ↔ U:手写的 v、V、u 在输入时统一归一为 U。我们比较两种 8 字节无损压缩方案:每符号 5 bit 的定长方案可存 12 个符号;24 进制整数打包方案利用 \(24^{13}<2^{64}<24^{14}\) 可存 13 个符号。最后给出打分器、极坐标分级与 Python 全栈示例。
Abstract
We propose a 24-symbol alphabet that removes visually ambiguous glyphs and normalizes handwritten v to U. Fixed 5-bit coding stores 12 symbols in a uint64, while radix-24 packing stores 13, since \(24^{13}<2^{64}<24^{14}\). A scorer, polar grading and Python code are given.
第一部分:字母表表格
\(\Sigma_{24}=\) 3479ABCDEFGHJKMNPQRTUWXY,按 ASCII 顺序赋值:
| 值 | 符号 | 值 | 符号 | 值 | 符号 | 值 | 符号 |
|---|---|---|---|---|---|---|---|
| 0 | 3 | 6 | C | 12 | J | 18 | R |
| 1 | 4 | 7 | D | 13 | K | 19 | T |
| 2 | 7 | 8 | E | 14 | M | 20 | U(← v/V/u) |
| 3 | 9 | 9 | F | 15 | N | 21 | W |
| 4 | A | 10 | G | 16 | P | 22 | X |
| 5 | B | 11 | H | 17 | Q | 23 | Y |
| 被剔除 | 原因 | 输入时处理 |
|---|---|---|
| 0, O | 互相混淆 | 拒绝 |
| 1, I, L | 竖线类字形 | 拒绝 |
| 2, Z | 手写相近 | 拒绝 |
| 5, S | 手写相近 | 拒绝 |
| 6(保留 G) | 6/G/b 相近 | 拒绝 |
| 8(保留 B) | 8/B 相近 | 拒绝 |
| V | 手写 v 与 U/u 难辨 | 归一为 U |
第二部分:v ↔ U 设计说明(手写防呆)
手写时 v 的底部常被写圆,u 的顶部常不闭合,二者在纸面上几乎不可区分。与其让用户猜测,不如在设计上消除歧义:字母表中只保留 U,并定义归一化映射
$$\nu:\ \{v,V,u,U\}\mapsto U,\qquad \nu(c)=\operatorname{upper}(c)\ \text{(其他符号)}$$于是无论用户把 U 读成 “v” 还是 “u”,输入结果都相同——这就是 “v ↔ U” 的含义:写作两种、存储一种。由于 V 本就不在字母表中,归一化不会造成碰撞,密码空间大小不变。这一“防呆”(poka-yoke)原则同样体现在大小写不敏感上:所有符号统一为大写存储。
第三部分:5-bit 方案与 24 进制打包公式
单符号信息量 \(\log_2 24\approx 4.585\) bit,不是整数,因此存在两种 uint64 打包思路:
3.1 定长 5-bit 方案
$$W_5(s)=\bigvee_{i=0}^{n-1}\big(v(c_i)\ll 5(n-1-i)\big),\qquad 5n\le 64\Rightarrow n_{\max}=12$$12 × 5 = 60 bit,余 4 bit 可作长度或版本标记;每个符号浪费 \(5-4.585=0.415\) bit(32 个码位只用 24 个)。13 × 5 = 65 bit 已超出 64 位。
3.2 24 进制整数打包
$$W_{24}(s)=\sum_{i=0}^{n-1} v(c_i)\cdot 24^{\,n-1-i}\quad(\text{Horner: } W\leftarrow 24W+v(c_i))$$精确计算:
$$24^{13}=876\,488\,338\,465\,357\,824\ <\ 2^{64}=18\,446\,744\,073\,709\,551\,616\ <\ 24^{14}=21\,035\,720\,123\,168\,587\,776$$因此 13 个符号可无损放入一个 uint64,占用 \(13\log_2 24\approx 59.60\) bit;\(2^{64}/24^{13}\approx 21.05\),剩余容量足以再附带一个 0–20 的标签值(如长度或校验)。
| 方案 | 最大符号数 | 有效熵 | 编码代价 |
|---|---|---|---|
| ASCII | 8 | 36.7 bit | 无 |
| 5-bit 定长 | 12 | 55.0 bit | 移位/掩码 |
| 24 进制 | 13 | 59.6 bit | 乘法/除法 |
第四部分:打分器
$$S(s)=n\log_2 24-P_{\text{rep}}-P_{\text{seq}}-P_{\text{dict}},\qquad S\in[0,\,13\log_2 24]$$| 扣分项 | 判定 | 扣分 |
|---|---|---|
| 重复 | 相邻符号相同 | 3 bit/处 |
| 顺序 | 相邻值差 ±1(如 ABC、DCB) | 2 bit/处 |
| 词典 | 含常见词片段(如 MAX、KEY、CAT) | 6 bit/个 |
| 长度不足 | \(n<10\) | 分级上限为“白” |
第五部分:极坐标分级
将归一化分数映射为半径,将“数字/字母”与“前半表/后半表”的偏好映射为角度:
$$r=\left\lfloor\frac{S}{13\log_2 24}\cdot 255\right\rfloor,\quad \theta=\left\lfloor\frac{\arctan2(y,x)+\pi}{2\pi}\cdot 255\right\rfloor,\quad \text{Level}_{24}=(r\ll 8)\,|\,\theta$$其中 \(x=\) 数字占比 \(-0.5\),\(y=\) 值 \(\ge 12\) 的符号占比 \(-0.5\)。
| \(r\) 区间 | 等级 | 建议 |
|---|---|---|
| 0–95 | 白 | 仅用于公开/低价值场景 |
| 96–191 | 灰 | 日常账户 |
| 192–255 | 黑 | 主密钥、离线备份 |
第六部分:Python 代码示例
import math
ALPHABET = "3479ABCDEFGHJKMNPQRTUWXY"
assert len(ALPHABET) == 24
IDX = {c: i for i, c in enumerate(ALPHABET)}
MAX_LEN = 13 # 24**13 < 2**64 < 24**14
H = math.log2(24)
def normalize(s: str) -> str:
s = s.strip().upper().replace("V", "U") # v <-> U 防呆
bad = [c for c in s if c not in IDX]
if bad:
raise ValueError(f"非法符号: {bad}")
return s
def pack24(s: str) -> int:
s = normalize(s)
assert 1 <= len(s) <= MAX_LEN
w = 0
for c in s:
w = w * 24 + IDX[c]
return w * 21 + len(s) # 余量存长度:24**13 * 21 < 2**64
def unpack24(w: int) -> str:
w, n = divmod(w, 21)
out = []
for _ in range(n):
w, d = divmod(w, 24)
out.append(ALPHABET[d])
return "".join(reversed(out))
def pack5(s: str) -> int:
s = normalize(s)
assert len(s) <= 12
w = 0
for c in s:
w = (w << 5) | IDX[c]
return (w << 4) | len(s) # 60 bit 数据 + 4 bit 长度
def score(s: str) -> float:
s = normalize(s)
v = [IDX[c] for c in s]
pen = sum(3 if a == b else 2 if abs(a - b) == 1 else 0
for a, b in zip(v, v[1:]))
pen += 6 * sum(w in s for w in ("MAX", "KEY", "CAT"))
return max(0.0, len(s) * H - pen)
def level24(s: str) -> int:
s = normalize(s)
r = int(min(score(s) / (MAX_LEN * H), 1) * 255)
x = sum(c.isdigit() for c in s) / len(s) - 0.5
y = sum(IDX[c] >= 12 for c in s) / len(s) - 0.5
t = int((math.atan2(y, x) + math.pi) / (2 * math.pi) * 255)
return (r << 8) | t
if __name__ == "__main__":
p = "k7vMQ3xa9HdTe" # 手写输入,v 自动变 U
w = pack24(p)
assert w < 2**64 and unpack24(w) == normalize(p)
print(normalize(p), w, round(score(p), 2), hex(level24(p)))
结论
24 字符体系以放弃移位对齐(需乘除法打包)为代价,在单符号熵(4.585 bit,高于 16 字符体系的 4 bit)上反而更优,同时换来人类手写与口述的鲁棒性;v ↔ U 归一化消除了最常见的手写歧义;24 进制打包让 8 字节容纳 13 个符号并附带长度,实现无损、定长、可索引的全栈设计。
参考文献(示意)
- Gray Password GP-000101 / GP-000102 / GP-16(本站前文)。
- Crockford, D. Base32 Encoding(易混淆字符剔除思想)。
- Shingo, S. Poka-yoke 防呆原则。