不是所测任何语言的简单替换 就现有扫描件而言已结案
用一个以5-gram语言模型打分的求解器(模拟退火),在十种语言下攻击了流传的两种转录。各语言最好也只达到每字母−2.69 nats,300次重启也没有改变这一结果。同一求解器能复原用同样方式加密的15段随机103字母文本中的12段,而这一长度的真实文本得分在−1.4到−2.1之间。要么这张纸条不是这些语言中某段文本的简单替换,要么两种读法都带有同一个转录错误。
01 实物及其来历
纸条上有三行短字。它大约在1950年代末发现于哥本哈根一座军事博物馆中一幅1835年丹麦将军肖像的背后,并被寄给了美国密码协会(American Cryptogram Association)。ACA 没有记录任何解答,也从未撰文介绍。博物馆、将军和发现者都没有记录。
Klaus Schmeh 从 ACA 会员 Kent Ramliden 那里得到一份扫描件;Ramliden 是旅居佛罗里达的瑞典人,2016年去世。Schmeh 三次发表过它:2015年1月用德语,2017年8月作为他 Top 50 的第23号,2021年10月又以德语和英语的“悬案”帖子发表。流传的唯一图像就是那份扫描件,614 × 147 像素。2021年评论中有两条读者贡献与此相关:“ShadowWolf”的25符号转录,以及 Matthew Brown 用丹麦语四元组做的爬山法,后者一无所获。
02 转录
使用了两种转录。 cipher.txt 是本项目的转录,根据放大四倍和六倍的扫描件,每次半行做成。 cipher_sw.txt 是 ShadowWolf 在2021年10月7日那篇德语帖子评论中给出的25符号读法。以下是合并后的读法,每个符号一个记号,以空格分隔:
7 N F n F B 3 A D B n o 3 D B P 6 3 B 3 P 8 D F 2 n 3 | 2 3 A W R F 0 3 A n 2 0 B n o 9 P n P 9 2 B 3 v v 3 D B | 0 A B v 3 D B P n o F 3 8 0 D B P X 0 3 B n o n F 9 n D B v n | 1 3 | n o n 3 F D B 3 F B A 1 2 3 A
共107个记号:20种共103个符号,外加4条长竖线,写作 | ,当作句子分隔符处理。数字4和5从未出现。
| 记号 | 纸条上的标记 | 次数 |
|---|---|---|
3 | 数字3 | 16 |
n | 普通的 n | 13 |
B | 后面带点的反斜线(\.) | 14 |
F | 后面带点的斜线(/.) | 8 |
P | 加号 | 6 |
A | 尖角或 lambda(^) | 6 |
D | 位于笔画之前的独立圆点 | 8 |
o | 小圆字母 o | 5 |
2 | 数字2 | 5 |
0 | 高的零 | 5 |
v | 照写 | 4 |
9 | 数字9 | 3 |
1 | 数字1 | 2 |
8 | 数字8 | 2 |
7 6 X | 照写 | 各1次 |
N | 上方带两点的 n | 1 |
R | 第1行末尾一个形似“or”的小字形 | 1 |
W | 上方带双重音符的尖角 | 1 |
| | 长竖线,句子分隔符,不计入103个之内 | 4 |
两种读法的分歧
两种读法逐个符号一致,只是 ShadowWolf 把这里合并的东西拆开了。他把后面带点的反斜线与两侧都带点的反斜线区分开来,后者在这里是一个独立圆点 D 后接 B。他把后面带点的斜线与前面带点的斜线区分开来,后者在这里是 D 后接 F。他把第一对尖角之后那个小小的上标双撇单列为一个符号。他区分了四种尖角形状:一种高的、像 lambda 的,即第1行的第一个尖角和第3行的最后一个符号;一种小的;一种后面带点、位于第2行开头的;还有一种在第2行的零之后。他还把第3行 1 3 附近的第二条高竖线读作数字1,而不是分隔符。这样拆分后符号数为25,与 Schmeh 的数字一致;合并后为20。两种读法都做了攻击。
两条内部规律
信件 o 出现五次,每次都紧跟在 n: no3, no9, +no, non 和 non3之后,其中 non 在第3行出现两次。成对的 vv 出现一次,在第2行。第06节说明了它们能走多远。
03 攻击
solve.py 用 Project Gutenberg 中十种语言的文本构建字符5-gram语言模型:丹麦语、瑞典语、挪威语、德语、荷兰语、法语、英语、拉丁语、冰岛语和芬兰语,每种0.6至240万字母。语料不在仓库中,而是通过 Gutendex API 按会话日志中的 id 获取。求解器是在单射的符号到字母映射上做模拟退火,每次运行重启60到80次,长竖线视为句子边界。求解器只优化5-gram得分;得分最高的候选随后再按词典覆盖率重新排序,而词典覆盖率并不在优化目标之内。
在合并读法上测试了六种记号约定:独立圆点作为字母、删去、作为边界,或与后面的笔画合并;长竖线作为字母;带点的 n 与普通 n 合并。25符号读法则按原样运行。另有一个脚本 solve_sp.py检验“两种带点笔画是单词分隔符”这一假设——那样的话,文本就是22个词,平均每词3.7个字母——它使用一个考虑空格的模型,并以整词命中来计分。
在任何语言、任何约定下都没有得出可读的东西。各语言的最佳每字母得分,以及最佳密钥得出的结果:
| 语言 | 最佳每字母得分(任意约定) | 结果大致如何 |
|---|---|---|
| 拉丁语 | −2.69 | fxsisted tine togeteom scie ... |
| 丹麦语 | −2.77 | junindel dige dskedesf ... / ... udstødes ... velsigne ... |
| 挪威语 | −2.81 | bærersag sena stjasatm ... |
| 瑞典语 | −2.81 | honensam sera stfasatv ... |
| 中文释义 | −2.88 | cydidsea sine stbesetf ... |
| 德语 | −2.93 | uchthien itze irbeierm ... |
| 法语 | −2.97 | chiailes lape ltbeletr ... |
| 荷兰语、冰岛语、芬兰语 | −3.07 至 −3.30 |
单词分隔符假设在带空格的模型下每字母得分−4.6,而真实文本约为−2.3。它只得出一些孤立的丹麦语单词:er、elle、unge、død、døde。最后两个就是第06节讨论的片段。
04 匹配对照
一次失败的搜索本身证明不了什么,所以先用求解器本应能解开的文本对它进行校准。 control.py 从同样的语料中随机抽取103字母、含三个句子断点的段落,每段用约20个符号的随机简单替换加密,再用完全相同的求解器攻击。每种语言试验三次,每次重启40次;而真正的密码用了60到80次,后来又用了300次。准确率是复原出的段落所占比例。
| 语言 | 试验 | 密钥中的字母数 | 准确率 | 找到的得分 | 真实文本的得分 | 结果 |
|---|---|---|---|---|---|---|
| 丹麦语 | 1 | 19 | 0.99 | −1.99 | −1.85 | 已复原 |
| 丹麦语 | 2 | 19 | 0.99 | −1.93 | −1.96 | 已复原 |
| 丹麦语 | 3 | 18 | 0.99 | −1.85 | −1.76 | 已复原 |
| 德语 | 1 | 22 | 0.96 | −1.88 | −1.78 | 已复原 |
| 德语 | 2 | 19 | 0.79 | −2.64 | −1.75 | 部分 |
| 德语 | 3 | 22 | 0.14 | −3.42 | −1.81 | 搜索失败 |
| 中文释义 | 1 | 20 | 1.00 | −1.44 | −1.44 | 已复原 |
| 中文释义 | 2 | 21 | 0.99 | −1.99 | −1.85 | 已复原 |
| 中文释义 | 3 | 20 | 1.00 | −1.67 | −1.67 | 已复原 |
| 拉丁语 | 1 | 18 | 1.00 | −2.09 | −2.09 | 已复原 |
| 拉丁语 | 2 | 18 | 1.00 | −1.74 | −1.74 | 已复原 |
| 拉丁语 | 3 | 19 | 0.99 | −1.87 | −1.72 | 已复原 |
| 瑞典语 | 1 | 22 | 1.00 | −1.95 | −1.95 | 已复原 |
| 瑞典语 | 2 | 22 | 0.05 | −3.49 | −1.59 | 搜索失败 |
| 瑞典语 | 3 | 22 | 1.00 | −1.99 | −1.99 | 已复原 |
15段中有12段基本完全复原,真实文本的每字母得分总在−1.4到−2.1之间。两次试验彻底失败,一次部分失败。每一例中,真正的密钥得分都会远好于求解器找到的结果:−1.81对−3.42,−1.59对−3.49,−1.75对−2.64。这些是40次重启下的搜索失败,很可能与22字母的密钥有关;它们意味着,对真实文本的单次失败运行证明不了什么。正因如此,最有希望的几种候选语言在两种读法上都用300次重启重跑了一遍。
对照这一校准,该密码在任何语言、任何读法下的得分都从未好于−2.7。所以,要么它不是这十种语言中某段文本的简单替换,要么转录以两种独立读法共有的方式合并或拆分了字母。丹麦语自2015年以来一直是默认假设,但它的契合度并不比拉丁语或挪威语好。Matthew Brown 在2021年评论中做的丹麦语四元组爬山法,也同样一无所获。
05 300次重启的确认
以种子5重启300次的最佳每字母得分,对象为合并读法的 full 和 nodot 约定以及25符号读法。 dotattach 的运行在写这些笔记时仍在进行,尚未超过这些分数。
| 语言 | 合并,完整 | 合并,去掉圆点 | 25符号读法 |
|---|---|---|---|
| 拉丁语 | −2.90 | −2.82 | −3.16 |
| 丹麦语 | −3.04 | −2.90 | −3.16 |
| 瑞典语 | −3.17 | −2.99 | −3.20 |
| 挪威语 | −3.12 | −3.01 | −3.17 |
| 中文释义 | −3.20 | −3.13 | −3.28 |
| 德语 | −3.29 | −3.32 | −3.46 |
重启次数增至五倍,结果纹丝不动;第03节取的是六种约定中的最佳值,这些运行固定了约定和种子,没有一格超过它。上限依旧,在每种语言中都比这一长度的真实文本得分低0.7到1.4 nats每字母。搜索失败这一解释可以排除。
06 唯一引人遐想的片段
non 读作 død
在 n = d, o = ø, 3 = e 的情况下,n-o-n 模式给出丹麦语 død(死的,死亡), non3 给出 døde(死了,死者),而 no3 给出 døe,即 dø(死)在1900年以前的拼法。这是整个搜索中唯一引人遐想的片段。
它出现在单词分隔符那次运行中,那次运行的其他整词命中只有 er、elle 和 unge。涉及的三个符号占103个记号中的34个,所以这一指派固定了三分之一的文本。但它无法延伸。那次解密的其余部分都是噪声,整次运行每字母得分−4.6,而真实文本在同一模型下约为−2.3。
07 现状及重启的条件
未破解 就现有扫描件而言已结案。有三条途径可以重启。
- 原始纸条或更好的扫描件。ACA 是从博物馆收到这张纸条的,所以应去 ACA 档案或 Kent Ramliden 的遗稿中查询。第二张图像就能解决第02节中拆分还是合并的问题。
- 那幅画。哥本哈根一座军事博物馆中一幅1835年丹麦将军的肖像,指向 Tøjhusmuseet,即今天的丹麦战争博物馆。查明这位将军,就能得到一个名字、一个日期和一种语言。
- 速记或私人代码。如果这些笔画和圆点是速记或私人代码而不是字母,那么任何替换模型都不适用,而这里没有任何东西检验了这种可能。
08 文件
所有内容都在项目仓库的 targets/copenhagen/ 中。 cipher.txt 是合并后的20符号读法,文件头中注明了记号约定; cipher_sw.txt 是与之对齐的 ShadowWolf 25符号读法。 solve.py 是带词典重排序的5-gram求解器(模拟退火), solve_sp.py 是单词分隔符测试, control.py 是匹配对照的测试框架。 NOTES.md 保存了这次尝试的完整记录。Gutenberg 语料不在仓库中;Gutendex id 记在会话日志里。