Initial commit

This commit is contained in:
user
2026-07-12 14:06:45 +04:00
commit a75028d68f
148 changed files with 3275 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
import json
import sys
from cli.parse_args import parse_args
def main() -> int:
args = parse_args()
try:
if args.cmd == "keygen":
# Класс для режима генерации ключей
from modes.keygen import KeygenMode
runner = KeygenMode()
result = runner.run(
p=args.p,
q=args.q,
min=args.min,
max=args.max,
bits=args.bits,
seed=args.seed,
)
elif args.cmd == "encrypt":
# Класс для режима шифрования
from modes.encrypt import EncryptMode
runner = EncryptMode()
result = runner.run(
text=args.text,
m=args.m,
n=args.n,
e=args.e,
)
elif args.cmd == "decrypt":
# Класс для режима расшифрования
from modes.decrypt import DecryptMode
runner = DecryptMode()
result = runner.run(
c=args.c,
n=args.n,
d=args.d,
)
elif args.cmd == "auto":
from modes.auto import AutoMode
runner = AutoMode()
result = runner.run(
text=args.text,
m=args.m,
p=args.p,
q=args.q,
min=args.min,
max=args.max,
bits=args.bits,
seed=args.seed,
)
else:
raise RuntimeError(f"Неизвестная команда: {args.cmd}")
# Печать результата, если он что-то возвращает
if result is not None:
if isinstance(result, (dict, list)):
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(result)
return 0
except Exception as exc:
print(f"Ошибка: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
main()
@@ -0,0 +1,104 @@
from argparse import ArgumentParser, RawDescriptionHelpFormatter
def _int_auto(x: str) -> int:
return int(x, 0)
def parse_args(argv=None):
ap = ArgumentParser(
description=(
"RSA Algorithm CLI with refactored argument handling.\n\n"
"Examples:\n"
" app.py keygen --bits 8\n"
" app.py encrypt --text \"RSA\" --e 17 --n 0xca1\n"
" app.py decrypt --c 0x357 --n 0xca1 --d 0xac1\n"
" app.py auto --text \"RSA\" --bits 8\n"
" app.py auto --m 65 --min 100 --max 1000\n"
),
formatter_class=RawDescriptionHelpFormatter,
)
sub = ap.add_subparsers(dest="cmd", required=True)
# -------- parents to deduplicate args --------
# Общие параметры генерации ключей (используются в keygen и auto)
parent_keygen = ArgumentParser(add_help=False)
parent_keygen.add_argument("--p", type=_int_auto, help="Простое число p.")
parent_keygen.add_argument("--q", type=_int_auto, help="Простое число q.")
parent_keygen.add_argument(
"--min",
type=int,
default=2,
help="Нижняя граница поиска простых (при случайной генерации диапазоном).",
)
parent_keygen.add_argument(
"--max",
type=int,
help="Верхняя граница поиска простых (при случайной генерации диапазоном).",
)
parent_keygen.add_argument(
"--bits",
type=int,
help="Количество бит для случайной генерации простых (альтернатива min/max).",
)
parent_keygen.add_argument(
"--seed", type=int, help="Seed для ГСЧ (опционально, для воспроизводимости)."
)
# Общий модуль n (используется в encrypt и decrypt)
parent_n = ArgumentParser(add_help=False)
parent_n.add_argument("--n", type=_int_auto,
required=True, help="Модуль n.")
# Общий ввод открытого сообщения (используется в encrypt и auto)
parent_plain = ArgumentParser(add_help=False)
plain_group = parent_plain.add_mutually_exclusive_group(required=True)
plain_group.add_argument(
"--text", type=str, help="Открытый текст (ASCII).")
plain_group.add_argument(
"--m", type=_int_auto, help="Открытое сообщение как неотрицательное целое."
)
# -------- keygen --------
sub.add_parser(
"keygen",
help="Сгенерировать приватный (p, q, d, n) и публичный (e, n) ключи.",
formatter_class=RawDescriptionHelpFormatter,
parents=[parent_keygen],
)
# -------- encrypt --------
sp_enc = sub.add_parser(
"encrypt",
help="Зашифровать сообщение публичным ключом (e, n).",
formatter_class=RawDescriptionHelpFormatter,
parents=[parent_n, parent_plain],
)
sp_enc.add_argument(
"--e", type=_int_auto, required=True, help="Публичная экспонента e (простое)."
)
# -------- decrypt --------
sp_dec = sub.add_parser(
"decrypt",
help="Расшифровать шифртекст приватным ключом (n, d).",
formatter_class=RawDescriptionHelpFormatter,
parents=[parent_n],
)
sp_dec.add_argument(
"--c", type=_int_auto, required=True, help="Шифртекст как неотрицательное целое."
)
sp_dec.add_argument(
"--d", type=_int_auto, required=True, help="Приватная экспонента d."
)
# -------- auto --------
sub.add_parser(
"auto",
help="Автоматический режим: как keygen + обязательный ввод сообщения.",
formatter_class=RawDescriptionHelpFormatter,
parents=[parent_keygen, parent_plain],
)
return ap.parse_args(argv)
@@ -0,0 +1,88 @@
import random
import math
class PrimeNumber:
"""
Класс-обёртка для простого числа.
Контракт: внутри всегда хранится валидное простое число.
"""
MAX_ATTEMPTS: int = 1000 # лимит попыток генерации
def __init__(self, value: int):
if not self._is_prime(value):
raise ValueError(f"Число {value} не является простым")
self._value = value
@classmethod
def from_value(cls, value: int) -> "PrimeNumber":
"""Фабричный метод: создать простое число из заданного значения."""
return cls(value)
@classmethod
def random(
cls,
min_value: int | None = None,
max_value: int | None = None,
seed: int | None = None
) -> "PrimeNumber":
"""
Случайное простое число из диапазона [min_value, max_value].
Если seed указан, ГСЧ инициализируется для воспроизводимости.
"""
if min_value < 2:
raise ValueError("Нижняя граница min_value должна быть ≥ 2")
if min_value > max_value:
raise ValueError("min_value не может быть больше max_value")
if seed is not None:
random.seed(seed)
for _ in range(cls.MAX_ATTEMPTS):
candidate = random.randint(min_value, max_value)
if cls._is_prime(candidate):
return cls(candidate)
raise ValueError(
f"Не удалось найти простое число в диапазоне [{
min_value}, {max_value}] "
f"за {cls.MAX_ATTEMPTS} попыток"
)
@classmethod
def random_bits(cls, bits: int, seed: int | None = None) -> "PrimeNumber":
if bits < 2:
raise ValueError("Количество бит должно быть не меньше 2")
# Нижняя граница — 2^(bits-1), верхняя — 2^bits - 1
min_value = 1 << (bits - 1)
max_value = (1 << bits) - 1
return cls.random(
min_value=min_value,
max_value=max_value,
seed=seed,
)
@property
def value(self) -> int:
return self._value
def __repr__(self) -> str:
return f"PrimeNumber({self._value})"
@staticmethod
def _is_prime(n: int) -> bool:
"""Проверка на простоту (НУ ОООЧЕНЬ МЕДЛЕННАЯ)."""
if n < 2:
return False
if n in (2, 3):
return True
if n % 2 == 0:
return False
limit = int(math.isqrt(n)) + 1
for i in range(3, limit, 2):
if n % i == 0:
return False
return True
@@ -0,0 +1,51 @@
from math import gcd
from .prime_number import PrimeNumber
from .public_key import PublicKey
class PrivateKey:
"""
Закрытый ключ RSA.
p1, p2 — экземпляры PrimeNumber.
e — экземпляр PrimeNumber (открытая степень).
n = p1 * p2 (int).
"""
def __init__(self, p1: PrimeNumber, p2: PrimeNumber):
if not isinstance(p1, PrimeNumber) or not isinstance(p2, PrimeNumber):
raise TypeError("p1 и p2 должны быть экземплярами PrimeNumber")
if p1.value == p2.value:
raise ValueError("p1 и p2 должны быть разными простыми")
self.p1 = p1
self.p2 = p2
self.n = p1.value * p2.value
self.phi = (p1.value - 1) * (p2.value - 1)
self.e = self._choose_e()
self.d = self._compute_d()
def _choose_e(self) -> PrimeNumber:
# ищем простое e, взаимнопростое с φ(n)
candidate = 3
while candidate < self.phi:
if gcd(candidate, self.phi) == 1 and PrimeNumber._is_prime(candidate):
return PrimeNumber(candidate)
candidate += 2
raise ValueError("Не удалось подобрать e")
def _compute_d(self) -> int:
k = 1
while True:
if (k * self.phi + 1) % self.e.value == 0:
return (k * self.phi + 1) // self.e.value
k += 1
def make_public_key(self) -> PublicKey:
return PublicKey(self.e, self.n)
def decrypt(self, c: int) -> int:
if not isinstance(c, int) or c < 0:
raise ValueError(
"Шифртекст c должен быть неотрицательным целым числом")
return pow(c, self.d, self.n)
@@ -0,0 +1,23 @@
from .prime_number import PrimeNumber
class PublicKey:
"""
Публичный ключ (e, n).
e — экземпляр PrimeNumber (простое число).
n — целое (составное число p1*p2).
"""
def __init__(self, e: PrimeNumber, n: int):
if not isinstance(e, PrimeNumber):
raise TypeError("e должно быть экземпляром PrimeNumber")
if not isinstance(n, int):
raise TypeError("n должно быть int (составное число)")
self.e = e
self.n = n
def encrypt(self, m: int) -> int:
if not isinstance(m, int) or m < 0:
raise ValueError(
"Сообщение m должно быть неотрицательным целым числом")
return pow(m, self.e.value, self.n)
+68
View File
@@ -0,0 +1,68 @@
from __future__ import annotations
from typing import Dict, Any
from modes.keygen import KeygenMode
from modes.encrypt import EncryptMode
from modes.decrypt import DecryptMode
class AutoMode:
"""
Автоматический режим:
1. Генерация ключей (p, q, n, e, d, phi).
2. Шифрование текста или числа.
3. Расшифрование результата обратно.
"""
def run(
self,
*,
text: str | None,
m: int | None,
p: int | None = None,
q: int | None = None,
min: int | None = None,
max: int | None = None,
bits: int | None = None,
seed: int | None = None,
) -> Dict[str, Any]:
# 1. Генерация ключей
keygen = KeygenMode()
keys = keygen.run(
p=p,
q=q,
min=min,
max=max,
bits=bits,
seed=seed,
)
n = keys["n"]["dec"]
e = keys["e"]["dec"]
d = keys["d"]["dec"]
# 2. Шифрование
encrypt = EncryptMode()
encrypted = encrypt.run(
text=text,
m=m,
n=n,
e=e,
)
# 3. Расшифрование
decrypt = DecryptMode()
# В зависимости от формата encrypted["output"]
if encrypted["mode"] == "text":
ciphers = [item["cipher"] for item in encrypted["output"]]
decrypted = decrypt.run(c=ciphers, n=n, d=d)
else:
cipher = encrypted["output"]["cipher"]
decrypted = decrypt.run(c=cipher, n=n, d=d)
return {
"keys": keys,
"encrypted": encrypted,
"decrypted": decrypted,
}
@@ -0,0 +1,55 @@
# modes/decrypt.py
from __future__ import annotations
from typing import Dict, Any, List
class DecryptMode:
def run(
self,
*,
c: int | List[int],
n: int,
d: int,
) -> Dict[str, Any]:
# Один блок
if isinstance(c, int):
plain = pow(c, d, n) # Дешифратор
char = chr(plain) if 0 <= plain < 128 else None
return {
"mode": "int",
"input": {
"cipher": c,
"hex_in": hex(c),
},
"output": {
"ascii": plain,
"char": char,
"hex_out": hex(plain),
},
}
# Несколько блоков (список чисел)
if isinstance(c, list):
result: List[Dict[str, Any]] = []
text = ""
for block in c:
plain = pow(block, d, n) # Дешифратор
char = chr(plain) if 0 <= plain < 128 else None
if char:
text += char
result.append({
"cipher": block,
"hex_in": hex(block),
"ascii": plain,
"char": char,
"hex_out": hex(plain),
})
return {
"mode": "list",
"input": c,
"output": result,
"text": text,
}
raise ValueError("c должен быть числом или списком чисел")
@@ -0,0 +1,57 @@
from __future__ import annotations
from typing import Dict, Any, List
from core.public_key import PublicKey
from core.prime_number import PrimeNumber
class EncryptMode:
def run(
self,
*,
text: str | None,
m: int | None,
n: int,
e: int,
) -> Dict[str, Any]:
pub = PublicKey(n=n, e=PrimeNumber(e))
if text is not None:
result: List[Dict[str, Any]] = []
for ch in text:
code = ord(ch)
cipher = pub.encrypt(code)
result.append({
"char": ch,
"ascii": code,
"hex_in": hex(code),
"cipher": cipher,
"hex_out": hex(cipher),
})
return {
"mode": "text",
"input": text,
"output": result,
}
if m is not None:
cipher = pub.encrypt(m)
char = chr(m) if 0 <= m < 128 else None
return {
"mode": "int",
"input": {
"char": char,
"ascii": m,
"hex_in": hex(m),
},
"output": {
"char": char,
"ascii": m,
"cipher": cipher,
"hex_out": hex(cipher),
},
}
raise ValueError("Необходимо указать либо text, либо m")
+133
View File
@@ -0,0 +1,133 @@
from __future__ import annotations
from typing import Dict, Any
from core.prime_number import PrimeNumber
from core.private_key import PrivateKey
class KeygenMode:
MAX_BITS: int = 2**5
MIN_VALUE: int = 2
MAX_VALUE: int = (1 << MAX_BITS) - 1
MAX_ATTEMPTS: int = 1000
def run(
self,
*,
p: int | None,
q: int | None,
min: int | None = None,
max: int | None = None,
bits: int | None = None,
seed: int | None = None,
) -> Dict[str, Any]:
if min is None:
min = self.MIN_VALUE
if max is None:
max = self.MAX_VALUE
self._validate_inputs(p, q, min, max, bits)
P = self._generate_prime(p, min, max, bits, seed)
Q = self._generate_distinct_prime(P, q, min, max, bits, seed)
priv = PrivateKey(P, Q)
pub = priv.make_public_key()
return {
"p": {
"dec": P.value,
"hex": hex(P.value),
},
"q": {
"dec": Q.value,
"hex": hex(Q.value),
},
"n": {
"dec": priv.n,
"hex": hex(priv.n),
},
"phi": {
"dec": priv.phi,
"hex": hex(priv.phi),
},
"e": {
"dec": pub.e.value,
"hex": hex(pub.e.value),
},
"d": {
"dec": priv.d,
"hex": hex(priv.d),
},
}
# ----------------- helpers -----------------
def _validate_inputs(
self,
p: int | None,
q: int | None,
min: int,
max: int,
bits: int | None,
) -> None:
if p is not None and q is not None and p == q:
raise ValueError("p и q должны быть разными простыми числами.")
if bits is not None and bits > self.MAX_BITS:
raise ValueError(f"Максимальная длина простого: {
self.MAX_BITS} бит.")
if min > max:
raise ValueError("min не может быть больше max.")
if max > self.MAX_VALUE:
raise ValueError(
f"max не может превышать {self.MAX_VALUE} (ограничение {
self.MAX_BITS} бит)."
)
if min == max:
raise ValueError("min и max не должны совпадать.")
def _generate_prime(
self,
value: int | None,
min: int,
max: int,
bits: int | None,
seed: int | None,
) -> PrimeNumber:
if value is not None:
return PrimeNumber.from_value(value)
if bits is not None:
return PrimeNumber.random_bits(bits=bits, seed=seed)
else:
return PrimeNumber.random(min_value=min, max_value=max, seed=seed)
return PrimeNumber.random_bits(bits=self.MAX_BITS, seed=seed)
def _generate_distinct_prime(
self,
other: PrimeNumber,
value: int | None,
min: int,
max: int,
bits: int | None,
seed: int | None,
) -> PrimeNumber:
attempts = 0
while True:
effective_seed = None if seed is None else seed + attempts
candidate = self._generate_prime(
value, min, max, bits, effective_seed)
if candidate.value != other.value:
return candidate
attempts += 1
if attempts >= self.MAX_ATTEMPTS:
raise RuntimeError(
f"Не удалось сгенерировать q, отличное от p, за {
self.MAX_ATTEMPTS} попыток."
)
@@ -0,0 +1,26 @@
import pytest
from core.prime_number import PrimeNumber
def test_from_value_valid():
p = PrimeNumber.from_value(61)
assert p.value == 61
assert repr(p) == "PrimeNumber(61)"
def test_from_value_invalid():
with pytest.raises(ValueError):
PrimeNumber.from_value(60) # не простое
def test_random_in_range():
p = PrimeNumber.random(2, 20)
assert isinstance(p, PrimeNumber)
assert 2 <= p.value <= 20
# дополнительно проверим, что оно действительно простое
assert PrimeNumber._is_prime(p.value)
def test_random_no_primes():
with pytest.raises(ValueError):
PrimeNumber.random(90, 92) # в этом диапазоне простых нет
@@ -0,0 +1,50 @@
import pytest
from core.private_key import PrivateKey
from core.prime_number import PrimeNumber
def test_encrypt_decrypt_cycle():
"""Проверка: сообщение после encrypt → decrypt восстанавливается"""
p1 = PrimeNumber.from_value(61)
p2 = PrimeNumber.from_value(53)
priv = PrivateKey(p1, p2)
pub = priv.make_public_key()
messages = [1, 2, 42, 123, priv.n - 1]
for m in messages:
c = pub.encrypt(m)
m_back = priv.decrypt(c)
assert m == m_back
def test_e_is_prime_and_coprime_with_phi():
"""e должно быть простым и взаимно простым с φ(n)"""
p1 = PrimeNumber.from_value(61)
p2 = PrimeNumber.from_value(53)
priv = PrivateKey(p1, p2)
assert isinstance(priv.e, PrimeNumber)
assert (priv.e.value > 1)
assert (priv.phi % priv.e.value != 0)
def test_d_is_inverse_of_e_mod_phi():
"""e*d ≡ 1 (mod φ(n))"""
p1 = PrimeNumber.from_value(61)
p2 = PrimeNumber.from_value(53)
priv = PrivateKey(p1, p2)
assert (priv.e.value * priv.d) % priv.phi == 1
def test_decrypt_invalid_inputs():
"""Должны бросаться ошибки при неверных входных данных"""
p1 = PrimeNumber.from_value(61)
p2 = PrimeNumber.from_value(53)
priv = PrivateKey(p1, p2)
with pytest.raises(ValueError):
priv.decrypt(-5)
with pytest.raises(ValueError):
priv.decrypt("not a number") # type: ignore
@@ -0,0 +1,47 @@
import pytest
from core.prime_number import PrimeNumber
from core.public_key import PublicKey
def test_encrypt_basic_cycle():
"""Шифрование совпадает с pow(m, e, n)"""
e = PrimeNumber.from_value(3)
n = 11 # n может быть простым или составным числом
pub = PublicKey(e, n)
for m in range(1, 10):
expected = pow(m, e.value, n)
assert pub.encrypt(m) == expected
def test_encrypt_with_composite_n():
"""n = p1 * p2 должно работать корректно"""
p1 = PrimeNumber.from_value(5)
p2 = PrimeNumber.from_value(11)
n = p1.value * p2.value
e = PrimeNumber.from_value(3)
pub = PublicKey(e, n)
m = 7
expected = pow(m, e.value, n)
assert pub.encrypt(m) == expected
def test_encrypt_rejects_negative():
"""Отрицательные сообщения запрещены"""
e = PrimeNumber.from_value(3)
n = 11
pub = PublicKey(e, n)
with pytest.raises(ValueError):
pub.encrypt(-1)
def test_encrypt_rejects_non_integer():
"""Сообщение должно быть int"""
e = PrimeNumber.from_value(3)
n = 11
pub = PublicKey(e, n)
with pytest.raises(ValueError):
pub.encrypt(3.14)
Binary file not shown.