Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install gmssl
pip install pytest pytest-cov gmssl
pip install pytest pytest-cov gmssl numpy

- name: Run unit tests with coverage
run: |
Expand Down
123 changes: 123 additions & 0 deletions tests/unit/test_hill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import pytest
import numpy as np
from core.algorithms.classical.Hill import multi_inverse, Thread


class TestMultiInverse:
"""测试 multi_inverse 函数"""

def test_inverse_3_mod_26(self):
"""测试 3 在模 26 下的乘法逆元"""
result = multi_inverse(3, 26)
assert result == 9 # 3 * 9 = 27 ≡ 1 (mod 26)

def test_inverse_5_mod_26(self):
"""测试 5 在模 26 下的乘法逆元"""
result = multi_inverse(5, 26)
assert result == 21 # 5 * 21 = 105 ≡ 1 (mod 26)

def test_inverse_7_mod_26(self):
"""测试 7 在模 26 下的乘法逆元"""
result = multi_inverse(7, 26)
assert result == 15 # 7 * 15 = 105 ≡ 1 (mod 26)

def test_no_inverse_gcd_not_1(self):
"""测试当 gcd(x, m) != 1 时返回 0"""
# 2 和 26 的最大公约数是 2,不存在乘法逆元
result = multi_inverse(2, 26)
assert result == 0

def test_inverse_1_mod_n(self):
"""测试 1 在任何模数下的乘法逆元是 1"""
for n in [5, 7, 11, 26, 100]:
result = multi_inverse(1, n)
assert result == 1

def test_inverse_n_minus_1_mod_n(self):
"""测试 n-1 的乘法逆元是 n-1 (因为 (n-1)^2 = n^2 - 2n + 1 ≡ 1 (mod n))"""
for n in [5, 7, 11, 26]:
result = multi_inverse(n - 1, n)
assert result == n - 1


class TestHillKeyMatrix:
"""测试 Hill 密钥矩阵"""

def test_2x2_key_matrix(self):
"""测试 2x2 密钥矩阵"""
# 模拟 2x2 密钥矩阵
key = "6 24 1 13" # K = [[6, 24], [1, 13]]
key_list = list(map(int, key.split()))
key_arr = np.array(key_list).reshape(2, 2)
assert key_arr.shape == (2, 2)
assert key_arr[0, 0] == 6
assert key_arr[1, 1] == 13

def test_3x3_key_matrix(self):
"""测试 3x3 密钥矩阵"""
# 模拟 3x3 密钥矩阵
key = "1 2 3 4 5 6 7 8 9"
key_list = list(map(int, key.split()))
key_arr = np.array(key_list).reshape(3, 3)
assert key_arr.shape == (3, 3)
assert key_arr[0, 0] == 1
assert key_arr[2, 2] == 9

def test_matrix_determinant(self):
"""测试矩阵行列式计算"""
key_arr = np.array([6, 24, 1, 13]).reshape(2, 2)
det = np.linalg.det(key_arr)
det_int = round(det) % 26
assert det_int != 0 # 行列式必须与 26 互素才能求逆

def test_matrix_inverse_exists(self):
"""测试密钥矩阵在模 26 下是否存在逆矩阵"""
# 使用正确的 Hill 矩阵例子,gcd(det, 26) = 1
# K = [[6, 24], [1, 13]] -> det = 54, gcd(54, 26) = 2 (错误)
# 使用 K = [[3, 3], [2, 5]] -> det = 9, gcd(9, 26) = 1 (正确)
key_arr = np.array([3, 3, 2, 5]).reshape(2, 2)
det = round(np.linalg.det(key_arr))
gcd = np.gcd(int(det), 26)
assert gcd == 1 # 行列式与 26 互素


class TestHillMathProperties:
"""测试 Hill 密码的数学性质"""

def test_padding_needed(self):
"""测试需要填充的情况"""
# 如果明文长度不能被密钥矩阵行数整除,需要填充
plaintext_len = 5
row_key = 2
remain = plaintext_len % row_key
expected_padding = row_key - remain if remain != 0 else 0
assert expected_padding == 1

