Lint cleanup: ruff fixes across entire codebase
- Strip trailing whitespace from all Python files - Fix import sorting (I001) across all modules - Convert Optional[X] to X | None syntax (UP045) - Remove unused imports (F401) - Convert lambda assignments to def functions (E731) - Add TYPE_CHECKING import for forward references - Update pyproject.toml ruff config: - Move select/ignore to [tool.ruff.lint] section - Add per-file ignores for DCT colorspace naming (N803/N806) - Add per-file ignores for __init__.py import structure (E402) - Exclude defunct test_routes.py - Remove frontends/web/test_routes.py (defunct debug snippet) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,53 +21,47 @@ NEW in v3.0: LSB and DCT embedding modes.
|
|||||||
NEW in v3.0.1: DCT color mode and JPEG output format.
|
NEW in v3.0.1: DCT color mode and JPEG output format.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
|
||||||
import sys
|
|
||||||
import base64
|
import base64
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Literal
|
from typing import Literal
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Query
|
from fastapi import FastAPI, File, Form, HTTPException, Query, UploadFile
|
||||||
from fastapi.responses import Response, JSONResponse
|
from fastapi.responses import JSONResponse, Response
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
# Add parent to path for development
|
# Add parent to path for development
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
||||||
|
|
||||||
import stegasoo
|
|
||||||
from stegasoo import (
|
from stegasoo import (
|
||||||
encode, decode, generate_credentials,
|
|
||||||
validate_image,
|
|
||||||
__version__,
|
|
||||||
StegasooError, DecryptionError, CapacityError,
|
|
||||||
has_argon2,
|
|
||||||
FilePayload,
|
|
||||||
MAX_FILE_PAYLOAD_SIZE,
|
MAX_FILE_PAYLOAD_SIZE,
|
||||||
# Embedding modes
|
CapacityError,
|
||||||
EMBED_MODE_LSB,
|
DecryptionError,
|
||||||
EMBED_MODE_DCT,
|
FilePayload,
|
||||||
EMBED_MODE_AUTO,
|
StegasooError,
|
||||||
has_dct_support,
|
__version__,
|
||||||
compare_modes,
|
|
||||||
will_fit_by_mode,
|
|
||||||
calculate_capacity_by_mode,
|
calculate_capacity_by_mode,
|
||||||
# Channel key functions (v4.0.0)
|
|
||||||
generate_channel_key,
|
|
||||||
get_channel_key,
|
|
||||||
set_channel_key,
|
|
||||||
clear_channel_key,
|
clear_channel_key,
|
||||||
has_channel_key,
|
compare_modes,
|
||||||
|
decode,
|
||||||
|
encode,
|
||||||
|
generate_channel_key,
|
||||||
|
generate_credentials,
|
||||||
get_channel_status,
|
get_channel_status,
|
||||||
|
has_argon2,
|
||||||
|
has_channel_key,
|
||||||
|
has_dct_support,
|
||||||
|
set_channel_key,
|
||||||
validate_channel_key,
|
validate_channel_key,
|
||||||
format_channel_key,
|
validate_image,
|
||||||
get_active_channel_key,
|
will_fit_by_mode,
|
||||||
get_channel_fingerprint,
|
|
||||||
)
|
)
|
||||||
from stegasoo.constants import (
|
from stegasoo.constants import (
|
||||||
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
|
||||||
MIN_PASSPHRASE_WORDS, MAX_PASSPHRASE_WORDS,
|
|
||||||
DEFAULT_PASSPHRASE_WORDS,
|
DEFAULT_PASSPHRASE_WORDS,
|
||||||
|
MAX_PASSPHRASE_WORDS,
|
||||||
|
MAX_PIN_LENGTH,
|
||||||
|
MIN_PASSPHRASE_WORDS,
|
||||||
|
MIN_PIN_LENGTH,
|
||||||
VALID_RSA_SIZES,
|
VALID_RSA_SIZES,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -151,11 +145,11 @@ class GenerateRequest(BaseModel):
|
|||||||
|
|
||||||
class GenerateResponse(BaseModel):
|
class GenerateResponse(BaseModel):
|
||||||
passphrase: str = Field(description="Single passphrase (v3.2.0: no daily rotation)")
|
passphrase: str = Field(description="Single passphrase (v3.2.0: no daily rotation)")
|
||||||
pin: Optional[str] = None
|
pin: str | None = None
|
||||||
rsa_key_pem: Optional[str] = None
|
rsa_key_pem: str | None = None
|
||||||
entropy: dict[str, int]
|
entropy: dict[str, int]
|
||||||
# Legacy field for compatibility
|
# Legacy field for compatibility
|
||||||
phrases: Optional[dict[str, str]] = Field(
|
phrases: dict[str, str] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Deprecated: Use 'passphrase' instead"
|
description="Deprecated: Use 'passphrase' instead"
|
||||||
)
|
)
|
||||||
@@ -167,10 +161,10 @@ class EncodeRequest(BaseModel):
|
|||||||
carrier_image_base64: str
|
carrier_image_base64: str
|
||||||
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
|
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
|
||||||
pin: str = ""
|
pin: str = ""
|
||||||
rsa_key_base64: Optional[str] = None
|
rsa_key_base64: str | None = None
|
||||||
rsa_password: Optional[str] = None
|
rsa_password: str | None = None
|
||||||
# Channel key (v4.0.0)
|
# Channel key (v4.0.0)
|
||||||
channel_key: Optional[str] = Field(
|
channel_key: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Channel key for deployment isolation. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
|
description="Channel key for deployment isolation. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
|
||||||
)
|
)
|
||||||
@@ -192,15 +186,15 @@ class EncodeFileRequest(BaseModel):
|
|||||||
"""Request for embedding a file (base64-encoded)."""
|
"""Request for embedding a file (base64-encoded)."""
|
||||||
file_data_base64: str
|
file_data_base64: str
|
||||||
filename: str
|
filename: str
|
||||||
mime_type: Optional[str] = None
|
mime_type: str | None = None
|
||||||
reference_photo_base64: str
|
reference_photo_base64: str
|
||||||
carrier_image_base64: str
|
carrier_image_base64: str
|
||||||
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
|
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
|
||||||
pin: str = ""
|
pin: str = ""
|
||||||
rsa_key_base64: Optional[str] = None
|
rsa_key_base64: str | None = None
|
||||||
rsa_password: Optional[str] = None
|
rsa_password: str | None = None
|
||||||
# Channel key (v4.0.0)
|
# Channel key (v4.0.0)
|
||||||
channel_key: Optional[str] = Field(
|
channel_key: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Channel key for deployment isolation. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
|
description="Channel key for deployment isolation. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
|
||||||
)
|
)
|
||||||
@@ -236,16 +230,16 @@ class EncodeResponse(BaseModel):
|
|||||||
default="public",
|
default="public",
|
||||||
description="Channel mode: 'public' or 'private'"
|
description="Channel mode: 'public' or 'private'"
|
||||||
)
|
)
|
||||||
channel_fingerprint: Optional[str] = Field(
|
channel_fingerprint: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Channel key fingerprint (if private mode)"
|
description="Channel key fingerprint (if private mode)"
|
||||||
)
|
)
|
||||||
# Legacy fields (v3.2.0: no longer used in crypto)
|
# Legacy fields (v3.2.0: no longer used in crypto)
|
||||||
date_used: Optional[str] = Field(
|
date_used: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Deprecated: Date no longer used in v3.2.0"
|
description="Deprecated: Date no longer used in v3.2.0"
|
||||||
)
|
)
|
||||||
day_of_week: Optional[str] = Field(
|
day_of_week: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Deprecated: Date no longer used in v3.2.0"
|
description="Deprecated: Date no longer used in v3.2.0"
|
||||||
)
|
)
|
||||||
@@ -256,10 +250,10 @@ class DecodeRequest(BaseModel):
|
|||||||
reference_photo_base64: str
|
reference_photo_base64: str
|
||||||
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
|
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
|
||||||
pin: str = ""
|
pin: str = ""
|
||||||
rsa_key_base64: Optional[str] = None
|
rsa_key_base64: str | None = None
|
||||||
rsa_password: Optional[str] = None
|
rsa_password: str | None = None
|
||||||
# Channel key (v4.0.0)
|
# Channel key (v4.0.0)
|
||||||
channel_key: Optional[str] = Field(
|
channel_key: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Channel key for decryption. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
|
description="Channel key for decryption. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
|
||||||
)
|
)
|
||||||
@@ -272,10 +266,10 @@ class DecodeRequest(BaseModel):
|
|||||||
class DecodeResponse(BaseModel):
|
class DecodeResponse(BaseModel):
|
||||||
"""Response for decode - can be text or file."""
|
"""Response for decode - can be text or file."""
|
||||||
payload_type: str # 'text' or 'file'
|
payload_type: str # 'text' or 'file'
|
||||||
message: Optional[str] = None # For text
|
message: str | None = None # For text
|
||||||
file_data_base64: Optional[str] = None # For file (base64-encoded)
|
file_data_base64: str | None = None # For file (base64-encoded)
|
||||||
filename: Optional[str] = None # For file
|
filename: str | None = None # For file
|
||||||
mime_type: Optional[str] = None # For file
|
mime_type: str | None = None # For file
|
||||||
|
|
||||||
|
|
||||||
class ModeCapacity(BaseModel):
|
class ModeCapacity(BaseModel):
|
||||||
@@ -292,7 +286,7 @@ class ImageInfoResponse(BaseModel):
|
|||||||
pixels: int
|
pixels: int
|
||||||
capacity_bytes: int = Field(description="LSB mode capacity (for backwards compatibility)")
|
capacity_bytes: int = Field(description="LSB mode capacity (for backwards compatibility)")
|
||||||
capacity_kb: int = Field(description="LSB mode capacity in KB")
|
capacity_kb: int = Field(description="LSB mode capacity in KB")
|
||||||
modes: Optional[dict[str, ModeCapacity]] = Field(
|
modes: dict[str, ModeCapacity] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Capacity by embedding mode (v3.0+)"
|
description="Capacity by embedding mode (v3.0+)"
|
||||||
)
|
)
|
||||||
@@ -301,7 +295,7 @@ class ImageInfoResponse(BaseModel):
|
|||||||
class CompareModesRequest(BaseModel):
|
class CompareModesRequest(BaseModel):
|
||||||
"""Request for comparing embedding modes."""
|
"""Request for comparing embedding modes."""
|
||||||
carrier_image_base64: str
|
carrier_image_base64: str
|
||||||
payload_size: Optional[int] = Field(
|
payload_size: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Optional payload size to check if it fits"
|
description="Optional payload size to check if it fits"
|
||||||
)
|
)
|
||||||
@@ -313,7 +307,7 @@ class CompareModesResponse(BaseModel):
|
|||||||
height: int
|
height: int
|
||||||
lsb: dict
|
lsb: dict
|
||||||
dct: dict
|
dct: dict
|
||||||
payload_check: Optional[dict] = None
|
payload_check: dict | None = None
|
||||||
recommendation: str
|
recommendation: str
|
||||||
|
|
||||||
|
|
||||||
@@ -332,9 +326,9 @@ class ChannelStatusResponse(BaseModel):
|
|||||||
"""Response for channel key status (v4.0.0)."""
|
"""Response for channel key status (v4.0.0)."""
|
||||||
mode: str = Field(description="'public' or 'private'")
|
mode: str = Field(description="'public' or 'private'")
|
||||||
configured: bool = Field(description="Whether a channel key is configured")
|
configured: bool = Field(description="Whether a channel key is configured")
|
||||||
fingerprint: Optional[str] = Field(default=None, description="Key fingerprint (partial)")
|
fingerprint: str | None = Field(default=None, description="Key fingerprint (partial)")
|
||||||
source: Optional[str] = Field(default=None, description="Where the key comes from")
|
source: str | None = Field(default=None, description="Where the key comes from")
|
||||||
key: Optional[str] = Field(default=None, description="Full key (only if reveal=true)")
|
key: str | None = Field(default=None, description="Full key (only if reveal=true)")
|
||||||
|
|
||||||
|
|
||||||
class ChannelGenerateResponse(BaseModel):
|
class ChannelGenerateResponse(BaseModel):
|
||||||
@@ -342,7 +336,7 @@ class ChannelGenerateResponse(BaseModel):
|
|||||||
key: str = Field(description="Generated channel key")
|
key: str = Field(description="Generated channel key")
|
||||||
fingerprint: str = Field(description="Key fingerprint")
|
fingerprint: str = Field(description="Key fingerprint")
|
||||||
saved: bool = Field(default=False, description="Whether key was saved to config")
|
saved: bool = Field(default=False, description="Whether key was saved to config")
|
||||||
save_location: Optional[str] = Field(default=None, description="Where key was saved")
|
save_location: str | None = Field(default=None, description="Where key was saved")
|
||||||
|
|
||||||
|
|
||||||
class ChannelSetRequest(BaseModel):
|
class ChannelSetRequest(BaseModel):
|
||||||
@@ -356,7 +350,7 @@ class ModesResponse(BaseModel):
|
|||||||
lsb: dict
|
lsb: dict
|
||||||
dct: DctModeInfo
|
dct: DctModeInfo
|
||||||
# Channel key status (v4.0.0)
|
# Channel key status (v4.0.0)
|
||||||
channel: Optional[dict] = Field(
|
channel: dict | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Channel key status (v4.0.0)"
|
description="Channel key status (v4.0.0)"
|
||||||
)
|
)
|
||||||
@@ -369,12 +363,12 @@ class StatusResponse(BaseModel):
|
|||||||
has_dct: bool
|
has_dct: bool
|
||||||
max_payload_kb: int
|
max_payload_kb: int
|
||||||
available_modes: list[str]
|
available_modes: list[str]
|
||||||
dct_features: Optional[dict] = Field(
|
dct_features: dict | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="DCT mode features (v3.0.1+)"
|
description="DCT mode features (v3.0.1+)"
|
||||||
)
|
)
|
||||||
# Channel key status (v4.0.0)
|
# Channel key status (v4.0.0)
|
||||||
channel: Optional[dict] = Field(
|
channel: dict | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Channel key status (v4.0.0)"
|
description="Channel key status (v4.0.0)"
|
||||||
)
|
)
|
||||||
@@ -385,8 +379,8 @@ class StatusResponse(BaseModel):
|
|||||||
|
|
||||||
class QrExtractResponse(BaseModel):
|
class QrExtractResponse(BaseModel):
|
||||||
success: bool
|
success: bool
|
||||||
key_pem: Optional[str] = None
|
key_pem: str | None = None
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class WillFitRequest(BaseModel):
|
class WillFitRequest(BaseModel):
|
||||||
@@ -408,14 +402,14 @@ class WillFitResponse(BaseModel):
|
|||||||
|
|
||||||
class ErrorResponse(BaseModel):
|
class ErrorResponse(BaseModel):
|
||||||
error: str
|
error: str
|
||||||
detail: Optional[str] = None
|
detail: str | None = None
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# HELPER: RESOLVE CHANNEL KEY
|
# HELPER: RESOLVE CHANNEL KEY
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def _resolve_channel_key(channel_key: Optional[str]) -> Optional[str]:
|
def _resolve_channel_key(channel_key: str | None) -> str | None:
|
||||||
"""
|
"""
|
||||||
Resolve channel key from API parameter.
|
Resolve channel key from API parameter.
|
||||||
|
|
||||||
@@ -443,13 +437,13 @@ def _resolve_channel_key(channel_key: Optional[str]) -> Optional[str]:
|
|||||||
if not validate_channel_key(channel_key):
|
if not validate_channel_key(channel_key):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
400,
|
400,
|
||||||
f"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
||||||
)
|
)
|
||||||
|
|
||||||
return channel_key
|
return channel_key
|
||||||
|
|
||||||
|
|
||||||
def _get_channel_info(channel_key: Optional[str]) -> tuple[str, Optional[str]]:
|
def _get_channel_info(channel_key: str | None) -> tuple[str, str | None]:
|
||||||
"""
|
"""
|
||||||
Get channel mode and fingerprint for response.
|
Get channel mode and fingerprint for response.
|
||||||
|
|
||||||
@@ -1112,10 +1106,10 @@ async def api_encode_multipart(
|
|||||||
reference_photo: UploadFile = File(...),
|
reference_photo: UploadFile = File(...),
|
||||||
carrier: UploadFile = File(...),
|
carrier: UploadFile = File(...),
|
||||||
message: str = Form(""),
|
message: str = Form(""),
|
||||||
payload_file: Optional[UploadFile] = File(None),
|
payload_file: UploadFile | None = File(None),
|
||||||
pin: str = Form(""),
|
pin: str = Form(""),
|
||||||
rsa_key: Optional[UploadFile] = File(None),
|
rsa_key: UploadFile | None = File(None),
|
||||||
rsa_key_qr: Optional[UploadFile] = File(None),
|
rsa_key_qr: UploadFile | None = File(None),
|
||||||
rsa_password: str = Form(""),
|
rsa_password: str = Form(""),
|
||||||
# Channel key (v4.0.0)
|
# Channel key (v4.0.0)
|
||||||
channel_key: str = Form("auto", description="Channel key: 'auto'=server config, 'none'=public, 'XXXX-...'=explicit"),
|
channel_key: str = Form("auto", description="Channel key: 'auto'=server config, 'none'=public, 'XXXX-...'=explicit"),
|
||||||
@@ -1253,8 +1247,8 @@ async def api_decode_multipart(
|
|||||||
reference_photo: UploadFile = File(...),
|
reference_photo: UploadFile = File(...),
|
||||||
stego_image: UploadFile = File(...),
|
stego_image: UploadFile = File(...),
|
||||||
pin: str = Form(""),
|
pin: str = Form(""),
|
||||||
rsa_key: Optional[UploadFile] = File(None),
|
rsa_key: UploadFile | None = File(None),
|
||||||
rsa_key_qr: Optional[UploadFile] = File(None),
|
rsa_key_qr: UploadFile | None = File(None),
|
||||||
rsa_password: str = Form(""),
|
rsa_password: str = Form(""),
|
||||||
# Channel key (v4.0.0)
|
# Channel key (v4.0.0)
|
||||||
channel_key: str = Form("auto", description="Channel key: 'auto'=server config, 'none'=public, 'XXXX-...'=explicit"),
|
channel_key: str = Form("auto", description="Channel key: 'auto'=server config, 'none'=public, 'XXXX-...'=explicit"),
|
||||||
|
|||||||
@@ -26,85 +26,59 @@ Usage:
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
# Add parent to path for development
|
# Add parent to path for development
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
||||||
|
|
||||||
import stegasoo
|
|
||||||
from stegasoo import (
|
from stegasoo import (
|
||||||
# Core operations
|
|
||||||
encode, decode,
|
|
||||||
|
|
||||||
# Credential generation
|
|
||||||
generate_credentials,
|
|
||||||
generate_passphrase,
|
|
||||||
generate_pin,
|
|
||||||
export_rsa_key_pem,
|
|
||||||
load_rsa_key,
|
|
||||||
|
|
||||||
# Validation
|
|
||||||
validate_image,
|
|
||||||
|
|
||||||
# Image utilities
|
|
||||||
get_image_info,
|
|
||||||
compare_capacity,
|
|
||||||
|
|
||||||
# Steganography functions
|
|
||||||
has_dct_support,
|
|
||||||
compare_modes,
|
|
||||||
will_fit_by_mode,
|
|
||||||
|
|
||||||
# Utilities
|
|
||||||
generate_filename,
|
|
||||||
|
|
||||||
# Version
|
|
||||||
__version__,
|
|
||||||
|
|
||||||
# Exceptions
|
|
||||||
StegasooError,
|
|
||||||
DecryptionError,
|
DecryptionError,
|
||||||
ExtractionError,
|
ExtractionError,
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
FilePayload,
|
FilePayload,
|
||||||
|
# Exceptions
|
||||||
|
StegasooError,
|
||||||
|
# Utilities
|
||||||
|
__version__,
|
||||||
|
clear_channel_key,
|
||||||
|
compare_modes,
|
||||||
|
decode,
|
||||||
|
# Core operations
|
||||||
|
encode,
|
||||||
|
export_rsa_key_pem,
|
||||||
# Channel key functions (v4.0.0)
|
# Channel key functions (v4.0.0)
|
||||||
generate_channel_key,
|
generate_channel_key,
|
||||||
get_channel_key,
|
# Credential generation
|
||||||
set_channel_key,
|
generate_credentials,
|
||||||
clear_channel_key,
|
|
||||||
has_channel_key,
|
|
||||||
get_channel_status,
|
get_channel_status,
|
||||||
|
# Validation
|
||||||
|
get_image_info,
|
||||||
|
has_channel_key,
|
||||||
|
has_dct_support,
|
||||||
|
load_rsa_key,
|
||||||
|
set_channel_key,
|
||||||
validate_channel_key,
|
validate_channel_key,
|
||||||
format_channel_key,
|
will_fit_by_mode,
|
||||||
get_active_channel_key,
|
|
||||||
get_channel_fingerprint,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import constants - try main module first, then constants submodule
|
# Import constants - try main module first, then constants submodule
|
||||||
try:
|
try:
|
||||||
from stegasoo import (
|
from stegasoo import ( # noqa: F401
|
||||||
EMBED_MODE_LSB,
|
|
||||||
EMBED_MODE_DCT,
|
|
||||||
EMBED_MODE_AUTO,
|
EMBED_MODE_AUTO,
|
||||||
|
EMBED_MODE_DCT,
|
||||||
|
EMBED_MODE_LSB,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from stegasoo.constants import (
|
pass
|
||||||
EMBED_MODE_LSB,
|
|
||||||
EMBED_MODE_DCT,
|
|
||||||
EMBED_MODE_AUTO,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Import constants that may not be in main __init__
|
# Import constants that may not be in main __init__
|
||||||
try:
|
try:
|
||||||
from stegasoo.constants import (
|
from stegasoo.constants import (
|
||||||
DEFAULT_PASSPHRASE_WORDS,
|
DEFAULT_PASSPHRASE_WORDS,
|
||||||
DEFAULT_PIN_LENGTH,
|
DEFAULT_PIN_LENGTH,
|
||||||
MIN_PIN_LENGTH,
|
|
||||||
MAX_PIN_LENGTH,
|
MAX_PIN_LENGTH,
|
||||||
|
MIN_PIN_LENGTH,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Fallback defaults if constants not available
|
# Fallback defaults if constants not available
|
||||||
@@ -122,17 +96,23 @@ except ImportError:
|
|||||||
|
|
||||||
# QR Code utilities
|
# QR Code utilities
|
||||||
try:
|
try:
|
||||||
from stegasoo.qr_utils import (
|
from stegasoo.qr_utils import ( # noqa: F401
|
||||||
|
can_fit_in_qr,
|
||||||
extract_key_from_qr_file,
|
extract_key_from_qr_file,
|
||||||
generate_qr_code,
|
generate_qr_code,
|
||||||
has_qr_read, has_qr_write,
|
has_qr_read,
|
||||||
can_fit_in_qr, needs_compression,
|
has_qr_write,
|
||||||
|
needs_compression,
|
||||||
)
|
)
|
||||||
HAS_QR = True
|
HAS_QR = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_QR = False
|
HAS_QR = False
|
||||||
has_qr_read = lambda: False
|
|
||||||
has_qr_write = lambda: False
|
def has_qr_read() -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_qr_write() -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -179,8 +159,8 @@ def cli():
|
|||||||
# CHANNEL KEY HELPERS
|
# CHANNEL KEY HELPERS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def resolve_channel_key_option(channel: Optional[str], channel_file: Optional[str],
|
def resolve_channel_key_option(channel: str | None, channel_file: str | None,
|
||||||
no_channel: bool) -> Optional[str]:
|
no_channel: bool) -> str | None:
|
||||||
"""
|
"""
|
||||||
Resolve channel key from CLI options.
|
Resolve channel key from CLI options.
|
||||||
|
|
||||||
@@ -208,8 +188,8 @@ def resolve_channel_key_option(channel: Optional[str], channel_file: Optional[st
|
|||||||
# Explicit key provided
|
# Explicit key provided
|
||||||
if not validate_channel_key(channel):
|
if not validate_channel_key(channel):
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n"
|
"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n"
|
||||||
f"Generate a new key with: stegasoo channel generate"
|
"Generate a new key with: stegasoo channel generate"
|
||||||
)
|
)
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
@@ -217,7 +197,7 @@ def resolve_channel_key_option(channel: Optional[str], channel_file: Optional[st
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def format_channel_status_line(quiet: bool = False) -> Optional[str]:
|
def format_channel_status_line(quiet: bool = False) -> str | None:
|
||||||
"""Get a one-line status for channel key configuration."""
|
"""Get a one-line status for channel key configuration."""
|
||||||
if quiet:
|
if quiet:
|
||||||
return None
|
return None
|
||||||
@@ -338,14 +318,14 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
|||||||
if creds.rsa_key_pem:
|
if creds.rsa_key_pem:
|
||||||
click.echo(f" RSA entropy: {creds.rsa_entropy} bits")
|
click.echo(f" RSA entropy: {creds.rsa_entropy} bits")
|
||||||
click.echo(f" Combined: {creds.total_entropy} bits")
|
click.echo(f" Combined: {creds.total_entropy} bits")
|
||||||
click.secho(f" + photo entropy: 80-256 bits", dim=True)
|
click.secho(" + photo entropy: 80-256 bits", dim=True)
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
# Show channel key status
|
# Show channel key status
|
||||||
if has_channel_key():
|
if has_channel_key():
|
||||||
status = get_channel_status()
|
status = get_channel_status()
|
||||||
click.secho("─── CHANNEL KEY ───", fg='magenta')
|
click.secho("─── CHANNEL KEY ───", fg='magenta')
|
||||||
click.echo(f" Status: Private mode")
|
click.echo(" Status: Private mode")
|
||||||
click.echo(f" Fingerprint: {status['fingerprint']}")
|
click.echo(f" Fingerprint: {status['fingerprint']}")
|
||||||
click.secho(f" (configured via {status['source']})", dim=True)
|
click.secho(f" (configured via {status['source']})", dim=True)
|
||||||
click.echo()
|
click.echo()
|
||||||
@@ -547,16 +527,16 @@ def channel_set(key, key_file, project):
|
|||||||
|
|
||||||
if not validate_channel_key(key):
|
if not validate_channel_key(key):
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"Invalid channel key format.\n"
|
"Invalid channel key format.\n"
|
||||||
f"Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n"
|
"Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n"
|
||||||
f"Generate a new key with: stegasoo channel generate"
|
"Generate a new key with: stegasoo channel generate"
|
||||||
)
|
)
|
||||||
|
|
||||||
location = 'project' if project else 'user'
|
location = 'project' if project else 'user'
|
||||||
set_channel_key(key, location=location)
|
set_channel_key(key, location=location)
|
||||||
|
|
||||||
status = get_channel_status()
|
status = get_channel_status()
|
||||||
click.secho(f"✓ Channel key saved", fg='green')
|
click.secho("✓ Channel key saved", fg='green')
|
||||||
click.echo(f" Location: {status['source']}")
|
click.echo(f" Location: {status['source']}")
|
||||||
click.echo(f" Fingerprint: {status['fingerprint']}")
|
click.echo(f" Fingerprint: {status['fingerprint']}")
|
||||||
|
|
||||||
@@ -759,7 +739,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
|
|||||||
|
|
||||||
suggestion = ""
|
suggestion = ""
|
||||||
if alt_mode == 'lsb' and alt_check['fits']:
|
if alt_mode == 'lsb' and alt_check['fits']:
|
||||||
suggestion = f"\n Tip: Payload would fit in LSB mode (--mode lsb)"
|
suggestion = "\n Tip: Payload would fit in LSB mode (--mode lsb)"
|
||||||
|
|
||||||
raise click.ClickException(
|
raise click.ClickException(
|
||||||
f"Payload too large for {embed_mode.upper()} mode.\n"
|
f"Payload too large for {embed_mode.upper()} mode.\n"
|
||||||
@@ -810,7 +790,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
|
|||||||
out_path.write_bytes(result.stego_image)
|
out_path.write_bytes(result.stego_image)
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
click.secho(f"✓ Encoded successfully!", fg='green')
|
click.secho("✓ Encoded successfully!", fg='green')
|
||||||
click.echo(f" Output: {out_path}")
|
click.echo(f" Output: {out_path}")
|
||||||
click.echo(f" Size: {len(result.stego_image):,} bytes")
|
click.echo(f" Size: {len(result.stego_image):,} bytes")
|
||||||
click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
|
click.echo(f" Capacity used: {result.capacity_percent:.1f}%")
|
||||||
@@ -1240,7 +1220,7 @@ def compare(image, payload, size, as_json):
|
|||||||
click.secho(" ┌─── LSB Mode ───", fg='green')
|
click.secho(" ┌─── LSB Mode ───", fg='green')
|
||||||
click.echo(f" │ Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
|
click.echo(f" │ Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
|
||||||
click.echo(f" │ Output: {comparison['lsb']['output']}")
|
click.echo(f" │ Output: {comparison['lsb']['output']}")
|
||||||
click.echo(f" │ Status: ✓ Available")
|
click.echo(" │ Status: ✓ Available")
|
||||||
click.echo(" │")
|
click.echo(" │")
|
||||||
|
|
||||||
# DCT mode
|
# DCT mode
|
||||||
@@ -1248,11 +1228,11 @@ def compare(image, payload, size, as_json):
|
|||||||
click.echo(f" │ Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)")
|
click.echo(f" │ Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)")
|
||||||
click.echo(f" │ Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
|
click.echo(f" │ Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity")
|
||||||
if comparison['dct']['available']:
|
if comparison['dct']['available']:
|
||||||
click.echo(f" │ Status: ✓ Available")
|
click.echo(" │ Status: ✓ Available")
|
||||||
click.echo(f" │ Formats: PNG (lossless), JPEG (smaller)")
|
click.echo(" │ Formats: PNG (lossless), JPEG (smaller)")
|
||||||
click.echo(f" │ Colors: Grayscale (default), Color")
|
click.echo(" │ Colors: Grayscale (default), Color")
|
||||||
else:
|
else:
|
||||||
click.secho(f" │ Status: ✗ Requires scipy (pip install scipy)", fg='yellow')
|
click.secho(" │ Status: ✗ Requires scipy (pip install scipy)", fg='yellow')
|
||||||
click.echo(" │")
|
click.echo(" │")
|
||||||
|
|
||||||
# Payload check
|
# Payload check
|
||||||
@@ -1268,9 +1248,9 @@ def compare(image, payload, size, as_json):
|
|||||||
lsb_color = 'green' if fits_lsb else 'red'
|
lsb_color = 'green' if fits_lsb else 'red'
|
||||||
dct_color = 'green' if fits_dct else 'red'
|
dct_color = 'green' if fits_dct else 'red'
|
||||||
|
|
||||||
click.echo(f" │ LSB mode: ", nl=False)
|
click.echo(" │ LSB mode: ", nl=False)
|
||||||
click.secho(f"{lsb_icon} {'Fits' if fits_lsb else 'Too large'}", fg=lsb_color)
|
click.secho(f"{lsb_icon} {'Fits' if fits_lsb else 'Too large'}", fg=lsb_color)
|
||||||
click.echo(f" │ DCT mode: ", nl=False)
|
click.echo(" │ DCT mode: ", nl=False)
|
||||||
click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color)
|
click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color)
|
||||||
click.echo(" │")
|
click.echo(" │")
|
||||||
|
|
||||||
|
|||||||
@@ -22,20 +22,16 @@ NEW in v3.0.1: DCT output format selection (PNG or JPEG) and color mode (graysca
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import secrets
|
|
||||||
import mimetypes
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
|
||||||
|
from flask import Flask, flash, jsonify, redirect, render_template, request, send_file, url_for
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from flask import (
|
|
||||||
Flask, render_template, request, send_file,
|
|
||||||
jsonify, flash, redirect, url_for
|
|
||||||
)
|
|
||||||
|
|
||||||
import os
|
|
||||||
os.environ['NUMPY_MADVISE_HUGEPAGE'] = '0'
|
os.environ['NUMPY_MADVISE_HUGEPAGE'] = '0'
|
||||||
os.environ['OMP_NUM_THREADS'] = '1'
|
os.environ['OMP_NUM_THREADS'] = '1'
|
||||||
|
|
||||||
@@ -44,75 +40,76 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
|||||||
|
|
||||||
import stegasoo
|
import stegasoo
|
||||||
from stegasoo import (
|
from stegasoo import (
|
||||||
generate_credentials,
|
CapacityError,
|
||||||
export_rsa_key_pem, load_rsa_key,
|
DecryptionError,
|
||||||
validate_pin, validate_message, validate_image,
|
|
||||||
validate_rsa_key, validate_security_factors,
|
|
||||||
validate_file_payload, validate_passphrase,
|
|
||||||
generate_filename,
|
|
||||||
StegasooError, DecryptionError, CapacityError,
|
|
||||||
has_argon2,
|
|
||||||
FilePayload,
|
FilePayload,
|
||||||
# Embedding modes
|
StegasooError,
|
||||||
EMBED_MODE_LSB,
|
export_rsa_key_pem,
|
||||||
EMBED_MODE_DCT,
|
generate_credentials,
|
||||||
EMBED_MODE_AUTO,
|
generate_filename,
|
||||||
has_dct_support,
|
|
||||||
# Channel key functions (v4.0.0)
|
|
||||||
has_channel_key,
|
|
||||||
get_channel_status,
|
get_channel_status,
|
||||||
|
has_argon2,
|
||||||
|
# Channel key functions (v4.0.0)
|
||||||
|
has_dct_support,
|
||||||
|
load_rsa_key,
|
||||||
validate_channel_key,
|
validate_channel_key,
|
||||||
generate_channel_key,
|
validate_file_payload,
|
||||||
# NOTE: encode, decode, compare_modes, will_fit_by_mode now use subprocess isolation
|
validate_image,
|
||||||
|
validate_message,
|
||||||
|
validate_passphrase,
|
||||||
|
validate_pin,
|
||||||
|
validate_rsa_key,
|
||||||
|
validate_security_factors,
|
||||||
)
|
)
|
||||||
from stegasoo.constants import (
|
from stegasoo.constants import (
|
||||||
__version__,
|
|
||||||
MAX_MESSAGE_SIZE, MAX_MESSAGE_CHARS,
|
|
||||||
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
|
||||||
MIN_PASSPHRASE_WORDS, RECOMMENDED_PASSPHRASE_WORDS,
|
|
||||||
DEFAULT_PASSPHRASE_WORDS,
|
DEFAULT_PASSPHRASE_WORDS,
|
||||||
VALID_RSA_SIZES, MAX_FILE_SIZE,
|
MAX_FILE_PAYLOAD_SIZE,
|
||||||
MAX_FILE_PAYLOAD_SIZE, MAX_UPLOAD_SIZE,
|
MAX_FILE_SIZE,
|
||||||
TEMP_FILE_EXPIRY, TEMP_FILE_EXPIRY_MINUTES,
|
MAX_MESSAGE_CHARS,
|
||||||
THUMBNAIL_SIZE, THUMBNAIL_QUALITY,
|
MAX_PIN_LENGTH,
|
||||||
|
MAX_UPLOAD_SIZE,
|
||||||
|
MIN_PASSPHRASE_WORDS,
|
||||||
|
MIN_PIN_LENGTH,
|
||||||
|
RECOMMENDED_PASSPHRASE_WORDS,
|
||||||
|
TEMP_FILE_EXPIRY,
|
||||||
|
TEMP_FILE_EXPIRY_MINUTES,
|
||||||
|
THUMBNAIL_QUALITY,
|
||||||
|
THUMBNAIL_SIZE,
|
||||||
|
VALID_RSA_SIZES,
|
||||||
|
__version__,
|
||||||
)
|
)
|
||||||
|
|
||||||
# QR Code support
|
# QR Code support
|
||||||
try:
|
try:
|
||||||
import qrcode
|
import qrcode # noqa: F401
|
||||||
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
|
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M # noqa: F401
|
||||||
HAS_QRCODE = True
|
HAS_QRCODE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_QRCODE = False
|
HAS_QRCODE = False
|
||||||
|
|
||||||
# QR Code reading
|
# QR Code reading
|
||||||
try:
|
try:
|
||||||
from pyzbar.pyzbar import decode as pyzbar_decode
|
from pyzbar.pyzbar import decode as pyzbar_decode # noqa: F401
|
||||||
HAS_QRCODE_READ = True
|
HAS_QRCODE_READ = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_QRCODE_READ = False
|
HAS_QRCODE_READ = False
|
||||||
|
|
||||||
import zlib
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Import QR utilities
|
# Import QR utilities
|
||||||
from stegasoo.qr_utils import (
|
|
||||||
compress_data, decompress_data, auto_decompress,
|
|
||||||
is_compressed, can_fit_in_qr, needs_compression,
|
|
||||||
generate_qr_code, read_qr_code, extract_key_from_qr,
|
|
||||||
detect_and_crop_qr,
|
|
||||||
has_qr_write, has_qr_read,
|
|
||||||
QR_MAX_BINARY, COMPRESSION_PREFIX
|
|
||||||
)
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SUBPROCESS ISOLATION FOR STEGASOO OPERATIONS
|
# SUBPROCESS ISOLATION FOR STEGASOO OPERATIONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Runs encode/decode/compare in subprocesses to prevent jpegio/scipy crashes
|
# Runs encode/decode/compare in subprocesses to prevent jpegio/scipy crashes
|
||||||
# from taking down the Flask server.
|
# from taking down the Flask server.
|
||||||
|
|
||||||
from subprocess_stego import SubprocessStego
|
from subprocess_stego import SubprocessStego
|
||||||
|
|
||||||
|
from stegasoo.qr_utils import (
|
||||||
|
can_fit_in_qr,
|
||||||
|
detect_and_crop_qr,
|
||||||
|
extract_key_from_qr,
|
||||||
|
generate_qr_code,
|
||||||
|
)
|
||||||
|
|
||||||
# Initialize subprocess wrapper (worker script must be in same directory)
|
# Initialize subprocess wrapper (worker script must be in same directory)
|
||||||
subprocess_stego = SubprocessStego(timeout=180) # 3 minute timeout for large images
|
subprocess_stego = SubprocessStego(timeout=180) # 3 minute timeout for large images
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ Usage:
|
|||||||
echo '{"operation": "encode", ...}' | python stego_worker.py
|
echo '{"operation": "encode", ...}' | python stego_worker.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ def _get_channel_info(resolved_key):
|
|||||||
Returns:
|
Returns:
|
||||||
(mode, fingerprint) tuple
|
(mode, fingerprint) tuple
|
||||||
"""
|
"""
|
||||||
from stegasoo import has_channel_key, get_channel_status
|
from stegasoo import get_channel_status, has_channel_key
|
||||||
|
|
||||||
if resolved_key == "":
|
if resolved_key == "":
|
||||||
return "public", None
|
return "public", None
|
||||||
@@ -73,7 +73,7 @@ def _get_channel_info(resolved_key):
|
|||||||
|
|
||||||
def encode_operation(params: dict) -> dict:
|
def encode_operation(params: dict) -> dict:
|
||||||
"""Handle encode operation."""
|
"""Handle encode operation."""
|
||||||
from stegasoo import encode, FilePayload
|
from stegasoo import FilePayload, encode
|
||||||
|
|
||||||
# Decode base64 inputs
|
# Decode base64 inputs
|
||||||
carrier_data = base64.b64decode(params['carrier_b64'])
|
carrier_data = base64.b64decode(params['carrier_b64'])
|
||||||
|
|||||||
@@ -43,14 +43,13 @@ Usage:
|
|||||||
result = stego.compare_modes(carrier_bytes)
|
result = stego.compare_modes(carrier_bytes)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Dict, Any, Union
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
# Default timeout for operations (seconds)
|
# Default timeout for operations (seconds)
|
||||||
DEFAULT_TIMEOUT = 120
|
DEFAULT_TIMEOUT = 120
|
||||||
@@ -63,14 +62,14 @@ WORKER_SCRIPT = Path(__file__).parent / 'stego_worker.py'
|
|||||||
class EncodeResult:
|
class EncodeResult:
|
||||||
"""Result from encode operation."""
|
"""Result from encode operation."""
|
||||||
success: bool
|
success: bool
|
||||||
stego_data: Optional[bytes] = None
|
stego_data: bytes | None = None
|
||||||
filename: Optional[str] = None
|
filename: str | None = None
|
||||||
stats: Optional[Dict[str, Any]] = None
|
stats: dict[str, Any] | None = None
|
||||||
# Channel info (v4.0.0)
|
# Channel info (v4.0.0)
|
||||||
channel_mode: Optional[str] = None
|
channel_mode: str | None = None
|
||||||
channel_fingerprint: Optional[str] = None
|
channel_fingerprint: str | None = None
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
error_type: Optional[str] = None
|
error_type: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -78,12 +77,12 @@ class DecodeResult:
|
|||||||
"""Result from decode operation."""
|
"""Result from decode operation."""
|
||||||
success: bool
|
success: bool
|
||||||
is_file: bool = False
|
is_file: bool = False
|
||||||
message: Optional[str] = None
|
message: str | None = None
|
||||||
file_data: Optional[bytes] = None
|
file_data: bytes | None = None
|
||||||
filename: Optional[str] = None
|
filename: str | None = None
|
||||||
mime_type: Optional[str] = None
|
mime_type: str | None = None
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
error_type: Optional[str] = None
|
error_type: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -92,9 +91,9 @@ class CompareResult:
|
|||||||
success: bool
|
success: bool
|
||||||
width: int = 0
|
width: int = 0
|
||||||
height: int = 0
|
height: int = 0
|
||||||
lsb: Optional[Dict[str, Any]] = None
|
lsb: dict[str, Any] | None = None
|
||||||
dct: Optional[Dict[str, Any]] = None
|
dct: dict[str, Any] | None = None
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -107,7 +106,7 @@ class CapacityResult:
|
|||||||
usage_percent: float = 0.0
|
usage_percent: float = 0.0
|
||||||
headroom: int = 0
|
headroom: int = 0
|
||||||
mode: str = ""
|
mode: str = ""
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -116,10 +115,10 @@ class ChannelStatusResult:
|
|||||||
success: bool
|
success: bool
|
||||||
mode: str = "public"
|
mode: str = "public"
|
||||||
configured: bool = False
|
configured: bool = False
|
||||||
fingerprint: Optional[str] = None
|
fingerprint: str | None = None
|
||||||
source: Optional[str] = None
|
source: str | None = None
|
||||||
key: Optional[str] = None
|
key: str | None = None
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class SubprocessStego:
|
class SubprocessStego:
|
||||||
@@ -132,8 +131,8 @@ class SubprocessStego:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
worker_path: Optional[Path] = None,
|
worker_path: Path | None = None,
|
||||||
python_executable: Optional[str] = None,
|
python_executable: str | None = None,
|
||||||
timeout: int = DEFAULT_TIMEOUT,
|
timeout: int = DEFAULT_TIMEOUT,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -151,7 +150,7 @@ class SubprocessStego:
|
|||||||
if not self.worker_path.exists():
|
if not self.worker_path.exists():
|
||||||
raise FileNotFoundError(f"Worker script not found: {self.worker_path}")
|
raise FileNotFoundError(f"Worker script not found: {self.worker_path}")
|
||||||
|
|
||||||
def _run_worker(self, params: Dict[str, Any], timeout: Optional[int] = None) -> Dict[str, Any]:
|
def _run_worker(self, params: dict[str, Any], timeout: int | None = None) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Run the worker subprocess with given parameters.
|
Run the worker subprocess with given parameters.
|
||||||
|
|
||||||
@@ -215,20 +214,20 @@ class SubprocessStego:
|
|||||||
self,
|
self,
|
||||||
carrier_data: bytes,
|
carrier_data: bytes,
|
||||||
reference_data: bytes,
|
reference_data: bytes,
|
||||||
message: Optional[str] = None,
|
message: str | None = None,
|
||||||
file_data: Optional[bytes] = None,
|
file_data: bytes | None = None,
|
||||||
file_name: Optional[str] = None,
|
file_name: str | None = None,
|
||||||
file_mime: Optional[str] = None,
|
file_mime: str | None = None,
|
||||||
passphrase: str = "",
|
passphrase: str = "",
|
||||||
pin: Optional[str] = None,
|
pin: str | None = None,
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
embed_mode: str = "lsb",
|
embed_mode: str = "lsb",
|
||||||
dct_output_format: str = "png",
|
dct_output_format: str = "png",
|
||||||
dct_color_mode: str = "color",
|
dct_color_mode: str = "color",
|
||||||
# Channel key (v4.0.0)
|
# Channel key (v4.0.0)
|
||||||
channel_key: Optional[str] = "auto",
|
channel_key: str | None = "auto",
|
||||||
timeout: Optional[int] = None,
|
timeout: int | None = None,
|
||||||
) -> EncodeResult:
|
) -> EncodeResult:
|
||||||
"""
|
"""
|
||||||
Encode a message or file into an image.
|
Encode a message or file into an image.
|
||||||
@@ -298,13 +297,13 @@ class SubprocessStego:
|
|||||||
stego_data: bytes,
|
stego_data: bytes,
|
||||||
reference_data: bytes,
|
reference_data: bytes,
|
||||||
passphrase: str = "",
|
passphrase: str = "",
|
||||||
pin: Optional[str] = None,
|
pin: str | None = None,
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
embed_mode: str = "auto",
|
embed_mode: str = "auto",
|
||||||
# Channel key (v4.0.0)
|
# Channel key (v4.0.0)
|
||||||
channel_key: Optional[str] = "auto",
|
channel_key: str | None = "auto",
|
||||||
timeout: Optional[int] = None,
|
timeout: int | None = None,
|
||||||
) -> DecodeResult:
|
) -> DecodeResult:
|
||||||
"""
|
"""
|
||||||
Decode a message or file from a stego image.
|
Decode a message or file from a stego image.
|
||||||
@@ -364,7 +363,7 @@ class SubprocessStego:
|
|||||||
def compare_modes(
|
def compare_modes(
|
||||||
self,
|
self,
|
||||||
carrier_data: bytes,
|
carrier_data: bytes,
|
||||||
timeout: Optional[int] = None,
|
timeout: int | None = None,
|
||||||
) -> CompareResult:
|
) -> CompareResult:
|
||||||
"""
|
"""
|
||||||
Compare LSB and DCT capacity for a carrier image.
|
Compare LSB and DCT capacity for a carrier image.
|
||||||
@@ -403,7 +402,7 @@ class SubprocessStego:
|
|||||||
carrier_data: bytes,
|
carrier_data: bytes,
|
||||||
payload_size: int,
|
payload_size: int,
|
||||||
embed_mode: str = "lsb",
|
embed_mode: str = "lsb",
|
||||||
timeout: Optional[int] = None,
|
timeout: int | None = None,
|
||||||
) -> CapacityResult:
|
) -> CapacityResult:
|
||||||
"""
|
"""
|
||||||
Check if a payload will fit in the carrier.
|
Check if a payload will fit in the carrier.
|
||||||
@@ -446,7 +445,7 @@ class SubprocessStego:
|
|||||||
def get_channel_status(
|
def get_channel_status(
|
||||||
self,
|
self,
|
||||||
reveal: bool = False,
|
reveal: bool = False,
|
||||||
timeout: Optional[int] = None,
|
timeout: int | None = None,
|
||||||
) -> ChannelStatusResult:
|
) -> ChannelStatusResult:
|
||||||
"""
|
"""
|
||||||
Get current channel key status (v4.0.0).
|
Get current channel key status (v4.0.0).
|
||||||
@@ -483,7 +482,7 @@ class SubprocessStego:
|
|||||||
|
|
||||||
|
|
||||||
# Convenience function for quick usage
|
# Convenience function for quick usage
|
||||||
_default_stego: Optional[SubprocessStego] = None
|
_default_stego: SubprocessStego | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_subprocess_stego() -> SubprocessStego:
|
def get_subprocess_stego() -> SubprocessStego:
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
"""
|
|
||||||
Minimal test to isolate the memory corruption crash.
|
|
||||||
|
|
||||||
Add this route to your app.py temporarily to test if the crash
|
|
||||||
is in Flask/Pillow or in stegasoo code.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
1. Add this code to app.py
|
|
||||||
2. Restart the server
|
|
||||||
3. Use the /test-capacity endpoint instead of /api/compare-capacity
|
|
||||||
4. If it crashes: Flask or Pillow issue
|
|
||||||
5. If it works: Stegasoo code issue
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Add these imports at the top of app.py if not present:
|
|
||||||
# from PIL import Image
|
|
||||||
# import io
|
|
||||||
|
|
||||||
# Add this route to app.py:
|
|
||||||
|
|
||||||
@app.route('/test-capacity', methods=['POST'])
|
|
||||||
def test_capacity():
|
|
||||||
"""
|
|
||||||
Minimal capacity test - no stegasoo code, just PIL.
|
|
||||||
"""
|
|
||||||
carrier = request.files.get('carrier')
|
|
||||||
if not carrier:
|
|
||||||
return jsonify({'error': 'No carrier image provided'}), 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Read the file data
|
|
||||||
carrier_data = carrier.read()
|
|
||||||
|
|
||||||
# Method 1: Just get size from PIL
|
|
||||||
buffer = io.BytesIO(carrier_data)
|
|
||||||
img = Image.open(buffer)
|
|
||||||
width, height = img.size
|
|
||||||
fmt = img.format
|
|
||||||
mode = img.mode
|
|
||||||
img.close()
|
|
||||||
buffer.close()
|
|
||||||
|
|
||||||
# Simple capacity calculation (no scipy, no numpy)
|
|
||||||
pixels = width * height
|
|
||||||
lsb_bytes = (pixels * 3) // 8
|
|
||||||
blocks = (width // 8) * (height // 8)
|
|
||||||
dct_bytes = (blocks * 16) // 8 - 10
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'width': width,
|
|
||||||
'height': height,
|
|
||||||
'format': fmt,
|
|
||||||
'mode': mode,
|
|
||||||
'lsb': {
|
|
||||||
'capacity_bytes': lsb_bytes,
|
|
||||||
'capacity_kb': round(lsb_bytes / 1024, 1),
|
|
||||||
},
|
|
||||||
'dct': {
|
|
||||||
'capacity_bytes': dct_bytes,
|
|
||||||
'capacity_kb': round(dct_bytes / 1024, 1),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
return jsonify({'error': str(e), 'trace': traceback.format_exc()}), 500
|
|
||||||
|
|
||||||
|
|
||||||
# Alternative: completely bypass PIL too
|
|
||||||
@app.route('/test-capacity-nopil', methods=['POST'])
|
|
||||||
def test_capacity_nopil():
|
|
||||||
"""
|
|
||||||
Ultra-minimal test - no PIL, no stegasoo.
|
|
||||||
"""
|
|
||||||
carrier = request.files.get('carrier')
|
|
||||||
if not carrier:
|
|
||||||
return jsonify({'error': 'No carrier image provided'}), 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
carrier_data = carrier.read()
|
|
||||||
|
|
||||||
# Just return size info, no image processing at all
|
|
||||||
return jsonify({
|
|
||||||
'success': True,
|
|
||||||
'data_size': len(carrier_data),
|
|
||||||
'first_bytes': carrier_data[:20].hex() if len(carrier_data) >= 20 else carrier_data.hex(),
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
return jsonify({'error': str(e), 'trace': traceback.format_exc()}), 500
|
|
||||||
@@ -114,9 +114,18 @@ target-version = ["py310", "py311", "py312"]
|
|||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
exclude = ["frontends/web/test_routes.py"] # Debug snippet, not a real module
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "I", "N", "W", "UP"]
|
select = ["E", "F", "I", "N", "W", "UP"]
|
||||||
ignore = ["E501"]
|
ignore = ["E501"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
# YCbCr colorspace variables (R, G, B, Y, Cb, Cr) are standard names
|
||||||
|
"src/stegasoo/dct_steganography.py" = ["N803", "N806"]
|
||||||
|
# Package __init__.py has imports after try/except and aliases - intentional structure
|
||||||
|
"src/stegasoo/__init__.py" = ["E402"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.10"
|
python_version = "3.10"
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
|
|||||||
@@ -10,56 +10,55 @@ Changes in v4.0.0:
|
|||||||
__version__ = "4.0.1"
|
__version__ = "4.0.1"
|
||||||
|
|
||||||
# Core functionality
|
# Core functionality
|
||||||
from .encode import encode
|
# Channel key management (v4.0.0)
|
||||||
|
from .channel import (
|
||||||
|
clear_channel_key,
|
||||||
|
format_channel_key,
|
||||||
|
generate_channel_key,
|
||||||
|
get_channel_key,
|
||||||
|
get_channel_status,
|
||||||
|
has_channel_key,
|
||||||
|
set_channel_key,
|
||||||
|
validate_channel_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Crypto functions
|
||||||
|
from .crypto import get_active_channel_key, get_channel_fingerprint, has_argon2
|
||||||
from .decode import decode, decode_file, decode_text
|
from .decode import decode, decode_file, decode_text
|
||||||
|
from .encode import encode
|
||||||
|
|
||||||
# Credential generation
|
# Credential generation
|
||||||
from .generate import (
|
from .generate import (
|
||||||
generate_pin,
|
|
||||||
generate_passphrase,
|
|
||||||
generate_rsa_key,
|
|
||||||
generate_credentials,
|
|
||||||
export_rsa_key_pem,
|
export_rsa_key_pem,
|
||||||
|
generate_credentials,
|
||||||
|
generate_passphrase,
|
||||||
|
generate_pin,
|
||||||
|
generate_rsa_key,
|
||||||
load_rsa_key,
|
load_rsa_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Image utilities
|
# Image utilities
|
||||||
from .image_utils import (
|
from .image_utils import (
|
||||||
get_image_info,
|
|
||||||
compare_capacity,
|
compare_capacity,
|
||||||
|
get_image_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Steganography functions
|
||||||
|
from .steganography import (
|
||||||
|
compare_modes,
|
||||||
|
has_dct_support,
|
||||||
|
will_fit_by_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
from .utils import generate_filename
|
from .utils import generate_filename
|
||||||
|
|
||||||
# Crypto functions
|
|
||||||
from .crypto import has_argon2, get_active_channel_key, get_channel_fingerprint
|
|
||||||
|
|
||||||
# Channel key management (v4.0.0)
|
|
||||||
from .channel import (
|
|
||||||
generate_channel_key,
|
|
||||||
get_channel_key,
|
|
||||||
set_channel_key,
|
|
||||||
clear_channel_key,
|
|
||||||
has_channel_key,
|
|
||||||
get_channel_status,
|
|
||||||
validate_channel_key,
|
|
||||||
format_channel_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Steganography functions
|
|
||||||
from .steganography import (
|
|
||||||
has_dct_support,
|
|
||||||
compare_modes,
|
|
||||||
will_fit_by_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
# QR Code utilities - optional, may not be available
|
# QR Code utilities - optional, may not be available
|
||||||
try:
|
try:
|
||||||
from .qr_utils import (
|
from .qr_utils import (
|
||||||
generate_qr_code,
|
|
||||||
extract_key_from_qr,
|
|
||||||
detect_and_crop_qr,
|
detect_and_crop_qr,
|
||||||
|
extract_key_from_qr,
|
||||||
|
generate_qr_code,
|
||||||
)
|
)
|
||||||
HAS_QR_UTILS = True
|
HAS_QR_UTILS = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -70,12 +69,12 @@ except ImportError:
|
|||||||
|
|
||||||
# Validation
|
# Validation
|
||||||
from .validation import (
|
from .validation import (
|
||||||
|
validate_file_payload,
|
||||||
|
validate_image,
|
||||||
|
validate_message,
|
||||||
validate_passphrase,
|
validate_passphrase,
|
||||||
validate_pin,
|
validate_pin,
|
||||||
validate_rsa_key,
|
validate_rsa_key,
|
||||||
validate_message,
|
|
||||||
validate_file_payload,
|
|
||||||
validate_image,
|
|
||||||
validate_security_factors,
|
validate_security_factors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,62 +83,61 @@ validate_reference_photo = validate_image
|
|||||||
validate_carrier = validate_image
|
validate_carrier = validate_image
|
||||||
|
|
||||||
# Additional validators
|
# Additional validators
|
||||||
from .validation import (
|
# Constants
|
||||||
validate_embed_mode,
|
from .constants import (
|
||||||
validate_dct_output_format,
|
DEFAULT_PASSPHRASE_WORDS,
|
||||||
validate_dct_color_mode,
|
EMBED_MODE_AUTO,
|
||||||
)
|
EMBED_MODE_DCT,
|
||||||
|
EMBED_MODE_LSB,
|
||||||
# Models
|
FORMAT_VERSION,
|
||||||
from .models import (
|
LOSSLESS_FORMATS,
|
||||||
ImageInfo,
|
MAX_IMAGE_PIXELS,
|
||||||
CapacityComparison,
|
MAX_MESSAGE_SIZE,
|
||||||
GenerateResult,
|
MAX_PASSPHRASE_WORDS,
|
||||||
EncodeResult,
|
MAX_PIN_LENGTH,
|
||||||
DecodeResult,
|
MIN_IMAGE_PIXELS,
|
||||||
FilePayload,
|
MIN_PASSPHRASE_WORDS,
|
||||||
Credentials,
|
MIN_PIN_LENGTH,
|
||||||
ValidationResult,
|
RECOMMENDED_PASSPHRASE_WORDS,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Exceptions
|
# Exceptions
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
StegasooError,
|
CapacityError,
|
||||||
ValidationError,
|
|
||||||
PinValidationError,
|
|
||||||
MessageValidationError,
|
|
||||||
ImageValidationError,
|
|
||||||
KeyValidationError,
|
|
||||||
SecurityFactorError,
|
|
||||||
CryptoError,
|
CryptoError,
|
||||||
EncryptionError,
|
|
||||||
DecryptionError,
|
DecryptionError,
|
||||||
|
EmbeddingError,
|
||||||
|
EncryptionError,
|
||||||
|
ExtractionError,
|
||||||
|
ImageValidationError,
|
||||||
|
InvalidHeaderError,
|
||||||
KeyDerivationError,
|
KeyDerivationError,
|
||||||
KeyGenerationError,
|
KeyGenerationError,
|
||||||
KeyPasswordError,
|
KeyPasswordError,
|
||||||
|
KeyValidationError,
|
||||||
|
MessageValidationError,
|
||||||
|
PinValidationError,
|
||||||
|
SecurityFactorError,
|
||||||
SteganographyError,
|
SteganographyError,
|
||||||
CapacityError,
|
StegasooError,
|
||||||
ExtractionError,
|
ValidationError,
|
||||||
EmbeddingError,
|
|
||||||
InvalidHeaderError,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Constants
|
# Models
|
||||||
from .constants import (
|
from .models import (
|
||||||
FORMAT_VERSION,
|
CapacityComparison,
|
||||||
MIN_PASSPHRASE_WORDS,
|
Credentials,
|
||||||
RECOMMENDED_PASSPHRASE_WORDS,
|
DecodeResult,
|
||||||
DEFAULT_PASSPHRASE_WORDS,
|
EncodeResult,
|
||||||
MAX_PASSPHRASE_WORDS,
|
FilePayload,
|
||||||
MIN_PIN_LENGTH,
|
GenerateResult,
|
||||||
MAX_PIN_LENGTH,
|
ImageInfo,
|
||||||
MAX_MESSAGE_SIZE,
|
ValidationResult,
|
||||||
MIN_IMAGE_PIXELS,
|
)
|
||||||
MAX_IMAGE_PIXELS,
|
from .validation import (
|
||||||
LOSSLESS_FORMATS,
|
validate_dct_color_mode,
|
||||||
EMBED_MODE_LSB,
|
validate_dct_output_format,
|
||||||
EMBED_MODE_DCT,
|
validate_embed_mode,
|
||||||
EMBED_MODE_AUTO,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Aliases for backward compatibility
|
# Aliases for backward compatibility
|
||||||
|
|||||||
@@ -9,15 +9,14 @@ Changes in v3.2.0:
|
|||||||
- Updated all credential handling to use v3.2.0 API
|
- Updated all credential handling to use v3.2.0 API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
from dataclasses import dataclass, field, asdict
|
|
||||||
from typing import Optional, Callable, Iterator
|
|
||||||
from enum import Enum
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
from collections.abc import Callable, Iterator
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from .constants import ALLOWED_IMAGE_EXTENSIONS, LOSSLESS_FORMATS
|
from .constants import ALLOWED_IMAGE_EXTENSIONS, LOSSLESS_FORMATS
|
||||||
|
|
||||||
@@ -35,17 +34,17 @@ class BatchStatus(Enum):
|
|||||||
class BatchItem:
|
class BatchItem:
|
||||||
"""Represents a single item in a batch operation."""
|
"""Represents a single item in a batch operation."""
|
||||||
input_path: Path
|
input_path: Path
|
||||||
output_path: Optional[Path] = None
|
output_path: Path | None = None
|
||||||
status: BatchStatus = BatchStatus.PENDING
|
status: BatchStatus = BatchStatus.PENDING
|
||||||
error: Optional[str] = None
|
error: str | None = None
|
||||||
start_time: Optional[float] = None
|
start_time: float | None = None
|
||||||
end_time: Optional[float] = None
|
end_time: float | None = None
|
||||||
input_size: int = 0
|
input_size: int = 0
|
||||||
output_size: int = 0
|
output_size: int = 0
|
||||||
message: str = ""
|
message: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration(self) -> Optional[float]:
|
def duration(self) -> float | None:
|
||||||
"""Processing duration in seconds."""
|
"""Processing duration in seconds."""
|
||||||
if self.start_time and self.end_time:
|
if self.start_time and self.end_time:
|
||||||
return self.end_time - self.start_time
|
return self.end_time - self.start_time
|
||||||
@@ -88,8 +87,8 @@ class BatchCredentials:
|
|||||||
reference_photo: bytes
|
reference_photo: bytes
|
||||||
passphrase: str # v3.2.0: renamed from day_phrase
|
passphrase: str # v3.2.0: renamed from day_phrase
|
||||||
pin: str = ""
|
pin: str = ""
|
||||||
rsa_key_data: Optional[bytes] = None
|
rsa_key_data: bytes | None = None
|
||||||
rsa_password: Optional[str] = None
|
rsa_password: str | None = None
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert to dictionary for API compatibility."""
|
"""Convert to dictionary for API compatibility."""
|
||||||
@@ -129,11 +128,11 @@ class BatchResult:
|
|||||||
failed: int = 0
|
failed: int = 0
|
||||||
skipped: int = 0
|
skipped: int = 0
|
||||||
start_time: float = field(default_factory=time.time)
|
start_time: float = field(default_factory=time.time)
|
||||||
end_time: Optional[float] = None
|
end_time: float | None = None
|
||||||
items: list[BatchItem] = field(default_factory=list)
|
items: list[BatchItem] = field(default_factory=list)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration(self) -> Optional[float]:
|
def duration(self) -> float | None:
|
||||||
"""Total batch duration in seconds."""
|
"""Total batch duration in seconds."""
|
||||||
if self.end_time:
|
if self.end_time:
|
||||||
return self.end_time - self.start_time
|
return self.end_time - self.start_time
|
||||||
@@ -265,14 +264,14 @@ class BatchProcessor:
|
|||||||
def batch_encode(
|
def batch_encode(
|
||||||
self,
|
self,
|
||||||
images: list[str | Path],
|
images: list[str | Path],
|
||||||
message: Optional[str] = None,
|
message: str | None = None,
|
||||||
file_payload: Optional[Path] = None,
|
file_payload: Path | None = None,
|
||||||
output_dir: Optional[Path] = None,
|
output_dir: Path | None = None,
|
||||||
output_suffix: str = "_encoded",
|
output_suffix: str = "_encoded",
|
||||||
credentials: dict | BatchCredentials | None = None,
|
credentials: dict | BatchCredentials | None = None,
|
||||||
compress: bool = True,
|
compress: bool = True,
|
||||||
recursive: bool = False,
|
recursive: bool = False,
|
||||||
progress_callback: Optional[ProgressCallback] = None,
|
progress_callback: ProgressCallback | None = None,
|
||||||
encode_func: Callable = None,
|
encode_func: Callable = None,
|
||||||
) -> BatchResult:
|
) -> BatchResult:
|
||||||
"""
|
"""
|
||||||
@@ -360,10 +359,10 @@ class BatchProcessor:
|
|||||||
def batch_decode(
|
def batch_decode(
|
||||||
self,
|
self,
|
||||||
images: list[str | Path],
|
images: list[str | Path],
|
||||||
output_dir: Optional[Path] = None,
|
output_dir: Path | None = None,
|
||||||
credentials: dict | BatchCredentials | None = None,
|
credentials: dict | BatchCredentials | None = None,
|
||||||
recursive: bool = False,
|
recursive: bool = False,
|
||||||
progress_callback: Optional[ProgressCallback] = None,
|
progress_callback: ProgressCallback | None = None,
|
||||||
decode_func: Callable = None,
|
decode_func: Callable = None,
|
||||||
) -> BatchResult:
|
) -> BatchResult:
|
||||||
"""
|
"""
|
||||||
@@ -436,7 +435,7 @@ class BatchProcessor:
|
|||||||
self,
|
self,
|
||||||
result: BatchResult,
|
result: BatchResult,
|
||||||
process_func: Callable[[BatchItem], BatchItem],
|
process_func: Callable[[BatchItem], BatchItem],
|
||||||
progress_callback: Optional[ProgressCallback] = None,
|
progress_callback: ProgressCallback | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Execute batch processing with thread pool."""
|
"""Execute batch processing with thread pool."""
|
||||||
completed = 0
|
completed = 0
|
||||||
@@ -467,8 +466,8 @@ class BatchProcessor:
|
|||||||
def _do_encode(
|
def _do_encode(
|
||||||
self,
|
self,
|
||||||
item: BatchItem,
|
item: BatchItem,
|
||||||
message: Optional[str],
|
message: str | None,
|
||||||
file_payload: Optional[Path],
|
file_payload: Path | None,
|
||||||
creds: BatchCredentials,
|
creds: BatchCredentials,
|
||||||
compress: bool
|
compress: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -478,7 +477,7 @@ class BatchProcessor:
|
|||||||
Override this method to customize encoding behavior.
|
Override this method to customize encoding behavior.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from .encode import encode, encode_file
|
from .encode import encode
|
||||||
from .models import FilePayload
|
from .models import FilePayload
|
||||||
|
|
||||||
# Read carrier image
|
# Read carrier image
|
||||||
@@ -590,6 +589,7 @@ def batch_capacity_check(
|
|||||||
List of dicts with path, dimensions, and estimated capacity
|
List of dicts with path, dimensions, and estimated capacity
|
||||||
"""
|
"""
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .constants import MAX_IMAGE_PIXELS
|
from .constants import MAX_IMAGE_PIXELS
|
||||||
|
|
||||||
processor = BatchProcessor()
|
processor = BatchProcessor()
|
||||||
@@ -628,7 +628,7 @@ def batch_capacity_check(
|
|||||||
|
|
||||||
def _get_image_warnings(img, path: Path) -> list[str]:
|
def _get_image_warnings(img, path: Path) -> list[str]:
|
||||||
"""Generate warnings for an image."""
|
"""Generate warnings for an image."""
|
||||||
from .constants import MAX_IMAGE_PIXELS, LOSSLESS_FORMATS
|
from .constants import LOSSLESS_FORMATS, MAX_IMAGE_PIXELS
|
||||||
|
|
||||||
warnings = []
|
warnings = []
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,11 @@ INTEGRATION STATUS (v4.0.0):
|
|||||||
- ✅ Helpful error messages for channel key mismatches
|
- ✅ Helpful error messages for channel key mismatches
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import secrets
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import secrets
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
from .debug import debug
|
from .debug import debug
|
||||||
|
|
||||||
@@ -129,7 +128,7 @@ def validate_channel_key(key: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_channel_key() -> Optional[str]:
|
def get_channel_key() -> str | None:
|
||||||
"""
|
"""
|
||||||
Get the current channel key from environment or config.
|
Get the current channel key from environment or config.
|
||||||
|
|
||||||
@@ -165,7 +164,7 @@ def get_channel_key() -> Optional[str]:
|
|||||||
if key and validate_channel_key(key):
|
if key and validate_channel_key(key):
|
||||||
debug.print(f"Channel key from {config_path}: {get_channel_fingerprint(key)}")
|
debug.print(f"Channel key from {config_path}: {get_channel_fingerprint(key)}")
|
||||||
return format_channel_key(key)
|
return format_channel_key(key)
|
||||||
except (IOError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
debug.print(f"Could not read {config_path}: {e}")
|
debug.print(f"Could not read {config_path}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -216,7 +215,7 @@ def set_channel_key(key: str, location: str = 'project') -> Path:
|
|||||||
return config_path
|
return config_path
|
||||||
|
|
||||||
|
|
||||||
def clear_channel_key(location: str = 'all') -> List[Path]:
|
def clear_channel_key(location: str = 'all') -> list[Path]:
|
||||||
"""
|
"""
|
||||||
Remove channel key configuration.
|
Remove channel key configuration.
|
||||||
|
|
||||||
@@ -244,13 +243,13 @@ def clear_channel_key(location: str = 'all') -> List[Path]:
|
|||||||
path.unlink()
|
path.unlink()
|
||||||
deleted.append(path)
|
deleted.append(path)
|
||||||
debug.print(f"Removed channel key: {path}")
|
debug.print(f"Removed channel key: {path}")
|
||||||
except (IOError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
debug.print(f"Could not remove {path}: {e}")
|
debug.print(f"Could not remove {path}: {e}")
|
||||||
|
|
||||||
return deleted
|
return deleted
|
||||||
|
|
||||||
|
|
||||||
def get_channel_key_hash(key: Optional[str] = None) -> Optional[bytes]:
|
def get_channel_key_hash(key: str | None = None) -> bytes | None:
|
||||||
"""
|
"""
|
||||||
Get the channel key as a 32-byte hash suitable for key derivation.
|
Get the channel key as a 32-byte hash suitable for key derivation.
|
||||||
|
|
||||||
@@ -279,7 +278,7 @@ def get_channel_key_hash(key: Optional[str] = None) -> Optional[bytes]:
|
|||||||
return hashlib.sha256(formatted.encode('utf-8')).digest()
|
return hashlib.sha256(formatted.encode('utf-8')).digest()
|
||||||
|
|
||||||
|
|
||||||
def get_channel_fingerprint(key: Optional[str] = None) -> Optional[str]:
|
def get_channel_fingerprint(key: str | None = None) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get a short fingerprint for display purposes.
|
Get a short fingerprint for display purposes.
|
||||||
Shows first and last 4 chars with masked middle.
|
Shows first and last 4 chars with masked middle.
|
||||||
@@ -341,7 +340,7 @@ def get_channel_status() -> dict:
|
|||||||
if file_key and format_channel_key(file_key) == key:
|
if file_key and format_channel_key(file_key) == key:
|
||||||
source = str(config_path)
|
source = str(config_path)
|
||||||
break
|
break
|
||||||
except (IOError, PermissionError):
|
except (OSError, PermissionError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -409,7 +408,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
if cmd == 'generate':
|
if cmd == 'generate':
|
||||||
key = generate_channel_key()
|
key = generate_channel_key()
|
||||||
print(f"Generated channel key:")
|
print("Generated channel key:")
|
||||||
print(f" {key}")
|
print(f" {key}")
|
||||||
print()
|
print()
|
||||||
save = input("Save to config? [y/N]: ").strip().lower()
|
save = input("Save to config? [y/N]: ").strip().lower()
|
||||||
|
|||||||
@@ -8,33 +8,29 @@ Changes in v3.2.0:
|
|||||||
- Updated help text to use 'passphrase' terminology
|
- Updated help text to use 'passphrase' terminology
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from .constants import (
|
|
||||||
__version__,
|
|
||||||
MAX_MESSAGE_SIZE,
|
|
||||||
MAX_FILE_PAYLOAD_SIZE,
|
|
||||||
DEFAULT_PIN_LENGTH,
|
|
||||||
DEFAULT_PASSPHRASE_WORDS, # v3.2.0: renamed from DEFAULT_PHRASE_WORDS
|
|
||||||
)
|
|
||||||
from .compression import (
|
|
||||||
CompressionAlgorithm,
|
|
||||||
get_available_algorithms,
|
|
||||||
algorithm_name,
|
|
||||||
HAS_LZ4,
|
|
||||||
)
|
|
||||||
from .batch import (
|
from .batch import (
|
||||||
BatchProcessor,
|
BatchProcessor,
|
||||||
BatchResult,
|
|
||||||
batch_capacity_check,
|
batch_capacity_check,
|
||||||
print_batch_result,
|
print_batch_result,
|
||||||
)
|
)
|
||||||
|
from .compression import (
|
||||||
|
HAS_LZ4,
|
||||||
|
CompressionAlgorithm,
|
||||||
|
algorithm_name,
|
||||||
|
get_available_algorithms,
|
||||||
|
)
|
||||||
|
from .constants import (
|
||||||
|
DEFAULT_PASSPHRASE_WORDS, # v3.2.0: renamed from DEFAULT_PHRASE_WORDS
|
||||||
|
DEFAULT_PIN_LENGTH,
|
||||||
|
MAX_FILE_PAYLOAD_SIZE,
|
||||||
|
MAX_MESSAGE_SIZE,
|
||||||
|
__version__,
|
||||||
|
)
|
||||||
|
|
||||||
# Click context settings
|
# Click context settings
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
||||||
@@ -426,12 +422,12 @@ def info(ctx):
|
|||||||
click.echo(json.dumps(info_data, indent=2))
|
click.echo(json.dumps(info_data, indent=2))
|
||||||
else:
|
else:
|
||||||
click.echo(f"Stegasoo v{__version__}")
|
click.echo(f"Stegasoo v{__version__}")
|
||||||
click.echo(f"\nCompression algorithms:")
|
click.echo("\nCompression algorithms:")
|
||||||
for algo in get_available_algorithms():
|
for algo in get_available_algorithms():
|
||||||
click.echo(f" • {algorithm_name(algo)}")
|
click.echo(f" • {algorithm_name(algo)}")
|
||||||
if not HAS_LZ4:
|
if not HAS_LZ4:
|
||||||
click.echo(" (install 'lz4' for LZ4 support)")
|
click.echo(" (install 'lz4' for LZ4 support)")
|
||||||
click.echo(f"\nLimits:")
|
click.echo("\nLimits:")
|
||||||
click.echo(f" • Max message: {MAX_MESSAGE_SIZE:,} bytes")
|
click.echo(f" • Max message: {MAX_MESSAGE_SIZE:,} bytes")
|
||||||
click.echo(f" • Max file payload: {MAX_FILE_PAYLOAD_SIZE:,} bytes")
|
click.echo(f" • Max file payload: {MAX_FILE_PAYLOAD_SIZE:,} bytes")
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ Provides transparent compression/decompression for payloads before encryption.
|
|||||||
Supports multiple algorithms with automatic detection on decompression.
|
Supports multiple algorithms with automatic detection on decompression.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import zlib
|
|
||||||
import struct
|
import struct
|
||||||
|
import zlib
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
# Optional LZ4 support (faster, slightly worse ratio)
|
# Optional LZ4 support (faster, slightly worse ratio)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ BREAKING CHANGES in v3.2.0:
|
|||||||
- Renamed day_phrase → passphrase throughout codebase
|
- Renamed day_phrase → passphrase throughout codebase
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -199,7 +198,7 @@ def get_bip39_words() -> list[str]:
|
|||||||
"Please ensure bip39-words.txt is in the data directory."
|
"Please ensure bip39-words.txt is in the data directory."
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(wordlist_path, 'r') as f:
|
with open(wordlist_path) as f:
|
||||||
return [line.strip() for line in f if line.strip()]
|
return [line.strip() for line in f if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,38 +15,40 @@ BREAKING CHANGES in v3.2.0:
|
|||||||
- Renamed day_phrase → passphrase (no daily rotation needed)
|
- Renamed day_phrase → passphrase (no daily rotation needed)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import io
|
||||||
import secrets
|
import secrets
|
||||||
import struct
|
import struct
|
||||||
import json
|
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
MAGIC_HEADER, FORMAT_VERSION,
|
ARGON2_MEMORY_COST,
|
||||||
SALT_SIZE, IV_SIZE, TAG_SIZE,
|
ARGON2_PARALLELISM,
|
||||||
ARGON2_TIME_COST, ARGON2_MEMORY_COST, ARGON2_PARALLELISM,
|
ARGON2_TIME_COST,
|
||||||
PBKDF2_ITERATIONS,
|
FORMAT_VERSION,
|
||||||
PAYLOAD_TEXT, PAYLOAD_FILE,
|
IV_SIZE,
|
||||||
|
MAGIC_HEADER,
|
||||||
MAX_FILENAME_LENGTH,
|
MAX_FILENAME_LENGTH,
|
||||||
|
PAYLOAD_FILE,
|
||||||
|
PAYLOAD_TEXT,
|
||||||
|
PBKDF2_ITERATIONS,
|
||||||
|
SALT_SIZE,
|
||||||
|
TAG_SIZE,
|
||||||
)
|
)
|
||||||
from .models import FilePayload, DecodeResult
|
from .exceptions import DecryptionError, EncryptionError, InvalidHeaderError, KeyDerivationError
|
||||||
from .exceptions import (
|
from .models import DecodeResult, FilePayload
|
||||||
EncryptionError, DecryptionError, KeyDerivationError, InvalidHeaderError
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check for Argon2 availability
|
# Check for Argon2 availability
|
||||||
try:
|
try:
|
||||||
from argon2.low_level import hash_secret_raw, Type
|
from argon2.low_level import Type, hash_secret_raw
|
||||||
HAS_ARGON2 = True
|
HAS_ARGON2 = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_ARGON2 = False
|
HAS_ARGON2 = False
|
||||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -57,7 +59,7 @@ except ImportError:
|
|||||||
CHANNEL_KEY_AUTO = "auto"
|
CHANNEL_KEY_AUTO = "auto"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_channel_key(channel_key: Optional[Union[str, bool]]) -> Optional[bytes]:
|
def _resolve_channel_key(channel_key: str | bool | None) -> bytes | None:
|
||||||
"""
|
"""
|
||||||
Resolve channel key parameter to actual key hash.
|
Resolve channel key parameter to actual key hash.
|
||||||
|
|
||||||
@@ -121,8 +123,8 @@ def derive_hybrid_key(
|
|||||||
passphrase: str,
|
passphrase: str,
|
||||||
salt: bytes,
|
salt: bytes,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
Derive encryption key from multiple factors.
|
Derive encryption key from multiple factors.
|
||||||
@@ -206,8 +208,8 @@ def derive_pixel_key(
|
|||||||
photo_data: bytes,
|
photo_data: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
Derive key for pseudo-random pixel selection.
|
Derive key for pseudo-random pixel selection.
|
||||||
@@ -247,7 +249,7 @@ def derive_pixel_key(
|
|||||||
|
|
||||||
|
|
||||||
def _pack_payload(
|
def _pack_payload(
|
||||||
content: Union[str, bytes, FilePayload],
|
content: str | bytes | FilePayload,
|
||||||
) -> tuple[bytes, int]:
|
) -> tuple[bytes, int]:
|
||||||
"""
|
"""
|
||||||
Pack payload with type marker and metadata.
|
Pack payload with type marker and metadata.
|
||||||
@@ -359,12 +361,12 @@ FLAG_CHANNEL_KEY = 0x01 # Set if encoded with a channel key
|
|||||||
|
|
||||||
|
|
||||||
def encrypt_message(
|
def encrypt_message(
|
||||||
message: Union[str, bytes, FilePayload],
|
message: str | bytes | FilePayload,
|
||||||
photo_data: bytes,
|
photo_data: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
Encrypt message or file using AES-256-GCM with hybrid key derivation.
|
Encrypt message or file using AES-256-GCM with hybrid key derivation.
|
||||||
@@ -438,7 +440,7 @@ def encrypt_message(
|
|||||||
raise EncryptionError(f"Encryption failed: {e}") from e
|
raise EncryptionError(f"Encryption failed: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
def parse_header(encrypted_data: bytes) -> Optional[dict]:
|
def parse_header(encrypted_data: bytes) -> dict | None:
|
||||||
"""
|
"""
|
||||||
Parse the header from encrypted data.
|
Parse the header from encrypted data.
|
||||||
|
|
||||||
@@ -488,8 +490,8 @@ def decrypt_message(
|
|||||||
photo_data: bytes,
|
photo_data: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> DecodeResult:
|
) -> DecodeResult:
|
||||||
"""
|
"""
|
||||||
Decrypt message (v4.0.0 - with channel key support).
|
Decrypt message (v4.0.0 - with channel key support).
|
||||||
@@ -566,8 +568,8 @@ def decrypt_message_text(
|
|||||||
photo_data: bytes,
|
photo_data: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Decrypt message and return as text string.
|
Decrypt message and return as text string.
|
||||||
@@ -613,7 +615,7 @@ def has_argon2() -> bool:
|
|||||||
# CHANNEL KEY UTILITIES (exposed for convenience)
|
# CHANNEL KEY UTILITIES (exposed for convenience)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
def get_active_channel_key() -> Optional[str]:
|
def get_active_channel_key() -> str | None:
|
||||||
"""
|
"""
|
||||||
Get the currently configured channel key (if any).
|
Get the currently configured channel key (if any).
|
||||||
|
|
||||||
@@ -624,7 +626,7 @@ def get_active_channel_key() -> Optional[str]:
|
|||||||
return get_channel_key()
|
return get_channel_key()
|
||||||
|
|
||||||
|
|
||||||
def get_channel_fingerprint(key: Optional[str] = None) -> Optional[str]:
|
def get_channel_fingerprint(key: str | None = None) -> str | None:
|
||||||
"""
|
"""
|
||||||
Get a display-safe fingerprint of a channel key.
|
Get a display-safe fingerprint of a channel key.
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ v3.2.0-patch2 Changes:
|
|||||||
Requires: scipy (for PNG mode), optionally jpegio (for JPEG mode)
|
Requires: scipy (for PNG mode), optionally jpegio (for JPEG mode)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import gc
|
||||||
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import struct
|
import struct
|
||||||
import hashlib
|
|
||||||
import gc
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Tuple
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -206,7 +205,7 @@ def _extract_y_channel(image_data: bytes) -> np.ndarray:
|
|||||||
return np.array(Y, dtype=np.float64, copy=True, order='C')
|
return np.array(Y, dtype=np.float64, copy=True, order='C')
|
||||||
|
|
||||||
|
|
||||||
def _pad_to_blocks(image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
|
def _pad_to_blocks(image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]:
|
||||||
h, w = image.shape
|
h, w = image.shape
|
||||||
new_h = ((h + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
|
new_h = ((h + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
|
||||||
new_w = ((w + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
|
new_w = ((w + BLOCK_SIZE - 1) // BLOCK_SIZE) * BLOCK_SIZE
|
||||||
@@ -230,7 +229,7 @@ def _pad_to_blocks(image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
|
|||||||
return padded, (h, w)
|
return padded, (h, w)
|
||||||
|
|
||||||
|
|
||||||
def _unpad_image(image: np.ndarray, original_size: Tuple[int, int]) -> np.ndarray:
|
def _unpad_image(image: np.ndarray, original_size: tuple[int, int]) -> np.ndarray:
|
||||||
h, w = original_size
|
h, w = original_size
|
||||||
return np.array(image[:h, :w], dtype=np.float64, copy=True, order='C')
|
return np.array(image[:h, :w], dtype=np.float64, copy=True, order='C')
|
||||||
|
|
||||||
@@ -282,7 +281,7 @@ def _save_color_image(rgb_array: np.ndarray, output_format: str = OUTPUT_FORMAT_
|
|||||||
return buffer.getvalue()
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def _rgb_to_ycbcr(rgb: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
def _rgb_to_ycbcr(rgb: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||||
R = rgb[:, :, 0].astype(np.float64)
|
R = rgb[:, :, 0].astype(np.float64)
|
||||||
G = rgb[:, :, 1].astype(np.float64)
|
G = rgb[:, :, 1].astype(np.float64)
|
||||||
B = rgb[:, :, 2].astype(np.float64)
|
B = rgb[:, :, 2].astype(np.float64)
|
||||||
@@ -310,7 +309,7 @@ def _create_header(data_length: int, flags: int = 0) -> bytes:
|
|||||||
return struct.pack('>4sBBI', DCT_MAGIC, 1, flags, data_length)
|
return struct.pack('>4sBBI', DCT_MAGIC, 1, flags, data_length)
|
||||||
|
|
||||||
|
|
||||||
def _parse_header(header_bits: list) -> Tuple[int, int, int]:
|
def _parse_header(header_bits: list) -> tuple[int, int, int]:
|
||||||
if len(header_bits) < HEADER_SIZE * 8:
|
if len(header_bits) < HEADER_SIZE * 8:
|
||||||
raise ValueError("Insufficient header data")
|
raise ValueError("Insufficient header data")
|
||||||
|
|
||||||
@@ -332,8 +331,8 @@ def _parse_header(header_bits: list) -> Tuple[int, int, int]:
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
def _jpegio_bytes_to_file(data: bytes, suffix: str = '.jpg') -> str:
|
def _jpegio_bytes_to_file(data: bytes, suffix: str = '.jpg') -> str:
|
||||||
import tempfile
|
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
fd, path = tempfile.mkstemp(suffix=suffix)
|
fd, path = tempfile.mkstemp(suffix=suffix)
|
||||||
try:
|
try:
|
||||||
os.write(fd, data)
|
os.write(fd, data)
|
||||||
@@ -366,7 +365,7 @@ def _jpegio_create_header(data_length: int, flags: int = 0) -> bytes:
|
|||||||
return struct.pack('>4sBBI', JPEGIO_MAGIC, 1, flags, data_length)
|
return struct.pack('>4sBBI', JPEGIO_MAGIC, 1, flags, data_length)
|
||||||
|
|
||||||
|
|
||||||
def _jpegio_parse_header(header_bytes: bytes) -> Tuple[int, int, int]:
|
def _jpegio_parse_header(header_bytes: bytes) -> tuple[int, int, int]:
|
||||||
if len(header_bytes) < HEADER_SIZE:
|
if len(header_bytes) < HEADER_SIZE:
|
||||||
raise ValueError("Insufficient header data")
|
raise ValueError("Insufficient header data")
|
||||||
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes[:HEADER_SIZE])
|
magic, version, flags, length = struct.unpack('>4sBBI', header_bytes[:HEADER_SIZE])
|
||||||
@@ -455,7 +454,7 @@ def embed_in_dct(
|
|||||||
seed: bytes,
|
seed: bytes,
|
||||||
output_format: str = OUTPUT_FORMAT_PNG,
|
output_format: str = OUTPUT_FORMAT_PNG,
|
||||||
color_mode: str = 'color',
|
color_mode: str = 'color',
|
||||||
) -> Tuple[bytes, DCTEmbedStats]:
|
) -> tuple[bytes, DCTEmbedStats]:
|
||||||
"""Embed data using DCT coefficient modification."""
|
"""Embed data using DCT coefficient modification."""
|
||||||
if output_format not in (OUTPUT_FORMAT_PNG, OUTPUT_FORMAT_JPEG):
|
if output_format not in (OUTPUT_FORMAT_PNG, OUTPUT_FORMAT_JPEG):
|
||||||
raise ValueError(f"Invalid output format: {output_format}")
|
raise ValueError(f"Invalid output format: {output_format}")
|
||||||
@@ -476,7 +475,7 @@ def _embed_scipy_dct_safe(
|
|||||||
seed: bytes,
|
seed: bytes,
|
||||||
output_format: str,
|
output_format: str,
|
||||||
color_mode: str = 'color',
|
color_mode: str = 'color',
|
||||||
) -> Tuple[bytes, DCTEmbedStats]:
|
) -> tuple[bytes, DCTEmbedStats]:
|
||||||
"""
|
"""
|
||||||
Embed using scipy DCT with safe memory handling.
|
Embed using scipy DCT with safe memory handling.
|
||||||
|
|
||||||
@@ -688,10 +687,10 @@ def _embed_jpegio(
|
|||||||
carrier_image: bytes,
|
carrier_image: bytes,
|
||||||
seed: bytes,
|
seed: bytes,
|
||||||
color_mode: str = 'color',
|
color_mode: str = 'color',
|
||||||
) -> Tuple[bytes, DCTEmbedStats]:
|
) -> tuple[bytes, DCTEmbedStats]:
|
||||||
"""Embed using jpegio for proper JPEG coefficient modification."""
|
"""Embed using jpegio for proper JPEG coefficient modification."""
|
||||||
import tempfile
|
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
# Normalize JPEG to avoid crashes with quality=100 images
|
# Normalize JPEG to avoid crashes with quality=100 images
|
||||||
carrier_image = _normalize_jpeg_for_jpegio(carrier_image)
|
carrier_image = _normalize_jpeg_for_jpegio(carrier_image)
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ Debugging, logging, and performance monitoring tools.
|
|||||||
Can be disabled for production use.
|
Can be disabled for production use.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Callable, Any, Optional, Dict, Union
|
from typing import Any
|
||||||
import sys
|
|
||||||
|
|
||||||
# Global debug configuration
|
# Global debug configuration
|
||||||
DEBUG_ENABLED = False # Set to True to enable debug output
|
DEBUG_ENABLED = False # Set to True to enable debug output
|
||||||
@@ -89,11 +90,12 @@ def validate_assertion(condition: bool, message: str) -> None:
|
|||||||
raise AssertionError(f"Validation failed: {message}")
|
raise AssertionError(f"Validation failed: {message}")
|
||||||
|
|
||||||
|
|
||||||
def memory_usage() -> Dict[str, Union[float, str]]:
|
def memory_usage() -> dict[str, float | str]:
|
||||||
"""Get current memory usage (if psutil is available)."""
|
"""Get current memory usage (if psutil is available)."""
|
||||||
try:
|
try:
|
||||||
import psutil
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import psutil
|
||||||
process = psutil.Process(os.getpid())
|
process = psutil.Process(os.getpid())
|
||||||
mem_info = process.memory_info()
|
mem_info = process.memory_info()
|
||||||
|
|
||||||
@@ -153,7 +155,7 @@ class Debug:
|
|||||||
"""Runtime validation assertion."""
|
"""Runtime validation assertion."""
|
||||||
validate_assertion(condition, message)
|
validate_assertion(condition, message)
|
||||||
|
|
||||||
def memory(self) -> Dict[str, Union[float, str]]:
|
def memory(self) -> dict[str, float | str]:
|
||||||
"""Get current memory usage."""
|
"""Get current memory usage."""
|
||||||
return memory_usage()
|
return memory_usage()
|
||||||
|
|
||||||
|
|||||||
@@ -8,21 +8,20 @@ Changes in v4.0.0:
|
|||||||
- Improved error messages for channel key mismatches
|
- Improved error messages for channel key mismatches
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional, Union
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .models import DecodeInput, DecodeResult
|
from .constants import EMBED_MODE_AUTO
|
||||||
from .crypto import decrypt_message
|
from .crypto import decrypt_message
|
||||||
|
from .debug import debug
|
||||||
|
from .exceptions import DecryptionError, ExtractionError
|
||||||
|
from .models import DecodeResult
|
||||||
from .steganography import extract_from_image
|
from .steganography import extract_from_image
|
||||||
from .validation import (
|
from .validation import (
|
||||||
require_valid_image,
|
|
||||||
require_security_factors,
|
require_security_factors,
|
||||||
|
require_valid_image,
|
||||||
require_valid_pin,
|
require_valid_pin,
|
||||||
require_valid_rsa_key,
|
require_valid_rsa_key,
|
||||||
)
|
)
|
||||||
from .constants import EMBED_MODE_AUTO
|
|
||||||
from .exceptions import ExtractionError, DecryptionError
|
|
||||||
from .debug import debug
|
|
||||||
|
|
||||||
|
|
||||||
def decode(
|
def decode(
|
||||||
@@ -30,10 +29,10 @@ def decode(
|
|||||||
reference_photo: bytes,
|
reference_photo: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
embed_mode: str = EMBED_MODE_AUTO,
|
embed_mode: str = EMBED_MODE_AUTO,
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> DecodeResult:
|
) -> DecodeResult:
|
||||||
"""
|
"""
|
||||||
Decode a message or file from a stego image.
|
Decode a message or file from a stego image.
|
||||||
@@ -122,12 +121,12 @@ def decode_file(
|
|||||||
stego_image: bytes,
|
stego_image: bytes,
|
||||||
reference_photo: bytes,
|
reference_photo: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
output_path: Optional[Path] = None,
|
output_path: Path | None = None,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
embed_mode: str = EMBED_MODE_AUTO,
|
embed_mode: str = EMBED_MODE_AUTO,
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""
|
"""
|
||||||
Decode a file from a stego image and save it.
|
Decode a file from a stego image and save it.
|
||||||
@@ -182,10 +181,10 @@ def decode_text(
|
|||||||
reference_photo: bytes,
|
reference_photo: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
embed_mode: str = EMBED_MODE_AUTO,
|
embed_mode: str = EMBED_MODE_AUTO,
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Decode a text message from a stego image.
|
Decode a text message from a stego image.
|
||||||
|
|||||||
@@ -7,37 +7,36 @@ Changes in v4.0.0:
|
|||||||
- Added channel_key parameter for deployment/group isolation
|
- Added channel_key parameter for deployment/group isolation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional, Union
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .models import EncodeInput, EncodeResult, FilePayload
|
from .constants import EMBED_MODE_LSB
|
||||||
from .crypto import encrypt_message, derive_pixel_key
|
from .crypto import derive_pixel_key, encrypt_message
|
||||||
|
from .debug import debug
|
||||||
|
from .models import EncodeResult, FilePayload
|
||||||
from .steganography import embed_in_image
|
from .steganography import embed_in_image
|
||||||
|
from .utils import generate_filename
|
||||||
from .validation import (
|
from .validation import (
|
||||||
require_valid_payload,
|
|
||||||
require_valid_image,
|
|
||||||
require_security_factors,
|
require_security_factors,
|
||||||
|
require_valid_image,
|
||||||
|
require_valid_payload,
|
||||||
require_valid_pin,
|
require_valid_pin,
|
||||||
require_valid_rsa_key,
|
require_valid_rsa_key,
|
||||||
)
|
)
|
||||||
from .utils import generate_filename
|
|
||||||
from .constants import EMBED_MODE_LSB
|
|
||||||
from .debug import debug
|
|
||||||
|
|
||||||
|
|
||||||
def encode(
|
def encode(
|
||||||
message: Union[str, bytes, FilePayload],
|
message: str | bytes | FilePayload,
|
||||||
reference_photo: bytes,
|
reference_photo: bytes,
|
||||||
carrier_image: bytes,
|
carrier_image: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
output_format: Optional[str] = None,
|
output_format: str | None = None,
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
dct_output_format: str = "png",
|
dct_output_format: str = "png",
|
||||||
dct_color_mode: str = "grayscale",
|
dct_color_mode: str = "grayscale",
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> EncodeResult:
|
) -> EncodeResult:
|
||||||
"""
|
"""
|
||||||
Encode a message or file into an image.
|
Encode a message or file into an image.
|
||||||
@@ -148,19 +147,19 @@ def encode(
|
|||||||
|
|
||||||
|
|
||||||
def encode_file(
|
def encode_file(
|
||||||
filepath: Union[str, Path],
|
filepath: str | Path,
|
||||||
reference_photo: bytes,
|
reference_photo: bytes,
|
||||||
carrier_image: bytes,
|
carrier_image: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
output_format: Optional[str] = None,
|
output_format: str | None = None,
|
||||||
filename_override: Optional[str] = None,
|
filename_override: str | None = None,
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
dct_output_format: str = "png",
|
dct_output_format: str = "png",
|
||||||
dct_color_mode: str = "grayscale",
|
dct_color_mode: str = "grayscale",
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> EncodeResult:
|
) -> EncodeResult:
|
||||||
"""
|
"""
|
||||||
Encode a file into an image.
|
Encode a file into an image.
|
||||||
@@ -210,14 +209,14 @@ def encode_bytes(
|
|||||||
carrier_image: bytes,
|
carrier_image: bytes,
|
||||||
passphrase: str,
|
passphrase: str,
|
||||||
pin: str = "",
|
pin: str = "",
|
||||||
rsa_key_data: Optional[bytes] = None,
|
rsa_key_data: bytes | None = None,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
output_format: Optional[str] = None,
|
output_format: str | None = None,
|
||||||
mime_type: Optional[str] = None,
|
mime_type: str | None = None,
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
dct_output_format: str = "png",
|
dct_output_format: str = "png",
|
||||||
dct_color_mode: str = "grayscale",
|
dct_color_mode: str = "grayscale",
|
||||||
channel_key: Optional[Union[str, bool]] = None,
|
channel_key: str | bool | None = None,
|
||||||
) -> EncodeResult:
|
) -> EncodeResult:
|
||||||
"""
|
"""
|
||||||
Encode raw bytes with metadata into an image.
|
Encode raw bytes with metadata into an image.
|
||||||
|
|||||||
@@ -4,23 +4,25 @@ Stegasoo Generate Module (v3.2.0)
|
|||||||
Public API for generating credentials (PINs, passphrases, RSA keys).
|
Public API for generating credentials (PINs, passphrases, RSA keys).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .keygen import (
|
|
||||||
generate_pin as _generate_pin,
|
|
||||||
generate_phrase,
|
|
||||||
generate_rsa_key as _generate_rsa_key,
|
|
||||||
export_rsa_key_pem,
|
|
||||||
load_rsa_key,
|
|
||||||
)
|
|
||||||
from .models import Credentials
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
DEFAULT_PIN_LENGTH,
|
|
||||||
DEFAULT_PASSPHRASE_WORDS,
|
DEFAULT_PASSPHRASE_WORDS,
|
||||||
|
DEFAULT_PIN_LENGTH,
|
||||||
DEFAULT_RSA_BITS,
|
DEFAULT_RSA_BITS,
|
||||||
)
|
)
|
||||||
from .debug import debug
|
from .debug import debug
|
||||||
|
from .keygen import (
|
||||||
|
export_rsa_key_pem,
|
||||||
|
generate_phrase,
|
||||||
|
load_rsa_key,
|
||||||
|
)
|
||||||
|
from .keygen import (
|
||||||
|
generate_pin as _generate_pin,
|
||||||
|
)
|
||||||
|
from .keygen import (
|
||||||
|
generate_rsa_key as _generate_rsa_key,
|
||||||
|
)
|
||||||
|
from .models import Credentials
|
||||||
|
|
||||||
# Re-export from keygen for convenience
|
# Re-export from keygen for convenience
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -78,7 +80,7 @@ def generate_passphrase(words: int = DEFAULT_PASSPHRASE_WORDS) -> str:
|
|||||||
|
|
||||||
def generate_rsa_key(
|
def generate_rsa_key(
|
||||||
bits: int = DEFAULT_RSA_BITS,
|
bits: int = DEFAULT_RSA_BITS,
|
||||||
password: Optional[str] = None
|
password: str | None = None
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate an RSA private key in PEM format.
|
Generate an RSA private key in PEM format.
|
||||||
@@ -106,7 +108,7 @@ def generate_credentials(
|
|||||||
pin_length: int = DEFAULT_PIN_LENGTH,
|
pin_length: int = DEFAULT_PIN_LENGTH,
|
||||||
rsa_bits: int = DEFAULT_RSA_BITS,
|
rsa_bits: int = DEFAULT_RSA_BITS,
|
||||||
passphrase_words: int = DEFAULT_PASSPHRASE_WORDS,
|
passphrase_words: int = DEFAULT_PASSPHRASE_WORDS,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
) -> Credentials:
|
) -> Credentials:
|
||||||
"""
|
"""
|
||||||
Generate a complete set of credentials.
|
Generate a complete set of credentials.
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ Stegasoo Image Utilities (v3.2.0)
|
|||||||
Functions for analyzing images and comparing capacity.
|
Functions for analyzing images and comparing capacity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .models import ImageInfo, CapacityComparison
|
from .constants import EMBED_MODE_LSB
|
||||||
from .steganography import calculate_capacity, has_dct_support
|
|
||||||
from .constants import EMBED_MODE_LSB, EMBED_MODE_DCT
|
|
||||||
from .debug import debug
|
from .debug import debug
|
||||||
|
from .models import CapacityComparison, ImageInfo
|
||||||
|
from .steganography import calculate_capacity, has_dct_support
|
||||||
|
|
||||||
|
|
||||||
def get_image_info(image_data: bytes) -> ImageInfo:
|
def get_image_info(image_data: bytes) -> ImageInfo:
|
||||||
@@ -69,7 +69,7 @@ def get_image_info(image_data: bytes) -> ImageInfo:
|
|||||||
|
|
||||||
def compare_capacity(
|
def compare_capacity(
|
||||||
carrier_image: bytes,
|
carrier_image: bytes,
|
||||||
reference_photo: Optional[bytes] = None,
|
reference_photo: bytes | None = None,
|
||||||
) -> CapacityComparison:
|
) -> CapacityComparison:
|
||||||
"""
|
"""
|
||||||
Compare embedding capacity between LSB and DCT modes.
|
Compare embedding capacity between LSB and DCT modes.
|
||||||
|
|||||||
@@ -10,24 +10,28 @@ Changes in v3.2.0:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Optional, Dict, Union
|
|
||||||
|
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
DAY_NAMES,
|
DAY_NAMES,
|
||||||
MIN_PIN_LENGTH, MAX_PIN_LENGTH, DEFAULT_PIN_LENGTH,
|
DEFAULT_PASSPHRASE_WORDS,
|
||||||
MIN_PASSPHRASE_WORDS, MAX_PASSPHRASE_WORDS, DEFAULT_PASSPHRASE_WORDS,
|
DEFAULT_PIN_LENGTH,
|
||||||
MIN_RSA_BITS, VALID_RSA_SIZES, DEFAULT_RSA_BITS,
|
DEFAULT_RSA_BITS,
|
||||||
|
MAX_PASSPHRASE_WORDS,
|
||||||
|
MAX_PIN_LENGTH,
|
||||||
|
MIN_PASSPHRASE_WORDS,
|
||||||
|
MIN_PIN_LENGTH,
|
||||||
|
VALID_RSA_SIZES,
|
||||||
get_wordlist,
|
get_wordlist,
|
||||||
)
|
)
|
||||||
from .models import Credentials, KeyInfo
|
|
||||||
from .exceptions import KeyGenerationError, KeyPasswordError
|
|
||||||
from .debug import debug
|
from .debug import debug
|
||||||
|
from .exceptions import KeyGenerationError, KeyPasswordError
|
||||||
|
from .models import Credentials, KeyInfo
|
||||||
|
|
||||||
|
|
||||||
def generate_pin(length: int = DEFAULT_PIN_LENGTH) -> str:
|
def generate_pin(length: int = DEFAULT_PIN_LENGTH) -> str:
|
||||||
@@ -92,7 +96,7 @@ def generate_phrase(words_per_phrase: int = DEFAULT_PASSPHRASE_WORDS) -> str:
|
|||||||
generate_passphrase = generate_phrase
|
generate_passphrase = generate_phrase
|
||||||
|
|
||||||
|
|
||||||
def generate_day_phrases(words_per_phrase: int = DEFAULT_PASSPHRASE_WORDS) -> Dict[str, str]:
|
def generate_day_phrases(words_per_phrase: int = DEFAULT_PASSPHRASE_WORDS) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Generate phrases for all days of the week.
|
Generate phrases for all days of the week.
|
||||||
|
|
||||||
@@ -162,7 +166,7 @@ def generate_rsa_key(bits: int = DEFAULT_RSA_BITS) -> rsa.RSAPrivateKey:
|
|||||||
|
|
||||||
def export_rsa_key_pem(
|
def export_rsa_key_pem(
|
||||||
private_key: rsa.RSAPrivateKey,
|
private_key: rsa.RSAPrivateKey,
|
||||||
password: Optional[str] = None
|
password: str | None = None
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
Export RSA key to PEM format.
|
Export RSA key to PEM format.
|
||||||
@@ -182,10 +186,7 @@ def export_rsa_key_pem(
|
|||||||
"""
|
"""
|
||||||
debug.validate(private_key is not None, "Private key cannot be None")
|
debug.validate(private_key is not None, "Private key cannot be None")
|
||||||
|
|
||||||
encryption_algorithm: Union[
|
encryption_algorithm: serialization.BestAvailableEncryption | serialization.NoEncryption
|
||||||
serialization.BestAvailableEncryption,
|
|
||||||
serialization.NoEncryption
|
|
||||||
]
|
|
||||||
|
|
||||||
if password:
|
if password:
|
||||||
encryption_algorithm = serialization.BestAvailableEncryption(password.encode())
|
encryption_algorithm = serialization.BestAvailableEncryption(password.encode())
|
||||||
@@ -203,7 +204,7 @@ def export_rsa_key_pem(
|
|||||||
|
|
||||||
def load_rsa_key(
|
def load_rsa_key(
|
||||||
key_data: bytes,
|
key_data: bytes,
|
||||||
password: Optional[str] = None
|
password: str | None = None
|
||||||
) -> rsa.RSAPrivateKey:
|
) -> rsa.RSAPrivateKey:
|
||||||
"""
|
"""
|
||||||
Load RSA private key from PEM data.
|
Load RSA private key from PEM data.
|
||||||
@@ -253,7 +254,7 @@ def load_rsa_key(
|
|||||||
raise KeyGenerationError(f"Could not load RSA key: {e}") from e
|
raise KeyGenerationError(f"Could not load RSA key: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
def get_key_info(key_data: bytes, password: Optional[str] = None) -> KeyInfo:
|
def get_key_info(key_data: bytes, password: str | None = None) -> KeyInfo:
|
||||||
"""
|
"""
|
||||||
Get information about an RSA key.
|
Get information about an RSA key.
|
||||||
|
|
||||||
@@ -293,7 +294,7 @@ def generate_credentials(
|
|||||||
pin_length: int = DEFAULT_PIN_LENGTH,
|
pin_length: int = DEFAULT_PIN_LENGTH,
|
||||||
rsa_bits: int = DEFAULT_RSA_BITS,
|
rsa_bits: int = DEFAULT_RSA_BITS,
|
||||||
passphrase_words: int = DEFAULT_PASSPHRASE_WORDS,
|
passphrase_words: int = DEFAULT_PASSPHRASE_WORDS,
|
||||||
rsa_password: Optional[str] = None,
|
rsa_password: str | None = None,
|
||||||
) -> Credentials:
|
) -> Credentials:
|
||||||
"""
|
"""
|
||||||
Generate a complete set of credentials.
|
Generate a complete set of credentials.
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ Changes in v3.2.0:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import date
|
|
||||||
from typing import Optional, Union, List
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -24,13 +22,13 @@ class Credentials:
|
|||||||
v3.2.0: Simplified to use single passphrase instead of daily rotation.
|
v3.2.0: Simplified to use single passphrase instead of daily rotation.
|
||||||
"""
|
"""
|
||||||
passphrase: str # Single passphrase (no daily rotation)
|
passphrase: str # Single passphrase (no daily rotation)
|
||||||
pin: Optional[str] = None
|
pin: str | None = None
|
||||||
rsa_key_pem: Optional[str] = None
|
rsa_key_pem: str | None = None
|
||||||
rsa_bits: Optional[int] = None
|
rsa_bits: int | None = None
|
||||||
words_per_passphrase: int = 4 # Increased from 3 in v3.1.0
|
words_per_passphrase: int = 4 # Increased from 3 in v3.1.0
|
||||||
|
|
||||||
# Optional: backup passphrases for multi-factor or rotation
|
# Optional: backup passphrases for multi-factor or rotation
|
||||||
backup_passphrases: Optional[list[str]] = None
|
backup_passphrases: list[str] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def passphrase_entropy(self) -> int:
|
def passphrase_entropy(self) -> int:
|
||||||
@@ -68,17 +66,17 @@ class FilePayload:
|
|||||||
"""Represents a file to be embedded."""
|
"""Represents a file to be embedded."""
|
||||||
data: bytes
|
data: bytes
|
||||||
filename: str
|
filename: str
|
||||||
mime_type: Optional[str] = None
|
mime_type: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
return len(self.data)
|
return len(self.data)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, filepath: str, filename: Optional[str] = None) -> 'FilePayload':
|
def from_file(cls, filepath: str, filename: str | None = None) -> 'FilePayload':
|
||||||
"""Create FilePayload from a file path."""
|
"""Create FilePayload from a file path."""
|
||||||
from pathlib import Path
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
path = Path(filepath)
|
path = Path(filepath)
|
||||||
data = path.read_bytes()
|
data = path.read_bytes()
|
||||||
@@ -95,13 +93,13 @@ class EncodeInput:
|
|||||||
|
|
||||||
v3.2.0: Removed date_str (date no longer used in crypto).
|
v3.2.0: Removed date_str (date no longer used in crypto).
|
||||||
"""
|
"""
|
||||||
message: Union[str, bytes, FilePayload] # Text, raw bytes, or file
|
message: str | bytes | FilePayload # Text, raw bytes, or file
|
||||||
reference_photo: bytes
|
reference_photo: bytes
|
||||||
carrier_image: bytes
|
carrier_image: bytes
|
||||||
passphrase: str # Renamed from day_phrase
|
passphrase: str # Renamed from day_phrase
|
||||||
pin: str = ""
|
pin: str = ""
|
||||||
rsa_key_data: Optional[bytes] = None
|
rsa_key_data: bytes | None = None
|
||||||
rsa_password: Optional[str] = None
|
rsa_password: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -116,7 +114,7 @@ class EncodeResult:
|
|||||||
pixels_modified: int
|
pixels_modified: int
|
||||||
total_pixels: int
|
total_pixels: int
|
||||||
capacity_used: float # 0.0 - 1.0
|
capacity_used: float # 0.0 - 1.0
|
||||||
date_used: Optional[str] = None # Cosmetic only (for filename organization)
|
date_used: str | None = None # Cosmetic only (for filename organization)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def capacity_percent(self) -> float:
|
def capacity_percent(self) -> float:
|
||||||
@@ -135,8 +133,8 @@ class DecodeInput:
|
|||||||
reference_photo: bytes
|
reference_photo: bytes
|
||||||
passphrase: str # Renamed from day_phrase
|
passphrase: str # Renamed from day_phrase
|
||||||
pin: str = ""
|
pin: str = ""
|
||||||
rsa_key_data: Optional[bytes] = None
|
rsa_key_data: bytes | None = None
|
||||||
rsa_password: Optional[str] = None
|
rsa_password: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -147,11 +145,11 @@ class DecodeResult:
|
|||||||
v3.2.0: date_encoded is always None (date removed from crypto).
|
v3.2.0: date_encoded is always None (date removed from crypto).
|
||||||
"""
|
"""
|
||||||
payload_type: str # 'text' or 'file'
|
payload_type: str # 'text' or 'file'
|
||||||
message: Optional[str] = None # For text payloads
|
message: str | None = None # For text payloads
|
||||||
file_data: Optional[bytes] = None # For file payloads
|
file_data: bytes | None = None # For file payloads
|
||||||
filename: Optional[str] = None # Original filename for file payloads
|
filename: str | None = None # Original filename for file payloads
|
||||||
mime_type: Optional[str] = None # MIME type hint
|
mime_type: str | None = None # MIME type hint
|
||||||
date_encoded: Optional[str] = None # Always None in v3.2.0 (kept for compatibility)
|
date_encoded: str | None = None # Always None in v3.2.0 (kept for compatibility)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_file(self) -> bool:
|
def is_file(self) -> bool:
|
||||||
@@ -161,7 +159,7 @@ class DecodeResult:
|
|||||||
def is_text(self) -> bool:
|
def is_text(self) -> bool:
|
||||||
return self.payload_type == 'text'
|
return self.payload_type == 'text'
|
||||||
|
|
||||||
def get_content(self) -> Union[str, bytes]:
|
def get_content(self) -> str | bytes:
|
||||||
"""Get the decoded content (text or bytes)."""
|
"""Get the decoded content (text or bytes)."""
|
||||||
if self.is_text:
|
if self.is_text:
|
||||||
return self.message or ""
|
return self.message or ""
|
||||||
@@ -196,10 +194,10 @@ class ValidationResult:
|
|||||||
is_valid: bool
|
is_valid: bool
|
||||||
error_message: str = ""
|
error_message: str = ""
|
||||||
details: dict = field(default_factory=dict)
|
details: dict = field(default_factory=dict)
|
||||||
warning: Optional[str] = None # v3.2.0: Added for passphrase length warnings
|
warning: str | None = None # v3.2.0: Added for passphrase length warnings
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ok(cls, warning: Optional[str] = None, **details) -> 'ValidationResult':
|
def ok(cls, warning: str | None = None, **details) -> 'ValidationResult':
|
||||||
"""Create a successful validation result."""
|
"""Create a successful validation result."""
|
||||||
result = cls(is_valid=True, details=details)
|
result = cls(is_valid=True, details=details)
|
||||||
if warning:
|
if warning:
|
||||||
@@ -227,8 +225,8 @@ class ImageInfo:
|
|||||||
file_size: int
|
file_size: int
|
||||||
lsb_capacity_bytes: int
|
lsb_capacity_bytes: int
|
||||||
lsb_capacity_kb: float
|
lsb_capacity_kb: float
|
||||||
dct_capacity_bytes: Optional[int] = None
|
dct_capacity_bytes: int | None = None
|
||||||
dct_capacity_kb: Optional[float] = None
|
dct_capacity_kb: float | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -241,18 +239,18 @@ class CapacityComparison:
|
|||||||
lsb_kb: float
|
lsb_kb: float
|
||||||
lsb_output_format: str
|
lsb_output_format: str
|
||||||
dct_available: bool
|
dct_available: bool
|
||||||
dct_bytes: Optional[int] = None
|
dct_bytes: int | None = None
|
||||||
dct_kb: Optional[float] = None
|
dct_kb: float | None = None
|
||||||
dct_output_formats: Optional[List[str]] = None
|
dct_output_formats: list[str] | None = None
|
||||||
dct_ratio_vs_lsb: Optional[float] = None
|
dct_ratio_vs_lsb: float | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GenerateResult:
|
class GenerateResult:
|
||||||
"""Result of credential generation."""
|
"""Result of credential generation."""
|
||||||
passphrase: str
|
passphrase: str
|
||||||
pin: Optional[str] = None
|
pin: str | None = None
|
||||||
rsa_key_pem: Optional[str] = None
|
rsa_key_pem: str | None = None
|
||||||
passphrase_words: int = 4
|
passphrase_words: int = 4
|
||||||
passphrase_entropy: int = 0
|
passphrase_entropy: int = 0
|
||||||
pin_entropy: int = 0
|
pin_entropy: int = 0
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ IMPROVEMENTS IN THIS VERSION:
|
|||||||
- Improved error messages
|
- Improved error messages
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
import io
|
import io
|
||||||
import zlib
|
import zlib
|
||||||
import base64
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -27,20 +26,19 @@ except ImportError:
|
|||||||
|
|
||||||
# QR code reading
|
# QR code reading
|
||||||
try:
|
try:
|
||||||
from pyzbar.pyzbar import decode as pyzbar_decode
|
|
||||||
from pyzbar.pyzbar import ZBarSymbol
|
from pyzbar.pyzbar import ZBarSymbol
|
||||||
|
from pyzbar.pyzbar import decode as pyzbar_decode
|
||||||
HAS_QRCODE_READ = True
|
HAS_QRCODE_READ = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_QRCODE_READ = False
|
HAS_QRCODE_READ = False
|
||||||
|
|
||||||
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
QR_MAX_BINARY,
|
|
||||||
QR_CROP_PADDING_PERCENT,
|
|
||||||
QR_CROP_MIN_PADDING_PX,
|
QR_CROP_MIN_PADDING_PX,
|
||||||
|
QR_CROP_PADDING_PERCENT,
|
||||||
|
QR_MAX_BINARY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
COMPRESSION_PREFIX = "STEGASOO-Z:"
|
COMPRESSION_PREFIX = "STEGASOO-Z:"
|
||||||
|
|
||||||
@@ -273,7 +271,7 @@ def generate_qr_code(
|
|||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def read_qr_code(image_data: bytes) -> Optional[str]:
|
def read_qr_code(image_data: bytes) -> str | None:
|
||||||
"""
|
"""
|
||||||
Read QR code from image data.
|
Read QR code from image data.
|
||||||
|
|
||||||
@@ -313,7 +311,7 @@ def read_qr_code(image_data: bytes) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def read_qr_code_from_file(filepath: str) -> Optional[str]:
|
def read_qr_code_from_file(filepath: str) -> str | None:
|
||||||
"""
|
"""
|
||||||
Read QR code from image file.
|
Read QR code from image file.
|
||||||
|
|
||||||
@@ -327,7 +325,7 @@ def read_qr_code_from_file(filepath: str) -> Optional[str]:
|
|||||||
return read_qr_code(f.read())
|
return read_qr_code(f.read())
|
||||||
|
|
||||||
|
|
||||||
def extract_key_from_qr(image_data: bytes) -> Optional[str]:
|
def extract_key_from_qr(image_data: bytes) -> str | None:
|
||||||
"""
|
"""
|
||||||
Extract RSA key from QR code image, auto-decompressing if needed.
|
Extract RSA key from QR code image, auto-decompressing if needed.
|
||||||
|
|
||||||
@@ -375,7 +373,7 @@ def extract_key_from_qr(image_data: bytes) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract_key_from_qr_file(filepath: str) -> Optional[str]:
|
def extract_key_from_qr_file(filepath: str) -> str | None:
|
||||||
"""
|
"""
|
||||||
Extract RSA key from QR code image file.
|
Extract RSA key from QR code image file.
|
||||||
|
|
||||||
@@ -393,7 +391,7 @@ def detect_and_crop_qr(
|
|||||||
image_data: bytes,
|
image_data: bytes,
|
||||||
padding_percent: float = QR_CROP_PADDING_PERCENT,
|
padding_percent: float = QR_CROP_PADDING_PERCENT,
|
||||||
min_padding_px: int = QR_CROP_MIN_PADDING_PX
|
min_padding_px: int = QR_CROP_MIN_PADDING_PX
|
||||||
) -> Optional[bytes]:
|
) -> bytes | None:
|
||||||
"""
|
"""
|
||||||
Detect QR code in image and crop to it, handling rotation.
|
Detect QR code in image and crop to it, handling rotation.
|
||||||
|
|
||||||
@@ -488,7 +486,7 @@ def detect_and_crop_qr_file(
|
|||||||
filepath: str,
|
filepath: str,
|
||||||
padding_percent: float = QR_CROP_PADDING_PERCENT,
|
padding_percent: float = QR_CROP_PADDING_PERCENT,
|
||||||
min_padding_px: int = QR_CROP_MIN_PADDING_PX
|
min_padding_px: int = QR_CROP_MIN_PADDING_PX
|
||||||
) -> Optional[bytes]:
|
) -> bytes | None:
|
||||||
"""
|
"""
|
||||||
Detect QR code in image file and crop to it.
|
Detect QR code in image file and crop to it.
|
||||||
|
|
||||||
|
|||||||
@@ -20,22 +20,24 @@ Changes in v3.2.0:
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import struct
|
import struct
|
||||||
from typing import Optional, Tuple, List, Union
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .dct_steganography import DCTEmbedStats
|
||||||
|
|
||||||
from .models import EmbedStats, FilePayload
|
|
||||||
from .exceptions import CapacityError, ExtractionError, EmbeddingError
|
|
||||||
from .debug import debug
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
EMBED_MODE_LSB,
|
|
||||||
EMBED_MODE_DCT,
|
|
||||||
EMBED_MODE_AUTO,
|
EMBED_MODE_AUTO,
|
||||||
|
EMBED_MODE_DCT,
|
||||||
|
EMBED_MODE_LSB,
|
||||||
VALID_EMBED_MODES,
|
VALID_EMBED_MODES,
|
||||||
)
|
)
|
||||||
|
from .debug import debug
|
||||||
|
from .exceptions import CapacityError, EmbeddingError
|
||||||
|
from .models import EmbedStats, FilePayload
|
||||||
|
|
||||||
# Lossless formats that preserve LSB data
|
# Lossless formats that preserve LSB data
|
||||||
LOSSLESS_FORMATS = {'PNG', 'BMP', 'TIFF'}
|
LOSSLESS_FORMATS = {'PNG', 'BMP', 'TIFF'}
|
||||||
@@ -122,7 +124,7 @@ def has_dct_support() -> bool:
|
|||||||
# FORMAT UTILITIES
|
# FORMAT UTILITIES
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
def get_output_format(input_format: Optional[str]) -> Tuple[str, str]:
|
def get_output_format(input_format: str | None) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Determine the output format based on input format.
|
Determine the output format based on input format.
|
||||||
|
|
||||||
@@ -151,7 +153,7 @@ def get_output_format(input_format: Optional[str]) -> Tuple[str, str]:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
def will_fit(
|
def will_fit(
|
||||||
payload: Union[str, bytes, FilePayload, int],
|
payload: str | bytes | FilePayload | int,
|
||||||
carrier_image: bytes,
|
carrier_image: bytes,
|
||||||
bits_per_channel: int = 1,
|
bits_per_channel: int = 1,
|
||||||
include_compression_estimate: bool = True,
|
include_compression_estimate: bool = True,
|
||||||
@@ -297,7 +299,7 @@ def calculate_capacity_by_mode(
|
|||||||
|
|
||||||
|
|
||||||
def will_fit_by_mode(
|
def will_fit_by_mode(
|
||||||
payload: Union[str, bytes, FilePayload, int],
|
payload: str | bytes | FilePayload | int,
|
||||||
carrier_image: bytes,
|
carrier_image: bytes,
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
bits_per_channel: int = 1,
|
bits_per_channel: int = 1,
|
||||||
@@ -424,7 +426,7 @@ def compare_modes(image_data: bytes) -> dict:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
@debug.time
|
@debug.time
|
||||||
def generate_pixel_indices(key: bytes, num_pixels: int, num_needed: int) -> List[int]:
|
def generate_pixel_indices(key: bytes, num_pixels: int, num_needed: int) -> list[int]:
|
||||||
"""
|
"""
|
||||||
Generate pseudo-random pixel indices for embedding.
|
Generate pseudo-random pixel indices for embedding.
|
||||||
|
|
||||||
@@ -508,11 +510,11 @@ def embed_in_image(
|
|||||||
image_data: bytes,
|
image_data: bytes,
|
||||||
pixel_key: bytes,
|
pixel_key: bytes,
|
||||||
bits_per_channel: int = 1,
|
bits_per_channel: int = 1,
|
||||||
output_format: Optional[str] = None,
|
output_format: str | None = None,
|
||||||
embed_mode: str = EMBED_MODE_LSB,
|
embed_mode: str = EMBED_MODE_LSB,
|
||||||
dct_output_format: str = DCT_OUTPUT_PNG,
|
dct_output_format: str = DCT_OUTPUT_PNG,
|
||||||
dct_color_mode: str = 'grayscale',
|
dct_color_mode: str = 'grayscale',
|
||||||
) -> Tuple[bytes, Union[EmbedStats, 'DCTEmbedStats'], str]:
|
) -> tuple[bytes, Union[EmbedStats, 'DCTEmbedStats'], str]:
|
||||||
"""
|
"""
|
||||||
Embed data into an image using specified mode.
|
Embed data into an image using specified mode.
|
||||||
|
|
||||||
@@ -586,8 +588,8 @@ def _embed_lsb(
|
|||||||
image_data: bytes,
|
image_data: bytes,
|
||||||
pixel_key: bytes,
|
pixel_key: bytes,
|
||||||
bits_per_channel: int = 1,
|
bits_per_channel: int = 1,
|
||||||
output_format: Optional[str] = None,
|
output_format: str | None = None,
|
||||||
) -> Tuple[bytes, EmbedStats, str]:
|
) -> tuple[bytes, EmbedStats, str]:
|
||||||
"""
|
"""
|
||||||
Embed data using LSB steganography (internal implementation).
|
Embed data using LSB steganography (internal implementation).
|
||||||
"""
|
"""
|
||||||
@@ -722,7 +724,7 @@ def extract_from_image(
|
|||||||
pixel_key: bytes,
|
pixel_key: bytes,
|
||||||
bits_per_channel: int = 1,
|
bits_per_channel: int = 1,
|
||||||
embed_mode: str = EMBED_MODE_AUTO,
|
embed_mode: str = EMBED_MODE_AUTO,
|
||||||
) -> Optional[bytes]:
|
) -> bytes | None:
|
||||||
"""
|
"""
|
||||||
Extract hidden data from a stego image.
|
Extract hidden data from a stego image.
|
||||||
|
|
||||||
@@ -765,7 +767,7 @@ def extract_from_image(
|
|||||||
return _extract_lsb(image_data, pixel_key, bits_per_channel)
|
return _extract_lsb(image_data, pixel_key, bits_per_channel)
|
||||||
|
|
||||||
|
|
||||||
def _extract_dct(image_data: bytes, pixel_key: bytes) -> Optional[bytes]:
|
def _extract_dct(image_data: bytes, pixel_key: bytes) -> bytes | None:
|
||||||
"""Extract using DCT mode."""
|
"""Extract using DCT mode."""
|
||||||
try:
|
try:
|
||||||
dct_mod = _get_dct_module()
|
dct_mod = _get_dct_module()
|
||||||
@@ -779,7 +781,7 @@ def _extract_lsb(
|
|||||||
image_data: bytes,
|
image_data: bytes,
|
||||||
pixel_key: bytes,
|
pixel_key: bytes,
|
||||||
bits_per_channel: int = 1
|
bits_per_channel: int = 1
|
||||||
) -> Optional[bytes]:
|
) -> bytes | None:
|
||||||
"""
|
"""
|
||||||
Extract using LSB mode (internal implementation).
|
Extract using LSB mode (internal implementation).
|
||||||
"""
|
"""
|
||||||
@@ -878,7 +880,7 @@ def _extract_lsb(
|
|||||||
# UTILITY FUNCTIONS
|
# UTILITY FUNCTIONS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
def get_image_dimensions(image_data: bytes) -> Tuple[int, int]:
|
def get_image_dimensions(image_data: bytes) -> tuple[int, int]:
|
||||||
"""Get image dimensions without loading full image."""
|
"""Get image dimensions without loading full image."""
|
||||||
debug.validate(len(image_data) > 0, "Image data cannot be empty")
|
debug.validate(len(image_data) > 0, "Image data cannot be empty")
|
||||||
img = Image.open(io.BytesIO(image_data))
|
img = Image.open(io.BytesIO(image_data))
|
||||||
@@ -890,7 +892,7 @@ def get_image_dimensions(image_data: bytes) -> Tuple[int, int]:
|
|||||||
img.close()
|
img.close()
|
||||||
|
|
||||||
|
|
||||||
def get_image_format(image_data: bytes) -> Optional[str]:
|
def get_image_format(image_data: bytes) -> str | None:
|
||||||
"""Get image format (PIL format string like 'PNG', 'JPEG')."""
|
"""Get image format (PIL format string like 'PNG', 'JPEG')."""
|
||||||
try:
|
try:
|
||||||
img = Image.open(io.BytesIO(image_data))
|
img = Image.open(io.BytesIO(image_data))
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ import os
|
|||||||
import random
|
import random
|
||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import date, datetime
|
from datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ def strip_image_metadata(image_data: bytes, output_format: str = 'PNG') -> bytes
|
|||||||
|
|
||||||
|
|
||||||
def generate_filename(
|
def generate_filename(
|
||||||
date_str: Optional[str] = None,
|
date_str: str | None = None,
|
||||||
prefix: str = "",
|
prefix: str = "",
|
||||||
extension: str = "png"
|
extension: str = "png"
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -96,7 +95,7 @@ def generate_filename(
|
|||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def parse_date_from_filename(filename: str) -> Optional[str]:
|
def parse_date_from_filename(filename: str) -> str | None:
|
||||||
"""
|
"""
|
||||||
Extract date from a stego filename.
|
Extract date from a stego filename.
|
||||||
|
|
||||||
@@ -205,7 +204,7 @@ class SecureDeleter:
|
|||||||
>>> deleter.execute()
|
>>> deleter.execute()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path: Union[str, Path], passes: int = 7):
|
def __init__(self, path: str | Path, passes: int = 7):
|
||||||
"""
|
"""
|
||||||
Initialize secure deleter.
|
Initialize secure deleter.
|
||||||
|
|
||||||
@@ -294,7 +293,7 @@ class SecureDeleter:
|
|||||||
debug.print(f"Path does not exist: {self.path}")
|
debug.print(f"Path does not exist: {self.path}")
|
||||||
|
|
||||||
|
|
||||||
def secure_delete(path: Union[str, Path], passes: int = 7) -> None:
|
def secure_delete(path: str | Path, passes: int = 7) -> None:
|
||||||
"""
|
"""
|
||||||
Convenience function for secure deletion.
|
Convenience function for secure deletion.
|
||||||
|
|
||||||
|
|||||||
@@ -10,25 +10,35 @@ Changes in v3.2.0:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
ALLOWED_IMAGE_EXTENSIONS,
|
||||||
MAX_MESSAGE_SIZE, MAX_FILE_PAYLOAD_SIZE, MAX_IMAGE_PIXELS, MAX_FILE_SIZE,
|
ALLOWED_KEY_EXTENSIONS,
|
||||||
MIN_RSA_BITS, MIN_KEY_PASSWORD_LENGTH,
|
EMBED_MODE_AUTO,
|
||||||
ALLOWED_IMAGE_EXTENSIONS, ALLOWED_KEY_EXTENSIONS,
|
EMBED_MODE_DCT,
|
||||||
MIN_PASSPHRASE_WORDS, RECOMMENDED_PASSPHRASE_WORDS,
|
EMBED_MODE_LSB,
|
||||||
EMBED_MODE_LSB, EMBED_MODE_DCT, EMBED_MODE_AUTO,
|
MAX_FILE_PAYLOAD_SIZE,
|
||||||
|
MAX_FILE_SIZE,
|
||||||
|
MAX_IMAGE_PIXELS,
|
||||||
|
MAX_MESSAGE_SIZE,
|
||||||
|
MAX_PIN_LENGTH,
|
||||||
|
MIN_KEY_PASSWORD_LENGTH,
|
||||||
|
MIN_PASSPHRASE_WORDS,
|
||||||
|
MIN_PIN_LENGTH,
|
||||||
|
MIN_RSA_BITS,
|
||||||
|
RECOMMENDED_PASSPHRASE_WORDS,
|
||||||
)
|
)
|
||||||
from .models import ValidationResult, FilePayload
|
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
ValidationError, PinValidationError, MessageValidationError,
|
ImageValidationError,
|
||||||
ImageValidationError, KeyValidationError, SecurityFactorError,
|
KeyValidationError,
|
||||||
FileTooLargeError, UnsupportedFileTypeError,
|
MessageValidationError,
|
||||||
|
PinValidationError,
|
||||||
|
SecurityFactorError,
|
||||||
)
|
)
|
||||||
from .keygen import load_rsa_key
|
from .keygen import load_rsa_key
|
||||||
|
from .models import FilePayload, ValidationResult
|
||||||
|
|
||||||
|
|
||||||
def validate_pin(pin: str, required: bool = False) -> ValidationResult:
|
def validate_pin(pin: str, required: bool = False) -> ValidationResult:
|
||||||
@@ -87,7 +97,7 @@ def validate_message(message: str) -> ValidationResult:
|
|||||||
return ValidationResult.ok(length=len(message))
|
return ValidationResult.ok(length=len(message))
|
||||||
|
|
||||||
|
|
||||||
def validate_payload(payload: Union[str, bytes, FilePayload]) -> ValidationResult:
|
def validate_payload(payload: str | bytes | FilePayload) -> ValidationResult:
|
||||||
"""
|
"""
|
||||||
Validate a payload (text message, bytes, or file).
|
Validate a payload (text message, bytes, or file).
|
||||||
|
|
||||||
@@ -212,7 +222,7 @@ def validate_image(
|
|||||||
|
|
||||||
def validate_rsa_key(
|
def validate_rsa_key(
|
||||||
key_data: bytes,
|
key_data: bytes,
|
||||||
password: Optional[str] = None,
|
password: str | None = None,
|
||||||
required: bool = False
|
required: bool = False
|
||||||
) -> ValidationResult:
|
) -> ValidationResult:
|
||||||
"""
|
"""
|
||||||
@@ -248,7 +258,7 @@ def validate_rsa_key(
|
|||||||
|
|
||||||
def validate_security_factors(
|
def validate_security_factors(
|
||||||
pin: str,
|
pin: str,
|
||||||
rsa_key_data: Optional[bytes]
|
rsa_key_data: bytes | None
|
||||||
) -> ValidationResult:
|
) -> ValidationResult:
|
||||||
"""
|
"""
|
||||||
Validate that at least one security factor is provided.
|
Validate that at least one security factor is provided.
|
||||||
@@ -456,7 +466,7 @@ def require_valid_message(message: str) -> None:
|
|||||||
raise MessageValidationError(result.error_message)
|
raise MessageValidationError(result.error_message)
|
||||||
|
|
||||||
|
|
||||||
def require_valid_payload(payload: Union[str, bytes, FilePayload]) -> None:
|
def require_valid_payload(payload: str | bytes | FilePayload) -> None:
|
||||||
"""Validate payload (text, bytes, or file), raising exception on failure."""
|
"""Validate payload (text, bytes, or file), raising exception on failure."""
|
||||||
result = validate_payload(payload)
|
result = validate_payload(payload)
|
||||||
if not result.is_valid:
|
if not result.is_valid:
|
||||||
@@ -472,7 +482,7 @@ def require_valid_image(image_data: bytes, name: str = "Image") -> None:
|
|||||||
|
|
||||||
def require_valid_rsa_key(
|
def require_valid_rsa_key(
|
||||||
key_data: bytes,
|
key_data: bytes,
|
||||||
password: Optional[str] = None,
|
password: str | None = None,
|
||||||
required: bool = False
|
required: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate RSA key, raising exception on failure."""
|
"""Validate RSA key, raising exception on failure."""
|
||||||
@@ -481,7 +491,7 @@ def require_valid_rsa_key(
|
|||||||
raise KeyValidationError(result.error_message)
|
raise KeyValidationError(result.error_message)
|
||||||
|
|
||||||
|
|
||||||
def require_security_factors(pin: str, rsa_key_data: Optional[bytes]) -> None:
|
def require_security_factors(pin: str, rsa_key_data: bytes | None) -> None:
|
||||||
"""Validate security factors, raising exception on failure."""
|
"""Validate security factors, raising exception on failure."""
|
||||||
result = validate_security_factors(pin, rsa_key_data)
|
result = validate_security_factors(pin, rsa_key_data)
|
||||||
if not result.is_valid:
|
if not result.is_valid:
|
||||||
|
|||||||
@@ -7,18 +7,19 @@ Updated for v4.0.0:
|
|||||||
- BatchCredentials.passphrase is a single string
|
- BatchCredentials.passphrase is a single string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from stegasoo.batch import (
|
from stegasoo.batch import (
|
||||||
|
BatchCredentials,
|
||||||
|
BatchItem,
|
||||||
BatchProcessor,
|
BatchProcessor,
|
||||||
BatchResult,
|
BatchResult,
|
||||||
BatchItem,
|
|
||||||
BatchStatus,
|
BatchStatus,
|
||||||
BatchCredentials,
|
|
||||||
batch_capacity_check,
|
batch_capacity_check,
|
||||||
print_batch_result,
|
print_batch_result,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ Tests for Stegasoo compression module.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from stegasoo.compression import (
|
from stegasoo.compression import (
|
||||||
compress,
|
|
||||||
decompress,
|
|
||||||
CompressionAlgorithm,
|
|
||||||
CompressionError,
|
|
||||||
get_compression_ratio,
|
|
||||||
estimate_compressed_size,
|
|
||||||
get_available_algorithms,
|
|
||||||
algorithm_name,
|
|
||||||
MIN_COMPRESS_SIZE,
|
|
||||||
COMPRESSION_MAGIC,
|
COMPRESSION_MAGIC,
|
||||||
HAS_LZ4,
|
HAS_LZ4,
|
||||||
|
MIN_COMPRESS_SIZE,
|
||||||
|
CompressionAlgorithm,
|
||||||
|
CompressionError,
|
||||||
|
algorithm_name,
|
||||||
|
compress,
|
||||||
|
decompress,
|
||||||
|
estimate_compressed_size,
|
||||||
|
get_available_algorithms,
|
||||||
|
get_compression_ratio,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,29 +11,28 @@ Updated for v4.0.0:
|
|||||||
- Python 3.12 recommended (3.13 not supported)
|
- Python 3.12 recommended (3.13 not supported)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import io
|
|
||||||
|
|
||||||
import stegasoo
|
import stegasoo
|
||||||
from stegasoo import (
|
from stegasoo import (
|
||||||
generate_pin,
|
|
||||||
generate_passphrase,
|
|
||||||
generate_credentials,
|
|
||||||
validate_pin,
|
|
||||||
validate_message,
|
|
||||||
validate_passphrase,
|
|
||||||
validate_channel_key,
|
|
||||||
encode,
|
|
||||||
decode,
|
decode,
|
||||||
decode_text,
|
decode_text,
|
||||||
|
encode,
|
||||||
generate_channel_key,
|
generate_channel_key,
|
||||||
|
generate_credentials,
|
||||||
|
generate_passphrase,
|
||||||
|
generate_pin,
|
||||||
get_channel_fingerprint,
|
get_channel_fingerprint,
|
||||||
__version__,
|
validate_channel_key,
|
||||||
|
validate_message,
|
||||||
|
validate_passphrase,
|
||||||
|
validate_pin,
|
||||||
)
|
)
|
||||||
from stegasoo.steganography import get_output_format
|
from stegasoo.steganography import get_output_format
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Fixtures
|
# Fixtures
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user