Cap RSA at 3072 bits, add zstd compression for QR codes
- RSA key size capped at 3072 bits (4096 too large for QR codes) - Added zstd compression for QR code RSA keys (better ratio than zlib) - New prefix STEGASOO-ZS: for zstd, backward compatible with STEGASOO-Z: (zlib) - Added zstandard dependency to web/api/compression extras - Updated all docs, CLI options, and web UI to reflect 3072 max - Version bump to 4.2.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
CLI.md
4
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -573,7 +573,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-shield me-2"></i>RSA keys</td>
|
||||
<td><strong>2048, 3072, 4096 bit</strong></td>
|
||||
<td><strong>2048, 3072 bit</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-chat-quote me-2"></i>Passphrase</td>
|
||||
|
||||
@@ -65,11 +65,7 @@
|
||||
<select name="rsa_bits" class="form-select form-select-sm" id="rsaBitsSelect">
|
||||
<option value="2048" selected>2048 bits (~128 bits entropy)</option>
|
||||
<option value="3072">3072 bits (~128 bits entropy)</option>
|
||||
<option value="4096">4096 bits (~128 bits entropy)</option>
|
||||
</select>
|
||||
<div class="form-text text-warning d-none" id="rsaQrWarning">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>QR code unavailable for keys >3072 bits
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -286,12 +282,6 @@
|
||||
<i class="bi bi-shield-exclamation me-1"></i>
|
||||
<strong>Security note:</strong> 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 %}
|
||||
<br><br>
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
<strong>4096-bit keys</strong> produce very dense QR codes. If scanning fails,
|
||||
use the PEM text or download options instead.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user