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 |
|
| `--pin/--no-pin` | | flag | `--pin` | Generate a PIN |
|
||||||
| `--rsa/--no-rsa` | | flag | `--no-rsa` | Generate an RSA key |
|
| `--rsa/--no-rsa` | | flag | `--no-rsa` | Generate an RSA key |
|
||||||
| `--pin-length` | | 6-9 | 6 | PIN length in digits |
|
| `--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 |
|
| `--words` | | 3-12 | 4 | Words in passphrase |
|
||||||
| `--output` | `-o` | path | | Save RSA key to file |
|
| `--output` | `-o` | path | | Save RSA key to file |
|
||||||
| `--password` | `-p` | string | | Password for RSA key file |
|
| `--password` | `-p` | string | | Password for RSA key file |
|
||||||
@@ -180,7 +180,7 @@ stegasoo generate
|
|||||||
stegasoo generate --words 6
|
stegasoo generate --words 6
|
||||||
|
|
||||||
# Generate with RSA key
|
# Generate with RSA key
|
||||||
stegasoo generate --rsa --rsa-bits 4096
|
stegasoo generate --rsa --rsa-bits 3072
|
||||||
|
|
||||||
# Save RSA key to encrypted file
|
# Save RSA key to encrypted file
|
||||||
stegasoo generate --rsa -o mykey.pem -p "mysecretpassword"
|
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 |
|
| Use PIN | on/off | on | Generate a numeric PIN |
|
||||||
| PIN length | 6-9 | 6 | Digits in the PIN |
|
| PIN length | 6-9 | 6 | Digits in the PIN |
|
||||||
| Use RSA Key | on/off | off | Generate an RSA key pair |
|
| 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
|
#### Entropy Calculator
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ Quick reference for all Jinja2 templates in `frontends/web/templates/`.
|
|||||||
- `use_pin` - checkbox
|
- `use_pin` - checkbox
|
||||||
- `pin_length` - PIN digits (6-9)
|
- `pin_length` - PIN digits (6-9)
|
||||||
- `use_rsa` - checkbox
|
- `use_rsa` - checkbox
|
||||||
- `rsa_bits` - key size (2048/3072/4096)
|
- `rsa_bits` - key size (2048/3072)
|
||||||
|
|
||||||
**Output panels:**
|
**Output panels:**
|
||||||
- Passphrase display
|
- 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})",
|
help=f"PIN length (6-9, default: {DEFAULT_PIN_LENGTH})",
|
||||||
)
|
)
|
||||||
@click.option(
|
@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(
|
@click.option(
|
||||||
"--words",
|
"--words",
|
||||||
@@ -261,7 +261,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
|||||||
Examples:
|
Examples:
|
||||||
stegasoo generate
|
stegasoo generate
|
||||||
stegasoo generate --words 5
|
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 --rsa -o mykey.pem -p "secretpassword"
|
||||||
stegasoo generate --no-pin --rsa
|
stegasoo generate --no-pin --rsa
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ from stegasoo.qr_utils import (
|
|||||||
detect_and_crop_qr,
|
detect_and_crop_qr,
|
||||||
extract_key_from_qr,
|
extract_key_from_qr,
|
||||||
generate_qr_code,
|
generate_qr_code,
|
||||||
|
is_compressed,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize subprocess wrapper (worker script must be in same directory)
|
# Initialize subprocess wrapper (worker script must be in same directory)
|
||||||
@@ -1209,8 +1210,8 @@ def encode_page():
|
|||||||
rsa_key_from_qr = False
|
rsa_key_from_qr = False
|
||||||
|
|
||||||
if rsa_key_pem:
|
if rsa_key_pem:
|
||||||
# Webcam-scanned PEM key (v4.1.5) - may be compressed
|
# Webcam-scanned PEM key (v4.1.5+) - may be compressed (zlib or zstd)
|
||||||
if rsa_key_pem.startswith("STEGASOO-Z:"):
|
if is_compressed(rsa_key_pem):
|
||||||
rsa_key_pem = decompress_data(rsa_key_pem)
|
rsa_key_pem = decompress_data(rsa_key_pem)
|
||||||
rsa_key_data = rsa_key_pem.encode("utf-8")
|
rsa_key_data = rsa_key_pem.encode("utf-8")
|
||||||
rsa_key_from_qr = True
|
rsa_key_from_qr = True
|
||||||
@@ -1648,8 +1649,8 @@ def decode_page():
|
|||||||
rsa_key_from_qr = False
|
rsa_key_from_qr = False
|
||||||
|
|
||||||
if rsa_key_pem:
|
if rsa_key_pem:
|
||||||
# Webcam-scanned PEM key (v4.1.5) - may be compressed
|
# Webcam-scanned PEM key (v4.1.5+) - may be compressed (zlib or zstd)
|
||||||
if rsa_key_pem.startswith("STEGASOO-Z:"):
|
if is_compressed(rsa_key_pem):
|
||||||
rsa_key_pem = decompress_data(rsa_key_pem)
|
rsa_key_pem = decompress_data(rsa_key_pem)
|
||||||
rsa_key_data = rsa_key_pem.encode("utf-8")
|
rsa_key_data = rsa_key_pem.encode("utf-8")
|
||||||
rsa_key_from_qr = True
|
rsa_key_from_qr = True
|
||||||
|
|||||||
@@ -573,7 +573,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><i class="bi bi-shield me-2"></i>RSA keys</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><i class="bi bi-chat-quote me-2"></i>Passphrase</td>
|
<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">
|
<select name="rsa_bits" class="form-select form-select-sm" id="rsaBitsSelect">
|
||||||
<option value="2048" selected>2048 bits (~128 bits entropy)</option>
|
<option value="2048" selected>2048 bits (~128 bits entropy)</option>
|
||||||
<option value="3072">3072 bits (~128 bits entropy)</option>
|
<option value="3072">3072 bits (~128 bits entropy)</option>
|
||||||
<option value="4096">4096 bits (~128 bits entropy)</option>
|
|
||||||
</select>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -286,12 +282,6 @@
|
|||||||
<i class="bi bi-shield-exclamation me-1"></i>
|
<i class="bi bi-shield-exclamation me-1"></i>
|
||||||
<strong>Security note:</strong> The QR code contains your unencrypted private key.
|
<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.
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ cli = [
|
|||||||
]
|
]
|
||||||
compression = [
|
compression = [
|
||||||
"lz4>=4.0.0",
|
"lz4>=4.0.0",
|
||||||
|
"zstandard>=0.22.0",
|
||||||
]
|
]
|
||||||
web = [
|
web = [
|
||||||
"flask>=3.0.0",
|
"flask>=3.0.0",
|
||||||
@@ -65,6 +66,7 @@ web = [
|
|||||||
"qrcode>=7.3.0",
|
"qrcode>=7.3.0",
|
||||||
"pyzbar>=0.1.9",
|
"pyzbar>=0.1.9",
|
||||||
"piexif>=1.1.0",
|
"piexif>=1.1.0",
|
||||||
|
"zstandard>=0.22.0", # v4.2.0: Better compression for QR keys
|
||||||
# Include DCT support for web UI
|
# Include DCT support for web UI
|
||||||
"numpy>=2.0.0",
|
"numpy>=2.0.0",
|
||||||
"scipy>=1.10.0",
|
"scipy>=1.10.0",
|
||||||
@@ -77,6 +79,7 @@ api = [
|
|||||||
"python-multipart>=0.0.6",
|
"python-multipart>=0.0.6",
|
||||||
"qrcode>=7.30",
|
"qrcode>=7.30",
|
||||||
"pyzbar>=0.1.9",
|
"pyzbar>=0.1.9",
|
||||||
|
"zstandard>=0.22.0", # v4.2.0: Better compression for QR keys
|
||||||
# Include DCT support for API
|
# Include DCT support for API
|
||||||
"numpy>=2.0.0",
|
"numpy>=2.0.0",
|
||||||
"scipy>=1.10.0",
|
"scipy>=1.10.0",
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_LZ4 = False
|
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):
|
class CompressionAlgorithm(IntEnum):
|
||||||
"""Supported compression algorithms."""
|
"""Supported compression algorithms."""
|
||||||
@@ -24,6 +32,7 @@ class CompressionAlgorithm(IntEnum):
|
|||||||
NONE = 0
|
NONE = 0
|
||||||
ZLIB = 1
|
ZLIB = 1
|
||||||
LZ4 = 2
|
LZ4 = 2
|
||||||
|
ZSTD = 3 # v4.2.0: Best ratio, fast compression
|
||||||
|
|
||||||
|
|
||||||
# Magic bytes for compressed payloads
|
# Magic bytes for compressed payloads
|
||||||
@@ -72,6 +81,15 @@ def compress(data: bytes, algorithm: CompressionAlgorithm = CompressionAlgorithm
|
|||||||
algorithm = CompressionAlgorithm.ZLIB
|
algorithm = CompressionAlgorithm.ZLIB
|
||||||
else:
|
else:
|
||||||
compressed = lz4.frame.compress(data)
|
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:
|
else:
|
||||||
raise CompressionError(f"Unknown compression algorithm: {algorithm}")
|
raise CompressionError(f"Unknown compression algorithm: {algorithm}")
|
||||||
|
|
||||||
@@ -123,6 +141,15 @@ def decompress(data: bytes) -> bytes:
|
|||||||
result = lz4.frame.decompress(compressed_data)
|
result = lz4.frame.decompress(compressed_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise CompressionError(f"LZ4 decompression failed: {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:
|
else:
|
||||||
raise CompressionError(f"Unknown compression algorithm: {algorithm}")
|
raise CompressionError(f"Unknown compression algorithm: {algorithm}")
|
||||||
|
|
||||||
@@ -181,6 +208,9 @@ def estimate_compressed_size(
|
|||||||
compressed_sample = zlib.compress(sample, level=ZLIB_LEVEL)
|
compressed_sample = zlib.compress(sample, level=ZLIB_LEVEL)
|
||||||
elif algorithm == CompressionAlgorithm.LZ4 and HAS_LZ4:
|
elif algorithm == CompressionAlgorithm.LZ4 and HAS_LZ4:
|
||||||
compressed_sample = lz4.frame.compress(sample)
|
compressed_sample = lz4.frame.compress(sample)
|
||||||
|
elif algorithm == CompressionAlgorithm.ZSTD and HAS_ZSTD:
|
||||||
|
cctx = zstd.ZstdCompressor(level=19)
|
||||||
|
compressed_sample = cctx.compress(sample)
|
||||||
else:
|
else:
|
||||||
compressed_sample = zlib.compress(sample, level=ZLIB_LEVEL)
|
compressed_sample = zlib.compress(sample, level=ZLIB_LEVEL)
|
||||||
|
|
||||||
@@ -195,14 +225,24 @@ def get_available_algorithms() -> list[CompressionAlgorithm]:
|
|||||||
algorithms = [CompressionAlgorithm.NONE, CompressionAlgorithm.ZLIB]
|
algorithms = [CompressionAlgorithm.NONE, CompressionAlgorithm.ZLIB]
|
||||||
if HAS_LZ4:
|
if HAS_LZ4:
|
||||||
algorithms.append(CompressionAlgorithm.LZ4)
|
algorithms.append(CompressionAlgorithm.LZ4)
|
||||||
|
if HAS_ZSTD:
|
||||||
|
algorithms.append(CompressionAlgorithm.ZSTD)
|
||||||
return algorithms
|
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:
|
def algorithm_name(algo: CompressionAlgorithm) -> str:
|
||||||
"""Get human-readable algorithm name."""
|
"""Get human-readable algorithm name."""
|
||||||
names = {
|
names = {
|
||||||
CompressionAlgorithm.NONE: "None",
|
CompressionAlgorithm.NONE: "None",
|
||||||
CompressionAlgorithm.ZLIB: "Zlib (deflate)",
|
CompressionAlgorithm.ZLIB: "Zlib (deflate)",
|
||||||
CompressionAlgorithm.LZ4: "LZ4 (fast)",
|
CompressionAlgorithm.LZ4: "LZ4 (fast)",
|
||||||
|
CompressionAlgorithm.ZSTD: "Zstd (best)",
|
||||||
}
|
}
|
||||||
return names.get(algo, "Unknown")
|
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.
|
Central location for all magic numbers, limits, and crypto parameters.
|
||||||
All version numbers, limits, and configuration values should be defined here.
|
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:
|
CHANGES in v4.0.2:
|
||||||
- Added Web UI authentication with SQLite3 user storage
|
- Added Web UI authentication with SQLite3 user storage
|
||||||
- Added optional HTTPS with auto-generated self-signed certificates
|
- Added optional HTTPS with auto-generated self-signed certificates
|
||||||
@@ -25,7 +31,7 @@ from pathlib import Path
|
|||||||
# VERSION
|
# VERSION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
__version__ = "4.1.5"
|
__version__ = "4.2.0"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# FILE FORMAT
|
# FILE FORMAT
|
||||||
@@ -98,7 +104,7 @@ DEFAULT_PHRASE_WORDS = DEFAULT_PASSPHRASE_WORDS
|
|||||||
|
|
||||||
# RSA configuration
|
# RSA configuration
|
||||||
MIN_RSA_BITS = 2048
|
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
|
DEFAULT_RSA_BITS = 2048
|
||||||
|
|
||||||
MIN_KEY_PASSWORD_LENGTH = 8
|
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.
|
Generate an RSA private key in PEM format.
|
||||||
|
|
||||||
Args:
|
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
|
password: Optional password to encrypt the key
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ def generate_rsa_key(bits: int = DEFAULT_RSA_BITS) -> rsa.RSAPrivateKey:
|
|||||||
Generate an RSA private key.
|
Generate an RSA private key.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
bits: Key size (2048, 3072, or 4096)
|
bits: Key size (2048 or 3072)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
RSA private key object
|
RSA private key object
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ IMPROVEMENTS IN THIS VERSION:
|
|||||||
- Much more robust PEM normalization
|
- Much more robust PEM normalization
|
||||||
- Better handling of QR code extraction edge cases
|
- Better handling of QR code extraction edge cases
|
||||||
- Improved error messages
|
- Improved error messages
|
||||||
|
- v4.2.0: Added zstd compression (better ratio than zlib)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
@@ -16,6 +17,14 @@ import zlib
|
|||||||
|
|
||||||
from PIL import Image
|
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
|
# QR code generation
|
||||||
try:
|
try:
|
||||||
import qrcode
|
import qrcode
|
||||||
@@ -42,30 +51,46 @@ from .constants import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Constants
|
# 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:
|
def compress_data(data: str) -> str:
|
||||||
"""
|
"""
|
||||||
Compress string data for QR code storage.
|
Compress string data for QR code storage.
|
||||||
|
|
||||||
|
Uses zstd if available (better ratio), falls back to zlib.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: String to compress
|
data: String to compress
|
||||||
|
|
||||||
Returns:
|
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)
|
data_bytes = data.encode("utf-8")
|
||||||
encoded = base64.b64encode(compressed).decode("ascii")
|
|
||||||
return COMPRESSION_PREFIX + encoded
|
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:
|
def decompress_data(data: str) -> str:
|
||||||
"""
|
"""
|
||||||
Decompress data from QR code.
|
Decompress data from QR code.
|
||||||
|
|
||||||
|
Supports both zstd (STEGASOO-ZS:) and zlib (STEGASOO-Z:) formats.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Compressed string with STEGASOO-Z: prefix
|
data: Compressed string with STEGASOO-ZS: or STEGASOO-Z: prefix
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Original uncompressed string
|
Original uncompressed string
|
||||||
@@ -73,12 +98,26 @@ def decompress_data(data: str) -> str:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If data is not valid compressed format
|
ValueError: If data is not valid compressed format
|
||||||
"""
|
"""
|
||||||
if not data.startswith(COMPRESSION_PREFIX):
|
if data.startswith(COMPRESSION_PREFIX_ZSTD):
|
||||||
raise ValueError("Data is not in compressed format")
|
# 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) :]
|
elif data.startswith(COMPRESSION_PREFIX_ZLIB):
|
||||||
compressed = base64.b64decode(encoded)
|
# Legacy zlib compression
|
||||||
return zlib.decompress(compressed).decode("utf-8")
|
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:
|
def normalize_pem(pem_data: str) -> str:
|
||||||
@@ -166,8 +205,8 @@ def normalize_pem(pem_data: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def is_compressed(data: str) -> bool:
|
def is_compressed(data: str) -> bool:
|
||||||
"""Check if data has compression prefix."""
|
"""Check if data has compression prefix (zstd or zlib)."""
|
||||||
return data.startswith(COMPRESSION_PREFIX)
|
return data.startswith(COMPRESSION_PREFIX_ZSTD) or data.startswith(COMPRESSION_PREFIX_ZLIB)
|
||||||
|
|
||||||
|
|
||||||
def auto_decompress(data: str) -> str:
|
def auto_decompress(data: str) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user