1. 栅栏密码是什么:把一句话拆进几道“栅栏”里
我最早接触栅栏密码,并不是在密码学教材里,而是小学时候跟同桌玩传纸条。规则特别简单:把一句话竖着写,每隔一个字往下跳一行,写几行之后再横着把每行连起来读。你会发现原来那句话完全变样了,别人拿到纸条根本看不懂。后来上了大学,在信息安全课程里重新见到它,才知道这个玩法有个正式名字,叫栅栏密码(Rail Fence Cipher),网上流传最多、搜索热度最高的就是“3栏栅栏密码”。
栅栏密码能解决什么问题?对现代通信来说,它已经不能承担任何保密任务了,任何一个普通电脑都能在几毫秒之内把它破解。但作为密码学入门的第一个案例,它的价值不可替代:它用最直观的“换位置”方式,展示了密码系统(明文、密文、密钥、加解密算法)到底是怎么运作的,也是理解后面所有换位密码、乃至更复杂分组密码的起点。这篇文章我会把栅栏密码讲得透透的:它是什么、为什么叫这个名字、如何手搓加密、如何手搓解密、栏数怎么定、用 Python 怎么写、以及它到底安全吗。
适合看这篇文章的人有三类:一是密码学爱好者,想从经典古典密码入手;二是正在学密码学、信息安全相关课程的同学,需要一个能真正手动跑通的完整案例;三是纯好奇的朋友,想知道热搜上的“栅栏密码 3栏”到底是什么。内容不难,保证你看完就能自己动手加密一段话,也能把别人发你的密文一步步还原。
1.1 名字的由来:Rail Fence 和 Zigzag
栅栏密码这个名字,直译自英文 Rail Fence Cipher。Rail 是轨道、栏杆,Fence 是栅栏、围栏,所以它也叫“轨道围栏密码”。不过我更喜欢的名字是它的别名:Zigzag Cipher,也就是锯齿形密码。为什么叫这个?因为加密时,明文字符所走的路径在纸上画出来,就是一个在几条水平线之间上下跳动的锯齿形折线,像缝纫机走线一样。
那“栅栏”又从哪来?当你把字符按折线轨迹分配到多条水平轨道之后,每一条轨道上的字符列看起来就像一排栅栏的木条;把这些“木条”按行拼起来,就是密文。所以这个名字不是随便起的,它描述的是加密完成后的视觉形态,而不是某个发明者的名字。
1.2 它属于“换位密码”,不是“替换密码”
很多人第一次接触古典密码,会把栅栏密码跟凯撒密码搞混,觉得都是“把字母变来变去”。这里有一个关键区别:
- 替换密码(Substitution Cipher)会把明文字符替换成另一个字符。比如凯撒密码里,A 变成 D、B 变成 E,字符本身变了,但顺序没变。
- 换位密码(Transposition Cipher)相反,所有字符都保持原样,一个都不改,改的只是排列位置。栅栏密码就是最典型的换位密码。
一个特别容易记的类比:替换密码像给人改名换姓,人还是那个人,但名字变了;换位密码像让一队人重新排队,每个人还是原来的名字,但站的顺序全变了。
这个区分不是抠概念,而是直接决定后面怎么破解。正因为栅栏密码只是换了顺序,明文中字符出现的频率在密文里完全保留,原文长度也不变。这些特点,都是破解的突破口。
1.3 加密原理一句话浓缩版
栅栏密码的加密规则,就三步:
- 选定一个正整数 n,表示“栏数”或“轨道数”。n=2 最常见,n=3 是网上流传最广的版本,也就是“栅栏密码 3栏”。
- 从明文第一个字符开始,按一个锯齿形轨迹,把字符依次放进第 1 栏、第 2 栏……直到第 n 栏,然后折返,依次放第 n-1 栏、第 n-2 栏……直到第 1 栏,再折返。反复来回,直到字符放完。
- 所有字符分配完毕后,按栏从上往下把每栏字符依次拼接,得到密文。
解密就是完全逆向的操作:先按栏数画一个空的锯齿形网格,把密文按行填回去,再沿锯齿轨迹把字符读出来。
原理看着简单,但实际操作时,新手最容易被两个地方绊住:一是折返规则没踩准,二是“按行填”和“按轨迹读”的顺序搞反。下面我用完整例子逐步拆开讲。
2. 手搓加密:纸上画出完整过程
这一节的目标,是让完全没接触过密码学的人,手里拿一支笔一张纸,也能把一段明文加密成栅栏密码。我准备了三个例子:英文 3 栏、英文 2 栏、中文 3 栏。建议你看到例子时真的拿纸跟画一遍,动手一次比看十遍都管用。
2.1 3栏加密完整演示
明文用HELLOWORLD,栏数取 3。
第一步,画三条水平线,代表三栏,从下到上或者从上到下都行,只要自己心里清楚第 0 栏、第 1 栏、第 2 栏分别是谁。然后从明文的第一个字符 H 开始,按轨迹依次分配:
- 第 0 个字符 H → 第 0 栏
- 第 1 个字符 E → 第 1 栏(往下走)
- 第 2 个字符 L → 第 2 栏(继续往下)
- 到达最下栏,必须折返,下一步向上走
- 第 3 个字符 L → 第 1 栏(往上走)
- 第 4 个字符 O → 第 0 栏(继续往上)
- 到达最上栏,再折返,下一步向下走
- 第 5 个字符 W → 第 1 栏
- 第 6 个字符 O → 第 2 栏
- 第 7 个字符 R → 第 1 栏
- 第 8 个字符 L → 第 0 栏
- 第 9 个字符 D → 第 1 栏
把每栏收到的字符汇总一下,看这个表:
| 字符位置 | 明文 | 所在栏 |
|---|---|---|
| 0 | H | 0 |
| 1 | E | 1 |
| 2 | L | 2 |
| 3 | L | 1 |
| 4 | O | 0 |
| 5 | W | 1 |
| 6 | O | 2 |
| 7 | R | 1 |
| 8 | L | 0 |
| 9 | D | 1 |
接着把各栏的字符按顺序拼接:
- 第 0 栏:H O L → HOL
- 第 1 栏:E L W R D → ELWRD
- 第 2 栏:L O → LO
三栏拼在一起,密文就是HOLELWRDLO。
对比一下明文 HELLOWORLD,内容完全变样了,但仔细数一下,密文还是 10 个字符,不多不少。这正是换位密码的特征:换了顺序,不换长度。
2.2 折返规则到底怎么定
上面这个例子里埋了两个最容易踩的坑,我单独拿出来说。
第一个坑:轨迹怎么走才叫“锯齿形”?正确规则是“碰到底就回头,碰到顶就回头”。3 栏时的完整路径是 0→1→2→1→0→1→2→1→0→1……方向到边界后反转,所以是一组有规律的上下震荡。很多资料会画出这样的图:
第0栏:H . . . O . . . L 第1栏:. E . L . W . R . D 第2栏:. . L . . . O . . .这里每一列只对应一个字符,从上往下看,所有点连起来恰好还原 HELLOWORLD。折线只是帮助眼睛定位,实际处理时,你只需要关注“当前字符落在第几栏”。
第二个坑:到达最上栏和最下栏之后,各自要折返。怎么判断该不该折返?有一个简单通用的条件:如果当前方向是“下”,并且当前栏号已经等于 n-1,说明到最底了,下一步转向“上”;如果当前方向是“上”,并且当前栏号已经等于 0,说明到最顶了,下一步转向“下”。这个规则在写代码时同样适用,几乎是栅栏密码算法的核心逻辑。
2.3 2栏栅栏密码:把字符拆成两条轨道
栏数取 2 时,栅栏密码会变得异常简单,很多教材拿它当入门第一课。规则就是:奇数位置的字符进第 0 栏,偶数位置的字符进第 1 栏,然后拼起来。
还是用HELLOWORLD做例子:
- 第 0 栏(位置 0、2、4、6、8):H L O O L → HLOOL
- 第 1 栏(位置 1、3、5、7、9):E L W R D → ELWRD
拼接后密文是HLOOLELWRD。
你看,2 栏甚至连“折返”的概念都几乎感觉不到,本质就是把字符串按奇偶位置拆成两半再拼起来。也正因为这样,2 栏栅栏密码特别好识别、好破解,只要把密文从中间分成两半,再逐字交错读取,明文就回来了。我给完全没接触过密码的朋友做热手,一般都用 2 栏,它能迅速讲明白“换位”这个概念,又不至于被轨迹绕晕。
2.4 中文文本怎么处理
网上搜“栅栏密码 3栏”的人,很多其实是想加密中文。中文和英文在这个密码里没有本质区别:一个字就是一个字符,轨迹规则完全一样。
比如明文是“我是栅栏密码的忠实粉丝”,一共 11 个汉字,栏数取 3。按轨迹依次分配:
- 第 0 栏:我、密、实
- 第 1 栏:是、栏、码、忠、粉
- 第 2 栏:栅、的、丝
拼接后密文是“我密实是栏码忠粉栅的丝”。读起来不会有任何歧义,因为所有汉字都没变,只是顺序乱了。不过要注意,中文解密时不像英文那样容易靠“拼出单词”判断对不对,你得靠语义来猜。所以我手搓中文栅栏密码时会约定:去掉标点、去掉空格、只保留汉字,加密前双方先讲好规则,不然一会儿带标点一会儿不带,解密时会非常难受。
3. 手搓解密:从密文倒推原文
解密是加密的逆过程。只要知道栏数 n,解密过程就能完全按照加密的轨迹倒推。这里我讲三种手搓方法,从最直观的“画图法”到适合口算的“切块法”,你自己选顺手的那种。
3.1 画图法:先用点占位,再填字符
以密文HOLELWRDLO、栏数 3 为例。
第一步,先画出 3 行轨道图,但不要急着填字符,先用点号占位。占位的数量等于密文长度,这里是 10 个位置。然后按锯齿轨迹标出每个位置属于哪一栏。
3 栏轨迹:0→1→2→1→0→1→2→1→0→1。于是 10 个位置的栏归属是:
| 位置 | 所在栏 |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 2 |
| 3 | 1 |
| 4 | 0 |
| 5 | 1 |
| 6 | 2 |
| 7 | 1 |
| 8 | 0 |
| 9 | 1 |
统计可得:第 0 栏有 3 个位置,第 1 栏有 5 个位置,第 2 栏有 2 个位置。
第二步,把密文从左到右切块,对应填入各栏。前 3 个字符HOL填入第 0 栏,接下来 5 个字符ELWRD填入第 1 栏,最后 2 个字符LO填入第 2 栏。
填完后,如果按行显示就是:
第0栏:H O L 第1栏:E L W R D 第2栏:L O但要注意,这个“按行显示”看着简单,填入时不能一个挨一个横着填,而是要把每栏的字符放到它对应的位置上去。这里因为我们只关心最终读取,所以可以简化理解:第 0 栏的 3 个字符分别对应位置 0、4、8;第 1 栏的 5 个字符分别对应位置 1、3、5、7、9;第 2 栏的 2 个字符对应位置 2、6。
第三步,沿着锯齿轨迹 0→1→2→1→0→1→2→1→0→1 读取。位置 0 取第 0 栏第 1 个字符 H,位置 1 取第 1 栏第 1 个字符 E,位置 2 取第 2 栏第 1 个字符 L,位置 3 取第 1 栏第 2 个字符 L……一路读下去,得到明文HELLOWORLD。
整个过程的钥匙,就是那张“位置与栏的对应表”。这张表加密时生成过一次,解密时再复用一次,方向倒过来就行。
3.2 切块法:不用画图,直接算每栏字符数
如果你觉得画点号太麻烦,可以只算每栏字符数,然后直接切块、穿插。这个方法在写代码时非常实用。
还是密文HOLELWRDLO、栏数 3、长度 10。先模拟一遍轨迹,统计每栏字符数,得到 [3, 5, 2]。然后:
- 密文前 3 个字符
HOL分给第 0 栏; - 中间 5 个字符
ELWRD分给第 1 栏; - 最后 2 个字符
LO分给第 2 栏。
再按轨迹从各栏依次取字符:第 0 栏取第 0 个 H,第 1 栏取第 0 个 E,第 2 栏取第 0 个 L,第 1 栏取第 1 个 L,第 0 栏取第 1 个 O……循环往复。
这个方法怎么在脑子里快速跑?把三栏字符排成三列:
0: H O L 1: E L W R D 2: L O然后按轨迹索引 0-1-2-1-0-1-2-1-0-1,依次到对应栏取“下一个还没取过的字符”即可。H(栏0取0),E(栏1取0),L(栏2取0),L(栏1取1),O(栏0取1),W(栏1取2),O(栏2取1),R(栏1取3),L(栏0取2),D(栏1取4)。
结果就是 HELLOWORLD。
3.3 中文密文解密示例
用刚才的中文密文“我密实是栏码忠粉栅的丝”来还原,栏数还是 3。
长度 11,轨迹 0→1→2→1→0→1→2→1→0→1→2,统计每栏数量:
- 第 0 栏:位置 0、4、8,共 3 个
- 第 1 栏:位置 1、3、5、7、9,共 5 个
- 第 2 栏:位置 2、6、10,共 3 个
于是密文切块:
- 前 3 个字符“我密实”是第 0 栏
- 中间 5 个字符“是栏码忠粉”是第 1 栏
- 后 3 个字符“栅的丝”是第 2 栏
按轨迹读取:位置 0 取第 0 栏第 0 个“我”,位置 1 取第 1 栏第 0 个“是”,位置 2 取第 2 栏第 0 个“栅”,位置 3 取第 1 栏第 1 个“栏”,位置 4 取第 0 栏第 1 个“密”……读完得到“我是栅栏密码的忠实粉丝”。
加密解密一轮走下来,你会发现它本质上就是一个“重新排队”的游戏。知道几栏,就等于拿到了队形图;不知道几栏,就只能靠猜。
3.4 手工解密最容易翻车的三个细节
第一,密文中间的空格先去掉。如果你收到的密文是按英文单词分的,比如HOL ELW RDLO,不处理空格就直接切块,会被空格带偏。正确做法是先把所有空格删掉,再当作连续字符串处理。
第二,切块切错位置。切块之前一定要先数清楚每栏字符数,而不是随手按比例猜。比如 10 个字符、3 栏,有人想当然切成 3、3、4,结果整个轨迹全乱。正确做法是把轨迹从头到尾走一遍,哪怕只写 10 个数字,也比瞎猜强。
第三,读取顺序千万不要从行头横着读。解密读取必须沿着锯齿轨迹走,不是按行从上往下读。如果按行读,等于直接把加密后的结果又读了一遍,得到的还是乱码。
4. 实操问答:栏数、空格、标点、大小写怎么办
手搓过几轮之后,你肯定会遇到一些边界问题。这些问题单看任何一个都不难,但组合起来却能让人半天解不出来。我把最常见的几个整理成一份问答清单。
4.1 “3栏栅栏密码”的栏数真的是“3行”吗
是,也不是。栏数 n 指的是横向的轨道数量,也就是你画几条水平线。3 栏就画 3 条线,这在视觉上就是 3 行。但要注意,密文拼接时是三行连成一行,不是三行并列输出。所以别人给你的密文是一串连续字符,你拿到手之后,心里要知道它内部可以被拆成 3 段,但纸面上它只是一行。
另外,栏数 n 和明文长度之间没有限制关系。就算明文只有 5 个字符,也可以取 10 栏;轨迹会在第 5 个字符处断掉,并没有走完一个完整周期。实际操作中,栏数远大于明文长度并没有意义,网上大家默认的 3 栏,是因为它在“复杂度”和“可手算”之间取得了平衡。
4.2 英文里的空格到底怎么处理
这个问题几乎每个第一次手搓的人都会问。空格如果保留,会被当成普通字符参与轨迹分配。比如HELLO WORLD(带一个空格),加密后空格也会被换位到密文中间,解密时只要同样把空格当成普通字符处理,就能无损还原。
我的建议是:加密前和接收方约定好“要不要保留空格”。如果要保留,就把空格当作普通字符,和其他字符一视同仁;如果不要,加密前先删掉所有空格。最怕的是两个人约定不一致:发方保留了空格,收方按无空格处理,解密结果就多出一个奇怪的字符位置,整段话都对不上。
现实中大多数教程默认“删空格”,原因很简单:空格在换位后会打乱人的直觉,而且很多场景下空格本来就不携带加密所需的信息。中文没有空格问题,但标点符号是同类问题,处理原则完全一样。
4.3 标点和数字要不要管
标点和数字在栅栏密码里本质上是普通字符,参与轨迹分配没有任何障碍。但有个很实际的建议:如果密文最终要手写传递,尽量减少特殊符号。原因不是加密算法处理不了,而是手写时逗号、句号、引号容易抄错,抄错一个字符,整段解密全废。数字尽量用阿拉伯数字,不要转成中文“一二三”,因为转写后字符数变了,解密时长度就对不上。
我曾经做实验时把一段带引号和省略号的中文文本加密,结果手抄密文时漏了一个省略号点,解密后整句语义彻底变了,最后花二十分钟逐字符比对才找到错误。从那以后我给自己定了个习惯:交给别人的密文,只用字母或汉字,统一大写或统一小写,标点另用明文方式提前告知。
4.4 大小写怎么统一
英文栅栏密码不区分大小写,因为换位只关心字符顺序,不关心字符形态。但为了解密时方便读,我建议加密前统一转成大写或统一转成小写。如果你不统一,比如明文Hello World,空格删掉后变成HelloWorld,加密解密本身没问题,只是密文里大小写混杂,别人拿到后第一眼会以为有别的加密规则,平白增加迷惑感。
当然,如果你故意想增加破解难度,保留大小写也是一种思路,但这点迷惑性对破解者来说几乎没有意义,后面破解章节会说原因。
4.5 为什么网上同一个题目的答案会不一样
在网上搜栅栏密码,你会发现同一个明文、同一个“3栏”,有时候答案不同。原因主要有两个:
一是空格处理方式不同。有人保留空格,有人删空格,结果密文自然不同。
二是起点和方向约定不同。有些实现从上往下,有些实现从下往上。虽然起点不同对密文的影响很大,但注意,它们解出来的明文是一样的,因为密钥(栏数)没变,只是画图的镜像而已。如果你拿自己加密的结果去跟别人的工具对比,先确认双方对空格和方向的约定是否一致。
我自己的习惯是:起点固定为第 0 栏,方向固定为先下后上,空格一律删除。这样和绝大多数在线工具对上时,结果都是一致的。
5. 用 Python 手搓一个栅栏密码工具箱
手搓了两篇文章的量,接下来可以上代码了。这里我用 Python 实现加密、解密和暴力破解栏数三个函数。代码量不大,但每一行都有明确作用,建议自己敲一遍,然后把注释改写成你自己的理解。
5.1 加密函数:用列表模拟轨道
加密的思路非常直接:准备 n 个空列表代表 n 条轨道,用 direction 变量记录当前移动方向,1 表示向下,-1 表示向上。遍历明文每个字符,放入当前轨道列表,然后根据是否到达边界决定是否调转方向。
def encrypt(plaintext, rails): if rails < 2: return plaintext rows = [[] for _ in range(rails)] r, direction = 0, 1 # r 表示当前在哪一条轨道 for ch in plaintext: rows[r].append(ch) # 到达最上栏,转为向下 if r == 0: direction = 1 # 到达最下栏,转为向上 elif r == rails - 1: direction = -1 r += direction return ''.join(''.join(row) for row in rows)测试一下:
print(encrypt("HELLOWORLD", 3)) # HOLELWRDLO这段代码有几个细节值得说明。direction是 +1 或 -1,用整数表示方向比布尔值方便得多,直接加在轨道索引 r 上就行。方向判断放在“放入字符之后”,这保证了一个字符放入轨道后,下个字符才切换方向。边界判断用两次 if,不是 if-elif,因为 r 在 0 和 rails-1 不会同时成立,写成两个 if 也不会冲突,但逻辑更清楚。
5.2 解密函数:先统计数量,再按轨迹穿插
解密函数是加密的逆过程。思路分三步:先模拟一遍轨迹,统计每栏应该有多少个字符;再把密文按每栏数量切块;最后沿轨迹从各栏依次取字符拼接。
def decrypt(ciphertext, rails): if rails < 2: return ciphertext n = len(ciphertext) # 第一步:统计每栏字符数量 counts = [0] * rails r, direction = 0, 1 for _ in range(n): counts[r] += 1 if r == 0: direction = 1 elif r == rails - 1: direction = -1 r += direction # 第二步:按数量把密文切给各栏 rows = [] idx = 0 for cnt in counts: rows.append(list(ciphertext[idx:idx + cnt])) idx += cnt # 第三步:沿轨迹从各栏取字符 cur = [0] * rails result = [] r, direction = 0, 1 for _ in range(n): result.append(rows[r][cur[r]]) cur[r] += 1 if r == 0: direction = 1 elif r == rails - 1: direction = -1 r += direction return ''.join(result)测试:
print(decrypt("HOLELWRDLO", 3)) # HELLOWORLD这个解密函数里最微妙的地方是第二步切块。rows不是一个二维数组按位置对齐的结果,它只是把密文按“数量”切成了 n 段。真正的位置对应关系,是靠第三步里每个轨道使用cur指针,根据轨迹顺序逐个消费实现的。这个写法比预先建空二维数组更简洁,也不容易越界。
5.3 暴力破解:不知道栏数怎么办
不知道栏数,那就把可能的栏数都试一遍。栅栏密码的栏数不可能超过密文长度,实际上超过一定值后就没有意义了。我习惯从 2 试到 20,对英文密文来说,几乎必然有一个结果能拼出可读英文。
def brute_rail(ciphertext, max_rails=20): for n in range(2, max_rails + 1): print(f"rails={n:>2}: {decrypt(ciphertext, n)}")试一下:
brute_rail("HOLELWRDLO")输出里 rails=3 那一行就是 HELLOWORLD,其他栏数都是乱码。这个脚本虽然简陋,但已经足够说明栅栏密码的一个根本弱点:密钥空间太小,栏数撑死了也就几十种可能,暴力穷举完全没有压力。
5.4 给代码加点容错
实际使用中,加密函数不应该假设输入一定干净。我建议在函数入口做两件事:把空白字符全部去掉,对英文字母统一转大写。这样能避免很多因为空格、大小写不一致导致的对不上暗号问题。
def normalize(text): return ''.join(text.split()).upper() def encrypt_safe(plaintext, rails): return encrypt(normalize(plaintext), rails) def decrypt_safe(ciphertext, rails): return decrypt(normalize(ciphertext), rails)这里的text.split()会把所有空白字符(空格、换行、制表符)都去掉,而不仅仅是空格,对从文件里读入的多行文本特别管用。
5.5 顺手写一个交互式小工具
如果只是自己玩,可以把上面几个函数打包成一个简单的命令行交互工具。输入 1 选择加密,输入 2 选择解密,然后输入明文或密文和栏数,即可得到结果。这个工具没什么高深内容,但很适合在群里跟朋友玩“密文接龙”。
def main(): while True: mode = input("1) 加密 2) 解密 其他退出: ") if mode not in ("1", "2"): break text = input("输入文本: ") rails = int(input("输入栏数: ")) if mode == "1": print("密文:", encrypt_safe(text, rails)) else: print("明文:", decrypt_safe(text, rails)) if __name__ == "__main__": main()这个小工具的局限是:加密时会把空格去掉,解密时也会把密文里的空格去掉。如果你和对方事先约定好“保留空格”,那不要用这个工具,用上面原始的encrypt()和decrypt()就行。
6. 栅栏密码安全吗:破解思路与进阶玩法
答案很明确:完全不安全。但“不安全”不等于“没有学习价值”,理解它怎么被破解,反而能帮你建立更深的密码学直觉。
6.1 换位密码的致命弱点
栅栏密码保留了明文的所有字符以及每个字符出现的频率。英文里 e 永远是出现频率最高的字母,密文的频率分布没变,破解者光是看统计直方图就知道这不是替换密码,而是换位密码。
更致命的是,换位密码不隐藏长度。密文长度等于明文长度,这直接砍掉了一大半猜测空间。对破解者来说,只要把不同的 n 全部试一遍,再用频率分析、词典匹配或人工阅读判断哪一个是可读文本,瞬间就能破解。
6.2 破解思路一:按栏数穷举
拿到一段栅栏密码密文,破解者通常这样操作:
- 猜测栏数 n,从 2 开始逐一到密文长度。
- 对每个 n,用解密函数还原一段候选明文。
- 判断哪段候选明文是正常文本。英文可以用单词词典匹配,中文可以看是否成句。
因为 n 通常不可能太大,这个过程几乎不需要优化,几毫秒就能跑完。我试过的最极端情况是 n=50,虽然每栏字符很少,但穷举 50 次也完全不是负担。
6.3 破解思路二:用频率分布确认“是不是换位密码”
如果你不确定对方的加密方式是不是栅栏密码,可以先把密文里每个字符的频率统计出来,跟明文语言的典型频率分布对比。如果高度吻合,那八成就是换位密码;再结合“长度没变”“字符集合没变”这两个特征,基本可以锁定。
这个方法对英文特别有效,中文因为汉字数量庞大,频率分析效果差一些,但如果拿到足够长的文本,还是能看出明显的分布规律。
6.4 栅栏密码 vs 凯撒密码,别再搞混了
这是被测频率最高的两个古典密码,我专门做个对比:
| 密码类型 | 是否改变字符 | 是否改变顺序 | 典型例子 |
|---|---|---|---|
| 凯撒密码(替换) | 是 | 否 | 把每个字母移 3 位 |
| 栅栏密码(换位) | 否 | 是 | 把字符按轨道重排 |
| 维吉尼亚密码 | 是 | 否 | 用密钥流逐字替换 |
判断一个密码是替换还是换位,就看密文里出现过的字符是不是全部来自明文。如果密文里有明文本来没有的字符,那是替换;如果密文字符集和明文完全一致只是顺序变了,那是换位。这个方法对古典密码几乎是一击必中。
6.5 栅栏密码的正确用法
既然这么不安全,是不是完全没用了?也不是。栅栏密码在现代密码学语境里还有三个价值:
第一,教学工具。它是理解“换位”这个概念的最小完整案例,也是理解更复杂的换位密码(比如列移位密码)的基础。
第二,组成复合加密。栅栏密码和凯撒密码组合使用,先做一次换位再做一次替换,就能同时打乱字符顺序和改变字符本身,破解难度会明显提高,虽然离现代密码标准还很远,但作为趣味加密已经够玩了。
第三,隐写场景。如果你把栅栏密码的密文伪装成一段“乱码文本”夹在正常内容里,不了解规则的人根本不知道这里面有信息。这种“藏”的思路,比“加密”本身更有实用价值。
我自己试过一种组合玩法:明文先做一次 3 栏栅栏换位,再用凯撒密码整体位移 5 位,两个古典密码叠在一起。看着挺唬人,但真的要保护什么实质内容时,还是老老实实用现代加密算法,这点必须说清楚。
写在最后:几个手搓之后才明白的小经验
玩栅栏密码这几年,踩过的坑不少,最后分享几条实在经验。
第一,任何加密操作之前,先和对方确认“空格怎么处理、大小写怎么统一”。这不是算法问题,是沟通问题,但 90% 的解密失败都是沟通问题引起的,而不是算法算错。
第二,手搓时一定要先写轨迹编号,再填字符。哪怕只在纸上写一排 0、1、2 的数字,也能让你少犯很多错。我第一次手搓中文栅栏密码时,想省事直接凭感觉填空,结果填了三位才发现其中一栏数量不对,只好从头来过。
第三,如果你想拿栅栏密码做趣味游戏,强烈推荐 3 栏以上。2 栏太简单,别人一眼就能看出“把密文从中间切开再交错”的规律,失去神秘感;3 栏难度刚好,既不会太难查,又有足够迷惑性。加上中文语境下,3 栏生成的密文往往比 2 栏更像“正经乱码”,玩起来更有意思。
栅栏密码不是什么高深技术,但它像一把钥匙,打开的是整个换位密码家族的门。理解它之后,你再去看列移位密码、旋转栅栏密码,会发现它们本质上都是同一个思路的变体:把字符重新排队,只是排队规则越来越复杂而已。偶尔拿它跟朋友玩一玩,顺便检验一下自己算法的边界条件,还是挺有乐趣的。