用Python玩转置换加密:从原理到代码实现43


大家好,我是您的中文知识博主!今天我们要探索一个既古老又充满趣味的密码学世界——置换加密。在信息安全日益重要的今天,了解古典密码的原理,不仅能帮助我们理解现代加密算法的基石,还能锻炼我们的编程思维。今天,我们就来“卷起袖子”,用Python亲手实现置换加密算法,从原理到代码,一步步带你玩转它!

置换加密(Permutation Cipher),顾名思义,就是通过“置换”——即改变明文中字符的位置,而不改变其本身——来达到加密目的的一种密码技术。它与替换加密(Substitution Cipher,如凯撒密码)形成对比,后者是改变字符本身(比如A变成D),但不改变位置。置换加密就像把一副扑克牌的顺序打乱,牌还是那些牌,但组合方式完全不同了。

一、置换加密的核心原理

置换加密的核心思想是:打乱明文的顺序。它不改变明文中的字母或符号,只是重新排列它们。最常见的置换加密算法是列移位密码(Columnar Transposition Cipher),我们将以此为例进行讲解和实现。

列移位密码(Columnar Transposition Cipher)详解:


列移位密码的工作原理非常直观:
确定密钥:密钥通常是一个单词或短语,其长度决定了加密矩阵的列数。密钥字母的字母顺序将决定列的读取顺序。
写入明文:将明文逐行写入一个矩阵(表格)中,每行的长度与密钥长度相同。
填充:如果明文的长度不能填满最后一行的所有单元格,通常会用一些特定字符(如'X'、'Z'或其他随机字母)进行填充。
读取密文:根据密钥字母的字母顺序,重新排列矩阵的列。然后,逐列从上到下读取这些重新排列的列,组合成密文。

举个例子:

明文:WE ARE DISCOVERED FLEE AT ONCE

密钥:PYTHON

首先,将密钥中的字母按照字母顺序排序:

P Y T H O N

对应索引:

4 6 5 1 3 2 (H是第1个,N是第2个,O是第3个,P是第4个,T是第5个,Y是第6个)

或者直接使用`sorted(key)`,得到排序后的`HNOPTY`,然后用其索引来决定原始列的读取顺序。

为了简化,我们以字母在密钥中的原始位置为基准,然后根据字母的排序来决定读取顺序。
密钥:`P Y T H O N`
排序后的密钥字母及它们的原始索引:
H (原索引3) -> 1
N (原索引5) -> 2
O (原索引4) -> 3
P (原索引0) -> 4
T (原索引2) -> 5
Y (原索引1) -> 6
所以读取的列顺序是:原始列3 -> 原始列5 -> 原始列4 -> 原始列0 -> 原始列2 -> 原始列1。

我们将明文(去除空格)`WEAREDISCOVEREDFLEEATONCE` 写入一个6列的矩阵:
P Y T H O N (密钥)
0 1 2 3 4 5 (列索引)
W E A R E D
I S C O V E
R E D F L E
E A T O N C
E D X X X X (填充X)

现在,根据密钥字母的排序:`H` (索引3), `N` (索引5), `O` (索引4), `P` (索引0), `T` (索引2), `Y` (索引1)。
我们将按照这个顺序读取列:
列3 (H): R O F O X
列5 (N): D E E C X
列4 (O): E V L N X
列0 (P): W I R E E
列2 (T): A C D T X
列1 (Y): E S E A D

将这些读取的列拼接起来,得到密文:
`ROFOX DEECX EVLNX WIRIE ACDTX ESEAD`

二、Python实现:加密过程