def test_no_padding_needed(self):
"""测试不需要填充的情况"""
plaintext_len = 6
row_key = 2
remain = plaintext_len % row_key
expected_padding = row_key - remain if remain != 0 else 0
assert expected_padding == 0

def test_letter_to_number_conversion(self):
"""测试字母到数字的转换"""
assert (ord('A') - ord('A')) % 26 == 0
assert (ord('B') - ord('A')) % 26 == 1
assert (ord('Z') - ord('A')) % 26 == 25

def test_letter_to_number_lowercase(self):
"""测试小写字母到数字的转换"""
assert (ord('a') - ord('a')) % 26 == 0
assert (ord('b') - ord('a')) % 26 == 1
assert (ord('z') - ord('a')) % 26 == 25

def test_matrix_multiplication_mod_26(self):
"""测试矩阵乘法模 26"""
# [[1, 2], [3, 4]] * [[5], [6]] mod 26
A = np.array([1, 2, 3, 4]).reshape(2, 2)
B = np.array([5, 6]).reshape(2, 1)
result = np.dot(A, B) % 26
assert result[0, 0] == (1 * 5 + 2 * 6) % 26 # 17
assert result[1, 0] == (3 * 5 + 4 * 6) % 26 # 39 % 26 = 13
172 changes: 172 additions & 0 deletions tests/unit/test_playfair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import pytest
from core.algorithms.classical.Playfair import Thread


class TestPlayfairMatrix:
"""测试 Playfair 密钥矩阵"""

def test_matrix_size(self):
"""测试密钥矩阵大小为 5x5"""
letter_matrix = [[''] * 5 for _ in range(5)]
assert len(letter_matrix) == 5
assert len(letter_matrix[0]) == 5

def test_get_matrix_index(self):
"""测试获取字符在矩阵中的位置"""
# 创建一个简单的测试矩阵
letter_matrix = [
['A', 'B', 'C', 'D', 'E'],
['F', 'G', 'H', 'I', 'K'],
['L', 'M', 'N', 'O', 'P'],
['Q', 'R', 'S', 'T', 'U'],
['V', 'W', 'X', 'Y', 'Z']
]
# A 在 (0, 0)
assert Thread.get_matrix_index('A', letter_matrix) == (0, 0)
# E 在 (0, 4)
assert Thread.get_matrix_index('E', letter_matrix) == (0, 4)
# K (替代 J) 在 (1, 4)
assert Thread.get_matrix_index('K', letter_matrix) == (1, 4)
# Z 在 (4, 4)
assert Thread.get_matrix_index('Z', letter_matrix) == (4, 4)

def test_j_not_found(self):
"""测试 J 在矩阵中找不到(返回 None)"""
letter_matrix = [
['A', 'B', 'C', 'D', 'E'],
['F', 'G', 'H', 'I', 'K'],
['L', 'M', 'N', 'O', 'P'],
['Q', 'R', 'S', 'T', 'U'],
['V', 'W', 'X', 'Y', 'Z']
]
# J 在矩阵中不存在(因为被跳过)
assert Thread.get_matrix_index('J', letter_matrix) is None


class TestPlayfairKeyProcessing:
"""测试 Playfair 密钥处理"""

def test_key_deduplication(self):
"""测试密钥去重"""
key = "PLAYFAIR"
key_list = []
for ch in key:
if ch == 'J':
ch = 'I'
if ch not in key_list:
key_list.append(ch)
assert len(key_list) == len(set(key_list))

def test_key_to_matrix_conversion(self):
"""测试密钥转换为 5x5 矩阵"""
key = "KEYWORD"
key_str = "KEYWORD"
for ch in key_str:
if ch == 'J':
ch = 'I'
if ch not in key_str[:key_str.index(ch)]:
key_str = key_str.replace(ch, '', 1)

