More snazzy 4.0 Web UI improvements.
This commit is contained in:
1202
frontends/API.md
1202
frontends/API.md
File diff suppressed because it is too large
Load Diff
1143
frontends/CLI.md
1143
frontends/CLI.md
File diff suppressed because it is too large
Load Diff
@@ -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) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
1073
frontends/cli/main.py_old
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}'}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 & 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 & DCT Modes</strong>
|
||||
<br><small class="text-muted">Choose capacity (LSB) or JPEG resilience (DCT)</small>
|
||||
<strong>DCT & 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 & Specifications</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits & 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 }} •
|
||||
<i class="bi bi-github me-1"></i>Open Source •
|
||||
Built with Python, Flask, and cryptography
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
|
||||
Reference in New Issue
Block a user