More snazzy 4.0 Web UI improvements.
This commit is contained in:
59
README.md
59
README.md
@@ -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" \
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
1198
frontends/API.md
1198
frontends/API.md
File diff suppressed because it is too large
Load Diff
1143
frontends/CLI.md
1143
frontends/CLI.md
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
|||||||
# Stegasoo Web UI Documentation (v3.2.0)
|
# Stegasoo Web UI Documentation (v3.3.0)
|
||||||
|
|
||||||
Complete guide for the Stegasoo web-based steganography interface.
|
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) │
|
||||||
│ │
|
│ │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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={
|
||||||
|
"v4_channel_key": "Messages encoded with channel key require same key to decode",
|
||||||
|
"format_version": 5,
|
||||||
|
"backward_compatible": False,
|
||||||
|
"v3_notes": {
|
||||||
"date_removed": "No date_str parameter needed - encode/decode anytime",
|
"date_removed": "No date_str parameter needed - encode/decode anytime",
|
||||||
"passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)",
|
"passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)",
|
||||||
"format_version": 4,
|
}
|
||||||
"backward_compatible": False,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
# Get channel info for headers
|
||||||
content=result.stego_image,
|
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
|
||||||
media_type=mime_type,
|
|
||||||
headers={
|
headers = {
|
||||||
"Content-Disposition": f"attachment; filename={result.filename}",
|
"Content-Disposition": f"attachment; filename={result.filename}",
|
||||||
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
|
"X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}",
|
||||||
"X-Stegasoo-Embed-Mode": embed_mode,
|
"X-Stegasoo-Embed-Mode": embed_mode,
|
||||||
"X-Stegasoo-Output-Format": output_format,
|
"X-Stegasoo-Output-Format": output_format,
|
||||||
"X-Stegasoo-Color-Mode": color_mode,
|
"X-Stegasoo-Color-Mode": color_mode,
|
||||||
|
"X-Stegasoo-Channel-Mode": channel_mode,
|
||||||
"X-Stegasoo-Version": __version__,
|
"X-Stegasoo-Version": __version__,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if channel_fingerprint:
|
||||||
|
headers["X-Stegasoo-Channel-Fingerprint"] = channel_fingerprint
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
content=result.stego_image,
|
||||||
|
media_type=mime_type,
|
||||||
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -432,7 +775,17 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
|
|||||||
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)")
|
||||||
|
|
||||||
# v3.2.0: No date_str parameter
|
# 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)
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
if 'lsb_capacity' in img_info:
|
||||||
click.echo()
|
click.echo()
|
||||||
|
click.secho(" Capacity Estimates:", fg='green')
|
||||||
click.secho(" Capacity:", bold=True)
|
click.echo(f" LSB mode: {img_info['lsb_capacity']:,} bytes")
|
||||||
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
|
if 'dct_capacity' in img_info:
|
||||||
|
click.echo(f" DCT mode: {img_info['dct_capacity']:,} bytes")
|
||||||
dct_status = "✓" if comparison['dct']['available'] else "✗ (scipy not installed)"
|
|
||||||
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}")
|
|
||||||
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB")
|
|
||||||
|
|
||||||
if comparison['dct']['available']:
|
|
||||||
click.secho(" DCT options: grayscale/color, png/jpeg", dim=True)
|
|
||||||
|
|
||||||
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
1073
frontends/cli/main.py_old
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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')
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}'}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -412,6 +443,44 @@ class SubprocessStego:
|
|||||||
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
|
||||||
_default_stego: Optional[SubprocessStego] = None
|
_default_stego: Optional[SubprocessStego] = None
|
||||||
|
|||||||
@@ -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 & File Embedding</strong>
|
<strong>Text & File Embedding</strong>
|
||||||
<br><small class="text-muted">Hide messages or any file type (PDF, ZIP, documents)</small>
|
<br><small class="text-muted">Any file type: PDF, ZIP, documents</small>
|
||||||
</li>
|
</li>
|
||||||
<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 & DCT Modes</strong>
|
<strong>DCT & 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 & Specifications</h5>
|
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits & 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 }} •
|
|
||||||
<i class="bi bi-github me-1"></i>Open Source •
|
|
||||||
Built with Python, Flask, and cryptography
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -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;">
|
||||||
@@ -275,8 +276,10 @@
|
|||||||
</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>
|
||||||
@@ -335,6 +338,61 @@
|
|||||||
</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>
|
|
||||||
<strong>Auto</strong>
|
|
||||||
<div class="small text-muted">Try both</div>
|
|
||||||
</label>
|
</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>
|
|
||||||
<strong>LSB</strong>
|
|
||||||
<div class="small text-muted">Spatial only</div>
|
|
||||||
</label>
|
</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>
|
|
||||||
<strong>DCT</strong>
|
|
||||||
<div class="small text-muted">
|
|
||||||
{% if has_dct %}Frequency only{% else %}N/A{% endif %}
|
|
||||||
</div>
|
|
||||||
</label>
|
</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 %}
|
||||||
|
|||||||
@@ -333,6 +333,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;">
|
||||||
@@ -342,8 +343,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-text">Static 6-9 digit PIN</div>
|
<div class="form-text">Static 6-9 digit PIN</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>
|
||||||
@@ -402,6 +405,67 @@
|
|||||||
</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">
|
||||||
@@ -411,72 +475,45 @@
|
|||||||
</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">
|
|
||||||
<i class="bi bi-palette-fill text-success fs-5 d-block"></i>
|
|
||||||
<strong>Color</strong>
|
|
||||||
<div class="small text-muted">Recommended</div>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<label class="mode-btn equal-width" id="dctGrayscaleCard" for="dctColorGrayscale">
|
||||||
</div>
|
<input class="form-check-input" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
|
||||||
<div class="col-6">
|
<i class="bi bi-circle-half text-secondary"></i>
|
||||||
<div class="form-check card p-2 text-center" id="dctGrayscaleCard">
|
<span class="ms-2"><strong>Grayscale</strong></span>
|
||||||
<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>
|
</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">
|
|
||||||
<i class="bi bi-file-earmark-image text-primary fs-5 d-block"></i>
|
|
||||||
<strong>PNG</strong>
|
|
||||||
<div class="small text-muted">Lossless, larger</div>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<label class="mode-btn equal-width" id="dctPngCard" for="dctFormatPng">
|
||||||
</div>
|
<input class="form-check-input" type="radio" name="dct_output_format" id="dctFormatPng" value="png">
|
||||||
<div class="col-6">
|
<i class="bi bi-file-earmark-image text-primary"></i>
|
||||||
<div class="form-check card p-2 text-center border-warning border-2" id="dctJpegCard">
|
<span class="ms-2"><strong>PNG</strong> <span class="text-muted d-none d-sm-inline">· Lossless</span></span>
|
||||||
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 & 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 & 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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,13 +540,24 @@ 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:
|
||||||
|
# Provide more helpful error message for channel key issues
|
||||||
|
if message_has_key and not has_configured_key:
|
||||||
raise DecryptionError(
|
raise DecryptionError(
|
||||||
"Decryption failed. Check your passphrase, PIN, RSA key, and reference photo."
|
"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
|
) from e
|
||||||
|
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user