在现代密码学的宏大图景中,古典密码学虽然不再用于高强度的安全防护,但它们是理解现代加密原理的基石。今天,我们将深入探讨一种既古老又精巧的加密技术——仿射密码(Affine Cipher)。
在这篇文章中,我们将从数学的视角拆解其工作原理,探究如何确保加密的可逆性,并一步步通过代码实现这一机制。无论你是为了应对算法面试,还是仅仅对密码学背后的逻辑感到好奇,本文都将为你提供详尽的解析和实用的代码范例。
什么是仿射密码?
简单来说,仿射密码是一种单表替换密码。但这并不意味着它仅仅是简单的字母移位(比如凯撒密码)。仿射密码的精妙之处在于,它引入了模运算和线性代数的概念。
在这种机制中,我们会将字母表中的每个字母映射为对应的数值,通过一个数学函数进行转换,然后再转换回字母。这不仅仅是“移位”,而是对字母表进行了一种线性的“拉伸”和“平移”。
数学模型:字母的数字化
为了进行数学运算,我们首先需要将字母数字化。通常,我们使用标准的英文字母表(A-Z),并假设其长度 m = 26。
我们会将字母映射到 0 到 25 的整数范围:
- A -> 0
- B -> 1
- …
- Z -> 25
核心加密原理:数学函数
仿射密码的核心由两个密钥数字控制:a 和 b。加密过程使用模算术将明文字母对应的整数转换为密文字母。
加密函数
公式如下:
E(x) = (ax + b) \pmod m
这里:
- x 是明文字母对应的数值(例如 ‘A‘=0)。
- a 是乘数密钥。
- b 是偏移量密钥。
- m 是模数(字母表大小,通常为 26)。
关键约束:对于 a 的选择,必须非常讲究。它必须与 m 互质。换句话说,gcd(a, m) = 1。
为什么必须互质?
这是很多初学者容易忽略的地方。如果 INLINECODE1df258a4 和 INLINECODE5a7073e4 有公因数(例如 INLINECODEe3125aa8, INLINECODE642ade18),那么 INLINECODE045116ad 的结果将永远是偶数。这意味着密文字母只能映射到字母表中一半的字母,导致无法解密(因为多对一的映射)。只有当 INLINECODEbfde25f5 与 INLINECODEedaaf624 互质时,INLINECODEb9773825 才能覆盖模 m 的所有剩余类,保证加密是双射(一一对应)的。
逆向工程:解密算法
为了解密,我们必须执行逆运算以检索出明文。解密函数如下:
D(x) = a^{-1}(x - b) \pmod m
这里出现了一个新概念:$a^{-1}$。
什么是模乘逆元?
$a^{-1}$ 并不是分数中的“倒数”,而是 $a$ 在模 $m$ 下的模乘逆元。即满足以下条件的数字:
(a \times a^{-1}) \pmod m = 1
这意味着,如果你将 $a$ 和它的逆元相乘并对结果取模,你会得到 1。这个逆元是解密过程中将“拉伸”还原的关键。
寻找乘法逆元的方法
由于我们通常使用的是 26 个字母,我们可以通过暴力搜索的方式找到 $a$ 的逆元。我们需要找到一个数 $x$,在 1 到 25 之间,使得 $(a \times x) \pmod{26} = 1$。
当然,在更高级的数学中,我们可以使用扩展欧几里得算法来高效地找到逆元,这在模数很大时非常有用。但在 $m=26$ 的情况下,简单的循环查找就足够快了。
代码实战:从理论到实现
接下来,让我们看看如何用代码实现这套逻辑。我们将使用 C++ 和 Java 两种语言,并添加详细的中文注释。
实现示例 1:基础 C++ 实现
首先,我们看一个标准的实现方案。为了方便演示,我们固定了密钥 $a=17, b=20$。在实际应用中,你可以将这些作为参数传入。
// 仿射密码的 C++ 实现演示
#include
using namespace std;
// 密钥配置
// 注意:a 必须与 26 互质,这里选 17
const int a = 17;
const int b = 20;
// 函数:加密明文
string encryptMessage(string msg)
{
string cipher = "";
for (int i = 0; i < msg.length(); i++)
{
// 处理空格:保持原样不加密
if(msg[i] != ' ')
{
// 核心加密公式:E(x) = (ax + b) % 26
// msg[i]-'A' 将字符转换为 0-25 的数值
// 最后 +'A' 将计算结果转回 ASCII 字符
cipher = cipher +
(char) ((((a * (msg[i]-'A') ) + b) % 26) + 'A');
}
else
{
cipher += msg[i];
}
}
return cipher;
}
// 函数:解密密文
string decryptCipher(string cipher)
{
string msg = "";
int a_inv = 0; // 存储 a 的模逆元
int flag = 0;
// 步骤 1:寻找 a 的模乘逆元 a_inv
// 暴力搜索 0 到 25
for (int i = 0; i < 26; i++)
{
flag = (a * i) % 26;
// 检查 (a * i) % 26 == 1
if (flag == 1)
{
a_inv = i;
}
}
// 步骤 2:应用解密公式
for (int i = 0; i < cipher.length(); i++)
{
if(cipher[i] != ' ')
{
// 核心解密公式:D(x) = a^-1(x - b) % 26
// 这里的 x 对应 cipher[i]-'A'
// 注意:在 C++ 中负数取模需要特殊处理,但这里 a_inv * ((cipher[i]-'A') - b)
// 由于我们对 26 取模,加上 26 再取模可以防止负数干扰,但在本例逻辑中直接取模通常也可行
// 为了严谨,我们可以稍微调整逻辑
int val = (cipher[i] - 'A');
val = (a_inv * (val - b)) % 26;
// 处理负数情况:确保结果是正数
if (val < 0) val += 26;
msg = msg + (char)(val + 'A');
}
else
{
msg += cipher[i];
}
}
return msg;
}
// 主函数
int main(void)
{
string msg = "AFFINE CIPHER";
cout << "原始消息: " << msg << endl;
// 调用加密函数
string cipherText = encryptMessage(msg);
cout << "加密结果: " << cipherText << endl;
// 调用解密函数
cout << "解密结果: " << decryptCipher(cipherText) << endl;
return 0;
}
实现示例 2:Java 版本(面向对象风格)
对于 Java 开发者,我们可以将其封装为工具类。这里我们展示了更清晰的逻辑分离。
// 仿射密码的 Java 实现演示
class AffineCipher {
// 密钥
static final int a = 17;
static final int b = 20;
static final int m = 26; // 字母表大小
// 扩展欧几里得算法求逆元(更专业的做法)
static int findInverse(int num, int mod) {
num = num % mod;
for (int x = 1; x < mod; x++) {
if ((num * x) % mod == 1) {
return x;
}
}
return 1; // 未找到(在 a 与 m 不互质时发生)
}
static String encrypt(char[] msg) {
String cipher = "";
for (int i = 0; i < msg.length; i++) {
// 避免加密空格
if (msg[i] != ' ') {
// 应用加密公式 E(x) = (ax + b) % m
int x = msg[i] - 'A';
int val = (a * x + b) % m;
cipher += (char) (val + 'A');
} else {
cipher += msg[i];
}
}
return cipher;
}
static String decrypt(char[] msg) {
String plain = "";
// 获取 a 的逆元
int a_inv = findInverse(a, m);
if (a_inv == 1 && a != 1) {
return "错误:密钥 a 与 m 不互质,无法解密";
}
for (int i = 0; i < msg.length; i++) {
if (msg[i] != ' ') {
// 应用解密公式 D(x) = a^-1(x - b) % m
int x = msg[i] - 'A';
// 在 Java 中 % 操作符对负数的处理结果可能是负数
// 所以我们要加上 m 再取模以确保结果为正
int val = (a_inv * (x - b)) % m;
if (val < 0) val += m;
plain += (char) (val + 'A');
} else {
plain += msg[i];
}
}
return plain;
}
public static void main(String[] args) {
String msg = "HELLO WORLD";
System.out.println("原始: " + msg);
String encrypted = encrypt(msg.toCharArray());
System.out.println("加密: " + encrypted);
System.out.println("解密: " + decrypt(encrypted.toCharArray()));
}
}
实现示例 3:Python 版本(简洁与直观)
Python 非常适合快速原型开发。我们可以利用列表推导式让代码更加“Pythonic”。
import math
class AffineCipherPython:
def __init__(self, a, b):
self.a = a
self.b = b
self.m = 26
# 在初始化时就检查互质
if math.gcd(self.a, self.m) != 1:
raise ValueError(f"错误: 密钥 a={a} 与 m={self.m} 不互质,无法保证唯一解密。")
def encrypt(self, msg):
cipher = ""
for char in msg:
if char != ‘ ‘:
# ord(‘A‘) = 65
x = ord(char) - ord(‘A‘)
val = (self.a * x + self.b) % self.m
cipher += chr(val + ord(‘A‘))
else:
cipher += char
return cipher
def decrypt(self, msg):
# 暴力查找逆元
a_inv = None
for i in range(self.m):
if (self.a * i) % self.m == 1:
a_inv = i
break
if a_inv is None:
return "无法解密:未找到逆元"
plain = ""
for char in msg:
if char != ‘ ‘:
x = ord(char) - ord(‘A‘)
val = (a_inv * (x - self.b)) % self.m
plain += chr(val + ord(‘A‘))
else:
plain += char
return plain
# --- 使用示例 ---
try:
# 密钥 a=17, b=20
ac = AffineCipherPython(17, 20)
text = "PYTHON CODE"
enc = ac.encrypt(text)
dec = ac.decrypt(enc)
print(f"原文: {text}")
print(f"加密: {enc}")
print(f"解密: {dec}")
except ValueError as e:
print(e)
进阶探讨:模运算的陷阱与最佳实践
在编写代码时,尤其是处理解密算法,你可能会遇到一个非常隐蔽的 bug:负数取模问题。
负数取模的坑
让我们再看一眼解密公式:$D(x) = a^{-1}(x – b) \pmod m$。
假设 $x = 1$ (对应字母 ‘B‘), $b = 20$,那么 $x – b = -19$。
- 在数学定义中,$-19 \pmod{26}$ 应该等于 $7$(因为 $-19 + 26 = 7$)。
- 但在 C++ 和 Java 中,运算符
%的结果是 -19。 - 在 Python 中,
%的结果才是符合数学定义的 7。
解决方案:在 C++/Java 中,我们必须对取模后的结果进行检查,如果为负数,就加上模数 $m$,使其回到正数区间。
代码修正逻辑(C++ 风格):
int res = (a_inv * (val - b)) % 26;
if (res < 0) res += 26; // 关键步骤
实际应用场景与局限性
虽然仿射密码比凯撒密码复杂,但它仍然是古典密码。我们今天学习它的目的,不是为了保护银行数据,而是为了:
- 理解密码学数学基础:它是理解现代公钥密码学(如 RSA)中模运算概念的绝佳起点。
- CTF 竞赛:在信息安全夺旗赛中,仿射密码常作为入门级的 misc 题目出现。
- 简单的数据混淆:如果你需要一个非常简单的混淆层,且数据价值极低,它可以作为一种轻量级的手段。
安全提示:仿射密码非常容易被攻破。攻击者可以通过频率分析轻松破解它。因此,永远不要将其用于真实的敏感数据加密。
总结
在这篇文章中,我们深入了仿射密码的各个方面。我们从定义出发,理解了“互质”和“模逆元”对于确保加密可逆性的重要性。我们看到了加密公式 $E(x) = (ax+b) \pmod m$ 和解密公式 $D(x) = a^{-1}(x-b) \pmod m$ 的实际运作。
更重要的是,通过 C++、Java 和 Python 的实战代码,我们学习了如何处理编程中的边界情况(如负数取模),并构建了完整的加密解密工具。
希望这篇文章不仅让你掌握了仿射 cipher 的实现,更让你对算法背后的数学严谨性有了更深的体会。你可以尝试修改代码中的密钥 $a$ 和 $b$,或者尝试支持小写字母,看看程序是否能正常工作。动手实践是掌握算法的最好方式!
下一步建议
如果你想继续探索,我建议你尝试实现一个通用的古典密码工具箱,或者去研究一下“维吉尼亚密码”(Vigenère Cipher),它解决了单表替换密码频率分析过于简单的问题。