古典密码学核心:置换与代换原理及Python实战解析

古典密码学核心:置换与代换原理及Python实战解析

1. 项目概述:从“密信”到“比特流”的古典密码之旅

最近在整理一些历史遗留的旧项目代码,翻出来一个早年写的、用于教学演示的古典加密算法工具箱。看着那些用凯撒、维吉尼亚、栅栏加密过的字符串,突然觉得挺有意思。在当今这个AES、RSA、ECC等现代密码学大行其道的时代,我们似乎很少再回头去细究这些“古老”的加密方法了。但恰恰是这些古典密码,构成了整个密码学的基石。理解它们,不仅仅是怀旧,更是理解现代密码算法设计思想的一把钥匙。今天,我就以这个工具箱为引子,和大家深入聊聊古典加密算法的两大核心支柱:置换密码代换密码。无论你是安全领域的初学者,想夯实基础,还是对密码学历史感兴趣的开发者,希望这篇结合了原理、实战与“踩坑”经验的解析,能给你带来一些实实在在的收获。

简单来说,古典加密算法主要解决的是“保密通信”的最原始需求。在电报、无线电甚至更早的密信时代,如何让一段信息在传递过程中,即使被第三方截获也无法读懂,就是密码学的全部目标。置换和代换,是实现这一目标的两种最根本的思路。置换,可以理解为“位置变换”,就像把一队士兵的排列顺序打乱,但士兵本身还是原来那些士兵;代换,则是“内容替换”,相当于给每个士兵都戴上一个面具,改变了他们的外在身份。这两种思想看似简单,但其组合与演变,却衍生出了无数精妙(或脆弱)的加密方案。我们接下来的讨论,将完全围绕这两种核心思想展开,通过代码实例、手工推演和安全性分析,让你不仅知道怎么用,更明白为什么这么用,以及为什么它们最终被现代密码学所取代。

2. 核心原理拆解:置换与代换的“道”与“术”

在深入具体算法之前,我们必须先建立起对这两种核心思想的清晰认知。这决定了我们后续分析任何古典密码时的思维方式。

2.1 代换密码:每个字符的“变脸”艺术

代换密码的核心思想是“一对一映射”。它建立一个明文字符集到密文字符集的固定映射关系。加密时,将明文中的每个字符,根据这个映射关系,替换成对应的密文字符。

1. 单表代换:最简单的“密码本”这是最基础的代换形式。整个加密过程只使用一张固定的替换表。例如,著名的凯撒密码就是一种单表代换,它将字母表平移固定的位数(如3位):A->D, B->E, ..., Z->C。

  • 数学抽象:可以看作一个函数E(x) = (x + k) mod n,其中x是明文字母的索引(如A=0, B=1),k是密钥(偏移量),n是字母表大小(如26)。
  • 特点与缺陷:实现极其简单,但安全性非常脆弱。因为它保留了原始语言的统计特征(如英文中‘e’的出现频率最高),通过频率分析可以轻易破解。

2. 多表代换:让映射“动”起来为了克服单表代换的统计特征缺陷,多表代换被发明出来。它使用多个不同的替换表,在加密过程中根据某种规则循环或依密钥决定使用哪一张表。

  • 核心突破:同一个明文字母,在不同位置可能被替换成不同的密文字母。这极大地破坏了原始语言的统计规律。
  • 典型代表:维吉尼亚密码。它使用一个关键词(如“KEY”)来决定偏移量序列。加密时,明文的第一个字母用‘K’对应的偏移量(A=0, 故K=10)进行凯撒移位,第二个字母用‘E’对应的偏移量(4),第三个用‘Y’(24),然后循环重复这个关键词。这样,“e”这个高频字母就不会总是被加密成同一个密文字母了。

注意:多表代换的安全性提升是巨大的,但并非无懈可击。如果密钥长度较短,攻击者可以通过分析密文的周期性等方法来推测密钥长度,进而将其分解为多个单表代换分别破解。维吉尼亚密码的破解史就是一部密码分析学的精彩篇章。

2.2 置换密码:字符队伍的“重新列队”

置换密码不改变字符本身,只改变它们在明文中的出现顺序。你可以把它想象成对一个序列进行“洗牌”。