现在,让我们用Python来实现这个加密过程。我们将编写一个 `encrypt(plaintext, key)` 函数。
import math
def encrypt(plaintext, key):
# 1. 清理明文:移除空格,转换为大写
plaintext = "".join(filter(, plaintext)).upper()
key = "".join(filter(, key)).upper()
if not plaintext or not key:
return ""
num_cols = len(key)
num_rows = (len(plaintext) / num_cols)
# 2. 填充明文:用'X'填充,使其长度为列数的整数倍
padding_needed = (num_cols * num_rows) - len(plaintext)
plaintext += 'X' * padding_needed
# 3. 创建加密矩阵 (列表的列表)
# 按照行填充
grid = [['' for _ in range(num_cols)] for _ in range(num_rows)]
k = 0
for r in range(num_rows):
for c in range(num_cols):
grid[r][c] = plaintext[k]
k += 1
# 4. 根据密钥获取列的读取顺序
# key_order 是一个列表,存储 (字母, 原始索引) 的元组
# 例如,如果 key="PYTHON", 则 key_order = [('P',0), ('Y',1), ('T',2), ('H',3), ('O',4), ('N',5)]
key_order = sorted([(key[i], i) for i in range(num_cols)])
# 排序后:[('H',3), ('N',5), ('O',4), ('P',0), ('T',2), ('Y',1)]
ciphertext = []
# 5. 按照排序后的密钥字母顺序读取列
for _, original_col_index in key_order:
for r in range(num_rows):
(grid[r][original_col_index])
return "".join(ciphertext)
# 示例测试
plaintext = "WE ARE DISCOVERED FLEE AT ONCE"
key = "PYTHON"
encrypted_text = encrypt(plaintext, key)
print(f"明文: {plaintext}")
print(f"密钥: {key}")
print(f"密文: {encrypted_text}")
# 预期输出: ROFOXDEECXEVLNXWIRIEACDTXESEAD

代码解释:

清理与准备:我们首先将明文和密钥中的非字母字符去除,并转换为大写,以简化处理。
计算尺寸与填充:根据明文和密钥的长度,计算出加密矩阵的行数和列数。如果明文不够填充最后一行,则使用 'X' 补齐。
构建矩阵:使用一个二维列表 `grid` 来表示加密矩阵,将处理后的明文逐个字符填入。
确定列顺序:这是关键一步。我们创建一个 `key_order` 列表,其中包含 `(密钥字母, 原始索引)` 的元组。然后,根据密钥字母对这个列表进行排序。这样,我们就得到了加密时应该按什么顺序读取原始列的指令。
读取与拼接:遍历 `key_order`,按照其中指示的原始列索引,从矩阵中逐行读取字符,并将其添加到 `ciphertext` 列表中。最后将列表拼接成字符串。

三、Python实现:解密过程

解密过程是加密的逆操作。我们需要知道密文的长度、密钥的长度,以及如何将密文重新放置到正确的列中,再逐行读取。

解密原理:


1. 计算原始矩阵尺寸:与加密时相同,计算出行数和列数。
2. 确定列长度:因为可能存在填充,所以不是所有列都等长。需要计算每个列在密文中的实际长度。
3. 重建空矩阵:创建一个与原始矩阵相同尺寸的空矩阵。
4. 填充矩阵:根据密钥的排序,我们知道密文中的哪一部分对应哪个原始列。将密文按正确的顺序和长度拆分,并填充回原始列的位置。
5. 读取明文:逐行读取填充好的矩阵,得到原始明文。
6. 移除填充:如果加密时有填充,解密后需要将填充字符移除。
def decrypt(ciphertext, key):
key = "".join(filter(, key)).upper()
if not ciphertext or not key:
return ""
num_cols = len(key)
num_rows = (len(ciphertext) / num_cols) # 密文长度就是填充后的明文长度
# 1. 根据密钥获取列的排序信息
# key_order: sorted list of (char, original_index)
key_order = sorted([(key[i], i) for i in range(num_cols)])
# 2. 计算每个原始列在密文中的长度
# 考虑填充:不是所有列都等长,特别是最后一行的填充会影响列的长度
col_lengths = [num_rows for _ in range(num_cols)]

