二、古典密码学:凯撒密码、维吉尼亚密码、仿射密码的原理与破解方法

古典密码学,说白了就是现代密码学的老祖宗。虽然今天没人再用它们保护银行数据,但理解这些基础算法,能帮你搞懂加密的本质——替换和换位。我当年刚入行时,就是从破解一个凯撒密码开始对密码学产生兴趣的。嗯,咱们一个一个来看。

2.1 凯撒密码:最简单的替换

凯撒密码的原理极其简单:把字母表平移固定的位数。比如移3位,A变成D,B变成E,以此类推。我记得古罗马时期凯撒就用这种方式传递军令,当时确实够用。

核心公式:

  • 加密:C = (P + K) mod 26
  • 解密:P = (C - K) mod 26

其中P是明文数字(A=0, B=1, ..., Z=25),C是密文数字,K是密钥(1~25)。

举个例子,明文"HELLO",密钥K=3:

  • H(7) → K(10)
  • E(4) → H(7)
  • L(11) → O(14)
  • L(11) → O(14)
  • O(14) → R(17)

结果就是"KHOOR"。是不是很简单?

破解方法:暴力穷举

凯撒密码的密钥空间只有25种可能。我经常跟新人说,你手动试25次就能破解,写个脚本更是秒出结果。为什么会这样?因为密钥太少,根本扛不住现代计算能力。

# Python 暴力破解凯撒密码
def caesar_break(ciphertext):
    for key in range(1, 26):
        plain = ''
        for c in ciphertext.upper():
            if c.isalpha():
                plain += chr((ord(c) - 65 - key) % 26 + 65)
            else:
                plain += c
        print(f"Key {key:2d}: {plain}")

caesar_break("KHOOR")

输出结果里,只有Key=3那行是"HELLO",其他都是乱码。一眼就能看出来。

个人经验:我在做CTF比赛时,经常遇到凯撒密码的变种——只加密字母、保留标点。写脚本时记得过滤非字母字符,否则解密结果会带一堆奇怪符号。

2.2 维吉尼亚密码:多表替换的升级

凯撒密码太脆弱,于是有人发明了维吉尼亚密码。它用关键词决定每个字母的偏移量,说白了就是多个凯撒密码轮着用。你想想看,如果密钥是"KEY",明文第一个字母用K(10)偏移,第二个用E(4)偏移,第三个用Y(24)偏移,然后重复。

核心公式:

  • 加密:Ci = (Pi + Ki mod len(K)) mod 26
  • 解密:Pi = (Ci - Ki mod len(K)) mod 26

举个例子,明文"ATTACK",密钥"KEY":

  • A(0) + K(10) = K(10)
  • T(19) + E(4) = X(23)
  • T(19) + Y(24) = R(17)
  • A(0) + K(10) = K(10)
  • C(2) + E(4) = G(6)
  • K(10) + Y(24) = I(8)

密文是"KXRKGI"。你看,同样的字母A,第一次加密成K,第二次还是K,但T加密成了X和R——这就是多表替换的效果。

破解方法:卡西斯基测试 + 频率分析

维吉尼亚密码比凯撒难破,但也不是铁板一块。破解分两步:

  1. 找密钥长度:用卡西斯基测试,找密文中重复出现的片段,计算间隔的最大公约数。
  2. 逐列频率分析:按密钥长度分组,每组就是一个凯撒密码,用频率分析破解。

我曾经踩过的坑:有一次破解维吉尼亚密码,卡西斯基测试给出的密钥长度是5,但实际是10。后来发现是因为明文本身有重复模式,干扰了测试结果。建议多试几个候选长度,别只信第一个。

# 卡西斯基测试简化版
def kasiski_examination(ciphertext, min_len=3):
    from collections import defaultdict
    repeats = defaultdict(list)
    for i in range(len(ciphertext) - min_len + 1):
        seq = ciphertext[i:i+min_len]
        repeats[seq].append(i)
    distances = []
    for seq, positions in repeats.items():
        if len(positions) > 1:
            for j in range(1, len(positions)):
                distances.append(positions[j] - positions[j-1])
    # 找距离的最大公约数
    from math import gcd
    if distances:
        g = distances[0]
        for d in distances[1:]:
            g = gcd(g, d)
        return g
    return None

