Initial commit
@@ -0,0 +1,37 @@
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Build files
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints/
|
||||
|
||||
# Test and coverage
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
@@ -0,0 +1,109 @@
|
||||
from argparse import ArgumentParser
|
||||
from secrets import randbelow
|
||||
|
||||
from utils.check_case import check_case
|
||||
from utils.format_line import format_line
|
||||
from orchestration.param_generator import ParamGenerator
|
||||
|
||||
|
||||
def _random_secret() -> int:
|
||||
# 2..9999 включительно (меньше 10000, исключаем 0/1)
|
||||
return 2 + randbelow(9998)
|
||||
|
||||
|
||||
def run_auto(count: int) -> None:
|
||||
"""
|
||||
Автоматический режим:
|
||||
- случайно генерируем (p, q, g);
|
||||
- случайно выбираем x, y (<10000);
|
||||
- вычисляем K и проверяем через check_case;
|
||||
- печатаем результат через format_line
|
||||
"""
|
||||
for i in range(1, count + 1):
|
||||
p, q, g = ParamGenerator.generate() # q выводим для информации
|
||||
x = _random_secret()
|
||||
y = _random_secret()
|
||||
|
||||
# Вычислим K заранее, чтобы передать как эталон в check_case
|
||||
# (тем самым используем общий хелпер единообразно)
|
||||
ok, (A, B, K_a, K_b) = check_case(p, g, x, y, K_expected=None)
|
||||
# Когда K_expected=None, наша старая версия check_case
|
||||
# сравнивала строго с эталоном. Если у тебя именно так,
|
||||
# просто заменим на повторный вызов с «эталоном = K_a»
|
||||
if K_a != K_b:
|
||||
ok = False
|
||||
else:
|
||||
ok, (A, B, K_a, K_b) = check_case(p, g, x, y, K_expected=K_a)
|
||||
|
||||
print(f"[auto #{i}] p={p} q={q} g={g}")
|
||||
print(format_line(i, A, B, K_a, K_b, ok))
|
||||
|
||||
|
||||
def run_manual(p: int, g: int, x: int, y: int, k: int | None) -> None:
|
||||
"""
|
||||
Ручной режим:
|
||||
- принимаем p, g, x, y;
|
||||
- если указан k — проверяем с эталоном;
|
||||
- если k не указан — просто считаем и показываем
|
||||
"""
|
||||
if k is None:
|
||||
ok, (A, B, K_a, K_b) = check_case(p, g, x, y, K_expected=None)
|
||||
ok = (K_a == K_b)
|
||||
print(f"[manual] p={p} g={g}")
|
||||
print(format_line(1, A, B, K_a, K_b, ok))
|
||||
else:
|
||||
ok, (A, B, K_a, K_b) = check_case(p, g, x, y, K_expected=k)
|
||||
print(f"[manual] p={p} g={g} K_expected={k}")
|
||||
print(format_line(1, A, B, K_a, K_b, ok))
|
||||
|
||||
|
||||
def parse_args():
|
||||
ap = ArgumentParser(
|
||||
description=(
|
||||
"Demo Diffie–Hellman: режимы manual и auto\n"
|
||||
"Auto: случайно генерирует (p,q,g) и секреты (<10000)\n"
|
||||
"Manual: используется заданный набор параметров"
|
||||
)
|
||||
)
|
||||
ap.add_argument(
|
||||
"--mode",
|
||||
choices=["auto", "manual"],
|
||||
default="auto",
|
||||
help="Режим работы (auto|manual). По умолчанию auto",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--count",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Сколько генераций в auto-режиме (по умолчанию 5)",
|
||||
)
|
||||
ap.add_argument("--p", type=int, help="Модуль (prime_modulus) в manual")
|
||||
ap.add_argument("--g", type=int, help="Генератор (generator) в manual")
|
||||
ap.add_argument("--x", type=int, help="Секрет Алисы (<10000) в manual")
|
||||
ap.add_argument("--y", type=int, help="Секрет Боба (<10000) в manual")
|
||||
ap.add_argument(
|
||||
"--k",
|
||||
type=int,
|
||||
help="Эталонный общий ключ для проверки в manual (необязательно)",
|
||||
)
|
||||
return ap.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
if args.mode == "auto":
|
||||
run_auto(count=args.count)
|
||||
return
|
||||
|
||||
# manual
|
||||
missing = [n for n, v in [("p", args.p), ("g", args.g),
|
||||
("x", args.x), ("y", args.y)] if v is None]
|
||||
if missing:
|
||||
need = ", ".join(missing)
|
||||
raise SystemExit(f"manual-режим: необходимо указать параметры: {need}")
|
||||
run_manual(p=args.p, g=args.g, x=args.x, y=args.y, k=args.k)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,19 @@
|
||||
from secrets import randbelow
|
||||
from .public_params import PublicParams
|
||||
|
||||
|
||||
class Participant:
|
||||
"""
|
||||
Участник протокола (Алиса или Боб).
|
||||
Держит свой секрет, публикует g^secret mod p,
|
||||
умеет вычислять общий ключ.
|
||||
"""
|
||||
|
||||
def __init__(self, params: PublicParams, secret: int | None = None):
|
||||
self.params = params
|
||||
self.secret = secret if secret is not None else (2 + randbelow(9998))
|
||||
self.public_value = pow(self.params.generator,
|
||||
self.secret, self.params.prime_modulus)
|
||||
|
||||
def compute_shared_key(self, other_public: int) -> int:
|
||||
return pow(other_public, self.secret, self.params.prime_modulus)
|
||||
@@ -0,0 +1,12 @@
|
||||
def is_prime(n: int) -> bool:
|
||||
"""Проверка на простоту"""
|
||||
if n < 2:
|
||||
return False
|
||||
if n % 2 == 0:
|
||||
return n == 2
|
||||
d = 3
|
||||
while d * d <= n:
|
||||
if n % d == 0:
|
||||
return False
|
||||
d += 2
|
||||
return True
|
||||
@@ -0,0 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
from .prime_utils import is_prime
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PublicParams:
|
||||
"""
|
||||
Публичные параметры протокола:
|
||||
- prime_modulus: простое число p
|
||||
- generator: порождающий элемент g
|
||||
"""
|
||||
prime_modulus: int
|
||||
generator: int
|
||||
|
||||
def __post_init__(self):
|
||||
p = self.prime_modulus
|
||||
g = self.generator
|
||||
|
||||
# Проверка простоты модуля
|
||||
if not is_prime(p):
|
||||
raise ValueError(f"prime_modulus={p} не является простым числом")
|
||||
|
||||
# Проверка допустимости генератора
|
||||
if g <= 1 or g >= p - 1:
|
||||
raise ValueError(
|
||||
f"generator={g} вне допустимого диапазона (2..p-2)")
|
||||
|
||||
# Проверка, что генератор не сворачивается в малую подгруппу
|
||||
if pow(g, (p - 1) // 2, p) == 1:
|
||||
raise ValueError(f"generator={g} не является корректным для p={p}")
|
||||
@@ -0,0 +1,44 @@
|
||||
from secrets import randbelow, randbits
|
||||
from core.prime_utils import is_prime
|
||||
|
||||
|
||||
class ParamGenerator:
|
||||
"""
|
||||
Генератор параметров протокола Диффи–Хеллмана.
|
||||
Возвращает тройку (p, q, g):
|
||||
- p = 2*q + 1, где p и q простые
|
||||
- g — корректный генератор для группы по модулю p
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def generate() -> tuple[int, int, int]:
|
||||
"""
|
||||
Случайным образом выбирает размерность (кол-во бит) для q,
|
||||
подбирает безопасное простое p и генератор g.
|
||||
Возвращает (p, q, g).
|
||||
"""
|
||||
# Размер числа выбираем случайно от 8 до 16 бит (от 256 до 65 535)
|
||||
bits = 8 + randbelow(9)
|
||||
|
||||
# Генерация безопасного простого
|
||||
while True:
|
||||
q = randbits(bits - 1) | 1 # нечётное число
|
||||
if not is_prime(q):
|
||||
continue
|
||||
p = 2 * q + 1
|
||||
if not is_prime(p):
|
||||
continue
|
||||
# Найдём генератор
|
||||
g = ParamGenerator._find_generator(p, q)
|
||||
return p, q, g
|
||||
|
||||
@staticmethod
|
||||
def _find_generator(p: int, q: int) -> int:
|
||||
"""
|
||||
Ищет генератор g: минимальное число 2..p-2,
|
||||
для которого g^q mod p != 1.
|
||||
"""
|
||||
for g in range(2, p - 1):
|
||||
if pow(g, q, p) != 1:
|
||||
return g
|
||||
raise ValueError("Не удалось найти генератор для p")
|
||||
@@ -0,0 +1,16 @@
|
||||
from core.participant import Participant
|
||||
from core.public_params import PublicParams
|
||||
|
||||
|
||||
class Alice:
|
||||
def __init__(self, params: PublicParams, secret: int | None = None):
|
||||
self.party = Participant(params, secret=secret)
|
||||
self.shared_key = None
|
||||
|
||||
@property
|
||||
def A(self) -> int:
|
||||
return self.party.public_value
|
||||
|
||||
def receive_B(self, B: int):
|
||||
self.shared_key = self.party.compute_shared_key(B)
|
||||
return self.shared_key
|
||||
@@ -0,0 +1,16 @@
|
||||
from core.participant import Participant
|
||||
from core.public_params import PublicParams
|
||||
|
||||
|
||||
class Bob:
|
||||
def __init__(self, params: PublicParams, secret: int | None = None):
|
||||
self.party = Participant(params, secret=secret)
|
||||
self.shared_key = None
|
||||
|
||||
@property
|
||||
def B(self) -> int:
|
||||
return self.party.public_value
|
||||
|
||||
def receive_A(self, A: int):
|
||||
self.shared_key = self.party.compute_shared_key(A)
|
||||
return self.shared_key
|
||||
@@ -0,0 +1,30 @@
|
||||
from utils.check_case import check_case
|
||||
from utils.format_line import format_line
|
||||
|
||||
CASES = [
|
||||
(29, 2, 60, 82, 23),
|
||||
(53, 2, 20, 77, 42),
|
||||
(73, 5, 37, 47, 31),
|
||||
(31, 3, 31, 51, 15),
|
||||
(41, 6, 31, 98, 8),
|
||||
(19, 2, 20, 73, 4),
|
||||
(71, 7, 30, 89, 45),
|
||||
(109, 6, 7, 14, 84),
|
||||
(131, 2, 88, 15, 52),
|
||||
(97, 5, 58, 68, 6),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
all_ok = True
|
||||
for i, case in enumerate(CASES, start=1):
|
||||
ok, (A, B, K_a, K_b) = check_case(*case)
|
||||
print(format_line(i, A, B, K_a, K_b, ok))
|
||||
if not ok:
|
||||
all_ok = False
|
||||
if not all_ok:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
#
|
||||
@@ -0,0 +1,21 @@
|
||||
from core.public_params import PublicParams
|
||||
from core.participant import Participant
|
||||
|
||||
|
||||
def check_case(prime_modulus, generator, x, y, K_expected):
|
||||
"""
|
||||
Строит участников, имитирует обмен и проверяет совпадение ключа с эталоном.
|
||||
Возвращает: ok (bool), (A, B, K_a, K_b)
|
||||
"""
|
||||
params = PublicParams(prime_modulus, generator)
|
||||
alice = Participant(params, secret=x)
|
||||
bob = Participant(params, secret=y)
|
||||
|
||||
A = alice.public_value
|
||||
B = bob.public_value
|
||||
|
||||
K_a = alice.compute_shared_key(B)
|
||||
K_b = bob.compute_shared_key(A)
|
||||
|
||||
ok = (K_a == K_b == K_expected)
|
||||
return ok, (A, B, K_a, K_b)
|
||||
@@ -0,0 +1,13 @@
|
||||
def format_line(i, A, B, K_a, K_b, ok):
|
||||
"""
|
||||
Форматирует вывод строки для теста.
|
||||
"""
|
||||
status = "OK" if ok else "FAIL"
|
||||
return (
|
||||
f"#{i:>2}: "
|
||||
f"A={A:>6} "
|
||||
f"B={B:>6} "
|
||||
f"K_A={K_a:>6} "
|
||||
f"K_B={K_b:>6} "
|
||||
f"-> {status}"
|
||||
)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,68 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from cli.parse_args import parse_args
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
try:
|
||||
if args.cmd == "hash":
|
||||
from modes.hash_mode import HashMode
|
||||
runner = HashMode()
|
||||
result = runner.run(
|
||||
variant=args.variant,
|
||||
message=args.message,
|
||||
salt=args.salt,
|
||||
bytes=args.bytes,
|
||||
)
|
||||
|
||||
elif args.cmd == "avalanche":
|
||||
from modes.test_avalanche import AvalancheTest
|
||||
runner = AvalancheTest()
|
||||
result = runner.run(
|
||||
variant=args.variant,
|
||||
message=args.message,
|
||||
salt=args.salt,
|
||||
bytes=args.bytes,
|
||||
)
|
||||
|
||||
elif args.cmd == "collisions":
|
||||
from modes.test_collisions import CollisionsTest
|
||||
runner = CollisionsTest()
|
||||
result = runner.run(
|
||||
variant=args.variant,
|
||||
max_len=args.max_len,
|
||||
alphabet=args.alphabet,
|
||||
bytes=args.bytes,
|
||||
)
|
||||
|
||||
elif args.cmd == "analyze":
|
||||
from modes.analyze_uniformity import UniformityAnalyzer
|
||||
runner = UniformityAnalyzer()
|
||||
result = runner.run(
|
||||
variant=args.variant,
|
||||
count=args.count,
|
||||
buckets=args.buckets,
|
||||
)
|
||||
|
||||
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,53 @@
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
ap = ArgumentParser(
|
||||
prog="app.py",
|
||||
description="CLI для лабораторной по хэшированию"
|
||||
)
|
||||
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
# --- Общие группы ---
|
||||
common_variant = ArgumentParser(add_help=False)
|
||||
common_variant.add_argument("-v", "--variant", choices=["1", "2", "3"], required=True,
|
||||
help="Выбор варианта: 1=sin, 2=cos, 3=xor")
|
||||
|
||||
common_salt = ArgumentParser(add_help=False)
|
||||
common_salt.add_argument("-s", "--salt", type=str, default="",
|
||||
help="Соль (по умолчанию пустая)")
|
||||
|
||||
common_bytes = ArgumentParser(add_help=False)
|
||||
common_bytes.add_argument("--bytes", type=int, default=16,
|
||||
help="Количество значимых байтов хэша (по умолчанию 16)")
|
||||
|
||||
# --- HASH ---
|
||||
sp_hash = sub.add_parser("hash", parents=[common_variant, common_salt, common_bytes],
|
||||
help="Посчитать хэш сообщения")
|
||||
sp_hash.add_argument("-m", "--message", type=str, default="",
|
||||
help="Сообщение (может быть пустым)")
|
||||
|
||||
# --- AVALANCHE ---
|
||||
sp_av = sub.add_parser("avalanche", parents=[common_variant, common_salt, common_bytes],
|
||||
help="Тест эффекта лавины")
|
||||
sp_av.add_argument("-m", "--message", type=str, required=True,
|
||||
help="Базовое сообщение")
|
||||
|
||||
# --- COLLISIONS ---
|
||||
sp_col = sub.add_parser("collisions", parents=[common_variant, common_bytes],
|
||||
help="Поиск коллизий (перебор)")
|
||||
sp_col.add_argument("--max-len", type=int, default=3,
|
||||
help="Максимальная длина сообщений (по умолчанию 3)")
|
||||
sp_col.add_argument("--alphabet", choices=["alnum", "alpha", "digits"], default="alnum",
|
||||
help="Алфавит: alnum[a-z0-9], alpha[a-z], digits[0-9]")
|
||||
|
||||
# --- ANALYZE ---
|
||||
sp_an = sub.add_parser("analyze", parents=[common_variant, common_bytes],
|
||||
help="Анализ равномерности")
|
||||
sp_an.add_argument("--count", type=int, default=100,
|
||||
help="Сколько сообщений сгенерировать")
|
||||
sp_an.add_argument("--buckets", type=int, default=10,
|
||||
help="Сколько корзин для распределения (по умолчанию 10)")
|
||||
|
||||
return ap.parse_args(argv)
|
||||
@@ -0,0 +1,44 @@
|
||||
import string
|
||||
import random
|
||||
from modes.hash_mode import HashMode
|
||||
|
||||
|
||||
class UniformityAnalyzer:
|
||||
"""
|
||||
Анализ равномерности распределения хэшей.
|
||||
"""
|
||||
|
||||
def run(self, variant: str, count: int = 100, buckets: int = 10, bytes: int = 16) -> dict:
|
||||
hasher = HashMode()
|
||||
alphabet = string.ascii_lowercase + string.digits
|
||||
|
||||
# случайные сообщения
|
||||
messages = [
|
||||
"".join(random.choice(alphabet)
|
||||
for _ in range(random.randint(5, 10)))
|
||||
for _ in range(count)
|
||||
]
|
||||
|
||||
# хэши
|
||||
hashes = [hasher.run(variant, msg, bytes=bytes) for msg in messages]
|
||||
|
||||
# берём первые 8 hex-символов → число
|
||||
values = [int(h[:8], 16) for h in hashes]
|
||||
values.sort()
|
||||
|
||||
# диапазоны
|
||||
min_val, max_val = values[0], values[-1]
|
||||
step = (max_val - min_val) / buckets if max_val > min_val else 1
|
||||
|
||||
distribution = [0] * buckets
|
||||
for v in values:
|
||||
idx = min(int((v - min_val) / step), buckets - 1)
|
||||
distribution[idx] += 1
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"buckets": buckets,
|
||||
"messages": messages,
|
||||
"hashes": hashes,
|
||||
"distribution": distribution,
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# modes/hash_mode.py
|
||||
import math
|
||||
|
||||
|
||||
class HashMode:
|
||||
"""
|
||||
Учебная хэш-функция.
|
||||
Варианты:
|
||||
1 = sin-based
|
||||
2 = cos-based
|
||||
3 = xor-based
|
||||
"""
|
||||
|
||||
def run(self, variant: str, message: str, salt: str = "", bytes: int = 16) -> str:
|
||||
if not (2 <= bytes <= 16):
|
||||
raise ValueError("Параметр bytes должен быть в диапазоне [2, 16]")
|
||||
|
||||
data = (message + salt).encode("utf-8")
|
||||
|
||||
if variant == "1":
|
||||
out = self._sin_hash(data)
|
||||
elif variant == "2":
|
||||
out = self._cos_hash(data)
|
||||
elif variant == "3":
|
||||
out = self._xor_hash(data)
|
||||
else:
|
||||
raise ValueError(f"Неизвестный вариант: {variant}")
|
||||
|
||||
# Обрезаем до нужного числа байт
|
||||
truncated = self._truncate_bytes(out, bytes)
|
||||
return ''.join(f"{x:02x}" for x in truncated)
|
||||
|
||||
# --- служебные ------------------------------------------------------
|
||||
|
||||
def _truncate_bytes(self, out: bytearray, bytes: int) -> bytearray:
|
||||
if bytes >= len(out):
|
||||
return out
|
||||
truncated = out[:bytes]
|
||||
# примешиваем все отброшенные байты в оставшиеся
|
||||
for i, b in enumerate(out[bytes:], start=1):
|
||||
truncated[i % len(truncated)] ^= b
|
||||
return truncated
|
||||
|
||||
# --- реализации ------------------------------------------------------
|
||||
|
||||
def _sin_hash(self, data: bytes) -> bytearray:
|
||||
out = [0x55 ^ i for i in range(16)]
|
||||
if not data:
|
||||
return out
|
||||
|
||||
angle = (2 * math.pi) / (len(out) * len(data))
|
||||
|
||||
for i, b in enumerate(data, 1):
|
||||
for j in range(len(out)):
|
||||
val = int((math.sin(b + (i + j) * angle) + 1) * 127)
|
||||
out[j] ^= val & 0xFF
|
||||
|
||||
return out
|
||||
|
||||
def _cos_hash(self, data: bytes) -> bytearray:
|
||||
out = [0x77 ^ i for i in range(16)]
|
||||
if not data:
|
||||
return out
|
||||
|
||||
angle = (2 * math.pi) / (len(out) * len(data))
|
||||
|
||||
for i, b in enumerate(data, 1):
|
||||
for j in range(len(out)):
|
||||
val = int((math.cos(b + (i + j) * angle) + 1) * 127)
|
||||
out[j] ^= val & 0xFF
|
||||
|
||||
return out
|
||||
|
||||
def _mix_bits(self, mix: int) -> bytearray:
|
||||
# количество байт у числа = (битовая длина + 7) // 8
|
||||
count = (mix.bit_length() + 7) // 8
|
||||
for i in range(1, count):
|
||||
mix ^= (mix >> (8 * i))
|
||||
|
||||
return mix
|
||||
|
||||
def _xor_hash(self, data: bytes) -> bytearray:
|
||||
out = [0xAA ^ i for i in range(16)]
|
||||
if not data:
|
||||
return out
|
||||
|
||||
phi = (1 + 5 ** 0.5) / 2 # золотое сечение
|
||||
|
||||
for i, b in enumerate(data, 1):
|
||||
for j in range(16):
|
||||
mix = (b * (i + j + 1)) ^ int(phi * 1e6)
|
||||
mix = self._mix_bits(mix)
|
||||
out[j] ^= mix & 0xFF # только младший байт
|
||||
|
||||
return out
|
||||
@@ -0,0 +1,39 @@
|
||||
from modes.hash_mode import HashMode
|
||||
|
||||
|
||||
class AvalancheTest:
|
||||
"""
|
||||
Проверка эффекта лавины:
|
||||
небольшое изменение во входном сообщении должно сильно менять хэш.
|
||||
"""
|
||||
|
||||
def run(self, variant: str, message: str, salt: str = "", bytes: int = 16) -> dict:
|
||||
hasher = HashMode()
|
||||
|
||||
# базовый хэш
|
||||
h1 = hasher.run(variant, message, salt=salt, bytes=bytes)
|
||||
|
||||
# изменяем один символ (если пусто — подставим "a")
|
||||
if message:
|
||||
i = 0 # всегда первый символ для простоты
|
||||
flipped_char = chr((ord(message[i]) + 1) % 128)
|
||||
mod_msg = flipped_char + message[1:]
|
||||
else:
|
||||
mod_msg = "a"
|
||||
|
||||
h2 = hasher.run(variant, mod_msg, salt=salt, bytes=bytes)
|
||||
|
||||
# сравнение побитово
|
||||
diff_bits = sum(c1 != c2 for c1, c2 in zip(
|
||||
bin(int(h1, 16))[2:], bin(int(h2, 16))[2:]))
|
||||
total_bits = len(h1) * 4 # HEX -> биты
|
||||
|
||||
return {
|
||||
"original": message,
|
||||
"modified": mod_msg,
|
||||
"hash1": h1,
|
||||
"hash2": h2,
|
||||
"diff_bits": diff_bits,
|
||||
"total_bits": total_bits,
|
||||
"diff_percent": round(100 * diff_bits / total_bits, 2),
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import itertools
|
||||
import string
|
||||
from modes.hash_mode import HashMode
|
||||
|
||||
|
||||
class CollisionsTest:
|
||||
"""
|
||||
Грубый поиск коллизий: перебираем строки до max_len
|
||||
из заданного алфавита и проверяем, совпадут ли хэши.
|
||||
"""
|
||||
|
||||
def run(self, variant: str, max_len: int = 3, alphabet: str = "alnum", bytes: int = 16) -> dict:
|
||||
hasher = HashMode()
|
||||
seen = {}
|
||||
|
||||
# выбор алфавита
|
||||
if alphabet == "alnum":
|
||||
chars = string.ascii_lowercase + string.digits
|
||||
elif alphabet == "alpha":
|
||||
chars = string.ascii_lowercase
|
||||
elif alphabet == "digits":
|
||||
chars = string.digits
|
||||
else:
|
||||
raise ValueError(f"Неизвестный алфавит: {alphabet}")
|
||||
|
||||
for length in range(1, max_len + 1):
|
||||
for msg_tuple in itertools.product(chars, repeat=length):
|
||||
msg = "".join(msg_tuple)
|
||||
h = hasher.run(variant, msg, bytes=bytes)
|
||||
if h in seen:
|
||||
return {
|
||||
"collision_found": True,
|
||||
"hash": h,
|
||||
"messages": [seen[h], msg],
|
||||
}
|
||||
seen[h] = msg
|
||||
|
||||
return {"collision_found": False}
|
||||
@@ -0,0 +1,89 @@
|
||||
import subprocess
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from cli.parse_args import parse_args
|
||||
|
||||
LAB2 = Path(__file__).resolve().parents[2] / \
|
||||
"lab2-the-RSA-algorithm" / "app" / "app.py"
|
||||
LAB3 = Path(__file__).resolve().parents[2] / \
|
||||
"lab3-hash-function" / "app" / "app.py"
|
||||
|
||||
|
||||
def run_lab2_keygen(bits: int, seed: int | None):
|
||||
cmd = [sys.executable, str(LAB2), "keygen", "--bits", str(bits)]
|
||||
if seed is not None:
|
||||
cmd += ["--seed", str(seed)]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
return json.loads(res.stdout)
|
||||
|
||||
|
||||
def run_lab3_hash(variant: str, text: str, bytes_len: int):
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(LAB3),
|
||||
"hash",
|
||||
"-v",
|
||||
variant,
|
||||
"-m",
|
||||
text,
|
||||
"--bytes",
|
||||
str(bytes_len),
|
||||
]
|
||||
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
return res.stdout.strip() # ожидается hex-строка
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
# Фиксированные параметры RSA и HASH
|
||||
bits = 32
|
||||
bytes_len = 7
|
||||
|
||||
# 1. Генерация ключей RSA
|
||||
keys = run_lab2_keygen(bits, args.seed)
|
||||
n = int(keys["n"]["dec"])
|
||||
e = int(keys["e"]["dec"])
|
||||
d = int(keys["d"]["dec"])
|
||||
|
||||
print("RSA ключи сгенерированы:")
|
||||
print(json.dumps(keys, indent=2, ensure_ascii=False))
|
||||
|
||||
# 2. Хэш исходного текста
|
||||
h_hex = run_lab3_hash(args.variant, args.text, bytes_len)
|
||||
h = int(h_hex, 16)
|
||||
print(f"\nХэш исходного сообщения: {h_hex} (dec={h})")
|
||||
|
||||
# 3. Проверка условия n > h
|
||||
if h >= n:
|
||||
print(
|
||||
"\n[ОШИБКА] Хэш больше или равен n. Нужно пересоздать ключи с большим размером.")
|
||||
sys.exit(1)
|
||||
|
||||
# 4. Подпись: S = h^d mod n
|
||||
S = pow(h, d, n)
|
||||
print(f"\nПодпись (S): {S}")
|
||||
|
||||
# 5. Проверка подписи для исходного сообщения
|
||||
h_check = pow(S, e, n)
|
||||
print(f"Проверка подписи: {h_check} == {h} ? {
|
||||
'OK' if h_check == h else 'FAIL'}")
|
||||
|
||||
# 6. Если передан модифицированный текст — проверить и его
|
||||
if args.tampered:
|
||||
h_tampered_hex = run_lab3_hash(args.variant, args.tampered, bytes_len)
|
||||
h_tampered = int(h_tampered_hex, 16)
|
||||
print(f"\nХэш модифицированного сообщения: {
|
||||
h_tampered_hex} (dec={h_tampered})")
|
||||
|
||||
h_check_tampered = pow(S, e, n)
|
||||
print(
|
||||
f"Проверка подписи для модифицированного текста: "
|
||||
f"{h_check_tampered} == {h_tampered} ? {
|
||||
'OK' if h_check_tampered == h_tampered else 'FAIL'}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,25 @@
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
ap = ArgumentParser(
|
||||
prog="app.py",
|
||||
description="ЭЦП (RSA + собственная хэш-функция). "
|
||||
"Выполняет полный цикл: генерация ключей, подпись и проверку."
|
||||
)
|
||||
|
||||
# --- RSA ---
|
||||
ap.add_argument("-s", "--seed", type=int,
|
||||
help="Seed для ГСЧ (опционально, для воспроизводимости).")
|
||||
|
||||
# --- HASH ---
|
||||
ap.add_argument("-v", "--variant", choices=["1", "2", "3"], required=True,
|
||||
help="Вариант хэш-функции (см. lab3).")
|
||||
|
||||
# --- MESSAGES ---
|
||||
ap.add_argument("-t", "--text", type=str, required=True,
|
||||
help="Исходное сообщение (ASCII).")
|
||||
ap.add_argument("-m", "--tampered", type=str,
|
||||
help="Модифицированное сообщение для имитации нарушения целостности.")
|
||||
|
||||
return ap.parse_args(argv)
|
||||
@@ -0,0 +1,34 @@
|
||||
from core import PasswordHasher, IntegrityChecker, CryptoManager, AuditLogger, AccountManager
|
||||
from gui import MainWindow
|
||||
|
||||
|
||||
class App:
|
||||
"""
|
||||
Класс точки входа для запуска менеджера учётных записей.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.db_file = None
|
||||
self.hash_file = "data/accounts.db.hash"
|
||||
self.log_file = "data/audit.log"
|
||||
|
||||
self.password_hasher = PasswordHasher()
|
||||
self.integrity_checker = IntegrityChecker()
|
||||
self.crypto_manager = CryptoManager()
|
||||
self.audit_logger = AuditLogger(self.log_file)
|
||||
self.account_manager = AccountManager(self.db_file)
|
||||
|
||||
self.gui = MainWindow(
|
||||
account_manager=self.account_manager,
|
||||
password_hasher=self.password_hasher,
|
||||
audit_logger=self.audit_logger,
|
||||
crypto_manager=self.crypto_manager,
|
||||
integrity_checker=self.integrity_checker,
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.gui.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App().run()
|
||||
@@ -0,0 +1,5 @@
|
||||
from .password_hasher import PasswordHasher
|
||||
from .integrity_checker import IntegrityChecker
|
||||
from .crypto_manager import CryptoManager
|
||||
from .audit_logger import AuditLogger
|
||||
from .account_manager import AccountManager
|
||||
@@ -0,0 +1,62 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class AccountManager:
|
||||
"""
|
||||
Класс для управления учётными записями пользователей.
|
||||
"""
|
||||
|
||||
def __init__(self, db_file: str | None):
|
||||
self.db_file = Path(db_file) if db_file else None
|
||||
if isinstance(self.db_file, Path) and not self.db_file.exists():
|
||||
self._save_db({})
|
||||
|
||||
def set_db_file(self, db_file: str | None) -> None:
|
||||
self.db_file = Path(db_file) if db_file else None
|
||||
if isinstance(self.db_file, Path) and not self.db_file.exists():
|
||||
self._save_db({})
|
||||
|
||||
def _load_db(self) -> dict:
|
||||
if not isinstance(self.db_file, Path):
|
||||
return {}
|
||||
if self.db_file.exists():
|
||||
return json.loads(self.db_file.read_text(encoding="utf-8"))
|
||||
return {}
|
||||
|
||||
def _save_db(self, data: dict) -> None:
|
||||
if not isinstance(self.db_file, Path):
|
||||
raise RuntimeError("База данных не открыта")
|
||||
self.db_file.write_text(json.dumps(
|
||||
data, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
def add_user(self, username: str, password_hash: str, salt: str) -> None:
|
||||
data = self._load_db()
|
||||
data[username] = {"hash": password_hash, "salt": salt}
|
||||
self._save_db(data)
|
||||
|
||||
def update_user(self, username: str, password_hash: str, salt: str) -> bool:
|
||||
data = self._load_db()
|
||||
if username not in data:
|
||||
return False
|
||||
data[username] = {"hash": password_hash, "salt": salt}
|
||||
self._save_db(data)
|
||||
return True
|
||||
|
||||
def delete_user(self, username: str) -> bool:
|
||||
data = self._load_db()
|
||||
if username not in data:
|
||||
return False
|
||||
del data[username]
|
||||
self._save_db(data)
|
||||
return True
|
||||
|
||||
def get_user(self, username: str) -> dict | None:
|
||||
data = self._load_db()
|
||||
return data.get(username)
|
||||
|
||||
def list_users(self) -> list[str]:
|
||||
if not isinstance(self.db_file, Path):
|
||||
return []
|
||||
data = self._load_db()
|
||||
return list(data.keys())
|
||||
@@ -0,0 +1,17 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class AuditLogger:
|
||||
"""
|
||||
Класс для ведения журнала событий.
|
||||
"""
|
||||
|
||||
def __init__(self, log_file: str):
|
||||
self.log_file = Path(log_file)
|
||||
|
||||
def log(self, event: str) -> None:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
line = f"[{timestamp}] {event}\n"
|
||||
with open(self.log_file, "a", encoding="utf-8") as f:
|
||||
f.write(line)
|
||||
@@ -0,0 +1,23 @@
|
||||
from cryptography.fernet import Fernet
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class CryptoManager:
|
||||
"""
|
||||
Класс для симметричного шифрования и расшифрования файлов.
|
||||
"""
|
||||
|
||||
def generate_key(self) -> bytes:
|
||||
return Fernet.generate_key()
|
||||
|
||||
def encrypt_file(self, file_path: str, key: bytes, output_path: str) -> None:
|
||||
fernet = Fernet(key)
|
||||
data = Path(file_path).read_bytes()
|
||||
encrypted = fernet.encrypt(data)
|
||||
Path(output_path).write_bytes(encrypted)
|
||||
|
||||
def decrypt_file(self, file_path: str, key: bytes, output_path: str) -> None:
|
||||
fernet = Fernet(key)
|
||||
data = Path(file_path).read_bytes()
|
||||
decrypted = fernet.decrypt(data)
|
||||
Path(output_path).write_bytes(decrypted)
|
||||
@@ -0,0 +1,26 @@
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class IntegrityChecker:
|
||||
"""
|
||||
Класс для проверки целостности файлов по хэш-сумме.
|
||||
"""
|
||||
|
||||
def calculate_hash(self, file_path: str) -> str:
|
||||
hasher = hashlib.sha256()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hasher.update(chunk)
|
||||
return hasher.hexdigest()
|
||||
|
||||
def save_hash(self, file_path: str, hash_file: str) -> None:
|
||||
file_hash = self.calculate_hash(file_path)
|
||||
Path(hash_file).write_text(file_hash, encoding="utf-8")
|
||||
|
||||
def verify_hash(self, file_path: str, hash_file: str) -> bool:
|
||||
if not Path(hash_file).exists():
|
||||
return False
|
||||
expected = Path(hash_file).read_text(encoding="utf-8").strip()
|
||||
actual = self.calculate_hash(file_path)
|
||||
return expected == actual
|
||||
@@ -0,0 +1,19 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
|
||||
class PasswordHasher:
|
||||
"""
|
||||
Класс для безопасного хэширования паролей с использованием соли.
|
||||
"""
|
||||
|
||||
def generate_salt(self, length: int = 16) -> str:
|
||||
return secrets.token_hex(length)
|
||||
|
||||
def hash_password(self, password: str, salt: str) -> str:
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(salt.encode("utf-8") + password.encode("utf-8"))
|
||||
return hasher.hexdigest()
|
||||
|
||||
def verify_password(self, password: str, salt: str, expected_hash: str) -> bool:
|
||||
return self.hash_password(password, salt) == expected_hash
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"user2": {
|
||||
"hash": "4d3596717c7887c07c607485fa8c287547a5e65559b3269844261417b1e7a97b",
|
||||
"salt": "a1bb56a86e8d504634668696d9e65189"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
109fb5ce85641ad0ed60cf8ae800289517d33c2cb100f39e5c7c74cb88b9b722
|
||||
@@ -0,0 +1,3 @@
|
||||
[2025-10-01 21:51:42] Добавлен пользователь: user
|
||||
[2025-10-01 21:51:48] Добавлен пользователь: user2
|
||||
[2025-10-01 21:51:55] Добавлен пользователь: user3
|
||||
@@ -0,0 +1 @@
|
||||
gAAAAABo3Wp_Jc6ye1Yf6rYq3GQ64_S6XHAQjm7WSCvS6fK48kMk5NmP7zFyX5AGShawV7uvFEVvx2v5Efn_hFWdML4I_iAf1x6jZheZLJuWzMLtsm4x25wEAo7c8vokR8YV-MaiJWKw_c9kdAzPoPpkfuZSAuwSU6hD_oNHooG8SPCWVTbbvmqwnquMchLpXaK_gfyYOgye_ERB5s5GBxMICwM47W87tGoKnFL6jtyGk8iIowPYD0xlrAhUwvuTeDfxlIhyOZBc72R5iTmiSbOgxNu6DyJy9-49kdSau35LTVWRThg7MEWxK1xTC5P6stXkhovNsjujsdcSBk8Rx-AgKBmSRPGznkC7zYTNw6gQhYujsi_WtaQ=
|
||||
@@ -0,0 +1 @@
|
||||
ir7gZ6qT1gP-wAcNvKB3Ua_k7zJ52bH_GmgdSO9iryo=
|
||||
@@ -0,0 +1,3 @@
|
||||
from .main_window import MainWindow
|
||||
|
||||
__all__ = ["MainWindow"]
|
||||
@@ -0,0 +1,16 @@
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
class ControlsPanel:
|
||||
"""
|
||||
Панель управления действиями над пользователями.
|
||||
"""
|
||||
|
||||
def __init__(self, master, on_add, on_update, on_delete):
|
||||
self.frame = tk.Frame(master)
|
||||
tk.Button(self.frame, text="Добавить",
|
||||
command=on_add).pack(side=tk.LEFT, padx=5)
|
||||
tk.Button(self.frame, text="Изменить",
|
||||
command=on_update).pack(side=tk.LEFT, padx=5)
|
||||
tk.Button(self.frame, text="Удалить", command=on_delete).pack(
|
||||
side=tk.LEFT, padx=5)
|
||||
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox, filedialog as fd
|
||||
|
||||
|
||||
class CryptoTools:
|
||||
"""Операции шифрования и расшифрования файлов."""
|
||||
|
||||
def __init__(self, crypto_manager, audit_logger):
|
||||
self.crypto_manager = crypto_manager
|
||||
self.audit_logger = audit_logger
|
||||
|
||||
def _generate_and_save_key(self) -> bytes | None:
|
||||
key = self.crypto_manager.generate_key()
|
||||
path = fd.asksaveasfilename(
|
||||
title="Сохранить ключ шифрования",
|
||||
defaultextension=".key",
|
||||
filetypes=[("Key file", "*.key"), ("All files", "*.*")],
|
||||
initialfile="encryption.key",
|
||||
)
|
||||
if not path:
|
||||
return None
|
||||
try:
|
||||
Path(path).write_bytes(key)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Сохранение ключа",
|
||||
f"Не удалось сохранить ключ:\n{e}")
|
||||
return None
|
||||
messagebox.showinfo("Ключ сохранён", f"Ключ сохранён в:\n{path}")
|
||||
return key
|
||||
|
||||
def _load_key_from_file(self) -> bytes | None:
|
||||
path = fd.askopenfilename(
|
||||
title="Выберите файл ключа",
|
||||
filetypes=[("Key file", "*.key"), ("All files", "*.*")],
|
||||
)
|
||||
if not path:
|
||||
return None
|
||||
try:
|
||||
return Path(path).read_bytes()
|
||||
except Exception as e:
|
||||
messagebox.showerror(
|
||||
"Чтение ключа", f"Не удалось прочитать ключ:\n{e}")
|
||||
return None
|
||||
|
||||
def _select_output_path(self, suggested: Path, title: str, defext: str, pattern: str) -> Path | None:
|
||||
path = fd.asksaveasfilename(
|
||||
title=title,
|
||||
defaultextension=defext,
|
||||
filetypes=[(f"*{defext}", pattern), ("All files", "*.*")],
|
||||
initialdir=str(suggested.parent),
|
||||
initialfile=suggested.name,
|
||||
)
|
||||
return Path(path) if path else None
|
||||
|
||||
def encrypt_file(self):
|
||||
src = fd.askopenfilename(title="Выберите файл для шифрования")
|
||||
if not src:
|
||||
return
|
||||
key = self._generate_and_save_key()
|
||||
if key is None:
|
||||
return
|
||||
suggested = Path(src).with_suffix(Path(src).suffix + ".enc")
|
||||
out = self._select_output_path(
|
||||
suggested, "Сохранить зашифрованный файл", ".enc", "*.enc")
|
||||
if out is None:
|
||||
return
|
||||
self.crypto_manager.encrypt_file(src, key, str(out))
|
||||
self.audit_logger.log(f"Зашифрован файл → {out.name}")
|
||||
messagebox.showinfo("Шифрование файла", f"Создан файл: {out}")
|
||||
|
||||
def decrypt_file(self):
|
||||
enc = fd.askopenfilename(title="Выберите зашифрованный файл", filetypes=[
|
||||
("Encrypted", "*.enc"), ("All files", "*.*")])
|
||||
if not enc:
|
||||
return
|
||||
key = self._load_key_from_file()
|
||||
if key is None:
|
||||
return
|
||||
suggested = Path(enc).with_suffix(".dec")
|
||||
out = self._select_output_path(
|
||||
suggested, "Сохранить расшифрованный файл", ".dec", "*.dec")
|
||||
if out is None:
|
||||
return
|
||||
try:
|
||||
self.crypto_manager.decrypt_file(enc, key, str(out))
|
||||
except Exception as e:
|
||||
messagebox.showerror("Расшифрование файла", f"Ошибка:\n{e}")
|
||||
return
|
||||
self.audit_logger.log(f"Расшифрован файл → {out.name}")
|
||||
messagebox.showinfo("Расшифрование файла", f"Создан файл: {out}")
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from tkinter import messagebox, filedialog as fd
|
||||
|
||||
|
||||
class IntegrityTools:
|
||||
"""Операции сохранения и проверки хэша БД."""
|
||||
|
||||
def __init__(self, integrity_checker, audit_logger, db_path: str | None):
|
||||
self.integrity_checker = integrity_checker
|
||||
self.audit_logger = audit_logger
|
||||
self.db_path = Path(db_path) if db_path else None
|
||||
|
||||
def set_db_path(self, db_path: str | None) -> None:
|
||||
self.db_path = Path(db_path) if db_path else None
|
||||
|
||||
def _hash_path(self) -> Path:
|
||||
return self.db_path.with_suffix(self.db_path.suffix + ".hash")
|
||||
|
||||
def save_db_hash(self):
|
||||
if not self.db_path:
|
||||
messagebox.showwarning("Сохранить хэш", "База данных не выбрана.")
|
||||
return
|
||||
if not self.db_path.exists():
|
||||
messagebox.showwarning(
|
||||
"Сохранить хэш", f"Файл БД не найден:\n{self.db_path}")
|
||||
return
|
||||
suggested = self.db_path.with_suffix(self.db_path.suffix + ".hash")
|
||||
path = fd.asksaveasfilename(
|
||||
title="Сохранить контрольную сумму БД",
|
||||
defaultextension=".hash",
|
||||
filetypes=[("Hash file", "*.hash"), ("All files", "*.*")],
|
||||
initialdir=str(suggested.parent),
|
||||
initialfile=suggested.name,
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
self.integrity_checker.save_hash(str(self.db_path), str(path))
|
||||
except Exception as e:
|
||||
messagebox.showerror("Сохранить хэш", f"Ошибка сохранения:\n{e}")
|
||||
return
|
||||
self.audit_logger.log(
|
||||
f"Сохранена контрольная сумма БД → {Path(path).name}")
|
||||
messagebox.showinfo("Сохранить хэш", f"Хэш сохранён в {path}")
|
||||
|
||||
def verify_db_hash(self):
|
||||
if not self.db_path:
|
||||
messagebox.showwarning(
|
||||
"Проверка целостности", "База данных не выбрана.")
|
||||
return
|
||||
if not self.db_path.exists():
|
||||
messagebox.showwarning(
|
||||
"Проверка целостности", f"Файл БД не найден:\n{self.db_path}")
|
||||
return
|
||||
suggested = self.db_path.with_suffix(self.db_path.suffix + ".hash")
|
||||
path = fd.askopenfilename(
|
||||
title="Выбрать файл контрольной суммы БД",
|
||||
filetypes=[("Hash file", "*.hash"), ("All files", "*.*")],
|
||||
initialdir=str(suggested.parent),
|
||||
initialfile=suggested.name,
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
ok = self.integrity_checker.verify_hash(str(self.db_path), str(path))
|
||||
self.audit_logger.log("Проверка целостности БД: " +
|
||||
("OK" if ok else "НЕСОВПАДЕНИЕ"))
|
||||
if ok:
|
||||
messagebox.showinfo("Проверка целостности", "БД в целостности.")
|
||||
else:
|
||||
messagebox.showerror("Проверка целостности",
|
||||
"БД была изменена извне.")
|
||||
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox, simpledialog, filedialog as fd
|
||||
from pathlib import Path
|
||||
|
||||
from .user_list_view import UserListView
|
||||
from .controls_panel import ControlsPanel
|
||||
from .integrity_tools import IntegrityTools
|
||||
from .crypto_tools import CryptoTools
|
||||
|
||||
|
||||
class MainWindow:
|
||||
"""
|
||||
Главное окно менеджера учётных записей.
|
||||
"""
|
||||
|
||||
def __init__(self, account_manager, password_hasher, audit_logger, crypto_manager, integrity_checker):
|
||||
self.account_manager = account_manager
|
||||
self.password_hasher = password_hasher
|
||||
self.audit_logger = audit_logger
|
||||
|
||||
self.integrity_tools = IntegrityTools(
|
||||
integrity_checker, audit_logger, account_manager.db_file)
|
||||
self.crypto_tools = CryptoTools(crypto_manager, audit_logger)
|
||||
|
||||
self.root = tk.Tk()
|
||||
self.root.title("Менеджер учётных записей")
|
||||
|
||||
self._build_menu()
|
||||
|
||||
self.users_view = UserListView(self.root)
|
||||
self.users_view.frame.pack()
|
||||
|
||||
self.controls = ControlsPanel(
|
||||
self.root,
|
||||
on_add=self.add_user,
|
||||
on_update=self.update_user,
|
||||
on_delete=self.delete_user
|
||||
)
|
||||
self.controls.frame.pack(pady=5)
|
||||
|
||||
self.refresh_users()
|
||||
|
||||
def _build_menu(self):
|
||||
menubar = tk.Menu(self.root)
|
||||
|
||||
file_menu = tk.Menu(menubar, tearoff=0)
|
||||
file_menu.add_command(
|
||||
label="Открыть базу данных...", command=self.open_db)
|
||||
file_menu.add_command(label="Закрыть базу данных",
|
||||
command=self.close_db)
|
||||
menubar.add_cascade(label="Файл", menu=file_menu)
|
||||
|
||||
tools = tk.Menu(menubar, tearoff=0)
|
||||
tools.add_command(label="Сохранить хэш БД",
|
||||
command=self.integrity_tools.save_db_hash)
|
||||
tools.add_command(label="Проверить целостность БД",
|
||||
command=self.integrity_tools.verify_db_hash)
|
||||
tools.add_separator()
|
||||
tools.add_command(label="Зашифровать файл...",
|
||||
command=self.crypto_tools.encrypt_file)
|
||||
tools.add_command(label="Расшифровать файл...",
|
||||
command=self.crypto_tools.decrypt_file)
|
||||
|
||||
menubar.add_cascade(label="Инструменты", menu=tools)
|
||||
self.root.config(menu=menubar)
|
||||
|
||||
# --- File ops ---
|
||||
|
||||
def open_db(self):
|
||||
path = fd.askopenfilename(
|
||||
title="Открыть базу данных",
|
||||
filetypes=[("JSON", "*.json"), ("DB/TXT", "*.db *.txt"),
|
||||
("Все файлы", "*.*")],
|
||||
initialdir="data"
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
self.account_manager.set_db_file(path)
|
||||
self.integrity_tools.set_db_path(path)
|
||||
self.audit_logger.log(f"Открыта база данных: {Path(path).name}")
|
||||
self.refresh_users()
|
||||
|
||||
# --- Account ops ---
|
||||
def refresh_users(self):
|
||||
self.users_view.set_items(self.account_manager.list_users())
|
||||
|
||||
def add_user(self):
|
||||
username = simpledialog.askstring(
|
||||
"Добавить пользователя", "Введите имя пользователя:")
|
||||
if not username:
|
||||
return
|
||||
password = simpledialog.askstring(
|
||||
"Пароль", "Введите пароль:", show="*")
|
||||
if not password:
|
||||
return
|
||||
salt = self.password_hasher.generate_salt()
|
||||
password_hash = self.password_hasher.hash_password(password, salt)
|
||||
self.account_manager.add_user(username, password_hash, salt)
|
||||
self.audit_logger.log(f"Добавлен пользователь: {username}")
|
||||
self.refresh_users()
|
||||
|
||||
def update_user(self):
|
||||
username = self.users_view.get_selected()
|
||||
if not username:
|
||||
messagebox.showwarning("Изменить", "Выберите пользователя")
|
||||
return
|
||||
password = simpledialog.askstring(
|
||||
"Новый пароль", f"Введите новый пароль для {username}:", show="*")
|
||||
if not password:
|
||||
return
|
||||
salt = self.password_hasher.generate_salt()
|
||||
password_hash = self.password_hasher.hash_password(password, salt)
|
||||
if self.account_manager.update_user(username, password_hash, salt):
|
||||
self.audit_logger.log(f"Обновлён пароль пользователя: {username}")
|
||||
self.refresh_users()
|
||||
|
||||
def delete_user(self):
|
||||
username = self.users_view.get_selected()
|
||||
if not username:
|
||||
messagebox.showwarning("Удалить", "Выберите пользователя")
|
||||
return
|
||||
if self.account_manager.delete_user(username):
|
||||
self.audit_logger.log(f"Удалён пользователь: {username}")
|
||||
self.refresh_users()
|
||||
|
||||
def run(self):
|
||||
self.root.mainloop()
|
||||
|
||||
def close_db(self):
|
||||
self.account_manager.set_db_file(None)
|
||||
self.integrity_tools.set_db_path(None)
|
||||
self.audit_logger.log("Закрыта база данных")
|
||||
self.users_view.set_items([])
|
||||
@@ -0,0 +1,21 @@
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
class UserListView:
|
||||
"""
|
||||
Виджет списка пользователей.
|
||||
"""
|
||||
|
||||
def __init__(self, master):
|
||||
self.frame = tk.Frame(master)
|
||||
self._listbox = tk.Listbox(self.frame, width=40)
|
||||
self._listbox.pack(padx=10, pady=10)
|
||||
|
||||
def set_items(self, items: list[str]) -> None:
|
||||
self._listbox.delete(0, tk.END)
|
||||
for it in items:
|
||||
self._listbox.insert(tk.END, it)
|
||||
|
||||
def get_selected(self) -> str | None:
|
||||
sel = self._listbox.curselection()
|
||||
return self._listbox.get(sel[0]) if sel else None
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Создание виртуального окружения в .venv (если ещё нет)
|
||||
if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
|
||||
# Активация окружения
|
||||
. .venv/bin/activate
|
||||
|
||||
# Обновление pip и установка зависимостей
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo "Виртуальное окружение создано и зависимости установлены."
|
||||
echo "Активируйте его командой: . .venv/bin/activate"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['app.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='lab5-manager',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
cryptography>=42.0.0
|
||||
pytest>=7.0
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Проверка наличия окружения
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo "Виртуальное окружение не найдено. Сначала выполните ./install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Активация окружения
|
||||
. .venv/bin/activate
|
||||
|
||||
# Запуск приложения
|
||||
python app.py
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Проверка наличия окружения
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo "Виртуальное окружение не найдено. Сначала выполните ./install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Активация окружения
|
||||
. .venv/bin/activate
|
||||
|
||||
# Запуск тестов
|
||||
pytest -v tests
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..")))
|
||||
@@ -0,0 +1,23 @@
|
||||
from core.account_manager import AccountManager
|
||||
|
||||
|
||||
def test_crud_operations(tmp_path):
|
||||
db_file = tmp_path / "accounts.json"
|
||||
manager = AccountManager(db_file)
|
||||
|
||||
# Добавление
|
||||
manager.add_user("alice", "hash1", "salt1")
|
||||
assert "alice" in manager.list_users()
|
||||
|
||||
# Чтение
|
||||
user = manager.get_user("alice")
|
||||
assert user["hash"] == "hash1"
|
||||
|
||||
# Обновление
|
||||
assert manager.update_user("alice", "hash2", "salt2")
|
||||
user = manager.get_user("alice")
|
||||
assert user["hash"] == "hash2"
|
||||
|
||||
# Удаление
|
||||
assert manager.delete_user("alice")
|
||||
assert "alice" not in manager.list_users()
|
||||
@@ -0,0 +1,12 @@
|
||||
from core.audit_logger import AuditLogger
|
||||
|
||||
|
||||
def test_log_event(tmp_path):
|
||||
log_file = tmp_path / "audit.log"
|
||||
logger = AuditLogger(log_file)
|
||||
|
||||
logger.log("Test event")
|
||||
content = log_file.read_text(encoding="utf-8")
|
||||
|
||||
assert "Test event" in content
|
||||
assert "[" in content # должно быть время
|
||||
@@ -0,0 +1,17 @@
|
||||
from core.crypto_manager import CryptoManager
|
||||
|
||||
|
||||
def test_encrypt_decrypt_file(tmp_path):
|
||||
file = tmp_path / "plain.txt"
|
||||
file.write_text("super secret", encoding="utf-8")
|
||||
|
||||
enc_file = tmp_path / "plain.enc"
|
||||
dec_file = tmp_path / "plain.dec"
|
||||
|
||||
crypto = CryptoManager()
|
||||
key = crypto.generate_key()
|
||||
|
||||
crypto.encrypt_file(file, key, enc_file)
|
||||
crypto.decrypt_file(enc_file, key, dec_file)
|
||||
|
||||
assert dec_file.read_text(encoding="utf-8") == "super secret"
|
||||
@@ -0,0 +1,17 @@
|
||||
from pathlib import Path
|
||||
from core.integrity_checker import IntegrityChecker
|
||||
|
||||
|
||||
def test_calculate_and_verify_hash(tmp_path: Path):
|
||||
file = tmp_path / "data.txt"
|
||||
file.write_text("hello", encoding="utf-8")
|
||||
|
||||
hash_file = tmp_path / "data.hash"
|
||||
checker = IntegrityChecker()
|
||||
|
||||
checker.save_hash(file, hash_file)
|
||||
assert checker.verify_hash(file, hash_file)
|
||||
|
||||
# Нарушим целостность
|
||||
file.write_text("changed", encoding="utf-8")
|
||||
assert not checker.verify_hash(file, hash_file)
|
||||
@@ -0,0 +1,12 @@
|
||||
import pytest
|
||||
from core.password_hasher import PasswordHasher
|
||||
|
||||
|
||||
def test_hash_and_verify():
|
||||
hasher = PasswordHasher()
|
||||
salt = hasher.generate_salt()
|
||||
pwd = "secret123"
|
||||
hashed = hasher.hash_password(pwd, salt)
|
||||
|
||||
assert hasher.verify_password(pwd, salt, hashed)
|
||||
assert not hasher.verify_password("wrong", salt, hashed)
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
from cli.parse_args import parse_args
|
||||
|
||||
|
||||
def _normalize_channel(ch: str) -> str:
|
||||
"""Y/Cb/Cr в канонический вид."""
|
||||
m = {"y": "Y", "cb": "Cb", "cr": "Cr"}
|
||||
s = (ch or "").strip().lower()
|
||||
return m.get(s, ch)
|
||||
|
||||
|
||||
def _print_result(result):
|
||||
if result is None:
|
||||
return
|
||||
if isinstance(result, (dict, list)):
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(result)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
if args.cmd == "embed":
|
||||
from modes.embed_mode import EmbedMode
|
||||
runner = EmbedMode()
|
||||
result = runner.run(
|
||||
input_path=args.input,
|
||||
label_path=args.label,
|
||||
output_path=args.output,
|
||||
channel=_normalize_channel(args.channel),
|
||||
seq0=args.seq0,
|
||||
)
|
||||
|
||||
elif args.cmd == "extract":
|
||||
from modes.extract_mode import ExtractMode
|
||||
runner = ExtractMode()
|
||||
result = runner.run(
|
||||
input_path=args.input,
|
||||
output_path=args.output,
|
||||
channel=_normalize_channel(args.channel),
|
||||
seq0=args.seq0
|
||||
)
|
||||
|
||||
elif args.cmd == "analyze":
|
||||
from modes.analyze_quality import AnalyzeQuality
|
||||
runner = AnalyzeQuality()
|
||||
result = runner.run(
|
||||
original_path=args.original,
|
||||
stego_path=args.stego,
|
||||
space=args.space,
|
||||
metrics=args.metrics,
|
||||
)
|
||||
|
||||
elif args.cmd == "generate":
|
||||
from modes.generate_mode import GenerateMode
|
||||
runner = GenerateMode()
|
||||
# Ветка generate имеет вложенные подкоманды: args.gen in {"gradient","chess"}
|
||||
result = runner.run(
|
||||
kind=args.gen,
|
||||
size=args.size,
|
||||
channels=args.channels,
|
||||
output_path=args.output,
|
||||
tile=getattr(args, "tile", None),
|
||||
)
|
||||
|
||||
else:
|
||||
raise RuntimeError(f"Неизвестная команда: {args.cmd}")
|
||||
|
||||
_print_result(result)
|
||||
return 0
|
||||
|
||||
except Exception as exc:
|
||||
print(f"Ошибка: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
__all__ = ["parse_args"]
|
||||
|
||||
from .parse_args import parse_args
|
||||
@@ -0,0 +1,147 @@
|
||||
|
||||
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
ap = ArgumentParser(
|
||||
prog="app.py",
|
||||
description="CLI для лабораторной №6: Стеганография в изображениях",
|
||||
formatter_class=ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
# -------- Parent parsers --------
|
||||
parent_channel_seq = ArgumentParser(add_help=False)
|
||||
parent_channel_seq.add_argument(
|
||||
"-c", "--channel",
|
||||
choices=["Y", "Cb", "Cr", "y", "cb", "cr"],
|
||||
default="Cb",
|
||||
help="Канал для встраивания/извлечения: Y, Cb или Cr",
|
||||
)
|
||||
parent_channel_seq.add_argument(
|
||||
"--seq0",
|
||||
type=str,
|
||||
default="01010101",
|
||||
help="8-битная последовательность для бита 0 (seq1 считается инверсией seq0)",
|
||||
)
|
||||
|
||||
parent_gen_common = ArgumentParser(add_help=False)
|
||||
parent_gen_common.add_argument(
|
||||
"--size",
|
||||
type=int,
|
||||
default=512,
|
||||
help="Размер стороны квадратного изображения",
|
||||
)
|
||||
parent_gen_common.add_argument(
|
||||
"--channels",
|
||||
type=int,
|
||||
choices=[1, 3],
|
||||
default=3,
|
||||
help="Количество каналов: 1 (grayscale) или 3 (RGB)",
|
||||
)
|
||||
parent_gen_common.add_argument(
|
||||
"-o", "--output",
|
||||
required=True,
|
||||
help="Куда сохранить изображение",
|
||||
)
|
||||
|
||||
# ---------------- EMBED ----------------
|
||||
sp_embed = sub.add_parser(
|
||||
"embed",
|
||||
parents=[parent_channel_seq],
|
||||
help="Встраивание метки в изображение-контейнер",
|
||||
formatter_class=ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
sp_embed.add_argument(
|
||||
"-i", "--input",
|
||||
required=True,
|
||||
help="Путь к исходному изображению-контейнеру (512x512, RGB)",
|
||||
)
|
||||
sp_embed.add_argument(
|
||||
"-l", "--label",
|
||||
required=True,
|
||||
help="Путь к бинарной метке (64x64)",
|
||||
)
|
||||
sp_embed.add_argument(
|
||||
"-o", "--output",
|
||||
required=True,
|
||||
help="Куда сохранить стего-изображение",
|
||||
)
|
||||
|
||||
# ---------------- EXTRACT ----------------
|
||||
sp_extract = sub.add_parser(
|
||||
"extract",
|
||||
parents=[parent_channel_seq],
|
||||
help="Извлечение метки из стего-изображения",
|
||||
formatter_class=ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
sp_extract.add_argument(
|
||||
"-i", "--input",
|
||||
required=True,
|
||||
help="Путь к стего-изображению",
|
||||
)
|
||||
sp_extract.add_argument(
|
||||
"-o", "--output",
|
||||
required=True,
|
||||
help="Куда сохранить восстановленную бинарную метку (64x64)",
|
||||
)
|
||||
|
||||
# ---------------- ANALYZE ----------------
|
||||
sp_analyze = sub.add_parser(
|
||||
"analyze",
|
||||
help="Сравнение изображений и метрик качества",
|
||||
formatter_class=ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
sp_analyze.add_argument(
|
||||
"-a", "--original",
|
||||
required=True,
|
||||
help="Путь к исходному изображению-контейнеру",
|
||||
)
|
||||
sp_analyze.add_argument(
|
||||
"-b", "--stego",
|
||||
required=True,
|
||||
help="Путь к стего-изображению",
|
||||
)
|
||||
sp_analyze.add_argument(
|
||||
"--space",
|
||||
choices=["rgb", "ycbcr"],
|
||||
default="ycbcr",
|
||||
help="Цветовое пространство для метрик",
|
||||
)
|
||||
sp_analyze.add_argument(
|
||||
"--metrics",
|
||||
nargs="+",
|
||||
choices=["psnr"],
|
||||
default=["psnr"],
|
||||
help="Набор метрик",
|
||||
)
|
||||
|
||||
# ---------------- GENERATE ----------------
|
||||
sp_gen = sub.add_parser(
|
||||
"generate",
|
||||
help="Генерация изображений",
|
||||
formatter_class=ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
gen_sub = sp_gen.add_subparsers(dest="gen", required=True)
|
||||
|
||||
gen_sub.add_parser(
|
||||
"gradient",
|
||||
parents=[parent_gen_common],
|
||||
help="Сгенерировать градиент",
|
||||
formatter_class=ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
|
||||
sp_chess = gen_sub.add_parser(
|
||||
"chess",
|
||||
parents=[parent_gen_common],
|
||||
help="Сгенерировать шахматную доску",
|
||||
formatter_class=ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
sp_chess.add_argument(
|
||||
"--tile",
|
||||
type=int,
|
||||
default=32,
|
||||
help="Размер квадрата (должен делить --size)",
|
||||
)
|
||||
|
||||
return ap.parse_args(argv)
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 444 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 114 B |
|
After Width: | Height: | Size: 133 B |
|
After Width: | Height: | Size: 959 B |
|
After Width: | Height: | Size: 667 B |
@@ -0,0 +1,21 @@
|
||||
__all__ = [
|
||||
"Label64",
|
||||
"Sequence8",
|
||||
"ColorSpace",
|
||||
"BlockGrid8",
|
||||
"ChannelView",
|
||||
"RgbImage512",
|
||||
"PsnrMetric",
|
||||
"YcbcrImage512",
|
||||
"LsbRow8",
|
||||
]
|
||||
|
||||
from .label64 import Label64
|
||||
from .sequence8 import Sequence8
|
||||
from .color_space import ColorSpace
|
||||
from .block_grid8 import BlockGrid8
|
||||
from .channel_view import ChannelView
|
||||
from .rgb_image512 import RgbImage512
|
||||
from .psnr_metric import PsnrMetric
|
||||
from .ycbcr_image512 import YcbcrImage512
|
||||
from .lsb_row8 import LsbRow8
|
||||
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BlockGrid8:
|
||||
"""Разбиение и сборка блоков 8×8."""
|
||||
|
||||
@staticmethod
|
||||
def split(component: np.ndarray) -> np.ndarray:
|
||||
"""Разбить 2D-компоненту на блоки (N,8,8)."""
|
||||
if component.ndim != 2:
|
||||
raise ValueError("Ожидается 2D-массив компоненты.")
|
||||
h, w = component.shape
|
||||
if h % 8 != 0 or w % 8 != 0:
|
||||
raise ValueError("Размеры должны быть кратны 8.")
|
||||
# Перестановка осей даёт (nH, nW, 8, 8) -> (N, 8, 8)
|
||||
arr = np.asarray(component)
|
||||
nH, nW = h // 8, w // 8
|
||||
blocks = arr.reshape(nH, 8, nW, 8).transpose(
|
||||
0, 2, 1, 3).reshape(nH * nW, 8, 8)
|
||||
return blocks
|
||||
|
||||
@staticmethod
|
||||
def merge(blocks: np.ndarray, height: int, width: int) -> np.ndarray:
|
||||
"""Собрать 2D-компоненту из блоков (N,8,8)."""
|
||||
if blocks.ndim != 3 or blocks.shape[1:] != (8, 8):
|
||||
raise ValueError("Ожидается массив блоков формы (N,8,8).")
|
||||
if height % 8 != 0 or width % 8 != 0:
|
||||
raise ValueError("Размеры должны быть кратны 8.")
|
||||
nH, nW = height // 8, width // 8
|
||||
if blocks.shape[0] != nH * nW:
|
||||
raise ValueError(
|
||||
"Количество блоков не соответствует размеру выходной матрицы.")
|
||||
# Обратная перестановка из (N,8,8) -> (nH, nW, 8, 8) -> (H, W)
|
||||
arr = np.asarray(blocks)
|
||||
comp = arr.reshape(nH, nW, 8, 8).transpose(
|
||||
0, 2, 1, 3).reshape(height, width)
|
||||
return comp
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChannelView:
|
||||
"""Доступ к компонентам Y, Cb, Cr в массиве YCbCr."""
|
||||
|
||||
_MAP = {"y": 0, "cb": 1, "cr": 2}
|
||||
|
||||
@staticmethod
|
||||
def _idx(name: str) -> int:
|
||||
"""Вернуть индекс канала."""
|
||||
k = name.strip().lower()
|
||||
if k not in ChannelView._MAP:
|
||||
raise ValueError("Канал должен быть Y, Cb или Cr.")
|
||||
return ChannelView._MAP[k]
|
||||
|
||||
@staticmethod
|
||||
def get(ycbcr: np.ndarray, channel: str) -> np.ndarray:
|
||||
"""Вернуть 2D-компоненту выбранного канала."""
|
||||
if ycbcr.ndim != 3 or ycbcr.shape[-1] != 3:
|
||||
raise ValueError("Ожидается массив HxWx3.")
|
||||
i = ChannelView._idx(channel)
|
||||
return ycbcr[..., i]
|
||||
|
||||
@staticmethod
|
||||
def set(ycbcr: np.ndarray, channel: str, component: np.ndarray) -> np.ndarray:
|
||||
"""Вернуть копию YCbCr с заменённым каналом."""
|
||||
if ycbcr.ndim != 3 or ycbcr.shape[-1] != 3:
|
||||
raise ValueError("Ожидается массив HxWx3.")
|
||||
if component.shape != ycbcr.shape[:2]:
|
||||
raise ValueError("Размер компоненты не совпадает с HxW.")
|
||||
out = ycbcr.copy()
|
||||
i = ChannelView._idx(channel)
|
||||
# Короткая отсечка и приведение типа
|
||||
out[..., i] = np.clip(component, 0, 255).astype(np.uint8)
|
||||
return out
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ColorSpace:
|
||||
"""Конвертация RGB↔YCbCr с отсечкой диапазона."""
|
||||
|
||||
# Матрица прямого преобразования (используются коэффициенты из задания)
|
||||
_M = np.array([
|
||||
[0.299, 0.587, 0.144],
|
||||
[-0.168736, -0.331264, 0.5],
|
||||
[0.5, -0.418688, -0.081312],
|
||||
], dtype=np.float64)
|
||||
|
||||
_M_INV = np.linalg.inv(_M)
|
||||
|
||||
@staticmethod
|
||||
def _clip_u8(arr: np.ndarray) -> np.ndarray:
|
||||
"""Округлить и отсечь к uint8."""
|
||||
return np.clip(np.rint(arr), 0, 255).astype(np.uint8)
|
||||
|
||||
@classmethod
|
||||
def rgb_to_ycbcr(cls, rgb: np.ndarray) -> np.ndarray:
|
||||
"""RGB uint8 -> YCbCr uint8."""
|
||||
if rgb.ndim != 3 or rgb.shape[-1] != 3:
|
||||
raise ValueError("Ожидается массив HxWx3.")
|
||||
x = rgb.astype(np.float64).reshape(-1, 3)
|
||||
ycbcr = x @ cls._M.T
|
||||
ycbcr[:, 1:] += 128.0 # смещения Cb/Cr
|
||||
ycbcr = ycbcr.reshape(rgb.shape)
|
||||
return cls._clip_u8(ycbcr)
|
||||
|
||||
@classmethod
|
||||
def ycbcr_to_rgb(cls, ycbcr: np.ndarray) -> np.ndarray:
|
||||
"""YCbCr uint8 -> RGB uint8."""
|
||||
if ycbcr.ndim != 3 or ycbcr.shape[-1] != 3:
|
||||
raise ValueError("Ожидается массив HxWx3.")
|
||||
x = ycbcr.astype(np.float64).reshape(-1, 3)
|
||||
x[:, 1:] -= 128.0 # убрать смещения Cb/Cr
|
||||
rgb = x @ cls._M_INV.T
|
||||
rgb = rgb.reshape(ycbcr.shape)
|
||||
return cls._clip_u8(rgb)
|
||||
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Label64:
|
||||
"""Бинарная метка 64x64."""
|
||||
|
||||
data: np.ndarray # uint8, shape (64, 64), значения {0,1}
|
||||
|
||||
def __post_init__(self):
|
||||
arr = np.asarray(self.data, dtype=np.uint8)
|
||||
if arr.shape != (64, 64):
|
||||
raise ValueError("Метка должна быть 64x64.")
|
||||
# Нормализация к {0,1}
|
||||
arr = (arr > 0).astype(np.uint8)
|
||||
object.__setattr__(self, "data", arr)
|
||||
|
||||
@classmethod
|
||||
def from_image(cls, path: str, threshold: int = 128) -> "Label64":
|
||||
"""Создать из изображения 64x64."""
|
||||
img = Image.open(path).convert("L")
|
||||
if img.size != (64, 64):
|
||||
raise ValueError("Изображение метки должно быть 64x64.")
|
||||
arr = np.array(img, dtype=np.uint8)
|
||||
arr = (arr >= threshold).astype(np.uint8)
|
||||
return cls(arr)
|
||||
|
||||
@classmethod
|
||||
def from_vector(cls, vec: Iterable[int]) -> "Label64":
|
||||
"""Создать из вектора длиной 4096."""
|
||||
v = np.fromiter(vec, dtype=np.uint8, count=4096)
|
||||
if v.size != 4096:
|
||||
raise ValueError("Длина вектора должна быть 4096.")
|
||||
return cls(v.reshape(64, 64))
|
||||
|
||||
def to_vector(self) -> np.ndarray:
|
||||
"""Вернуть вектор длиной 4096."""
|
||||
return self.data.reshape(-1).astype(np.uint8)
|
||||
|
||||
def to_image(self, path: str) -> None:
|
||||
"""Сохранить PNG 64x64."""
|
||||
Image.fromarray(self.data * 255, mode="L").save(path)
|
||||
@@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LsbRow8:
|
||||
"""Встраивание/извлечение 8 бит в первый ряд блока 8×8 через LSB."""
|
||||
|
||||
@staticmethod
|
||||
def embed(block: np.ndarray, bits: np.ndarray) -> np.ndarray:
|
||||
"""Вернуть копию блока с вшитыми 8 битами."""
|
||||
if block.shape != (8, 8):
|
||||
raise ValueError("Нужен блок 8x8.")
|
||||
b = np.asarray(block, dtype=np.uint8).copy()
|
||||
v = np.asarray(bits, dtype=np.uint8).reshape(-1)
|
||||
if v.size != 8 or not np.isin(v, [0, 1]).all():
|
||||
raise ValueError("Нужно 8 бит 0/1.")
|
||||
# Меняем только младшие биты первых 8 пикселей верхней строки
|
||||
row = b[0, :8]
|
||||
row = (row & 0xFE) | v
|
||||
b[0, :8] = row
|
||||
return b
|
||||
|
||||
@staticmethod
|
||||
def extract(block: np.ndarray) -> np.ndarray:
|
||||
"""Извлечь 8 бит из LSB первого ряда блока."""
|
||||
if block.shape != (8, 8):
|
||||
raise ValueError("Нужен блок 8x8.")
|
||||
b = np.asarray(block, dtype=np.uint8)
|
||||
return (b[0, :8] & 1).astype(np.uint8)
|
||||
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PsnrMetric:
|
||||
"""Расчёт PSNR для двух массивов одинаковой формы."""
|
||||
|
||||
max_value: float = 255.0
|
||||
|
||||
def psnr(self, a: np.ndarray, b: np.ndarray) -> float:
|
||||
"""Вычислить PSNR."""
|
||||
if a.shape != b.shape:
|
||||
raise ValueError("Массивы должны иметь одинаковую форму.")
|
||||
x = a.astype(np.float64)
|
||||
y = b.astype(np.float64)
|
||||
# MSE по всем элементам (каналы включены)
|
||||
mse = np.mean((x - y) ** 2)
|
||||
if mse == 0.0:
|
||||
return float("inf")
|
||||
return 20.0 * np.log10(self.max_value) - 10.0 * np.log10(mse)
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RgbImage512:
|
||||
"""RGB-изображение 512x512."""
|
||||
|
||||
data: np.ndarray # uint8, shape (512, 512, 3)
|
||||
|
||||
def __post_init__(self):
|
||||
arr = np.asarray(self.data, dtype=np.uint8)
|
||||
if arr.shape != (512, 512, 3):
|
||||
raise ValueError("Изображение должно быть 512x512x3 (RGB).")
|
||||
object.__setattr__(self, "data", arr)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: str) -> "RgbImage512":
|
||||
"""Загрузить из файла."""
|
||||
img = Image.open(path).convert("RGB")
|
||||
if img.size != (512, 512):
|
||||
raise ValueError("Размер изображения должен быть 512x512.")
|
||||
return cls(np.array(img, dtype=np.uint8))
|
||||
|
||||
@classmethod
|
||||
def from_array(cls, arr: np.ndarray) -> "RgbImage512":
|
||||
"""Создать из массива 512x512x3."""
|
||||
return cls(arr)
|
||||
|
||||
def to_array(self) -> np.ndarray:
|
||||
"""Вернуть массив uint8 512x512x3."""
|
||||
return self.data
|
||||
|
||||
def save(self, path: str) -> None:
|
||||
"""Сохранить в файл."""
|
||||
Image.fromarray(self.data, mode="RGB").save(path)
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Sequence8:
|
||||
"""Пара 8-битных последовательностей: seq0 и инверсная seq1."""
|
||||
|
||||
_seq0: np.ndarray # uint8, shape (8,)
|
||||
|
||||
def __post_init__(self):
|
||||
arr = np.asarray(self._seq0, dtype=np.uint8).reshape(-1)
|
||||
if arr.size != 8:
|
||||
raise ValueError("seq0 должна содержать 8 бит.")
|
||||
if not np.isin(arr, [0, 1]).all():
|
||||
raise ValueError("seq0 может состоять только из 0 и 1.")
|
||||
object.__setattr__(self, "_seq0", arr)
|
||||
|
||||
@property
|
||||
def seq0(self) -> np.ndarray:
|
||||
"""Последовательность для бита 0."""
|
||||
return self._seq0
|
||||
|
||||
@property
|
||||
def seq1(self) -> np.ndarray:
|
||||
"""Инверсия seq0 для бита 1."""
|
||||
return 1 - self._seq0
|
||||
|
||||
def for_bit(self, bit: int) -> np.ndarray:
|
||||
"""Вернуть последовательность по значению бита."""
|
||||
if bit not in (0, 1):
|
||||
raise ValueError("bit должен быть 0 или 1.")
|
||||
return self._seq0 if bit == 0 else (1 - self._seq0)
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, bits: str) -> "Sequence8":
|
||||
"""Создать из строки вида '01010101'."""
|
||||
if len(bits) != 8 or any(ch not in "01" for ch in bits):
|
||||
raise ValueError("Строка должна быть из 8 символов 0/1.")
|
||||
arr = np.fromiter((int(ch) for ch in bits), dtype=np.uint8, count=8)
|
||||
return cls(arr)
|
||||
|
||||
@classmethod
|
||||
def from_iterable(cls, bits: Iterable[int]) -> "Sequence8":
|
||||
"""Создать из итерируемого набора из 8 бит."""
|
||||
arr = np.fromiter(bits, dtype=np.uint8, count=8)
|
||||
if arr.size != 8:
|
||||
raise ValueError("Нужно 8 бит.")
|
||||
return cls(arr)
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Длина последовательности."""
|
||||
return 8
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
# Локальные импорты внутри методов уменьшают риск циклов зависимостей
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class YcbcrImage512:
|
||||
"""YCbCr-изображение 512x512."""
|
||||
|
||||
data: np.ndarray # uint8, shape (512, 512, 3)
|
||||
|
||||
def __post_init__(self):
|
||||
arr = np.asarray(self.data, dtype=np.uint8)
|
||||
if arr.shape != (512, 512, 3):
|
||||
raise ValueError("Изображение должно быть 512x512x3 (YCbCr).")
|
||||
object.__setattr__(self, "data", arr)
|
||||
|
||||
@classmethod
|
||||
def from_rgb(cls, rgb_img) -> "YcbcrImage512":
|
||||
"""Создать из RgbImage512."""
|
||||
from .color_space import ColorSpace
|
||||
from .rgb_image512 import RgbImage512
|
||||
if not isinstance(rgb_img, RgbImage512):
|
||||
raise TypeError("Ожидается RgbImage512.")
|
||||
ycbcr = ColorSpace.rgb_to_ycbcr(rgb_img.to_array())
|
||||
return cls(ycbcr)
|
||||
|
||||
def to_rgb(self):
|
||||
"""Преобразовать в RgbImage512."""
|
||||
from .color_space import ColorSpace
|
||||
from .rgb_image512 import RgbImage512
|
||||
rgb = ColorSpace.ycbcr_to_rgb(self.data)
|
||||
return RgbImage512.from_array(rgb)
|
||||
|
||||
def get_channel(self, name: str) -> np.ndarray:
|
||||
"""Вернуть 2D-компоненту канала."""
|
||||
from .channel_view import ChannelView
|
||||
return ChannelView.get(self.data, name)
|
||||
|
||||
def with_channel(self, name: str, component: np.ndarray) -> "YcbcrImage512":
|
||||
"""Вернуть копию с заменённым каналом."""
|
||||
from .channel_view import ChannelView
|
||||
new_ycbcr = ChannelView.set(self.data, name, component)
|
||||
return YcbcrImage512(new_ycbcr)
|
||||
|
||||
def to_array(self) -> np.ndarray:
|
||||
"""Вернуть массив uint8 512x512x3."""
|
||||
return self.data
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
__all__ = ["TestImageGenerator"]
|
||||
|
||||
from .chessboard_generator import ChessboardGenerator
|
||||
from .gradient_generator import GradientGenerator
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
|
||||
class ChessboardGenerator:
|
||||
"""Шахматная доска произвольного размера и каналов."""
|
||||
|
||||
@staticmethod
|
||||
def make(size: int = 512, channels: int = 3, tile: int = 32) -> np.ndarray:
|
||||
"""Создать шахматку size×size; для RGB каждый квадрат случайного цвета."""
|
||||
if size <= 0:
|
||||
raise ValueError("size должен быть > 0.")
|
||||
if channels not in (1, 3):
|
||||
raise ValueError("channels должен быть 1 или 3.")
|
||||
if tile <= 0 or size % tile != 0:
|
||||
raise ValueError("tile должен быть > 0 и делить size без остатка.")
|
||||
|
||||
n = size // tile # число клеток по стороне
|
||||
|
||||
if channels == 1:
|
||||
base = (np.add.outer(np.arange(n), np.arange(n)) % 2).astype(np.uint8) * 255
|
||||
return np.kron(base, np.ones((tile, tile), dtype=np.uint8))
|
||||
|
||||
# RGB: случайный цвет на каждую клетку (uint8)
|
||||
rng = np.random.default_rng()
|
||||
colors = rng.integers(0, 256, size=(n, n, 3), dtype=np.uint8)
|
||||
|
||||
# Апсемплинг каждой компоненты по плитке
|
||||
r = np.kron(colors[:, :, 0], np.ones((tile, tile), dtype=np.uint8))
|
||||
g = np.kron(colors[:, :, 1], np.ones((tile, tile), dtype=np.uint8))
|
||||
b = np.kron(colors[:, :, 2], np.ones((tile, tile), dtype=np.uint8))
|
||||
return np.stack([r, g, b], axis=-1)
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import numpy as np
|
||||
|
||||
|
||||
class GradientGenerator:
|
||||
"""Градиентное изображение произвольного размера и каналов."""
|
||||
|
||||
@staticmethod
|
||||
def make(size: int = 512, channels: int = 3) -> np.ndarray:
|
||||
"""Создать квадратный градиент size×size для 1 или 3 каналов."""
|
||||
if not isinstance(size, int) or size <= 0:
|
||||
raise ValueError("size должен быть положительным целым.")
|
||||
if channels not in (1, 3):
|
||||
raise ValueError("channels должен быть 1 или 3.")
|
||||
|
||||
x = np.linspace(0.0, 255.0, num=size, dtype=np.float64)
|
||||
h = np.tile(x, (size, 1)) # горизонтальный градиент
|
||||
v = np.tile(x[:, None], (1, size)) # вертикальный градиент
|
||||
d = (0.5 * (h + v)) # диагональный градиент
|
||||
|
||||
if channels == 1:
|
||||
return d.astype(np.uint8)
|
||||
|
||||
rgb = np.stack([h, v, d], axis=2).astype(np.uint8) # R=горизонталь, G=вертикаль, B=диагональ
|
||||
return rgb
|
||||
@@ -0,0 +1,6 @@
|
||||
__all__ = ["EmbedMode", "ExtractMode", "AnalyzeQuality", "GenerateMode"]
|
||||
|
||||
from .embed_mode import EmbedMode
|
||||
from .extract_mode import ExtractMode
|
||||
from .analyze_quality import AnalyzeQuality
|
||||
from .generate_mode import GenerateMode
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List
|
||||
import numpy as np
|
||||
|
||||
from entities.rgb_image512 import RgbImage512
|
||||
from entities.color_space import ColorSpace
|
||||
from entities.psnr_metric import PsnrMetric
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalyzeQuality:
|
||||
"""CLI-режим расчёта метрик качества."""
|
||||
|
||||
def run(
|
||||
self,
|
||||
original_path: str,
|
||||
stego_path: str,
|
||||
space: str = "ycbcr",
|
||||
metrics: List[str] = None,
|
||||
) -> Dict[str, float]:
|
||||
"""Посчитать метрики между изображениями."""
|
||||
if metrics is None:
|
||||
metrics = ["psnr"]
|
||||
metrics = [m.lower() for m in metrics]
|
||||
if any(m != "psnr" for m in metrics):
|
||||
raise ValueError("Поддерживается только метрика PSNR.")
|
||||
|
||||
orig = RgbImage512.from_file(original_path).to_array()
|
||||
steg = RgbImage512.from_file(stego_path).to_array()
|
||||
|
||||
if space.lower() == "ycbcr":
|
||||
a = ColorSpace.rgb_to_ycbcr(orig)
|
||||
b = ColorSpace.rgb_to_ycbcr(steg)
|
||||
elif space.lower() == "rgb":
|
||||
a, b = orig, steg
|
||||
else:
|
||||
raise ValueError("space должен быть 'rgb' или 'ycbcr'.")
|
||||
|
||||
psnr = PsnrMetric().psnr(a, b)
|
||||
return {"psnr": float(psnr)}
|
||||