# 实际加密时,填充字符只加到最后几列的底部
# 在加密的grid中,如果明文不足填满最后一行的所有列,
# 那么最后几列的最后一格实际上是被填充的,或者根本就没数据
# 但是密文的长度是总的 grid 长度。
#
# 更准确的计算方法是:
# 假设明文长度是 L,密钥长度是 K,则行数 R = ceil(L/K)
# 有 remainder = L % K 列是 R 行,K - remainder 列是 R-1 行
# 如果 remainder == 0,所有列都是 R 行。

# 假设密文长度就是填充后的明文长度
# 找到有多少列是 num_rows 行,有多少列是 num_rows - 1 行
# 这是一个关键步骤:计算哪些列会短一截(因为填充通常在最后几列的底部)
# total_cells = num_rows * num_cols
# num_empty_cells = total_cells - len(ciphertext) # 等同于 padding_needed

# 如果 num_rows * num_cols 大于密文长度,说明有填充
# 这些填充字符会出现在原始矩阵的某些列的底部,导致这些列的长度是 num_rows - 1
# 哪些列会少一行呢?就是那些在原始矩阵中,最后一行的末尾被填充的列。
# 加密时,填充字符会从 `plaintext[k]` 开始填入。
# 实际明文长度 `len_original_plaintext = len(ciphertext) - padding_needed`
# 计算有多少列是满的 (num_rows 行),有多少列是少一行的 (num_rows-1 行)

# num_full_rows_cols = len(ciphertext) % num_cols # 最后一行的非填充字符数量
# if num_full_rows_cols == 0 and len(ciphertext) > 0: # 如果恰好填满,所有列都是num_rows
# num_full_rows_cols = num_cols
# 解密时,密文长度就是加密时填充后的明文长度
# 我们知道总的字符数是 len(ciphertext)
# num_rows = ceil(len(ciphertext) / num_cols)
# num_cols_with_short_rows = (num_cols * num_rows) - len(ciphertext) # 有多少列会是 num_rows-1 长度

# 哪些列会是 num_rows-1 长度?是原始的 grid 中,最后几列(从右往左数)
# 但由于我们加密是按字母顺序读列,所以密文是打乱的。
# 解密时要根据 key_order 中原始列的索引来确定哪一列是短的。

# 获取原始列的索引顺序,然后判断哪些列是短的。
# 原始索引中,最后几个索引对应的列会是短的

num_short_cols = (num_cols * num_rows) - len(ciphertext) # 需要填充的格子数,也是短列的数量
# 构建一个列表,指示哪些原始列是短的
# 这些短列是原始矩阵中,最右边被填充的那部分
# 它们的原始索引是 (num_cols - num_short_cols) 到 (num_cols - 1)

# 创建一个空的解密网格
decryption_grid = [['' for _ in range(num_cols)] for _ in range(num_rows)]
# 3. 将密文块按正确顺序填充回解密网格
current_char_index = 0

# 对key_order中的每一项,我们知道它对应哪个原始列 (original_col_index)
# 以及该原始列现在在密文中的位置 (因为它已经根据key排序了)
#
# 关键在于:如何知道当前处理的这一列在原始矩阵中有多长?
#
# 我们需要知道在原始排列中,哪些列是短的。
# 假设 key = "ABC", 明文 "ABCDE" (len=5)
# num_cols = 3, num_rows = ceil(5/3) = 2
# grid:
# A B C
# D E X
#
# 原始列0 (A,D) 长度2
# 原始列1 (B,E) 长度2
# 原始列2 (C,X) 长度2
# 这里所有列都等长了,因为填充在原始矩阵的最后一列。

