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