1. 列置换:写在表格里,按列读取这是最直观的置换方法。将明文按一定宽度(密钥)写成矩阵的行,然后按特定的列顺序(密钥决定)读出,形成密文。

  • 操作示例:明文“HELLOWORLD”,密钥为“312”(表示矩阵有3列,读取顺序是第3列、第1列、第2列)。
    1. 按3列写成行:H E LL O WO R LD X X(最后用‘X’补齐)。
    2. 按密钥“312”顺序读列:先读第3列(L, W, L, X),再读第1列(H, L, O, D),最后读第2列(E, O, R, X)。
    3. 得到密文:“LWXLHOD EORX”(通常连写为“LWXLHODEORX”)。
  • 安全性思考:单纯的列置换,字符频率特征完全保留,仅增加了位置混乱度。其安全性依赖于密钥(列宽和读取顺序)的保密。但可能的排列组合数量有限(列数的阶乘),对于现代计算机来说,暴力破解短密钥的列置换并不困难。

2. 栅栏密码:最简单的“之字形”排列可以看作列置换的一种特例。将明文写成“之”字形(或说波浪形)的两行或多行,然后按行读出。

  • 示例(2栏栅栏):明文“CRYPTOGRAPHY”。
    1. 写成两行:第一行取奇数位字符C R P O R P,第二行取偶数位字符Y T G A H Y
    2. 连接两行得到密文:“CRPORPYTGAHY”。
  • 本质:这是一种固定模式的、规则极其简单的置换,安全性几乎为零,常作为趣味谜题出现。

置换与代换的根本区别:代换改变了信息的“形态”(字符本身),而置换只改变了信息的“布局”(字符顺序)。一个形象的比喻是:代换像是把一篇文章翻译成另一种语言(字符变了);置换像是把一篇文章的段落顺序全部打乱(字符没变,但读不懂了)。

3. 实战演练:从凯撒到维吉尼亚的代码实现与解析

理解了原理,我们动手实现它们。这里我用Python来演示,因为其语法清晰,易于理解算法本质。我们会实现一个简单的加密类,包含凯撒(单表代换)和维吉尼亚(多表代换)算法。

3.1 环境准备与基础工具函数

首先,我们构建一些共用的基础函数,比如处理字母表、确保输入合规等。