# 另一种情况:key="ABCD", 明文 "ABCDEF" (len=6)
# num_cols = 4, num_rows = ceil(6/4) = 2
# grid:
# A B C D
# E F X X
#
# 原始列0 (A,E) 长度2
# 原始列1 (B,F) 长度2
# 原始列2 (C,X) 长度2
# 原始列3 (D,X) 长度2
# 看起来还是所有列都等长。
# 这段逻辑需要修正:
# 实际上,`num_cols_with_short_rows` 应该表示:有多少个列是 `num_rows - 1` 高度的。
# 这些是原始列索引中,从 `num_cols - num_short_cols` 到 `num_cols - 1` 的列。
# 例如,如果 `num_cols=6`,`num_short_cols=2`,那么原始列 `4` 和 `5` 是短的。
# Let's adjust col_lengths based on original column indices
actual_col_lengths = []
for i in range(num_cols):
# 如果当前原始列的索引 i 大于等于 (num_cols - num_short_cols),则该列是短的
if i >= (num_cols - num_short_cols):
(num_rows - 1)
else:
(num_rows)

# current_char_index 用于从密文中截取子串
current_char_index = 0
# 按照密钥排序后的顺序,将密文块放回到对应的原始列
for _, original_col_index in key_order:
col_len = actual_col_lengths[original_col_index] # 该原始列的实际长度
col_data = ciphertext[current_char_index : current_char_index + col_len]

for r in range(col_len):
decryption_grid[r][original_col_index] = col_data[r]

current_char_index += col_len
# 4. 逐行读取解密网格以获取明文
decrypted_text_list = []
for r in range(num_rows):
for c in range(num_cols):
(decryption_grid[r][c])
decrypted_text = "".join(decrypted_text_list)
# 5. 移除填充字符
# 通常填充字符是 'X',但为了通用性,可以假设填充字符是最后一个重复出现的字符
# 或者如果知道填充规则,直接移除
# 我们在加密时用 'X' 填充,所以在这里移除末尾的 'X'
while ('X'):
decrypted_text = decrypted_text[:-1]
return decrypted_text
# 示例测试
decrypted_text = decrypt(encrypted_text, key)
print(f"解密密文: {encrypted_text}")
print(f"解密密钥: {key}")
print(f"解密结果: {decrypted_text}")
# 预期输出: WEAREDISCOVEREDFLEEATONCE

关于 `actual_col_lengths` 的修正解释:

在加密过程中,我们是逐行填充矩阵,然后按列读取。如果明文不够填满最后一行的所有单元格,那么这些未填充的单元格(通常会用`X`填充)会出现在原始矩阵的右侧。当按列读取时,这些包含填充字符的列以及它们右边的列会是短的(因为它们底部有一部分是填充字符,或者在更右边的列,整个底层都是填充字符)。

因此,`num_short_cols` 表示的是原始矩阵中最右侧有多少列的实际长度是 `num_rows - 1`。这些短列对应的原始索引是 `num_cols - num_short_cols` 到 `num_cols - 1`。

例如,如果 `num_cols=6`,`num_rows=5` (总共30个格子),而 `len(ciphertext)=28` (填充了2个`X`),那么 `num_short_cols = 30 - 28 = 2`。这意味着原始矩阵的最后两列(索引4和5)是短的,它们的长度是 `num_rows - 1 = 4`,而其他列的长度是 `num_rows = 5`。

所以,在解密时,我们首先要确定每个原始列(通过其原始索引)的长度,然后才能正确地从密文中截取相应长度的子串,并将其放回正确的位置。

四、完整代码示例

将上述加密和解密函数组合起来,可以得到一个完整的置换加密工具。
import math
def encrypt(plaintext, key):
plaintext = "".join(filter(, plaintext)).upper()
key = "".join(filter(, key)).upper()
if not plaintext or not key:
return ""
num_cols = len(key)
num_rows = (len(plaintext) / num_cols)
padding_needed = (num_cols * num_rows) - len(plaintext)
plaintext += 'X' * padding_needed
grid = [['' for _ in range(num_cols)] for _ in range(num_rows)]
k = 0
for r in range(num_rows):
for c in range(num_cols):
grid[r][c] = plaintext[k]
k += 1
key_order = sorted([(key[i], i) for i in range(num_cols)])

ciphertext = []
for _, original_col_index in key_order:
for r in range(num_rows):
(grid[r][original_col_index])
return "".join(ciphertext)
def decrypt(ciphertext, key):
key = "".join(filter(, key)).upper()
if not ciphertext or not key:
return ""
num_cols = len(key)
num_rows = (len(ciphertext) / num_cols)
key_order = sorted([(key[i], i) for i in range(num_cols)])