# 添加剩余字母
letter_list = 'ABCDEFGHIKLMNOPQRSTUVWXYZ'
for ch in letter_list:
if ch not in key_str:
key_str += ch

# 转换为 5x5 矩阵
letter_matrix = [''] * 5
j = 0
for i in range(len(key_str)):
letter_matrix[j] += key_str[i]
if (i + 1) % 5 == 0:
j += 1

assert len(letter_matrix) == 5
assert len(letter_matrix[0]) == 5


class TestPlayfairEncryptionRules:
"""测试 Playfair 加密规则"""

def test_same_row_encryption(self):
"""测试同一行的加密规则"""
# 如果两个字符在同一行,则取其右边的字符
letter_matrix = [
['A', 'B', 'C', 'D', 'E'],
['F', 'G', 'H', 'I', 'K'],
['L', 'M', 'N', 'O', 'P'],
['Q', 'R', 'S', 'T', 'U'],
['V', 'W', 'X', 'Y', 'Z']
]
# A(0,0) 和 B(0,1) 在同一行
# 加密后应该是 B(0,1) 和 C(0,2)
row, col = 0, 0
new_col = (col + 1) % 5
assert letter_matrix[row][new_col] == 'B'

def test_same_column_encryption(self):
"""测试同一列的加密规则"""
# 如果两个字符在同一列,则取其下方的字符
letter_matrix = [
['A', 'B', 'C', 'D', 'E'],
['F', 'G', 'H', 'I', 'K'],
['L', 'M', 'N', 'O', 'P'],
['Q', 'R', 'S', 'T', 'U'],
['V', 'W', 'X', 'Y', 'Z']
]
# A(0,0) 和 F(1,0) 在同一列
# 加密后应该是 F(1,0) 和 L(2,0)
row, col = 0, 0
new_row = (row + 1) % 5
assert letter_matrix[new_row][col] == 'F'

def test_different_row_column_encryption(self):
"""测试不同行不同列的加密规则"""
# 如果两个字符既不在同一行也不在同一列,则取其对角位置的字符
letter_matrix = [
['A', 'B', 'C', 'D', 'E'],
['F', 'G', 'H', 'I', 'K'],
['L', 'M', 'N', 'O', 'P'],
['Q', 'R', 'S', 'T', 'U'],
['V', 'W', 'X', 'Y', 'Z']
]
# A(0,0) 和 G(1,1) 不在同一行也不在同一列
# 加密规则:取同行不同列的对角字符
# A(0,0) -> matrix[0][1] = 'B'
# G(1,1) -> matrix[1][0] = 'F'
x = (0, 0)
y = (1, 1)
# 同列交换:matrix[x[0]][y[1]], matrix[y[0]][x[1]]
assert letter_matrix[x[0]][y[1]] == 'B' # A -> B
assert letter_matrix[y[0]][x[1]] == 'F' # G -> F


class TestPlayfairMathProperties:
"""测试 Playfair 数学性质"""

def test_padding_for_odd_length(self):
"""测试奇数长度明文需要填充"""
plaintext = "HELLO"
if len(plaintext) % 2 != 0:
plaintext += 'Z'
assert len(plaintext) % 2 == 0

def test_no_padding_for_even_length(self):
"""测试偶数长度明文不需要填充"""
plaintext = "HELP"
if len(plaintext) % 2 != 0:
plaintext += 'Z'
assert len(plaintext) % 2 == 0
assert len(plaintext) == 4

def test_repeated_letter_insertion(self):
"""测试重复字母插入 Q"""
plaintext = "BOOK"
# B-O-O-K 应该变成 B-O-Q-O-K (在重复的 O 之间插入 Q)
result = []
for i in range(len(plaintext)):
result.append(plaintext[i])
if i < len(plaintext) - 1 and plaintext[i] == plaintext[i + 1]:
result.append('Q')
# 但是由于实际算法会处理,这里只是验证插入逻辑
assert len(result) >= len(plaintext)
Loading