2.3 仿射密码:数学化的替换

仿射密码是凯撒密码的数学升级版。它用线性变换来加密:C = (a × P + b) mod 26。这里a和b都是密钥,但a必须和26互质,否则解密时找不到逆元。

核心公式:

  • 加密:C = (a × P + b) mod 26
  • 解密:P = a-1 × (C - b) mod 26

其中a-1是a模26的乘法逆元,满足a × a-1 ≡ 1 mod 26。

举个例子,a=5, b=8,加密"HI":

  • H(7) → (5×7+8) mod 26 = 43 mod 26 = 17 → R
  • I(8) → (5×8+8) mod 26 = 48 mod 26 = 22 → W

密文是"RW"。解密时先求5的逆元,5×21=105≡1 mod 26,所以a-1=21。然后P=21×(C-8) mod 26,就能还原回"HI"。

破解方法:已知明文攻击 + 穷举

仿射密码的密钥空间是12×26=312种(a有12种选择,b有26种)。虽然比凯撒大,但依然脆弱。如果有已知明文对,可以直接解方程求a和b。

我建议:在实际项目中,仿射密码常用于简单的数据混淆,比如隐藏URL参数。但千万别用于真正的加密——312种密钥,现代计算机一秒能试几百万次。

# 仿射密码破解(已知明文攻击)
def affine_break_known(plaintext, ciphertext):
    # 假设已知至少两个字符的对应关系
    p0 = ord(plaintext[0].upper()) - 65
    c0 = ord(ciphertext[0].upper()) - 65
    p1 = ord(plaintext[1].upper()) - 65
    c1 = ord(ciphertext[1].upper()) - 65
    # 解方程组:c0 = (a*p0 + b) mod 26, c1 = (a*p1 + b) mod 26
    # 相减得:c1-c0 = a*(p1-p0) mod 26
    diff_c = (c1 - c0) % 26
    diff_p = (p1 - p0) % 26
    # 求a = diff_c * diff_p^-1 mod 26
    for a in range(1, 26):
        if (a * diff_p) % 26 == diff_c:
            b = (c0 - a * p0) % 26
            return a, b
    return None, None

2.4 三种密码对比

特性 凯撒密码 维吉尼亚密码 仿射密码
密钥类型 单整数(1~25) 关键词(字符串) 两个整数(a, b)
密钥空间 25 26n (n为密钥长度) 312
替换方式 单表替换 多表替换 单表替换(线性)
破解难度 极低 中等 低
典型破解方法 暴力穷举 卡西斯基+频率分析 已知明文攻击

2.5 知识体系图

下面这张图展示了本章的核心逻辑:从凯撒到维吉尼亚再到仿射,破解方法也逐步升级。

古典密码学知识体系 古典密码学知识体系 凯撒密码 维吉尼亚密码 仿射密码 单表替换(固定偏移) 多表替换(关键词驱动) 线性变换(a×P+b) 暴力穷举(25种) 卡西斯基+频率分析 已知明文攻击 共同弱点:密钥空间小 + 统计特征保留 现代密码学已不再使用这些算法,但理解它们能帮你掌握替换、密钥空间、频率分析等核心概念

2.6 避坑指南

我曾经犯过的错:

  • 用凯撒密码加密用户密码——结果被同事一眼看穿,当场社死。
  • 破解维吉尼亚密码时,忘了考虑密钥长度可能不是卡西斯基测试的直接结果,浪费了半天时间。
  • 仿射密码的a值选了偶数,导致解密失败——后来才想起a必须和26互质。

嗯,古典密码学就讲到这里。记住一句话:任何依赖算法保密性的加密都是不安全的。凯撒、维吉尼亚、仿射,它们的共同弱点就是密钥空间太小,而且密文保留了明文的统计特征。现代密码学怎么解决这些问题?咱们后面章节慢慢聊。