class ClassicalCipher: """古典加密算法工具箱""" def __init__(self, alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZ'): """ 初始化,定义字母表。 默认使用26个大写英文字母,可根据需要扩展(如包含空格、标点)。 """ self.alphabet = alphabet self.n = len(alphabet) # 字母表大小 # 创建字符到索引和索引到字符的快速映射字典 self.char_to_index = {char: idx for idx, char in enumerate(alphabet)} self.index_to_char = {idx: char for idx, char in enumerate(alphabet)} def _preprocess_text(self, text, keep_spaces=False): """ 预处理文本:转换为大写,移除非字母字符(除非keep_spaces为True)。 这是古典密码的常见做法,专注于字母加密。 """ text = text.upper() if keep_spaces: # 只保留字母和空格 processed = ''.join(ch for ch in text if ch.isalpha() or ch == ' ') else: # 只保留字母 processed = ''.join(ch for ch in text if ch.isalpha()) return processed def _validate_key(self, key, key_type='numeric'): """简单的密钥验证。""" if key_type == 'numeric' and not isinstance(key, int): raise ValueError("凯撒密码密钥需为整数。") # 其他验证可以根据算法添加 return True

3.2 单表代换典范:凯撒密码的实现与局限

让我们从最简单的凯撒密码开始。

def caesar_encrypt(self, plaintext, shift): """ 凯撒加密。 :param plaintext: 明文字符串 :param shift: 偏移量(整数,可为负) :return: 密文字符串 """ self._validate_key(shift, 'numeric') plaintext = self._preprocess_text(plaintext) ciphertext_chars = [] for char in plaintext: if char in self.char_to_index: original_index = self.char_to_index[char] # 核心加密操作: (原索引 + 偏移量) mod 字母表大小 new_index = (original_index + shift) % self.n ciphertext_chars.append(self.index_to_char[new_index]) # 预处理后理论上不会出现非字母字符,这里else分支可忽略或处理空格 return ''.join(ciphertext_chars) def caesar_decrypt(self, ciphertext, shift): """ 凯撒解密。 解密就是加密的逆过程,偏移量取反即可。 """ # 直接复用加密函数,偏移量取负 return self.caesar_encrypt(ciphertext, -shift)

实操与测试:

cipher = ClassicalCipher() plaintext = "ATTACK AT DAWN" shift = 3 encrypted = cipher.caesar_encrypt(plaintext, shift) decrypted = cipher.caesar_decrypt(encrypted, shift) print(f"明文: {plaintext}") print(f"偏移量: {shift}") print(f"密文: {encrypted}") # 输出: DWWDFN DW GDZQ print(f"解密: {decrypted}") # 输出: ATTACKATDAWN (注意空格被预处理掉了)

凯撒密码的脆弱性分析:

  1. 密钥空间极小:对于26个字母,只有25个非零的有效偏移量(偏移0等于没加密)。攻击者最多尝试25次即可破解。
  2. 完全保留统计特征:密文中字母的频率分布与明文完全一致,只是整体平移。例如,在足够长的英文密文中,出现频率最高的那个字母,极大概率对应明文的‘E’。
  3. 实战心得:凯撒密码在今天毫无安全价值,但它是一个完美的教学工具,用于理解“代换”的概念和“模运算”在密码学中的应用。在写演示代码时,务必处理好字母表的边界(mod n),这是初学者常出错的地方。

3.3 多表代换进阶:维吉尼亚密码的完整实现

维吉尼亚密码是古典密码学的一座高峰,其实现比凯撒稍复杂,但思路一脉相承。

def vigenere_encrypt(self, plaintext, keyword): """ 维吉尼亚加密。 :param plaintext: 明文字符串 :param keyword: 密钥字符串(仅字母) :return: 密文字符串 """ plaintext = self._preprocess_text(plaintext) keyword = self._preprocess_text(keyword) if not keyword: raise ValueError("密钥不能为空。") ciphertext_chars = [] key_length = len(keyword) for i, char in enumerate(plaintext): if char in self.char_to_index: p_idx = self.char_to_index[char] # 明文字母索引 # 获取当前使用的密钥字母的偏移量 k_char = keyword[i % key_length] k_idx = self.char_to_index[k_char] # 密钥字母索引 # 加密:C_i = (P_i + K_i) mod n c_idx = (p_idx + k_idx) % self.n ciphertext_chars.append(self.index_to_char[c_idx]) return ''.join(ciphertext_chars) def vigenere_decrypt(self, ciphertext, keyword): """ 维吉尼亚解密。 解密:P_i = (C_i - K_i) mod n """ ciphertext = self._preprocess_text(ciphertext) keyword = self._preprocess_text(keyword) if not keyword: raise ValueError("密钥不能为空。") plaintext_chars = [] key_length = len(keyword) for i, char in enumerate(ciphertext): if char in self.char_to_index: c_idx = self.char_to_index[char] # 密文字母索引 k_char = keyword[i % key_length] k_idx = self.char_to_index[k_char] # 密钥字母索引 # 解密:P_i = (C_i - K_i) mod n (注意避免负数) p_idx = (c_idx - k_idx) % self.n plaintext_chars.append(self.index_to_char[p_idx]) return ''.join(plaintext_chars)

实操与测试:

cipher = ClassicalCipher() plaintext = "HELLO WORLD" keyword = "KEY" encrypted = cipher.vigenere_encrypt(plaintext, keyword) decrypted = cipher.vigenere_decrypt(encrypted, keyword) print(f"明文: {plaintext}") print(f"密钥: {keyword}") print(f"密文: {encrypted}") # 输出: RIJVS UYVJN (手动计算验证:H(7)+K(10)=R(17), E(4)+E(4)=I(8)...) print(f"解密: {decrypted}") # 输出: HELLOWORLD

维吉尼亚密码的关键点与注意事项:

  1. 密钥的重复使用:算法核心是让密钥循环使用。如果明文很长而密钥很短,这种周期性会成为突破口。因此,理论上,一次一密(密钥长度>=明文长度且完全随机)的维吉尼亚密码是不可破译的,但这在实际中很难做到。
  2. 模运算的细节:加解密中的mod n操作必须一致,且处理减法时要确保结果为非负数(Python的%运算符已自动处理)。
  3. 密钥的选择:应避免使用有意义的单词作为密钥,因为这会降低密钥空间。理想的密钥是随机生成的字母序列。
  4. 实战心得:在实现时,我最初曾忘记对密钥也进行_preprocess_text处理,导致当密钥包含小写或空格时出现映射错误。务必保证参与运算的所有文本(明文、密文、密钥)都在同一个处理后的字符集内,这是实现古典密码算法时最常见的错误之一。

4. 置换密码实战:列置换算法的模拟与破解视角

我们来实现一个通用的列置换密码,它能更清晰地展示置换的过程。

4.1 列置换加密与解密实现

def columnar_transposition_encrypt(self, plaintext, key): """ 列置换加密。 :param plaintext: 明文字符串 :param key: 密钥,一个数字列表或字符串,指示列数和读取顺序。 例如,密钥“312”表示分3列,按第3、1、2列顺序读取。 :return: 密文字符串 """ plaintext = self._preprocess_text(plaintext, keep_spaces=False) # 置换通常也去掉空格 # 将密钥转换为列顺序列表,例如“312” -> [2, 0, 1] (索引从0开始) if isinstance(key, str): # 假设密钥是像“312”这样的字符串,数字代表列的顺序(从1开始计数) col_order = [int(ch) - 1 for ch in key if ch.isdigit()] elif isinstance(key, list): col_order = [k - 1 for k in key] # 同样假设输入是从1开始 else: raise ValueError("密钥应为数字列表或数字字符串。") num_cols = len(col_order) num_rows = (len(plaintext) + num_cols - 1) // num_cols # 向上取整 # 创建矩阵并填充明文,不足处用‘X’填充(古典密码常见做法) matrix = [['X'] * num_cols for _ in range(num_rows)] index = 0 for r in range(num_rows): for c in range(num_cols): if index < len(plaintext): matrix[r][c] = plaintext[index] index += 1 else: break # 按密钥指定的列顺序读取 ciphertext_chars = [] for col in col_order: for r in range(num_rows): ciphertext_chars.append(matrix[r][col]) return ''.join(ciphertext_chars) def columnar_transposition_decrypt(self, ciphertext, key): """ 列置换解密。 解密是加密的逆过程:将密文按列写回矩阵,再按行读取。 """ ciphertext = self._preprocess_text(ciphertext, keep_spaces=False) if isinstance(key, str): col_order = [int(ch) - 1 for ch in key if ch.isdigit()] elif isinstance(key, list): col_order = [k - 1 for k in key] else: raise ValueError("密钥应为数字列表或数字字符串。") num_cols = len(col_order) num_rows = (len(ciphertext) + num_cols - 1) // num_cols # 我们需要知道原始列顺序的逆序,才能把密文填回正确位置 # 例如,加密顺序是[2,0,1],则解密时我们需要知道密文的第一块对应原矩阵的第2列... # 更直观的方法:创建一个空矩阵,然后按照加密时填充的相反顺序,将密文字符填入列中。 # 计算每列在原矩阵中有多少字符(最后一列可能少一些) full_rows = len(ciphertext) // num_cols extra_chars = len(ciphertext) % num_cols # 创建一个映射,记录加密时每一列被取走了多少个字符 col_lengths = [full_rows + (1 if col < extra_chars else 0) for col in range(num_cols)] # 现在,我们需要按照加密时读取列的顺序,将密文切片,并放回原列 matrix = [[''] * num_cols for _ in range(num_rows)] cipher_idx = 0 # 按照加密顺序(col_order)遍历列 for col in col_order: chars_for_this_col = col_lengths[col] for r in range(chars_for_this_col): if cipher_idx < len(ciphertext): matrix[r][col] = ciphertext[cipher_idx] cipher_idx += 1 # 按行读取矩阵,得到明文 plaintext_chars = [] for r in range(num_rows): for c in range(num_cols): if matrix[r][c]: # 只追加有字符的位置 plaintext_chars.append(matrix[r][c]) # 移除填充的‘X’(简单处理:去掉末尾的X。更严谨的做法需记录原始长度) result = ''.join(plaintext_chars) # 注意:我们无法准确区分末尾的‘X’是填充还是原文,这是古典置换密码的一个固有问题。 # 在实际中,可能需要约定或使用其他不常用的字符作为填充。 return result.rstrip('X') # 简单移除末尾的X

实操与测试:

cipher = ClassicalCipher() plaintext = "HELLOWORLD" key = "312" # 3列,按第3、1、2列顺序读 encrypted = cipher.columnar_transposition_encrypt(plaintext, key) decrypted = cipher.columnar_transposition_decrypt(encrypted, key) print(f"明文: {plaintext}") print(f"列置换密钥: {key}") print(f"密文: {encrypted}") # 输出: LWXLHODEOR (与前面手工计算略有不同,因填充处理) print(f"解密: {decrypted}") # 输出: HELLOWORLD

4.2 从攻击者视角看置换密码的弱点

实现了解密,我们更要理解它为何不安全。假设我们截获了一段列置换密文,但不知道密钥。

  1. 已知明文攻击:如果攻击者知道(或猜中)密文中包含的某个单词或短语(如“ATTACK”),他可以通过尝试不同的列数和排列,观察何时该单词能对齐,从而反推出密钥长度和顺序。
  2. 唯密文攻击——穷举密钥:密钥空间由列数n和列排列顺序决定。可能的密钥总数是n!(n的阶乘)。对于较小的n(如5, 5!=120),现代计算机可以瞬间穷举。对于n=10,10! = 3,628,800,也仍然在可暴力破解的范围内。
  3. 唯密文攻击——频率分析辅助:虽然置换打乱了顺序,但字母本身的频率分布没有改变。攻击者可以将密文按不同列数假设进行“分割”,然后检查每一“列”的字母频率。如果列数猜对了,那么每一列实际上都是原始明文的一段,其频率分布应符合英文(或原文语言)特征。通过分析这种特征,可以验证列数假设,甚至辅助确定列顺序。
  4. 实操心得:填充问题:在实现中,填充字符(如‘X’)的处理是个麻烦事。在解密时,我们简单地rstrip('X'),这可能导致问题:如果明文本身就以‘X’结尾怎么办?更健壮的做法是记录原始明文长度,解密后精确截取。这提醒我们,密码算法的设计必须无歧义,编解码的每一步都需要精确可逆,否则会在实际通信中引入错误或安全隐患。

5. 安全性深度探讨与古典密码的现代启示

通过上面的实战,我们已经直观感受到古典密码的局限性。现在,我们系统性地总结一下,并看看它们给现代密码学留下了什么遗产。

5.1 古典密码的通用破译方法

  1. 频率分析:这是对付单表代换的“杀手锏”。每种语言都有其稳定的字母、双字母(如TH, HE)、三字母(如THE, AND)频率分布。通过统计密文中符号的频率,并与标准分布对比,可以逐步推测出替换表。工具(如在线频率分析器)让这个过程自动化。
  2. 卡西斯基试验与重合指数法:这是破解维吉尼亚密码等多表代换的关键。卡西斯基试验通过寻找密文中重复出现的片段,来推测密钥长度。弗里德曼发明的重合指数则通过计算密文的自相关性,更科学地估计密钥长度。一旦密钥长度m被确定,密文就可以被分解成m组,每组都是由同一个单表代换加密的,从而退化到可用频率分析破解。
  3. 暴力穷举与已知明文攻击:对于密钥空间小的算法(如凯撒、短密钥的列置换),直接尝试所有可能的密钥是最简单的方法。如果攻击者能获得一部分“明文-密文对”(已知明文攻击),破解过程将大大加速。
  4. 字典攻击与社交工程:如果密钥是一个有意义的单词(如维吉尼亚密码的密钥),攻击者可以尝试用字典中的单词进行破解,这比穷举所有字母组合快得多。这也引出了现代密码学的一个重要原则:密钥应该足够随机

5.2 古典密码的设计缺陷与现代密码学原则

古典密码的失败,归根结底是违反了现代密码学的基本设计原则:

  • 柯克霍夫原则:密码系统的安全性应依赖于密钥的保密,而不是算法的保密。大多数古典密码(除了像一次性便笺这样的特例)即使算法公开,也因密钥空间不足或结构缺陷而不安全。
  • 混淆与扩散:香农提出的这两个概念是现代密码设计的核心。
    • 混淆:指密文与密钥之间的关系应尽可能复杂。代换密码主要提供混淆。
    • 扩散:指明文的统计特征应消散到密文中。置换密码主要提供扩散。
    • 古典密码的不足:单表代换只有混淆,没有扩散(频率特征保留)。简单置换只有扩散,没有混淆(字符本身不变)。维吉尼亚和更复杂的古典密码试图结合两者,但扩散和混淆的程度远远不够,且结构有规律可循(如维吉尼亚的周期性)。
  • 巨大的密钥空间:足以抵抗暴力攻击。古典密码的密钥空间通常太小。

5.3 古典思想的现代演变

尽管古典密码本身不再安全,但其思想在现代密码算法中得以升华和延续:

  1. 代换-置换网络:这正是SPN结构,是现代分组密码(如AES)的基础。AES的每一轮都包含字节代换(SubBytes, 非线性代换)、行移位(ShiftRows, 置换)、列混合(MixColumns, 扩散)和轮密钥加(AddRoundKey)操作。这完美体现了香农的混淆与扩散思想,且代换和置换的复杂度远超古典时代。
  2. 流密码:维吉尼亚密码可以看作是一种原始的流密码:密钥流(重复的关键词)与明文流进行模加。现代流密码(如ChaCha20)使用复杂的伪随机数生成器产生密钥流,安全性基于数学难题,而非简单的字母表移位。
  3. 密码模块的构建:学习古典密码让我们理解,一个安全的密码系统往往由多个简单的、不安全的组件通过精巧的方式组合而成。这种模块化设计思想是现代密码工程的核心。

6. 常见问题与实战避坑指南

在编写和演示这些古典密码算法的过程中,我遇到过不少坑。这里总结一下,如果你要自己实现或教学,请特别注意。

6.1 字符集与大小写处理

  • 问题:算法对大小写敏感吗?支持空格和标点吗?
  • 方案:在算法内部统一处理(如我们代码中的_preprocess_text)。通常的做法是:
    1. 将所有输入转换为大写(或小写)。
    2. 移除非字母字符(根据算法需求决定是否保留空格)。
    3. 使用统一的字母表(如26个英文字母)进行运算。
  • 踩坑记录:早期版本没有统一处理,导致加密“Hello”和解密“HELLO”结果不同,调试了很久。务必在加密和解密前使用完全相同的预处理流程

6.2 填充方案的选择

  • 问题:分组密码(如列置换需要矩阵)或当明文长度不是算法块的整数倍时,需要填充。
  • 古典密码常见填充:使用不常用的字母,如‘X’或‘Z’。但这不是好方法,因为可能和真实内容冲突。
  • 现代启示:现代密码学有标准的填充方案,如PKCS#7。在古典密码实践中,一个更好的方法是记录原始明文长度,并在解密后根据该长度截取结果,而不是依赖特定的填充字符。

6.3 密钥的管理与输入

  • 问题:密钥如何安全地传递和存储?用户输入密钥的格式如何规范?
  • 古典密码的教训:维吉尼亚密码如果使用短密钥或常见单词,安全性急剧下降。列置换的密钥(列数及顺序)如果太短,也容易被穷举。
  • 实操建议:即使在教学演示中,也应该在代码中加入对密钥强度的简单检查(如最小长度警告),并提示用户使用随机生成的密钥。对于列置换,密钥的表示(字符串还是列表)要清晰定义,并在文档中说明。

6.4 算法模式与边界情况

  • 问题:我们的维吉尼亚实现是最简单的ECB(电子密码本)模式,即相同的明文块(在相同密钥位置下)会产生相同的密文块。这也会泄露信息。
  • 延伸思考:现代密码学引入了CBC、CTR等模式来消除这种模式重复。虽然古典密码不涉及这些,但理解ECB模式的缺陷有助于理解为什么现代密码需要更复杂的工作模式。
  • 边界情况:模运算中负数处理、空字符串输入、密钥为空等情况,代码中必须有健壮的处理,否则会抛出异常。

6.5 性能与扩展性

  • 问题:这些Python实现用于教学尚可,但效率不高。如果处理超大文本呢?
  • 优化思路:使用Python的str.translate()配合maketrans()进行单表代换会非常高效。对于维吉尼亚,可以预先计算好所有可能的移位结果(一个26x26的维吉尼亚方阵),然后通过查表而非实时计算模加来提升速度。这体现了密码实现中一个常见的权衡:时间与空间的取舍。

回过头看,实现这些古典密码的过程,就像一次密码学历史的徒步旅行。你亲手用代码复现了千百年前人们为保护秘密而创造的智慧,同时也清晰地看到了这些方法的边界与裂痕。这比任何教科书上的描述都来得深刻。当你再去看AES的轮函数,或理解流密码的概念时,你会自然而然地想到:“哦,这里的S盒就是超级加强版的代换表,这里的行移位就是更精巧的置换,而整个结构就是为了实现充分的混淆和扩散。” 这就是学习古典密码最大的价值——它不是让你去使用它们,而是为你搭建起理解现代密码学庞大殿堂的脚手架。最后一个小建议是,如果你有兴趣,可以尝试将凯撒密码和列置换组合起来,先代换再置换,或者先置换再代换,看看密文特征有什么不同,又能如何被分析。这种组合实验能让你对混淆和扩散的协同效应有更感性的认识。