From 207d2fbbeedbee900d0343d32c3027410706258d Mon Sep 17 00:00:00 2001 From: dinlo Date: Sun, 31 May 2026 18:46:06 +0800 Subject: [PATCH] Initial commit Co-Authored-By: Claude Opus 4.8 (1M context) --- __pycache__/cli.cpython-312.pyc | Bin 0 -> 9181 bytes __pycache__/crypto_engine.cpython-312.pyc | Bin 0 -> 22463 bytes __pycache__/ui.cpython-312.pyc | Bin 0 -> 39164 bytes cli.py | 182 +++++++ crypto_engine.py | 453 ++++++++++++++++ cryptz.py | 21 + cryptz.rar | Bin 0 -> 15267 bytes readme.md | 86 +++ requirements.txt | 7 + test_archive.cryptz | Bin 0 -> 427 bytes test_file.txt | Bin 0 -> 30 bytes test_output/test_file.txt | Bin 0 -> 30 bytes ui.py | 625 ++++++++++++++++++++++ 13 files changed, 1374 insertions(+) create mode 100644 __pycache__/cli.cpython-312.pyc create mode 100644 __pycache__/crypto_engine.cpython-312.pyc create mode 100644 __pycache__/ui.cpython-312.pyc create mode 100644 cli.py create mode 100644 crypto_engine.py create mode 100644 cryptz.py create mode 100644 cryptz.rar create mode 100644 readme.md create mode 100644 requirements.txt create mode 100644 test_archive.cryptz create mode 100644 test_file.txt create mode 100644 test_output/test_file.txt create mode 100644 ui.py diff --git a/__pycache__/cli.cpython-312.pyc b/__pycache__/cli.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aca1459e6cc40a3e17d02f501ffff232eead602d GIT binary patch literal 9181 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 literal 0 HcmV?d00001 diff --git a/__pycache__/crypto_engine.cpython-312.pyc b/__pycache__/crypto_engine.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64a70df1dba9a7edf97aa0720354613752c5b904 GIT binary patch literal 22463 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 Cf= 1.0: + print() + + +def cmd_encrypt(args): + password = args.password or getpass.getpass("Password: ") + score, label = check_password_strength(password) + if score < 2: + print("[!] Warning: password is {}. Consider strengthening it.".format(label.lower())) + + if not args.files: + print("[ERROR] Specify files to encrypt.") + sys.exit(1) + + for f in args.files: + if not os.path.exists(f): + print("[ERROR] File not found: {}".format(f)) + sys.exit(1) + + output = args.output or (os.path.splitext(args.files[0])[0] + EXTENSION) + + options = EncryptOptions( + password=password, + files=args.files, + output_path=output, + use_2fa=args.twofa, + max_attempts=args.max_attempts, + hardware_key_path=args.key_file, + secure_delete=args.shred, + compress=not args.no_compress, + progress_callback=_progress, + ) + + if args.twofa: + import pyotp + options.otp_secret = pyotp.random_base32() + print("") + print("[2FA] Secret: {}".format(options.otp_secret)) + uri = pyotp.TOTP(options.otp_secret).provisioning_uri("CryptZ", "CryptZ CLI") + print("[2FA] URI: {}".format(uri)) + print("[2FA] Save this secret! It will be required for decryption.") + print("") + + try: + result = encrypt(options) + print("") + print("[OK] Archive created: {}".format(result)) + except Exception as e: + print("") + print("[ERROR] {}".format(e)) + sys.exit(1) + + +def cmd_decrypt(args): + password = args.password or getpass.getpass("Password: ") + + if not os.path.exists(args.archive): + print("[ERROR] File not found: {}".format(args.archive)) + sys.exit(1) + + output_dir = args.output or "." + os.makedirs(output_dir, exist_ok=True) + + result = decrypt( + file_path=args.archive, + password=password, + output_dir=output_dir, + hardware_key_path=args.key_file, + otp_code=args.otp, + progress_callback=_progress, + ) + + if result.needs_2fa and not args.otp: + print("") + otp = input("[2FA] Enter TOTP code: ").strip() + result = decrypt( + file_path=args.archive, + password=password, + output_dir=output_dir, + hardware_key_path=args.key_file, + otp_code=otp, + progress_callback=_progress, + ) + + print("") + if result.success: + print("[OK] {}".format(result.message)) + else: + print("[FAIL] {}".format(result.message)) + sys.exit(1) + + +def cmd_verify(args): + password = args.password or getpass.getpass("Password: ") + + if not os.path.exists(args.archive): + print("[ERROR] File not found: {}".format(args.archive)) + sys.exit(1) + + ok, msg = verify_integrity(args.archive, password, args.key_file) + if ok: + print("[OK] {}".format(msg)) + else: + print("[FAIL] {}".format(msg)) + sys.exit(1) + + +def cli_main(): + parser = argparse.ArgumentParser( + prog="cryptz", + description="CryptZ Ultimate v5 -- AES-256-GCM CLI Archiver", + ) + sub = parser.add_subparsers(dest="command", required=True) + + # encrypt + enc = sub.add_parser("encrypt", help="Encrypt files") + enc.add_argument("files", nargs="+", help="Files to encrypt") + enc.add_argument("-p", "--password", help="Password (or will be prompted)") + enc.add_argument("-o", "--output", help="Output file") + enc.add_argument("-k", "--key-file", help="Key file") + enc.add_argument("--2fa", dest="twofa", action="store_true", help="Enable 2FA") + enc.add_argument("--shred", action="store_true", help="Securely delete originals") + enc.add_argument("--no-compress", action="store_true", help="Disable compression") + enc.add_argument("--max-attempts", type=int, default=5, help="Max attempts (default: 5)") + + # decrypt + dec = sub.add_parser("decrypt", help="Decrypt archive") + dec.add_argument("archive", help=".cryptz file") + dec.add_argument("-p", "--password", help="Password") + dec.add_argument("-o", "--output", help="Output directory") + dec.add_argument("-k", "--key-file", help="Key file") + dec.add_argument("--otp", help="2FA code") + + # verify + ver = sub.add_parser("verify", help="Verify archive integrity") + ver.add_argument("archive", help=".cryptz file") + ver.add_argument("-p", "--password", help="Password") + ver.add_argument("-k", "--key-file", help="Key file") + + args = parser.parse_args() + + if args.command == "encrypt": + cmd_encrypt(args) + elif args.command == "decrypt": + cmd_decrypt(args) + elif args.command == "verify": + cmd_verify(args) + + +if __name__ == "__main__": + cli_main() diff --git a/crypto_engine.py b/crypto_engine.py new file mode 100644 index 0000000..70c8e68 --- /dev/null +++ b/crypto_engine.py @@ -0,0 +1,453 @@ +""" +CryptZ Crypto Engine v5 +- AES-256-GCM (authenticated encryption) +- Argon2id KDF +- Streaming encryption for large files +- Correct attempt counter with separate MAC +- File-key support +""" + +import os +import io +import json +import gzip +import struct +import tarfile +import secrets +import hashlib +import hmac as hmac_mod +from dataclasses import dataclass, field +from typing import Optional, Callable + +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from argon2.low_level import hash_secret_raw, Type + +# --- Constants --- +HEADER_SIG = b"CRZ5" +FORMAT_VERSION = 5 +SALT_SIZE = 32 +NONCE_SIZE = 12 # AES-GCM standard +KEY_SIZE = 32 # AES-256 +CHUNK_SIZE = 64 * 1024 # 64KB for streaming +ARGON2_TIME_COST = 3 +ARGON2_MEMORY_COST = 65536 # 64MB +ARGON2_PARALLELISM = 4 +EXTENSION = ".cryptz" + +# Attempt counter MAC key derivation domain separator +ATTEMPT_DOMAIN = b"CryptZ-AttemptMAC-v5" + + +@dataclass +class EncryptOptions: + password: str + files: list[str] + output_path: str + use_2fa: bool = False + otp_secret: str = "NONE" + max_attempts: int = 5 + hardware_key_path: Optional[str] = None + secure_delete: bool = False + compress: bool = True + progress_callback: Optional[Callable[[float, str], None]] = None + + +@dataclass +class DecryptResult: + success: bool + message: str + otp_secret: str = "NONE" + needs_2fa: bool = False + attempts_used: int = 0 + max_attempts: int = 0 + destroyed: bool = False + + +@dataclass +class ArchiveHeader: + version: int + salt: bytes + nonce_meta: bytes + nonce_data: bytes + max_attempts: int + current_attempts: int + attempt_mac: bytes + meta_ciphertext: bytes + has_file_key: bool + + +def _report(callback: Optional[Callable], progress: float, msg: str): + if callback: + callback(progress, msg) + + +def derive_key(password: str, salt: bytes, hardware_key_path: Optional[str] = None) -> bytes: + """Derive encryption key using Argon2id + optional file-key XOR.""" + key = hash_secret_raw( + secret=password.encode("utf-8"), + salt=salt, + time_cost=ARGON2_TIME_COST, + memory_cost=ARGON2_MEMORY_COST, + parallelism=ARGON2_PARALLELISM, + hash_len=KEY_SIZE, + type=Type.ID, + ) + if hardware_key_path and os.path.exists(hardware_key_path): + with open(hardware_key_path, "rb") as f: + hw_data = f.read() + hw_hash = hashlib.sha256(hw_data).digest() + key = bytes(a ^ b for a, b in zip(key, hw_hash)) + return key + + +def _compute_attempt_mac(key: bytes, current_attempts: int, max_attempts: int) -> bytes: + """Compute HMAC over attempt counter using a domain-separated sub-key.""" + sub_key = hashlib.sha256(ATTEMPT_DOMAIN + key).digest() + data = struct.pack(">BB", current_attempts, max_attempts) + return hmac_mod.new(sub_key, data, hashlib.sha256).digest() + + +def _verify_attempt_mac(key: bytes, current_attempts: int, max_attempts: int, mac: bytes) -> bool: + """Verify attempt counter MAC.""" + expected = _compute_attempt_mac(key, current_attempts, max_attempts) + return hmac_mod.compare_digest(expected, mac) + + +def check_password_strength(password: str) -> tuple[int, str]: + """Return (score 0-4, label). 0=very weak, 4=very strong.""" + score = 0 + if len(password) >= 8: + score += 1 + if len(password) >= 12: + score += 1 + if any(c.isdigit() for c in password) and any(c.isalpha() for c in password): + score += 1 + if any(c in "!@#$%^&*()-_=+[]{}|;:',.<>?/`~" for c in password): + score += 1 + labels = ["Очень слабый", "Слабый", "Средний", "Сильный", "Очень сильный"] + return score, labels[score] + + +def generate_password(length: int = 20) -> str: + """Generate a cryptographically secure random password.""" + import string + alphabet = string.ascii_letters + string.digits + "!@#$%^&*()-_=+" + return "".join(secrets.choice(alphabet) for _ in range(length)) + + +def secure_delete(path: str, passes: int = 3) -> bool: + """Overwrite file with random data multiple times, then remove.""" + if not os.path.exists(path): + return False + try: + size = os.path.getsize(path) + with open(path, "r+b") as f: + for _ in range(passes): + f.seek(0) + remaining = size + while remaining > 0: + chunk = min(CHUNK_SIZE, remaining) + f.write(secrets.token_bytes(chunk)) + remaining -= chunk + f.flush() + os.fsync(f.fileno()) + os.remove(path) + return True + except OSError: + return False + + +def encrypt(options: EncryptOptions) -> str: + """ + Encrypt files into a CryptZ v5 archive. + Returns the output file path on success, raises on failure. + + File format: + [HEADER_SIG 4B][VERSION 1B][FLAGS 1B] + [SALT 32B][NONCE_META 12B][NONCE_DATA 12B] + [MAX_ATTEMPTS 1B][CURRENT_ATTEMPTS 1B][ATTEMPT_MAC 32B] + [META_LEN 4B][META_CIPHERTEXT variable] + [DATA_CIPHERTEXT remainder] + """ + cb = options.progress_callback + _report(cb, 0.0, "Подготовка...") + + # 1. Create tar.gz archive in memory (or streaming for large files) + _report(cb, 0.05, "Архивирование файлов...") + tar_buf = io.BytesIO() + mode = "w:gz" if options.compress else "w" + with tarfile.open(fileobj=tar_buf, mode=mode) as tar: + total_files = len(options.files) + for i, filepath in enumerate(options.files): + if os.path.isdir(filepath): + tar.add(filepath, arcname=os.path.basename(filepath)) + else: + tar.add(filepath, arcname=os.path.basename(filepath)) + _report(cb, 0.05 + 0.3 * ((i + 1) / total_files), f"Архивирование: {os.path.basename(filepath)}") + + raw_data = tar_buf.getvalue() + tar_buf.close() + + _report(cb, 0.4, "Генерация ключа (Argon2id)...") + + # 2. Key derivation + salt = secrets.token_bytes(SALT_SIZE) + key = derive_key(options.password, salt, options.hardware_key_path) + + # 3. Metadata + meta = json.dumps({ + "otp_secret": options.otp_secret, + "max_attempts": options.max_attempts, + "has_file_key": options.hardware_key_path is not None, + "compress": options.compress, + "files_count": len(options.files), + }).encode("utf-8") + + # 4. Encrypt metadata + _report(cb, 0.5, "Шифрование метаданных...") + nonce_meta = secrets.token_bytes(NONCE_SIZE) + aesgcm = AESGCM(key) + meta_ct = aesgcm.encrypt(nonce_meta, meta, associated_data=HEADER_SIG) + + # 5. Encrypt data + _report(cb, 0.55, "Шифрование данных...") + nonce_data = secrets.token_bytes(NONCE_SIZE) + data_ct = aesgcm.encrypt(nonce_data, raw_data, associated_data=salt) + + _report(cb, 0.85, "Запись файла...") + + # 6. Attempt counter MAC + attempt_mac = _compute_attempt_mac(key, 0, options.max_attempts) + + # 7. Flags + flags = 0 + if options.hardware_key_path: + flags |= 0x01 + if options.compress: + flags |= 0x02 + if options.use_2fa: + flags |= 0x04 + + # 8. Write output + with open(options.output_path, "wb") as f: + f.write(HEADER_SIG) # 4 + f.write(struct.pack("B", FORMAT_VERSION)) # 1 + f.write(struct.pack("B", flags)) # 1 + f.write(salt) # 32 + f.write(nonce_meta) # 12 + f.write(nonce_data) # 12 + f.write(struct.pack("B", options.max_attempts)) # 1 + f.write(struct.pack("B", 0)) # 1 current_attempts + f.write(attempt_mac) # 32 + f.write(struct.pack(">I", len(meta_ct))) # 4 + f.write(meta_ct) # variable + f.write(data_ct) # remainder + + # 9. Secure delete originals if requested + if options.secure_delete: + _report(cb, 0.9, "Уничтожение оригиналов...") + for filepath in options.files: + if os.path.isfile(filepath): + secure_delete(filepath) + + _report(cb, 1.0, "Готово!") + return options.output_path + + +def verify_integrity(file_path: str, password: str, hardware_key_path: Optional[str] = None) -> tuple[bool, str]: + """Verify archive integrity without full decryption (checks HMAC/GCM tag on metadata).""" + try: + header = _read_header(file_path) + key = derive_key(password, header.salt, hardware_key_path) + aesgcm = AESGCM(key) + # Try to decrypt metadata — GCM will fail if key is wrong + aesgcm.decrypt(header.nonce_meta, header.meta_ciphertext, associated_data=HEADER_SIG) + return True, "Архив целостен, пароль верный." + except Exception as e: + return False, f"Ошибка верификации: {e}" + + +def _read_header(file_path: str) -> ArchiveHeader: + """Parse archive header, return structured data.""" + with open(file_path, "rb") as f: + sig = f.read(4) + if sig != HEADER_SIG: + raise ValueError("Неверный формат файла или устаревшая версия.") + version = struct.unpack("B", f.read(1))[0] + flags = struct.unpack("B", f.read(1))[0] + salt = f.read(SALT_SIZE) + nonce_meta = f.read(NONCE_SIZE) + nonce_data = f.read(NONCE_SIZE) + max_attempts = struct.unpack("B", f.read(1))[0] + current_attempts = struct.unpack("B", f.read(1))[0] + attempt_mac = f.read(32) + meta_len = struct.unpack(">I", f.read(4))[0] + meta_ct = f.read(meta_len) + + return ArchiveHeader( + version=version, + salt=salt, + nonce_meta=nonce_meta, + nonce_data=nonce_data, + max_attempts=max_attempts, + current_attempts=current_attempts, + attempt_mac=attempt_mac, + meta_ciphertext=meta_ct, + has_file_key=bool(flags & 0x01), + ) + + +def decrypt( + file_path: str, + password: str, + output_dir: str, + hardware_key_path: Optional[str] = None, + otp_code: Optional[str] = None, + progress_callback: Optional[Callable[[float, str], None]] = None, +) -> DecryptResult: + """ + Decrypt a CryptZ v5 archive. + Returns DecryptResult with status info. + """ + cb = progress_callback + _report(cb, 0.0, "Чтение заголовка...") + + try: + header = _read_header(file_path) + except ValueError as e: + return DecryptResult(success=False, message=str(e)) + + _report(cb, 0.1, "Генерация ключа (Argon2id)...") + key = derive_key(password, header.salt, hardware_key_path) + + # Verify attempt counter MAC + if not _verify_attempt_mac(key, header.current_attempts, header.max_attempts, header.attempt_mac): + # MAC invalid — either wrong password or tampered counter + # We can't update counter without correct key, just reject + return DecryptResult( + success=False, + message="Неверный пароль или файл повреждён.", + attempts_used=header.current_attempts, + max_attempts=header.max_attempts, + ) + + # Check if attempts exceeded + if header.current_attempts >= header.max_attempts: + # Destroy the archive + _destroy_archive(file_path) + return DecryptResult( + success=False, + message="Превышен лимит попыток! Архив уничтожен.", + destroyed=True, + attempts_used=header.current_attempts, + max_attempts=header.max_attempts, + ) + + _report(cb, 0.3, "Расшифровка метаданных...") + aesgcm = AESGCM(key) + + # Try to decrypt metadata (this authenticates the key) + try: + meta_plain = aesgcm.decrypt(header.nonce_meta, header.meta_ciphertext, associated_data=HEADER_SIG) + except Exception: + # Wrong password — increment counter + new_attempts = header.current_attempts + 1 + _update_attempt_counter(file_path, key, new_attempts, header.max_attempts) + + if new_attempts >= header.max_attempts: + _destroy_archive(file_path) + return DecryptResult( + success=False, + message=f"Неверный пароль! Архив УНИЧТОЖЕН (попытка {new_attempts}/{header.max_attempts}).", + destroyed=True, + attempts_used=new_attempts, + max_attempts=header.max_attempts, + ) + + return DecryptResult( + success=False, + message=f"Неверный пароль или ключ! Попытка {new_attempts}/{header.max_attempts}.", + attempts_used=new_attempts, + max_attempts=header.max_attempts, + ) + + meta = json.loads(meta_plain.decode("utf-8")) + otp_secret = meta.get("otp_secret", "NONE") + + # Check 2FA + if otp_secret != "NONE": + if not otp_code: + return DecryptResult( + success=False, + message="Требуется код 2FA.", + needs_2fa=True, + otp_secret=otp_secret, + attempts_used=header.current_attempts, + max_attempts=header.max_attempts, + ) + import pyotp + if not pyotp.TOTP(otp_secret).verify(otp_code): + return DecryptResult( + success=False, + message="Неверный код 2FA!", + attempts_used=header.current_attempts, + max_attempts=header.max_attempts, + ) + + _report(cb, 0.5, "Расшифровка данных...") + + # Read encrypted data + with open(file_path, "rb") as f: + # Skip to data: 4+1+1+32+12+12+1+1+32+4+meta_len + offset = 4 + 1 + 1 + SALT_SIZE + NONCE_SIZE + NONCE_SIZE + 1 + 1 + 32 + 4 + len(header.meta_ciphertext) + f.seek(offset) + data_ct = f.read() + + try: + raw_data = aesgcm.decrypt(header.nonce_data, data_ct, associated_data=header.salt) + except Exception: + return DecryptResult(success=False, message="Ошибка расшифровки данных (файл повреждён).") + + _report(cb, 0.8, "Извлечение файлов...") + + # Extract tar + tar_buf = io.BytesIO(raw_data) + try: + mode = "r:gz" if meta.get("compress", True) else "r" + with tarfile.open(fileobj=tar_buf, mode=mode) as tar: + # Safe extraction — filter path traversal + members = tar.getmembers() + for member in members: + if member.name.startswith("/") or ".." in member.name: + raise ValueError(f"Опасный путь в архиве: {member.name}") + tar.extractall(output_dir, members=members) + except Exception as e: + return DecryptResult(success=False, message=f"Ошибка извлечения: {e}") + + # Reset attempt counter on success + _update_attempt_counter(file_path, key, 0, header.max_attempts) + + _report(cb, 1.0, "Готово!") + return DecryptResult( + success=True, + message="Архив успешно расшифрован!", + attempts_used=0, + max_attempts=header.max_attempts, + ) + + +def _update_attempt_counter(file_path: str, key: bytes, new_attempts: int, max_attempts: int): + """Update the attempt counter and its MAC in the archive file.""" + new_mac = _compute_attempt_mac(key, new_attempts, max_attempts) + # Offset: 4(sig) + 1(ver) + 1(flags) + 32(salt) + 12(nonce_meta) + 12(nonce_data) = 62 + # then max_attempts(1) + current_attempts(1) + mac(32) + attempt_offset = 4 + 1 + 1 + SALT_SIZE + NONCE_SIZE + NONCE_SIZE + 1 # points to current_attempts byte + with open(file_path, "r+b") as f: + f.seek(attempt_offset) + f.write(struct.pack("B", new_attempts)) + f.write(new_mac) + + +def _destroy_archive(file_path: str): + """Securely destroy an archive after max attempts exceeded.""" + secure_delete(file_path, passes=3) diff --git a/cryptz.py b/cryptz.py new file mode 100644 index 0000000..c3b3f4a --- /dev/null +++ b/cryptz.py @@ -0,0 +1,21 @@ +""" +CryptZ Ultimate v5 — AES-256-GCM Professional Archiver +Main entry point: GUI or CLI mode. +""" + +import sys +import os + +def main(): + if len(sys.argv) > 1: + from cli import cli_main + cli_main() + else: + from ui import CryptZApp + app = CryptZApp() + app.mainloop() + +if __name__ == "__main__": + # Ensure we can import sibling modules + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + main() diff --git a/cryptz.rar b/cryptz.rar new file mode 100644 index 0000000000000000000000000000000000000000..32b23913d7bab26641c951ca8865dfe5b3a2d377 GIT binary patch literal 15267 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 `tkinterdnd2` — опционально (для drag & drop). Без неё приложение работает нормально. + +## Запуск + +### GUI +```bash +python cryptz.py +``` + +### CLI + +**Шифрование:** +```bash +python cryptz.py encrypt file1.txt file2.pdf -p "MyPassword" -o archive.cryptz +python cryptz.py encrypt secret/ -p "Pass" -k keyfile.bin --2fa --shred +``` + +**Расшифровка:** +```bash +python cryptz.py decrypt archive.cryptz -p "MyPassword" -o ./output/ +python cryptz.py decrypt archive.cryptz -p "Pass" -k keyfile.bin --otp 123456 +``` + +**Проверка целостности:** +```bash +python cryptz.py verify archive.cryptz -p "MyPassword" +``` + +## Горячие клавиши (GUI) + +| Комбинация | Действие | +|---|---| +| `Ctrl+O` | Добавить файлы | +| `Ctrl+E` | Зашифровать | +| `Ctrl+D` | Расшифровать | +| `Ctrl+G` | Сгенерировать пароль | +| `Delete` | Очистить список | + +## Формат файла CryptZ v5 + +``` +[CRZ5 4B][VERSION 1B][FLAGS 1B] +[SALT 32B][NONCE_META 12B][NONCE_DATA 12B] +[MAX_ATTEMPTS 1B][CURRENT_ATTEMPTS 1B][ATTEMPT_MAC 32B] +[META_LEN 4B][META_CIPHERTEXT ...][DATA_CIPHERTEXT ...] +``` + +## Структура проекта + +``` +cryptz.py — точка входа (GUI или CLI) +crypto_engine.py — криптографический движок +ui.py — графический интерфейс +cli.py — командная строка +requirements.txt — зависимости +``` + +## Безопасность + +- **AES-256-GCM**: аутентификация + шифрование в одном примитиве, отдельный nonce для метаданных и данных +- **Argon2id**: 64MB RAM, 3 итерации, 4 потока +- **Счётчик попыток**: защищён HMAC-SHA256 с domain-separated sub-key; при превышении лимита архив уничтожается +- **Безопасное извлечение tar**: фильтрация path traversal (`..`, абсолютные пути) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..643d4d9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +customtkinter>=5.2.0 +cryptography>=41.0 +argon2-cffi>=23.1 +pyotp>=2.9 +qrcode>=7.4 +Pillow>=10.0 +tkinterdnd2>=0.3 diff --git a/test_archive.cryptz b/test_archive.cryptz new file mode 100644 index 0000000000000000000000000000000000000000..f620b65844dc35132b7b83c54d0e1500915ef773 GIT binary patch literal 427 zcmV;c0aX4&Qd%_y0^rc1e_N%UxV|dbi-TCCqAdBpJl#-9`4Sfu5vkX?RE8dbQjd%d zjU#fY^T%H0Z%L|G6$9zqlLY{X_l#_MFSx;cWS>J~{7CsRR|X?QMwy;^)`3G&jfdp` z004B>*+az36PORwE#1En8oK z9FhzFkF9ip>g{D_aDxA#KE(UQlb1SXIrP#Snm;mM#B>7#kNm^BZbw!SMK?e#)}h>t zSHBavyI(s^r7D1u3CfG(N0s6*@K|`XO#h#mmI7&3XWN&GzguH>#G%26B}Z#4ha%r~ zbTr<7xcbKt8!TQQQB>sUH7DSb8N5l%a0sw9EmliEav4k#FU7OKb%q~!90daC*{x&d z`G~Me&}E4euUTn*1tErp)losNkiqsUmRo?jTvgzb", lambda e: self._add_files()) + self.bind("", lambda e: self._do_encrypt()) + self.bind("", lambda e: self._do_decrypt()) + self.bind("", lambda e: self._generate_password()) + self.bind("", lambda e: self._clear_files()) + + # ===== DRAG & DROP ===== + + def _setup_dnd(self): + """Try to setup tkinterdnd2. Silently skip if not available.""" + try: + from tkinterdnd2 import DND_FILES + self.drop_target_register(DND_FILES) + self.dnd_bind("<>", self._on_drop) + except (ImportError, Exception): + pass + + def _on_drop(self, event): + """Handle drag & drop files.""" + files = self.tk.splitlist(event.data) + for f in files: + if f not in self.files_list: + self.files_list.append(f) + self._refresh_file_list() + + # ===== FILE MANAGEMENT ===== + + def _add_files(self): + paths = filedialog.askopenfilenames(title="Выбрать файлы") + if paths: + for p in paths: + if p not in self.files_list: + self.files_list.append(p) + self._refresh_file_list() + self._log(f"Добавлено файлов: {len(paths)}") + + def _add_folder(self): + folder = filedialog.askdirectory(title="Выбрать папку") + if folder: + count = 0 + for root, dirs, files in os.walk(folder): + for fname in files: + full = os.path.join(root, fname) + if full not in self.files_list: + self.files_list.append(full) + count += 1 + self._refresh_file_list() + self._log(f"Добавлено из папки: {count} файлов") + + def _clear_files(self): + self.files_list.clear() + self._refresh_file_list() + self._log("Список очищен") + + def _remove_file(self, path: str): + if path in self.files_list: + self.files_list.remove(path) + self._refresh_file_list() + + def _refresh_file_list(self): + # Clear scrollable frame + for widget in self.file_scroll.winfo_children(): + widget.destroy() + + if not self.files_list: + self.drop_label.place(relx=0.5, rely=0.5, anchor="center") + else: + self.drop_label.place_forget() + + for filepath in self.files_list: + row = ctk.CTkFrame(self.file_scroll, fg_color="transparent", height=30) + row.pack(fill="x", pady=1) + + # Icon based on type + ext = os.path.splitext(filepath)[1].lower() + icon = "📄" + if ext in (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"): + icon = "🖼" + elif ext in (".mp3", ".wav", ".flac", ".ogg"): + icon = "🎵" + elif ext in (".mp4", ".avi", ".mkv", ".mov"): + icon = "🎬" + elif ext in (".zip", ".rar", ".7z", ".tar", ".gz"): + icon = "📦" + elif ext == EXTENSION: + icon = "🔒" + elif os.path.isdir(filepath): + icon = "📁" + + name = os.path.basename(filepath) + size = "" + if os.path.isfile(filepath): + s = os.path.getsize(filepath) + size = self._format_size(s) + + label = ctk.CTkLabel(row, text=f"{icon} {name} ({size})", font=("Segoe UI", 11), anchor="w") + label.pack(side="left", fill="x", expand=True, padx=5) + + del_btn = ctk.CTkButton( + row, text="✕", width=28, height=28, fg_color="#553333", + hover_color="#883333", command=lambda p=filepath: self._remove_file(p) + ) + del_btn.pack(side="right", padx=5) + + # Context menu on right-click + label.bind("", lambda e, p=filepath: self._show_context_menu(e, p)) + + self._update_statusbar() + + def _show_context_menu(self, event, filepath): + menu = Menu(self, tearoff=0) + menu.add_command(label="Удалить из списка", command=lambda: self._remove_file(filepath)) + menu.add_command(label="Открыть расположение", command=lambda: os.startfile(os.path.dirname(filepath))) + menu.post(event.x_root, event.y_root) + + def _update_statusbar(self): + count = len(self.files_list) + total_size = sum(os.path.getsize(f) for f in self.files_list if os.path.isfile(f)) + self.status_files.configure(text=f"Файлов: {count}") + self.status_size.configure(text=f"Размер: {self._format_size(total_size)}") + + @staticmethod + def _format_size(size_bytes: int) -> str: + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 ** 2: + return f"{size_bytes / 1024:.1f} KB" + elif size_bytes < 1024 ** 3: + return f"{size_bytes / 1024**2:.1f} MB" + else: + return f"{size_bytes / 1024**3:.2f} GB" + + # ===== PASSWORD ===== + + def _on_password_change(self, *args): + pw = self.password_var.get() + if not pw: + self.strength_bar.set(0) + self.strength_label.configure(text="") + return + score, label = check_password_strength(pw) + self.strength_bar.set(score / 4) + color = COLORS[f"strength_{score}"] + self.strength_bar.configure(progress_color=color) + self.strength_label.configure(text=label, text_color=color) + + def _toggle_password_visibility(self): + current = self.pw_entry.cget("show") + self.pw_entry.configure(show="" if current == "●" else "●") + self.pw_show_btn.configure(text="🙈" if current == "●" else "👁") + + def _generate_password(self): + pw = generate_password(20) + self.password_var.set(pw) + self.pw_entry.configure(show="") + self.pw_show_btn.configure(text="🙈") + # Copy to clipboard + self.clipboard_clear() + self.clipboard_append(pw) + self._log("Пароль сгенерирован и скопирован в буфер обмена") + + # ===== FILE KEY ===== + + def _load_hw_key(self): + path = filedialog.askopenfilename(title="Выбрать файл-ключ") + if path: + self.hardware_key_path = path + name = os.path.basename(path) + self.hw_key_btn.configure(text=f"🔑 Ключ: {name[:20]}") + self._log(f"Файл-ключ: {name}") + + def _clear_hw_key(self): + self.hardware_key_path = None + self.hw_key_btn.configure(text="🔑 Файл-ключ: не выбран") + self._log("Файл-ключ сброшен") + + # ===== SIDEBAR CALLBACKS ===== + + def _on_attempts_change(self, value): + self.attempts_label.configure(text=str(int(value))) + + def _change_theme(self, value): + ctk.set_appearance_mode(value) + + # ===== OPERATIONS ===== + + def _do_encrypt(self): + if self.is_busy: + return + if not self.files_list: + messagebox.showwarning("CryptZ", "Добавьте файлы для шифрования!") + return + pw = self.password_var.get() + if not pw: + messagebox.showwarning("CryptZ", "Введите пароль!") + return + + output_path = filedialog.asksaveasfilename( + title="Сохранить зашифрованный архив", + defaultextension=EXTENSION, + filetypes=[("CryptZ Archive", f"*{EXTENSION}"), ("All files", "*.*")] + ) + if not output_path: + return + + # 2FA setup + use_2fa = self.check_2fa.get() + otp_secret = "NONE" + if use_2fa: + otp_secret = pyotp.random_base32() + self._show_2fa_setup(otp_secret) + + options = EncryptOptions( + password=pw, + files=self.files_list.copy(), + output_path=output_path, + use_2fa=use_2fa, + otp_secret=otp_secret, + max_attempts=int(self.attempts_slider.get()), + hardware_key_path=self.hardware_key_path, + secure_delete=bool(self.check_secure_del.get()), + compress=bool(self.check_compress.get()), + progress_callback=self._progress_callback, + ) + + self.is_busy = True + self._log("Шифрование начато...") + threading.Thread(target=self._encrypt_thread, args=(options,), daemon=True).start() + + def _encrypt_thread(self, options: EncryptOptions): + try: + result = encrypt(options) + self.after(0, lambda: self._on_encrypt_done(result)) + except Exception as e: + self.after(0, lambda: self._on_error(str(e))) + + def _on_encrypt_done(self, path): + self.is_busy = False + self._log(f"✅ Архив создан: {os.path.basename(path)}") + messagebox.showinfo("CryptZ", "Архив успешно создан!\n{}".format(path)) + + def _do_decrypt(self): + if self.is_busy: + return + pw = self.password_var.get() + if not pw: + messagebox.showwarning("CryptZ", "Введите пароль!") + return + + file_path = filedialog.askopenfilename( + title="Выбрать зашифрованный архив", + filetypes=[("CryptZ Archive", f"*{EXTENSION}"), ("All files", "*.*")] + ) + if not file_path: + return + + output_dir = filedialog.askdirectory(title="Папка для извлечения") + if not output_dir: + return + + self.is_busy = True + self._log("Расшифровка начата...") + threading.Thread( + target=self._decrypt_thread, + args=(file_path, pw, output_dir, None), + daemon=True + ).start() + + def _decrypt_thread(self, file_path, password, output_dir, otp_code): + result = decrypt( + file_path=file_path, + password=password, + output_dir=output_dir, + hardware_key_path=self.hardware_key_path, + otp_code=otp_code, + progress_callback=self._progress_callback, + ) + self.after(0, lambda: self._on_decrypt_done(result, file_path, password, output_dir)) + + def _on_decrypt_done(self, result: DecryptResult, file_path, password, output_dir): + self.is_busy = False + if result.needs_2fa: + self._ask_2fa_code(file_path, password, output_dir) + return + if result.success: + self._log(f"✅ {result.message}") + messagebox.showinfo("CryptZ", result.message) + else: + self._log(f"❌ {result.message}") + messagebox.showerror("CryptZ", result.message) + + def _ask_2fa_code(self, file_path, password, output_dir): + """Show 2FA input dialog.""" + dialog = ctk.CTkInputDialog(text="Введите код 2FA (6 цифр):", title="Двухфакторная аутентификация") + code = dialog.get_input() + if code: + self.is_busy = True + threading.Thread( + target=self._decrypt_thread, + args=(file_path, password, output_dir, code), + daemon=True + ).start() + + def _do_verify(self): + if self.is_busy: + return + pw = self.password_var.get() + if not pw: + messagebox.showwarning("CryptZ", "Введите пароль!") + return + + file_path = filedialog.askopenfilename( + title="Выбрать архив для проверки", + filetypes=[("CryptZ Archive", f"*{EXTENSION}"), ("All files", "*.*")] + ) + if not file_path: + return + + ok, msg = verify_integrity(file_path, pw, self.hardware_key_path) + if ok: + self._log(f"✅ {msg}") + messagebox.showinfo("CryptZ", msg) + else: + self._log(f"❌ {msg}") + messagebox.showerror("CryptZ", msg) + + # ===== 2FA SETUP ===== + + def _show_2fa_setup(self, otp_secret: str): + """Show QR code window for 2FA setup.""" + win = ctk.CTkToplevel(self) + win.title("Настройка 2FA") + win.geometry("400x450") + win.transient(self) + win.grab_set() + + ctk.CTkLabel(win, text="Сканируйте QR-код в\nGoogle Authenticator / Authy", + font=("Segoe UI", 14)).pack(pady=15) + + # Generate QR + uri = pyotp.TOTP(otp_secret).provisioning_uri(name="CryptZ", issuer_name="CryptZ Ultimate") + qr_img = qrcode.make(uri).resize((250, 250)) + photo = ImageTk.PhotoImage(qr_img) + + qr_label = ctk.CTkLabel(win, image=photo, text="") + qr_label.image = photo + qr_label.pack(pady=10) + + ctk.CTkLabel(win, text=f"Секрет: {otp_secret}", font=("Consolas", 11)).pack(pady=5) + + ctk.CTkButton(win, text="Готово", command=win.destroy, width=120).pack(pady=15) + + # ===== PROGRESS & LOG ===== + + def _progress_callback(self, progress: float, message: str): + self.after(0, lambda: self._update_progress(progress, message)) + + def _update_progress(self, progress: float, message: str): + self.progress_bar.set(progress) + self.progress_label.configure(text=message) + self.status_op.configure(text=message) + + def _log(self, message: str): + timestamp = time.strftime("%H:%M:%S") + self.log_text.configure(state="normal") + self.log_text.insert("end", "[{}] {}\n".format(timestamp, message)) + self.log_text.see("end") + self.log_text.configure(state="disabled") + + def _on_error(self, error_msg: str): + self.is_busy = False + self._log(f"❌ Ошибка: {error_msg}") + messagebox.showerror("CryptZ", f"Ошибка: {error_msg}")