Initial commit
This commit is contained in:
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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.
Reference in New Issue
Block a user