Ntrlegendzip
def encrypt_zip(
src_paths: List[pathlib.Path],
dst_path: pathlib.Path,
password: str,
compression: int = zipfile.ZIP_DEFLATED,
compresslevel: int = 9,
) -> None:
# Normalise arguments
src_paths = [p if isinstance(p, pathlib.Path) else pathlib.Path(p) for p in src_paths]
dst_path = pathlib.Path(dst_path)
# Ensure destination folder exists
dst_path.parent.mkdir(parents=True, exist_ok=True)
# Open a new zip file in write‑binary mode
with zipfile.ZipFile(
dst_path,
mode='w',
compression=compression,
compresslevel=compresslevel,
allowZip64=True,
) as zf:
for src in src_paths:
if not src.exists():
raise FileNotFoundError(f"Source src!s does not exist")
# Walk directories recursively
for root, _, files in os.walk(src):
for fname in files:
full_path = pathlib.Path(root) / fname
# Compute the archive name (relative to the root `src`)
arcname = full_path.relative_to(src.parent).as_posix()
# ----- read raw bytes -----
with full_path.open('rb') as f:
plaintext = f.read()
# ----- encrypt -----
salt = secrets.token_bytes(SALT_SIZE)
iv = secrets.token_bytes(IV_SIZE)
key = _derive_key(password, salt)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(iv, plaintext, None) # returns ct || tag
# GCM tag is last TAG_SIZE bytes of the ciphertext
tag = ciphertext[-TAG_SIZE:]
ct_body = ciphertext[:-TAG_SIZE]
# ----- build header + payload -----
header = _make_encryption_header(salt, iv, tag)
payload = header + ct_body
# ----- write to zip -----
zinfo = zipfile.ZipInfo(arcname)
# Preserve original timestamp (optional)
st = full_path.stat()
zinfo.date_time = tuple(time.localtime(st.st_mtime)[:6])
zinfo.compress_type = compression
# Force ZIP stored size to match our payload length
zinfo.file_size = len(payload)
# Store encrypted data as a regular file entry
zf.writestr(zinfo, payload)
The holy grail for most users is media content (images, videos, PDFs). If you open the zip and find a .exe, .scr, or .bat file, delete the archive immediately. Legitimate "legendary" collections of images should only contain .jpg, .png, .gif, or .txt files.
# ntrlegendzip/encrypted.py
import os
import zipfile
import pathlib
import secrets
import struct
from typing import List
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from ._constants import (
MAGIC, VERSION, SALT_SIZE, IV_SIZE, TAG_SIZE,
PBKDF2_ITERATIONS, KEY_SIZE,
)
class NtlzError(RuntimeError): pass
class NtlzBadPassword(NtlzError): pass
class NtlzCorruptHeader(NtlzError): pass
def _derive_key(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=KEY_SIZE,
salt=salt,
iterations=PBKDF2_ITERATIONS,
)
return kdf.derive(password.encode('utf-8'))
def _make_encryption_header(salt: bytes, iv: bytes, tag: bytes) -> bytes:
"""
Layout (total 3+1+16+12+16 = 48 bytes):
MAGIC (3) | VERSION (1) | SALT (16) | IV (12) | TAG (16)
"""
return MAGIC + VERSION + salt + iv + tag
def _parse_encryption_header(data: bytes) -> tuple[bytes, bytes, bytes]:
if len(data) < len(MAGIC) + 1 + SALT_SIZE + IV_SIZE + TAG_SIZE:
raise NtlzCorruptHeader("Header too short")
if not data.startswith(MAGIC):
raise NtlzCorruptHeader("Missing NLZ magic")
# Slice according to the layout defined above
offset = len(MAGIC) + 1
salt = data[offset:offset + SALT_SIZE]
offset += SALT_SIZE
iv = data[offset:offset + IV_SIZE]
offset += IV_SIZE
tag = data[offset:offset + TAG_SIZE]
return salt, iv, tag