diff --git a/CLI.md b/CLI.md index 4db092f..c333978 100644 --- a/CLI.md +++ b/CLI.md @@ -164,7 +164,7 @@ stegasoo generate [OPTIONS] | `--pin/--no-pin` | | flag | `--pin` | Generate a PIN | | `--rsa/--no-rsa` | | flag | `--no-rsa` | Generate an RSA key | | `--pin-length` | | 6-9 | 6 | PIN length in digits | -| `--rsa-bits` | | choice | 2048 | RSA key size (2048, 3072, 4096) | +| `--rsa-bits` | | choice | 2048 | RSA key size (2048, 3072) | | `--words` | | 3-12 | 4 | Words in passphrase | | `--output` | `-o` | path | | Save RSA key to file | | `--password` | `-p` | string | | Password for RSA key file | @@ -180,7 +180,7 @@ stegasoo generate stegasoo generate --words 6 # Generate with RSA key -stegasoo generate --rsa --rsa-bits 4096 +stegasoo generate --rsa --rsa-bits 3072 # Save RSA key to encrypted file stegasoo generate --rsa -o mykey.pem -p "mysecretpassword" diff --git a/WEB_UI.md b/WEB_UI.md index ccfddfa..5d9959d 100644 --- a/WEB_UI.md +++ b/WEB_UI.md @@ -411,7 +411,7 @@ Create a new set of credentials for steganography operations. | Use PIN | on/off | on | Generate a numeric PIN | | PIN length | 6-9 | 6 | Digits in the PIN | | Use RSA Key | on/off | off | Generate an RSA key pair | -| RSA key size | 2048/3072/4096 | 2048 | Key size in bits | +| RSA key size | 2048/3072 | 2048 | Key size in bits | #### Entropy Calculator diff --git a/docs/TEMPLATES.md b/docs/TEMPLATES.md index 8c3f834..c856447 100644 --- a/docs/TEMPLATES.md +++ b/docs/TEMPLATES.md @@ -126,7 +126,7 @@ Quick reference for all Jinja2 templates in `frontends/web/templates/`. - `use_pin` - checkbox - `pin_length` - PIN digits (6-9) - `use_rsa` - checkbox -- `rsa_bits` - key size (2048/3072/4096) +- `rsa_bits` - key size (2048/3072) **Output panels:** - Passphrase display diff --git a/frontends/cli/main.py b/frontends/cli/main.py index 9ebde3b..6a708df 100644 --- a/frontends/cli/main.py +++ b/frontends/cli/main.py @@ -236,7 +236,7 @@ def format_channel_status_line(quiet: bool = False) -> str | None: help=f"PIN length (6-9, default: {DEFAULT_PIN_LENGTH})", ) @click.option( - "--rsa-bits", type=click.Choice(["2048", "3072", "4096"]), default="2048", help="RSA key size" + "--rsa-bits", type=click.Choice(["2048", "3072"]), default="2048", help="RSA key size" ) @click.option( "--words", @@ -261,7 +261,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): Examples: stegasoo generate stegasoo generate --words 5 - stegasoo generate --rsa --rsa-bits 4096 + stegasoo generate --rsa --rsa-bits 3072 stegasoo generate --rsa -o mykey.pem -p "secretpassword" stegasoo generate --no-pin --rsa """ diff --git a/frontends/web/app.py b/frontends/web/app.py index c41cef4..0715fad 100644 --- a/frontends/web/app.py +++ b/frontends/web/app.py @@ -253,6 +253,7 @@ from stegasoo.qr_utils import ( detect_and_crop_qr, extract_key_from_qr, generate_qr_code, + is_compressed, ) # Initialize subprocess wrapper (worker script must be in same directory) @@ -1209,8 +1210,8 @@ def encode_page(): rsa_key_from_qr = False if rsa_key_pem: - # Webcam-scanned PEM key (v4.1.5) - may be compressed - if rsa_key_pem.startswith("STEGASOO-Z:"): + # Webcam-scanned PEM key (v4.1.5+) - may be compressed (zlib or zstd) + if is_compressed(rsa_key_pem): rsa_key_pem = decompress_data(rsa_key_pem) rsa_key_data = rsa_key_pem.encode("utf-8") rsa_key_from_qr = True @@ -1648,8 +1649,8 @@ def decode_page(): rsa_key_from_qr = False if rsa_key_pem: - # Webcam-scanned PEM key (v4.1.5) - may be compressed - if rsa_key_pem.startswith("STEGASOO-Z:"): + # Webcam-scanned PEM key (v4.1.5+) - may be compressed (zlib or zstd) + if is_compressed(rsa_key_pem): rsa_key_pem = decompress_data(rsa_key_pem) rsa_key_data = rsa_key_pem.encode("utf-8") rsa_key_from_qr = True diff --git a/frontends/web/templates/about.html b/frontends/web/templates/about.html index 7db8d58..95a413c 100644 --- a/frontends/web/templates/about.html +++ b/frontends/web/templates/about.html @@ -573,7 +573,7 @@ RSA keys - 2048, 3072, 4096 bit + 2048, 3072 bit Passphrase diff --git a/frontends/web/templates/generate.html b/frontends/web/templates/generate.html index 9a4659b..5a26b94 100644 --- a/frontends/web/templates/generate.html +++ b/frontends/web/templates/generate.html @@ -65,11 +65,7 @@ -
- QR code unavailable for keys >3072 bits -
@@ -286,12 +282,6 @@ Security note: The QR code contains your unencrypted private key. Only scan in a secure environment. Consider using the password-protected download instead. - {% if rsa_bits >= 4096 %} -

