
一、背景、来历与用途先搞清它们从哪来学一个密码如果只背规则很快就会忘。所以先花三分钟把时间线捋清楚。1.1 背景单表替换的致命伤最早的替换密码凯撒密码、仿射密码都是单表替换每个字母固定换成另一个字母。它的问题很直白一个字母永远映射到同一个字母字母频率被完整保留英文里E / T / A / O占比极高攻击者拿一段密文做频率统计再配合TH / HE / IN这类常见字母组合很快就能还原明文。只要统计规律不变替换关系就一定能被反推。这就是 19 世纪密码学家必须解决的痛点。1.2 中间站Playfair 的突破与它的两个麻烦1854 年出现的Playfair 密码换了个角度一次加密两个字母digraph让单个字母的频率统计失真。它只用一个5×5 方阵规则三条情形规则两字母同一行各自右移一格到行尾回卷到行首两字母同一列各自下移一格到底部回卷到顶部两字母既不同行也不同列构成矩形的对角互换列Playfair 一度是英军的标准战地密码布尔战争、一战中都有使用但它有两个让人难受的地方规则分叉三种情况要分别处理战地手算时容易看错到底同不同行/同不同列必须处理双字母明文里出现LL、SS这类连续相同字母时同一格无法构成矩形规则失效必须先插入填充字符如X把它们拆开解密时还要再把填充字符删掉。1.3 来历Delastelle 的多用几个方阵1899 年前后法国密码学家Félix Delastelle1840–1902给出了一个更优雅的解法。他的代表作是 1902 年出版的《Traité élémentaire de cryptographie》密码学基础教程四方密码正是这本书里最有名的发明之一二方密码同样属于他这一套体系在历史上还有个外号叫double Playfair双 Playfair。他的核心思路只有一句话与其让一个方阵承担所有情况不如用多个方阵分工——第一个字母去 A 阵定位第二个字母去 B 阵定位密文字母则从另外的方阵里交叉取出来。这样一来特殊分支消失了只剩一条矩形规则两个明文字母位置相同时双字母取出的两个密文字母来自不同方阵天然不会退化成同一个字母——双字母问题自动消失。按使用方阵的数量就有了本文的两个主角名称英文方阵数量方阵排列二方密码Two-square cipher2左右并排或上下堆叠四方密码Four-square cipher42×2 田字格1.4 用途它们当年用来干什么当年19 世纪末二战战地手工加解密。这类密码只要一支笔、一张纸、一张写好的方阵卡片不依赖任何机器非常适合前线电台、侦察分队、情报员在野外的即时通信。军事通信。二战期间德军使用的Doppelkastenschlüssel双密码卡就是二方密码这一类的实现对当时的通信兵来说它比 Enigma 更随手可用——虽然强度远不如机器密码。低强度、短时通信。情报传递、一次性暗号这类只要撑过很短一段时间的场合用它换来的便利性很划算。现在教学。密码学课程用它讲多字母替换密钥空间统计攻击这几个核心概念规则清晰、可以完全手算又能清楚展示设计上的小改动会带来多大的强度差异。CTF 与谜题。CTF 的古典密码题、报纸上的字谜栏目、密码爱好者社区里二方/四方密码都是常客。复古游戏与兴趣项目。很多解谜游戏、密码学小工具里都能看到它的实现。但必须强调它们都是古典密码在现代计算能力面前不能保护任何真实数据。学习它们的价值在于理解原理和设计取舍而不是拿来加密你的文件。二、地基25 个字母的 5×5 方阵2.1 字母表怎么放进方阵英文字母有 26 个塞不进 5×525 的格子所以传统做法是合并两个字母最常用的是I和J合并本文全部采用这一种遇到J一律当I处理也可以合并C/K、V/W、I/Y等只要双方约定一致即可。因此方阵里用的是这 25 个字母A B C D E F G H I K L M N O P Q R S T U V W X Y Z注意没有J字母表在I之后直接跳到K。2.2 标准方阵明文方阵最简单的排法就是按字母表顺序从左到右、从上到下填满这也是四方密码里明文方阵的固定形态行\列012340ABCDE1FGHIK2LMNOP3QRSTU4VWXYZ本文所有方阵的行、列都从 0 开始编号方便和代码里的数组下标直接对应。2.3 关键字方阵怎么造三步光用标准方阵就没有密钥了所以至少要有一些方阵是用关键字打乱的。生成规则三步关键字转大写J换成I从左到右写下关键字里的字母重复出现的只保留第一次关键字写完后把字母表中还没出现过的字母按顺序补在后面填满 25 格。举个例子关键字是EXAMPLE去重后的关键字E X A M P L6 个字母剩余字母按序补B C D F G H I K N O Q R S T U V W Y Z拼起来E X A M P L B C D F G H I K N O Q R S T U V W Y Z排成方阵就是行\列012340EXAMP1LBCDF2GHIKN3OQRST4UVWYZ2.4 本文要用到的 4 个关键字方阵为了后面举例这里一次性把四个方阵都造好都用上面的三步规则建议你自己动手验证一遍方阵 A关键字EXAMPLE行\列012340EXAMP1LBCDF2GHIKN3OQRST4UVWYZ方阵 B关键字KEYWORD行\列012340KEYWO1RDABC2FGHIL3MNPQS4TUVXZ方阵 C关键字PLAYFAIR行\列012340PLAYF1IRBCD2EGHKM3NOQST4UVWXZ方阵 D关键字CIPHER行\列012340CIPHE1RABDF2GKLMN3OQSTU4VWXYZ三、四方密码Four-square3.1 布局四个方阵摆成田字格左上角和右下角放同一个标准明文方阵右上角和左下角放两个关键字方阵-------------------------------------- | PLAIN | CIPHER-1 | | 0 1 2 3 4 | 0 1 2 3 4 | | 0 A B C D E | 0 E X A M P | | 1 F G H I K | 1 L B C D F | | 2 L M N O P | 2 G H I K N | | 3 Q R S T U | 3 O Q R S T | | 4 V W X Y Z | 4 U V W Y Z | -------------------------------------- -------------------------------------- | CIPHER-2 | PLAIN | | 0 1 2 3 4 | 0 1 2 3 4 | | 0 K E Y W O | 0 A B C D E | | 1 R D A B C | 1 F G H I K | | 2 F G H I L | 2 L M N O P | | 3 M N P Q S | 3 Q R S T U | | 4 T U V X Z | 4 V W X Y Z | --------------------------------------对应关系位置名称方阵来源左上PLAIN2.2 节的标准字母表方阵右上CIPHER-1方阵 A关键字EXAMPLE左下CIPHER-2方阵 B关键字KEYWORD右下PLAIN与左上完全相同的标准字母表方阵关键设计明文方阵是同一个但两个密文方阵用的是不同的关键字。这正是四方密码强度高于二方密码的原因4.5 节会展开讲。3.2 加密规则一个矩形两次交叉取字把明文按两两一组切开做成HE LL OW OR LD这样。对每一组甲乙甲第 1 个字母在左上明文方阵里定位得到它的行号 r₁乙第 2 个字母在右下明文方阵里定位得到它的列号 c₂到右上密文方阵取第r₁行、第c₂列的那个字母 —— 这是密文第 1 个字母反过来用乙的行号 r₂和甲的列号 c₁到左下密文方阵取第r₂行第c₁列的字母 —— 这是密文第 2 个字母。一句话口诀甲的行 乙的列 → 右上取一乙的行 甲的列 → 左下取二。几何上这相当于把左上和右下这两个明文字母看成一个矩形的对角顶点然后去右上、左下这两个另外的方阵里读另外两个角上的字母——这才是四方名字的由来四个角各用一次。3.3 逐步走一遍加密HELLOWORLD预处理转大写、去空格 →HELLOWORLD10 个字母偶数无需补字符。切分为HE/LL/OW/OR/LD。第一组HEH在左上明文方阵第1行、第2列E在右下明文方阵第 0 行、第4列右上阵EXAMPLE第1行第4列 →L B C D F里下标 4 的字母 F左下阵KEYWORD第0行第2列 →K E Y W O里下标 2 的字母 Y得到密文对FY。其余各对同理完整过程如下表明文对甲的位置左上乙的位置右下密文 1 右上甲行密文 2 左下乙行密文对HEH(1,2)E(0,4)右上1 F左下0 YFYLLL(2,0)L(2,0)右上2 G左下2 FGFOWO(2,3)W(4,1)右上2 H左下4 XHXORO(2,3)R(3,1)右上2 H左下3 QHQLDL(2,0)D(0,3)右上2 K左下0 KKK密文结果FYGFHXHQKK注意第二组LL两个字母在明文方阵里位置完全相同但因为一个去右上、一个去左下取值结果是GF而不是GG。四方密码因此完全不需要像 Playfair 那样插填充字符处理双字母。3.4 解密把规则倒过来读解密时甲、乙的身份变成密文第 1、2 个字母查找的方阵也跟着换密文第 1 个字母在右上密文方阵里定位 → 得到 (r₁, c₂)密文第 2 个字母在左下密文方阵里定位 → 得到 (r₂, c₁)明文字母 1 左上明文方阵r₁明文字母 2 右下明文方阵r₂。验证密文对FYF在右上阵EXAMPLE→ 第 1 行第 4 列 ⇒ r₁1、c₂4Y在左下阵KEYWORD→ 第 0 行第 2 列 ⇒ r₂0、c₁2明文 1 左上1 H明文 2 右下0 E⇒ 还原出HE✓整串FYGFHXHQKK解回来就是HELLOWORLD与加密完全对称。四、二方密码Two-square4.1 布局两个方阵两种摆法二方密码只用两个方阵有两种变体横向二方horizontal两阵左右并排 ——第 1 个字母查左阵第 2 个字母查右阵纵向二方vertical两阵上下堆叠 ——第 1 个字母查上阵第 2 个字母查下阵。两种变体的交叉取值逻辑完全一样本文以横向为例-------------------------------------- | LEFT | RIGHT | | 0 1 2 3 4 | 0 1 2 3 4 | | 0 P L A Y F | 0 C I P H E | | 1 I R B C D | 1 R A B D F | | 2 E G H K M | 2 G K L M N | | 3 N O Q S T | 3 O Q S T U | | 4 U V W X Z | 4 V W X Y Z | --------------------------------------左阵 方阵 C关键字PLAYFAIR右阵 方阵 D关键字CIPHER。与四方密码不同这里两个方阵都必须是完整的 25 个字母因为它们各自被指定承担第 1 / 第 2 个字母的定位任务不存在二选一的歧义。4.2 加密规则左边取行右边取列对明文的每一组甲乙甲在左阵定位 → 得到行号 r₁、列号 c₁乙在右阵定位 → 得到行号 r₂、列号 c₂密文 1 左阵r₁甲的行 乙的列回到左阵取字密文 2 右阵r₂乙的行 甲的列回到右阵取字。口诀甲的行配乙的列回左阵取第一个乙的行配甲的列回右阵取第二个。可以把它想象成两个方阵被拼接成一个 5×10 的大矩形而密文就是同一行里两个字母的交叉投影。4.3 逐步走一遍加密ATTACKATDAWN明文转大写去空格 →ATTACKATDAWN12 个字母偶数。切分为AT/TA/CK/AT/DA/WN。明文对甲的位置左阵乙的位置右阵密文 1 左甲行密文 2 右乙行密文对ATA(0,2)T(3,3)左0 Y右3 SYSTAT(3,4)A(1,1)左3 O右1 FOFCKC(1,3)K(2,1)左1 R右2 MRMATA(0,2)T(3,3)左0 Y右3 SYSDAD(1,4)A(1,1)左1 R右1 FRFWNW(4,2)N(2,4)左4 Z右2 LZL密文结果YSOFRMYSRFZL拿第一组对照着算一遍A在左阵第 0 行第 2 列T在右阵第 3 行第 3 列。密文 1 左阵第 0 行第 3 列 P L A Y F里下标 3 的字母 Y密文 2 右阵第 3 行第 2 列 O Q S T U里下标 2 的字母 S得到YS✓4.4 解密同样的交叉反向查找密文第 1 个字母在左阵定位 → 得到 (r₁, c₂)密文第 2 个字母在右阵定位 → 得到 (r₂, c₁)明文字母 1 左阵r₁明文字母 2 右阵r₂。验证密文对YSY在左阵第 0 行第 3 列 ⇒ r₁0、c₂3S在右阵第 3 行第 2 列 ⇒ r₂3、c₁2。明文 1 左阵0 A明文 2 右阵3 T⇒ 还原出AT✓4.5 二方密码的两个硬伤硬伤 1同列退化密文可能等于明文。横向二方里如果甲在左阵的列号恰好等于乙在右阵的列号那么密文 1 左阵[r₁][c₂] 左阵[r₁][c₁] 甲 列号相同取回的就是自己 密文 2 右阵[r₂][c₁] 右阵[r₂][c₂] 乙整组字母原封不动地抄进了密文。用本文的PLAYFAIR / CIPHER方阵试试ABA在左阵第 0 行第 2 列B在右阵第 1 行第 2 列列号相同 → 密文 AB。在这组方阵下5 个列号 × 5 × 5 125 种字母组合都会出现这种原地不动的情况。而 Playfair 遇到类似情况至少还有右移一格的兜底规则二方密码什么都没有——明文信息直接泄漏。硬伤 2行信息暴露方阵结构可被统计出来。再看加密公式密文 1 左阵r₁它必然落在左阵的第 r₁ 行上。也就是说密文字母 1 所在的行就是明文字母 1 在左阵里所在的行。每行只有 5 个字母攻击者只要统计哪些字母会互相映射就能先把 25 个字母的行划分分成 5 组、每组 5 个字母恢复出来密钥空间被砍掉一大截行内 5 个字母的排列虽然还剩但配合频率分析用不了太长密文就能解出来。四方密码没有这个问题密文 1 取自右上阵的打乱位置密文与明文的位置关系被彻底搅乱强度明显更高。五、完整 Python 实现下面这份代码可以直接复制运行把上面所有例子跑一遍。# -*- coding: utf-8 -*- 二方密码 / 四方密码 教学实现方阵生成 加解密 演示 ALPHABET ABCDEFGHIKLMNOPQRSTUVWXYZ # 25 个字母I/J 合并所以没有 J PAD X # 明文长度为奇数时的填充字符 def build_square(keyword): 按关键字生成 5x5 方阵关键字去重后在前其余字母按序补全。 letters [] for ch in keyword.upper().replace(J, I): if ch.isalpha() and ch not in letters: letters.append(ch) for ch in ALPHABET: if ch not in letters: letters.append(ch) return [letters[i * 5:(i 1) * 5] for i in range(5)] def locate(square, ch): 返回字母在方阵中的 (行, 列)行列从 0 开始。 ch ch.upper().replace(J, I) for r, row in enumerate(square): if ch in row: return r, row.index(ch) raise ValueError(f字母 {ch} 不在方阵中) def normalize(text): 预处理转大写、剔除字母以外的字符、J - I、奇数长度补 PAD。 text .join(c for c in text.upper().replace(J, I) if c.isalpha()) return text PAD if len(text) % 2 else text def to_pairs(text): 两两分组HELLOWORLD - [HE, LL, OW, OR, LD] return [text[i:i 2] for i in range(0, len(text), 2)] class FourSquareCipher: 四方密码左上、右下为同一个明文方阵右上、左下各一个关键字方阵。 def __init__(self, key1, key2): self.plain build_square() self.top_right build_square(key1) self.bottom_left build_square(key2) def encrypt(self, text): out [] for a, b in to_pairs(normalize(text)): r1, c1 locate(self.plain, a) # 甲的行、列 r2, c2 locate(self.plain, b) # 乙的行、列 out.append(self.top_right[r1][c2]) # 甲的行 乙的列 - 右上 out.append(self.bottom_left[r2][c1]) # 乙的行 甲的列 - 左下 return .join(out) def decrypt(self, text): out [] for a, b in to_pairs(normalize(text)): r1, c2 locate(self.top_right, a) # 反查拿到 甲行 / 乙列 r2, c1 locate(self.bottom_left, b) # 反查拿到 乙行 / 甲列 out.append(self.plain[r1][c1]) out.append(self.plain[r2][c2]) return .join(out) class TwoSquareCipher: 二方密码横向左阵管第 1 个字母右阵管第 2 个字母。 def __init__(self, key1, key2): self.left build_square(key1) self.right build_square(key2) def encrypt(self, text): out [] for a, b in to_pairs(normalize(text)): r1, c1 locate(self.left, a) r2, c2 locate(self.right, b) out.append(self.left[r1][c2]) # 左阵甲行 乙列 out.append(self.right[r2][c1]) # 右阵乙行 甲列 return .join(out) def decrypt(self, text): out [] for a, b in to_pairs(normalize(text)): r1, c2 locate(self.left, a) r2, c1 locate(self.right, b) out.append(self.left[r1][c1]) out.append(self.right[r2][c2]) return .join(out) def dump(square, title): 把方阵打印成和文中一致的形状。 print(f[{title}]) print( .join(str(c) for c in range(5))) for r, row in enumerate(square): print(f {r} .join(row)) if __name__ __main__: dump(build_square(), 左上/右下明文方阵) dump(build_square(EXAMPLE), 右上密文方阵key EXAMPLE) dump(build_square(KEYWORD), 左下密文方阵key KEYWORD) f4 FourSquareCipher(EXAMPLE, KEYWORD) ct f4.encrypt(HELLO WORLD) print(四方密码 HELLO WORLD -, ct, - 解密, f4.decrypt(ct)) f2 TwoSquareCipher(PLAYFAIR, CIPHER) ct2 f2.encrypt(ATTACK AT DAWN) print(二方密码 ATTACK AT DAWN -, ct2, - 解密, f2.decrypt(ct2)) print(退化演示 AB -, f2.encrypt(AB)) # 同列退化密文 明文运行结果和文中手算完全一致[左上/右下明文方阵] 0 1 2 3 4 0 A B C D E 1 F G H I K 2 L M N O P 3 Q R S T U 4 V W X Y Z [右上密文方阵key EXAMPLE] 0 1 2 3 4 0 E X A M P 1 L B C D F 2 G H I K N 3 O Q R S T 4 U V W Y Z [左下密文方阵key KEYWORD] 0 1 2 3 4 0 K E Y W O 1 R D A B C 2 F G H I L 3 M N P Q S 4 T U V X Z 四方密码 HELLO WORLD - FYGFHXHQKK - 解密 HELLOWORLD 二方密码 ATTACK AT DAWN - YSOFRMYSRFZL - 解密 ATTACKATDAWN 退化演示 AB - AB5.1 几个实现细节normalize里的PAD二方、四方都按双字母加密明文长度必须是偶数。长度为奇数时补一个X也可以用Q、Z。四方密码虽然不怕双字母但仍然要处理奇数长度。J的处理加密前J - I所以解密结果里不会出现J需要按约定人工还原。关键字要够长、够随机关键字越短方阵越接近标准字母表两个关键字不能相同否则四方密码的右上、左下阵一致双字母又会退化成相同密文比如LL - GG可被直接识别。只处理英文字母数字、标点、中文在上面的实现里会被直接丢掉。工程上通常先做编码/数字化处理而不是硬塞进 25 格。六、对比二方、四方与 Playfair维度Playfair二方密码四方密码方阵数量124加密单位双字母双字母双字母核心规则同行右移 / 同列下移 / 矩形换列甲行配乙列、乙行配甲列交叉取值甲行配乙列 → 右上乙行配甲列 → 左下规则分支3 条手算易错1 条1 条双字母如LL必须插填充字符拆开不需要特殊处理不需要特殊处理同列退化风险有兜底规则无兜底密文 明文不会退化结构弱点单字母频率失真但双字母频率仍在行信息暴露、同列退化明显强于前两者手算友好度中高高现代安全性不安全不安全不安全但三者中最强强度关系一句话四方密码 二方密码 Playfair但它们都属于古典密码在现代标准下全部不安全。6.1 四方密码怎么被破解了解即可四方密码是确定性的双字母替换同样的字母对永远映射到同样的密文对。这意味着明文里重复出现的组合如ATTACKATDAWN里的AT两次都变成YS会在密文中暴露结构密文的双字母频率仍然保留了大量统计特征只要密文足够长几百个字符以上就可以用频率分析 爬山 / 模拟退火搜索自动还原两个关键字已有公开工具能做到这一点。二方密码更弱它的行信息泄漏让攻击者能先把方阵的行分组恢复出来剩下的搜索空间小得多用已知明文攻击更是几步就能推完。七、动手练习附答案建议先把上面的方阵抄在纸上每题都真的用手算一遍再对答案。练习 1四方密码加密关键字EXAMPLE/KEYWORD方阵用 3.1 节的布局加密明文SECRET MESSAGE。练习 2四方密码解密同样方阵解密NEMSNEMMEWWG。练习 3二方密码加密关键字PLAYFAIR/CIPHER横向二方加密明文GOOD LUCK。练习 4二方密码解密同样方阵解密ZPPISKHV。练习 5思考题在本文的PLAYFAIR / CIPHER方阵下为什么AB加密后还是AB这一类原地不动的组合一共有多少种参考答案练习 1先预处理——去空格得SECRETMESSAGE13 个字母奇数→ 补X得SECRETMESSAGEX。切分并查表明文对密文 1 右上甲行密文 2 左下乙行密文对SE右上3 T左下0 YTYCR右上0 X左下3 PXPET右上0 M左下3 SMSME右上2 N左下0 ENESS右上3 R左下3 PRPAG右上0 X左下1 RXREX右上0 A左下4 ZAZ密文 TYXPMSNERPXRAZ。注意SS变成了RP两个字母完全不同——这就是四方密码相对 Playfair 的优势。练习 2NEMSNEMMEWWG切分为NE/MS/NE/MM/EW/WG逐对反查得ME/ET/ME/AT/DA/WN即MEETMEATDAWNmeet me at dawn。练习 3GOODLUCK切分为GO/OD/LU/CK明文对密文 1 左甲行密文 2 右乙行密文对GO左2 E右3 QEQOD左3 S右1 ASALU左0 F右3 QFQCK左1 R右2 MRM密文 EQSAFQRM。练习 4ZPPISKHV切分为ZP/PI/SK/HV逐对反查得WE/LC/OM/EX即WELCOMEX去掉末尾填充字符X得明文WELCOME。练习 5A在左阵的列号是 2B在右阵的列号也是 2两个字母列号相同矩形退化成一条竖线交叉取值取回的就是自己所以密文 明文。这类组合的数量是 5列× 5左阵每列 5 个字母× 5右阵每列 5 个字母125 种。八、总结背景与来历单表替换打不赢频率分析 → 1854 年 Playfair 用一次两个字母破局但规则分叉、还怕双字母 → 19 世纪末 Delastelle 用多个方阵分工给出更干净的答案这就是二方密码与四方密码。当年用途它们是只靠纸笔就能完成的战地手工密码用在军事通信、情报传递这类短时低强度场合今天主要活在教学、CTF 与谜题里绝不能保护真实数据。方阵是地基5×5 装 25 个字母I/J合并标准方阵按字母序填关键字方阵按关键字去重 剩余补全三步生成。先把方阵画对加解密就只剩查表。二方密码两阵左右并排甲行配乙列、乙行配甲列交叉取值。规则只有一条、手算舒服但同列会原地退化且密文字母的行号暴露了明文字母的行号强度最弱。四方密码四阵摆成田字格左上右下是同一个明文方阵右上左下各用一个关键字方阵加密甲行乙列去右上、乙行甲列去左下解密反向查。结构对称、天然不吃双字母是三者中最强的。都不安全它们只是把单字母频率问题升级成了双字母频率问题。学习它们的价值在于体会多表/多字母替换如何用空间换强度以及少一条分支规则、多一个方阵安全性到底差多少。如果这篇把二方/四方密码的方阵算明白了欢迎点赞 收藏 关注。下一篇我会用同一套方阵讲Bifid、Trifid、ADFGVX这几个 Delastelle 家族的兄弟密码以及怎么用程序把四方密码暴力破解出来。本文所有方阵、密文均由文中的 Python 代码实际运行验证过可放心对照手算。