num_short_cols = (num_cols * num_rows) - len(ciphertext)
actual_col_lengths = []
for i in range(num_cols):
if i >= (num_cols - num_short_cols):
(num_rows - 1)
else:
(num_rows)

decryption_grid = [['' for _ in range(num_cols)] for _ in range(num_rows)]
current_char_index = 0
for _, original_col_index in key_order:
col_len = actual_col_lengths[original_col_index]
col_data = ciphertext[current_char_index : current_char_index + col_len]

for r in range(col_len):
decryption_grid[r][original_col_index] = col_data[r]

current_char_index += col_len
decrypted_text_list = []
for r in range(num_rows):
for c in range(num_cols):
(decryption_grid[r][c])
decrypted_text = "".join(decrypted_text_list)
while ('X'):
decrypted_text = decrypted_text[:-1]
return decrypted_text
if __name__ == "__main__":
test_plaintext = "WE ARE DISCOVERED FLEE AT ONCE"
test_key = "PYTHON"
print("--- 置换加密示例 ---")
print(f"原始明文: {test_plaintext}")
print(f"加密密钥: {test_key}")
encrypted = encrypt(test_plaintext, test_key)
print(f"加密密文: {encrypted}")
decrypted = decrypt(encrypted, test_key)
print(f"解密结果: {decrypted}")
print("--- 另一个测试 ---")
test_plaintext_2 = "CRYPTOGRAPHY IS FUN"
test_key_2 = "KEY"
print(f"原始明文: {test_plaintext_2}")
print(f"加密密钥: {test_key_2}")

encrypted_2 = encrypt(test_plaintext_2, test_key_2)
print(f"加密密文: {encrypted_2}")
decrypted_2 = decrypt(encrypted_2, test_key_2)
print(f"解密结果: {decrypted_2}")

五、安全性分析

置换加密是一种相对简单的古典密码。它的安全性主要依赖于密钥的长度和复杂性。然而,它有几个明显的弱点:
频率分析:置换加密只改变字符的位置,不改变字符本身。这意味着明文中的字母频率在密文中依然保留。攻击者可以通过统计密文中的字母频率来推断出原始语言的字母分布,从而辅助破解。
短密钥弱点:如果密钥很短,或者重复使用,攻击者可以尝试穷举所有可能的密钥排列(密钥词的字母排列)。
已知明文攻击:如果攻击者能获得一部分明文和其对应的密文,他们可以尝试找出排列规则,进而破解其余密文。
不抵抗模式:对于一些具有明显模式的明文(例如,许多重复的字符或短语),置换加密可能无法提供足够的混淆。

因此,置换加密不适用于现代通信的安全保障。它通常被认为是教学和理解密码学基本原理的好例子,或者作为更复杂加密系统(如Enigma机器)的一部分。

六、总结与展望

通过本文,我们不仅了解了置换加密(尤其是列移位密码)的原理,还亲手使用Python将其实现。从明文的清理、矩阵的构建,到根据密钥进行列的重排,再到解密的逆向操作,每一步都充满了逻辑的乐趣。

虽然置换加密在现代信息安全领域已不再实用,但它为我们揭示了“混淆”这一密码学基本思想的重要性。在现实世界中,更强大的加密算法如AES(高级加密标准)等结合了复杂的置换和替换操作,以提供更高的安全性。

希望这篇博文能激发您对密码学和Python编程的兴趣。动手实践是最好的学习方式,继续探索更复杂的加密算法,或者尝试将置换加密与其他古典密码(如凯撒密码)结合起来,创造一个“双重加密”系统,会是非常有趣的挑战!

2025-10-25


上一篇:Python安装完毕,如何迈出编程第一步?新手入门完全指南

下一篇:掌上Python编程:随时随地,让你的手机变身代码利器!