- - 4096-bit keys produce very dense QR codes. If scanning fails, - use the PEM text or download options instead. - {% endif %} diff --git a/pyproject.toml b/pyproject.toml index ba772ce..ad50405 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ cli = [ ] compression = [ "lz4>=4.0.0", + "zstandard>=0.22.0", ] web = [ "flask>=3.0.0", @@ -65,6 +66,7 @@ web = [ "qrcode>=7.3.0", "pyzbar>=0.1.9", "piexif>=1.1.0", + "zstandard>=0.22.0", # v4.2.0: Better compression for QR keys # Include DCT support for web UI "numpy>=2.0.0", "scipy>=1.10.0", @@ -77,6 +79,7 @@ api = [ "python-multipart>=0.0.6", "qrcode>=7.30", "pyzbar>=0.1.9", + "zstandard>=0.22.0", # v4.2.0: Better compression for QR keys # Include DCT support for API "numpy>=2.0.0", "scipy>=1.10.0", diff --git a/src/stegasoo/compression.py b/src/stegasoo/compression.py index 46d8979..50f5f4c 100644 --- a/src/stegasoo/compression.py +++ b/src/stegasoo/compression.py @@ -17,6 +17,14 @@ try: except ImportError: HAS_LZ4 = False +# Optional ZSTD support (best ratio, fast) +try: + import zstandard as zstd + + HAS_ZSTD = True +except ImportError: + HAS_ZSTD = False + class CompressionAlgorithm(IntEnum): """Supported compression algorithms.""" @@ -24,6 +32,7 @@ class CompressionAlgorithm(IntEnum): NONE = 0 ZLIB = 1 LZ4 = 2 + ZSTD = 3 # v4.2.0: Best ratio, fast compression # Magic bytes for compressed payloads @@ -72,6 +81,15 @@ def compress(data: bytes, algorithm: CompressionAlgorithm = CompressionAlgorithm algorithm = CompressionAlgorithm.ZLIB else: compressed = lz4.frame.compress(data) + + elif algorithm == CompressionAlgorithm.ZSTD: + if not HAS_ZSTD: + # Fall back to zlib if ZSTD not available + compressed = zlib.compress(data, level=ZLIB_LEVEL) + algorithm = CompressionAlgorithm.ZLIB + else: + cctx = zstd.ZstdCompressor(level=19) # High compression level + compressed = cctx.compress(data) else: raise CompressionError(f"Unknown compression algorithm: {algorithm}") @@ -123,6 +141,15 @@ def decompress(data: bytes) -> bytes: result = lz4.frame.decompress(compressed_data) except Exception as e: raise CompressionError(f"LZ4 decompression failed: {e}") + + elif algorithm == CompressionAlgorithm.ZSTD: + if not HAS_ZSTD: + raise CompressionError("ZSTD compression used but zstandard package not installed") + try: + dctx = zstd.ZstdDecompressor() + result = dctx.decompress(compressed_data) + except Exception as e: + raise CompressionError(f"ZSTD decompression failed: {e}") else: raise CompressionError(f"Unknown compression algorithm: {algorithm}") @@ -181,6 +208,9 @@ def estimate_compressed_size( compressed_sample = zlib.compress(sample, level=ZLIB_LEVEL) elif algorithm == CompressionAlgorithm.LZ4 and HAS_LZ4: compressed_sample = lz4.frame.compress(sample) + elif algorithm == CompressionAlgorithm.ZSTD and HAS_ZSTD: + cctx = zstd.ZstdCompressor(level=19) + compressed_sample = cctx.compress(sample) else: compressed_sample = zlib.compress(sample, level=ZLIB_LEVEL) @@ -195,14 +225,24 @@ def get_available_algorithms() -> list[CompressionAlgorithm]: algorithms = [CompressionAlgorithm.NONE, CompressionAlgorithm.ZLIB] if HAS_LZ4: algorithms.append(CompressionAlgorithm.LZ4) + if HAS_ZSTD: + algorithms.append(CompressionAlgorithm.ZSTD) return algorithms +def get_best_algorithm() -> CompressionAlgorithm: + """Get the best available compression algorithm (prefer ZSTD > ZLIB > LZ4).""" + if HAS_ZSTD: + return CompressionAlgorithm.ZSTD + return CompressionAlgorithm.ZLIB + + def algorithm_name(algo: CompressionAlgorithm) -> str: """Get human-readable algorithm name.""" names = { CompressionAlgorithm.NONE: "None", CompressionAlgorithm.ZLIB: "Zlib (deflate)", CompressionAlgorithm.LZ4: "LZ4 (fast)", + CompressionAlgorithm.ZSTD: "Zstd (best)", } return names.get(algo, "Unknown") diff --git a/src/stegasoo/constants.py b/src/stegasoo/constants.py index 38b419d..e0ff3bf 100644 --- a/src/stegasoo/constants.py +++ b/src/stegasoo/constants.py @@ -1,9 +1,15 @@ """ -Stegasoo Constants and Configuration (v4.0.2 - Web UI Authentication) +Stegasoo Constants and Configuration (v4.2.0 - Performance & Compression) Central location for all magic numbers, limits, and crypto parameters. All version numbers, limits, and configuration values should be defined here. +CHANGES in v4.2.0: +- Added zstd compression for QR codes (better ratio than zlib) +- RSA key size capped at 3072 bits (4096 too large for QR codes) +- Progress bar improvements for encode/decode operations +- File auto-expire increased to 10 minutes + CHANGES in v4.0.2: - Added Web UI authentication with SQLite3 user storage - Added optional HTTPS with auto-generated self-signed certificates @@ -25,7 +31,7 @@ from pathlib import Path # VERSION # ============================================================================ -__version__ = "4.1.5" +__version__ = "4.2.0" # ============================================================================ # FILE FORMAT @@ -98,7 +104,7 @@ DEFAULT_PHRASE_WORDS = DEFAULT_PASSPHRASE_WORDS # RSA configuration MIN_RSA_BITS = 2048 -VALID_RSA_SIZES = (2048, 3072, 4096) +VALID_RSA_SIZES = (2048, 3072) # 4096 removed - too large for QR codes DEFAULT_RSA_BITS = 2048 MIN_KEY_PASSWORD_LENGTH = 8 diff --git a/src/stegasoo/generate.py b/src/stegasoo/generate.py index 947edcc..3c91f66 100644 --- a/src/stegasoo/generate.py +++ b/src/stegasoo/generate.py @@ -82,7 +82,7 @@ def generate_rsa_key(bits: int = DEFAULT_RSA_BITS, password: str | None = None) Generate an RSA private key in PEM format. Args: - bits: Key size (2048, 3072, or 4096, default 2048) + bits: Key size (2048 or 3072, default 2048) password: Optional password to encrypt the key Returns: diff --git a/src/stegasoo/keygen.py b/src/stegasoo/keygen.py index ba8d09a..d959143 100644 --- a/src/stegasoo/keygen.py +++ b/src/stegasoo/keygen.py @@ -136,7 +136,7 @@ def generate_rsa_key(bits: int = DEFAULT_RSA_BITS) -> rsa.RSAPrivateKey: Generate an RSA private key. Args: - bits: Key size (2048, 3072, or 4096) + bits: Key size (2048 or 3072) Returns: RSA private key object diff --git a/src/stegasoo/qr_utils.py b/src/stegasoo/qr_utils.py index c5f69cc..9ef06d3 100644 --- a/src/stegasoo/qr_utils.py +++ b/src/stegasoo/qr_utils.py @@ -8,6 +8,7 @@ IMPROVEMENTS IN THIS VERSION: - Much more robust PEM normalization - Better handling of QR code extraction edge cases - Improved error messages +- v4.2.0: Added zstd compression (better ratio than zlib) """ import base64 @@ -16,6 +17,14 @@ import zlib from PIL import Image +# Optional ZSTD support (better compression ratio) +try: + import zstandard as zstd + + HAS_ZSTD = True +except ImportError: + HAS_ZSTD = False + # QR code generation try: import qrcode @@ -42,30 +51,46 @@ from .constants import ( ) # Constants -COMPRESSION_PREFIX = "STEGASOO-Z:" +COMPRESSION_PREFIX_ZLIB = "STEGASOO-Z:" # Legacy zlib compression +COMPRESSION_PREFIX_ZSTD = "STEGASOO-ZS:" # v4.2.0: New zstd compression (better ratio) +COMPRESSION_PREFIX = COMPRESSION_PREFIX_ZSTD if HAS_ZSTD else COMPRESSION_PREFIX_ZLIB def compress_data(data: str) -> str: """ Compress string data for QR code storage. + Uses zstd if available (better ratio), falls back to zlib. + Args: data: String to compress Returns: - Compressed string with STEGASOO-Z: prefix + Compressed string with STEGASOO-ZS: (zstd) or STEGASOO-Z: (zlib) prefix """ - compressed = zlib.compress(data.encode("utf-8"), level=9) - encoded = base64.b64encode(compressed).decode("ascii") - return COMPRESSION_PREFIX + encoded + data_bytes = data.encode("utf-8") + + if HAS_ZSTD: + # Use zstd (better compression ratio) + cctx = zstd.ZstdCompressor(level=19) + compressed = cctx.compress(data_bytes) + encoded = base64.b64encode(compressed).decode("ascii") + return COMPRESSION_PREFIX_ZSTD + encoded + else: + # Fall back to zlib + compressed = zlib.compress(data_bytes, level=9) + encoded = base64.b64encode(compressed).decode("ascii") + return COMPRESSION_PREFIX_ZLIB + encoded def decompress_data(data: str) -> str: """ Decompress data from QR code. + Supports both zstd (STEGASOO-ZS:) and zlib (STEGASOO-Z:) formats. + Args: - data: Compressed string with STEGASOO-Z: prefix + data: Compressed string with STEGASOO-ZS: or STEGASOO-Z: prefix Returns: Original uncompressed string @@ -73,12 +98,26 @@ def decompress_data(data: str) -> str: Raises: ValueError: If data is not valid compressed format """ - if not data.startswith(COMPRESSION_PREFIX): - raise ValueError("Data is not in compressed format") + if data.startswith(COMPRESSION_PREFIX_ZSTD): + # v4.2.0: ZSTD compression + if not HAS_ZSTD: + raise ValueError( + "Data compressed with zstd but zstandard package not installed. " + "Run: pip install zstandard" + ) + encoded = data[len(COMPRESSION_PREFIX_ZSTD):] + compressed = base64.b64decode(encoded) + dctx = zstd.ZstdDecompressor() + return dctx.decompress(compressed).decode("utf-8") - encoded = data[len(COMPRESSION_PREFIX) :] - compressed = base64.b64decode(encoded) - return zlib.decompress(compressed).decode("utf-8") + elif data.startswith(COMPRESSION_PREFIX_ZLIB): + # Legacy zlib compression + encoded = data[len(COMPRESSION_PREFIX_ZLIB):] + compressed = base64.b64decode(encoded) + return zlib.decompress(compressed).decode("utf-8") + + else: + raise ValueError("Data is not in compressed format") def normalize_pem(pem_data: str) -> str: @@ -166,8 +205,8 @@ def normalize_pem(pem_data: str) -> str: def is_compressed(data: str) -> bool: - """Check if data has compression prefix.""" - return data.startswith(COMPRESSION_PREFIX) + """Check if data has compression prefix (zstd or zlib).""" + return data.startswith(COMPRESSION_PREFIX_ZSTD) or data.startswith(COMPRESSION_PREFIX_ZLIB) def auto_decompress(data: str) -> str: