More snazzy 4.0 Web UI improvements.

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

View File

@@ -41,8 +41,8 @@ A secure steganography system for hiding encrypted messages in images using hybr
| Mode | Capacity (1080p) | JPEG Resilient | Best For | | Mode | Capacity (1080p) | JPEG Resilient | Best For |
|------|------------------|----------------|----------| |------|------------------|----------------|----------|
| **LSB** (default) | ~770 KB | ❌ No | Email, file transfer | | **DCT** (default) | ~150 KB | ✅ Yes | Social media, messaging apps |
| **DCT** | ~65 KB | ✅ Yes | Social media, messaging apps | | **LSB** | ~750 KB | ❌ No | Email, file transfer |
## WebUI Preview ## WebUI Preview
@@ -59,24 +59,22 @@ pip install -e ".[all]"
# Generate credentials (memorize these!) # Generate credentials (memorize these!)
stegasoo generate --pin --words 4 stegasoo generate --pin --words 4
# Encode a message (LSB mode - default) # Encode a message (DCT mode - default, best for social media)
stegasoo encode \
--ref photo.jpg \
--carrier meme.png \
--passphrase "apple forest thunder mountain" \
--pin 123456 \
--message "Secret message"
# Encode for social media (DCT mode)
stegasoo encode \ stegasoo encode \
--ref photo.jpg \ --ref photo.jpg \
--carrier meme.jpg \ --carrier meme.jpg \
--passphrase "apple forest thunder mountain" \ --passphrase "apple forest thunder mountain" \
--pin 123456 \ --pin 123456 \
--message "Secret message"
# Encode with LSB mode (higher capacity, for email/file transfer)
stegasoo encode \
--ref photo.jpg \
--carrier meme.png \
--passphrase "apple forest thunder mountain" \
--pin 123456 \
--message "Secret message" \ --message "Secret message" \
--mode dct \ --mode lsb
--dct-format jpeg \
--dct-color color
# Decode (auto-detects mode) # Decode (auto-detects mode)
stegasoo decode \ stegasoo decode \
@@ -156,12 +154,15 @@ Full-featured CLI with piping support:
# Generate with RSA key # Generate with RSA key
stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "password" stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "password"
# Encode from file # Encode (DCT mode is now default)
stegasoo encode -r ref.jpg -c carrier.png -p "passphrase words here" --pin 123456 -f secret.txt stegasoo encode -r ref.jpg -c carrier.jpg -p "passphrase words here" --pin 123456 -m "Message"
# Encode for social media (DCT + JPEG with color preservation) # Encode with LSB mode for higher capacity
stegasoo encode -r ref.jpg -c carrier.jpg -p "passphrase words here" --pin 123456 \ stegasoo encode -r ref.jpg -c carrier.png -p "passphrase words here" --pin 123456 \
-m "Message" --mode dct --dct-format jpeg --dct-color color -m "Message" --mode lsb
# Encode a file
stegasoo encode -r ref.jpg -c carrier.png -p "passphrase words here" --pin 123456 -f secret.txt
# Decode to stdout (quiet mode) # Decode to stdout (quiet mode)
stegasoo decode -r ref.jpg -s stego.png -p "passphrase words here" --pin 123456 -q stegasoo decode -r ref.jpg -s stego.png -p "passphrase words here" --pin 123456 -q
@@ -187,12 +188,13 @@ python app.py
``` ```
Features: Features:
- Drag-and-drop image uploads - Drag-and-drop image uploads with scan animations
- Real-time entropy calculator - Real-time entropy calculator
- Native mobile sharing (Web Share API) - Native mobile sharing (Web Share API)
- DCT mode with advanced options panel - DCT mode default with compact mode selector
- Subprocess isolation for stability - Subprocess isolation for stability
- Large image support (14MB+ tested) - Large image support (14MB+ tested)
- Streamlined form flow (v3.3.0)
📖 Full documentation: **[WEB_UI.md](WEB_UI.md)** 📖 Full documentation: **[WEB_UI.md](WEB_UI.md)**
@@ -215,18 +217,25 @@ curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"use_pin": true, "passphrase_words": 4}' -d '{"use_pin": true, "passphrase_words": 4}'
# Encode with DCT mode # Encode (DCT mode is default)
curl -X POST http://localhost:8000/encode/multipart \ curl -X POST http://localhost:8000/encode/multipart \
-F "message=Secret" \ -F "message=Secret" \
-F "passphrase=apple forest thunder mountain" \ -F "passphrase=apple forest thunder mountain" \
-F "pin=123456" \ -F "pin=123456" \
-F "embed_mode=dct" \
-F "dct_output_format=jpeg" \
-F "dct_color_mode=color" \
-F "reference_photo=@photo.jpg" \ -F "reference_photo=@photo.jpg" \
-F "carrier=@meme.jpg" \ -F "carrier=@meme.jpg" \
--output stego.jpg --output stego.jpg
# Encode with LSB mode
curl -X POST http://localhost:8000/encode/multipart \
-F "message=Secret" \
-F "passphrase=apple forest thunder mountain" \
-F "pin=123456" \
-F "embed_mode=lsb" \
-F "reference_photo=@photo.jpg" \
-F "carrier=@meme.png" \
--output stego.png
# Decode (auto-detects mode) # Decode (auto-detects mode)
curl -X POST http://localhost:8000/decode/multipart \ curl -X POST http://localhost:8000/decode/multipart \
-F "passphrase=apple forest thunder mountain" \ -F "passphrase=apple forest thunder mountain" \

View File

