@@ -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: