More snazzy 4.0 Web UI improvements.

This commit is contained in:
Aaron D. Lee
2026-01-02 15:45:43 -05:00
parent 1bb3589baf
commit 6fa4b447db
26 changed files with 4282 additions and 2282 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
# Stegasoo Web UI Documentation (v3.2.0)
# Stegasoo Web UI Documentation (v3.3.0)
Complete guide for the Stegasoo web-based steganography interface.
## Table of Contents
- [Overview](#overview)
- [What's New in v3.2.0](#whats-new-in-v320)
- [What's New in v3.3.0](#whats-new-in-v330)
- [Installation & Setup](#installation--setup)
- [Pages & Features](#pages--features)
- [Home Page](#home-page)
@@ -14,8 +14,8 @@ Complete guide for the Stegasoo web-based steganography interface.
- [Decode Message](#decode-message)
- [About Page](#about-page)
- [Embedding Modes](#embedding-modes)
- [LSB Mode (Default)](#lsb-mode-default)
- [DCT Mode](#dct-mode)
- [DCT Mode (Default)](#dct-mode-default)
- [LSB Mode](#lsb-mode)
- [User Interface Guide](#user-interface-guide)
- [Workflow Examples](#workflow-examples)
- [Security Features](#security-features)
@@ -39,38 +39,39 @@ Built with Flask, Bootstrap 5, and a modern dark theme.
### Features
- ✅ Drag-and-drop file uploads
- ✅ Image previews
- ✅ Image previews with scan animations
- ✅ Native sharing (Web Share API)
- ✅ Responsive design (mobile-friendly)
- ✅ Password-protected RSA key downloads
- ✅ Real-time entropy calculations
- ✅ Automatic file cleanup
-**DCT steganography mode** - Frequency domain embedding
-**DCT steganography mode** - Now the default for social media resilience
-**Color mode selection** - Preserve carrier colors
-**File embedding** - Hide files, not just text
-**v3.2.0: No date tracking** - Simplified workflow
-**QR code RSA keys** - Scan to import keys
-**v3.3.0: Streamlined UI** - Compact mode selection, improved form flow
---
## What's New in v3.2.0
## What's New in v3.3.0
Version 3.2.0 simplifies the user experience significantly:
Version 3.3.0 improves the user interface with a streamlined workflow:
| Change | Before (v3.1) | After (v3.2.0) |
| Change | Before (v3.2) | After (v3.3.0) |
|--------|---------------|----------------|
| Credentials | 7 daily phrases | Single passphrase |
| Encode form | Date selection required | No date field |
| Decode form | Date detection/input | No date needed |
| Default words | 3 words | 4 words |
| Field label | "Day Phrase" | "Passphrase" |
| Default mode | LSB | DCT (when available) |
| Mode selection | Large cards with bullet lists | Compact inline buttons with tooltips |
| Mode position | Top of form | After image upload, before payload |
| Mode details | Always visible | Hover tooltip on ⓘ icon |
| Capacity badges | LSB first | DCT first |
| Status labels | "Key Source", "Carrier", "RSA KEY" | "Hash Acquired", "Carrier Loaded"/"Stego Loaded", "KEY LOADED" |
**Key benefits:**
-No need to remember which day a message was encoded
-Simpler forms with fewer fields
-True asynchronous communication
-Stronger default security (4 words = ~44 bits entropy)
**Breaking Change:** v3.2.0 cannot decode images created with v3.1.x.
-DCT mode default - Better for social media sharing
-Logical form flow: Load images → Select mode → Enter payload
-Cleaner UI with less visual clutter
-Mode details available on hover without expanding
- ✅ Consistent "Loaded" status indicators
---
@@ -232,16 +233,27 @@ For easier sharing, you can also:
Hide a secret message or file inside an image.
#### Form Flow (v3.3.0)
The encode form follows a logical flow:
1. **Load Images** - Reference photo and carrier image
2. **View Capacity** - Shows available capacity for DCT and LSB modes
3. **Select Mode** - DCT (default) or LSB with inline tooltips
4. **Enter Payload** - Text message or file
5. **Add Security** - Passphrase, PIN, and/or RSA key
#### Input Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| Reference Photo | Image file | ✓ | Your shared secret photo |
| Carrier Image | Image file | ✓ | Image to hide message in |
| Embedding Mode | Toggle | ✓ | DCT (default) or LSB |
| Payload Type | Toggle | ✓ | Text message or file |
| Secret Message | Text | * | Message to hide (max 50KB) |
| File to Embed | File | * | File to hide (max 2MB) |
| Passphrase | Text | ✓ | Your passphrase (v3.2.0) |
| Passphrase | Text | ✓ | Your passphrase |
| PIN | Number | ** | Your static PIN |
| RSA Key | .pem file | ** | Your shared RSA key |
| RSA Key QR | Image file | ** | QR code containing RSA key |
@@ -250,25 +262,46 @@ Hide a secret message or file inside an image.
\* One of message or file required.
\*\* At least one security factor (PIN or RSA Key) required.
#### Advanced Options
#### Embedding Mode Selection (v3.3.0)
Expand "Advanced Options" to access embedding mode settings:
The mode selector is now a compact inline toggle:
```
┌────────────────────────────────────────────────────────────┐
│ ◉ 🔊 DCT · Social Media ⓘ │ ○ ⊞ LSB · Email & Files ⓘ │
└────────────────────────────────────────────────────────────┘
```
- **DCT** - Default, best for social media sharing
- **LSB** - Higher capacity, for lossless channels
- **ⓘ** - Hover for details (capacity, output format, etc.)
#### DCT Options
When DCT mode is selected, additional options appear:
| Option | Values | Default | Description |
|--------|--------|---------|-------------|
| Embedding Mode | LSB / DCT | LSB | Steganography algorithm |
| Output Format | PNG / JPEG | PNG | Output image format (DCT only) |
| Color Mode | Color / Grayscale | Color | Carrier color handling (DCT only) |
See [Embedding Modes](#embedding-modes) for detailed explanations.
| Output Format | PNG / JPEG | JPEG | Output image format |
| Color Mode | Color / Grayscale | Color | Carrier color handling |
#### Drag-and-Drop Upload
Both image upload zones support:
- Click to browse
- Drag and drop files
- Instant image preview
- File name display
- Instant image preview with scan animation
- Status indicators ("Hash Acquired", "Carrier Loaded")
#### Capacity Info Panel
After loading a carrier image, a capacity panel appears:
```
┌─────────────────────────────────────────────────────────┐
│ 📏 Carrier: 1920 × 1080 (2.1 MP) DCT: 150 KB LSB: 750 KB │
└─────────────────────────────────────────────────────────┘
```
#### Character Counter
@@ -281,11 +314,14 @@ Shows warning at 80% capacity.
#### Encoding Process
1. Fill in all required fields
2. (Optional) Expand "Advanced Options" for DCT mode
3. Click "Encode Message"
4. Wait for processing (shows spinner)
5. Redirected to result page
1. Upload reference photo and carrier image
2. View capacity info panel
3. Select embedding mode (DCT default)
4. Choose payload type and enter content
5. Enter passphrase and security factors
6. Click "Encode Message"
7. Wait for processing (shows spinner)
8. Redirected to result page
#### Result Page
@@ -413,8 +449,8 @@ If decryption fails:
Information about the Stegasoo project, security model, and credits.
Includes:
- Version information (v3.2.0)
- v3.2.0 changes explanation
- Version information (v3.3.0)
- Recent UI improvements
- Security model overview
- Dependency status (Argon2, QR code support)
@@ -424,44 +460,29 @@ Includes:
Stegasoo offers two steganography algorithms, each with different trade-offs.
### LSB Mode (Default)
### DCT Mode (Default)
**Least Significant Bit** embedding modifies the least significant bits of pixel values.
**Discrete Cosine Transform** embedding hides data in frequency domain coefficients. This is now the default mode when scipy is available.
| Aspect | Details |
|--------|---------|
| **Capacity** | ~3 bits/pixel (~375 KB for 1920×1080) |
| **Output Format** | PNG only (lossless required) |
| **Resilience** | ❌ Destroyed by JPEG compression |
| **Best For** | Maximum capacity, controlled sharing |
**When to use LSB:**
- Sharing via lossless channels (email attachment, file transfer)
- Maximum message capacity needed
- Recipient won't modify the image
### DCT Mode
**Discrete Cosine Transform** embedding hides data in frequency domain coefficients.
| Aspect | Details |
|--------|---------|
| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080 PNG, ~50 KB JPEG) |
| **Capacity** | ~0.5 bits/pixel (~75 KB/MP) |
| **Output Formats** | PNG or JPEG |
| **Resilience** | ✅ Better resistance to analysis |
| **Best For** | Stealth requirements, frequency domain hiding |
| **Resilience** | ✅ Survives JPEG recompression |
| **Best For** | Social media, messaging apps |
**When to use DCT:**
- Sharing via social media (Instagram, WhatsApp, Telegram)
- When image may be recompressed
- When stealth is important
- Smaller messages that fit in reduced capacity
- When you want JPEG output for natural appearance
#### DCT Output Formats
| Format | Pros | Cons |
|--------|------|------|
| **JPEG** | Native format, natural, smaller, resilient | Slightly lower capacity |
| **PNG** | Lossless, predictable | Larger file |
| **JPEG** | Native format, natural, smaller | Slightly lower capacity |
#### DCT Color Modes
@@ -470,15 +491,31 @@ Stegasoo offers two steganography algorithms, each with different trade-offs.
| **Color** | Embeds in luminance (Y), preserves chrominance | Most images, photos |
| **Grayscale** | Converts to grayscale before embedding | Black & white images |
### LSB Mode
**Least Significant Bit** embedding modifies the least significant bits of pixel values.
| Aspect | Details |
|--------|---------|
| **Capacity** | ~3 bits/pixel (~375 KB/MP) |
| **Output Format** | PNG only (lossless required) |
| **Resilience** | ❌ Destroyed by JPEG compression |
| **Best For** | Maximum capacity, controlled sharing |
**When to use LSB:**
- Sharing via lossless channels (email attachment, file transfer, cloud storage)
- Maximum message capacity needed
- Recipient won't modify/recompress the image
### Capacity Comparison
For a 1920×1080 image:
For a 1920×1080 image (~2 MP):
| Mode | Approximate Capacity |
|------|---------------------|
| LSB (PNG) | ~375 KB |
| DCT (PNG, Color) | ~65 KB |
| DCT (JPEG) | ~50 KB |
| LSB (PNG) | ~750 KB |
| DCT (PNG, Color) | ~150 KB |
| DCT (JPEG) | ~150 KB |
### Choosing the Right Mode
@@ -487,21 +524,22 @@ For a 1920×1080 image:
│ Mode Selection Guide │
├─────────────────────────────────────────────────────────────┤
│ │
Need maximum capacity?
Sharing via social media / messaging app?
│ │ │
│ ┌───────┴───────┐ │
│ ▼ ▼ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ Use LSB Need stealth?
│ Use DCT Need maximum capacity?
│ (default) │ │
│ ┌───────┴───────┐ │
│ ▼ ▼ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ Use DCT Use LSB
│ Use LSB Use DCT
│ (default) │
│ │
└─────────────────────────────────────────────────────────────┘
```

View File

@@ -6,9 +6,10 @@ FastAPI-based REST API for steganography operations.
Supports both text messages and file embedding.
CHANGES in v4.0.0:
- Updated from v3.2.0 with no functional API changes
- Internal: JPEG normalization for jpegio compatibility
- Internal: Python 3.12 recommended
- Added channel key support for deployment/group isolation
- New /channel endpoints for key management
- channel_key parameter on encode/decode endpoints
- Messages encoded with channel key require same key to decode
CHANGES in v3.2.0:
- Removed date dependency from all operations
@@ -51,6 +52,17 @@ from stegasoo import (
compare_modes,
will_fit_by_mode,
calculate_capacity_by_mode,
# Channel key functions (v4.0.0)
generate_channel_key,
get_channel_key,
set_channel_key,
clear_channel_key,
has_channel_key,
get_channel_status,
validate_channel_key,
format_channel_key,
get_active_channel_key,
get_channel_fingerprint,
)
from stegasoo.constants import (
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
@@ -82,8 +94,9 @@ Secure steganography with hybrid authentication. Supports text messages and file
## Version 4.0.0 Changes
- **Python 3.12 recommended** - jpegio compatibility improvements
- **JPEG normalization** - Handles quality=100 images automatically
- **Channel key support** - Deployment/group isolation for messages
- **New /channel endpoints** - Generate, view, and manage channel keys
- **channel_key parameter** - Added to encode/decode endpoints
## Version 3.2.0 Changes
@@ -156,12 +169,15 @@ class EncodeRequest(BaseModel):
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
# date_str removed in v3.2.0
# Channel key (v4.0.0)
channel_key: Optional[str] = Field(
default=None,
description="Channel key for deployment isolation. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
)
embed_mode: EmbedModeType = Field(
default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
)
# NEW in v3.0.1
dct_output_format: DctOutputFormatType = Field(
default="png",
description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode."
@@ -183,12 +199,15 @@ class EncodeFileRequest(BaseModel):
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
# date_str removed in v3.2.0
# Channel key (v4.0.0)
channel_key: Optional[str] = Field(
default=None,
description="Channel key for deployment isolation. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
)
embed_mode: EmbedModeType = Field(
default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
)
# NEW in v3.0.1
dct_output_format: DctOutputFormatType = Field(
default="png",
description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode."
@@ -204,7 +223,6 @@ class EncodeResponse(BaseModel):
filename: str
capacity_used_percent: float
embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'")
# NEW in v3.0.1
output_format: str = Field(
default="png",
description="Output format: 'png' or 'jpeg' (for DCT mode)"
@@ -213,6 +231,15 @@ class EncodeResponse(BaseModel):
default="color",
description="Color mode: 'color' (LSB/DCT color) or 'grayscale' (DCT grayscale)"
)
# Channel key info (v4.0.0)
channel_mode: str = Field(
default="public",
description="Channel mode: 'public' or 'private'"
)
channel_fingerprint: Optional[str] = Field(
default=None,
description="Channel key fingerprint (if private mode)"
)
# Legacy fields (v3.2.0: no longer used in crypto)
date_used: Optional[str] = Field(
default=None,
@@ -231,6 +258,11 @@ class DecodeRequest(BaseModel):
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
# Channel key (v4.0.0)
channel_key: Optional[str] = Field(
default=None,
description="Channel key for decryption. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key"
)
embed_mode: ExtractModeType = Field(
default="auto",
description="Extraction mode: 'auto' (default), 'lsb', or 'dct'"
@@ -260,7 +292,6 @@ class ImageInfoResponse(BaseModel):
pixels: int
capacity_bytes: int = Field(description="LSB mode capacity (for backwards compatibility)")
capacity_kb: int = Field(description="LSB mode capacity in KB")
# NEW in v3.0
modes: Optional[dict[str, ModeCapacity]] = Field(
default=None,
description="Capacity by embedding mode (v3.0+)"
@@ -297,10 +328,38 @@ class DctModeInfo(BaseModel):
requires: str
class ChannelStatusResponse(BaseModel):
"""Response for channel key status (v4.0.0)."""
mode: str = Field(description="'public' or 'private'")
configured: bool = Field(description="Whether a channel key is configured")
fingerprint: Optional[str] = Field(default=None, description="Key fingerprint (partial)")
source: Optional[str] = Field(default=None, description="Where the key comes from")
key: Optional[str] = Field(default=None, description="Full key (only if reveal=true)")
class ChannelGenerateResponse(BaseModel):
"""Response for channel key generation (v4.0.0)."""
key: str = Field(description="Generated channel key")
fingerprint: str = Field(description="Key fingerprint")
saved: bool = Field(default=False, description="Whether key was saved to config")
save_location: Optional[str] = Field(default=None, description="Where key was saved")
class ChannelSetRequest(BaseModel):
"""Request to set channel key (v4.0.0)."""
key: str = Field(description="Channel key to set")
location: str = Field(default="user", description="'user' or 'project'")
class ModesResponse(BaseModel):
"""Response showing available embedding modes."""
lsb: dict
dct: DctModeInfo
# Channel key status (v4.0.0)
channel: Optional[dict] = Field(
default=None,
description="Channel key status (v4.0.0)"
)
class StatusResponse(BaseModel):
@@ -310,14 +369,17 @@ class StatusResponse(BaseModel):
has_dct: bool
max_payload_kb: int
available_modes: list[str]
# NEW in v3.0.1
dct_features: Optional[dict] = Field(
default=None,
description="DCT mode features (v3.0.1+)"
)
# NEW in v3.2.0
# Channel key status (v4.0.0)
channel: Optional[dict] = Field(
default=None,
description="Channel key status (v4.0.0)"
)
breaking_changes: dict = Field(
description="v3.2.0 breaking changes"
description="v4.0.0 breaking changes"
)
@@ -349,6 +411,67 @@ class ErrorResponse(BaseModel):
detail: Optional[str] = None
# ============================================================================
# HELPER: RESOLVE CHANNEL KEY
# ============================================================================
def _resolve_channel_key(channel_key: Optional[str]) -> Optional[str]:
"""
Resolve channel key from API parameter.
Args:
channel_key: API parameter value
- None: Use server-configured key (auto mode)
- "": Public mode (no channel key)
- "XXXX-...": Explicit key
Returns:
Resolved channel key to pass to encode/decode
Raises:
HTTPException: If key format is invalid
"""
if channel_key is None:
# Auto mode - use server config
return None
if channel_key == "":
# Public mode
return ""
# Explicit key - validate format
if not validate_channel_key(channel_key):
raise HTTPException(
400,
f"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
)
return channel_key
def _get_channel_info(channel_key: Optional[str]) -> tuple[str, Optional[str]]:
"""
Get channel mode and fingerprint for response.
Returns:
(mode, fingerprint) tuple
"""
if channel_key == "":
return "public", None
if channel_key is not None:
# Explicit key
fingerprint = f"{channel_key[:4]}-••••-••••-••••-••••-••••-••••-{channel_key[-4:]}"
return "private", fingerprint
# Auto mode - check server config
if has_channel_key():
status = get_channel_status()
return "private", status.get('fingerprint')
return "public", None
# ============================================================================
# ROUTES - STATUS & INFO
# ============================================================================
@@ -368,6 +491,15 @@ async def root():
"default_color_mode": "grayscale",
}
# Channel key status (v4.0.0)
channel_status = get_channel_status()
channel_info = {
"mode": channel_status['mode'],
"configured": channel_status['configured'],
"fingerprint": channel_status.get('fingerprint'),
"source": channel_status.get('source'),
}
return StatusResponse(
version=__version__,
has_argon2=has_argon2(),
@@ -376,11 +508,15 @@ async def root():
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024,
available_modes=available_modes,
dct_features=dct_features,
channel=channel_info,
breaking_changes={
"date_removed": "No date_str parameter needed - encode/decode anytime",
"passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)",
"format_version": 4,
"v4_channel_key": "Messages encoded with channel key require same key to decode",
"format_version": 5,
"backward_compatible": False,
"v3_notes": {
"date_removed": "No date_str parameter needed - encode/decode anytime",
"passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)",
}
}
)
@@ -390,9 +526,16 @@ async def api_modes():
"""
Get available embedding modes and their status.
NEW in v3.0: Shows LSB and DCT mode availability.
NEW in v3.0.1: Shows DCT color modes and output formats.
v4.0.0: Also includes channel key status.
"""
# Channel status
channel_status = get_channel_status()
channel_info = {
"mode": channel_status['mode'],
"configured": channel_status['configured'],
"fingerprint": channel_status.get('fingerprint'),
}
return ModesResponse(
lsb={
"available": True,
@@ -409,16 +552,137 @@ async def api_modes():
color_modes=["grayscale", "color"],
capacity_ratio="~20% of LSB",
requires="scipy",
)
),
channel=channel_info,
)
# ============================================================================
# ROUTES - CHANNEL KEY (v4.0.0)
# ============================================================================
@app.get("/channel/status", response_model=ChannelStatusResponse)
async def api_channel_status(
reveal: bool = Query(False, description="Include full key in response")
):
"""
Get current channel key status.
v4.0.0: New endpoint for channel key management.
Returns mode (public/private), fingerprint, and source.
Use reveal=true to include the full key.
"""
status = get_channel_status()
return ChannelStatusResponse(
mode=status['mode'],
configured=status['configured'],
fingerprint=status.get('fingerprint'),
source=status.get('source'),
key=status.get('key') if reveal and status['configured'] else None,
)
@app.post("/channel/generate", response_model=ChannelGenerateResponse)
async def api_channel_generate(
save: bool = Query(False, description="Save to user config"),
save_project: bool = Query(False, description="Save to project config"),
):
"""
Generate a new channel key.
v4.0.0: New endpoint for channel key management.
Optionally saves to user config (~/.stegasoo/channel.key) or
project config (./config/channel.key).
"""
if save and save_project:
raise HTTPException(400, "Cannot use both save and save_project")
key = generate_channel_key()
fingerprint = f"{key[:4]}-••••-••••-••••-••••-••••-••••-{key[-4:]}"
saved = False
save_location = None
if save:
set_channel_key(key, location='user')
saved = True
save_location = "~/.stegasoo/channel.key"
elif save_project:
set_channel_key(key, location='project')
saved = True
save_location = "./config/channel.key"
return ChannelGenerateResponse(
key=key,
fingerprint=fingerprint,
saved=saved,
save_location=save_location,
)
@app.post("/channel/set")
async def api_channel_set(request: ChannelSetRequest):
"""
Set/save a channel key to config.
v4.0.0: New endpoint for channel key management.
"""
if not validate_channel_key(request.key):
raise HTTPException(
400,
"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
)
if request.location not in ('user', 'project'):
raise HTTPException(400, "location must be 'user' or 'project'")
set_channel_key(request.key, location=request.location)
status = get_channel_status()
return {
"success": True,
"location": status.get('source'),
"fingerprint": status.get('fingerprint'),
}
@app.delete("/channel")
async def api_channel_clear(
location: str = Query("user", description="'user', 'project', or 'all'")
):
"""
Clear/remove channel key from config.
v4.0.0: New endpoint for channel key management.
Note: Does not affect environment variables.
"""
if location == "all":
clear_channel_key(location='user')
clear_channel_key(location='project')
elif location in ('user', 'project'):
clear_channel_key(location=location)
else:
raise HTTPException(400, "location must be 'user', 'project', or 'all'")
status = get_channel_status()
return {
"success": True,
"mode": status['mode'],
"still_configured": status['configured'],
"remaining_source": status.get('source'),
}
@app.post("/compare", response_model=CompareModesResponse)
async def api_compare_modes(request: CompareModesRequest):
"""
Compare LSB and DCT embedding modes for a carrier image.
NEW in v3.0: Returns capacity for both modes and recommendation.
Returns capacity for both modes and recommendation.
Optionally checks if a specific payload size would fit.
"""
try:
@@ -474,7 +738,7 @@ async def api_will_fit(request: WillFitRequest):
"""
Check if a payload of given size will fit in the carrier image.
NEW in v3.0: Supports both LSB and DCT modes.
Supports both LSB and DCT modes.
"""
try:
# Validate mode
@@ -555,17 +819,16 @@ async def api_generate(request: GenerateRequest):
raise HTTPException(400, f"rsa_bits must be one of {VALID_RSA_SIZES}")
try:
# v3.2.0: Call with passphrase_words parameter
creds = generate_credentials(
use_pin=request.use_pin,
use_rsa=request.use_rsa,
pin_length=request.pin_length,
rsa_bits=request.rsa_bits,
passphrase_words=request.words_per_passphrase, # Map API field to library parameter
passphrase_words=request.words_per_passphrase,
)
return GenerateResponse(
passphrase=creds.passphrase, # v3.2.0: Single passphrase
passphrase=creds.passphrase,
pin=creds.pin,
rsa_key_pem=creds.rsa_key_pem,
entropy={
@@ -626,15 +889,16 @@ async def api_encode(request: EncodeRequest):
Images must be base64-encoded. Returns base64-encoded stego image.
v4.0.0: Added channel_key parameter for deployment isolation.
v3.2.0: No date_str parameter needed - encode anytime!
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
"""
# Validate mode
if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
# Resolve channel key
resolved_channel_key = _resolve_channel_key(request.channel_key)
try:
ref_photo = base64.b64decode(request.reference_photo_base64)
carrier = base64.b64decode(request.carrier_image_base64)
@@ -647,17 +911,17 @@ async def api_encode(request: EncodeRequest):
request.dct_color_mode
)
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key
result = encode(
message=request.message,
reference_photo=ref_photo,
carrier_image=carrier,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase
passphrase=request.passphrase,
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
# date_str removed in v3.2.0
embed_mode=request.embed_mode,
channel_key=resolved_channel_key,
**dct_params,
)
@@ -669,6 +933,9 @@ async def api_encode(request: EncodeRequest):
request.dct_color_mode
)
# Get channel info for response
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
return EncodeResponse(
stego_image_base64=stego_b64,
filename=result.filename,
@@ -676,8 +943,10 @@ async def api_encode(request: EncodeRequest):
embed_mode=request.embed_mode,
output_format=output_format,
color_mode=color_mode,
date_used=None, # v3.2.0: No longer used
day_of_week=None, # v3.2.0: No longer used
channel_mode=channel_mode,
channel_fingerprint=channel_fingerprint,
date_used=None,
day_of_week=None,
)
except CapacityError as e:
@@ -695,15 +964,16 @@ async def api_encode_file(request: EncodeFileRequest):
File data must be base64-encoded.
v4.0.0: Added channel_key parameter for deployment isolation.
v3.2.0: No date_str parameter needed - encode anytime!
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format and dct_color_mode.
"""
# Validate mode
if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
# Resolve channel key
resolved_channel_key = _resolve_channel_key(request.channel_key)
try:
file_data = base64.b64decode(request.file_data_base64)
ref_photo = base64.b64decode(request.reference_photo_base64)
@@ -723,17 +993,17 @@ async def api_encode_file(request: EncodeFileRequest):
request.dct_color_mode
)
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key
result = encode(
message=payload,
reference_photo=ref_photo,
carrier_image=carrier,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase
passphrase=request.passphrase,
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
# date_str removed in v3.2.0
embed_mode=request.embed_mode,
channel_key=resolved_channel_key,
**dct_params,
)
@@ -745,6 +1015,9 @@ async def api_encode_file(request: EncodeFileRequest):
request.dct_color_mode
)
# Get channel info for response
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
return EncodeResponse(
stego_image_base64=stego_b64,
filename=result.filename,
@@ -752,8 +1025,10 @@ async def api_encode_file(request: EncodeFileRequest):
embed_mode=request.embed_mode,
output_format=output_format,
color_mode=color_mode,
date_used=None, # v3.2.0: No longer used
day_of_week=None, # v3.2.0: No longer used
channel_mode=channel_mode,
channel_fingerprint=channel_fingerprint,
date_used=None,
day_of_week=None,
)
except CapacityError as e:
@@ -775,32 +1050,31 @@ async def api_decode(request: DecodeRequest):
Returns payload_type to indicate if result is text or file.
v4.0.0: Added channel_key parameter - must match encoding key.
v3.2.0: No date_str parameter needed - decode anytime!
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
With 'auto' (default), tries LSB first then DCT.
Note: Extraction works regardless of whether the image was created with
color mode or grayscale mode - both use the same Y channel for data.
"""
# Validate mode
if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
# Resolve channel key
resolved_channel_key = _resolve_channel_key(request.channel_key)
try:
stego = base64.b64decode(request.stego_image_base64)
ref_photo = base64.b64decode(request.reference_photo_base64)
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key
result = decode(
stego_image=stego,
reference_photo=ref_photo,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase
passphrase=request.passphrase,
pin=request.pin,
rsa_key_data=rsa_key,
rsa_password=request.rsa_password,
embed_mode=request.embed_mode,
channel_key=resolved_channel_key,
)
if result.is_file:
@@ -817,6 +1091,10 @@ async def api_decode(request: DecodeRequest):
)
except DecryptionError as e:
# Provide helpful error message for channel key issues
error_msg = str(e)
if 'channel key' in error_msg.lower():
raise HTTPException(401, error_msg)
raise HTTPException(401, "Decryption failed. Check credentials.")
except StegasooError as e:
raise HTTPException(400, str(e))
@@ -839,9 +1117,9 @@ async def api_encode_multipart(
rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""),
# date_str removed in v3.2.0
# Channel key (v4.0.0)
channel_key: str = Form("auto", description="Channel key: 'auto'=server config, 'none'=public, 'XXXX-...'=explicit"),
embed_mode: str = Form("lsb"),
# NEW in v3.0.1
dct_output_format: str = Form("png"),
dct_color_mode: str = Form("grayscale"),
):
@@ -852,10 +1130,9 @@ async def api_encode_multipart(
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns the stego image directly with metadata headers.
v4.0.0: Added channel_key parameter for deployment isolation.
Use 'auto' for server config, 'none' for public mode.
v3.2.0: No date_str parameter needed - encode anytime!
NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct').
NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color').
"""
# Validate mode
if embed_mode not in ("lsb", "dct"):
@@ -869,6 +1146,15 @@ async def api_encode_multipart(
if dct_color_mode not in ("grayscale", "color"):
raise HTTPException(400, "dct_color_mode must be 'grayscale' or 'color'")
# Resolve channel key (v4.0.0)
# Form data: "auto" = use server config, "none" = public, otherwise explicit key
if channel_key.lower() == "auto":
resolved_channel_key = None # Auto mode
elif channel_key.lower() == "none":
resolved_channel_key = "" # Public mode
else:
resolved_channel_key = _resolve_channel_key(channel_key)
try:
ref_data = await reference_photo.read()
carrier_data = await carrier.read()
@@ -911,17 +1197,17 @@ async def api_encode_multipart(
# Get DCT parameters
dct_params = _get_dct_params(embed_mode, dct_output_format, dct_color_mode)
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key
result = encode(
message=payload,
reference_photo=ref_data,
carrier_image=carrier_data,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase
passphrase=passphrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=effective_password,
# date_str removed in v3.2.0
embed_mode=embed_mode,
channel_key=resolved_channel_key,
**dct_params,
)
@@ -929,17 +1215,26 @@ async def api_encode_multipart(
embed_mode, dct_output_format, dct_color_mode
)
# Get channel info for headers
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
headers = {
"Content-Disposition": f"attachment; filename={result.filename}",
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
"X-Stegasoo-Embed-Mode": embed_mode,
"X-Stegasoo-Output-Format": output_format,
"X-Stegasoo-Color-Mode": color_mode,
"X-Stegasoo-Channel-Mode": channel_mode,
"X-Stegasoo-Version": __version__,
}
if channel_fingerprint:
headers["X-Stegasoo-Channel-Fingerprint"] = channel_fingerprint
return Response(
content=result.stego_image,
media_type=mime_type,
headers={
"Content-Disposition": f"attachment; filename={result.filename}",
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
"X-Stegasoo-Embed-Mode": embed_mode,
"X-Stegasoo-Output-Format": output_format,
"X-Stegasoo-Color-Mode": color_mode,
"X-Stegasoo-Version": __version__,
}
headers=headers,
)
except CapacityError as e:
@@ -961,6 +1256,8 @@ async def api_decode_multipart(
rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""),
# Channel key (v4.0.0)
channel_key: str = Form("auto", description="Channel key: 'auto'=server config, 'none'=public, 'XXXX-...'=explicit"),
embed_mode: str = Form("auto"),
):
"""
@@ -969,11 +1266,9 @@ async def api_decode_multipart(
RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image).
Returns JSON with payload_type indicating text or file.
v4.0.0: Added channel_key parameter - must match what was used for encoding.
Use 'auto' for server config, 'none' for public mode.
v3.2.0: No date_str parameter needed - decode anytime!
NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct').
Note: Extraction works the same regardless of color mode used during encoding.
"""
# Validate mode
if embed_mode not in ("auto", "lsb", "dct"):
@@ -981,6 +1276,14 @@ async def api_decode_multipart(
if embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy")
# Resolve channel key (v4.0.0)
if channel_key.lower() == "auto":
resolved_channel_key = None # Auto mode
elif channel_key.lower() == "none":
resolved_channel_key = "" # Public mode
else:
resolved_channel_key = _resolve_channel_key(channel_key)
try:
ref_data = await reference_photo.read()
stego_data = await stego_image.read()
@@ -1007,15 +1310,16 @@ async def api_decode_multipart(
# QR code keys are never password-protected
effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key
result = decode(
stego_image=stego_data,
reference_photo=ref_data,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase
passphrase=passphrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=effective_password,
embed_mode=embed_mode,
channel_key=resolved_channel_key,
)
if result.is_file:
@@ -1031,7 +1335,10 @@ async def api_decode_multipart(
message=result.message
)
except DecryptionError:
except DecryptionError as e:
error_msg = str(e)
if 'channel key' in error_msg.lower():
raise HTTPException(401, error_msg)
raise HTTPException(401, "Decryption failed. Check credentials.")
except StegasooError as e:
raise HTTPException(400, str(e))
@@ -1053,7 +1360,7 @@ async def api_image_info(
"""
Get information about an image's capacity.
NEW in v3.0: Optionally includes capacity for both LSB and DCT modes.
Optionally includes capacity for both LSB and DCT modes.
"""
try:
image_data = await image.read()
@@ -1072,7 +1379,6 @@ async def api_image_info(
capacity_kb=capacity // 1024
)
# NEW in v3.0 - include mode comparison
if include_modes:
comparison = compare_modes(image_data)
response.modes = {

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env python3
"""
Stegasoo CLI - Command-line interface for steganography operations (v3.2.0).
Stegasoo CLI - Command-line interface for steganography operations (v4.0.0).
CHANGES in v4.0.0:
- Added channel key support for deployment/group isolation
- Messages encoded with a channel key can only be decoded with the same key
- New `channel` command group for key management
CHANGES in v3.2.0:
- Removed date dependency from all operations
@@ -16,6 +21,7 @@ Usage:
stegasoo info [OPTIONS]
stegasoo compare [OPTIONS]
stegasoo modes [OPTIONS]
stegasoo channel [SUBCOMMAND]
"""
import sys
@@ -64,6 +70,18 @@ from stegasoo import (
# Models
FilePayload,
# Channel key functions (v4.0.0)
generate_channel_key,
get_channel_key,
set_channel_key,
clear_channel_key,
has_channel_key,
get_channel_status,
validate_channel_key,
format_channel_key,
get_active_channel_key,
get_channel_fingerprint,
)
# Import constants - try main module first, then constants submodule
@@ -136,13 +154,13 @@ def cli():
- Reference photo (something you have)
- Passphrase (something you know)
- Static PIN or RSA key (additional security)
- Channel key (deployment/group isolation) [v4.0.0]
\b
Version 3.2.0 Changes:
- No more date parameters - encode/decode anytime!
- Simplified passphrase (no daily rotation)
- Default passphrase increased to 4 words
- True asynchronous communications
Version 4.0.0 Changes:
- Channel key support for group/deployment isolation
- Messages encoded with a channel key require the same key to decode
- New `stegasoo channel` command for key management
\b
Embedding Modes:
@@ -157,6 +175,60 @@ def cli():
pass
# ============================================================================
# CHANNEL KEY HELPERS
# ============================================================================
def resolve_channel_key_option(channel: Optional[str], channel_file: Optional[str],
no_channel: bool) -> Optional[str]:
"""
Resolve channel key from CLI options.
Returns:
None: Use server-configured key (auto mode)
"": Public mode (no channel key)
str: Explicit channel key
"""
if no_channel:
return "" # Public mode
if channel_file:
# Load from file
path = Path(channel_file)
if not path.exists():
raise click.ClickException(f"Channel key file not found: {channel_file}")
key = path.read_text().strip()
if not validate_channel_key(key):
raise click.ClickException(f"Invalid channel key format in file: {channel_file}")
return key
if channel:
if channel.lower() == 'auto':
return None # Use server config
# Explicit key provided
if not validate_channel_key(channel):
raise click.ClickException(
f"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n"
f"Generate a new key with: stegasoo channel generate"
)
return channel
# Default: use server-configured key (auto mode)
return None
def format_channel_status_line(quiet: bool = False) -> Optional[str]:
"""Get a one-line status for channel key configuration."""
if quiet:
return None
status = get_channel_status()
if status['mode'] == 'public':
return None
return f"Channel: {status['fingerprint']} ({status['source']})"
# ============================================================================
# GENERATE COMMAND
# ============================================================================
@@ -229,7 +301,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
# Pretty output
click.echo()
click.secho("=" * 60, fg='cyan')
click.secho(" STEGASOO CREDENTIALS (v3.2.0)", fg='cyan', bold=True)
click.secho(" STEGASOO CREDENTIALS (v4.0.0)", fg='cyan', bold=True)
click.secho("=" * 60, fg='cyan')
click.echo()
@@ -269,13 +341,278 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
click.secho(f" + photo entropy: 80-256 bits", dim=True)
click.echo()
click.secho("✓ v3.2.0: Use this passphrase anytime - no date needed!", fg='cyan')
# Show channel key status
if has_channel_key():
status = get_channel_status()
click.secho("─── CHANNEL KEY ───", fg='magenta')
click.echo(f" Status: Private mode")
click.echo(f" Fingerprint: {status['fingerprint']}")
click.secho(f" (configured via {status['source']})", dim=True)
click.echo()
click.secho("✓ v4.0.0: Use this passphrase anytime - no date needed!", fg='cyan')
click.echo()
except Exception as e:
raise click.ClickException(str(e))
# ============================================================================
# CHANNEL COMMAND GROUP (v4.0.0)
# ============================================================================
@cli.group()
def channel():
"""
Manage channel keys for deployment/group isolation.
Channel keys allow different deployments or groups to use Stegasoo
without being able to read each other's messages, even with identical
credentials.
\b
Key Storage (checked in order):
1. Environment variable: STEGASOO_CHANNEL_KEY
2. Project config: ./config/channel.key
3. User config: ~/.stegasoo/channel.key
\b
Subcommands:
generate Create a new channel key
show Display current channel key status
set Save a channel key to config file
clear Remove channel key from config
\b
Examples:
stegasoo channel generate
stegasoo channel show
stegasoo channel set XXXX-XXXX-...
stegasoo channel clear
"""
pass
@channel.command('generate')
@click.option('--save', '-s', is_flag=True, help='Save to user config (~/.stegasoo/channel.key)')
@click.option('--save-project', is_flag=True, help='Save to project config (./config/channel.key)')
@click.option('--env', '-e', is_flag=True, help='Output as environment variable export')
@click.option('--quiet', '-q', is_flag=True, help='Output only the key')
def channel_generate(save, save_project, env, quiet):
"""
Generate a new channel key.
Creates a cryptographically secure 256-bit channel key in the format:
XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
\b
Examples:
# Just display a new key
stegasoo channel generate
# Save to user config
stegasoo channel generate --save
# Output for .env file
stegasoo channel generate --env >> .env
# For scripts
KEY=$(stegasoo channel generate -q)
"""
key = generate_channel_key()
if save and save_project:
raise click.UsageError("Cannot use both --save and --save-project")
if save:
set_channel_key(key, location='user')
if not quiet:
click.secho("✓ Channel key saved to ~/.stegasoo/channel.key", fg='green')
click.echo()
if save_project:
set_channel_key(key, location='project')
if not quiet:
click.secho("✓ Channel key saved to ./config/channel.key", fg='green')
click.echo()
if env:
click.echo(f"STEGASOO_CHANNEL_KEY={key}")
elif quiet:
click.echo(key)
else:
click.echo()
click.secho("─── NEW CHANNEL KEY ───", fg='cyan', bold=True)
click.echo()
click.secho(f" {key}", fg='bright_yellow', bold=True)
click.echo()
fingerprint = f"{key[:4]}-••••-••••-••••-••••-••••-••••-{key[-4:]}"
click.echo(f" Fingerprint: {fingerprint}")
click.echo()
click.secho("Usage:", dim=True)
click.echo(" # Environment variable (recommended)")
click.echo(f" export STEGASOO_CHANNEL_KEY={key}")
click.echo()
click.echo(" # Or save to config")
click.echo(" stegasoo channel generate --save")
click.echo()
click.echo(" # Or add to .env file")
click.echo(" stegasoo channel generate --env >> .env")
click.echo()
@channel.command('show')
@click.option('--reveal', '-r', is_flag=True, help='Show full key (not just fingerprint)')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def channel_show(reveal, as_json):
"""
Display current channel key status.
Shows whether a channel key is configured and where it comes from.
By default shows only fingerprint; use --reveal to see full key.
\b
Examples:
stegasoo channel show
stegasoo channel show --reveal
stegasoo channel show --json
"""
status = get_channel_status()
if as_json:
import json
output = {
'mode': status['mode'],
'configured': status['configured'],
'fingerprint': status.get('fingerprint'),
'source': status.get('source'),
}
if reveal and status['configured']:
output['key'] = status.get('key')
click.echo(json.dumps(output, indent=2))
return
click.echo()
click.secho("─── CHANNEL KEY STATUS ───", fg='cyan', bold=True)
click.echo()
if status['mode'] == 'public':
click.secho(" Mode: PUBLIC", fg='yellow', bold=True)
click.echo(" No channel key configured.")
click.echo()
click.secho(" Messages can be read by any Stegasoo installation", dim=True)
click.secho(" with matching credentials.", dim=True)
else:
click.secho(" Mode: PRIVATE", fg='green', bold=True)
click.echo(f" Fingerprint: {status['fingerprint']}")
click.echo(f" Source: {status['source']}")
if reveal:
click.echo()
click.secho(f" Full key: {status['key']}", fg='bright_yellow')
click.echo()
click.secho(" Messages require this channel key to decode.", dim=True)
click.echo()
@channel.command('set')
@click.argument('key', required=False)
@click.option('--file', '-f', 'key_file', type=click.Path(exists=True), help='Read key from file')
@click.option('--project', '-p', is_flag=True, help='Save to project config instead of user config')
def channel_set(key, key_file, project):
"""
Save a channel key to config file.
Saves to user config (~/.stegasoo/channel.key) by default,
or project config (./config/channel.key) with --project.
\b
Examples:
stegasoo channel set XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
stegasoo channel set --file channel.key
stegasoo channel set XXXX-... --project
"""
if not key and not key_file:
raise click.UsageError("Must provide KEY argument or --file option")
if key and key_file:
raise click.UsageError("Cannot use both KEY argument and --file option")
if key_file:
key = Path(key_file).read_text().strip()
if not validate_channel_key(key):
raise click.ClickException(
f"Invalid channel key format.\n"
f"Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n"
f"Generate a new key with: stegasoo channel generate"
)
location = 'project' if project else 'user'
set_channel_key(key, location=location)
status = get_channel_status()
click.secho(f"✓ Channel key saved", fg='green')
click.echo(f" Location: {status['source']}")
click.echo(f" Fingerprint: {status['fingerprint']}")
@channel.command('clear')
@click.option('--project', '-p', is_flag=True, help='Clear project config instead of user config')
@click.option('--all', 'clear_all', is_flag=True, help='Clear both user and project configs')
@click.option('--force', '-f', is_flag=True, help='Skip confirmation')
def channel_clear(project, clear_all, force):
"""
Remove channel key from config.
Clears user config by default. Use --project for project config,
or --all to clear both.
Note: This does not affect environment variables.
\b
Examples:
stegasoo channel clear
stegasoo channel clear --project
stegasoo channel clear --all
"""
if not force:
if clear_all:
msg = "Clear channel key from both user and project configs?"
elif project:
msg = "Clear channel key from project config (./config/channel.key)?"
else:
msg = "Clear channel key from user config (~/.stegasoo/channel.key)?"
if not click.confirm(msg):
click.echo("Cancelled.")
return
if clear_all:
clear_channel_key(location='user')
clear_channel_key(location='project')
click.secho("✓ Cleared channel key from user and project configs", fg='green')
elif project:
clear_channel_key(location='project')
click.secho("✓ Cleared channel key from project config", fg='green')
else:
clear_channel_key(location='user')
click.secho("✓ Cleared channel key from user config", fg='green')
# Show current status
status = get_channel_status()
if status['configured']:
click.echo()
click.secho(f"Note: Channel key still active from {status['source']}", fg='yellow')
click.echo(f" Fingerprint: {status['fingerprint']}")
else:
click.echo(" Mode is now: PUBLIC")
# ============================================================================
# ENCODE COMMAND
# ============================================================================
@@ -291,6 +628,9 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)')
@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file')
@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)')
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb',
help='Embedding mode: lsb (default, color) or dct (requires scipy)')
@@ -300,18 +640,23 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
help='DCT color mode: grayscale (default) or color (preserves original colors)')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, key, key_qr,
key_password, output, embed_mode, dct_output_format, dct_color_mode, quiet):
key_password, channel_key, channel_file, no_channel, output, embed_mode,
dct_output_format, dct_color_mode, quiet):
"""
Encode a secret message or file into an image.
Requires a reference photo, carrier image, and passphrase.
Must provide either --pin or --key/--key-qr (or both).
v4.0.0: Channel key support for deployment isolation.
v3.2.0: No --date parameter needed! Encode and decode anytime.
For text messages, use -m or -f or pipe via stdin.
For binary files, use -e/--embed-file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
\b
Channel Key Options:
(no option) Use server-configured key (auto mode)
--channel KEY Use explicit channel key
--channel-file F Read channel key from file
--no-channel Force public mode (no isolation)
\b
Embedding Modes:
@@ -324,25 +669,17 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
- Lower capacity (~75 KB/megapixel)
- Better resistance to visual analysis
\b
DCT Options:
--dct-format png Lossless output (default)
--dct-format jpeg Smaller file, more natural appearance
--dct-color grayscale Convert to grayscale (default, traditional)
--dct-color color Preserve original colors (experimental)
\b
Examples:
# Text message with PIN (LSB mode, default)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" --pin 123456 -m "secret"
# Text message with PIN (auto channel key)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
# DCT mode - grayscale PNG (traditional)
stegasoo encode -r photo.jpg -c meme.png -p "secure words here now" --pin 123456 -m "secret" --mode dct
# Explicit channel key
stegasoo encode -r photo.jpg -c meme.png -p "words here" --pin 123456 -m "msg" \\
--channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
# DCT mode - color JPEG
stegasoo encode -r photo.jpg -c meme.png -p "my strong passphrase" --pin 123456 -m "secret" \\
--mode dct --dct-color color --dct-format jpeg
# Public mode (no channel key)
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "msg" --no-channel
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -356,6 +693,12 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
if not quiet:
click.secho("Note: --dct-format and --dct-color only apply to DCT mode", fg='yellow', dim=True)
# Resolve channel key
try:
resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel)
except click.ClickException:
raise
# Determine what to encode
payload = None
@@ -431,8 +774,18 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
if embed_mode == 'dct':
mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})"
click.echo(f"Mode: {mode_desc} ({fit_check['usage_percent']:.1f}% capacity)")
# Show channel status
channel_status = format_channel_status_line()
if resolved_channel_key == "":
click.echo("Channel: PUBLIC (no isolation)")
elif resolved_channel_key:
fingerprint = f"{resolved_channel_key[:4]}-••••-...-{resolved_channel_key[-4:]}"
click.echo(f"Channel: {fingerprint} (explicit)")
elif channel_status:
click.echo(channel_status)
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key parameter
result = encode(
message=payload,
reference_photo=ref_photo,
@@ -444,6 +797,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
embed_mode=embed_mode,
dct_output_format=dct_output_format,
dct_color_mode=dct_color_mode,
channel_key=resolved_channel_key,
)
# Determine output path
@@ -485,43 +839,43 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)')
@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file')
@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)')
@click.option('--output', '-o', type=click.Path(), help='Save decoded content to file')
@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct')
@click.option('--quiet', '-q', is_flag=True, help='Output only the content (for text) or suppress messages (for files)')
@click.option('--force', is_flag=True, help='Overwrite existing output file')
def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, embed_mode, quiet, force):
def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, channel_key, channel_file,
no_channel, output, embed_mode, quiet, force):
"""
Decode a secret message or file from a stego image.
Must use the same credentials that were used for encoding.
Automatically detects whether content is text or a file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
v4.0.0: Channel key support - must match what was used for encoding.
v3.2.0: No --date parameter needed! Just use your passphrase.
Note: Extraction works the same regardless of whether the image was
created with color mode or grayscale mode - both use the same Y channel.
\b
Extraction Modes:
--mode auto Auto-detect (default) - tries LSB first, then DCT
--mode lsb Only try LSB extraction
--mode dct Only try DCT extraction (requires scipy)
Channel Key Options:
(no option) Use server-configured key (auto mode)
--channel KEY Use explicit channel key
--channel-file F Read channel key from file
--no-channel Force public mode (for images encoded without channel key)
\b
Examples:
# Decode with PIN (auto-detect mode)
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" --pin 123456
# Decode with auto channel key
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
# Explicitly specify DCT mode
stegasoo decode -r photo.jpg -s stego.png -p "my passphrase here" --pin 123456 --mode dct
# Decode with explicit channel key
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 \\
--channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
# Decode with RSA key file
stegasoo decode -r photo.jpg -s stego.png -p "strong words" -k mykey.pem
# Save output to file
stegasoo decode -r photo.jpg -s stego.png -p "passphrase" --pin 123456 -o output.txt
# Decode public image (no channel key was used)
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 --no-channel
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -529,6 +883,12 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
"DCT mode requires scipy. Install with: pip install scipy"
)
# Resolve channel key
try:
resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel)
except click.ClickException:
raise
# Load key if provided (from .pem file or QR code image)
rsa_key_data = None
rsa_key_from_qr = False
@@ -563,7 +923,7 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
ref_photo = Path(ref).read_bytes()
stego_image = Path(stego).read_bytes()
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key parameter
result = decode(
stego_image=stego_image,
reference_photo=ref_photo,
@@ -572,6 +932,7 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
embed_mode=embed_mode,
channel_key=resolved_channel_key,
)
if result.is_file:
@@ -612,6 +973,10 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
click.echo(result.message)
except (DecryptionError, ExtractionError) as e:
# Provide helpful hints for channel key mismatches
error_msg = str(e)
if 'channel key' in error_msg.lower():
raise click.ClickException(error_msg)
raise click.ClickException(f"Decryption failed: {e}")
except StegasooError as e:
raise click.ClickException(str(e))
@@ -631,23 +996,29 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)')
@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file')
@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)')
@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, as_json):
def verify(ref, stego, passphrase, pin, key, key_qr, key_password, channel_key, channel_file,
no_channel, embed_mode, as_json):
"""
Verify that a stego image can be decoded without extracting the message.
Quick check to validate credentials are correct and data is intact.
Does NOT output the actual message content.
v4.0.0: Also verifies channel key matches.
\b
Examples:
stegasoo verify -r photo.jpg -s stego.png -p "my passphrase" --pin 123456
stegasoo verify -r photo.jpg -s stego.png -p "words here" -k mykey.pem --json
stegasoo verify -r photo.jpg -s stego.png -p "test phrase" --pin 123456 --mode dct
stegasoo verify -r photo.jpg -s stego.png -p "test phrase" --pin 123456 --no-channel
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -655,6 +1026,12 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a
"DCT mode requires scipy. Install with: pip install scipy"
)
# Resolve channel key
try:
resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel)
except click.ClickException:
raise
# Load key if provided
rsa_key_data = None
rsa_key_from_qr = False
@@ -685,7 +1062,7 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a
ref_photo = Path(ref).read_bytes()
stego_image = Path(stego).read_bytes()
# Attempt to decode
# Attempt to decode (v4.0.0: with channel_key)
result = decode(
stego_image=stego_image,
reference_photo=ref_photo,
@@ -694,51 +1071,44 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
embed_mode=embed_mode,
channel_key=resolved_channel_key,
)
# Calculate payload size
if result.is_file:
payload_size = len(result.file_data) if result.file_data else 0
payload_type = "file"
payload_desc = result.filename or "unnamed file"
if result.mime_type:
payload_desc += f" ({result.mime_type})"
payload_size = len(result.file_data)
content_type = result.mime_type or 'file'
else:
payload_size = len(result.message.encode('utf-8')) if result.message else 0
payload_type = "text"
payload_desc = f"{payload_size} bytes"
payload_size = len(result.message.encode('utf-8'))
content_type = 'text'
if as_json:
import json
output_data = {
"valid": True,
"stego_file": stego,
"payload_type": payload_type,
"payload_size": payload_size,
output = {
'valid': True,
'content_type': content_type,
'payload_size': payload_size,
'filename': result.filename if result.is_file else None,
}
if result.is_file:
output_data["filename"] = result.filename
output_data["mime_type"] = result.mime_type
click.echo(json.dumps(output_data, indent=2))
click.echo(json.dumps(output, indent=2))
else:
click.secho("✓ Valid stego image", fg='green', bold=True)
click.echo(f" Payload: {payload_type} ({payload_desc})")
click.echo(f" Size: {payload_size:,} bytes")
click.secho("✓ Verification successful!", fg='green')
click.echo(f" Content type: {content_type}")
click.echo(f" Payload size: {payload_size:,} bytes")
if result.is_file and result.filename:
click.echo(f" Filename: {result.filename}")
except (DecryptionError, ExtractionError) as e:
if as_json:
import json
output_data = {
"valid": False,
"stego_file": stego,
"error": str(e),
output = {
'valid': False,
'error': str(e),
}
click.echo(json.dumps(output_data, indent=2))
click.echo(json.dumps(output, indent=2))
sys.exit(1)
else:
click.secho("Verification failed", fg='red', bold=True)
click.echo(f" Error: {e}")
sys.exit(1)
raise click.ClickException(f"Verification failed: {e}")
except StegasooError as e:
raise click.ClickException(str(e))
except Exception as e:
@@ -754,64 +1124,38 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def info(image, as_json):
"""
Show information about an image.
Show information about an image file.
Displays dimensions, capacity for both LSB and DCT modes.
Displays dimensions, format, capacity estimates for different modes,
and whether the image appears suitable as a carrier.
\b
Examples:
stegasoo info photo.png
stegasoo info carrier.jpg --json
"""
try:
image_data = Path(image).read_bytes()
result = validate_image(image_data, check_size=False)
if not result.is_valid:
raise click.ClickException(result.error_message)
# Get capacity comparison
comparison = compare_modes(image_data)
img_info = get_image_info(image_data)
if as_json:
import json
output_data = {
"file": image,
"width": result.details['width'],
"height": result.details['height'],
"pixels": result.details['pixels'],
"mode": result.details['mode'],
"format": result.details['format'],
"capacity": {
"lsb": {
"bytes": comparison['lsb']['capacity_bytes'],
"kb": round(comparison['lsb']['capacity_kb'], 1),
},
"dct": {
"bytes": comparison['dct']['capacity_bytes'],
"kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'],
"ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1),
"output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"],
},
},
}
click.echo(json.dumps(output_data, indent=2))
click.echo(json.dumps(img_info, indent=2))
return
click.echo()
click.secho(f"Image: {image}", bold=True)
click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}")
click.echo(f" Pixels: {result.details['pixels']:,}")
click.echo(f" Mode: {result.details['mode']}")
click.echo(f" Format: {result.details['format']}")
click.echo()
click.secho(f"=== Image Info: {image} ===", fg='cyan', bold=True)
click.echo(f" Format: {img_info.get('format', 'Unknown')}")
click.echo(f" Dimensions: {img_info.get('width', '?')} × {img_info.get('height', '?')}")
click.echo(f" Mode: {img_info.get('mode', '?')}")
click.echo(f" Size: {len(image_data):,} bytes")
click.secho(" Capacity:", bold=True)
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
dct_status = "" if comparison['dct']['available'] else "✗ (scipy not installed)"
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}")
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB")
if comparison['dct']['available']:
click.secho(" DCT options: grayscale/color, png/jpeg", dim=True)
if 'lsb_capacity' in img_info:
click.echo()
click.secho(" Capacity Estimates:", fg='green')
click.echo(f" LSB mode: {img_info['lsb_capacity']:,} bytes")
if 'dct_capacity' in img_info:
click.echo(f" DCT mode: {img_info['dct_capacity']:,} bytes")
click.echo()
@@ -825,24 +1169,32 @@ def info(image, as_json):
@cli.command()
@click.argument('image', type=click.Path(exists=True))
@click.option('--payload-size', '-s', type=int, help='Check if specific payload size fits')
@click.option('--payload', '-p', type=click.Path(exists=True), help='Check if this file would fit')
@click.option('--size', '-s', type=int, help='Check if this many bytes would fit')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def compare(image, payload_size, as_json):
def compare(image, payload, size, as_json):
"""
Compare LSB and DCT embedding modes for an image.
Compare embedding mode capacities for an image.
Shows capacity for each mode and recommends which to use.
Optionally checks if a specific payload size would fit.
Shows LSB vs DCT capacity and helps choose the right mode.
Optionally checks if a specific payload would fit.
\b
Examples:
stegasoo compare carrier.png
stegasoo compare carrier.png --payload-size 50000
stegasoo compare carrier.png --json
stegasoo compare carrier.png --payload secret.pdf
stegasoo compare carrier.png --size 50000
"""
try:
image_data = Path(image).read_bytes()
# Get payload size if provided
payload_size = None
if payload:
payload_size = len(Path(payload).read_bytes())
elif size:
payload_size = size
comparison = compare_modes(image_data)
if as_json:
@@ -1004,7 +1356,7 @@ def modes():
Displays which modes are available and their characteristics.
"""
click.echo()
click.secho("=== Stegasoo Embedding Modes (v3.2.0) ===", fg='cyan', bold=True)
click.secho("=== Stegasoo Embedding Modes (v4.0.0) ===", fg='cyan', bold=True)
click.echo()
# LSB Mode
@@ -1039,24 +1391,41 @@ def modes():
click.echo(" --dct-color color Preserves original colors")
click.echo()
# v3.2.0 Note
click.secho(" v3.2.0 Changes:", fg='cyan', bold=True)
click.echo(" ✓ No date parameters needed")
click.echo(" ✓ Single passphrase (no daily rotation)")
click.echo(" ✓ Default passphrase increased to 4 words")
click.echo(" ✓ True asynchronous communications")
# Channel Key Status (v4.0.0)
click.secho(" Channel Key (v4.0.0)", fg='cyan', bold=True)
status = get_channel_status()
if status['mode'] == 'public':
click.echo(" Status: PUBLIC (no key configured)")
click.echo(" Effect: Messages readable by any installation")
else:
click.echo(" Status: PRIVATE")
click.echo(f" Fingerprint: {status['fingerprint']}")
click.echo(f" Source: {status['source']}")
click.echo(" Effect: Messages isolated to this channel")
click.echo()
click.echo(" CLI flags:")
click.echo(" --channel KEY Use explicit channel key")
click.echo(" --channel-file F Read key from file")
click.echo(" --no-channel Force public mode")
click.echo()
# v4.0.0 Changes
click.secho(" v4.0.0 Changes:", fg='cyan', bold=True)
click.echo(" ✓ Channel key support for deployment isolation")
click.echo(" ✓ New `stegasoo channel` command group")
click.echo(" ✓ Messages encoded with channel key require same key to decode")
click.echo()
# Examples
click.secho(" Examples:", dim=True)
click.echo(" # Traditional DCT (grayscale PNG)")
click.echo(" stegasoo encode ... --mode dct")
click.echo(" # Generate channel key")
click.echo(" stegasoo channel generate --save")
click.echo()
click.echo(" # Color-preserving DCT with JPEG output")
click.echo(" stegasoo encode ... --mode dct --dct-color color --dct-format jpeg")
click.echo(" # Encode with channel isolation")
click.echo(" stegasoo encode ... --channel XXXX-XXXX-...")
click.echo()
click.echo(" # Compare modes for an image")
click.echo(" stegasoo compare carrier.png")
click.echo(" # Decode public message (no channel key)")
click.echo(" stegasoo decode ... --no-channel")
click.echo()

1073
frontends/cli/main.py_old Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,16 @@
#!/usr/bin/env python3
"""
Stegasoo Web Frontend (v3.2.0)
Stegasoo Web Frontend (v4.0.0)
Flask-based web UI for steganography operations.
Supports both text messages and file embedding.
CHANGES in v4.0.0:
- Added channel key support for deployment/group isolation
- New /api/channel/status endpoint
- Channel key selector on encode/decode pages
- Messages encoded with channel key require same key to decode
CHANGES in v3.2.0:
- Removed date dependency from all operations
- Renamed day_phrase → passphrase
@@ -52,6 +58,11 @@ from stegasoo import (
EMBED_MODE_DCT,
EMBED_MODE_AUTO,
has_dct_support,
# Channel key functions (v4.0.0)
has_channel_key,
get_channel_status,
validate_channel_key,
generate_channel_key,
# NOTE: encode, decode, compare_modes, will_fit_by_mode now use subprocess isolation
)
from stegasoo.constants import (
@@ -126,6 +137,9 @@ THUMBNAIL_FILES: dict[str, bytes] = {}
@app.context_processor
def inject_globals():
"""Inject global variables into all templates."""
# Get channel status (v4.0.0)
channel_status = get_channel_status()
return {
'version': __version__,
'max_message_chars': MAX_MESSAGE_CHARS,
@@ -140,6 +154,11 @@ def inject_globals():
'default_passphrase_words': DEFAULT_PASSPHRASE_WORDS,
# NEW in v3.0
'has_dct': has_dct_support(),
# NEW in v4.0.0 - Channel key status
'channel_mode': channel_status['mode'],
'channel_configured': channel_status['configured'],
'channel_fingerprint': channel_status.get('fingerprint'),
'channel_source': channel_status.get('source'),
}
@@ -154,6 +173,13 @@ try:
print(f"DCT support: {has_dct_support()}")
print(f"QR code support: write={HAS_QRCODE}, read={HAS_QRCODE_READ}")
# Channel key status (v4.0.0)
channel_status = get_channel_status()
print(f"Channel key: {channel_status['mode']} mode")
if channel_status['configured']:
print(f" Fingerprint: {channel_status.get('fingerprint')}")
print(f" Source: {channel_status.get('source')}")
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
@@ -164,6 +190,33 @@ except Exception as e:
print(f"Could not override stegasoo limits: {e}")
# ============================================================================
# CHANNEL KEY HELPER (v4.0.0)
# ============================================================================
def resolve_channel_key_form(channel_key_value: str) -> str:
"""
Resolve channel key from form input.
Args:
channel_key_value: Form value ('auto', 'none', or explicit key)
Returns:
Value to pass to subprocess_stego ('auto', 'none', or explicit key)
"""
if not channel_key_value or channel_key_value == 'auto':
return 'auto'
elif channel_key_value == 'none':
return 'none'
else:
# Explicit key - validate format
if validate_channel_key(channel_key_value):
return channel_key_value
else:
# Invalid format, fall back to auto
return 'auto'
def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
"""Generate thumbnail from image data."""
try:
@@ -233,6 +286,71 @@ def index():
return render_template('index.html')
# ============================================================================
# CHANNEL KEY API (v4.0.0)
# ============================================================================
@app.route('/api/channel/status')
def api_channel_status():
"""
Get current channel key status (v4.0.0).
Returns JSON with mode, fingerprint, and source.
"""
# Use subprocess for isolation
result = subprocess_stego.get_channel_status(reveal=False)
if result.success:
return jsonify({
'success': True,
'mode': result.mode,
'configured': result.configured,
'fingerprint': result.fingerprint,
'source': result.source,
})
else:
# Fallback to direct call if subprocess fails
status = get_channel_status()
return jsonify({
'success': True,
'mode': status['mode'],
'configured': status['configured'],
'fingerprint': status.get('fingerprint'),
'source': status.get('source'),
})
@app.route('/api/channel/validate', methods=['POST'])
def api_channel_validate():
"""
Validate a channel key format (v4.0.0).
Returns JSON with validation result.
"""
key = request.form.get('key', '') or request.json.get('key', '') if request.is_json else ''
if not key:
return jsonify({'valid': False, 'error': 'No key provided'})
is_valid = validate_channel_key(key)
if is_valid:
fingerprint = f"{key[:4]}-••••-••••-••••-••••-••••-••••-{key[-4:]}"
return jsonify({
'valid': True,
'fingerprint': fingerprint,
})
else:
return jsonify({
'valid': False,
'error': 'Invalid format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX',
})
# ============================================================================
# GENERATE
# ============================================================================
@app.route('/generate', methods=['GET', 'POST'])
def generate():
if request.method == 'POST':
@@ -614,6 +732,9 @@ def encode_page():
if dct_color_mode not in ('grayscale', 'color'):
dct_color_mode = 'color'
# NEW in v4.0.0 - Channel key
channel_key = resolve_channel_key_form(request.form.get('channel_key', 'auto'))
# Check DCT availability
if embed_mode == 'dct' and not has_dct_support():
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
@@ -708,7 +829,7 @@ def encode_page():
flash(result.error_message, 'error')
return render_template('encode.html', has_qrcode_read=HAS_QRCODE_READ)
# v3.2.0: No date parameter needed
# v4.0.0: Include channel_key parameter
# Use subprocess-isolated encode to prevent crashes
if payload_type == 'file' and payload_file and payload_file.filename:
encode_result = subprocess_stego.encode(
@@ -724,6 +845,7 @@ def encode_page():
embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else 'png',
dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color',
channel_key=channel_key, # v4.0.0
)
else:
encode_result = subprocess_stego.encode(
@@ -737,6 +859,7 @@ def encode_page():
embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else 'png',
dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color',
channel_key=channel_key, # v4.0.0
)
# Check for subprocess errors
@@ -772,6 +895,9 @@ def encode_page():
'output_format': dct_output_format if embed_mode == 'dct' else 'png',
'color_mode': dct_color_mode if embed_mode == 'dct' else None,
'mime_type': output_mime,
# Channel info (v4.0.0)
'channel_mode': encode_result.channel_mode,
'channel_fingerprint': encode_result.channel_fingerprint,
}
return redirect(url_for('encode_result', file_id=file_id))
@@ -812,6 +938,9 @@ def encode_result(file_id):
embed_mode=file_info.get('embed_mode', 'lsb'),
output_format=file_info.get('output_format', 'png'),
color_mode=file_info.get('color_mode'),
# Channel info (v4.0.0)
channel_mode=file_info.get('channel_mode', 'public'),
channel_fingerprint=file_info.get('channel_fingerprint'),
)
@@ -901,6 +1030,9 @@ def decode_page():
if embed_mode not in ('auto', 'lsb', 'dct'):
embed_mode = 'auto'
# NEW in v4.0.0 - Channel key
channel_key = resolve_channel_key_form(request.form.get('channel_key', 'auto'))
# Check DCT availability
if embed_mode == 'dct' and not has_dct_support():
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
@@ -957,7 +1089,7 @@ def decode_page():
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# v3.2.0: No date_str parameter needed
# v4.0.0: Include channel_key parameter
# Use subprocess-isolated decode to prevent crashes
decode_result = subprocess_stego.decode(
stego_data=stego_data,
@@ -967,11 +1099,16 @@ def decode_page():
rsa_key_data=rsa_key_data,
rsa_password=key_password,
embed_mode=embed_mode,
channel_key=channel_key, # v4.0.0
)
# Check for subprocess errors
if not decode_result.success:
error_msg = decode_result.error or 'Decoding failed'
# Check for channel key related errors
if 'channel key' in error_msg.lower():
flash(error_msg, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
if 'decrypt' in error_msg.lower() or decode_result.error_type == 'DecryptionError':
raise DecryptionError(error_msg)
raise StegasooError(error_msg)
@@ -1005,7 +1142,7 @@ def decode_page():
)
except DecryptionError:
flash('Decryption failed. Check your passphrase, PIN, RSA key, and reference photo.', 'error')
flash('Decryption failed. Check your passphrase, PIN, RSA key, reference photo, and channel key.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')

View File

@@ -696,6 +696,177 @@ const Stegasoo = {
adjust();
},
// ========================================================================
// CHANNEL KEY HANDLING (v4.0.0)
// ========================================================================
/**
* Generate a random channel key in format XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
* @returns {string} Generated key
*/
generateChannelKey() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let key = '';
for (let i = 0; i < 8; i++) {
if (i > 0) key += '-';
for (let j = 0; j < 4; j++) {
key += chars.charAt(Math.floor(Math.random() * chars.length));
}
}
return key;
},
/**
* Validate channel key format
* @param {string} key - Key to validate
* @returns {boolean} True if valid
*/
validateChannelKey(key) {
const pattern = /^[A-Z0-9]{4}(-[A-Z0-9]{4}){7}$/;
return pattern.test(key);
},
/**
* Format channel key input (auto-add dashes, uppercase)
* @param {HTMLInputElement} input - Input element
*/
formatChannelKeyInput(input) {
let value = input.value.toUpperCase();
const clean = value.replace(/-/g, '');
if (clean.length > 0 && clean.length <= 32) {
const formatted = clean.match(/.{1,4}/g)?.join('-') || clean;
if (formatted !== value && formatted.length <= 39) {
input.value = formatted;
} else {
input.value = value;
}
}
// Validate and show/hide error state
const isValid = this.validateChannelKey(input.value);
input.classList.toggle('is-invalid', input.value.length > 0 && !isValid);
},
/**
* Initialize channel key UI for encode/decode pages
* @param {Object} config - Configuration object
* @param {string} config.radioName - Name of radio buttons (default: 'channel_key')
* @param {string} config.customInputId - ID of custom key input container
* @param {string} config.keyInputId - ID of key input field
* @param {string} config.generateBtnId - ID of generate button (optional)
* @param {string} config.customRadioId - ID of custom radio button
* @param {string[]} config.cardIds - Array of card/label IDs for active class toggling
*/
initChannelKey(config = {}) {
const radioName = config.radioName || 'channel_key';
const customInputId = config.customInputId || 'channelCustomInput';
const keyInputId = config.keyInputId || 'channelKeyInput';
const generateBtnId = config.generateBtnId;
const customRadioId = config.customRadioId || 'channelCustom';
const cardIds = config.cardIds || [];
const radios = document.querySelectorAll(`input[name="${radioName}"]`);
const customInput = document.getElementById(customInputId);
const keyInput = document.getElementById(keyInputId);
const generateBtn = generateBtnId ? document.getElementById(generateBtnId) : null;
const customRadio = document.getElementById(customRadioId);
// Toggle active class on mode-btn cards
const updateActiveState = () => {
radios.forEach(radio => {
const card = radio.closest('.mode-btn');
if (card) {
card.classList.toggle('active', radio.checked);
}
});
};
// Show/hide custom input based on selection
radios.forEach(radio => {
radio.addEventListener('change', () => {
updateActiveState();
const isCustom = customRadio?.checked;
customInput?.classList.toggle('d-none', !isCustom);
if (isCustom && keyInput) {
keyInput.focus();
}
});
});
// Initial state
updateActiveState();
// Format and validate key input
keyInput?.addEventListener('input', () => {
this.formatChannelKeyInput(keyInput);
});
// Generate button (if present)
generateBtn?.addEventListener('click', () => {
if (keyInput) {
keyInput.value = this.generateChannelKey();
keyInput.classList.remove('is-invalid');
}
});
},
/**
* Handle form submission with channel key validation
* @param {HTMLFormElement} form - Form element
* @param {string} customRadioId - ID of custom radio button
* @param {string} keyInputId - ID of key input field
* @returns {boolean} True if valid, false to prevent submission
*/
validateChannelKeyOnSubmit(form, customRadioId, keyInputId) {
const customRadio = document.getElementById(customRadioId);
const keyInput = document.getElementById(keyInputId);
if (customRadio?.checked && keyInput) {
if (!this.validateChannelKey(keyInput.value)) {
keyInput.classList.add('is-invalid');
keyInput.focus();
return false;
}
// Set the radio value to the actual key for form submission
customRadio.value = keyInput.value;
}
return true;
},
/**
* Initialize standalone channel key generator (for generate page)
* @param {string} inputId - ID of generated key input
* @param {string} generateBtnId - ID of generate button
* @param {string} copyBtnId - ID of copy button
*/
initChannelKeyGenerator(inputId, generateBtnId, copyBtnId) {
const input = document.getElementById(inputId);
const generateBtn = document.getElementById(generateBtnId);
const copyBtn = document.getElementById(copyBtnId);
generateBtn?.addEventListener('click', () => {
if (input) {
input.value = this.generateChannelKey();
}
if (copyBtn) {
copyBtn.disabled = false;
}
});
copyBtn?.addEventListener('click', () => {
if (input?.value) {
navigator.clipboard.writeText(input.value).then(() => {
const icon = copyBtn.querySelector('i');
if (icon) {
icon.className = 'bi bi-check';
setTimeout(() => { icon.className = 'bi bi-clipboard'; }, 2000);
}
});
}
});
},
// ========================================================================
// INITIALIZATION HELPERS
// ========================================================================
@@ -707,8 +878,30 @@ const Stegasoo = {
this.initClipboardPaste(['input[name="carrier"]', 'input[name="reference_photo"]']);
this.initQrCropAnimation('rsaQrInput');
this.initCollapseChevrons();
this.initFormLoading('encodeForm', 'encodeBtn', 'Encoding...');
this.initPassphraseFontResize();
// Channel key (v4.0.0) - uses mode-btn style
this.initChannelKey({
customInputId: 'channelCustomInput',
keyInputId: 'channelKeyInput',
generateBtnId: 'channelKeyGenerate',
customRadioId: 'channelCustom',
cardIds: ['channelAutoCard', 'channelPublicCard', 'channelCustomCard']
});
// Form submission with channel key validation
const form = document.getElementById('encodeForm');
const btn = document.getElementById('encodeBtn');
form?.addEventListener('submit', (e) => {
if (!this.validateChannelKeyOnSubmit(form, 'channelCustom', 'channelKeyInput')) {
e.preventDefault();
return false;
}
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...';
}
});
},
initDecodePage() {
@@ -718,13 +911,36 @@ const Stegasoo = {
this.initClipboardPaste(['input[name="stego_image"]', 'input[name="reference_photo"]']);
this.initQrCropAnimation('rsaKeyQrInput');
this.initCollapseChevrons();
this.initFormLoading('decodeForm', 'decodeBtn', 'Decoding...');
this.initPassphraseFontResize();
// Channel key (v4.0.0) - uses mode-btn style
this.initChannelKey({
customInputId: 'channelCustomInputDec',
keyInputId: 'channelKeyInputDec',
customRadioId: 'channelCustomDec',
cardIds: ['channelAutoCardDec', 'channelPublicCardDec', 'channelCustomCardDec']
});
// Form submission with channel key validation and mode display
const form = document.getElementById('decodeForm');
const btn = document.getElementById('decodeBtn');
form?.addEventListener('submit', (e) => {
if (!this.validateChannelKeyOnSubmit(form, 'channelCustomDec', 'channelKeyInputDec')) {
e.preventDefault();
return false;
}
const selectedMode = document.querySelector('input[name="embed_mode"]:checked')?.value || 'auto';
if (btn) {
btn.disabled = true;
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Decoding (${selectedMode.toUpperCase()})...`;
}
});
},
initGeneratePage() {
this.initPasswordToggles();
// Generate page has mostly unique functionality
// Channel key generator (v4.0.0)
this.initChannelKeyGenerator('channelKeyGenerated', 'generateChannelKeyBtn', 'copyChannelKeyBtn');
}
};

View File

@@ -26,6 +26,23 @@
font-weight: 700 !important;
}
/* ----------------------------------------------------------------------------
Channel Card Icons (About page) - Contrast fix for gradient backgrounds
---------------------------------------------------------------------------- */
#channel-keys .card-header i.bi {
/* Add outline/shadow for visibility on gradient backgrounds */
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
drop-shadow(0 0 4px rgba(0, 0, 0, 0.5));
}
/* Override green Auto icon to white for better contrast */
#channel-keys .card-header i.bi-gear-fill.text-success {
color: #ffffff !important;
filter: drop-shadow(0 0 3px rgba(34, 197, 94, 0.8))
drop-shadow(0 0 6px rgba(34, 197, 94, 0.5))
drop-shadow(0 0 2px rgba(0, 0, 0, 0.6));
}
/* ----------------------------------------------------------------------------
Mode Selection Buttons (Compact)
---------------------------------------------------------------------------- */
@@ -34,11 +51,13 @@
border: 2px solid var(--border-light);
border-radius: 0.5rem;
padding: 0.75rem 1rem;
padding-left: 2.75rem; /* Make room for absolutely positioned radio */
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
position: relative; /* For absolute positioning of radio */
}
.mode-btn:hover {
@@ -52,10 +71,35 @@
}
.mode-btn .form-check-input {
margin-top: 0;
position: absolute;
left: 15px; /* Fixed distance from left edge of card */
top: 50%;
transform: translateY(-50%);
margin: 0;
flex-shrink: 0;
}
/* Remove ms-2 margin from first icon after radio since radio is now absolute */
.mode-btn > i.bi:first-of-type {
margin-left: 0 !important;
}
/* Equal-width mode buttons (ignores content length) */
.mode-btn.equal-width {
flex: 1 1 0;
min-width: 0;
}
/* ----------------------------------------------------------------------------
Security Factor Boxes - Matches drop-zone dashed border style
---------------------------------------------------------------------------- */
.security-box {
border: 2px dashed rgba(255, 255, 255, 0.2);
border-radius: 0.5rem;
padding: 1rem;
height: 100%;
}
.mode-info-icon {
cursor: help;
opacity: 0.6;

View File

@@ -1,10 +1,14 @@
#!/usr/bin/env python3
"""
Stegasoo Subprocess Worker
Stegasoo Subprocess Worker (v4.0.0)
This script runs in a subprocess and handles encode/decode operations.
If it crashes due to jpegio/scipy issues, the parent Flask process survives.
CHANGES in v4.0.0:
- Added channel_key support for encode/decode operations
- New channel_status operation
Communication is via JSON over stdin/stdout:
- Input: JSON object with operation parameters
- Output: JSON object with results or error
@@ -24,6 +28,49 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
sys.path.insert(0, str(Path(__file__).parent))
def _resolve_channel_key(channel_key_param):
"""
Resolve channel_key parameter to value for stegasoo.
Args:
channel_key_param: 'auto', 'none', explicit key, or None
Returns:
None (auto), "" (public), or explicit key string
"""
if channel_key_param is None or channel_key_param == "auto":
return None # Auto mode - use server config
elif channel_key_param == "none":
return "" # Public mode
else:
return channel_key_param # Explicit key
def _get_channel_info(resolved_key):
"""
Get channel mode and fingerprint for response.
Returns:
(mode, fingerprint) tuple
"""
from stegasoo import has_channel_key, get_channel_status
if resolved_key == "":
return "public", None
if resolved_key is not None:
# Explicit key
fingerprint = f"{resolved_key[:4]}-••••-••••-••••-••••-••••-••••-{resolved_key[-4:]}"
return "private", fingerprint
# Auto mode - check server config
if has_channel_key():
status = get_channel_status()
return "private", status.get('fingerprint')
return "public", None
def encode_operation(params: dict) -> dict:
"""Handle encode operation."""
from stegasoo import encode, FilePayload
@@ -48,6 +95,9 @@ def encode_operation(params: dict) -> dict:
else:
payload = params.get('message', '')
# Resolve channel key (v4.0.0)
resolved_channel_key = _resolve_channel_key(params.get('channel_key', 'auto'))
# Call encode with correct parameter names
result = encode(
message=payload,
@@ -60,6 +110,7 @@ def encode_operation(params: dict) -> dict:
embed_mode=params.get('embed_mode', 'lsb'),
dct_output_format=params.get('dct_output_format', 'png'),
dct_color_mode=params.get('dct_color_mode', 'color'),
channel_key=resolved_channel_key, # v4.0.0
)
# Build stats dict if available
@@ -71,11 +122,16 @@ def encode_operation(params: dict) -> dict:
'bytes_embedded': getattr(result.stats, 'bytes_embedded', 0),
}
# Get channel info for response (v4.0.0)
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
return {
'success': True,
'stego_b64': base64.b64encode(result.stego_image).decode('ascii'),
'filename': getattr(result, 'filename', None),
'stats': stats,
'channel_mode': channel_mode,
'channel_fingerprint': channel_fingerprint,
}
@@ -92,6 +148,9 @@ def decode_operation(params: dict) -> dict:
if params.get('rsa_key_b64'):
rsa_key_data = base64.b64decode(params['rsa_key_b64'])
# Resolve channel key (v4.0.0)
resolved_channel_key = _resolve_channel_key(params.get('channel_key', 'auto'))
# Call decode with correct parameter names
result = decode(
stego_image=stego_data,
@@ -101,6 +160,7 @@ def decode_operation(params: dict) -> dict:
rsa_key_data=rsa_key_data,
rsa_password=params.get('rsa_password'),
embed_mode=params.get('embed_mode', 'auto'),
channel_key=resolved_channel_key, # v4.0.0
)
if result.is_file:
@@ -150,6 +210,25 @@ def capacity_check_operation(params: dict) -> dict:
}
def channel_status_operation(params: dict) -> dict:
"""Handle channel status check (v4.0.0)."""
from stegasoo import get_channel_status
status = get_channel_status()
reveal = params.get('reveal', False)
return {
'success': True,
'status': {
'mode': status['mode'],
'configured': status['configured'],
'fingerprint': status.get('fingerprint'),
'source': status.get('source'),
'key': status.get('key') if reveal and status['configured'] else None,
}
}
def main():
"""Main entry point - read JSON from stdin, write JSON to stdout."""
try:
@@ -170,6 +249,8 @@ def main():
output = compare_operation(params)
elif operation == 'capacity':
output = capacity_check_operation(params)
elif operation == 'channel_status':
output = channel_status_operation(params)
else:
output = {'success': False, 'error': f'Unknown operation: {operation}'}

View File

@@ -1,15 +1,19 @@
"""
Subprocess Steganography Wrapper
Subprocess Steganography Wrapper (v4.0.0)
Runs stegasoo operations in isolated subprocesses to prevent crashes
from taking down the Flask server.
CHANGES in v4.0.0:
- Added channel_key parameter to encode() and decode() methods
- Channel keys enable deployment/group isolation
Usage:
from subprocess_stego import SubprocessStego
stego = SubprocessStego()
# Encode
# Encode with channel key
result = stego.encode(
carrier_data=carrier_bytes,
reference_data=ref_bytes,
@@ -17,6 +21,7 @@ Usage:
passphrase="my passphrase",
pin="123456",
embed_mode="dct",
channel_key="auto", # or "none", or explicit key
)
if result.success:
@@ -31,6 +36,7 @@ Usage:
reference_data=ref_bytes,
passphrase="my passphrase",
pin="123456",
channel_key="auto",
)
# Compare modes (capacity)
@@ -60,6 +66,9 @@ class EncodeResult:
stego_data: Optional[bytes] = None
filename: Optional[str] = None
stats: Optional[Dict[str, Any]] = None
# Channel info (v4.0.0)
channel_mode: Optional[str] = None
channel_fingerprint: Optional[str] = None
error: Optional[str] = None
error_type: Optional[str] = None
@@ -101,6 +110,18 @@ class CapacityResult:
error: Optional[str] = None
@dataclass
class ChannelStatusResult:
"""Result from channel status check (v4.0.0)."""
success: bool
mode: str = "public"
configured: bool = False
fingerprint: Optional[str] = None
source: Optional[str] = None
key: Optional[str] = None
error: Optional[str] = None
class SubprocessStego:
"""
Subprocess-isolated steganography operations.
@@ -205,6 +226,8 @@ class SubprocessStego:
embed_mode: str = "lsb",
dct_output_format: str = "png",
dct_color_mode: str = "color",
# Channel key (v4.0.0)
channel_key: Optional[str] = "auto",
timeout: Optional[int] = None,
) -> EncodeResult:
"""
@@ -224,6 +247,7 @@ class SubprocessStego:
embed_mode: 'lsb' or 'dct'
dct_output_format: 'png' or 'jpeg' (for DCT mode)
dct_color_mode: 'grayscale' or 'color' (for DCT mode)
channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
timeout: Operation timeout in seconds
Returns:
@@ -239,6 +263,7 @@ class SubprocessStego:
'embed_mode': embed_mode,
'dct_output_format': dct_output_format,
'dct_color_mode': dct_color_mode,
'channel_key': channel_key, # v4.0.0
}
if file_data:
@@ -258,6 +283,8 @@ class SubprocessStego:
stego_data=base64.b64decode(result['stego_b64']),
filename=result.get('filename'),
stats=result.get('stats'),
channel_mode=result.get('channel_mode'),
channel_fingerprint=result.get('channel_fingerprint'),
)
else:
return EncodeResult(
@@ -275,6 +302,8 @@ class SubprocessStego:
rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None,
embed_mode: str = "auto",
# Channel key (v4.0.0)
channel_key: Optional[str] = "auto",
timeout: Optional[int] = None,
) -> DecodeResult:
"""
@@ -288,6 +317,7 @@ class SubprocessStego:
rsa_key_data: Optional RSA key PEM bytes
rsa_password: RSA key password if encrypted
embed_mode: 'auto', 'lsb', or 'dct'
channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
timeout: Operation timeout in seconds
Returns:
@@ -300,6 +330,7 @@ class SubprocessStego:
'passphrase': passphrase,
'pin': pin,
'embed_mode': embed_mode,
'channel_key': channel_key, # v4.0.0
}
if rsa_key_data:
@@ -411,6 +442,44 @@ class SubprocessStego:
success=False,
error=result.get('error', 'Unknown error'),
)
def get_channel_status(
self,
reveal: bool = False,
timeout: Optional[int] = None,
) -> ChannelStatusResult:
"""
Get current channel key status (v4.0.0).
Args:
reveal: Include full key in response
timeout: Operation timeout in seconds
Returns:
ChannelStatusResult with channel info
"""
params = {
'operation': 'channel_status',
'reveal': reveal,
}
result = self._run_worker(params, timeout)
if result.get('success'):
status = result.get('status', {})
return ChannelStatusResult(
success=True,
mode=status.get('mode', 'public'),
configured=status.get('configured', False),
fingerprint=status.get('fingerprint'),
source=status.get('source'),
key=status.get('key') if reveal else None,
)
else:
return ChannelStatusResult(
success=False,
error=result.get('error', 'Unknown error'),
)
# Convenience function for quick usage

View File

@@ -11,8 +11,7 @@
</div>
<div class="card-body">
<p class="lead">
Stegasoo is a steganography tool that hides encrypted messages and files
inside ordinary images using multi-factor authentication.
Stegasoo hides encrypted messages and files inside images using multi-factor authentication.
</p>
<h6 class="text-primary mt-4 mb-3">Features</h6>
@@ -22,22 +21,22 @@
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Text &amp; File Embedding</strong>
<br><small class="text-muted">Hide messages or any file type (PDF, ZIP, documents)</small>
<br><small class="text-muted">Any file type: PDF, ZIP, documents</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Multi-Factor Security</strong>
<br><small class="text-muted">Combines photo + passphrase + PIN/RSA key</small>
<br><small class="text-muted">Photo + passphrase + PIN/RSA key</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>AES-256-GCM Encryption</strong>
<br><small class="text-muted">Authenticated encryption with integrity verification</small>
<br><small class="text-muted">Authenticated encryption with integrity check</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>LSB &amp; DCT Modes</strong>
<br><small class="text-muted">Choose capacity (LSB) or JPEG resilience (DCT)</small>
<strong>DCT &amp; LSB Modes</strong>
<br><small class="text-muted">JPEG resilience (DCT) or high capacity (LSB)</small>
</li>
</ul>
</div>
@@ -46,12 +45,12 @@
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Random Pixel Embedding</strong>
<br><small class="text-muted">Key-derived selection defeats statistical analysis</small>
<br><small class="text-muted">Defeats statistical analysis</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Large Image Support</strong>
<br><small class="text-muted">Up to {{ max_payload_kb }} KB payload, tested with 14MB+ images</small>
<br><small class="text-muted">Up to {{ max_payload_kb }} KB, tested with 14MB+ images</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
@@ -61,7 +60,13 @@
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>QR Code Keys</strong>
<br><small class="text-muted">Import/export RSA keys via QR codes</small>
<br><small class="text-muted">Import/export RSA keys via QR</small>
</li>
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
<strong>Channel Keys</strong>
<span class="badge bg-info ms-1">v4.0</span>
<br><small class="text-muted">Group/deployment isolation</small>
</li>
</ul>
</div>
@@ -75,67 +80,61 @@
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Embedding Modes</h5>
</div>
<div class="card-body">
<p>
Stegasoo supports two embedding modes, each optimized for different use cases.
</p>
<p>Two modes optimized for different use cases.</p>
<div class="row mt-4">
<!-- LSB Mode -->
<div class="col-md-6 mb-4">
<div class="card bg-dark h-100">
<div class="card-header">
<i class="bi bi-grid-3x3-gap text-primary me-2"></i>
<strong>LSB Mode</strong>
<span class="badge bg-success ms-2">Default</span>
</div>
<div class="card-body">
<p class="small">
<strong>LSB (Least Significant Bit)</strong> embeds data in the lowest bit
of each color channel. Changing the LSB changes pixel values by at most 1,
which is imperceptible to the human eye.
</p>
<ul class="small mb-0">
<li><strong>Capacity:</strong> ~375 KB per megapixel</li>
<li><strong>Output:</strong> PNG (lossless)</li>
<li><strong>Color:</strong> Full color preserved</li>
<li><strong>Speed:</strong> Fast (~0.5s)</li>
</ul>
<hr>
<div class="small">
<i class="bi bi-check-circle text-success me-1"></i> Email attachments<br>
<i class="bi bi-check-circle text-success me-1"></i> Cloud storage (Dropbox, Drive)<br>
<i class="bi bi-check-circle text-success me-1"></i> Direct file transfer<br>
<i class="bi bi-x-circle text-danger me-1"></i> Social media (recompresses)
</div>
</div>
</div>
</div>
<!-- DCT Mode -->
<div class="col-md-6 mb-4">
<div class="card bg-dark h-100">
<div class="card-header">
<i class="bi bi-soundwave text-warning me-2"></i>
<strong>DCT Mode</strong>
<span class="badge bg-success ms-2">Default</span>
</div>
<div class="card-body">
<p class="small">
<strong>DCT (Discrete Cosine Transform)</strong> embeds data in frequency
coefficients rather than raw pixels. This survives JPEG recompression
because coefficients are preserved during re-encoding.
<strong>DCT (Discrete Cosine Transform)</strong> embeds data in frequency coefficients. Survives JPEG recompression.
</p>
<ul class="small mb-0">
<li><strong>Capacity:</strong> ~75 KB per megapixel</li>
<li><strong>Capacity:</strong> ~75 KB/MP</li>
<li><strong>Output:</strong> JPEG or PNG</li>
<li><strong>Color:</strong> Color or grayscale</li>
<li><strong>Speed:</strong> Slower (~2s)</li>
<li><strong>Speed:</strong> ~2s</li>
</ul>
<hr>
<div class="small">
<i class="bi bi-check-circle text-success me-1"></i> Instagram, Facebook<br>
<i class="bi bi-check-circle text-success me-1"></i> WhatsApp, Signal, Telegram<br>
<i class="bi bi-check-circle text-success me-1"></i> Twitter/X<br>
<i class="bi bi-check-circle text-success me-1"></i> Any platform that recompresses
<i class="bi bi-check-circle text-success me-1"></i> Any recompressing platform
</div>
</div>
</div>
</div>
<!-- LSB Mode -->
<div class="col-md-6 mb-4">
<div class="card bg-dark h-100">
<div class="card-header">
<i class="bi bi-grid-3x3-gap text-primary me-2"></i>
<strong>LSB Mode</strong>
</div>
<div class="card-body">
<p class="small">
<strong>LSB (Least Significant Bit)</strong> embeds data in the lowest bit of each color channel. Imperceptible to the eye.
</p>
<ul class="small mb-0">
<li><strong>Capacity:</strong> ~375 KB/MP</li>
<li><strong>Output:</strong> PNG (lossless)</li>
<li><strong>Color:</strong> Full color</li>
<li><strong>Speed:</strong> ~0.5s</li>
</ul>
<hr>
<div class="small">
<i class="bi bi-check-circle text-success me-1"></i> Email attachments<br>
<i class="bi bi-check-circle text-success me-1"></i> Cloud storage<br>
<i class="bi bi-check-circle text-success me-1"></i> Direct file transfer<br>
<i class="bi bi-x-circle text-danger me-1"></i> Social media
</div>
</div>
</div>
@@ -149,35 +148,30 @@
<thead>
<tr>
<th>Aspect</th>
<th>DCT Mode <span class="badge bg-success ms-1">Default</span></th>
<th>LSB Mode</th>
<th>DCT Mode</th>
</tr>
</thead>
<tbody>
<tr>
<td>Capacity (1080p)</td>
<td class="text-success">~770 KB</td>
<td class="text-warning">~50 KB</td>
<td class="text-success">~770 KB</td>
</tr>
<tr>
<td>Survives JPEG</td>
<td class="text-danger">❌ No</td>
<td class="text-success">✅ Yes</td>
<td class="text-danger">❌ No</td>
</tr>
<tr>
<td>Social Media</td>
<td class="text-danger">❌ Broken</td>
<td class="text-success">✅ Works</td>
<td class="text-danger">❌ Broken</td>
</tr>
<tr>
<td>Detection Resistance</td>
<td>Moderate</td>
<td>Better</td>
</tr>
<tr>
<td>Dependencies</td>
<td>Pillow, NumPy</td>
<td>+ scipy, jpegio</td>
<td>Moderate</td>
</tr>
</tbody>
</table>
@@ -185,8 +179,7 @@
<div class="alert alert-info small mt-3 mb-0">
<i class="bi bi-lightbulb me-2"></i>
<strong>Auto-Detection:</strong> When decoding, Stegasoo automatically detects whether
LSB or DCT mode was used. You don't need to specify the mode during decoding.
<strong>Auto-Detection:</strong> Mode is detected automatically when decoding.
</div>
</div>
</div>
@@ -196,64 +189,149 @@
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>How Security Works</h5>
</div>
<div class="card-body">
<p>Stegasoo uses <strong>multi-factor authentication</strong> to derive encryption keys:</p>
<p>Multi-factor authentication derives encryption keys:</p>
<div class="row text-center my-4">
<div class="col-md-3 mb-3">
<div class="p-3 bg-dark rounded">
<div class="col-6 col-lg-3 mb-3">
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
<i class="bi bi-image text-info fs-2 d-block mb-2"></i>
<strong>Reference Photo</strong>
<div class="small text-muted mt-1">Something you have</div>
<div class="small text-success">~80-256 bits</div>
<div class="small text-success mt-auto pt-2">~80-256 bits</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="p-3 bg-dark rounded">
<div class="col-6 col-lg-3 mb-3">
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
<i class="bi bi-chat-quote text-warning fs-2 d-block mb-2"></i>
<strong>Passphrase</strong>
<div class="small text-muted mt-1">Something you know</div>
<div class="small text-success">~44 bits (4 words)</div>
<div class="small text-success mt-auto pt-2">~44 bits (4 words)</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="p-3 bg-dark rounded">
<div class="col-6 col-lg-3 mb-3">
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
<i class="bi bi-123 text-danger fs-2 d-block mb-2"></i>
<strong>Static PIN</strong>
<div class="small text-muted mt-1">Something you know</div>
<div class="small text-success">~20 bits (6 digits)</div>
<div class="small text-success mt-auto pt-2">~20 bits (6 digits)</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="p-3 bg-dark rounded">
<div class="col-6 col-lg-3 mb-3">
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
<i class="bi bi-key text-primary fs-2 d-block mb-2"></i>
<strong>RSA Key</strong>
<div class="small text-muted mt-1">Something you have (optional)</div>
<div class="small text-success">~128 bits</div>
<div class="small text-muted mt-1">Optional</div>
<div class="small text-success mt-auto pt-2">~128 bits</div>
</div>
</div>
</div>
<div class="alert alert-secondary">
<i class="bi bi-calculator me-2"></i>
<strong>Combined entropy:</strong> 144-424+ bits depending on configuration.
For reference, 128 bits is considered computationally infeasible to brute force.
<strong>Combined entropy:</strong> 144-424+ bits. 128 bits is infeasible to brute force.
</div>
<h6 class="mt-4">Key Derivation</h6>
<p>
{% if has_argon2 %}
<span class="badge bg-success me-1"><i class="bi bi-check"></i> Argon2id</span>
Using <strong>Argon2id</strong> with 256MB memory cost — memory-hard KDF that
makes GPU/ASIC attacks infeasible.
256MB memory cost. Memory-hard KDF defeats GPU/ASIC attacks.
{% else %}
<span class="badge bg-warning text-dark me-1"><i class="bi bi-exclamation-triangle"></i> Argon2 Not Available</span>
Falling back to <strong>PBKDF2-SHA512</strong> with 600,000 iterations.
Using PBKDF2-SHA512 with 600k iterations.
Install <code>argon2-cffi</code> for stronger security.
{% endif %}
</p>
</div>
</div>
<!-- Channel Keys (v4.0.0) -->
<div class="card mb-4" id="channel-keys">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-broadcast me-2"></i>Channel Keys
<span class="badge bg-info ms-2">v4.0</span>
</h5>
</div>
<div class="card-body">
<p>
Channel keys provide <strong>deployment/group isolation</strong>. Messages encoded with one channel key
cannot be decoded with a different key, even if all other credentials match.
</p>
<div class="row mt-4">
<!-- Auto Mode -->
<div class="col-md-4 mb-3">
<div class="card bg-dark h-100">
<div class="card-header text-center">
<i class="bi bi-gear-fill text-success fs-2 d-block mb-2"></i>
<strong>Auto</strong>
</div>
<div class="card-body">
<p class="small mb-2">Uses server-configured key if available, otherwise public mode.</p>
<ul class="small mb-0">
<li>Set via <code>STEGASOO_CHANNEL_KEY</code> env var</li>
<li>Or <code>channel_key</code> in config file</li>
<li>All users share the same channel</li>
</ul>
</div>
</div>
</div>
<!-- Public Mode -->
<div class="col-md-4 mb-3">
<div class="card bg-dark h-100">
<div class="card-header text-center">
<i class="bi bi-globe text-info fs-2 d-block mb-2"></i>
<strong>Public</strong>
</div>
<div class="card-body">
<p class="small mb-2">No channel key. Compatible with other public installations.</p>
<ul class="small mb-0">
<li>Default if no server key configured</li>
<li>Anyone can decode (with credentials)</li>
<li>Interoperable between deployments</li>
</ul>
</div>
</div>
</div>
<!-- Custom Mode -->
<div class="col-md-4 mb-3">
<div class="card bg-dark h-100">
<div class="card-header text-center">
<i class="bi bi-key-fill text-warning fs-2 d-block mb-2"></i>
<strong>Custom</strong>
</div>
<div class="card-body">
<p class="small mb-2">Your own group key. Share with recipients.</p>
<ul class="small mb-0">
<li>Format: <code>XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX</code></li>
<li>32 chars (128 bits entropy)</li>
<li>Private group communication</li>
</ul>
</div>
</div>
</div>
</div>
{% if channel_configured %}
<div class="alert alert-success mt-3 mb-0">
<i class="bi bi-shield-lock me-2"></i>
<strong>This server has a channel key configured:</strong>
<code class="ms-2">{{ channel_fingerprint }}</code>
<span class="text-muted ms-2">({{ channel_source }})</span>
</div>
{% else %}
<div class="alert alert-info mt-3 mb-0">
<i class="bi bi-info-circle me-2"></i>
This server is running in <strong>public mode</strong>.
Set <code>STEGASOO_CHANNEL_KEY</code> to enable server-wide channel isolation.
</div>
{% endif %}
</div>
</div>
<!-- Version History -->
<div class="card mb-4">
<div class="card-header">
@@ -272,18 +350,18 @@
<tr>
<td><strong>4.0.0</strong></td>
<td>
Simplified auth (no date dependency), passphrase replaces day_phrase,
4-word default, JPEG normalization fix, large image support (14MB+ tested),
subprocess isolation for stability, Python 3.10-3.12 required
<strong>Channel keys</strong> for group/deployment isolation,
DCT default, simplified auth, passphrase replaces day_phrase,
4-word default, JPEG fix, large image support, subprocess isolation, Python 3.10-3.12
</td>
</tr>
<tr>
<td>3.2.0</td>
<td>Single passphrase (removed day-of-week rotation), increased default words</td>
<td>Single passphrase, more default words</td>
</tr>
<tr>
<td>3.0.0</td>
<td>DCT steganography mode, JPEG output, color preservation option</td>
<td>DCT mode, JPEG output, color preservation</td>
</tr>
<tr>
<td>2.2.0</td>
@@ -291,11 +369,11 @@
</tr>
<tr>
<td>2.1.0</td>
<td>File embedding, compression support</td>
<td>File embedding, compression</td>
</tr>
<tr>
<td>2.0.0</td>
<td>Web UI, REST API, RSA key support</td>
<td>Web UI, REST API, RSA keys</td>
</tr>
<tr>
<td>1.0.0</td>
@@ -304,12 +382,6 @@
</tbody>
</table>
</div>
<div class="alert alert-warning small mt-3 mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Compatibility:</strong> v4.0 cannot decode messages from v3.1 or earlier (different format).
Messages encoded with v3.2 should decode correctly.
</div>
</div>
</div>
@@ -329,11 +401,11 @@
<div id="setup" class="accordion-collapse collapse show" data-bs-parent="#usageAccordion">
<div class="accordion-body">
<ol>
<li>Both parties agree on a <strong>reference photo</strong> (shared secretly, never transmitted)</li>
<li>Go to <a href="/generate">Generate</a> and create credentials</li>
<li><strong>Memorize</strong> the passphrase and PIN</li>
<li>If using RSA, download and securely store the key file</li>
<li>Share credentials with your contact through a secure channel</li>
<li>Agree on a <strong>reference photo</strong> (never transmitted)</li>
<li>Go to <a href="/generate">Generate</a> to create credentials</li>
<li>Memorize passphrase and PIN</li>
<li>If using RSA, store the key file securely</li>
<li>Share credentials via secure channel</li>
</ol>
</div>
</div>
@@ -343,24 +415,23 @@
<h2 class="accordion-header">
<button class="accordion-button collapsed bg-dark text-light" type="button"
data-bs-toggle="collapse" data-bs-target="#encoding">
<i class="bi bi-2-circle me-2"></i>Encoding a Message
<i class="bi bi-2-circle me-2"></i>Encoding
</button>
</h2>
<div id="encoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
<div class="accordion-body">
<ol>
<li>Go to <a href="/encode">Encode</a></li>
<li>Choose your <strong>embedding mode</strong>:
<li>Upload <strong>reference photo</strong> and <strong>carrier image</strong></li>
<li>Choose mode:
<ul>
<li><strong>LSB</strong> for email, cloud storage, direct transfer</li>
<li><strong>DCT</strong> for social media (Instagram, WhatsApp, etc.)</li>
<li><strong>DCT</strong> (default): social media</li>
<li><strong>LSB</strong>: email, cloud, direct transfer</li>
</ul>
</li>
<li>Upload your <strong>reference photo</strong> and <strong>carrier image</strong></li>
<li>Enter your message or select a file to embed</li>
<li>Enter your <strong>passphrase</strong> and PIN/key</li>
<li>Download the resulting stego image</li>
<li>Send through any channel!</li>
<li>Enter message or select file</li>
<li>Enter passphrase and PIN/key</li>
<li>Download stego image</li>
</ol>
</div>
</div>
@@ -370,22 +441,21 @@
<h2 class="accordion-header">
<button class="accordion-button collapsed bg-dark text-light" type="button"
data-bs-toggle="collapse" data-bs-target="#decoding">
<i class="bi bi-3-circle me-2"></i>Decoding a Message
<i class="bi bi-3-circle me-2"></i>Decoding
</button>
</h2>
<div id="decoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
<div class="accordion-body">
<ol>
<li>Go to <a href="/decode">Decode</a></li>
<li>Upload your <strong>reference photo</strong> (same one used for encoding)</li>
<li>Upload the <strong>stego image</strong> you received</li>
<li>Enter your <strong>passphrase</strong></li>
<li>Enter your PIN and/or RSA key</li>
<li>View the decoded message or download the extracted file</li>
<li>Upload <strong>reference photo</strong></li>
<li>Upload <strong>stego image</strong></li>
<li>Enter passphrase and PIN/key</li>
<li>View message or download file</li>
</ol>
<div class="alert alert-info small mt-3 mb-0">
<i class="bi bi-magic me-2"></i>
<strong>Auto-detection:</strong> Stegasoo automatically detects LSB vs DCT mode.
Mode is auto-detected.
</div>
</div>
</div>
@@ -396,67 +466,64 @@
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits &amp; Specifications</h5>
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits &amp; Specs</h5>
</div>
<div class="card-body">
<table class="table table-dark table-striped small">
<tbody>
<tr>
<td><i class="bi bi-file-text me-2"></i>Max text message</td>
<td><strong>2 million characters</strong></td>
<td><i class="bi bi-file-text me-2"></i>Max text</td>
<td><strong>2M characters</strong></td>
</tr>
<tr>
<td><i class="bi bi-file-earmark me-2"></i>Max file payload</td>
<td><i class="bi bi-file-earmark me-2"></i>Max file</td>
<td><strong>{{ max_payload_kb }} KB</strong></td>
</tr>
<tr>
<td><i class="bi bi-image me-2"></i>Max carrier image</td>
<td><strong>24 megapixels</strong> (~6000×4000)</td>
</tr>
<tr>
<td><i class="bi bi-grid-3x3 me-2"></i>LSB capacity</td>
<td><strong>~375 KB/megapixel</strong></td>
<td><i class="bi bi-image me-2"></i>Max carrier</td>
<td><strong>24 MP</strong> (~6000x4000)</td>
</tr>
<tr>
<td><i class="bi bi-soundwave me-2"></i>DCT capacity</td>
<td><strong>~75 KB/megapixel</strong></td>
<td><strong>~75 KB/MP</strong></td>
</tr>
<tr>
<td><i class="bi bi-upload me-2"></i>Max upload size</td>
<td><i class="bi bi-grid-3x3 me-2"></i>LSB capacity</td>
<td><strong>~375 KB/MP</strong></td>
</tr>
<tr>
<td><i class="bi bi-upload me-2"></i>Max upload</td>
<td><strong>30 MB</strong></td>
</tr>
<tr>
<td><i class="bi bi-clock me-2"></i>Temp file expiry</td>
<td><strong>5 minutes</strong></td>
<td><i class="bi bi-clock me-2"></i>File expiry</td>
<td><strong>5 min</strong></td>
</tr>
<tr>
<td><i class="bi bi-key me-2"></i>PIN length</td>
<td><i class="bi bi-key me-2"></i>PIN</td>
<td><strong>6-9 digits</strong></td>
</tr>
<tr>
<td><i class="bi bi-shield me-2"></i>RSA key sizes</td>
<td><strong>2048, 3072, 4096 bits</strong></td>
<td><i class="bi bi-shield me-2"></i>RSA keys</td>
<td><strong>2048, 3072, 4096 bit</strong></td>
</tr>
<tr>
<td><i class="bi bi-chat-quote me-2"></i>Passphrase length</td>
<td><strong>3-12 words</strong> (BIP-39, recommended: 4+ words)</td>
<td><i class="bi bi-chat-quote me-2"></i>Passphrase</td>
<td><strong>3-12 words</strong> (BIP-39)</td>
</tr>
<tr>
<td><i class="bi bi-code me-2"></i>Python version</td>
<td><strong>3.10-3.12</strong> (3.13 not supported)</td>
<td><i class="bi bi-code me-2"></i>Python Version</td>
<td><strong>3.10-3.12</strong></td>
</tr>
<tr>
<td><i class="bi bi-box me-2"></i>Built with</td>
<td>Flask, Pillow, NumPy, SciPy, jpegio, cryptography, argon2-cffi</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="text-center mt-4 text-muted small">
<p>
Stegasoo v{{ version }} &bull;
<i class="bi bi-github me-1"></i>Open Source &bull;
Built with Python, Flask, and cryptography
</p>
</div>
</div>
</div>
{% endblock %}

View File

@@ -266,6 +266,7 @@
<div class="row">
<div class="col-md-4 mb-3">
<div class="security-box">
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<div class="input-group pin-input-container">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
@@ -274,9 +275,11 @@
</button>
</div>
<div class="form-text">If PIN was used during encoding</div>
</div>
</div>
<div class="col-md-8 mb-3">
<div class="security-box">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
@@ -333,9 +336,64 @@
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
</div>
<!-- ================================================================
CHANNEL KEY (v4.0.0) - Deployment/Group Isolation
================================================================ -->
<div class="mb-4">
<div class="security-box">
<label class="form-label">
<i class="bi bi-broadcast me-1"></i> Channel
<span class="badge bg-info ms-1">v4.0</span>
</label>
<div class="d-flex gap-2">
<!-- Auto Mode -->
<label class="mode-btn flex-fill {% if channel_configured %}active{% endif %}" id="channelAutoCardDec" for="channelAutoDec">
<input class="form-check-input" type="radio" name="channel_key" id="channelAutoDec" value="auto" checked>
<i class="bi bi-gear-fill {% if channel_configured %}text-success{% else %}text-secondary{% endif %} ms-2"></i>
<span class="ms-2"><strong>Auto</strong> <span class="text-muted d-none d-sm-inline">· {% if channel_configured %}Server Key{% else %}Public{% endif %}</span></span>
</label>
<!-- Public Mode -->
<label class="mode-btn flex-fill" id="channelPublicCardDec" for="channelPublicDec">
<input class="form-check-input" type="radio" name="channel_key" id="channelPublicDec" value="none">
<i class="bi bi-globe text-info ms-2"></i>
<span class="ms-2"><strong>Public</strong> <span class="text-muted d-none d-sm-inline">· No key</span></span>
</label>
<!-- Custom Key -->
<label class="mode-btn flex-fill" id="channelCustomCardDec" for="channelCustomDec">
<input class="form-check-input" type="radio" name="channel_key" id="channelCustomDec" value="custom">
<i class="bi bi-key-fill text-warning ms-2"></i>
<span class="ms-2"><strong>Custom</strong></span>
</label>
</div>
<!-- Server channel indicator (compact) -->
{% if channel_configured %}
<div class="small text-success mt-2">
<i class="bi bi-shield-lock me-1"></i>
Server: <code>{{ channel_fingerprint }}</code>
</div>
{% endif %}
<!-- Custom key input -->
<div class="mt-2 d-none" id="channelCustomInputDec">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" name="channel_key_custom" class="form-control font-monospace"
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}"
id="channelKeyInputDec">
</div>
</div>
</div>
</div>
<!-- ================================================================
ADVANCED OPTIONS (v3.0) - Extraction Mode
================================================================ -->
@@ -355,51 +413,34 @@
<span class="badge bg-info ms-1">v3.0</span>
</label>
<div class="row g-2">
<div class="d-flex gap-2">
<!-- Auto Mode -->
<div class="col-4">
<div class="form-check card p-2 text-center h-100" id="autoModeCard">
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeAuto" value="auto" checked>
<label class="form-check-label w-100" for="modeAuto">
<i class="bi bi-magic text-success fs-4 d-block mb-1"></i>
<strong>Auto</strong>
<div class="small text-muted">Try both</div>
</label>
</div>
</div>
<label class="mode-btn flex-fill active" id="autoModeCard" for="modeAuto">
<input class="form-check-input" type="radio" name="embed_mode" id="modeAuto" value="auto" checked>
<i class="bi bi-magic text-success"></i>
<span class="ms-2"><strong>Auto</strong> <span class="text-muted d-none d-sm-inline">· Try both</span></span>
</label>
<!-- LSB Mode -->
<div class="col-4">
<div class="form-check card p-2 text-center h-100" id="lsbModeCardDec">
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeLsbDec" value="lsb">
<label class="form-check-label w-100" for="modeLsbDec">
<i class="bi bi-grid-3x3-gap text-primary fs-4 d-block mb-1"></i>
<strong>LSB</strong>
<div class="small text-muted">Spatial only</div>
</label>
</div>
</div>
<label class="mode-btn flex-fill" id="lsbModeCardDec" for="modeLsbDec">
<input class="form-check-input" type="radio" name="embed_mode" id="modeLsbDec" value="lsb">
<i class="bi bi-grid-3x3-gap text-primary"></i>
<span class="ms-2"><strong>LSB</strong> <span class="text-muted d-none d-sm-inline">· Spatial</span></span>
</label>
<!-- DCT Mode -->
<div class="col-4">
<div class="form-check card p-2 text-center h-100 {% if not has_dct %}opacity-50{% endif %}" id="dctModeCardDec">
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeDctDec" value="dct" {% if not has_dct %}disabled{% endif %}>
<label class="form-check-label w-100" for="modeDctDec">
<i class="bi bi-soundwave text-info fs-4 d-block mb-1"></i>
<strong>DCT</strong>
<div class="small text-muted">
{% if has_dct %}Frequency only{% else %}N/A{% endif %}
</div>
</label>
</div>
</div>
<label class="mode-btn flex-fill {% if not has_dct %}opacity-50{% endif %}" id="dctModeCardDec" for="modeDctDec">
<input class="form-check-input" type="radio" name="embed_mode" id="modeDctDec" value="dct" {% if not has_dct %}disabled{% endif %}>
<i class="bi bi-soundwave text-warning"></i>
<span class="ms-2"><strong>DCT</strong> <span class="text-muted d-none d-sm-inline">· Frequency</span></span>
</label>
</div>
<div class="form-text mt-2">
<i class="bi bi-lightbulb me-1"></i>
<strong>Auto</strong> tries LSB first, then DCT. Use specific mode if you know how it was encoded.
<strong>Auto</strong> tries LSB first, then DCT.
{% if not has_dct %}
<br><span class="text-warning"><i class="bi bi-exclamation-triangle me-1"></i>DCT requires scipy: <code>pip install scipy</code></span>
<span class="text-warning ms-2"><i class="bi bi-exclamation-triangle me-1"></i>DCT requires scipy</span>
{% endif %}
</div>
</div>
@@ -442,6 +483,10 @@
<i class="bi bi-exclamation-triangle-fill text-warning me-1"></i>
<strong>Format compatibility:</strong> v4.0 cannot decode messages from v3.1 or earlier (different format)
</li>
<li class="mb-2">
<i class="bi bi-broadcast text-info me-1"></i>
<strong>Channel key:</strong> Use the same channel (Auto/Public/Custom) that was used during encoding
</li>
<li class="mb-2">
<i class="bi bi-info-circle-fill text-info me-1"></i>
If using an RSA key, verify the <strong>password is correct</strong> (if key is encrypted)
@@ -461,34 +506,22 @@
{% block scripts %}
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
<script>
// ============================================================================
// DECODE PAGE - Initialize shared components
// ============================================================================
// Extraction mode button active state toggle
const extractModeRadios = document.querySelectorAll('input[name="embed_mode"]');
const extractModeBtns = {
'auto': document.getElementById('autoModeCard'),
'lsb': document.getElementById('lsbModeCardDec'),
'dct': document.getElementById('dctModeCardDec')
};
Stegasoo.initPasswordToggles();
Stegasoo.initRsaMethodToggle();
Stegasoo.initDropZones();
Stegasoo.initClipboardPaste(['input[name="stego_image"]', 'input[name="reference_photo"]']);
Stegasoo.initQrCropAnimation('rsaKeyQrInput');
Stegasoo.initPassphraseFontResize();
// ============================================================================
// DECODE PAGE - Mode card highlighting
// ============================================================================
Stegasoo.initModeCards({
radioName: 'embed_mode',
cards: {
'auto': { id: 'autoModeCard', borderClass: 'border-success' },
'lsb': { id: 'lsbModeCardDec', borderClass: 'border-primary' },
'dct': { id: 'dctModeCardDec', borderClass: 'border-info' }
}
extractModeRadios.forEach(radio => {
radio.addEventListener('change', () => {
Object.values(extractModeBtns).forEach(btn => btn?.classList.remove('active'));
extractModeBtns[radio.value]?.classList.add('active');
});
});
// ============================================================================
// DECODE PAGE - Advanced options chevron
// ============================================================================
// Advanced options chevron
const advancedOptionsDec = document.getElementById('advancedOptionsDec');
advancedOptionsDec?.addEventListener('show.bs.collapse', () => {
document.getElementById('advancedChevronDec')?.classList.replace('bi-chevron-down', 'bi-chevron-up');
@@ -496,18 +529,5 @@ advancedOptionsDec?.addEventListener('show.bs.collapse', () => {
advancedOptionsDec?.addEventListener('hide.bs.collapse', () => {
document.getElementById('advancedChevronDec')?.classList.replace('bi-chevron-up', 'bi-chevron-down');
});
// ============================================================================
// DECODE PAGE - Form submission with mode-specific loading text
// ============================================================================
document.getElementById('decodeForm')?.addEventListener('submit', function() {
const btn = document.getElementById('decodeBtn');
const selectedMode = document.querySelector('input[name="embed_mode"]:checked')?.value || 'auto';
if (btn) {
btn.disabled = true;
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Decoding (${selectedMode.toUpperCase()})...`;
}
});
</script>
{% endblock %}

View File

@@ -333,17 +333,20 @@
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<div class="input-group pin-input-container">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
<button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput">
<i class="bi bi-eye"></i>
</button>
<div class="security-box">
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<div class="input-group pin-input-container">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
<button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="form-text">Static 6-9 digit PIN</div>
</div>
<div class="form-text">Static 6-9 digit PIN</div>
</div>
<div class="col-md-8 mb-3">
<div class="security-box">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
@@ -400,9 +403,70 @@
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
</div>
<!-- ================================================================
CHANNEL KEY (v4.0.0) - Deployment/Group Isolation
================================================================ -->
<div class="mb-4">
<div class="security-box">
<label class="form-label">
<i class="bi bi-broadcast me-1"></i> Channel
<span class="badge bg-info ms-1">v4.0</span>
</label>
<div class="d-flex gap-2">
<!-- Auto Mode -->
<label class="mode-btn flex-fill {% if channel_configured %}active{% endif %}" id="channelAutoCard" for="channelAuto">
<input class="form-check-input" type="radio" name="channel_key" id="channelAuto" value="auto" checked>
<i class="bi bi-gear-fill {% if channel_configured %}text-success{% else %}text-secondary{% endif %} ms-2"></i>
<span class="ms-2"><strong>Auto</strong> <span class="text-muted d-none d-sm-inline">· {% if channel_configured %}Server Key{% else %}Public{% endif %}</span></span>
</label>
<!-- Public Mode -->
<label class="mode-btn flex-fill" id="channelPublicCard" for="channelPublic">
<input class="form-check-input" type="radio" name="channel_key" id="channelPublic" value="none">
<i class="bi bi-globe text-info ms-2"></i>
<span class="ms-2"><strong>Public</strong> <span class="text-muted d-none d-sm-inline">· No key</span></span>
</label>
<!-- Custom Key -->
<label class="mode-btn flex-fill" id="channelCustomCard" for="channelCustom">
<input class="form-check-input" type="radio" name="channel_key" id="channelCustom" value="custom">
<i class="bi bi-key-fill text-warning ms-2"></i>
<span class="ms-2"><strong>Custom</strong></span>
</label>
</div>
<!-- Server channel indicator (compact) -->
{% if channel_configured %}
<div class="small text-success mt-2" id="channelServerInfo">
<i class="bi bi-shield-lock me-1"></i>
Server: <code>{{ channel_fingerprint }}</code>
</div>
{% endif %}
<!-- Custom key input -->
<div class="mt-2 d-none" id="channelCustomInput">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" name="channel_key_custom" class="form-control font-monospace"
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}"
id="channelKeyInput">
<button class="btn btn-outline-secondary" type="button" id="channelKeyGenerate" title="Generate random key">
<i class="bi bi-shuffle"></i>
</button>
</div>
<div class="invalid-feedback" id="channelKeyError">
Invalid format. Use: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
</div>
</div>
</div>
</div>
<!-- Advanced Options (DCT sub-options only) -->
<div class="mb-4 {% if not has_dct %}d-none{% endif %}" id="advancedOptionsContainer">
<a class="btn btn-sm btn-outline-secondary w-100" data-bs-toggle="collapse" href="#advancedOptions" role="button" aria-expanded="false">
@@ -411,70 +475,43 @@
</a>
<div class="collapse" id="advancedOptions">
<div class="card card-body mt-2 bg-dark border-secondary">
<div class="card card-body mt-2 bg-dark border-secondary py-3">
<div class="alert alert-info small mb-3">
<i class="bi bi-info-circle me-1"></i>
<strong>DCT defaults:</strong> Color mode + JPEG output for best social media compatibility.
</div>
<!-- DCT Color Mode -->
<!-- DCT Color Mode - Compact -->
<div class="mb-3">
<label class="form-label">
<i class="bi bi-palette me-1"></i> Color Mode
<label class="form-label small mb-2">
<i class="bi bi-palette me-1"></i> Color
</label>
<div class="row g-2">
<div class="col-6">
<div class="form-check card p-2 text-center border-success border-2" id="dctColorCard">
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked>
<label class="form-check-label w-100" for="dctColorColor">
<i class="bi bi-palette-fill text-success fs-5 d-block"></i>
<strong>Color</strong>
<div class="small text-muted">Recommended</div>
</label>
</div>
</div>
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctGrayscaleCard">
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
<label class="form-check-label w-100" for="dctColorGrayscale">
<i class="bi bi-circle-half text-secondary fs-5 d-block"></i>
<strong>Grayscale</strong>
<div class="small text-muted">B&W output</div>
</label>
</div>
</div>
<div class="d-flex gap-2">
<label class="mode-btn equal-width active" id="dctColorCard" for="dctColorColor">
<input class="form-check-input" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked>
<i class="bi bi-palette-fill text-success"></i>
<span class="ms-2"><strong>Color</strong> <span class="badge bg-success ms-1">Default</span></span>
</label>
<label class="mode-btn equal-width" id="dctGrayscaleCard" for="dctColorGrayscale">
<input class="form-check-input" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
<i class="bi bi-circle-half text-secondary"></i>
<span class="ms-2"><strong>Grayscale</strong></span>
</label>
</div>
</div>
<!-- DCT Output Format -->
<!-- DCT Output Format - Compact -->
<div class="mb-0">
<label class="form-label">
<i class="bi bi-file-image me-1"></i> Output Format
<label class="form-label small mb-2">
<i class="bi bi-file-image me-1"></i> Format
</label>
<div class="row g-2">
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctPngCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png">
<label class="form-check-label w-100" for="dctFormatPng">
<i class="bi bi-file-earmark-image text-primary fs-5 d-block"></i>
<strong>PNG</strong>
<div class="small text-muted">Lossless, larger</div>
</label>
</div>
</div>
<div class="col-6">
<div class="form-check card p-2 text-center border-warning border-2" id="dctJpegCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg" checked>
<label class="form-check-label w-100" for="dctFormatJpeg">
<i class="bi bi-file-earmark-richtext text-warning fs-5 d-block"></i>
<strong>JPEG</strong>
<div class="small text-muted">Recommended</div>
</label>
</div>
</div>
<div class="d-flex gap-2">
<label class="mode-btn equal-width active" id="dctJpegCard" for="dctFormatJpeg">
<input class="form-check-input" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg" checked>
<i class="bi bi-file-earmark-richtext text-warning"></i>
<span class="ms-2"><strong>JPEG</strong> <span class="badge bg-warning text-dark ms-1">Default</span></span>
</label>
<label class="mode-btn equal-width" id="dctPngCard" for="dctFormatPng">
<input class="form-check-input" type="radio" name="dct_output_format" id="dctFormatPng" value="png">
<i class="bi bi-file-earmark-image text-primary"></i>
<span class="ms-2"><strong>PNG</strong> <span class="text-muted d-none d-sm-inline">· Lossless</span></span>
</label>
</div>
</div>
@@ -520,17 +557,6 @@
{% block scripts %}
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
<script>
// ============================================================================
// ENCODE PAGE - Initialize shared components
// ============================================================================
Stegasoo.initPasswordToggles();
Stegasoo.initRsaMethodToggle();
Stegasoo.initDropZones();
Stegasoo.initClipboardPaste(['input[name="carrier"]', 'input[name="reference_photo"]']);
Stegasoo.initQrCropAnimation('rsaQrInput');
Stegasoo.initPassphraseFontResize();
// ============================================================================
// ENCODE PAGE - Payload type switching
// ============================================================================
@@ -683,22 +709,26 @@ document.querySelectorAll('input[name="embed_mode"]').forEach(radio => {
});
});
// DCT format cards
Stegasoo.initModeCards({
radioName: 'dct_output_format',
cards: {
'png': { id: 'dctPngCard', borderClass: 'border-primary' },
'jpeg': { id: 'dctJpegCard', borderClass: 'border-warning' }
}
// DCT color mode button active state toggle
const colorModeRadios = document.querySelectorAll('input[name="dct_color_mode"]');
const colorModeBtns = { 'color': document.getElementById('dctColorCard'), 'grayscale': document.getElementById('dctGrayscaleCard') };
colorModeRadios.forEach(radio => {
radio.addEventListener('change', () => {
Object.values(colorModeBtns).forEach(btn => btn?.classList.remove('active'));
colorModeBtns[radio.value]?.classList.add('active');
});
});
// DCT color cards
Stegasoo.initModeCards({
radioName: 'dct_color_mode',
cards: {
'color': { id: 'dctColorCard', borderClass: 'border-success' },
'grayscale': { id: 'dctGrayscaleCard', borderClass: 'border-secondary' }
}
// DCT format button active state toggle
const formatRadios = document.querySelectorAll('input[name="dct_output_format"]');
const formatBtns = { 'png': document.getElementById('dctPngCard'), 'jpeg': document.getElementById('dctJpegCard') };
formatRadios.forEach(radio => {
radio.addEventListener('change', () => {
Object.values(formatBtns).forEach(btn => btn?.classList.remove('active'));
formatBtns[radio.value]?.classList.add('active');
});
});
// Advanced options chevron
@@ -735,17 +765,5 @@ function checkDuplicateFiles() {
document.querySelector('input[name="reference_photo"]')?.addEventListener('change', checkDuplicateFiles);
document.querySelector('input[name="carrier"]')?.addEventListener('change', checkDuplicateFiles);
// ============================================================================
// ENCODE PAGE - Form submission
// ============================================================================
document.getElementById('encodeForm')?.addEventListener('submit', function() {
const btn = document.getElementById('encodeBtn');
if (btn) {
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...';
}
});
</script>
{% endblock %}

View File

@@ -91,6 +91,24 @@
</span>
<div class="small text-muted mt-2">Full color PNG, spatial LSB embedding</div>
{% endif %}
<!-- Channel info (v4.0.0) -->
<div class="mt-3">
{% if channel_mode == 'private' %}
<span class="badge bg-warning text-dark fs-6">
<i class="bi bi-shield-lock me-1"></i>Private Channel
</span>
{% if channel_fingerprint %}
<div class="small text-muted mt-1">
<code>{{ channel_fingerprint }}</code>
</div>
{% endif %}
{% else %}
<span class="badge bg-info fs-6">
<i class="bi bi-globe me-1"></i>Public Channel
</span>
{% endif %}
</div>
</div>
<div class="d-grid gap-2">
@@ -123,6 +141,9 @@
<li>Color preserved - extraction works on both color and grayscale</li>
{% endif %}
{% endif %}
{% if channel_mode == 'private' %}
<li><i class="bi bi-shield-lock text-warning me-1"></i>Recipient needs the <strong>same channel key</strong> to decode</li>
{% endif %}
</ul>
</div>

View File

@@ -3,7 +3,7 @@
{% block title %}Generate Credentials - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="row justify-content-center" data-page="generate">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
@@ -74,6 +74,32 @@
</div>
</div>
<hr class="my-4">
<!-- Channel Key Generation (v4.0.0) -->
<div class="mb-4">
<label class="form-label">
<i class="bi bi-broadcast me-1"></i> Channel Key
<span class="badge bg-info ms-1">v4.0</span>
<a href="{{ url_for('about') }}#channel-keys" class="text-muted ms-2" title="Learn about channel keys">
<i class="bi bi-question-circle"></i>
</a>
</label>
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" class="form-control font-monospace" id="channelKeyGenerated"
placeholder="Click Generate" readonly>
<button class="btn btn-outline-primary" type="button" id="generateChannelKeyBtn">
<i class="bi bi-shuffle me-1"></i>Generate
</button>
<button class="btn btn-outline-secondary" type="button" id="copyChannelKeyBtn" disabled title="Copy to clipboard">
<i class="bi bi-clipboard"></i>
</button>
</div>
<div class="form-text">For private groups: generate, then use <strong>Custom</strong> mode when encoding/decoding.</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100 mt-3">
<i class="bi bi-shuffle me-2"></i>Generate Credentials
</button>

View File

@@ -19,6 +19,20 @@
</div>
</div>
<!-- Channel Status Banner (v4.0.0) -->
{% if channel_configured %}
<div class="alert alert-success mb-4">
<div class="d-flex align-items-center justify-content-between">
<div>
<i class="bi bi-shield-lock me-2"></i>
<strong>Private Channel Active</strong>
<span class="text-muted ms-2">Messages are isolated to this deployment</span>
</div>
<code class="small">{{ channel_fingerprint }}</code>
</div>
</div>
{% endif %}
<div class="row g-4 mb-5">
<!-- Encode Card -->
<div class="col-md-4">
@@ -81,22 +95,22 @@
<div class="row text-center">
<div class="col-md-6 mb-3 mb-md-0">
<div class="p-3 bg-dark rounded h-100">
<i class="bi bi-grid-3x3-gap text-primary fs-2 d-block mb-2"></i>
<strong>LSB Mode</strong>
<i class="bi bi-soundwave text-warning fs-2 d-block mb-2"></i>
<strong>DCT Mode</strong>
<span class="badge bg-success ms-1">Default</span>
<div class="small text-muted mt-2">
Higher capacity (~375 KB/MP)<br>
Best for email &amp; file transfer
Survives JPEG recompression<br>
Best for social media
</div>
</div>
</div>
<div class="col-md-6">
<div class="p-3 bg-dark rounded h-100">
<i class="bi bi-soundwave text-warning fs-2 d-block mb-2"></i>
<strong>DCT Mode</strong>
<i class="bi bi-grid-3x3-gap text-primary fs-2 d-block mb-2"></i>
<strong>LSB Mode</strong>
<div class="small text-muted mt-2">
Survives JPEG recompression<br>
Best for social media
Higher capacity (~375 KB/MP)<br>
Best for email &amp; file transfer
</div>
</div>
</div>
@@ -116,15 +130,15 @@
<ul class="list-unstyled small">
<li class="mb-1">
<i class="bi bi-image text-info me-2"></i>
<strong>Reference Photo</strong> shared secret image
<strong>Reference Photo</strong>: shared secret
</li>
<li class="mb-1">
<i class="bi bi-chat-quote text-info me-2"></i>
<strong>Passphrase</strong> 4+ words
<strong>Passphrase</strong>: 4+ words
</li>
<li class="mb-1">
<i class="bi bi-123 text-info me-2"></i>
<strong>PIN</strong> 6-9 digits (and/or RSA key)
<strong>PIN</strong>: 6-9 digits (or RSA key)
</li>
</ul>
</div>
@@ -143,6 +157,11 @@
<i class="bi bi-shuffle text-success me-2"></i>
Pseudo-random embedding
</li>
<li class="mb-1">
<i class="bi bi-broadcast text-success me-2"></i>
<strong>Channel keys</strong> for group isolation
<span class="badge bg-info ms-1">v4.0</span>
</li>
</ul>
</div>
</div>