@@ -1,5 +1,9 @@
version: '3.8' version: '3.8'
# Shared environment variables
x-common-env: &common-env
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
services: services:
# ============================================================================ # ============================================================================
# Web UI (Flask) # Web UI (Flask)
@@ -12,12 +16,13 @@ services:
ports: ports:
- "5000:5000" - "5000:5000"
environment: environment:
- FLASK_ENV=production <<: *common-env
FLASK_ENV: production
restart: unless-stopped restart: unless-stopped
deploy: deploy:
resources: resources:
limits: limits:
memory: 768M # Increased for scipy + Argon2 memory: 768M
reservations: reservations:
memory: 384M memory: 384M
@@ -31,32 +36,12 @@ services:
container_name: stegasoo-api container_name: stegasoo-api
ports: ports:
- "8000:8000" - "8000:8000"
environment:
<<: *common-env
restart: unless-stopped restart: unless-stopped
deploy: deploy:
resources: resources:
limits: limits:
memory: 768M # Increased for scipy + Argon2 memory: 768M
reservations: reservations:
memory: 384M memory: 384M
# ============================================================================
# Nginx Reverse Proxy (optional, for production)
# ============================================================================
# nginx:
# image: nginx:alpine
# container_name: stegasoo-nginx
# ports:
# - "80:80"
# - "443:443"
# volumes:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
# - ./certs:/etc/nginx/certs:ro
# depends_on:
# - web
# - api
# restart: unless-stopped
# ============================================================================
# Development overrides
# ============================================================================
# Use: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -6,9 +6,10 @@ FastAPI-based REST API for steganography operations.
Supports both text messages and file embedding. Supports both text messages and file embedding.
CHANGES in v4.0.0: CHANGES in v4.0.0:
- Updated from v3.2.0 with no functional API changes - Added channel key support for deployment/group isolation
- Internal: JPEG normalization for jpegio compatibility - New /channel endpoints for key management
- Internal: Python 3.12 recommended - channel_key parameter on encode/decode endpoints
- Messages encoded with channel key require same key to decode
CHANGES in v3.2.0: CHANGES in v3.2.0:
- Removed date dependency from all operations - Removed date dependency from all operations
@@ -51,6 +52,17 @@ from stegasoo import (
compare_modes, compare_modes,
will_fit_by_mode, will_fit_by_mode,
calculate_capacity_by_mode, calculate_capacity_by_mode,
# Channel key functions (v4.0.0)
generate_channel_key,
get_channel_key,
set_channel_key,
clear_channel_key,
has_channel_key,
get_channel_status,
validate_channel_key,
format_channel_key,
get_active_channel_key,
get_channel_fingerprint,
) )
from stegasoo.constants import ( from stegasoo.constants import (
MIN_PIN_LENGTH, MAX_PIN_LENGTH, 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 ## Version 4.0.0 Changes
- **Python 3.12 recommended** - jpegio compatibility improvements - **Channel key support** - Deployment/group isolation for messages
- **JPEG normalization** - Handles quality=100 images automatically - **New /channel endpoints** - Generate, view, and manage channel keys
- **channel_key parameter** - Added to encode/decode endpoints
## Version 3.2.0 Changes ## Version 3.2.0 Changes
@@ -156,12 +169,15 @@ class EncodeRequest(BaseModel):
pin: str = "" pin: str = ""
rsa_key_base64: Optional[str] = None rsa_key_base64: Optional[str] = None
rsa_password: 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( embed_mode: EmbedModeType = Field(
default="lsb", default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)" description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
) )
# NEW in v3.0.1
dct_output_format: DctOutputFormatType = Field( dct_output_format: DctOutputFormatType = Field(
default="png", default="png",
description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode." description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode."
@@ -183,12 +199,15 @@ class EncodeFileRequest(BaseModel):
pin: str = "" pin: str = ""
rsa_key_base64: Optional[str] = None rsa_key_base64: Optional[str] = None
rsa_password: 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( embed_mode: EmbedModeType = Field(
default="lsb", default="lsb",
description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)" description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)"
) )
# NEW in v3.0.1
dct_output_format: DctOutputFormatType = Field( dct_output_format: DctOutputFormatType = Field(
default="png", default="png",
description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode." description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode."
@@ -204,7 +223,6 @@ class EncodeResponse(BaseModel):
filename: str filename: str
capacity_used_percent: float capacity_used_percent: float
embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'") embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'")
# NEW in v3.0.1
output_format: str = Field( output_format: str = Field(
default="png", default="png",
description="Output format: 'png' or 'jpeg' (for DCT mode)" description="Output format: 'png' or 'jpeg' (for DCT mode)"
@@ -213,6 +231,15 @@ class EncodeResponse(BaseModel):
default="color", default="color",
description="Color mode: 'color' (LSB/DCT color) or 'grayscale' (DCT grayscale)" 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) # Legacy fields (v3.2.0: no longer used in crypto)
date_used: Optional[str] = Field( date_used: Optional[str] = Field(
default=None, default=None,
@@ -231,6 +258,11 @@ class DecodeRequest(BaseModel):
pin: str = "" pin: str = ""
rsa_key_base64: Optional[str] = None rsa_key_base64: Optional[str] = None
rsa_password: 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( embed_mode: ExtractModeType = Field(
default="auto", default="auto",
description="Extraction mode: 'auto' (default), 'lsb', or 'dct'" description="Extraction mode: 'auto' (default), 'lsb', or 'dct'"
@@ -260,7 +292,6 @@ class ImageInfoResponse(BaseModel):
pixels: int pixels: int
capacity_bytes: int = Field(description="LSB mode capacity (for backwards compatibility)") capacity_bytes: int = Field(description="LSB mode capacity (for backwards compatibility)")
capacity_kb: int = Field(description="LSB mode capacity in KB") capacity_kb: int = Field(description="LSB mode capacity in KB")
# NEW in v3.0
modes: Optional[dict[str, ModeCapacity]] = Field( modes: Optional[dict[str, ModeCapacity]] = Field(
default=None, default=None,
description="Capacity by embedding mode (v3.0+)" description="Capacity by embedding mode (v3.0+)"
@@ -297,10 +328,38 @@ class DctModeInfo(BaseModel):
requires: str 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): class ModesResponse(BaseModel):
"""Response showing available embedding modes.""" """Response showing available embedding modes."""
lsb: dict lsb: dict
dct: DctModeInfo dct: DctModeInfo
# Channel key status (v4.0.0)
channel: Optional[dict] = Field(
default=None,
description="Channel key status (v4.0.0)"
)
class StatusResponse(BaseModel): class StatusResponse(BaseModel):
@@ -310,14 +369,17 @@ class StatusResponse(BaseModel):
has_dct: bool has_dct: bool
max_payload_kb: int max_payload_kb: int
available_modes: list[str] available_modes: list[str]
# NEW in v3.0.1
dct_features: Optional[dict] = Field( dct_features: Optional[dict] = Field(
default=None, default=None,
description="DCT mode features (v3.0.1+)" 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( 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 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 # ROUTES - STATUS & INFO
# ============================================================================ # ============================================================================
@@ -368,6 +491,15 @@ async def root():
"default_color_mode": "grayscale", "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( return StatusResponse(
version=__version__, version=__version__,
has_argon2=has_argon2(), has_argon2=has_argon2(),
@@ -376,11 +508,15 @@ async def root():
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024, max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024,
available_modes=available_modes, available_modes=available_modes,
dct_features=dct_features, dct_features=dct_features,
channel=channel_info,
breaking_changes={ breaking_changes={
"date_removed": "No date_str parameter needed - encode/decode anytime", "v4_channel_key": "Messages encoded with channel key require same key to decode",
"passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)", "format_version": 5,
"format_version": 4,
"backward_compatible": False, "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. Get available embedding modes and their status.
NEW in v3.0: Shows LSB and DCT mode availability. v4.0.0: Also includes channel key status.
NEW in v3.0.1: Shows DCT color modes and output formats.
""" """
# Channel status
channel_status = get_channel_status()
channel_info = {
"mode": channel_status['mode'],
"configured": channel_status['configured'],
"fingerprint": channel_status.get('fingerprint'),
}
return ModesResponse( return ModesResponse(
lsb={ lsb={
"available": True, "available": True,
@@ -409,16 +552,137 @@ async def api_modes():
color_modes=["grayscale", "color"], color_modes=["grayscale", "color"],
capacity_ratio="~20% of LSB", capacity_ratio="~20% of LSB",
requires="scipy", 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) @app.post("/compare", response_model=CompareModesResponse)
async def api_compare_modes(request: CompareModesRequest): async def api_compare_modes(request: CompareModesRequest):
""" """
Compare LSB and DCT embedding modes for a carrier image. 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. Optionally checks if a specific payload size would fit.
""" """
try: 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. 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: try:
# Validate mode # 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}") raise HTTPException(400, f"rsa_bits must be one of {VALID_RSA_SIZES}")
try: try:
# v3.2.0: Call with passphrase_words parameter
creds = generate_credentials( creds = generate_credentials(
use_pin=request.use_pin, use_pin=request.use_pin,
use_rsa=request.use_rsa, use_rsa=request.use_rsa,
pin_length=request.pin_length, pin_length=request.pin_length,
rsa_bits=request.rsa_bits, 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( return GenerateResponse(
passphrase=creds.passphrase, # v3.2.0: Single passphrase passphrase=creds.passphrase,
pin=creds.pin, pin=creds.pin,
rsa_key_pem=creds.rsa_key_pem, rsa_key_pem=creds.rsa_key_pem,
entropy={ entropy={
@@ -626,15 +889,16 @@ async def api_encode(request: EncodeRequest):
Images must be base64-encoded. Returns base64-encoded stego image. 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! 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 # Validate mode
if request.embed_mode == "dct" and not has_dct_support(): if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") 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: try:
ref_photo = base64.b64decode(request.reference_photo_base64) ref_photo = base64.b64decode(request.reference_photo_base64)
carrier = base64.b64decode(request.carrier_image_base64) carrier = base64.b64decode(request.carrier_image_base64)
@@ -647,17 +911,17 @@ async def api_encode(request: EncodeRequest):
request.dct_color_mode request.dct_color_mode
) )
# v3.2.0: No date_str parameter # v4.0.0: Include channel_key
result = encode( result = encode(
message=request.message, message=request.message,
reference_photo=ref_photo, reference_photo=ref_photo,
carrier_image=carrier, carrier_image=carrier,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase passphrase=request.passphrase,
pin=request.pin, pin=request.pin,
rsa_key_data=rsa_key, rsa_key_data=rsa_key,
rsa_password=request.rsa_password, rsa_password=request.rsa_password,
# date_str removed in v3.2.0
embed_mode=request.embed_mode, embed_mode=request.embed_mode,
channel_key=resolved_channel_key,
**dct_params, **dct_params,
) )
@@ -669,6 +933,9 @@ async def api_encode(request: EncodeRequest):
request.dct_color_mode request.dct_color_mode
) )
# Get channel info for response
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
return EncodeResponse( return EncodeResponse(
stego_image_base64=stego_b64, stego_image_base64=stego_b64,
filename=result.filename, filename=result.filename,
@@ -676,8 +943,10 @@ async def api_encode(request: EncodeRequest):
embed_mode=request.embed_mode, embed_mode=request.embed_mode,
output_format=output_format, output_format=output_format,
color_mode=color_mode, color_mode=color_mode,
date_used=None, # v3.2.0: No longer used channel_mode=channel_mode,
day_of_week=None, # v3.2.0: No longer used channel_fingerprint=channel_fingerprint,
date_used=None,
day_of_week=None,
) )
except CapacityError as e: except CapacityError as e:
@@ -695,15 +964,16 @@ async def api_encode_file(request: EncodeFileRequest):
File data must be base64-encoded. 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! 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 # Validate mode
if request.embed_mode == "dct" and not has_dct_support(): if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") 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: try:
file_data = base64.b64decode(request.file_data_base64) file_data = base64.b64decode(request.file_data_base64)
ref_photo = base64.b64decode(request.reference_photo_base64) ref_photo = base64.b64decode(request.reference_photo_base64)
@@ -723,17 +993,17 @@ async def api_encode_file(request: EncodeFileRequest):
request.dct_color_mode request.dct_color_mode
) )
# v3.2.0: No date_str parameter # v4.0.0: Include channel_key
result = encode( result = encode(
message=payload, message=payload,
reference_photo=ref_photo, reference_photo=ref_photo,
carrier_image=carrier, carrier_image=carrier,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase passphrase=request.passphrase,
pin=request.pin, pin=request.pin,
rsa_key_data=rsa_key, rsa_key_data=rsa_key,
rsa_password=request.rsa_password, rsa_password=request.rsa_password,
# date_str removed in v3.2.0
embed_mode=request.embed_mode, embed_mode=request.embed_mode,
channel_key=resolved_channel_key,
**dct_params, **dct_params,
) )
@@ -745,6 +1015,9 @@ async def api_encode_file(request: EncodeFileRequest):
request.dct_color_mode request.dct_color_mode
) )
# Get channel info for response
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
return EncodeResponse( return EncodeResponse(
stego_image_base64=stego_b64, stego_image_base64=stego_b64,
filename=result.filename, filename=result.filename,
@@ -752,8 +1025,10 @@ async def api_encode_file(request: EncodeFileRequest):
embed_mode=request.embed_mode, embed_mode=request.embed_mode,
output_format=output_format, output_format=output_format,
color_mode=color_mode, color_mode=color_mode,
date_used=None, # v3.2.0: No longer used channel_mode=channel_mode,
day_of_week=None, # v3.2.0: No longer used channel_fingerprint=channel_fingerprint,
date_used=None,
day_of_week=None,
) )
except CapacityError as e: 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. 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! 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 # Validate mode
if request.embed_mode == "dct" and not has_dct_support(): if request.embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") 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: try:
stego = base64.b64decode(request.stego_image_base64) stego = base64.b64decode(request.stego_image_base64)
ref_photo = base64.b64decode(request.reference_photo_base64) ref_photo = base64.b64decode(request.reference_photo_base64)
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None 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( result = decode(
stego_image=stego, stego_image=stego,
reference_photo=ref_photo, reference_photo=ref_photo,
passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase passphrase=request.passphrase,
pin=request.pin, pin=request.pin,
rsa_key_data=rsa_key, rsa_key_data=rsa_key,
rsa_password=request.rsa_password, rsa_password=request.rsa_password,
embed_mode=request.embed_mode, embed_mode=request.embed_mode,
channel_key=resolved_channel_key,
) )
if result.is_file: if result.is_file:
@@ -817,6 +1091,10 @@ async def api_decode(request: DecodeRequest):
) )
except DecryptionError as e: 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.") raise HTTPException(401, "Decryption failed. Check credentials.")
except StegasooError as e: except StegasooError as e:
raise HTTPException(400, str(e)) raise HTTPException(400, str(e))
@@ -839,9 +1117,9 @@ async def api_encode_multipart(
rsa_key: Optional[UploadFile] = File(None), rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None), rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""), 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"), embed_mode: str = Form("lsb"),
# NEW in v3.0.1
dct_output_format: str = Form("png"), dct_output_format: str = Form("png"),
dct_color_mode: str = Form("grayscale"), 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). 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. 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! 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 # Validate mode
if embed_mode not in ("lsb", "dct"): if embed_mode not in ("lsb", "dct"):
@@ -869,6 +1146,15 @@ async def api_encode_multipart(
if dct_color_mode not in ("grayscale", "color"): if dct_color_mode not in ("grayscale", "color"):
raise HTTPException(400, "dct_color_mode must be 'grayscale' or '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: try:
ref_data = await reference_photo.read() ref_data = await reference_photo.read()
carrier_data = await carrier.read() carrier_data = await carrier.read()
@@ -911,17 +1197,17 @@ async def api_encode_multipart(
# Get DCT parameters # Get DCT parameters
dct_params = _get_dct_params(embed_mode, dct_output_format, dct_color_mode) 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( result = encode(
message=payload, message=payload,
reference_photo=ref_data, reference_photo=ref_data,
carrier_image=carrier_data, carrier_image=carrier_data,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase passphrase=passphrase,
pin=pin, pin=pin,
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=effective_password, rsa_password=effective_password,
# date_str removed in v3.2.0
embed_mode=embed_mode, embed_mode=embed_mode,
channel_key=resolved_channel_key,
**dct_params, **dct_params,
) )
@@ -929,17 +1215,26 @@ async def api_encode_multipart(
embed_mode, dct_output_format, dct_color_mode 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( return Response(
content=result.stego_image, content=result.stego_image,
media_type=mime_type, media_type=mime_type,
headers={ headers=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__,
}
) )
except CapacityError as e: except CapacityError as e:
@@ -961,6 +1256,8 @@ async def api_decode_multipart(
rsa_key: Optional[UploadFile] = File(None), rsa_key: Optional[UploadFile] = File(None),
rsa_key_qr: Optional[UploadFile] = File(None), rsa_key_qr: Optional[UploadFile] = File(None),
rsa_password: str = Form(""), 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"), 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). 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. 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! 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 # Validate mode
if embed_mode not in ("auto", "lsb", "dct"): 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(): if embed_mode == "dct" and not has_dct_support():
raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") 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: try:
ref_data = await reference_photo.read() ref_data = await reference_photo.read()
stego_data = await stego_image.read() stego_data = await stego_image.read()
@@ -1007,15 +1310,16 @@ async def api_decode_multipart(
# QR code keys are never password-protected # QR code keys are never password-protected
effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None) 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( result = decode(
stego_image=stego_data, stego_image=stego_data,
reference_photo=ref_data, reference_photo=ref_data,
passphrase=passphrase, # v3.2.0: Renamed from day_phrase passphrase=passphrase,
pin=pin, pin=pin,
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=effective_password, rsa_password=effective_password,
embed_mode=embed_mode, embed_mode=embed_mode,
channel_key=resolved_channel_key,
) )
if result.is_file: if result.is_file:
@@ -1031,7 +1335,10 @@ async def api_decode_multipart(
message=result.message 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.") raise HTTPException(401, "Decryption failed. Check credentials.")
except StegasooError as e: except StegasooError as e:
raise HTTPException(400, str(e)) raise HTTPException(400, str(e))
@@ -1053,7 +1360,7 @@ async def api_image_info(
""" """
Get information about an image's capacity. 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: try:
image_data = await image.read() image_data = await image.read()
@@ -1072,7 +1379,6 @@ async def api_image_info(
capacity_kb=capacity // 1024 capacity_kb=capacity // 1024
) )
# NEW in v3.0 - include mode comparison
if include_modes: if include_modes:
comparison = compare_modes(image_data) comparison = compare_modes(image_data)
response.modes = { response.modes = {

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env python3 #!/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: CHANGES in v3.2.0:
- Removed date dependency from all operations - Removed date dependency from all operations
@@ -16,6 +21,7 @@ Usage:
stegasoo info [OPTIONS] stegasoo info [OPTIONS]
stegasoo compare [OPTIONS] stegasoo compare [OPTIONS]
stegasoo modes [OPTIONS] stegasoo modes [OPTIONS]
stegasoo channel [SUBCOMMAND]
""" """
import sys import sys
@@ -64,6 +70,18 @@ from stegasoo import (
# Models # Models
FilePayload, 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 # Import constants - try main module first, then constants submodule
@@ -136,13 +154,13 @@ def cli():
- Reference photo (something you have) - Reference photo (something you have)
- Passphrase (something you know) - Passphrase (something you know)
- Static PIN or RSA key (additional security) - Static PIN or RSA key (additional security)
- Channel key (deployment/group isolation) [v4.0.0]
\b \b
Version 3.2.0 Changes: Version 4.0.0 Changes:
- No more date parameters - encode/decode anytime! - Channel key support for group/deployment isolation
- Simplified passphrase (no daily rotation) - Messages encoded with a channel key require the same key to decode
- Default passphrase increased to 4 words - New `stegasoo channel` command for key management
- True asynchronous communications
\b \b
Embedding Modes: Embedding Modes:
@@ -157,6 +175,60 @@ def cli():
pass 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 # GENERATE COMMAND
# ============================================================================ # ============================================================================
@@ -229,7 +301,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
# Pretty output # Pretty output
click.echo() click.echo()
click.secho("=" * 60, fg='cyan') 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.secho("=" * 60, fg='cyan')
click.echo() 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.secho(f" + photo entropy: 80-256 bits", dim=True)
click.echo() 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() click.echo()
except Exception as e: except Exception as e:
raise click.ClickException(str(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 # 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', '-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-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('--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('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb', @click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb',
help='Embedding mode: lsb (default, color) or dct (requires scipy)') 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)') help='DCT color mode: grayscale (default) or color (preserves original colors)')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors') @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, 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. Encode a secret message or file into an image.
Requires a reference photo, carrier image, and passphrase. Requires a reference photo, carrier image, and passphrase.
Must provide either --pin or --key/--key-qr (or both). 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. v3.2.0: No --date parameter needed! Encode and decode anytime.
For text messages, use -m or -f or pipe via stdin. \b
For binary files, use -e/--embed-file. Channel Key Options:
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr). (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 \b
Embedding Modes: Embedding Modes:
@@ -324,25 +669,17 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
- Lower capacity (~75 KB/megapixel) - Lower capacity (~75 KB/megapixel)
- Better resistance to visual analysis - 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 \b
Examples: Examples:
# Text message with PIN (LSB mode, default) # Text message with PIN (auto channel key)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" --pin 123456 -m "secret" stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
# DCT mode - grayscale PNG (traditional) # Explicit channel key
stegasoo encode -r photo.jpg -c meme.png -p "secure words here now" --pin 123456 -m "secret" --mode dct 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 # Public mode (no channel key)
stegasoo encode -r photo.jpg -c meme.png -p "my strong passphrase" --pin 123456 -m "secret" \\ stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "msg" --no-channel
--mode dct --dct-color color --dct-format jpeg
""" """
# Check DCT mode availability # Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support(): 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: if not quiet:
click.secho("Note: --dct-format and --dct-color only apply to DCT mode", fg='yellow', dim=True) 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 # Determine what to encode
payload = None payload = None
@@ -431,8 +774,18 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
if embed_mode == 'dct': if embed_mode == 'dct':
mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})" mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})"
click.echo(f"Mode: {mode_desc} ({fit_check['usage_percent']:.1f}% capacity)") 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( result = encode(
message=payload, message=payload,
reference_photo=ref_photo, reference_photo=ref_photo,
@@ -444,6 +797,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
embed_mode=embed_mode, embed_mode=embed_mode,
dct_output_format=dct_output_format, dct_output_format=dct_output_format,
dct_color_mode=dct_color_mode, dct_color_mode=dct_color_mode,
channel_key=resolved_channel_key,
) )
# Determine output path # 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', '-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-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('--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('--output', '-o', type=click.Path(), help='Save decoded content to file')
@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto', @click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct') 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('--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') @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. Decode a secret message or file from a stego image.
Must use the same credentials that were used for encoding. Must use the same credentials that were used for encoding.
Automatically detects whether content is text or a file. 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. 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 \b
Extraction Modes: Channel Key Options:
--mode auto Auto-detect (default) - tries LSB first, then DCT (no option) Use server-configured key (auto mode)
--mode lsb Only try LSB extraction --channel KEY Use explicit channel key
--mode dct Only try DCT extraction (requires scipy) --channel-file F Read channel key from file
--no-channel Force public mode (for images encoded without channel key)
\b \b
Examples: Examples:
# Decode with PIN (auto-detect mode) # Decode with auto channel key
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" --pin 123456 stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
# Explicitly specify DCT mode # Decode with explicit channel key
stegasoo decode -r photo.jpg -s stego.png -p "my passphrase here" --pin 123456 --mode dct 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 # Decode public image (no channel key was used)
stegasoo decode -r photo.jpg -s stego.png -p "strong words" -k mykey.pem stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 --no-channel
# Save output to file
stegasoo decode -r photo.jpg -s stego.png -p "passphrase" --pin 123456 -o output.txt
""" """
# Check DCT mode availability # Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support(): 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" "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) # Load key if provided (from .pem file or QR code image)
rsa_key_data = None rsa_key_data = None
rsa_key_from_qr = False 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() ref_photo = Path(ref).read_bytes()
stego_image = Path(stego).read_bytes() stego_image = Path(stego).read_bytes()
# v3.2.0: No date_str parameter # v4.0.0: Include channel_key parameter
result = decode( result = decode(
stego_image=stego_image, stego_image=stego_image,
reference_photo=ref_photo, 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_key_data=rsa_key_data,
rsa_password=effective_key_password, rsa_password=effective_key_password,
embed_mode=embed_mode, embed_mode=embed_mode,
channel_key=resolved_channel_key,
) )
if result.is_file: 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) click.echo(result.message)
except (DecryptionError, ExtractionError) as e: 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}") raise click.ClickException(f"Decryption failed: {e}")
except StegasooError as e: except StegasooError as e:
raise click.ClickException(str(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', '-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-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('--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', @click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct') help='Extraction mode: auto (default), lsb, or dct')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON') @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. Verify that a stego image can be decoded without extracting the message.
Quick check to validate credentials are correct and data is intact. Quick check to validate credentials are correct and data is intact.
Does NOT output the actual message content. Does NOT output the actual message content.
v4.0.0: Also verifies channel key matches.
\b \b
Examples: Examples:
stegasoo verify -r photo.jpg -s stego.png -p "my passphrase" --pin 123456 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 "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 # Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support(): 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" "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 # Load key if provided
rsa_key_data = None rsa_key_data = None
rsa_key_from_qr = False 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() ref_photo = Path(ref).read_bytes()
stego_image = Path(stego).read_bytes() stego_image = Path(stego).read_bytes()
# Attempt to decode # Attempt to decode (v4.0.0: with channel_key)
result = decode( result = decode(
stego_image=stego_image, stego_image=stego_image,
reference_photo=ref_photo, 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_key_data=rsa_key_data,
rsa_password=effective_key_password, rsa_password=effective_key_password,
embed_mode=embed_mode, embed_mode=embed_mode,
channel_key=resolved_channel_key,
) )
# Calculate payload size # Calculate payload size
if result.is_file: if result.is_file:
payload_size = len(result.file_data) if result.file_data else 0 payload_size = len(result.file_data)
payload_type = "file" content_type = result.mime_type or 'file'
payload_desc = result.filename or "unnamed file"
if result.mime_type:
payload_desc += f" ({result.mime_type})"
else: else:
payload_size = len(result.message.encode('utf-8')) if result.message else 0 payload_size = len(result.message.encode('utf-8'))
payload_type = "text" content_type = 'text'
payload_desc = f"{payload_size} bytes"
if as_json: if as_json:
import json import json
output_data = { output = {
"valid": True, 'valid': True,
"stego_file": stego, 'content_type': content_type,
"payload_type": payload_type, 'payload_size': payload_size,
"payload_size": payload_size, 'filename': result.filename if result.is_file else None,
} }
if result.is_file: click.echo(json.dumps(output, indent=2))
output_data["filename"] = result.filename
output_data["mime_type"] = result.mime_type
click.echo(json.dumps(output_data, indent=2))
else: else:
click.secho("✓ Valid stego image", fg='green', bold=True) click.secho("✓ Verification successful!", fg='green')
click.echo(f" Payload: {payload_type} ({payload_desc})") click.echo(f" Content type: {content_type}")
click.echo(f" Size: {payload_size:,} bytes") 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: except (DecryptionError, ExtractionError) as e:
if as_json: if as_json:
import json import json
output_data = { output = {
"valid": False, 'valid': False,
"stego_file": stego, 'error': str(e),
"error": str(e),
} }
click.echo(json.dumps(output_data, indent=2)) click.echo(json.dumps(output, indent=2))
sys.exit(1) sys.exit(1)
else: else:
click.secho("Verification failed", fg='red', bold=True) raise click.ClickException(f"Verification failed: {e}")
click.echo(f" Error: {e}")
sys.exit(1)
except StegasooError as e: except StegasooError as e:
raise click.ClickException(str(e)) raise click.ClickException(str(e))
except Exception as 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') @click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def info(image, 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: try:
image_data = Path(image).read_bytes() image_data = Path(image).read_bytes()
img_info = get_image_info(image_data)
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)
if as_json: if as_json:
import json import json
output_data = { click.echo(json.dumps(img_info, indent=2))
"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))
return return
click.echo() click.echo()
click.secho(f"Image: {image}", bold=True) click.secho(f"=== Image Info: {image} ===", fg='cyan', bold=True)
click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}") click.echo(f" Format: {img_info.get('format', 'Unknown')}")
click.echo(f" Pixels: {result.details['pixels']:,}") click.echo(f" Dimensions: {img_info.get('width', '?')} × {img_info.get('height', '?')}")
click.echo(f" Mode: {result.details['mode']}") click.echo(f" Mode: {img_info.get('mode', '?')}")
click.echo(f" Format: {result.details['format']}") click.echo(f" Size: {len(image_data):,} bytes")
click.echo()
click.secho(" Capacity:", bold=True) if 'lsb_capacity' in img_info:
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)") click.echo()
click.secho(" Capacity Estimates:", fg='green')
dct_status = "" if comparison['dct']['available'] else "✗ (scipy not installed)" click.echo(f" LSB mode: {img_info['lsb_capacity']:,} bytes")
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}") if 'dct_capacity' in img_info:
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB") click.echo(f" DCT mode: {img_info['dct_capacity']:,} bytes")
if comparison['dct']['available']:
click.secho(" DCT options: grayscale/color, png/jpeg", dim=True)
click.echo() click.echo()
@@ -825,24 +1169,32 @@ def info(image, as_json):
@cli.command() @cli.command()
@click.argument('image', type=click.Path(exists=True)) @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') @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. Shows LSB vs DCT capacity and helps choose the right mode.
Optionally checks if a specific payload size would fit. Optionally checks if a specific payload would fit.
\b \b
Examples: Examples:
stegasoo compare carrier.png stegasoo compare carrier.png
stegasoo compare carrier.png --payload-size 50000 stegasoo compare carrier.png --payload secret.pdf
stegasoo compare carrier.png --json stegasoo compare carrier.png --size 50000
""" """
try: try:
image_data = Path(image).read_bytes() 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) comparison = compare_modes(image_data)
if as_json: if as_json:
@@ -1004,7 +1356,7 @@ def modes():
Displays which modes are available and their characteristics. Displays which modes are available and their characteristics.
""" """
click.echo() 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() click.echo()
# LSB Mode # LSB Mode
@@ -1039,24 +1391,41 @@ def modes():
click.echo(" --dct-color color Preserves original colors") click.echo(" --dct-color color Preserves original colors")
click.echo() click.echo()
# v3.2.0 Note # Channel Key Status (v4.0.0)
click.secho(" v3.2.0 Changes:", fg='cyan', bold=True) click.secho(" Channel Key (v4.0.0)", fg='cyan', bold=True)
click.echo(" ✓ No date parameters needed") status = get_channel_status()
click.echo(" ✓ Single passphrase (no daily rotation)") if status['mode'] == 'public':
click.echo(" ✓ Default passphrase increased to 4 words") click.echo(" Status: PUBLIC (no key configured)")
click.echo(" ✓ True asynchronous communications") 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() click.echo()
# Examples # Examples
click.secho(" Examples:", dim=True) click.secho(" Examples:", dim=True)
click.echo(" # Traditional DCT (grayscale PNG)") click.echo(" # Generate channel key")
click.echo(" stegasoo encode ... --mode dct") click.echo(" stegasoo channel generate --save")
click.echo() click.echo()
click.echo(" # Color-preserving DCT with JPEG output") click.echo(" # Encode with channel isolation")
click.echo(" stegasoo encode ... --mode dct --dct-color color --dct-format jpeg") click.echo(" stegasoo encode ... --channel XXXX-XXXX-...")
click.echo() click.echo()
click.echo(" # Compare modes for an image") click.echo(" # Decode public message (no channel key)")
click.echo(" stegasoo compare carrier.png") click.echo(" stegasoo decode ... --no-channel")
click.echo() click.echo()

1073
frontends/cli/main.py_old Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Stegasoo Web Frontend (v3.2.0) Stegasoo Web Frontend (v4.0.0)
Flask-based web UI for steganography operations. Flask-based web UI for steganography operations.
Supports both text messages and file embedding. 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: CHANGES in v3.2.0:
- Removed date dependency from all operations - Removed date dependency from all operations
- Renamed day_phrase → passphrase - Renamed day_phrase → passphrase
@@ -52,6 +58,11 @@ from stegasoo import (
EMBED_MODE_DCT, EMBED_MODE_DCT,
EMBED_MODE_AUTO, EMBED_MODE_AUTO,
has_dct_support, 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 # NOTE: encode, decode, compare_modes, will_fit_by_mode now use subprocess isolation
) )
from stegasoo.constants import ( from stegasoo.constants import (
@@ -126,6 +137,9 @@ THUMBNAIL_FILES: dict[str, bytes] = {}
@app.context_processor @app.context_processor
def inject_globals(): def inject_globals():
"""Inject global variables into all templates.""" """Inject global variables into all templates."""
# Get channel status (v4.0.0)
channel_status = get_channel_status()
return { return {
'version': __version__, 'version': __version__,
'max_message_chars': MAX_MESSAGE_CHARS, 'max_message_chars': MAX_MESSAGE_CHARS,
@@ -140,6 +154,11 @@ def inject_globals():
'default_passphrase_words': DEFAULT_PASSPHRASE_WORDS, 'default_passphrase_words': DEFAULT_PASSPHRASE_WORDS,
# NEW in v3.0 # NEW in v3.0
'has_dct': has_dct_support(), '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"DCT support: {has_dct_support()}")
print(f"QR code support: write={HAS_QRCODE}, read={HAS_QRCODE_READ}") 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 DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'): if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
@@ -164,6 +190,33 @@ except Exception as e:
print(f"Could not override stegasoo limits: {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: def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
"""Generate thumbnail from image data.""" """Generate thumbnail from image data."""
try: try:
@@ -233,6 +286,71 @@ def index():
return render_template('index.html') 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']) @app.route('/generate', methods=['GET', 'POST'])
def generate(): def generate():
if request.method == 'POST': if request.method == 'POST':
@@ -614,6 +732,9 @@ def encode_page():
if dct_color_mode not in ('grayscale', 'color'): if dct_color_mode not in ('grayscale', 'color'):
dct_color_mode = '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 # Check DCT availability
if embed_mode == 'dct' and not has_dct_support(): if embed_mode == 'dct' and not has_dct_support():
flash('DCT mode requires scipy. Install with: pip install scipy', 'error') flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
@@ -708,7 +829,7 @@ def encode_page():
flash(result.error_message, 'error') flash(result.error_message, 'error')
return render_template('encode.html', has_qrcode_read=HAS_QRCODE_READ) 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 # Use subprocess-isolated encode to prevent crashes
if payload_type == 'file' and payload_file and payload_file.filename: if payload_type == 'file' and payload_file and payload_file.filename:
encode_result = subprocess_stego.encode( encode_result = subprocess_stego.encode(
@@ -724,6 +845,7 @@ def encode_page():
embed_mode=embed_mode, embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else 'png', dct_output_format=dct_output_format if embed_mode == 'dct' else 'png',
dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color', dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color',
channel_key=channel_key, # v4.0.0
) )
else: else:
encode_result = subprocess_stego.encode( encode_result = subprocess_stego.encode(
@@ -737,6 +859,7 @@ def encode_page():
embed_mode=embed_mode, embed_mode=embed_mode,
dct_output_format=dct_output_format if embed_mode == 'dct' else 'png', dct_output_format=dct_output_format if embed_mode == 'dct' else 'png',
dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color', dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color',
channel_key=channel_key, # v4.0.0
) )
# Check for subprocess errors # Check for subprocess errors
@@ -772,6 +895,9 @@ def encode_page():
'output_format': dct_output_format if embed_mode == 'dct' else 'png', 'output_format': dct_output_format if embed_mode == 'dct' else 'png',
'color_mode': dct_color_mode if embed_mode == 'dct' else None, 'color_mode': dct_color_mode if embed_mode == 'dct' else None,
'mime_type': output_mime, '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)) 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'), embed_mode=file_info.get('embed_mode', 'lsb'),
output_format=file_info.get('output_format', 'png'), output_format=file_info.get('output_format', 'png'),
color_mode=file_info.get('color_mode'), 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'): if embed_mode not in ('auto', 'lsb', 'dct'):
embed_mode = 'auto' 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 # Check DCT availability
if embed_mode == 'dct' and not has_dct_support(): if embed_mode == 'dct' and not has_dct_support():
flash('DCT mode requires scipy. Install with: pip install scipy', 'error') flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
@@ -957,7 +1089,7 @@ def decode_page():
flash(result.error_message, 'error') flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) 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 # Use subprocess-isolated decode to prevent crashes
decode_result = subprocess_stego.decode( decode_result = subprocess_stego.decode(
stego_data=stego_data, stego_data=stego_data,
@@ -967,11 +1099,16 @@ def decode_page():
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=key_password, rsa_password=key_password,
embed_mode=embed_mode, embed_mode=embed_mode,
channel_key=channel_key, # v4.0.0
) )
# Check for subprocess errors # Check for subprocess errors
if not decode_result.success: if not decode_result.success:
error_msg = decode_result.error or 'Decoding failed' 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': if 'decrypt' in error_msg.lower() or decode_result.error_type == 'DecryptionError':
raise DecryptionError(error_msg) raise DecryptionError(error_msg)
raise StegasooError(error_msg) raise StegasooError(error_msg)
@@ -1005,7 +1142,7 @@ def decode_page():
) )
except DecryptionError: 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) return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e: except StegasooError as e:
flash(str(e), 'error') flash(str(e), 'error')

View File

@@ -696,6 +696,177 @@ const Stegasoo = {
adjust(); 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 // INITIALIZATION HELPERS
// ======================================================================== // ========================================================================
@@ -707,8 +878,30 @@ const Stegasoo = {
this.initClipboardPaste(['input[name="carrier"]', 'input[name="reference_photo"]']); this.initClipboardPaste(['input[name="carrier"]', 'input[name="reference_photo"]']);
this.initQrCropAnimation('rsaQrInput'); this.initQrCropAnimation('rsaQrInput');
this.initCollapseChevrons(); this.initCollapseChevrons();
this.initFormLoading('encodeForm', 'encodeBtn', 'Encoding...');
this.initPassphraseFontResize(); 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() { initDecodePage() {
@@ -718,13 +911,36 @@ const Stegasoo = {
this.initClipboardPaste(['input[name="stego_image"]', 'input[name="reference_photo"]']); this.initClipboardPaste(['input[name="stego_image"]', 'input[name="reference_photo"]']);
this.initQrCropAnimation('rsaKeyQrInput'); this.initQrCropAnimation('rsaKeyQrInput');
this.initCollapseChevrons(); this.initCollapseChevrons();
this.initFormLoading('decodeForm', 'decodeBtn', 'Decoding...');
this.initPassphraseFontResize(); 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() { initGeneratePage() {
this.initPasswordToggles(); this.initPasswordToggles();
// Generate page has mostly unique functionality // Channel key generator (v4.0.0)
this.initChannelKeyGenerator('channelKeyGenerated', 'generateChannelKeyBtn', 'copyChannelKeyBtn');
} }
}; };

View File

@@ -26,6 +26,23 @@
font-weight: 700 !important; 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) Mode Selection Buttons (Compact)
---------------------------------------------------------------------------- */ ---------------------------------------------------------------------------- */
@@ -34,11 +51,13 @@
border: 2px solid var(--border-light); border: 2px solid var(--border-light);
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
padding-left: 2.75rem; /* Make room for absolutely positioned radio */
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; /* For absolute positioning of radio */
} }
.mode-btn:hover { .mode-btn:hover {
@@ -52,10 +71,35 @@
} }
.mode-btn .form-check-input { .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; 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 { .mode-info-icon {
cursor: help; cursor: help;
opacity: 0.6; opacity: 0.6;

View File

@@ -1,10 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Stegasoo Subprocess Worker Stegasoo Subprocess Worker (v4.0.0)
This script runs in a subprocess and handles encode/decode operations. This script runs in a subprocess and handles encode/decode operations.
If it crashes due to jpegio/scipy issues, the parent Flask process survives. 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: Communication is via JSON over stdin/stdout:
- Input: JSON object with operation parameters - Input: JSON object with operation parameters
- Output: JSON object with results or error - 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)) 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: def encode_operation(params: dict) -> dict:
"""Handle encode operation.""" """Handle encode operation."""
from stegasoo import encode, FilePayload from stegasoo import encode, FilePayload
@@ -48,6 +95,9 @@ def encode_operation(params: dict) -> dict:
else: else:
payload = params.get('message', '') 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 # Call encode with correct parameter names
result = encode( result = encode(
message=payload, message=payload,
@@ -60,6 +110,7 @@ def encode_operation(params: dict) -> dict:
embed_mode=params.get('embed_mode', 'lsb'), embed_mode=params.get('embed_mode', 'lsb'),
dct_output_format=params.get('dct_output_format', 'png'), dct_output_format=params.get('dct_output_format', 'png'),
dct_color_mode=params.get('dct_color_mode', 'color'), dct_color_mode=params.get('dct_color_mode', 'color'),
channel_key=resolved_channel_key, # v4.0.0
) )
# Build stats dict if available # Build stats dict if available
@@ -71,11 +122,16 @@ def encode_operation(params: dict) -> dict:
'bytes_embedded': getattr(result.stats, 'bytes_embedded', 0), '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 { return {
'success': True, 'success': True,
'stego_b64': base64.b64encode(result.stego_image).decode('ascii'), 'stego_b64': base64.b64encode(result.stego_image).decode('ascii'),
'filename': getattr(result, 'filename', None), 'filename': getattr(result, 'filename', None),
'stats': stats, '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'): if params.get('rsa_key_b64'):
rsa_key_data = base64.b64decode(params['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 # Call decode with correct parameter names
result = decode( result = decode(
stego_image=stego_data, stego_image=stego_data,
@@ -101,6 +160,7 @@ def decode_operation(params: dict) -> dict:
rsa_key_data=rsa_key_data, rsa_key_data=rsa_key_data,
rsa_password=params.get('rsa_password'), rsa_password=params.get('rsa_password'),
embed_mode=params.get('embed_mode', 'auto'), embed_mode=params.get('embed_mode', 'auto'),
channel_key=resolved_channel_key, # v4.0.0
) )
if result.is_file: 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(): def main():
"""Main entry point - read JSON from stdin, write JSON to stdout.""" """Main entry point - read JSON from stdin, write JSON to stdout."""
try: try:
@@ -170,6 +249,8 @@ def main():
output = compare_operation(params) output = compare_operation(params)
elif operation == 'capacity': elif operation == 'capacity':
output = capacity_check_operation(params) output = capacity_check_operation(params)
elif operation == 'channel_status':
output = channel_status_operation(params)
else: else:
output = {'success': False, 'error': f'Unknown operation: {operation}'} output = {'success': False, 'error': f'Unknown operation: {operation}'}

View File

@@ -1,15 +1,19 @@
""" """
Subprocess Steganography Wrapper Subprocess Steganography Wrapper (v4.0.0)
Runs stegasoo operations in isolated subprocesses to prevent crashes Runs stegasoo operations in isolated subprocesses to prevent crashes
from taking down the Flask server. 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: Usage:
from subprocess_stego import SubprocessStego from subprocess_stego import SubprocessStego
stego = SubprocessStego() stego = SubprocessStego()
# Encode # Encode with channel key
result = stego.encode( result = stego.encode(
carrier_data=carrier_bytes, carrier_data=carrier_bytes,
reference_data=ref_bytes, reference_data=ref_bytes,
@@ -17,6 +21,7 @@ Usage:
passphrase="my passphrase", passphrase="my passphrase",
pin="123456", pin="123456",
embed_mode="dct", embed_mode="dct",
channel_key="auto", # or "none", or explicit key
) )
if result.success: if result.success:
@@ -31,6 +36,7 @@ Usage:
reference_data=ref_bytes, reference_data=ref_bytes,
passphrase="my passphrase", passphrase="my passphrase",
pin="123456", pin="123456",
channel_key="auto",
) )
# Compare modes (capacity) # Compare modes (capacity)
@@ -60,6 +66,9 @@ class EncodeResult:
stego_data: Optional[bytes] = None stego_data: Optional[bytes] = None
filename: Optional[str] = None filename: Optional[str] = None
stats: Optional[Dict[str, Any]] = 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: Optional[str] = None
error_type: Optional[str] = None error_type: Optional[str] = None
@@ -101,6 +110,18 @@ class CapacityResult:
error: Optional[str] = None 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: class SubprocessStego:
""" """
Subprocess-isolated steganography operations. Subprocess-isolated steganography operations.
@@ -205,6 +226,8 @@ class SubprocessStego:
embed_mode: str = "lsb", embed_mode: str = "lsb",
dct_output_format: str = "png", dct_output_format: str = "png",
dct_color_mode: str = "color", dct_color_mode: str = "color",
# Channel key (v4.0.0)
channel_key: Optional[str] = "auto",
timeout: Optional[int] = None, timeout: Optional[int] = None,
) -> EncodeResult: ) -> EncodeResult:
""" """
@@ -224,6 +247,7 @@ class SubprocessStego:
embed_mode: 'lsb' or 'dct' embed_mode: 'lsb' or 'dct'
dct_output_format: 'png' or 'jpeg' (for DCT mode) dct_output_format: 'png' or 'jpeg' (for DCT mode)
dct_color_mode: 'grayscale' or 'color' (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 timeout: Operation timeout in seconds
Returns: Returns:
@@ -239,6 +263,7 @@ class SubprocessStego:
'embed_mode': embed_mode, 'embed_mode': embed_mode,
'dct_output_format': dct_output_format, 'dct_output_format': dct_output_format,
'dct_color_mode': dct_color_mode, 'dct_color_mode': dct_color_mode,
'channel_key': channel_key, # v4.0.0
} }
if file_data: if file_data:
@@ -258,6 +283,8 @@ class SubprocessStego:
stego_data=base64.b64decode(result['stego_b64']), stego_data=base64.b64decode(result['stego_b64']),
filename=result.get('filename'), filename=result.get('filename'),
stats=result.get('stats'), stats=result.get('stats'),
channel_mode=result.get('channel_mode'),
channel_fingerprint=result.get('channel_fingerprint'),
) )
else: else:
return EncodeResult( return EncodeResult(
@@ -275,6 +302,8 @@ class SubprocessStego:
rsa_key_data: Optional[bytes] = None, rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None, rsa_password: Optional[str] = None,
embed_mode: str = "auto", embed_mode: str = "auto",
# Channel key (v4.0.0)
channel_key: Optional[str] = "auto",
timeout: Optional[int] = None, timeout: Optional[int] = None,
) -> DecodeResult: ) -> DecodeResult:
""" """
@@ -288,6 +317,7 @@ class SubprocessStego:
rsa_key_data: Optional RSA key PEM bytes rsa_key_data: Optional RSA key PEM bytes
rsa_password: RSA key password if encrypted rsa_password: RSA key password if encrypted
embed_mode: 'auto', 'lsb', or 'dct' embed_mode: 'auto', 'lsb', or 'dct'
channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
timeout: Operation timeout in seconds timeout: Operation timeout in seconds
Returns: Returns:
@@ -300,6 +330,7 @@ class SubprocessStego:
'passphrase': passphrase, 'passphrase': passphrase,
'pin': pin, 'pin': pin,
'embed_mode': embed_mode, 'embed_mode': embed_mode,
'channel_key': channel_key, # v4.0.0
} }
if rsa_key_data: if rsa_key_data:
@@ -411,6 +442,44 @@ class SubprocessStego:
success=False, success=False,
error=result.get('error', 'Unknown error'), 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 # Convenience function for quick usage

View File

@@ -11,8 +11,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="lead"> <p class="lead">
Stegasoo is a steganography tool that hides encrypted messages and files Stegasoo hides encrypted messages and files inside images using multi-factor authentication.
inside ordinary images using multi-factor authentication.
</p> </p>
<h6 class="text-primary mt-4 mb-3">Features</h6> <h6 class="text-primary mt-4 mb-3">Features</h6>
@@ -22,22 +21,22 @@
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i> <i class="bi bi-check-circle text-success me-2"></i>
<strong>Text &amp; File Embedding</strong> <strong>Text &amp; File Embedding</strong>
<br><small class="text-muted">Hide messages or any file type (PDF, ZIP, documents)</small> <br><small class="text-muted">Any file type: PDF, ZIP, documents</small>
</li> </li>
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i> <i class="bi bi-check-circle text-success me-2"></i>
<strong>Multi-Factor Security</strong> <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>
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i> <i class="bi bi-check-circle text-success me-2"></i>
<strong>AES-256-GCM Encryption</strong> <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>
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i> <i class="bi bi-check-circle text-success me-2"></i>
<strong>LSB &amp; DCT Modes</strong> <strong>DCT &amp; LSB Modes</strong>
<br><small class="text-muted">Choose capacity (LSB) or JPEG resilience (DCT)</small> <br><small class="text-muted">JPEG resilience (DCT) or high capacity (LSB)</small>
</li> </li>
</ul> </ul>
</div> </div>
@@ -46,12 +45,12 @@
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i> <i class="bi bi-check-circle text-success me-2"></i>
<strong>Random Pixel Embedding</strong> <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>
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i> <i class="bi bi-check-circle text-success me-2"></i>
<strong>Large Image Support</strong> <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>
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i> <i class="bi bi-check-circle text-success me-2"></i>
@@ -61,7 +60,13 @@
<li class="mb-2"> <li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i> <i class="bi bi-check-circle text-success me-2"></i>
<strong>QR Code Keys</strong> <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> </li>
</ul> </ul>
</div> </div>
@@ -75,67 +80,61 @@
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Embedding Modes</h5> <h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Embedding Modes</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<p> <p>Two modes optimized for different use cases.</p>
Stegasoo supports two embedding modes, each optimized for different use cases.
</p>
<div class="row mt-4"> <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 --> <!-- DCT Mode -->
<div class="col-md-6 mb-4"> <div class="col-md-6 mb-4">
<div class="card bg-dark h-100"> <div class="card bg-dark h-100">
<div class="card-header"> <div class="card-header">
<i class="bi bi-soundwave text-warning me-2"></i> <i class="bi bi-soundwave text-warning me-2"></i>
<strong>DCT Mode</strong> <strong>DCT Mode</strong>
<span class="badge bg-success ms-2">Default</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="small"> <p class="small">
<strong>DCT (Discrete Cosine Transform)</strong> embeds data in frequency <strong>DCT (Discrete Cosine Transform)</strong> embeds data in frequency coefficients. Survives JPEG recompression.
coefficients rather than raw pixels. This survives JPEG recompression
because coefficients are preserved during re-encoding.
</p> </p>
<ul class="small mb-0"> <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>Output:</strong> JPEG or PNG</li>
<li><strong>Color:</strong> Color or grayscale</li> <li><strong>Color:</strong> Color or grayscale</li>
<li><strong>Speed:</strong> Slower (~2s)</li> <li><strong>Speed:</strong> ~2s</li>
</ul> </ul>
<hr> <hr>
<div class="small"> <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> 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> 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> 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> </div>
</div> </div>
@@ -149,35 +148,30 @@
<thead> <thead>
<tr> <tr>
<th>Aspect</th> <th>Aspect</th>
<th>DCT Mode <span class="badge bg-success ms-1">Default</span></th>
<th>LSB Mode</th> <th>LSB Mode</th>
<th>DCT Mode</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>Capacity (1080p)</td> <td>Capacity (1080p)</td>
<td class="text-success">~770 KB</td>
<td class="text-warning">~50 KB</td> <td class="text-warning">~50 KB</td>
<td class="text-success">~770 KB</td>
</tr> </tr>
<tr> <tr>
<td>Survives JPEG</td> <td>Survives JPEG</td>
<td class="text-danger">❌ No</td>
<td class="text-success">✅ Yes</td> <td class="text-success">✅ Yes</td>
<td class="text-danger">❌ No</td>
</tr> </tr>
<tr> <tr>
<td>Social Media</td> <td>Social Media</td>
<td class="text-danger">❌ Broken</td>
<td class="text-success">✅ Works</td> <td class="text-success">✅ Works</td>
<td class="text-danger">❌ Broken</td>
</tr> </tr>
<tr> <tr>
<td>Detection Resistance</td> <td>Detection Resistance</td>
<td>Moderate</td>
<td>Better</td> <td>Better</td>
</tr> <td>Moderate</td>
<tr>
<td>Dependencies</td>
<td>Pillow, NumPy</td>
<td>+ scipy, jpegio</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -185,8 +179,7 @@
<div class="alert alert-info small mt-3 mb-0"> <div class="alert alert-info small mt-3 mb-0">
<i class="bi bi-lightbulb me-2"></i> <i class="bi bi-lightbulb me-2"></i>
<strong>Auto-Detection:</strong> When decoding, Stegasoo automatically detects whether <strong>Auto-Detection:</strong> Mode is detected automatically when decoding.
LSB or DCT mode was used. You don't need to specify the mode during decoding.
</div> </div>
</div> </div>
</div> </div>
@@ -196,64 +189,149 @@
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>How Security Works</h5> <h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>How Security Works</h5>
</div> </div>
<div class="card-body"> <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="row text-center my-4">
<div class="col-md-3 mb-3"> <div class="col-6 col-lg-3 mb-3">
<div class="p-3 bg-dark rounded"> <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> <i class="bi bi-image text-info fs-2 d-block mb-2"></i>
<strong>Reference Photo</strong> <strong>Reference Photo</strong>
<div class="small text-muted mt-1">Something you have</div> <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> </div>
<div class="col-md-3 mb-3"> <div class="col-6 col-lg-3 mb-3">
<div class="p-3 bg-dark rounded"> <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> <i class="bi bi-chat-quote text-warning fs-2 d-block mb-2"></i>
<strong>Passphrase</strong> <strong>Passphrase</strong>
<div class="small text-muted mt-1">Something you know</div> <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> </div>
<div class="col-md-3 mb-3"> <div class="col-6 col-lg-3 mb-3">
<div class="p-3 bg-dark rounded"> <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> <i class="bi bi-123 text-danger fs-2 d-block mb-2"></i>
<strong>Static PIN</strong> <strong>Static PIN</strong>
<div class="small text-muted mt-1">Something you know</div> <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> </div>
<div class="col-md-3 mb-3"> <div class="col-6 col-lg-3 mb-3">
<div class="p-3 bg-dark rounded"> <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> <i class="bi bi-key text-primary fs-2 d-block mb-2"></i>
<strong>RSA Key</strong> <strong>RSA Key</strong>
<div class="small text-muted mt-1">Something you have (optional)</div> <div class="small text-muted mt-1">Optional</div>
<div class="small text-success">~128 bits</div> <div class="small text-success mt-auto pt-2">~128 bits</div>
</div> </div>
</div> </div>
</div> </div>
<div class="alert alert-secondary"> <div class="alert alert-secondary">
<i class="bi bi-calculator me-2"></i> <i class="bi bi-calculator me-2"></i>
<strong>Combined entropy:</strong> 144-424+ bits depending on configuration. <strong>Combined entropy:</strong> 144-424+ bits. 128 bits is infeasible to brute force.
For reference, 128 bits is considered computationally infeasible to brute force.
</div> </div>
<h6 class="mt-4">Key Derivation</h6> <h6 class="mt-4">Key Derivation</h6>
<p> <p>
{% if has_argon2 %} {% if has_argon2 %}
<span class="badge bg-success me-1"><i class="bi bi-check"></i> Argon2id</span> <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 256MB memory cost. Memory-hard KDF defeats GPU/ASIC attacks.
makes GPU/ASIC attacks infeasible.
{% else %} {% else %}
<span class="badge bg-warning text-dark me-1"><i class="bi bi-exclamation-triangle"></i> Argon2 Not Available</span> <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. Install <code>argon2-cffi</code> for stronger security.
{% endif %} {% endif %}
</p> </p>
</div> </div>
</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 --> <!-- Version History -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
@@ -272,18 +350,18 @@
<tr> <tr>
<td><strong>4.0.0</strong></td> <td><strong>4.0.0</strong></td>
<td> <td>
Simplified auth (no date dependency), passphrase replaces day_phrase, <strong>Channel keys</strong> for group/deployment isolation,
4-word default, JPEG normalization fix, large image support (14MB+ tested), DCT default, simplified auth, passphrase replaces day_phrase,
subprocess isolation for stability, Python 3.10-3.12 required 4-word default, JPEG fix, large image support, subprocess isolation, Python 3.10-3.12
</td> </td>
</tr> </tr>
<tr> <tr>
<td>3.2.0</td> <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>
<tr> <tr>
<td>3.0.0</td> <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>
<tr> <tr>
<td>2.2.0</td> <td>2.2.0</td>
@@ -291,11 +369,11 @@
</tr> </tr>
<tr> <tr>
<td>2.1.0</td> <td>2.1.0</td>
<td>File embedding, compression support</td> <td>File embedding, compression</td>
</tr> </tr>
<tr> <tr>
<td>2.0.0</td> <td>2.0.0</td>
<td>Web UI, REST API, RSA key support</td> <td>Web UI, REST API, RSA keys</td>
</tr> </tr>
<tr> <tr>
<td>1.0.0</td> <td>1.0.0</td>
@@ -304,12 +382,6 @@
</tbody> </tbody>
</table> </table>
</div> </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>
</div> </div>
@@ -329,11 +401,11 @@
<div id="setup" class="accordion-collapse collapse show" data-bs-parent="#usageAccordion"> <div id="setup" class="accordion-collapse collapse show" data-bs-parent="#usageAccordion">
<div class="accordion-body"> <div class="accordion-body">
<ol> <ol>
<li>Both parties agree on a <strong>reference photo</strong> (shared secretly, never transmitted)</li> <li>Agree on a <strong>reference photo</strong> (never transmitted)</li>
<li>Go to <a href="/generate">Generate</a> and create credentials</li> <li>Go to <a href="/generate">Generate</a> to create credentials</li>
<li><strong>Memorize</strong> the passphrase and PIN</li> <li>Memorize passphrase and PIN</li>
<li>If using RSA, download and securely store the key file</li> <li>If using RSA, store the key file securely</li>
<li>Share credentials with your contact through a secure channel</li> <li>Share credentials via secure channel</li>
</ol> </ol>
</div> </div>
</div> </div>
@@ -343,24 +415,23 @@
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button collapsed bg-dark text-light" type="button" <button class="accordion-button collapsed bg-dark text-light" type="button"
data-bs-toggle="collapse" data-bs-target="#encoding"> 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> </button>
</h2> </h2>
<div id="encoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion"> <div id="encoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
<div class="accordion-body"> <div class="accordion-body">
<ol> <ol>
<li>Go to <a href="/encode">Encode</a></li> <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> <ul>
<li><strong>LSB</strong> for email, cloud storage, direct transfer</li> <li><strong>DCT</strong> (default): social media</li>
<li><strong>DCT</strong> for social media (Instagram, WhatsApp, etc.)</li> <li><strong>LSB</strong>: email, cloud, direct transfer</li>
</ul> </ul>
</li> </li>
<li>Upload your <strong>reference photo</strong> and <strong>carrier image</strong></li> <li>Enter message or select file</li>
<li>Enter your message or select a file to embed</li> <li>Enter passphrase and PIN/key</li>
<li>Enter your <strong>passphrase</strong> and PIN/key</li> <li>Download stego image</li>
<li>Download the resulting stego image</li>
<li>Send through any channel!</li>
</ol> </ol>
</div> </div>
</div> </div>
@@ -370,22 +441,21 @@
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button collapsed bg-dark text-light" type="button" <button class="accordion-button collapsed bg-dark text-light" type="button"
data-bs-toggle="collapse" data-bs-target="#decoding"> 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> </button>
</h2> </h2>
<div id="decoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion"> <div id="decoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
<div class="accordion-body"> <div class="accordion-body">
<ol> <ol>
<li>Go to <a href="/decode">Decode</a></li> <li>Go to <a href="/decode">Decode</a></li>
<li>Upload your <strong>reference photo</strong> (same one used for encoding)</li> <li>Upload <strong>reference photo</strong></li>
<li>Upload the <strong>stego image</strong> you received</li> <li>Upload <strong>stego image</strong></li>
<li>Enter your <strong>passphrase</strong></li> <li>Enter passphrase and PIN/key</li>
<li>Enter your PIN and/or RSA key</li> <li>View message or download file</li>
<li>View the decoded message or download the extracted file</li>
</ol> </ol>
<div class="alert alert-info small mt-3 mb-0"> <div class="alert alert-info small mt-3 mb-0">
<i class="bi bi-magic me-2"></i> <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> </div>
</div> </div>
@@ -396,67 +466,64 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits &amp; Specifications</h5> <h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits &amp; Specs</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<table class="table table-dark table-striped small"> <table class="table table-dark table-striped small">
<tbody> <tbody>
<tr> <tr>
<td><i class="bi bi-file-text me-2"></i>Max text message</td> <td><i class="bi bi-file-text me-2"></i>Max text</td>
<td><strong>2 million characters</strong></td> <td><strong>2M characters</strong></td>
</tr> </tr>
<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> <td><strong>{{ max_payload_kb }} KB</strong></td>
</tr> </tr>
<tr> <tr>
<td><i class="bi bi-image me-2"></i>Max carrier image</td> <td><i class="bi bi-image me-2"></i>Max carrier</td>
<td><strong>24 megapixels</strong> (~6000×4000)</td> <td><strong>24 MP</strong> (~6000x4000)</td>
</tr>
<tr>
<td><i class="bi bi-grid-3x3 me-2"></i>LSB capacity</td>
<td><strong>~375 KB/megapixel</strong></td>
</tr> </tr>
<tr> <tr>
<td><i class="bi bi-soundwave me-2"></i>DCT capacity</td> <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>
<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> <td><strong>30 MB</strong></td>
</tr> </tr>
<tr> <tr>
<td><i class="bi bi-clock me-2"></i>Temp file expiry</td> <td><i class="bi bi-clock me-2"></i>File expiry</td>
<td><strong>5 minutes</strong></td> <td><strong>5 min</strong></td>
</tr> </tr>
<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> <td><strong>6-9 digits</strong></td>
</tr> </tr>
<tr> <tr>
<td><i class="bi bi-shield me-2"></i>RSA key sizes</td> <td><i class="bi bi-shield me-2"></i>RSA keys</td>
<td><strong>2048, 3072, 4096 bits</strong></td> <td><strong>2048, 3072, 4096 bit</strong></td>
</tr> </tr>
<tr> <tr>
<td><i class="bi bi-chat-quote me-2"></i>Passphrase length</td> <td><i class="bi bi-chat-quote me-2"></i>Passphrase</td>
<td><strong>3-12 words</strong> (BIP-39, recommended: 4+ words)</td> <td><strong>3-12 words</strong> (BIP-39)</td>
</tr> </tr>
<tr> <tr>
<td><i class="bi bi-code me-2"></i>Python version</td> <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><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> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="text-center mt-4 text-muted small">
<p>
Stegasoo v{{ version }} &bull;
<i class="bi bi-github me-1"></i>Open Source &bull;
Built with Python, Flask, and cryptography
</p>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -266,6 +266,7 @@
<div class="row"> <div class="row">
<div class="col-md-4 mb-3"> <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> <label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<div class="input-group pin-input-container"> <div class="input-group pin-input-container">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;"> <input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
@@ -274,9 +275,11 @@
</button> </button>
</div> </div>
<div class="form-text">If PIN was used during encoding</div> <div class="form-text">If PIN was used during encoding</div>
</div>
</div> </div>
<div class="col-md-8 mb-3"> <div class="col-md-8 mb-3">
<div class="security-box">
<label class="form-label"> <label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key <i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label> </label>
@@ -333,9 +336,64 @@
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</button> </button>
</div> </div>
</div>
</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 ADVANCED OPTIONS (v3.0) - Extraction Mode
================================================================ --> ================================================================ -->
@@ -355,51 +413,34 @@
<span class="badge bg-info ms-1">v3.0</span> <span class="badge bg-info ms-1">v3.0</span>
</label> </label>
<div class="row g-2"> <div class="d-flex gap-2">
<!-- Auto Mode --> <!-- Auto Mode -->
<div class="col-4"> <label class="mode-btn flex-fill active" id="autoModeCard" for="modeAuto">
<div class="form-check card p-2 text-center h-100" id="autoModeCard"> <input class="form-check-input" type="radio" name="embed_mode" id="modeAuto" value="auto" checked>
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeAuto" value="auto" checked> <i class="bi bi-magic text-success"></i>
<label class="form-check-label w-100" for="modeAuto"> <span class="ms-2"><strong>Auto</strong> <span class="text-muted d-none d-sm-inline">· Try both</span></span>
<i class="bi bi-magic text-success fs-4 d-block mb-1"></i> </label>
<strong>Auto</strong>
<div class="small text-muted">Try both</div>
</label>
</div>
</div>
<!-- LSB Mode --> <!-- LSB Mode -->
<div class="col-4"> <label class="mode-btn flex-fill" id="lsbModeCardDec" for="modeLsbDec">
<div class="form-check card p-2 text-center h-100" id="lsbModeCardDec"> <input class="form-check-input" type="radio" name="embed_mode" id="modeLsbDec" value="lsb">
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeLsbDec" value="lsb"> <i class="bi bi-grid-3x3-gap text-primary"></i>
<label class="form-check-label w-100" for="modeLsbDec"> <span class="ms-2"><strong>LSB</strong> <span class="text-muted d-none d-sm-inline">· Spatial</span></span>
<i class="bi bi-grid-3x3-gap text-primary fs-4 d-block mb-1"></i> </label>
<strong>LSB</strong>
<div class="small text-muted">Spatial only</div>
</label>
</div>
</div>
<!-- DCT Mode --> <!-- DCT Mode -->
<div class="col-4"> <label class="mode-btn flex-fill {% if not has_dct %}opacity-50{% endif %}" id="dctModeCardDec" for="modeDctDec">
<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" type="radio" name="embed_mode" id="modeDctDec" value="dct" {% if not has_dct %}disabled{% endif %}>
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeDctDec" value="dct" {% if not has_dct %}disabled{% endif %}> <i class="bi bi-soundwave text-warning"></i>
<label class="form-check-label w-100" for="modeDctDec"> <span class="ms-2"><strong>DCT</strong> <span class="text-muted d-none d-sm-inline">· Frequency</span></span>
<i class="bi bi-soundwave text-info fs-4 d-block mb-1"></i> </label>
<strong>DCT</strong>
<div class="small text-muted">
{% if has_dct %}Frequency only{% else %}N/A{% endif %}
</div>
</label>
</div>
</div>
</div> </div>
<div class="form-text mt-2"> <div class="form-text mt-2">
<i class="bi bi-lightbulb me-1"></i> <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 %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
@@ -442,6 +483,10 @@
<i class="bi bi-exclamation-triangle-fill text-warning me-1"></i> <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) <strong>Format compatibility:</strong> v4.0 cannot decode messages from v3.1 or earlier (different format)
</li> </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"> <li class="mb-2">
<i class="bi bi-info-circle-fill text-info me-1"></i> <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) If using an RSA key, verify the <strong>password is correct</strong> (if key is encrypted)
@@ -461,34 +506,22 @@
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script> <script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
<script> <script>
// ============================================================================ // Extraction mode button active state toggle
// DECODE PAGE - Initialize shared components const extractModeRadios = document.querySelectorAll('input[name="embed_mode"]');
// ============================================================================ const extractModeBtns = {
'auto': document.getElementById('autoModeCard'),
'lsb': document.getElementById('lsbModeCardDec'),
'dct': document.getElementById('dctModeCardDec')
};
Stegasoo.initPasswordToggles(); extractModeRadios.forEach(radio => {
Stegasoo.initRsaMethodToggle(); radio.addEventListener('change', () => {
Stegasoo.initDropZones(); Object.values(extractModeBtns).forEach(btn => btn?.classList.remove('active'));
Stegasoo.initClipboardPaste(['input[name="stego_image"]', 'input[name="reference_photo"]']); extractModeBtns[radio.value]?.classList.add('active');
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' }
}
}); });
// ============================================================================ // Advanced options chevron
// DECODE PAGE - Advanced options chevron
// ============================================================================
const advancedOptionsDec = document.getElementById('advancedOptionsDec'); const advancedOptionsDec = document.getElementById('advancedOptionsDec');
advancedOptionsDec?.addEventListener('show.bs.collapse', () => { advancedOptionsDec?.addEventListener('show.bs.collapse', () => {
document.getElementById('advancedChevronDec')?.classList.replace('bi-chevron-down', 'bi-chevron-up'); 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', () => { advancedOptionsDec?.addEventListener('hide.bs.collapse', () => {
document.getElementById('advancedChevronDec')?.classList.replace('bi-chevron-up', 'bi-chevron-down'); 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> </script>
{% endblock %} {% endblock %}

View File

@@ -333,17 +333,20 @@
<div class="row"> <div class="row">
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label> <div class="security-box">
<div class="input-group pin-input-container"> <label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;"> <div class="input-group pin-input-container">
<button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput"> <input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
<i class="bi bi-eye"></i> <button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput">
</button> <i class="bi bi-eye"></i>
</button>
</div>
<div class="form-text">Static 6-9 digit PIN</div>
</div> </div>
<div class="form-text">Static 6-9 digit PIN</div>
</div> </div>
<div class="col-md-8 mb-3"> <div class="col-md-8 mb-3">
<div class="security-box">
<label class="form-label"> <label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key <i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label> </label>
@@ -400,9 +403,70 @@
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</button> </button>
</div> </div>
</div>
</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) --> <!-- Advanced Options (DCT sub-options only) -->
<div class="mb-4 {% if not has_dct %}d-none{% endif %}" id="advancedOptionsContainer"> <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"> <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> </a>
<div class="collapse" id="advancedOptions"> <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"> <!-- DCT Color Mode - Compact -->
<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 -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label"> <label class="form-label small mb-2">
<i class="bi bi-palette me-1"></i> Color Mode <i class="bi bi-palette me-1"></i> Color
</label> </label>
<div class="d-flex gap-2">
<div class="row g-2"> <label class="mode-btn equal-width active" id="dctColorCard" for="dctColorColor">
<div class="col-6"> <input class="form-check-input" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked>
<div class="form-check card p-2 text-center border-success border-2" id="dctColorCard"> <i class="bi bi-palette-fill text-success"></i>
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked> <span class="ms-2"><strong>Color</strong> <span class="badge bg-success ms-1">Default</span></span>
<label class="form-check-label w-100" for="dctColorColor"> </label>
<i class="bi bi-palette-fill text-success fs-5 d-block"></i> <label class="mode-btn equal-width" id="dctGrayscaleCard" for="dctColorGrayscale">
<strong>Color</strong> <input class="form-check-input" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
<div class="small text-muted">Recommended</div> <i class="bi bi-circle-half text-secondary"></i>
</label> <span class="ms-2"><strong>Grayscale</strong></span>
</div> </label>
</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> </div>
</div> </div>
<!-- DCT Output Format --> <!-- DCT Output Format - Compact -->
<div class="mb-0"> <div class="mb-0">
<label class="form-label"> <label class="form-label small mb-2">
<i class="bi bi-file-image me-1"></i> Output Format <i class="bi bi-file-image me-1"></i> Format
</label> </label>
<div class="d-flex gap-2">
<div class="row g-2"> <label class="mode-btn equal-width active" id="dctJpegCard" for="dctFormatJpeg">
<div class="col-6"> <input class="form-check-input" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg" checked>
<div class="form-check card p-2 text-center" id="dctPngCard"> <i class="bi bi-file-earmark-richtext text-warning"></i>
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png"> <span class="ms-2"><strong>JPEG</strong> <span class="badge bg-warning text-dark ms-1">Default</span></span>
<label class="form-check-label w-100" for="dctFormatPng"> </label>
<i class="bi bi-file-earmark-image text-primary fs-5 d-block"></i> <label class="mode-btn equal-width" id="dctPngCard" for="dctFormatPng">
<strong>PNG</strong> <input class="form-check-input" type="radio" name="dct_output_format" id="dctFormatPng" value="png">
<div class="small text-muted">Lossless, larger</div> <i class="bi bi-file-earmark-image text-primary"></i>
</label> <span class="ms-2"><strong>PNG</strong> <span class="text-muted d-none d-sm-inline">· Lossless</span></span>
</div> </label>
</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> </div>
</div> </div>
@@ -520,17 +557,6 @@
{% block scripts %} {% block scripts %}
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script> <script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
<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 // ENCODE PAGE - Payload type switching
// ============================================================================ // ============================================================================
@@ -683,22 +709,26 @@ document.querySelectorAll('input[name="embed_mode"]').forEach(radio => {
}); });
}); });
// DCT format cards // DCT color mode button active state toggle
Stegasoo.initModeCards({ const colorModeRadios = document.querySelectorAll('input[name="dct_color_mode"]');
radioName: 'dct_output_format', const colorModeBtns = { 'color': document.getElementById('dctColorCard'), 'grayscale': document.getElementById('dctGrayscaleCard') };
cards: {
'png': { id: 'dctPngCard', borderClass: 'border-primary' }, colorModeRadios.forEach(radio => {
'jpeg': { id: 'dctJpegCard', borderClass: 'border-warning' } radio.addEventListener('change', () => {
} Object.values(colorModeBtns).forEach(btn => btn?.classList.remove('active'));
colorModeBtns[radio.value]?.classList.add('active');
});
}); });
// DCT color cards // DCT format button active state toggle
Stegasoo.initModeCards({ const formatRadios = document.querySelectorAll('input[name="dct_output_format"]');
radioName: 'dct_color_mode', const formatBtns = { 'png': document.getElementById('dctPngCard'), 'jpeg': document.getElementById('dctJpegCard') };
cards: {
'color': { id: 'dctColorCard', borderClass: 'border-success' }, formatRadios.forEach(radio => {
'grayscale': { id: 'dctGrayscaleCard', borderClass: 'border-secondary' } radio.addEventListener('change', () => {
} Object.values(formatBtns).forEach(btn => btn?.classList.remove('active'));
formatBtns[radio.value]?.classList.add('active');
});
}); });
// Advanced options chevron // Advanced options chevron
@@ -735,17 +765,5 @@ function checkDuplicateFiles() {
document.querySelector('input[name="reference_photo"]')?.addEventListener('change', checkDuplicateFiles); document.querySelector('input[name="reference_photo"]')?.addEventListener('change', checkDuplicateFiles);
document.querySelector('input[name="carrier"]')?.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> </script>
{% endblock %} {% endblock %}

View File

@@ -91,6 +91,24 @@
</span> </span>
<div class="small text-muted mt-2">Full color PNG, spatial LSB embedding</div> <div class="small text-muted mt-2">Full color PNG, spatial LSB embedding</div>
{% endif %} {% 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>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
@@ -123,6 +141,9 @@
<li>Color preserved - extraction works on both color and grayscale</li> <li>Color preserved - extraction works on both color and grayscale</li>
{% endif %} {% endif %}
{% 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> </ul>
</div> </div>

View File

@@ -3,7 +3,7 @@
{% block title %}Generate Credentials - Stegasoo{% endblock %} {% block title %}Generate Credentials - Stegasoo{% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center" data-page="generate">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
@@ -74,6 +74,32 @@
</div> </div>
</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"> <button type="submit" class="btn btn-primary btn-lg w-100 mt-3">
<i class="bi bi-shuffle me-2"></i>Generate Credentials <i class="bi bi-shuffle me-2"></i>Generate Credentials
</button> </button>

View File

@@ -19,6 +19,20 @@
</div> </div>
</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"> <div class="row g-4 mb-5">
<!-- Encode Card --> <!-- Encode Card -->
<div class="col-md-4"> <div class="col-md-4">
@@ -81,22 +95,22 @@
<div class="row text-center"> <div class="row text-center">
<div class="col-md-6 mb-3 mb-md-0"> <div class="col-md-6 mb-3 mb-md-0">
<div class="p-3 bg-dark rounded h-100"> <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> <i class="bi bi-soundwave text-warning fs-2 d-block mb-2"></i>
<strong>LSB Mode</strong> <strong>DCT Mode</strong>
<span class="badge bg-success ms-1">Default</span> <span class="badge bg-success ms-1">Default</span>
<div class="small text-muted mt-2"> <div class="small text-muted mt-2">
Higher capacity (~375 KB/MP)<br> Survives JPEG recompression<br>
Best for email &amp; file transfer Best for social media
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="p-3 bg-dark rounded h-100"> <div class="p-3 bg-dark rounded h-100">
<i class="bi bi-soundwave text-warning fs-2 d-block mb-2"></i> <i class="bi bi-grid-3x3-gap text-primary fs-2 d-block mb-2"></i>
<strong>DCT Mode</strong> <strong>LSB Mode</strong>
<div class="small text-muted mt-2"> <div class="small text-muted mt-2">
Survives JPEG recompression<br> Higher capacity (~375 KB/MP)<br>
Best for social media Best for email &amp; file transfer
</div> </div>
</div> </div>
</div> </div>
@@ -116,15 +130,15 @@
<ul class="list-unstyled small"> <ul class="list-unstyled small">
<li class="mb-1"> <li class="mb-1">
<i class="bi bi-image text-info me-2"></i> <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>
<li class="mb-1"> <li class="mb-1">
<i class="bi bi-chat-quote text-info me-2"></i> <i class="bi bi-chat-quote text-info me-2"></i>
<strong>Passphrase</strong> 4+ words <strong>Passphrase</strong>: 4+ words
</li> </li>
<li class="mb-1"> <li class="mb-1">
<i class="bi bi-123 text-info me-2"></i> <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> </li>
</ul> </ul>
</div> </div>
@@ -143,6 +157,11 @@
<i class="bi bi-shuffle text-success me-2"></i> <i class="bi bi-shuffle text-success me-2"></i>
Pseudo-random embedding Pseudo-random embedding
</li> </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> </ul>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,13 @@
""" """
Stegasoo - Secure Steganography with Multi-Factor Authentication (v3.2.0) Stegasoo - Secure Steganography with Multi-Factor Authentication (v4.0.0)
Changes in v4.0.0:
- Added channel key support for deployment/group isolation
- New functions: get_channel_key, get_channel_fingerprint, generate_channel_key, etc.
- encode() and decode() now accept channel_key parameter
""" """
__version__ = "3.2.0" __version__ = "4.0.0"
# Core functionality # Core functionality
from .encode import encode from .encode import encode
@@ -28,7 +33,19 @@ from .image_utils import (
from .utils import generate_filename from .utils import generate_filename
# Crypto functions # Crypto functions
from .crypto import has_argon2 from .crypto import has_argon2, get_active_channel_key, get_channel_fingerprint
# Channel key management (v4.0.0)
from .channel import (
generate_channel_key,
get_channel_key,
set_channel_key,
clear_channel_key,
has_channel_key,
get_channel_status,
validate_channel_key,
format_channel_key,
)
# Steganography functions # Steganography functions
from .steganography import ( from .steganography import (
@@ -150,6 +167,18 @@ __all__ = [
"export_rsa_key_pem", "export_rsa_key_pem",
"load_rsa_key", "load_rsa_key",
# Channel key management (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",
# Image utilities # Image utilities
"get_image_info", "get_image_info",
"compare_capacity", "compare_capacity",
@@ -183,6 +212,7 @@ __all__ = [
"validate_embed_mode", "validate_embed_mode",
"validate_dct_output_format", "validate_dct_output_format",
"validate_dct_color_mode", "validate_dct_color_mode",
"validate_channel_key",
# Models # Models
"ImageInfo", "ImageInfo",

View File

@@ -1,5 +1,5 @@
""" """
Channel Key Management for Stegasoo (v3.2.0) Channel Key Management for Stegasoo (v4.0.0)
A channel key ties encode/decode operations to a specific deployment or group. A channel key ties encode/decode operations to a specific deployment or group.
Messages encoded with one channel key can only be decoded by systems with the Messages encoded with one channel key can only be decoded by systems with the
@@ -16,15 +16,12 @@ Storage priority:
2. Config file: ~/.stegasoo/channel.key or ./config/channel.key 2. Config file: ~/.stegasoo/channel.key or ./config/channel.key
3. None (public mode - compatible with any instance without a channel key) 3. None (public mode - compatible with any instance without a channel key)
STATUS: This module is IMPLEMENTED but NOT YET INTEGRATED into crypto.py. INTEGRATION STATUS (v4.0.0):
The get_channel_key_hash() function should be mixed into key derivation - ✅ get_channel_key_hash() integrated into derive_hybrid_key() in crypto.py
in a future release. - ✅ get_channel_key_hash() integrated into derive_pixel_key() in crypto.py
- ✅ channel_key parameter added to encode() and decode() functions
TODO (v3.3.0): - ✅ Header flags indicate whether message was encoded with channel key
- Integrate get_channel_key_hash() into derive_hybrid_key() in crypto.py - ✅ Helpful error messages for channel key mismatches
- Add --channel-key option to CLI
- Add channel key display to web UI
- Document channel key feature in README
""" """
import os import os
@@ -257,12 +254,9 @@ def get_channel_key_hash(key: Optional[str] = None) -> Optional[bytes]:
""" """
Get the channel key as a 32-byte hash suitable for key derivation. Get the channel key as a 32-byte hash suitable for key derivation.
This hash is designed to be mixed into the Argon2 key derivation to bind This hash is mixed into the Argon2 key derivation to bind
encryption to a specific channel. encryption to a specific channel.
NOTE: This function is implemented but not yet integrated into crypto.py.
See TODO at top of file for integration plan.
Args: Args:
key: Channel key (if None, reads from config) key: Channel key (if None, reads from config)

View File

@@ -1,14 +1,17 @@
""" """
Stegasoo Constants and Configuration (v3.2.0 - Date Independent) Stegasoo Constants and Configuration (v4.0.0 - Channel Key Support)
Central location for all magic numbers, limits, and crypto parameters. Central location for all magic numbers, limits, and crypto parameters.
All version numbers, limits, and configuration values should be defined here. All version numbers, limits, and configuration values should be defined here.
BREAKING CHANGES in v4.0.0:
- Added channel key support for deployment/group isolation
- FORMAT_VERSION bumped to 5 (adds flags byte to header)
- Header size increased by 1 byte for flags
BREAKING CHANGES in v3.2.0: BREAKING CHANGES in v3.2.0:
- Removed date dependency from cryptographic operations - Removed date dependency from cryptographic operations
- Renamed day_phrase → passphrase throughout codebase - Renamed day_phrase → passphrase throughout codebase
- FORMAT_VERSION bumped to 4 to indicate incompatibility
- Increased default passphrase length to compensate for removed date entropy
""" """
import os import os
@@ -28,8 +31,9 @@ MAGIC_HEADER = b'\x89ST3'
# FORMAT VERSION HISTORY: # FORMAT VERSION HISTORY:
# Version 1-3: Date-dependent encryption (v3.0.x - v3.1.x) # Version 1-3: Date-dependent encryption (v3.0.x - v3.1.x)
# Version 4: Date-independent encryption (v3.2.0+) - BREAKING CHANGE # Version 4: Date-independent encryption (v3.2.0)
FORMAT_VERSION = 4 # Version 5: Channel key support (v4.0.0) - adds flags byte to header
FORMAT_VERSION = 5
# Payload type markers # Payload type markers
PAYLOAD_TEXT = 0x01 PAYLOAD_TEXT = 0x01

View File

@@ -1,15 +1,18 @@
""" """
Stegasoo Cryptographic Functions (v3.2.0 - Date Independent) Stegasoo Cryptographic Functions (v4.0.0 - Channel Key Support)
Key derivation, encryption, and decryption using AES-256-GCM. Key derivation, encryption, and decryption using AES-256-GCM.
Supports both text messages and binary file payloads. Supports both text messages and binary file payloads.
BREAKING CHANGES in v4.0.0:
- Added channel key support for deployment/group isolation
- Messages encoded with a channel key require the same key to decode
- Channel key can be configured via environment, config file, or explicit parameter
- FORMAT_VERSION bumped to 5
BREAKING CHANGES in v3.2.0: BREAKING CHANGES in v3.2.0:
- Removed date dependency from key derivation - Removed date dependency from key derivation
- Renamed day_phrase → passphrase (no daily rotation needed) - Renamed day_phrase → passphrase (no daily rotation needed)
- Messages can now be decoded without knowing encoding date
- Enables true asynchronous communication
- NOT backward compatible with v3.1.0 and earlier
""" """
import io import io
@@ -46,6 +49,51 @@ except ImportError:
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
# =============================================================================
# CHANNEL KEY RESOLUTION
# =============================================================================
# Sentinel value for "use auto-detected channel key"
CHANNEL_KEY_AUTO = "auto"
def _resolve_channel_key(channel_key: Optional[Union[str, bool]]) -> Optional[bytes]:
"""
Resolve channel key parameter to actual key hash.
Args:
channel_key: Channel key parameter with these behaviors:
- None or "auto": Use server's configured key (from env/config)
- str (valid key): Use this specific key
- "" or False: Explicitly use NO channel key (public mode)
Returns:
32-byte channel key hash, or None for public mode
"""
# Explicit public mode
if channel_key == "" or channel_key is False:
return None
# Auto-detect from environment/config
if channel_key is None or channel_key == CHANNEL_KEY_AUTO:
from .channel import get_channel_key_hash
return get_channel_key_hash()
# Explicit key provided - validate and hash it
if isinstance(channel_key, str):
from .channel import format_channel_key, validate_channel_key
if not validate_channel_key(channel_key):
raise ValueError(f"Invalid channel key format: {channel_key}")
formatted = format_channel_key(channel_key)
return hashlib.sha256(formatted.encode('utf-8')).digest()
raise ValueError(f"Invalid channel_key type: {type(channel_key)}")
# =============================================================================
# CORE CRYPTO FUNCTIONS
# =============================================================================
def hash_photo(image_data: bytes) -> bytes: def hash_photo(image_data: bytes) -> bytes:
""" """
Compute deterministic hash of photo pixel content. Compute deterministic hash of photo pixel content.
@@ -73,7 +121,8 @@ def derive_hybrid_key(
passphrase: str, passphrase: str,
salt: bytes, salt: bytes,
pin: str = "", pin: str = "",
rsa_key_data: Optional[bytes] = None rsa_key_data: Optional[bytes] = None,
channel_key: Optional[Union[str, bool]] = None,
) -> bytes: ) -> bytes:
""" """
Derive encryption key from multiple factors. Derive encryption key from multiple factors.
@@ -83,19 +132,21 @@ def derive_hybrid_key(
- Passphrase (something you know) - Passphrase (something you know)
- PIN (something you know, static) - PIN (something you know, static)
- RSA key (something you have) - RSA key (something you have)
- Channel key (deployment/group binding)
- Salt (random per message) - Salt (random per message)
Uses Argon2id if available, falls back to PBKDF2. Uses Argon2id if available, falls back to PBKDF2.
NOTE: v3.2.0 removed date dependency and daily rotation.
Use a strong static passphrase instead (recommend 4+ words).
Args: Args:
photo_data: Reference photo bytes photo_data: Reference photo bytes
passphrase: Shared passphrase (recommend 4+ words) passphrase: Shared passphrase (recommend 4+ words)
salt: Random salt for this message salt: Random salt for this message
pin: Optional static PIN pin: Optional static PIN
rsa_key_data: Optional RSA key bytes rsa_key_data: Optional RSA key bytes
channel_key: Channel key parameter:
- None or "auto": Use configured key
- str: Use this specific key
- "" or False: No channel key (public mode)
Returns: Returns:
32-byte derived key 32-byte derived key
@@ -106,6 +157,10 @@ def derive_hybrid_key(
try: try:
photo_hash = hash_photo(photo_data) photo_hash = hash_photo(photo_data)
# Resolve channel key
channel_hash = _resolve_channel_key(channel_key)
# Build key material
key_material = ( key_material = (
photo_hash + photo_hash +
passphrase.lower().encode() + passphrase.lower().encode() +
@@ -117,6 +172,10 @@ def derive_hybrid_key(
if rsa_key_data: if rsa_key_data:
key_material += hashlib.sha256(rsa_key_data).digest() key_material += hashlib.sha256(rsa_key_data).digest()
# Add channel key hash if configured (v4.0.0)
if channel_hash:
key_material += channel_hash
if HAS_ARGON2: if HAS_ARGON2:
key = hash_secret_raw( key = hash_secret_raw(
secret=key_material, secret=key_material,
@@ -147,7 +206,8 @@ def derive_pixel_key(
photo_data: bytes, photo_data: bytes,
passphrase: str, passphrase: str,
pin: str = "", pin: str = "",
rsa_key_data: Optional[bytes] = None rsa_key_data: Optional[bytes] = None,
channel_key: Optional[Union[str, bool]] = None,
) -> bytes: ) -> bytes:
""" """
Derive key for pseudo-random pixel selection. Derive key for pseudo-random pixel selection.
@@ -155,19 +215,21 @@ def derive_pixel_key(
This key determines which pixels are used for embedding, This key determines which pixels are used for embedding,
making the message location unpredictable without the correct inputs. making the message location unpredictable without the correct inputs.
NOTE: v3.2.0 removed date dependency.
Args: Args:
photo_data: Reference photo bytes photo_data: Reference photo bytes
passphrase: Shared passphrase passphrase: Shared passphrase
pin: Optional static PIN pin: Optional static PIN
rsa_key_data: Optional RSA key bytes rsa_key_data: Optional RSA key bytes
channel_key: Channel key parameter (see derive_hybrid_key)
Returns: Returns:
32-byte key for pixel selection 32-byte key for pixel selection
""" """
photo_hash = hash_photo(photo_data) photo_hash = hash_photo(photo_data)
# Resolve channel key
channel_hash = _resolve_channel_key(channel_key)
material = ( material = (
photo_hash + photo_hash +
passphrase.lower().encode() + passphrase.lower().encode() +
@@ -177,6 +239,10 @@ def derive_pixel_key(
if rsa_key_data: if rsa_key_data:
material += hashlib.sha256(rsa_key_data).digest() material += hashlib.sha256(rsa_key_data).digest()
# Add channel key hash if configured (v4.0.0)
if channel_hash:
material += channel_hash
return hashlib.sha256(material + b"pixel_selection").digest() return hashlib.sha256(material + b"pixel_selection").digest()
@@ -284,19 +350,29 @@ def _unpack_payload(data: bytes) -> DecodeResult:
return DecodeResult(payload_type='file', file_data=data) return DecodeResult(payload_type='file', file_data=data)
# =============================================================================
# HEADER FLAGS (v4.0.0)
# =============================================================================
# Header flag bits
FLAG_CHANNEL_KEY = 0x01 # Set if encoded with a channel key
def encrypt_message( def encrypt_message(
message: Union[str, bytes, FilePayload], message: Union[str, bytes, FilePayload],
photo_data: bytes, photo_data: bytes,
passphrase: str, passphrase: str,
pin: str = "", pin: str = "",
rsa_key_data: Optional[bytes] = None rsa_key_data: Optional[bytes] = None,
channel_key: Optional[Union[str, bool]] = None,
) -> bytes: ) -> bytes:
""" """
Encrypt message or file using AES-256-GCM with hybrid key derivation. Encrypt message or file using AES-256-GCM with hybrid key derivation.
Message format (v3.2.0 - no date): Message format (v4.0.0 - with channel key support):
- Magic header (4 bytes) - Magic header (4 bytes)
- Version (1 byte) = 4 - Version (1 byte) = 5
- Flags (1 byte) - indicates if channel key was used
- Salt (32 bytes) - Salt (32 bytes)
- IV (12 bytes) - IV (12 bytes)
- Auth tag (16 bytes) - Auth tag (16 bytes)
@@ -308,6 +384,10 @@ def encrypt_message(
passphrase: Shared passphrase (recommend 4+ words for good entropy) passphrase: Shared passphrase (recommend 4+ words for good entropy)
pin: Optional static PIN pin: Optional static PIN
rsa_key_data: Optional RSA key bytes rsa_key_data: Optional RSA key bytes
channel_key: Channel key parameter:
- None or "auto": Use configured key
- str: Use this specific key
- "" or False: No channel key (public mode)
Returns: Returns:
Encrypted message bytes Encrypted message bytes
@@ -317,9 +397,15 @@ def encrypt_message(
""" """
try: try:
salt = secrets.token_bytes(SALT_SIZE) salt = secrets.token_bytes(SALT_SIZE)
key = derive_hybrid_key(photo_data, passphrase, salt, pin, rsa_key_data) key = derive_hybrid_key(photo_data, passphrase, salt, pin, rsa_key_data, channel_key)
iv = secrets.token_bytes(IV_SIZE) iv = secrets.token_bytes(IV_SIZE)
# Determine flags
flags = 0
channel_hash = _resolve_channel_key(channel_key)
if channel_hash:
flags |= FLAG_CHANNEL_KEY
# Pack payload with type marker # Pack payload with type marker
packed_payload, _ = _pack_payload(message) packed_payload, _ = _pack_payload(message)
@@ -330,16 +416,18 @@ def encrypt_message(
padding = secrets.token_bytes(padding_needed - 4) + struct.pack('>I', len(packed_payload)) padding = secrets.token_bytes(padding_needed - 4) + struct.pack('>I', len(packed_payload))
padded_message = packed_payload + padding padded_message = packed_payload + padding
# Build header for AAD
header = MAGIC_HEADER + bytes([FORMAT_VERSION, flags])
# Encrypt with AES-256-GCM # Encrypt with AES-256-GCM
cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend()) cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
encryptor = cipher.encryptor() encryptor = cipher.encryptor()
encryptor.authenticate_additional_data(MAGIC_HEADER + bytes([FORMAT_VERSION])) encryptor.authenticate_additional_data(header)
ciphertext = encryptor.update(padded_message) + encryptor.finalize() ciphertext = encryptor.update(padded_message) + encryptor.finalize()
# v3.2.0: Simplified header without date # v4.0.0: Header with flags byte
return ( return (
MAGIC_HEADER + header +
bytes([FORMAT_VERSION]) +
salt + salt +
iv + iv +
encryptor.tag + encryptor.tag +
@@ -354,16 +442,16 @@ def parse_header(encrypted_data: bytes) -> Optional[dict]:
""" """
Parse the header from encrypted data. Parse the header from encrypted data.
v3.2.0: No date field in header. v4.0.0: Includes flags byte for channel key indicator.
Args: Args:
encrypted_data: Raw encrypted bytes encrypted_data: Raw encrypted bytes
Returns: Returns:
Dict with salt, iv, tag, ciphertext or None if invalid Dict with salt, iv, tag, ciphertext, flags or None if invalid
""" """
# Min size: Magic(4) + Version(1) + Salt(32) + IV(12) + Tag(16) = 65 bytes # Min size: Magic(4) + Version(1) + Flags(1) + Salt(32) + IV(12) + Tag(16) = 66 bytes
if len(encrypted_data) < 65 or encrypted_data[:4] != MAGIC_HEADER: if len(encrypted_data) < 66 or encrypted_data[:4] != MAGIC_HEADER:
return None return None
try: try:
@@ -371,7 +459,9 @@ def parse_header(encrypted_data: bytes) -> Optional[dict]:
if version != FORMAT_VERSION: if version != FORMAT_VERSION:
return None return None
offset = 5 flags = encrypted_data[5]
offset = 6
salt = encrypted_data[offset:offset + SALT_SIZE] salt = encrypted_data[offset:offset + SALT_SIZE]
offset += SALT_SIZE offset += SALT_SIZE
iv = encrypted_data[offset:offset + IV_SIZE] iv = encrypted_data[offset:offset + IV_SIZE]
@@ -381,6 +471,9 @@ def parse_header(encrypted_data: bytes) -> Optional[dict]:
ciphertext = encrypted_data[offset:] ciphertext = encrypted_data[offset:]
return { return {
'version': version,
'flags': flags,
'has_channel_key': bool(flags & FLAG_CHANNEL_KEY),
'salt': salt, 'salt': salt,
'iv': iv, 'iv': iv,
'tag': tag, 'tag': tag,
@@ -395,10 +488,11 @@ def decrypt_message(
photo_data: bytes, photo_data: bytes,
passphrase: str, passphrase: str,
pin: str = "", pin: str = "",
rsa_key_data: Optional[bytes] = None rsa_key_data: Optional[bytes] = None,
channel_key: Optional[Union[str, bool]] = None,
) -> DecodeResult: ) -> DecodeResult:
""" """
Decrypt message (v3.2.0 - no date needed). Decrypt message (v4.0.0 - with channel key support).
Args: Args:
encrypted_data: Encrypted message bytes encrypted_data: Encrypted message bytes
@@ -406,6 +500,7 @@ def decrypt_message(
passphrase: Shared passphrase passphrase: Shared passphrase
pin: Optional static PIN pin: Optional static PIN
rsa_key_data: Optional RSA key bytes rsa_key_data: Optional RSA key bytes
channel_key: Channel key parameter (see encrypt_message)
Returns: Returns:
DecodeResult with decrypted content DecodeResult with decrypted content
@@ -418,18 +513,26 @@ def decrypt_message(
if not header: if not header:
raise InvalidHeaderError("Invalid or missing Stegasoo header") raise InvalidHeaderError("Invalid or missing Stegasoo header")
# Check for channel key mismatch and provide helpful error
channel_hash = _resolve_channel_key(channel_key)
has_configured_key = channel_hash is not None
message_has_key = header['has_channel_key']
try: try:
key = derive_hybrid_key( key = derive_hybrid_key(
photo_data, passphrase, header['salt'], pin, rsa_key_data photo_data, passphrase, header['salt'], pin, rsa_key_data, channel_key
) )
# Reconstruct header for AAD verification
aad_header = MAGIC_HEADER + bytes([FORMAT_VERSION, header['flags']])
cipher = Cipher( cipher = Cipher(
algorithms.AES(key), algorithms.AES(key),
modes.GCM(header['iv'], header['tag']), modes.GCM(header['iv'], header['tag']),
backend=default_backend() backend=default_backend()
) )
decryptor = cipher.decryptor() decryptor = cipher.decryptor()
decryptor.authenticate_additional_data(MAGIC_HEADER + bytes([FORMAT_VERSION])) decryptor.authenticate_additional_data(aad_header)
padded_plaintext = decryptor.update(header['ciphertext']) + decryptor.finalize() padded_plaintext = decryptor.update(header['ciphertext']) + decryptor.finalize()
original_length = struct.unpack('>I', padded_plaintext[-4:])[0] original_length = struct.unpack('>I', padded_plaintext[-4:])[0]
@@ -437,14 +540,25 @@ def decrypt_message(
payload_data = padded_plaintext[:original_length] payload_data = padded_plaintext[:original_length]
result = _unpack_payload(payload_data) result = _unpack_payload(payload_data)
# Note: No date_encoded field in v3.2.0
return result return result
except Exception as e: except Exception as e:
raise DecryptionError( # Provide more helpful error message for channel key issues
"Decryption failed. Check your passphrase, PIN, RSA key, and reference photo." if message_has_key and not has_configured_key:
) from e raise DecryptionError(
"Decryption failed. This message was encoded with a channel key, "
"but no channel key is configured. Provide the correct channel key."
) from e
elif not message_has_key and has_configured_key:
raise DecryptionError(
"Decryption failed. This message was encoded without a channel key, "
"but you have one configured. Try with channel_key='' for public mode."
) from e
else:
raise DecryptionError(
"Decryption failed. Check your passphrase, PIN, RSA key, "
"reference photo, and channel key."
) from e
def decrypt_message_text( def decrypt_message_text(
@@ -452,7 +566,8 @@ def decrypt_message_text(
photo_data: bytes, photo_data: bytes,
passphrase: str, passphrase: str,
pin: str = "", pin: str = "",
rsa_key_data: Optional[bytes] = None rsa_key_data: Optional[bytes] = None,
channel_key: Optional[Union[str, bool]] = None,
) -> str: ) -> str:
""" """
Decrypt message and return as text string. Decrypt message and return as text string.
@@ -465,6 +580,7 @@ def decrypt_message_text(
passphrase: Shared passphrase passphrase: Shared passphrase
pin: Optional static PIN pin: Optional static PIN
rsa_key_data: Optional RSA key bytes rsa_key_data: Optional RSA key bytes
channel_key: Channel key parameter
Returns: Returns:
Decrypted message string Decrypted message string
@@ -472,7 +588,7 @@ def decrypt_message_text(
Raises: Raises:
DecryptionError: If decryption fails or content is a file DecryptionError: If decryption fails or content is a file
""" """
result = decrypt_message(encrypted_data, photo_data, passphrase, pin, rsa_key_data) result = decrypt_message(encrypted_data, photo_data, passphrase, pin, rsa_key_data, channel_key)
if result.is_file: if result.is_file:
if result.file_data: if result.file_data:
@@ -491,3 +607,29 @@ def decrypt_message_text(
def has_argon2() -> bool: def has_argon2() -> bool:
"""Check if Argon2 is available.""" """Check if Argon2 is available."""
return HAS_ARGON2 return HAS_ARGON2
# =============================================================================
# CHANNEL KEY UTILITIES (exposed for convenience)
# =============================================================================
def get_active_channel_key() -> Optional[str]:
"""
Get the currently configured channel key (if any).
Returns:
Formatted channel key string, or None if not configured
"""
from .channel import get_channel_key
return get_channel_key()
def get_channel_fingerprint() -> Optional[str]:
"""
Get a display-safe fingerprint of the configured channel key.
Returns:
Masked key like "ABCD-••••-••••-••••-••••-••••-••••-3456" or None
"""
from .channel import get_channel_fingerprint as _get_fingerprint
return _get_fingerprint()

View File

@@ -98,8 +98,8 @@ def memory_usage() -> Dict[str, Union[float, str]]:
mem_info = process.memory_info() mem_info = process.memory_info()
return { return {
'rss_mb': mem_info.rss / 1024 / 1024, # Resident Set Size 'rss_mb': mem_info.rss / 1024 / 1024,
'vms_mb': mem_info.vms / 1024 / 1024, # Virtual Memory Size 'vms_mb': mem_info.vms / 1024 / 1024,
'percent': process.memory_percent(), 'percent': process.memory_percent(),
} }
except ImportError: except ImportError:
@@ -117,7 +117,7 @@ def hexdump(data: bytes, offset: int = 0, length: int = 64) -> str:
for i in range(0, len(data_to_dump), 16): for i in range(0, len(data_to_dump), 16):
chunk = data_to_dump[i:i+16] chunk = data_to_dump[i:i+16]
hex_str = ' '.join(f'{b:02x}' for b in chunk) hex_str = ' '.join(f'{b:02x}' for b in chunk)
hex_str = hex_str.ljust(47) # Pad to consistent width hex_str = hex_str.ljust(47)
ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk) ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
result.append(f"{offset + i:08x}: {hex_str} {ascii_str}") result.append(f"{offset + i:08x}: {hex_str} {ascii_str}")
@@ -127,7 +127,6 @@ def hexdump(data: bytes, offset: int = 0, length: int = 64) -> str:
return '\n'.join(result) return '\n'.join(result)
# Create singleton instance for easy import
class Debug: class Debug:
"""Debugging utility class.""" """Debugging utility class."""

View File

@@ -1,10 +1,14 @@
""" """
Stegasoo Decode Module (v3.2.0) Stegasoo Decode Module (v4.0.0)
High-level decoding functions for extracting messages and files from images. High-level decoding functions for extracting messages and files from images.
Changes in v4.0.0:
- Added channel_key parameter for deployment/group isolation
- Improved error messages for channel key mismatches
""" """
from typing import Optional from typing import Optional, Union
from pathlib import Path from pathlib import Path
from .models import DecodeInput, DecodeResult from .models import DecodeInput, DecodeResult
@@ -29,6 +33,7 @@ def decode(
rsa_key_data: Optional[bytes] = None, rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None, rsa_password: Optional[str] = None,
embed_mode: str = EMBED_MODE_AUTO, embed_mode: str = EMBED_MODE_AUTO,
channel_key: Optional[Union[str, bool]] = None,
) -> DecodeResult: ) -> DecodeResult:
""" """
Decode a message or file from a stego image. Decode a message or file from a stego image.
@@ -41,6 +46,10 @@ def decode(
rsa_key_data: Optional RSA key bytes (if used during encoding) rsa_key_data: Optional RSA key bytes (if used during encoding)
rsa_password: Optional RSA key password rsa_password: Optional RSA key password
embed_mode: 'auto' (default), 'lsb', or 'dct' embed_mode: 'auto' (default), 'lsb', or 'dct'
channel_key: Channel key for deployment/group isolation:
- None or "auto": Use server's configured key
- str: Use this specific channel key
- "" or False: No channel key (public mode)
Returns: Returns:
DecodeResult with message or file data DecodeResult with message or file data
@@ -57,9 +66,19 @@ def decode(
... else: ... else:
... with open(result.filename, 'wb') as f: ... with open(result.filename, 'wb') as f:
... f.write(result.file_data) ... f.write(result.file_data)
Example with explicit channel key:
>>> result = decode(
... stego_image=stego_bytes,
... reference_photo=ref_bytes,
... passphrase="apple forest thunder mountain",
... pin="123456",
... channel_key="ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456"
... )
""" """
debug.print(f"decode: passphrase length={len(passphrase.split())} words, " debug.print(f"decode: passphrase length={len(passphrase.split())} words, "
f"mode={embed_mode}") f"mode={embed_mode}, "
f"channel_key={'explicit' if isinstance(channel_key, str) and channel_key else 'auto' if channel_key is None else 'none'}")
# Validate inputs # Validate inputs
require_valid_image(stego_image, "Stego image") require_valid_image(stego_image, "Stego image")
@@ -71,10 +90,10 @@ def decode(
if rsa_key_data: if rsa_key_data:
require_valid_rsa_key(rsa_key_data, rsa_password) require_valid_rsa_key(rsa_key_data, rsa_password)
# Derive pixel/coefficient selection key # Derive pixel/coefficient selection key (with channel key)
from .crypto import derive_pixel_key from .crypto import derive_pixel_key
pixel_key = derive_pixel_key( pixel_key = derive_pixel_key(
reference_photo, passphrase, pin, rsa_key_data reference_photo, passphrase, pin, rsa_key_data, channel_key
) )
# Extract encrypted data # Extract encrypted data
@@ -90,9 +109,9 @@ def decode(
debug.print(f"Extracted {len(encrypted)} bytes from image") debug.print(f"Extracted {len(encrypted)} bytes from image")
# Decrypt # Decrypt (with channel key)
result = decrypt_message( result = decrypt_message(
encrypted, reference_photo, passphrase, pin, rsa_key_data encrypted, reference_photo, passphrase, pin, rsa_key_data, channel_key
) )
debug.print(f"Decryption successful: {result.payload_type}") debug.print(f"Decryption successful: {result.payload_type}")
@@ -108,6 +127,7 @@ def decode_file(
rsa_key_data: Optional[bytes] = None, rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None, rsa_password: Optional[str] = None,
embed_mode: str = EMBED_MODE_AUTO, embed_mode: str = EMBED_MODE_AUTO,
channel_key: Optional[Union[str, bool]] = None,
) -> Path: ) -> Path:
""" """
Decode a file from a stego image and save it. Decode a file from a stego image and save it.
@@ -121,6 +141,7 @@ def decode_file(
rsa_key_data: Optional RSA key bytes rsa_key_data: Optional RSA key bytes
rsa_password: Optional RSA key password rsa_password: Optional RSA key password
embed_mode: 'auto', 'lsb', or 'dct' embed_mode: 'auto', 'lsb', or 'dct'
channel_key: Channel key parameter (see decode())
Returns: Returns:
Path where file was saved Path where file was saved
@@ -136,6 +157,7 @@ def decode_file(
rsa_key_data, rsa_key_data,
rsa_password, rsa_password,
embed_mode, embed_mode,
channel_key,
) )
if not result.is_file: if not result.is_file:
@@ -163,6 +185,7 @@ def decode_text(
rsa_key_data: Optional[bytes] = None, rsa_key_data: Optional[bytes] = None,
rsa_password: Optional[str] = None, rsa_password: Optional[str] = None,
embed_mode: str = EMBED_MODE_AUTO, embed_mode: str = EMBED_MODE_AUTO,
channel_key: Optional[Union[str, bool]] = None,
) -> str: ) -> str:
""" """
Decode a text message from a stego image. Decode a text message from a stego image.
@@ -177,6 +200,7 @@ def decode_text(
rsa_key_data: Optional RSA key bytes rsa_key_data: Optional RSA key bytes
rsa_password: Optional RSA key password rsa_password: Optional RSA key password
embed_mode: 'auto', 'lsb', or 'dct' embed_mode: 'auto', 'lsb', or 'dct'
channel_key: Channel key parameter (see decode())
Returns: Returns:
Decoded message string Decoded message string
@@ -192,6 +216,7 @@ def decode_text(
rsa_key_data, rsa_key_data,
rsa_password, rsa_password,
embed_mode, embed_mode,
channel_key,
) )
if result.is_file: if result.is_file:

View File

@@ -1,7 +1,10 @@
""" """
Stegasoo Encode Module (v3.2.0) Stegasoo Encode Module (v4.0.0)
High-level encoding functions for hiding messages and files in images. High-level encoding functions for hiding messages and files in images.
Changes in v4.0.0:
- Added channel_key parameter for deployment/group isolation
""" """
from typing import Optional, Union from typing import Optional, Union
@@ -34,6 +37,7 @@ def encode(
embed_mode: str = EMBED_MODE_LSB, embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = "png", dct_output_format: str = "png",
dct_color_mode: str = "grayscale", dct_color_mode: str = "grayscale",
channel_key: Optional[Union[str, bool]] = None,
) -> EncodeResult: ) -> EncodeResult:
""" """
Encode a message or file into an image. Encode a message or file into an image.
@@ -50,6 +54,10 @@ def encode(
embed_mode: 'lsb' (default) or 'dct' embed_mode: 'lsb' (default) or 'dct'
dct_output_format: For DCT mode - 'png' or 'jpeg' dct_output_format: For DCT mode - 'png' or 'jpeg'
dct_color_mode: For DCT mode - 'grayscale' or 'color' dct_color_mode: For DCT mode - 'grayscale' or 'color'
channel_key: Channel key for deployment/group isolation:
- None or "auto": Use server's configured key
- str: Use this specific channel key
- "" or False: No channel key (public mode)
Returns: Returns:
EncodeResult with stego image and metadata EncodeResult with stego image and metadata
@@ -64,9 +72,20 @@ def encode(
... ) ... )
>>> with open('stego.png', 'wb') as f: >>> with open('stego.png', 'wb') as f:
... f.write(result.stego_image) ... f.write(result.stego_image)
Example with explicit channel key:
>>> result = encode(
... message="Secret message",
... reference_photo=ref_bytes,
... carrier_image=carrier_bytes,
... passphrase="apple forest thunder mountain",
... pin="123456",
... channel_key="ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456"
... )
""" """
debug.print(f"encode: passphrase length={len(passphrase.split())} words, " debug.print(f"encode: passphrase length={len(passphrase.split())} words, "
f"pin={'set' if pin else 'none'}, mode={embed_mode}") f"pin={'set' if pin else 'none'}, mode={embed_mode}, "
f"channel_key={'explicit' if isinstance(channel_key, str) and channel_key else 'auto' if channel_key is None else 'none'}")
# Validate inputs # Validate inputs
require_valid_payload(message) require_valid_payload(message)
@@ -79,16 +98,16 @@ def encode(
if rsa_key_data: if rsa_key_data:
require_valid_rsa_key(rsa_key_data, rsa_password) require_valid_rsa_key(rsa_key_data, rsa_password)
# Encrypt message # Encrypt message (with channel key)
encrypted = encrypt_message( encrypted = encrypt_message(
message, reference_photo, passphrase, pin, rsa_key_data message, reference_photo, passphrase, pin, rsa_key_data, channel_key
) )
debug.print(f"Encrypted payload: {len(encrypted)} bytes") debug.print(f"Encrypted payload: {len(encrypted)} bytes")
# Derive pixel/coefficient selection key # Derive pixel/coefficient selection key (with channel key)
pixel_key = derive_pixel_key( pixel_key = derive_pixel_key(
reference_photo, passphrase, pin, rsa_key_data reference_photo, passphrase, pin, rsa_key_data, channel_key
) )
# Embed in image # Embed in image
@@ -114,7 +133,7 @@ def encode(
pixels_modified=stats.pixels_modified, pixels_modified=stats.pixels_modified,
total_pixels=stats.total_pixels, total_pixels=stats.total_pixels,
capacity_used=stats.capacity_used, capacity_used=stats.capacity_used,
date_used=None, # No longer used in v3.2.0 date_used=None, # No longer used in v3.2.0+
) )
else: else:
# DCT mode stats # DCT mode stats
@@ -141,6 +160,7 @@ def encode_file(
embed_mode: str = EMBED_MODE_LSB, embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = "png", dct_output_format: str = "png",
dct_color_mode: str = "grayscale", dct_color_mode: str = "grayscale",
channel_key: Optional[Union[str, bool]] = None,
) -> EncodeResult: ) -> EncodeResult:
""" """
Encode a file into an image. Encode a file into an image.
@@ -160,6 +180,7 @@ def encode_file(
embed_mode: 'lsb' or 'dct' embed_mode: 'lsb' or 'dct'
dct_output_format: 'png' or 'jpeg' dct_output_format: 'png' or 'jpeg'
dct_color_mode: 'grayscale' or 'color' dct_color_mode: 'grayscale' or 'color'
channel_key: Channel key parameter (see encode())
Returns: Returns:
EncodeResult EncodeResult
@@ -178,6 +199,7 @@ def encode_file(
embed_mode=embed_mode, embed_mode=embed_mode,
dct_output_format=dct_output_format, dct_output_format=dct_output_format,
dct_color_mode=dct_color_mode, dct_color_mode=dct_color_mode,
channel_key=channel_key,
) )
@@ -195,6 +217,7 @@ def encode_bytes(
embed_mode: str = EMBED_MODE_LSB, embed_mode: str = EMBED_MODE_LSB,
dct_output_format: str = "png", dct_output_format: str = "png",
dct_color_mode: str = "grayscale", dct_color_mode: str = "grayscale",
channel_key: Optional[Union[str, bool]] = None,
) -> EncodeResult: ) -> EncodeResult:
""" """
Encode raw bytes with metadata into an image. Encode raw bytes with metadata into an image.
@@ -213,6 +236,7 @@ def encode_bytes(
embed_mode: 'lsb' or 'dct' embed_mode: 'lsb' or 'dct'
dct_output_format: 'png' or 'jpeg' dct_output_format: 'png' or 'jpeg'
dct_color_mode: 'grayscale' or 'color' dct_color_mode: 'grayscale' or 'color'
channel_key: Channel key parameter (see encode())
Returns: Returns:
EncodeResult EncodeResult
@@ -231,4 +255,5 @@ def encode_bytes(
embed_mode=embed_mode, embed_mode=embed_mode,
dct_output_format=dct_output_format, dct_output_format=dct_output_format,
dct_color_mode=dct_color_mode, dct_color_mode=dct_color_mode,
channel_key=channel_key,
) )