diff --git a/README.md b/README.md index 3dc5ffa..89f98ba 100644 --- a/README.md +++ b/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 | |------|------------------|----------------|----------| -| **LSB** (default) | ~770 KB | ❌ No | Email, file transfer | -| **DCT** | ~65 KB | ✅ Yes | Social media, messaging apps | +| **DCT** (default) | ~150 KB | ✅ Yes | Social media, messaging apps | +| **LSB** | ~750 KB | ❌ No | Email, file transfer | ## WebUI Preview @@ -59,24 +59,22 @@ pip install -e ".[all]" # Generate credentials (memorize these!) stegasoo generate --pin --words 4 -# Encode a message (LSB mode - default) -stegasoo encode \ - --ref photo.jpg \ - --carrier meme.png \ - --passphrase "apple forest thunder mountain" \ - --pin 123456 \ - --message "Secret message" - -# Encode for social media (DCT mode) +# Encode a message (DCT mode - default, best for social media) stegasoo encode \ --ref photo.jpg \ --carrier meme.jpg \ --passphrase "apple forest thunder mountain" \ --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" \ - --mode dct \ - --dct-format jpeg \ - --dct-color color + --mode lsb # Decode (auto-detects mode) stegasoo decode \ @@ -156,12 +154,15 @@ Full-featured CLI with piping support: # Generate with RSA key stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "password" -# Encode from file -stegasoo encode -r ref.jpg -c carrier.png -p "passphrase words here" --pin 123456 -f secret.txt +# Encode (DCT mode is now default) +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) -stegasoo encode -r ref.jpg -c carrier.jpg -p "passphrase words here" --pin 123456 \ - -m "Message" --mode dct --dct-format jpeg --dct-color color +# Encode with LSB mode for higher capacity +stegasoo encode -r ref.jpg -c carrier.png -p "passphrase words here" --pin 123456 \ + -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) stegasoo decode -r ref.jpg -s stego.png -p "passphrase words here" --pin 123456 -q @@ -187,12 +188,13 @@ python app.py ``` Features: -- Drag-and-drop image uploads +- Drag-and-drop image uploads with scan animations - Real-time entropy calculator - Native mobile sharing (Web Share API) -- DCT mode with advanced options panel +- DCT mode default with compact mode selector - Subprocess isolation for stability - Large image support (14MB+ tested) +- Streamlined form flow (v3.3.0) 📖 Full documentation: **[WEB_UI.md](WEB_UI.md)** @@ -215,18 +217,25 @@ curl -X POST http://localhost:8000/generate \ -H "Content-Type: application/json" \ -d '{"use_pin": true, "passphrase_words": 4}' -# Encode with DCT mode +# Encode (DCT mode is default) curl -X POST http://localhost:8000/encode/multipart \ -F "message=Secret" \ -F "passphrase=apple forest thunder mountain" \ -F "pin=123456" \ - -F "embed_mode=dct" \ - -F "dct_output_format=jpeg" \ - -F "dct_color_mode=color" \ -F "reference_photo=@photo.jpg" \ -F "carrier=@meme.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) curl -X POST http://localhost:8000/decode/multipart \ -F "passphrase=apple forest thunder mountain" \ diff --git a/docker-compose.yml b/docker-compose.yml index eca2961..cb4e500 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,9 @@ version: '3.8' +# Shared environment variables +x-common-env: &common-env + STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-} + services: # ============================================================================ # Web UI (Flask) @@ -12,12 +16,13 @@ services: ports: - "5000:5000" environment: - - FLASK_ENV=production + <<: *common-env + FLASK_ENV: production restart: unless-stopped deploy: resources: limits: - memory: 768M # Increased for scipy + Argon2 + memory: 768M reservations: memory: 384M @@ -31,32 +36,12 @@ services: container_name: stegasoo-api ports: - "8000:8000" + environment: + <<: *common-env restart: unless-stopped deploy: resources: limits: - memory: 768M # Increased for scipy + Argon2 + memory: 768M reservations: 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 diff --git a/frontends/API.md b/frontends/API.md index 6b34cc5..5cbf50c 100644 --- a/frontends/API.md +++ b/frontends/API.md @@ -1,17 +1,20 @@ -# Stegasoo REST API Documentation (v3.2.0) +# Stegasoo REST API Documentation (v4.0.0) Complete REST API reference for Stegasoo steganography operations. ## Table of Contents - [Overview](#overview) -- [What's New in v3.2.0](#whats-new-in-v320) +- [What's New in v4.0.0](#whats-new-in-v400) - [Installation](#installation) -- [Authentication](#authentication) - [Base URL](#base-url) - [Endpoints](#endpoints) - [GET /](#get--status) - [GET /modes](#get-modes) + - [GET /channel/status](#get-channelstatus) + - [POST /channel/generate](#post-channelgenerate) + - [POST /channel/set](#post-channelset) + - [DELETE /channel](#delete-channel) - [POST /generate](#post-generate) - [POST /encode](#post-encode-json) - [POST /encode/file](#post-encodefile) @@ -21,13 +24,10 @@ Complete REST API reference for Stegasoo steganography operations. - [POST /compare](#post-compare) - [POST /will-fit](#post-will-fit) - [POST /image/info](#post-imageinfo) - - [POST /extract-key-from-qr](#post-extract-key-from-qr) -- [Embedding Modes](#embedding-modes) +- [Channel Keys](#channel-keys) - [Data Models](#data-models) - [Error Handling](#error-handling) - [Code Examples](#code-examples) -- [Rate Limiting](#rate-limiting) -- [Security Considerations](#security-considerations) --- @@ -38,30 +38,30 @@ The Stegasoo REST API provides programmatic access to all steganography operatio - **Generate** credentials (passphrase, PINs, RSA keys) - **Encode** messages or files into images (LSB or DCT mode) - **Decode** messages or files from images (auto-detects mode) +- **Channel keys** for deployment/group isolation (v4.0.0) - **Analyze** image capacity and compare modes The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads). --- -## What's New in v3.2.0 +## What's New in v4.0.0 -Version 3.2.0 introduces breaking changes to simplify the API: +Version 4.0.0 adds **channel key** support for deployment/group isolation: -| Change | Before (v3.1) | After (v3.2.0) | -|--------|---------------|----------------| -| Passphrase | Daily rotation (`phrases` dict) | Single `passphrase` string | -| Date parameter | `date_str` required/optional | Removed entirely | -| Field name | `day_phrase` | `passphrase` | -| Default words | 3 words | 4 words | +| Feature | Description | +|---------|-------------| +| Channel keys | 256-bit keys that isolate message groups | +| New endpoints | `/channel/status`, `/channel/generate`, `/channel/set`, `DELETE /channel` | +| Encode/decode param | `channel_key` parameter on all encode/decode endpoints | +| Response headers | `X-Stegasoo-Channel-Mode` and `X-Stegasoo-Channel-Fingerprint` | **Key benefits:** -- ✅ No need to track encoding dates -- ✅ Simpler request/response models -- ✅ True asynchronous communications -- ✅ Stronger default security (4 words = ~44 bits) +- ✅ Isolate messages between teams, deployments, or groups +- ✅ Same credentials can't decode messages from different channels +- ✅ Backward compatible (public mode = no channel key) -**Breaking Change:** v3.2.0 cannot decode images created with v3.1.x due to different key derivation. +**Breaking change:** v4.0.0 messages (with channel key) cannot be decoded by v3.x installations. --- @@ -73,16 +73,6 @@ Version 3.2.0 introduces breaking changes to simplify the API: pip install stegasoo[api] ``` -This automatically installs DCT dependencies (scipy) for full functionality. - -### From Source - -```bash -git clone https://github.com/example/stegasoo.git -cd stegasoo -pip install -e ".[api]" -``` - ### Running the Server **Development:** @@ -93,23 +83,16 @@ python main.py **Production:** ```bash -cd frontends/api uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 ``` -**Docker:** +**Docker with channel key:** ```bash -docker-compose up api +STEGASOO_CHANNEL_KEY=XXXX-XXXX-... docker-compose up api ``` --- -## Authentication - -The API currently operates without authentication. For production deployments, implement authentication at the reverse proxy level (nginx, Caddy) or add API key middleware. - ---- - ## Base URL | Environment | URL | @@ -126,18 +109,11 @@ The API currently operates without authentication. For production deployments, i Check API status and configuration. -#### Request - -```http -GET / HTTP/1.1 -Host: localhost:8000 -``` - #### Response ```json { - "version": "3.2.0", + "version": "4.0.0", "has_argon2": true, "has_qrcode_read": true, "has_dct": true, @@ -145,50 +121,27 @@ Host: localhost:8000 "available_modes": ["lsb", "dct"], "dct_features": { "output_formats": ["png", "jpeg"], - "color_modes": ["grayscale", "color"], - "default_output_format": "png", - "default_color_mode": "grayscale" + "color_modes": ["grayscale", "color"] + }, + "channel": { + "mode": "private", + "configured": true, + "fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456", + "source": "~/.stegasoo/channel.key" }, "breaking_changes": { - "date_removed": "No date_str parameter needed - encode/decode anytime", - "passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)", - "format_version": 4, + "v4_channel_key": "Messages encoded with channel key require same key to decode", + "format_version": 5, "backward_compatible": false } } ``` -#### Response Fields - -| Field | Type | Description | -|-------|------|-------------| -| `version` | string | Stegasoo library version | -| `has_argon2` | boolean | Whether Argon2id is available | -| `has_qrcode_read` | boolean | Whether QR code reading is available | -| `has_dct` | boolean | Whether DCT mode is available (scipy) | -| `max_payload_kb` | integer | Maximum payload size in KB | -| `available_modes` | array | Available embedding modes | -| `dct_features` | object | DCT mode options (if available) | -| `breaking_changes` | object | v3.2.0 breaking changes info | - -#### cURL Example - -```bash -curl http://localhost:8000/ -``` - --- ### GET /modes -Get available embedding modes and their status. - -#### Request - -```http -GET /modes HTTP/1.1 -Host: localhost:8000 -``` +Get available embedding modes and channel status. #### Response @@ -204,28 +157,177 @@ Host: localhost:8000 "dct": { "available": true, "name": "DCT Domain", - "description": "Embed in DCT coefficients, frequency domain steganography", "output_formats": ["png", "jpeg"], "color_modes": ["grayscale", "color"], "capacity_ratio": "~20% of LSB", "requires": "scipy" + }, + "channel": { + "mode": "private", + "configured": true, + "fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456" } } ``` --- +### GET /channel/status + +Get current channel key status. **New in v4.0.0.** + +#### Query Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `reveal` | boolean | `false` | Include full key in response | + +#### Response + +```json +{ + "mode": "private", + "configured": true, + "fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456", + "source": "~/.stegasoo/channel.key", + "key": null +} +``` + +With `reveal=true`: + +```json +{ + "mode": "private", + "configured": true, + "fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456", + "source": "~/.stegasoo/channel.key", + "key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456" +} +``` + +#### cURL Example + +```bash +# Show status +curl http://localhost:8000/channel/status + +# Reveal full key +curl "http://localhost:8000/channel/status?reveal=true" +``` + +--- + +### POST /channel/generate + +Generate a new channel key. **New in v4.0.0.** + +#### Query Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `save` | boolean | `false` | Save to user config | +| `save_project` | boolean | `false` | Save to project config | + +#### Response + +```json +{ + "key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456", + "fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456", + "saved": true, + "save_location": "~/.stegasoo/channel.key" +} +``` + +#### cURL Examples + +```bash +# Just generate (don't save) +curl -X POST http://localhost:8000/channel/generate + +# Generate and save to user config +curl -X POST "http://localhost:8000/channel/generate?save=true" + +# Generate and save to project config +curl -X POST "http://localhost:8000/channel/generate?save_project=true" +``` + +--- + +### POST /channel/set + +Set/save a channel key to config. **New in v4.0.0.** + +#### Request Body + +```json +{ + "key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456", + "location": "user" +} +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `key` | string | required | Channel key | +| `location` | string | `"user"` | `"user"` or `"project"` | + +#### Response + +```json +{ + "success": true, + "location": "~/.stegasoo/channel.key", + "fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456" +} +``` + +--- + +### DELETE /channel + +Clear channel key from config. **New in v4.0.0.** + +#### Query Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `location` | string | `"user"` | `"user"`, `"project"`, or `"all"` | + +#### Response + +```json +{ + "success": true, + "mode": "public", + "still_configured": false, + "remaining_source": null +} +``` + +#### cURL Example + +```bash +# Clear user config +curl -X DELETE http://localhost:8000/channel + +# Clear project config +curl -X DELETE "http://localhost:8000/channel?location=project" + +# Clear all +curl -X DELETE "http://localhost:8000/channel?location=all" +``` + +--- + ### POST /generate Generate credentials for encoding/decoding. -#### Request - -```http -POST /generate HTTP/1.1 -Host: localhost:8000 -Content-Type: application/json +#### Request Body +```json { "use_pin": true, "use_rsa": false, @@ -235,16 +337,6 @@ Content-Type: application/json } ``` -#### Request Body - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `use_pin` | boolean | `true` | Generate a PIN | -| `use_rsa` | boolean | `false` | Generate an RSA key | -| `pin_length` | integer | `6` | PIN length (6-9) | -| `rsa_bits` | integer | `2048` | RSA key size (2048, 3072, 4096) | -| `words_per_passphrase` | integer | `4` | Words per passphrase (3-12) | - #### Response ```json @@ -257,66 +349,19 @@ Content-Type: application/json "pin": 19, "rsa": 0, "total": 63 - }, - "phrases": null + } } ``` -#### Response Fields - -| Field | Type | Description | -|-------|------|-------------| -| `passphrase` | string | Single passphrase (v3.2.0) | -| `pin` | string\|null | Generated PIN (if requested) | -| `rsa_key_pem` | string\|null | PEM-encoded RSA key (if requested) | -| `entropy.passphrase` | integer | Entropy from passphrase (bits) | -| `entropy.pin` | integer | Entropy from PIN (bits) | -| `entropy.rsa` | integer | Entropy from RSA key (bits) | -| `entropy.total` | integer | Combined entropy (bits) | -| `phrases` | null | Deprecated field (always null in v3.2.0) | - -#### cURL Examples - -**Default (PIN with 4-word passphrase):** -```bash -curl -X POST http://localhost:8000/generate \ - -H "Content-Type: application/json" \ - -d '{"use_pin": true}' -``` - -**RSA only with 6-word passphrase:** -```bash -curl -X POST http://localhost:8000/generate \ - -H "Content-Type: application/json" \ - -d '{"use_pin": false, "use_rsa": true, "rsa_bits": 4096, "words_per_passphrase": 6}' -``` - -**Both PIN and RSA:** -```bash -curl -X POST http://localhost:8000/generate \ - -H "Content-Type: application/json" \ - -d '{ - "use_pin": true, - "use_rsa": true, - "pin_length": 9, - "rsa_bits": 4096, - "words_per_passphrase": 6 - }' -``` - --- ### POST /encode (JSON) -Encode a text message using base64-encoded images. +Encode a text message into an image. -#### Request - -```http -POST /encode HTTP/1.1 -Host: localhost:8000 -Content-Type: application/json +#### Request Body +```json { "message": "Secret message here", "reference_photo_base64": "iVBORw0KGgo...", @@ -325,28 +370,20 @@ Content-Type: application/json "pin": "123456", "rsa_key_base64": null, "rsa_password": null, + "channel_key": null, "embed_mode": "lsb", "dct_output_format": "png", "dct_color_mode": "grayscale" } ``` -#### Request Body +#### Channel Key Parameter (v4.0.0) -| Field | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `message` | string | ✓ | | Message to encode | -| `reference_photo_base64` | string | ✓ | | Base64-encoded reference photo | -| `carrier_image_base64` | string | ✓ | | Base64-encoded carrier image | -| `passphrase` | string | ✓ | | Passphrase (v3.2.0) | -| `pin` | string | * | | Static PIN (6-9 digits) | -| `rsa_key_base64` | string | * | | Base64-encoded RSA key PEM | -| `rsa_password` | string | | | Password for RSA key | -| `embed_mode` | string | | `"lsb"` | `"lsb"` or `"dct"` | -| `dct_output_format` | string | | `"png"` | `"png"` or `"jpeg"` (DCT only) | -| `dct_color_mode` | string | | `"grayscale"` | `"grayscale"` or `"color"` (DCT only) | - -\* At least one of `pin` or `rsa_key_base64` required. +| Value | Effect | +|-------|--------| +| `null` | Auto mode - use server-configured key | +| `""` (empty string) | Public mode - no channel isolation | +| `"XXXX-XXXX-..."` | Explicit key - use this specific key | #### Response @@ -358,162 +395,82 @@ Content-Type: application/json "embed_mode": "lsb", "output_format": "png", "color_mode": "color", - "date_used": null, - "day_of_week": null + "channel_mode": "private", + "channel_fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456" } ``` -#### Response Fields - -| Field | Type | Description | -|-------|------|-------------| -| `stego_image_base64` | string | Base64-encoded stego image | -| `filename` | string | Suggested filename | -| `capacity_used_percent` | float | Percentage of capacity used | -| `embed_mode` | string | Mode used: `"lsb"` or `"dct"` | -| `output_format` | string | Output format: `"png"` or `"jpeg"` | -| `color_mode` | string | Color mode: `"color"` or `"grayscale"` | -| `date_used` | null | Deprecated (always null in v3.2.0) | -| `day_of_week` | null | Deprecated (always null in v3.2.0) | - -#### cURL Example (LSB Mode) - -```bash -# Prepare base64-encoded images -REF_B64=$(base64 -w0 reference.jpg) -CARRIER_B64=$(base64 -w0 carrier.png) - -curl -X POST http://localhost:8000/encode \ - -H "Content-Type: application/json" \ - -d "{ - \"message\": \"Secret message\", - \"reference_photo_base64\": \"$REF_B64\", - \"carrier_image_base64\": \"$CARRIER_B64\", - \"passphrase\": \"apple forest thunder mountain\", - \"pin\": \"123456\" - }" | jq -r '.stego_image_base64' | base64 -d > stego.png -``` - -#### cURL Example (DCT Mode with JPEG) - -```bash -curl -X POST http://localhost:8000/encode \ - -H "Content-Type: application/json" \ - -d "{ - \"message\": \"Secret message\", - \"reference_photo_base64\": \"$REF_B64\", - \"carrier_image_base64\": \"$CARRIER_B64\", - \"passphrase\": \"apple forest thunder mountain\", - \"pin\": \"123456\", - \"embed_mode\": \"dct\", - \"dct_output_format\": \"jpeg\", - \"dct_color_mode\": \"color\" - }" | jq -r '.stego_image_base64' | base64 -d > stego.jpg -``` - --- ### POST /encode/file -Encode a binary file using base64-encoded data. +Encode a file into an image (JSON with base64). -#### Request - -```http -POST /encode/file HTTP/1.1 -Host: localhost:8000 -Content-Type: application/json - -{ - "file_data_base64": "JVBERi0xLjQK...", - "filename": "secret.pdf", - "mime_type": "application/pdf", - "reference_photo_base64": "iVBORw0KGgo...", - "carrier_image_base64": "iVBORw0KGgo...", - "passphrase": "apple forest thunder mountain", - "pin": "123456", - "embed_mode": "lsb" -} -``` - -#### Request Body +Same parameters as `/encode`, plus: | Field | Type | Required | Description | |-------|------|----------|-------------| | `file_data_base64` | string | ✓ | Base64-encoded file data | | `filename` | string | ✓ | Original filename | -| `mime_type` | string | | MIME type of file | -| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo | -| `carrier_image_base64` | string | ✓ | Base64-encoded carrier image | -| `passphrase` | string | ✓ | Passphrase | -| `pin` | string | * | Static PIN | -| `rsa_key_base64` | string | * | Base64-encoded RSA key | -| `embed_mode` | string | | `"lsb"` or `"dct"` | -| `dct_output_format` | string | | `"png"` or `"jpeg"` | -| `dct_color_mode` | string | | `"grayscale"` or `"color"` | - -#### Response - -Same as `/encode` endpoint. +| `mime_type` | string | | MIME type | --- ### POST /encode/multipart -Encode using direct file uploads. Returns the stego image directly. +Encode using multipart form data (file uploads). #### Form Fields -| Field | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `passphrase` | string | ✓ | | Passphrase (v3.2.0) | -| `reference_photo` | file | ✓ | | Reference photo file | -| `carrier` | file | ✓ | | Carrier image file | -| `message` | string | * | | Text message to encode | -| `payload_file` | file | * | | Binary file to embed | -| `pin` | string | ** | | Static PIN | -| `rsa_key` | file | ** | | RSA key file (.pem) | -| `rsa_key_qr` | file | ** | | RSA key from QR code image | -| `rsa_password` | string | | | Password for RSA key | -| `embed_mode` | string | | `"lsb"` | `"lsb"` or `"dct"` | -| `dct_output_format` | string | | `"png"` | `"png"` or `"jpeg"` | -| `dct_color_mode` | string | | `"grayscale"` | `"grayscale"` or `"color"` | +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `passphrase` | string | ✓ | Passphrase | +| `reference_photo` | file | ✓ | Reference photo | +| `carrier` | file | ✓ | Carrier image | +| `message` | string | * | Text message | +| `payload_file` | file | * | Binary file to embed | +| `pin` | string | | Static PIN | +| `rsa_key` | file | | RSA key (.pem) | +| `rsa_key_qr` | file | | RSA key (QR code image) | +| `rsa_password` | string | | RSA key password | +| `channel_key` | string | | `"auto"` (default), `"none"=public`, or explicit key | +| `embed_mode` | string | | `"lsb"` or `"dct"` | +| `dct_output_format` | string | | `"png"` or `"jpeg"` | +| `dct_color_mode` | string | | `"grayscale"` or `"color"` | -\* At least one of `message` or `payload_file` required. -\*\* At least one of `pin`, `rsa_key`, or `rsa_key_qr` required. +\* Provide either `message` or `payload_file` + +#### Channel Key in Multipart + +For form data, the channel_key field uses strings: + +| Value | Effect | +|-------|--------| +| `"auto"` | Use server config (default) | +| `"none"` | Public mode | +| `"XXXX-XXXX-..."` | Explicit key | #### Response -Returns the image directly with headers: +Returns the stego image directly with headers: ```http HTTP/1.1 200 OK Content-Type: image/png -Content-Disposition: attachment; filename="a1b2c3d4.png" +Content-Disposition: attachment; filename=a1b2c3d4.png X-Stegasoo-Capacity-Percent: 12.4 X-Stegasoo-Embed-Mode: lsb -X-Stegasoo-Output-Format: png -X-Stegasoo-Color-Mode: color -X-Stegasoo-Version: 3.2.0 +X-Stegasoo-Channel-Mode: private +X-Stegasoo-Channel-Fingerprint: ABCD-••••-...-3456 +X-Stegasoo-Version: 4.0.0 ``` -#### Response Headers - -| Header | Description | -|--------|-------------| -| `Content-Type` | `image/png` or `image/jpeg` | -| `Content-Disposition` | Suggested filename | -| `X-Stegasoo-Capacity-Percent` | Capacity percentage used | -| `X-Stegasoo-Embed-Mode` | `lsb` or `dct` | -| `X-Stegasoo-Output-Format` | `png` or `jpeg` | -| `X-Stegasoo-Color-Mode` | `color` or `grayscale` | -| `X-Stegasoo-Version` | API version | - -#### cURL Example (LSB) +#### cURL Examples ```bash +# Encode with auto channel key (default) curl -X POST http://localhost:8000/encode/multipart \ -F "passphrase=apple forest thunder mountain" \ -F "pin=123456" \ @@ -521,30 +478,23 @@ curl -X POST http://localhost:8000/encode/multipart \ -F "reference_photo=@reference.jpg" \ -F "carrier=@carrier.png" \ --output stego.png -``` -#### cURL Example (DCT + JPEG) - -```bash +# Encode with explicit channel key curl -X POST http://localhost:8000/encode/multipart \ - -F "passphrase=apple forest thunder mountain" \ + -F "passphrase=words here" \ -F "pin=123456" \ - -F "message=Secret message for social media" \ - -F "embed_mode=dct" \ - -F "dct_output_format=jpeg" \ - -F "dct_color_mode=color" \ + -F "message=Team message" \ + -F "channel_key=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456" \ -F "reference_photo=@reference.jpg" \ -F "carrier=@carrier.png" \ - --output stego.jpg -``` + --output stego.png -#### cURL Example (Embed File) - -```bash +# Encode in public mode (no channel isolation) curl -X POST http://localhost:8000/encode/multipart \ - -F "passphrase=apple forest thunder mountain" \ + -F "passphrase=words here" \ -F "pin=123456" \ - -F "payload_file=@secret.pdf" \ + -F "message=Public message" \ + -F "channel_key=none" \ -F "reference_photo=@reference.jpg" \ -F "carrier=@carrier.png" \ --output stego.png @@ -554,15 +504,11 @@ curl -X POST http://localhost:8000/encode/multipart \ ### POST /decode (JSON) -Decode a message or file using base64-encoded images. Auto-detects embedding mode. +Decode a message or file from a stego image. -#### Request - -```http -POST /decode HTTP/1.1 -Host: localhost:8000 -Content-Type: application/json +#### Request Body +```json { "stego_image_base64": "iVBORw0KGgo...", "reference_photo_base64": "iVBORw0KGgo...", @@ -570,24 +516,11 @@ Content-Type: application/json "pin": "123456", "rsa_key_base64": null, "rsa_password": null, + "channel_key": null, "embed_mode": "auto" } ``` -#### Request Body - -| Field | Type | Required | Default | Description | -|-------|------|----------|---------|-------------| -| `stego_image_base64` | string | ✓ | | Base64-encoded stego image | -| `reference_photo_base64` | string | ✓ | | Base64-encoded reference photo | -| `passphrase` | string | ✓ | | Passphrase | -| `pin` | string | * | | Static PIN | -| `rsa_key_base64` | string | * | | Base64-encoded RSA key | -| `rsa_password` | string | | | Password for RSA key | -| `embed_mode` | string | | `"auto"` | `"auto"`, `"lsb"`, or `"dct"` | - -\* Must match security factors used during encoding. - #### Response (Text) ```json @@ -606,310 +539,122 @@ Content-Type: application/json { "payload_type": "file", "message": null, - "file_data_base64": "JVBERi0xLjQK...", - "filename": "secret.pdf", + "file_data_base64": "UEsDBBQAAAA...", + "filename": "document.pdf", "mime_type": "application/pdf" } ``` -#### Response Fields - -| Field | Type | Description | -|-------|------|-------------| -| `payload_type` | string | `"text"` or `"file"` | -| `message` | string\|null | Decoded message (if text) | -| `file_data_base64` | string\|null | Base64-encoded file (if file) | -| `filename` | string\|null | Original filename (if file) | -| `mime_type` | string\|null | MIME type (if file) | - -#### cURL Example - -```bash -STEGO_B64=$(base64 -w0 stego.png) -REF_B64=$(base64 -w0 reference.jpg) - -curl -X POST http://localhost:8000/decode \ - -H "Content-Type: application/json" \ - -d "{ - \"stego_image_base64\": \"$STEGO_B64\", - \"reference_photo_base64\": \"$REF_B64\", - \"passphrase\": \"apple forest thunder mountain\", - \"pin\": \"123456\" - }" -``` - --- ### POST /decode/multipart -Decode using direct file uploads. Auto-detects embedding mode. +Decode using multipart form data. #### Form Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `passphrase` | string | ✓ | Passphrase | -| `reference_photo` | file | ✓ | Reference photo file | -| `stego_image` | file | ✓ | Stego image file | -| `pin` | string | * | Static PIN | -| `rsa_key` | file | * | RSA key file (.pem) | -| `rsa_key_qr` | file | * | RSA key from QR code image | -| `rsa_password` | string | | Password for RSA key | +| `reference_photo` | file | ✓ | Reference photo | +| `stego_image` | file | ✓ | Stego image to decode | +| `pin` | string | | Static PIN | +| `rsa_key` | file | | RSA key (.pem) | +| `rsa_key_qr` | file | | RSA key (QR code image) | +| `rsa_password` | string | | RSA key password | +| `channel_key` | string | | `"auto"` (default), `"none"=public`, or explicit key | | `embed_mode` | string | | `"auto"`, `"lsb"`, or `"dct"` | -#### Response +--- -Same JSON format as `/decode` endpoint. +## Channel Keys -#### cURL Example +### Overview + +Channel keys provide **deployment/group isolation**. Messages encoded with a channel key can only be decoded with the same key. + +### Key Format + +``` +ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 +└──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ + 8 groups of 4 alphanumeric characters (256 bits) +``` + +### Storage Locations + +Keys are checked in order: + +| Priority | Location | Best For | +|----------|----------|----------| +| 1 | `STEGASOO_CHANNEL_KEY` env var | Docker, CI/CD | +| 2 | `./config/channel.key` | Project-specific | +| 3 | `~/.stegasoo/channel.key` | User default | + +### API Parameter Values + +#### JSON Endpoints (`/encode`, `/decode`) + +| Value | Effect | +|-------|--------| +| `null` | Auto - use server config | +| `""` | Public mode | +| `"XXXX-..."` | Explicit key | + +#### Multipart Endpoints (`/encode/multipart`, `/decode/multipart`) + +| Value | Effect | +|-------|--------| +| `"auto"` | Use server config (default) | +| `"none"` | Public mode | +| `"XXXX-..."` | Explicit key | + +### Workflow Example ```bash -curl -X POST http://localhost:8000/decode/multipart \ - -F "passphrase=apple forest thunder mountain" \ +# 1. Generate a channel key for the team +KEY=$(curl -s -X POST http://localhost:8000/channel/generate | jq -r '.key') +echo "Team key: $KEY" + +# 2. Distribute to team members (securely!) + +# 3. Each deployment sets the key +export STEGASOO_CHANNEL_KEY=$KEY + +# 4. Encode - automatically uses server key +curl -X POST http://localhost:8000/encode/multipart \ + -F "passphrase=team passphrase" \ -F "pin=123456" \ - -F "reference_photo=@reference.jpg" \ + -F "message=Team secret" \ + -F "reference_photo=@ref.jpg" \ + -F "carrier=@carrier.png" \ + --output stego.png + +# 5. Decode - automatically uses server key +curl -X POST http://localhost:8000/decode/multipart \ + -F "passphrase=team passphrase" \ + -F "pin=123456" \ + -F "reference_photo=@ref.jpg" \ -F "stego_image=@stego.png" ``` --- -### POST /compare - -Compare LSB and DCT embedding modes for a carrier image. - -#### Request - -```http -POST /compare HTTP/1.1 -Host: localhost:8000 -Content-Type: application/json - -{ - "carrier_image_base64": "iVBORw0KGgo...", - "payload_size": 50000 -} -``` - -#### Request Body - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `carrier_image_base64` | string | ✓ | Base64-encoded carrier image | -| `payload_size` | integer | | Optional payload size to check | - -#### Response - -```json -{ - "width": 1920, - "height": 1080, - "lsb": { - "capacity_bytes": 776970, - "capacity_kb": 758.8, - "available": true, - "output_format": "PNG" - }, - "dct": { - "capacity_bytes": 64800, - "capacity_kb": 63.3, - "available": true, - "output_formats": ["png", "jpeg"], - "color_modes": ["grayscale", "color"], - "ratio_vs_lsb_percent": 8.3 - }, - "payload_check": { - "size_bytes": 50000, - "fits_lsb": true, - "fits_dct": true - }, - "recommendation": "dct (payload fits, better stealth)" -} -``` - ---- - -### POST /will-fit - -Check if a payload of given size will fit in a carrier image. - -#### Request - -```http -POST /will-fit HTTP/1.1 -Host: localhost:8000 -Content-Type: application/json - -{ - "carrier_image_base64": "iVBORw0KGgo...", - "payload_size": 50000, - "embed_mode": "lsb" -} -``` - -#### Response - -```json -{ - "fits": true, - "payload_size": 50000, - "capacity": 776970, - "usage_percent": 6.4, - "headroom": 726970, - "mode": "lsb" -} -``` - ---- - -### POST /image/info - -Get image information and capacity. - -#### Request (Multipart) - -```bash -curl -X POST http://localhost:8000/image/info \ - -F "image=@carrier.png" -``` - -#### Response - -```json -{ - "width": 1920, - "height": 1080, - "pixels": 2073600, - "capacity_bytes": 776970, - "capacity_kb": 758, - "modes": { - "lsb": { - "capacity_bytes": 776970, - "capacity_kb": 758.8, - "available": true, - "output_format": "PNG" - }, - "dct": { - "capacity_bytes": 64800, - "capacity_kb": 63.3, - "available": true, - "output_format": "PNG/JPEG (grayscale or color)" - } - } -} -``` - ---- - -### POST /extract-key-from-qr - -Extract RSA key from a QR code image. - -#### Request (Multipart) - -```bash -curl -X POST http://localhost:8000/extract-key-from-qr \ - -F "qr_image=@keyqr.png" -``` - -#### Response - -```json -{ - "success": true, - "key_pem": "-----BEGIN PRIVATE KEY-----\n...", - "error": null -} -``` - ---- - -## Embedding Modes - -### LSB Mode (Default) - -**Least Significant Bit** embedding modifies pixel values directly. - -| Aspect | Details | -|--------|---------| -| **Parameter** | `"embed_mode": "lsb"` | -| **Capacity** | ~3 bits/pixel (~375 KB for 1920×1080) | -| **Output** | PNG only (lossless required) | -| **Resilience** | ❌ Destroyed by JPEG compression | -| **Best For** | Maximum capacity, controlled channels | - -### DCT Mode - -**Discrete Cosine Transform** embedding hides data in frequency coefficients. - -| Aspect | Details | -|--------|---------| -| **Parameter** | `"embed_mode": "dct"` | -| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080) | -| **Output** | PNG or JPEG | -| **Resilience** | ✅ Better resistance to analysis | -| **Best For** | Stealth requirements | - -### DCT Options - -| Option | Values | Default | Description | -|--------|--------|---------|-------------| -| `dct_output_format` | `"png"`, `"jpeg"` | `"png"` | Output image format | -| `dct_color_mode` | `"grayscale"`, `"color"` | `"grayscale"` | Color processing mode | - -### Capacity Comparison - -| Mode | 1920×1080 Capacity | -|------|-------------------| -| LSB (PNG) | ~375 KB | -| DCT (PNG) | ~65 KB | -| DCT (JPEG) | ~50 KB | - ---- - ## Data Models -### GenerateRequest +### ChannelStatusResponse ```json { - "use_pin": true, - "use_rsa": false, - "pin_length": 6, - "rsa_bits": 2048, - "words_per_passphrase": 4 + "mode": "private", + "configured": true, + "fingerprint": "ABCD-••••-...-3456", + "source": "~/.stegasoo/channel.key", + "key": "ABCD-1234-..." } ``` -### GenerateResponse - -```json -{ - "passphrase": "word1 word2 word3 word4", - "pin": "123456", - "rsa_key_pem": null, - "entropy": {"passphrase": 44, "pin": 19, "rsa": 0, "total": 63}, - "phrases": null -} -``` - -### EncodeRequest - -```json -{ - "message": "string", - "reference_photo_base64": "string", - "carrier_image_base64": "string", - "passphrase": "string", - "pin": "string", - "rsa_key_base64": "string", - "rsa_password": "string", - "embed_mode": "lsb", - "dct_output_format": "png", - "dct_color_mode": "grayscale" -} -``` - -### EncodeResponse +### EncodeResponse (v4.0.0) ```json { @@ -919,22 +664,8 @@ curl -X POST http://localhost:8000/extract-key-from-qr \ "embed_mode": "lsb", "output_format": "png", "color_mode": "color", - "date_used": null, - "day_of_week": null -} -``` - -### DecodeRequest - -```json -{ - "stego_image_base64": "string", - "reference_photo_base64": "string", - "passphrase": "string", - "pin": "string", - "rsa_key_base64": "string", - "rsa_password": "string", - "embed_mode": "auto" + "channel_mode": "private", + "channel_fingerprint": "ABCD-••••-...-3456" } ``` @@ -950,14 +681,6 @@ curl -X POST http://localhost:8000/extract-key-from-qr \ } ``` -### ErrorResponse - -```json -{ - "detail": "Error message describing the problem" -} -``` - --- ## Error Handling @@ -967,176 +690,141 @@ curl -X POST http://localhost:8000/extract-key-from-qr \ | Code | Meaning | Use Case | |------|---------|----------| | 200 | OK | Successful operation | -| 400 | Bad Request | Invalid input, capacity error | -| 401 | Unauthorized | Decryption failed (wrong credentials) | +| 400 | Bad Request | Invalid input, capacity error, invalid channel key | +| 401 | Unauthorized | Decryption failed, channel key mismatch | | 500 | Internal Error | Unexpected server error | -| 501 | Not Implemented | Feature unavailable (e.g., QR without pyzbar) | +| 501 | Not Implemented | Feature unavailable | -### Common Errors +### Channel Key Errors -| Status | Error | Solution | -|--------|-------|----------| -| 400 | "Must enable at least one of use_pin or use_rsa" | Set `use_pin` or `use_rsa` to true | -| 400 | "Carrier image too small" | Use larger carrier image | -| 400 | "DCT mode requires scipy" | Install scipy | -| 400 | "embed_mode must be 'lsb' or 'dct'" | Fix embed_mode value | -| 401 | "Decryption failed. Check credentials." | Verify passphrase, PIN, ref photo | -| 501 | "QR code reading not available" | Install pyzbar and libzbar | +| Status | Error | Cause | +|--------|-------|-------| +| 400 | "Invalid channel key format" | Key doesn't match `XXXX-XXXX-...` pattern | +| 401 | "Message encoded with channel key but none configured" | Need to provide channel key | +| 401 | "Message encoded without channel key" | Use `channel_key=""` or `"none"` | --- ## Code Examples -### Python with requests +### Python ```python -import base64 import requests BASE_URL = "http://localhost:8000" -# Generate credentials -response = requests.post(f"{BASE_URL}/generate", json={ - "use_pin": True, - "words_per_passphrase": 4 -}) -creds = response.json() -print(f"Passphrase: {creds['passphrase']}") -print(f"PIN: {creds['pin']}") +# Check channel status +status = requests.get(f"{BASE_URL}/channel/status").json() +print(f"Channel mode: {status['mode']}") +print(f"Fingerprint: {status.get('fingerprint', 'N/A')}") -# Encode using multipart (LSB mode) -with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier: +# Generate channel key +response = requests.post(f"{BASE_URL}/channel/generate?save=true") +key_info = response.json() +print(f"Generated: {key_info['fingerprint']}") + +# Encode with channel key (auto from server) +with open("ref.jpg", "rb") as ref, open("carrier.png", "rb") as carrier: response = requests.post(f"{BASE_URL}/encode/multipart", files={ "reference_photo": ref, "carrier": carrier, }, data={ - "message": "Secret message", - "passphrase": "apple forest thunder mountain", - "pin": "123456" + "message": "Team secret", + "passphrase": "apple forest thunder", + "pin": "123456", + # channel_key defaults to "auto" (use server config) }) with open("stego.png", "wb") as f: f.write(response.content) + + print(f"Channel mode: {response.headers.get('X-Stegasoo-Channel-Mode')}") -# Encode with DCT for social media -with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier: +# Encode with explicit channel key +with open("ref.jpg", "rb") as ref, open("carrier.png", "rb") as carrier: response = requests.post(f"{BASE_URL}/encode/multipart", files={ "reference_photo": ref, "carrier": carrier, }, data={ - "message": "Secret message for Instagram", - "passphrase": "apple forest thunder mountain", + "message": "Using explicit key", + "passphrase": "words here", "pin": "123456", - "embed_mode": "dct", - "dct_output_format": "jpeg", - "dct_color_mode": "color" + "channel_key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456", }) - - with open("stego_social.jpg", "wb") as f: - f.write(response.content) -# Decode (auto-detects mode) -with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego: +# Decode +with open("ref.jpg", "rb") as ref, open("stego.png", "rb") as stego: response = requests.post(f"{BASE_URL}/decode/multipart", files={ "reference_photo": ref, "stego_image": stego, }, data={ - "passphrase": "apple forest thunder mountain", - "pin": "123456" + "passphrase": "apple forest thunder", + "pin": "123456", + # channel_key defaults to "auto" }) result = response.json() - if result['payload_type'] == 'text': - print(f"Decoded: {result['message']}") - else: - # Save decoded file - file_data = base64.b64decode(result['file_data_base64']) - with open(result['filename'], 'wb') as f: - f.write(file_data) + print(f"Decoded: {result.get('message')}") ``` -### JavaScript/Node.js +### JavaScript ```javascript +const axios = require('axios'); const FormData = require('form-data'); const fs = require('fs'); -const axios = require('axios'); const BASE_URL = 'http://localhost:8000'; -async function generate() { - const response = await axios.post(`${BASE_URL}/generate`, { - use_pin: true, - words_per_passphrase: 4 - }); +async function main() { + // Check channel status + const status = await axios.get(`${BASE_URL}/channel/status`); + console.log('Channel:', status.data.mode); - console.log('Passphrase:', response.data.passphrase); - console.log('PIN:', response.data.pin); - return response.data; -} - -async function encode(passphrase, pin) { + // Encode with auto channel key const form = new FormData(); - form.append('passphrase', passphrase); - form.append('pin', pin); - form.append('message', 'Secret message'); - form.append('reference_photo', fs.createReadStream('reference.jpg')); + form.append('passphrase', 'apple forest thunder'); + form.append('pin', '123456'); + form.append('message', 'Secret'); + form.append('reference_photo', fs.createReadStream('ref.jpg')); form.append('carrier', fs.createReadStream('carrier.png')); - + // channel_key defaults to "auto" (use server config) + const response = await axios.post(`${BASE_URL}/encode/multipart`, form, { headers: form.getHeaders(), responseType: 'arraybuffer' }); - + fs.writeFileSync('stego.png', response.data); - console.log('Encoded to stego.png'); + console.log('Channel mode:', response.headers['x-stegasoo-channel-mode']); } -async function decode(passphrase, pin) { - const form = new FormData(); - form.append('passphrase', passphrase); - form.append('pin', pin); - form.append('reference_photo', fs.createReadStream('reference.jpg')); - form.append('stego_image', fs.createReadStream('stego.png')); - - const response = await axios.post(`${BASE_URL}/decode/multipart`, form, { - headers: form.getHeaders() - }); - - console.log('Decoded:', response.data.message); -} - -// Usage -generate() - .then(creds => encode(creds.passphrase, creds.pin)) - .then(() => decode('apple forest thunder mountain', '123456')); +main(); ``` -### Shell Script (Bash) +### cURL / Bash ```bash #!/bin/bash BASE_URL="http://localhost:8000" -PASSPHRASE="apple forest thunder mountain" -PIN="123456" -# Generate credentials -echo "Generating credentials..." -CREDS=$(curl -s -X POST "$BASE_URL/generate" \ - -H "Content-Type: application/json" \ - -d '{"use_pin": true, "words_per_passphrase": 4}') +# Check channel status +echo "Channel status:" +curl -s "$BASE_URL/channel/status" | jq . -echo "Passphrase: $(echo $CREDS | jq -r '.passphrase')" -echo "PIN: $(echo $CREDS | jq -r '.pin')" +# Generate and save channel key +echo "Generating channel key..." +curl -s -X POST "$BASE_URL/channel/generate?save=true" | jq . -# Encode +# Encode (channel_key defaults to "auto") echo "Encoding..." curl -s -X POST "$BASE_URL/encode/multipart" \ - -F "passphrase=$PASSPHRASE" \ - -F "pin=$PIN" \ + -F "passphrase=apple forest thunder" \ + -F "pin=123456" \ -F "message=Secret message" \ - -F "reference_photo=@reference.jpg" \ + -F "reference_photo=@ref.jpg" \ -F "carrier=@carrier.png" \ --output stego.png @@ -1144,79 +832,46 @@ echo "Encoded to stego.png" # Decode echo "Decoding..." -RESULT=$(curl -s -X POST "$BASE_URL/decode/multipart" \ - -F "passphrase=$PASSPHRASE" \ - -F "pin=$PIN" \ - -F "reference_photo=@reference.jpg" \ - -F "stego_image=@stego.png") - -echo "Decoded: $(echo $RESULT | jq -r '.message')" +curl -s -X POST "$BASE_URL/decode/multipart" \ + -F "passphrase=apple forest thunder" \ + -F "pin=123456" \ + -F "reference_photo=@ref.jpg" \ + -F "stego_image=@stego.png" | jq . ``` --- -## Rate Limiting +## Docker Configuration -The API does not implement rate limiting by default. For production: +### docker-compose.yml -1. **Reverse Proxy**: Use nginx or Caddy rate limiting -2. **Application Level**: Add FastAPI middleware +```yaml +x-common-env: &common-env + STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-} -Example nginx rate limiting: -```nginx -limit_req_zone $binary_remote_addr zone=stegasoo:10m rate=10r/s; - -location /api/ { - limit_req zone=stegasoo burst=20 nodelay; - proxy_pass http://localhost:8000/; -} +services: + api: + build: + context: . + target: api + ports: + - "8000:8000" + environment: + <<: *common-env ``` ---- +### .env (gitignored) -## Security Considerations - -### In Transit - -- Use HTTPS in production -- Configure TLS at reverse proxy level - -### Memory Usage - -- Argon2id requires 256MB RAM per operation -- DCT mode adds ~100MB for scipy operations -- Concurrent requests can exhaust memory -- Limit workers based on available RAM - -**Worker calculation:** -``` -workers = (available_RAM - 512MB) / 350MB +```bash +STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 ``` -### Input Validation +### Generate key for .env -The API validates: -- PIN format (6-9 digits, no leading zero) -- Passphrase presence -- Image size limits -- RSA key validity -- Embedding mode values -- Output format compatibility - -### Credential Handling - -- Credentials are never logged -- No persistent storage of secrets -- Memory cleared after operations - ---- - -## Interactive Documentation - -When the API is running, visit: - -- **Swagger UI**: http://localhost:8000/docs -- **ReDoc**: http://localhost:8000/redoc +```bash +curl -s -X POST http://localhost:8000/channel/generate | \ + jq -r '"STEGASOO_CHANNEL_KEY=\(.key)"' >> .env +``` --- @@ -1224,5 +879,4 @@ When the API is running, visit: - [CLI Documentation](CLI.md) - Command-line interface - [Web UI Documentation](WEB_UI.md) - Browser interface -- [API Update Summary](api/API_UPDATE_SUMMARY_V3.2.0.md) - Migration guide - [README](../README.md) - Project overview diff --git a/frontends/CLI.md b/frontends/CLI.md index d58a1ba..619ffe4 100644 --- a/frontends/CLI.md +++ b/frontends/CLI.md @@ -1,21 +1,23 @@ -# Stegasoo CLI Documentation (v3.2.0) +# Stegasoo CLI Documentation (v4.0.0) Complete command-line interface reference for Stegasoo steganography operations. ## Table of Contents - [Installation](#installation) -- [What's New in v3.2.0](#whats-new-in-v320) +- [What's New in v4.0.0](#whats-new-in-v400) - [Quick Start](#quick-start) - [Commands](#commands) - [generate](#generate-command) - [encode](#encode-command) - [decode](#decode-command) - [verify](#verify-command) + - [channel](#channel-command) - [info](#info-command) - [compare](#compare-command) - [modes](#modes-command) - [strip-metadata](#strip-metadata-command) +- [Channel Keys](#channel-keys) - [Embedding Modes](#embedding-modes) - [Security Factors](#security-factors) - [Workflow Examples](#workflow-examples) @@ -56,28 +58,31 @@ stegasoo --help # Check DCT support python -c "from stegasoo import has_dct_support; print('DCT:', 'available' if has_dct_support() else 'requires scipy')" + +# Check channel key status +stegasoo channel show ``` --- -## What's New in v3.2.0 +## What's New in v4.0.0 -Version 3.2.0 brings major simplifications to the authentication system: +Version 4.0.0 adds **channel key** support for deployment/group isolation: -| Change | Before (v3.1) | After (v3.2.0) | -|--------|---------------|----------------| -| Passphrase | Daily rotation (7 phrases) | Single passphrase | -| Date parameter | Required for encode/decode | Removed entirely | -| Default words | 3 words per phrase | 4 words | -| Terminology | `day_phrase`, `phrase` | `passphrase` | +| Feature | Description | +|---------|-------------| +| Channel keys | 256-bit keys that isolate message groups | +| Deployment isolation | Different deployments can't read each other's messages | +| CLI management | New `stegasoo channel` command group | +| Flexible override | Use server config, explicit key, or public mode | **Key benefits:** -- ✅ No need to remember which day a message was encoded -- ✅ True asynchronous communication -- ✅ Simpler credential management -- ✅ Stronger default security (4 words = ~44 bits entropy) +- ✅ Isolate messages between teams, deployments, or groups +- ✅ Same credentials can't decode messages from different channels +- ✅ Backward compatible (public mode = no channel key) +- ✅ Easy key distribution via environment variables or config files -**Migration:** Old stego images encoded with v3.1.x cannot be decoded with v3.2.0 due to the removed date-based key derivation. Keep v3.1.x installed if you need to access old images. +**Breaking change:** v4.0.0 messages (with channel key) cannot be decoded by v3.x installations. --- @@ -87,7 +92,10 @@ Version 3.2.0 brings major simplifications to the authentication system: # 1. Generate credentials (do this once, memorize results) stegasoo generate -# 2. Encode a message (LSB mode - default) +# 2. (Optional) Set up channel key for deployment isolation +stegasoo channel generate --save + +# 3. Encode a message (uses configured channel key automatically) stegasoo encode \ --ref secret_photo.jpg \ --carrier meme.png \ @@ -95,22 +103,20 @@ stegasoo encode \ --pin 123456 \ --message "Meet at midnight" -# 3. Encode for social media (DCT mode) -stegasoo encode \ - --ref secret_photo.jpg \ - --carrier meme.png \ - --passphrase "apple forest thunder mountain" \ - --pin 123456 \ - --message "Meet at midnight" \ - --mode dct \ - --dct-format jpeg - -# 4. Decode a message (auto-detects mode) +# 4. Decode a message (uses same channel key) stegasoo decode \ --ref secret_photo.jpg \ --stego stego_abc123.png \ --passphrase "apple forest thunder mountain" \ --pin 123456 + +# 5. Decode without channel key (public mode) +stegasoo decode \ + --ref secret_photo.jpg \ + --stego public_stego.png \ + --passphrase "words here now" \ + --pin 123456 \ + --no-channel ``` --- @@ -119,7 +125,7 @@ stegasoo decode \ ### Generate Command -Generate credentials for encoding/decoding operations. Creates a passphrase and optionally a PIN and/or RSA key. +Generate credentials for encoding/decoding operations. #### Synopsis @@ -135,97 +141,32 @@ stegasoo generate [OPTIONS] | `--rsa/--no-rsa` | | flag | `--no-rsa` | Generate an RSA key | | `--pin-length` | | 6-9 | 6 | PIN length in digits | | `--rsa-bits` | | choice | 2048 | RSA key size (2048, 3072, 4096) | -| `--words` | | 3-12 | 4 | Words in passphrase (v3.2.0: default increased to 4) | +| `--words` | | 3-12 | 4 | Words in passphrase | | `--output` | `-o` | path | | Save RSA key to file | | `--password` | `-p` | string | | Password for RSA key file | | `--json` | | flag | | Output as JSON | #### Examples -**Basic generation with PIN (default):** ```bash +# Basic generation with PIN (default) stegasoo generate -``` -Output: -``` -============================================================ - STEGASOO CREDENTIALS (v3.2.0) -============================================================ - -⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW - Do not screenshot or save to file! - -─── STATIC PIN ─── - 847293 - -─── PASSPHRASE ─── - abandon ability able about - -─── SECURITY ─── - Passphrase entropy: 44 bits (4 words) - PIN entropy: 19 bits - Combined: 63 bits - + photo entropy: 80-256 bits - -✓ v3.2.0: Use this passphrase anytime - no date needed! -``` - -**Generate with more words for higher security:** -```bash +# Generate with more words for higher security stegasoo generate --words 6 -``` -**Generate with RSA key:** -```bash +# Generate with RSA key stegasoo generate --rsa --rsa-bits 4096 -``` -**Save RSA key to encrypted file:** -```bash +# Save RSA key to encrypted file stegasoo generate --rsa -o mykey.pem -p "mysecretpassword" ``` -**Maximum security (longer passphrase + both factors):** -```bash -stegasoo generate --pin --rsa --words 6 --pin-length 9 -``` - -**JSON output for scripting:** -```bash -stegasoo generate --json -``` - -Output: -```json -{ - "passphrase": "abandon ability able about", - "pin": "847293", - "rsa_key": null, - "entropy": { - "passphrase": 44, - "pin": 19, - "rsa": 0, - "total": 63 - } -} -``` - -**Extract passphrase from JSON:** -```bash -stegasoo generate --json | jq -r '.passphrase' -``` - -**RSA only (no PIN):** -```bash -stegasoo generate --no-pin --rsa -o key.pem -p "password123" -``` - --- ### Encode Command -Encode a secret message or file into an image using steganography. +Encode a secret message or file into an image. #### Synopsis @@ -237,158 +178,76 @@ stegasoo encode [OPTIONS] | Option | Short | Type | Required | Default | Description | |--------|-------|------|----------|---------|-------------| -| `--ref` | `-r` | path | ✓ | | Reference photo (shared secret) | -| `--carrier` | `-c` | path | ✓ | | Carrier image to hide message in | -| `--passphrase` | `-p` | string | ✓ | | Passphrase (v3.2.0: single, no date needed) | +| `--ref` | `-r` | path | ✓ | | Reference photo | +| `--carrier` | `-c` | path | ✓ | | Carrier image | +| `--passphrase` | `-p` | string | ✓ | | Passphrase | | `--message` | `-m` | string | | | Message to encode | | `--message-file` | `-f` | path | | | Read message from file | | `--embed-file` | `-e` | path | | | Embed a binary file | | `--pin` | | string | * | | Static PIN (6-9 digits) | | `--key` | `-k` | path | * | | RSA key file | -| `--key-qr` | | path | * | | RSA key from QR code image | -| `--key-password` | | string | | | Password for RSA key | +| `--key-qr` | | path | * | | RSA key from QR code | +| `--key-password` | | string | | | RSA key password | +| `--channel` | | string | | auto | Channel key (v4.0.0) | +| `--channel-file` | | path | | | Read channel key from file | +| `--no-channel` | | flag | | | Force public mode | | `--output` | `-o` | path | | | Output filename | -| `--mode` | | choice | | `lsb` | Embedding mode: `lsb` or `dct` | -| `--dct-format` | | choice | | `png` | DCT output: `png` or `jpeg` | -| `--dct-color` | | choice | | `grayscale` | DCT color: `grayscale` or `color` | +| `--mode` | | choice | | `lsb` | Embedding mode | +| `--dct-format` | | choice | | `png` | DCT output format | +| `--dct-color` | | choice | | `grayscale` | DCT color mode | | `--quiet` | `-q` | flag | | | Suppress output | \* At least one of `--pin`, `--key`, or `--key-qr` is required. -#### Message Input Methods +#### Channel Key Options -1. **Command line argument:** - ```bash - stegasoo encode -r ref.jpg -c carrier.png -p "four word passphrase" --pin 123456 -m "Secret message" - ``` - -2. **From file:** - ```bash - stegasoo encode -r ref.jpg -c carrier.png -p "four word passphrase" --pin 123456 -f message.txt - ``` - -3. **From stdin (pipe):** - ```bash - echo "Secret message" | stegasoo encode -r ref.jpg -c carrier.png -p "four word passphrase" --pin 123456 - ``` - -4. **Embed binary file:** - ```bash - stegasoo encode -r ref.jpg -c carrier.png -p "four word passphrase" --pin 123456 -e secret.pdf - ``` +| Option | Effect | +|--------|--------| +| *(none)* | Use server-configured key (auto mode) | +| `--channel KEY` | Use explicit channel key | +| `--channel auto` | Same as no option | +| `--channel-file F` | Read channel key from file | +| `--no-channel` | Force public mode (no isolation) | #### Examples -**Basic encoding with PIN (LSB mode - default):** ```bash +# Basic encoding (uses server channel key if configured) stegasoo encode \ - --ref photos/vacation.jpg \ - --carrier memes/funny_cat.png \ - --passphrase "correct horse battery staple" \ + -r photo.jpg -c meme.png \ + -p "correct horse battery staple" \ --pin 847293 \ - --message "The package arrives Tuesday" -``` + -m "The package arrives Tuesday" -Output: -``` -Mode: LSB (12.4% capacity) -✓ Encoded successfully! - Output: a1b2c3d4.png - Size: 245,832 bytes - Capacity used: 12.4% -``` - -**DCT mode for social media (JPEG output):** -```bash +# With explicit channel key stegasoo encode \ - --ref photos/vacation.jpg \ - --carrier memes/funny_cat.png \ - --passphrase "correct horse battery staple" \ + -r photo.jpg -c meme.png \ + -p "correct horse battery staple" \ --pin 847293 \ - --message "The package arrives Tuesday" \ - --mode dct \ - --dct-format jpeg -``` + -m "Secret message" \ + --channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 -Output: -``` -Mode: DCT (grayscale, JPEG) (45.2% capacity) -✓ Encoded successfully! - Output: a1b2c3d4.jpg - Size: 89,432 bytes - Capacity used: 45.2% - DCT output: JPEG (grayscale) -``` - -**DCT mode with color preservation:** -```bash +# Public mode (no channel isolation) stegasoo encode \ - -r ref.jpg \ - -c carrier.png \ - -p "phrase words here now" \ - --pin 123456 \ - -m "Message" \ - --mode dct \ - --dct-color color \ - --dct-format png -``` + -r photo.jpg -c meme.png \ + -p "correct horse battery staple" \ + --pin 847293 \ + -m "Public message" \ + --no-channel -**With RSA key:** -```bash +# DCT mode for social media stegasoo encode \ - -r reference.jpg \ - -c carrier.png \ - -p "apple forest thunder mountain" \ - -k mykey.pem \ - --key-password "secretpassword" \ - -m "Encrypted with RSA" -``` - -**Both PIN and RSA (maximum security):** -```bash -stegasoo encode \ - -r ref.jpg \ - -c carrier.png \ - -p "word1 word2 word3 word4" \ - --pin 123456 \ - -k mykey.pem \ - --key-password "pass" \ - -m "Double-locked message" -``` - -**Custom output filename:** -```bash -stegasoo encode \ - -r ref.jpg \ - -c carrier.png \ - -p "phrase words here now" \ - --pin 123456 \ - -m "Message" \ - -o holiday_photo.png -``` - -**Embed a binary file:** -```bash -stegasoo encode \ - -r ref.jpg \ - -c large_image.png \ - -p "secure phrase words here" \ - --pin 123456 \ - -e secret_document.pdf \ - -o output.png -``` - -**Quiet mode for scripting:** -```bash -stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "msg" -q -o out.png -# No output, just creates the file + -r photo.jpg -c meme.png \ + -p "words here" --pin 847293 \ + -m "Secret" \ + --mode dct --dct-format jpeg ``` --- ### Decode Command -Decode a secret message or file from a stego image. **Automatically detects LSB vs DCT mode.** +Decode a secret message or file from a stego image. #### Synopsis @@ -398,100 +257,56 @@ stegasoo decode [OPTIONS] #### Options -| Option | Short | Type | Required | Description | -|--------|-------|------|----------|-------------| -| `--ref` | `-r` | path | ✓ | Reference photo (same as encoding) | -| `--stego` | `-s` | path | ✓ | Stego image to decode | -| `--passphrase` | `-p` | string | ✓ | Passphrase used for encoding | -| `--pin` | | string | * | Static PIN | -| `--key` | `-k` | path | * | RSA key file | -| `--key-qr` | | path | * | RSA key from QR code image | -| `--key-password` | | string | | Password for RSA key | -| `--output` | `-o` | path | | Save message to file | -| `--mode` | | choice | | Extraction mode: `auto`, `lsb`, or `dct` | -| `--quiet` | `-q` | flag | | Output only the message | -| `--force` | | flag | | Overwrite existing output file | - -\* Must provide the same security factors used during encoding. +| Option | Short | Type | Required | Default | Description | +|--------|-------|------|----------|---------|-------------| +| `--ref` | `-r` | path | ✓ | | Reference photo | +| `--stego` | `-s` | path | ✓ | | Stego image | +| `--passphrase` | `-p` | string | ✓ | | Passphrase | +| `--pin` | | string | * | | Static PIN | +| `--key` | `-k` | path | * | | RSA key file | +| `--key-qr` | | path | * | | RSA key from QR code | +| `--key-password` | | string | | | RSA key password | +| `--channel` | | string | | auto | Channel key (v4.0.0) | +| `--channel-file` | | path | | | Read channel key from file | +| `--no-channel` | | flag | | | Force public mode | +| `--output` | `-o` | path | | | Save output to file | +| `--mode` | | choice | | `auto` | Extraction mode | +| `--quiet` | `-q` | flag | | | Minimal output | +| `--force` | | flag | | | Overwrite existing file | #### Examples -**Basic decoding with PIN:** ```bash +# Basic decoding (uses server channel key) stegasoo decode \ - --ref photos/vacation.jpg \ - --stego received_image.png \ - --passphrase "correct horse battery staple" \ + -r photo.jpg -s stego.png \ + -p "correct horse battery staple" \ --pin 847293 -``` -Output: -``` -✓ Decoded successfully! - -The package arrives Tuesday -``` - -**Decoding DCT image (auto-detected):** -```bash +# With explicit channel key stegasoo decode \ - --ref photos/vacation.jpg \ - --stego received_image.jpg \ - --passphrase "correct horse battery staple" \ - --pin 847293 -``` + -r photo.jpg -s stego.png \ + -p "words here" --pin 847293 \ + --channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 -**With RSA key:** -```bash +# Decode public image (no channel key was used) stegasoo decode \ - -r reference.jpg \ - -s stego_image.png \ - -p "apple forest thunder mountain" \ - -k mykey.pem \ - --key-password "secretpassword" -``` + -r photo.jpg -s stego.png \ + -p "words here" --pin 847293 \ + --no-channel -**Save decoded message to file:** -```bash +# Save to file stegasoo decode \ - -r ref.jpg \ - -s stego.png \ - -p "passphrase words here now" \ - --pin 123456 \ - -o decoded_message.txt -``` - -Output: -``` -✓ Decoded successfully! - Saved to: decoded_message.txt -``` - -**Quiet mode (message only):** -```bash -stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q -``` - -Output: -``` -The package arrives Tuesday -``` - -**Pipe to another command:** -```bash -stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | gpg --decrypt -``` - -**Force specific extraction mode:** -```bash -stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 --mode dct + -r photo.jpg -s stego.png \ + -p "words" --pin 123456 \ + -o decoded.txt ``` --- ### Verify Command -Verify that a stego image can be decoded without extracting the actual message content. +Verify credentials without extracting the message. #### Synopsis @@ -501,579 +316,419 @@ stegasoo verify [OPTIONS] #### Options -| Option | Short | Type | Required | Description | -|--------|-------|------|----------|-------------| -| `--ref` | `-r` | path | ✓ | Reference photo | -| `--stego` | `-s` | path | ✓ | Stego image to verify | -| `--passphrase` | `-p` | string | ✓ | Passphrase | -| `--pin` | | string | * | Static PIN | -| `--key` | `-k` | path | * | RSA key file | -| `--key-qr` | | path | * | RSA key from QR code | -| `--key-password` | | string | | Password for RSA key | -| `--mode` | | choice | | Extraction mode: `auto`, `lsb`, or `dct` | -| `--json` | | flag | | Output as JSON | +Same as `decode`, minus `--output` and `--force`. Adds `--json` for JSON output. #### Examples -**Basic verification:** ```bash -stegasoo verify -r photo.jpg -s stego.png -p "my passphrase here" --pin 123456 +# Quick verification +stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456 + +# With explicit channel key +stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456 \ + --channel ABCD-1234-... + +# JSON output +stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456 --json ``` -Output: -``` -✓ Valid stego image - Payload: text (142 bytes) - Size: 142 bytes -``` +--- + +### Channel Command + +Manage channel keys for deployment/group isolation. + +#### Subcommands + +| Subcommand | Description | +|------------|-------------| +| `generate` | Create a new channel key | +| `show` | Display current channel key status | +| `set` | Save a channel key to config | +| `clear` | Remove channel key from config | + +#### channel generate -**JSON output:** ```bash -stegasoo verify -r photo.jpg -s stego.png -p "words here" --pin 123456 --json +stegasoo channel generate [OPTIONS] ``` -Output: -```json -{ - "valid": true, - "stego_file": "stego.png", - "payload_type": "text", - "payload_size": 142 -} -``` +| Option | Short | Description | +|--------|-------|-------------| +| `--save` | `-s` | Save to user config (~/.stegasoo/channel.key) | +| `--save-project` | | Save to project config (./config/channel.key) | +| `--env` | `-e` | Output as environment variable export | +| `--quiet` | `-q` | Output only the key | + +**Examples:** -**Failed verification:** ```bash -stegasoo verify -r photo.jpg -s stego.png -p "wrong passphrase" --pin 123456 +# Just display a new key +stegasoo channel generate + +# Save to user config +stegasoo channel generate --save + +# Add to .env file +stegasoo channel generate --env >> .env + +# For scripts +KEY=$(stegasoo channel generate -q) ``` -Output: +#### channel show + +```bash +stegasoo channel show [OPTIONS] ``` -✗ Verification failed - Error: Decryption failed: Invalid authentication tag + +| Option | Short | Description | +|--------|-------|-------------| +| `--reveal` | `-r` | Show full key (not just fingerprint) | +| `--json` | | Output as JSON | + +**Examples:** + +```bash +# Show status (fingerprint only) +stegasoo channel show + +# Reveal full key +stegasoo channel show --reveal + +# JSON for scripts +stegasoo channel show --json +``` + +**Output:** + +``` +─── CHANNEL KEY STATUS ─── + + Mode: PRIVATE + Fingerprint: ABCD-••••-••••-••••-••••-••••-••••-3456 + Source: ~/.stegasoo/channel.key + + Messages require this channel key to decode. +``` + +#### channel set + +```bash +stegasoo channel set [KEY] [OPTIONS] +``` + +| Option | Short | Description | +|--------|-------|-------------| +| `--file` | `-f` | Read key from file | +| `--project` | `-p` | Save to project config instead of user | + +**Examples:** + +```bash +# Set from command line +stegasoo channel set ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 + +# Set from file +stegasoo channel set --file channel.key + +# Set in project config +stegasoo channel set XXXX-... --project +``` + +#### channel clear + +```bash +stegasoo channel clear [OPTIONS] +``` + +| Option | Short | Description | +|--------|-------|-------------| +| `--project` | `-p` | Clear project config | +| `--all` | | Clear both user and project configs | +| `--force` | `-f` | Skip confirmation | + +**Examples:** + +```bash +# Clear user config (with confirmation) +stegasoo channel clear + +# Clear project config +stegasoo channel clear --project + +# Clear all configs without confirmation +stegasoo channel clear --all --force ``` --- ### Info Command -Display information about an image's capacity for both LSB and DCT modes. - -#### Synopsis +Show information about an image file. ```bash stegasoo info IMAGE [OPTIONS] ``` -#### Options - -| Option | Type | Description | -|--------|------|-------------| -| `--json` | flag | Output as JSON | - -#### Examples - -**Check carrier image capacity:** -```bash -stegasoo info vacation_photo.png -``` - -Output: -``` -Image: vacation_photo.png - Dimensions: 1920 × 1080 - Pixels: 2,073,600 - Mode: RGB - Format: PNG - - Capacity: - LSB mode: ~776,970 bytes (758.8 KB) - DCT mode: ~64,800 bytes (63.3 KB) ✓ - DCT ratio: 8.3% of LSB - DCT options: grayscale/color, png/jpeg -``` - -**JSON output:** -```bash -stegasoo info photo.png --json -``` - -Output: -```json -{ - "file": "photo.png", - "width": 1920, - "height": 1080, - "pixels": 2073600, - "mode": "RGB", - "format": "PNG", - "capacity": { - "lsb": { - "bytes": 776970, - "kb": 758.8 - }, - "dct": { - "bytes": 64800, - "kb": 63.3, - "available": true, - "ratio_vs_lsb": 8.3, - "output_formats": ["png", "jpeg"], - "color_modes": ["grayscale", "color"] - } - } -} -``` - --- ### Compare Command -Compare LSB and DCT embedding modes for an image with recommendations. - -#### Synopsis +Compare embedding mode capacities for an image. ```bash stegasoo compare IMAGE [OPTIONS] ``` -#### Options - -| Option | Short | Type | Description | -|--------|-------|------|-------------| -| `--payload-size` | `-s` | int | Check if specific payload size fits | -| `--json` | | flag | Output as JSON | - -#### Examples - -**Basic comparison:** -```bash -stegasoo compare carrier.png -``` - -Output: -``` -=== Mode Comparison: carrier.png === - Dimensions: 1920 × 1080 - - ┌─── LSB Mode ─── - │ Capacity: 776,970 bytes (758.8 KB) - │ Output: PNG - │ Status: ✓ Available - │ - ├─── DCT Mode ─── - │ Capacity: 64,800 bytes (63.3 KB) - │ Ratio: 8.3% of LSB capacity - │ Status: ✓ Available - │ Formats: PNG (lossless), JPEG (smaller) - │ Colors: Grayscale (default), Color - │ - └─── Recommendation ─── - LSB for larger payloads, DCT for better stealth - DCT supports color output with --dct-color color -``` - -**Check if payload fits:** -```bash -stegasoo compare carrier.png --payload-size 50000 -``` - -Output: -``` -=== Mode Comparison: carrier.png === - Dimensions: 1920 × 1080 - - ┌─── LSB Mode ─── - │ Capacity: 776,970 bytes (758.8 KB) - │ Output: PNG - │ Status: ✓ Available - │ - ├─── DCT Mode ─── - │ Capacity: 64,800 bytes (63.3 KB) - │ Ratio: 8.3% of LSB capacity - │ Status: ✓ Available - │ Formats: PNG (lossless), JPEG (smaller) - │ Colors: Grayscale (default), Color - │ - ├─── Payload Check ─── - │ Size: 50,000 bytes - │ LSB mode: ✓ Fits - │ DCT mode: ✓ Fits - │ - └─── Recommendation ─── - DCT mode for better stealth (payload fits both modes) - Use --dct-color color to preserve original colors -``` - --- ### Modes Command Show available embedding modes and their status. -#### Synopsis - ```bash stegasoo modes ``` -#### Example Output - -``` -=== Stegasoo Embedding Modes (v3.2.0) === - - LSB Mode (Spatial LSB) - Status: ✓ Always available - Output: PNG/BMP (full color) - Capacity: ~375 KB per megapixel - Use case: Larger payloads, color preservation - CLI flag: --mode lsb (default) - - DCT Mode (Frequency Domain) - Status: ✓ Available - Capacity: ~75 KB per megapixel (~20% of LSB) - Use case: Better stealth, frequency domain hiding - CLI flag: --mode dct - - DCT Options - Output format: - --dct-format png Lossless, larger file (default) - --dct-format jpeg Lossy, smaller, more natural - - Color mode: - --dct-color grayscale Traditional DCT (default) - --dct-color color Preserves original colors - - v3.2.0 Changes: - ✓ No date parameters needed - ✓ Single passphrase (no daily rotation) - ✓ Default passphrase increased to 4 words - ✓ True asynchronous communications - - Examples: - # Traditional DCT (grayscale PNG) - stegasoo encode ... --mode dct - - # Color-preserving DCT with JPEG output - stegasoo encode ... --mode dct --dct-color color --dct-format jpeg - - # Compare modes for an image - stegasoo compare carrier.png -``` +Now also displays channel key status. --- ### Strip-Metadata Command -Remove all metadata (EXIF, GPS, etc.) from an image. - -#### Synopsis +Remove all metadata from an image. ```bash stegasoo strip-metadata IMAGE [OPTIONS] ``` -#### Options +--- -| Option | Short | Type | Default | Description | -|--------|-------|------|---------|-------------| -| `--output` | `-o` | path | | Output file (default: overwrites as PNG) | -| `--format` | `-f` | choice | PNG | Output format: `PNG` or `BMP` | -| `--quiet` | `-q` | flag | | Suppress output | +## Channel Keys -#### Examples +Channel keys provide **deployment/group isolation** - messages encoded with a channel key can only be decoded by systems with the same key. +### Key Format + +``` +ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 +└──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ + 8 groups of 4 alphanumeric characters (256 bits) +``` + +### Storage Locations + +Channel keys are checked in this order: + +| Priority | Location | Best For | +|----------|----------|----------| +| 1 | `STEGASOO_CHANNEL_KEY` env var | Docker, CI/CD | +| 2 | `./config/channel.key` | Project-specific | +| 3 | `~/.stegasoo/channel.key` | User default | + +### Modes + +| Mode | Description | CLI Option | +|------|-------------|------------| +| **Auto** | Use server-configured key | *(default)* | +| **Explicit** | Use specific key | `--channel KEY` | +| **Public** | No channel isolation | `--no-channel` | + +### Fingerprints + +For security, full keys aren't displayed by default. Instead, a fingerprint is shown: + +``` +Full key: ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 +Fingerprint: ABCD-••••-••••-••••-••••-••••-••••-3456 +``` + +### Use Cases + +**Team isolation:** ```bash -# Strip metadata, save as PNG -stegasoo strip-metadata photo.jpg -o clean.png +# Team A +export STEGASOO_CHANNEL_KEY=AAAA-1111-... -# Overwrite in place (converts to PNG) -stegasoo strip-metadata photo.jpg +# Team B +export STEGASOO_CHANNEL_KEY=BBBB-2222-... + +# Messages from Team A can only be decoded by Team A ``` -Output: +**Development vs Production:** +```bash +# Development +./config/channel.key contains DEV-KEY-... + +# Production +STEGASOO_CHANNEL_KEY=PROD-KEY-... in Docker + +# Dev messages can't be decoded in production ``` -✓ Metadata stripped - Input: photo.jpg (2,456,789 bytes) - Output: clean.png (1,234,567 bytes) + +**Public messages:** +```bash +# Anyone with credentials can decode +stegasoo encode ... --no-channel +stegasoo decode ... --no-channel ``` --- ## Embedding Modes -Stegasoo supports two steganography algorithms. - ### LSB Mode (Default) -**Least Significant Bit** embedding modifies pixel values directly. - ```bash stegasoo encode ... --mode lsb -# or just omit --mode (LSB is default) ``` | Aspect | Details | |--------|---------| -| **Capacity** | ~3 bits/pixel (~375 KB for 1920×1080) | -| **Output** | PNG only (lossless required) | -| **Resilience** | ❌ Destroyed by JPEG compression | -| **Best For** | Maximum capacity, controlled channels | +| **Capacity** | ~375 KB for 1920×1080 | +| **Output** | PNG only | +| **Best For** | Maximum capacity | ### DCT Mode -**Discrete Cosine Transform** embedding hides data in frequency coefficients. - ```bash stegasoo encode ... --mode dct --dct-format jpeg --dct-color color ``` | Aspect | Details | |--------|---------| -| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080) | +| **Capacity** | ~65 KB for 1920×1080 | | **Output** | PNG or JPEG | -| **Resilience** | ✅ Better resistance to analysis | -| **Best For** | Social media, stealth requirements | - -### DCT Options - -| Option | Values | Default | Description | -|--------|--------|---------|-------------| -| `--dct-format` | `png`, `jpeg` | `png` | Output image format | -| `--dct-color` | `grayscale`, `color` | `grayscale` | Color processing | - -### Choosing the Right Mode - -``` -Need maximum capacity? - │ - ┌──────┴──────┐ - ▼ ▼ - YES NO - │ │ - ▼ ▼ -Use LSB Need stealth? -(default) │ - ┌──────┴──────┐ - ▼ ▼ - YES NO - │ │ - ▼ ▼ - Use DCT Use LSB - --mode dct (default) -``` - -### Capacity Comparison - -| Mode | 1920×1080 Capacity | -|------|-------------------| -| LSB (PNG) | ~375 KB | -| DCT (PNG) | ~65 KB | -| DCT (JPEG) | ~50 KB | +| **Best For** | Social media, stealth | --- ## Security Factors -Stegasoo uses multiple authentication factors: - | Factor | Description | Entropy | |--------|-------------|---------| -| Reference Photo | A photo both parties have | ~80-256 bits | -| Passphrase | BIP-39 word phrase | ~44 bits (4 words) | -| Static PIN | Numeric PIN (6-9 digits) | ~20 bits (6 digits) | -| RSA Key | Shared key file | ~128 bits effective | - -### Minimum Requirements - -- At least one of PIN or RSA key must be provided -- Reference photo is always required -- Passphrase is always required - -### Security Configurations - -| Configuration | Entropy (excl. photo) | Use Case | -|--------------|----------------------|----------| -| 4-word passphrase + 6-digit PIN | ~63 bits | Standard use | -| 6-word passphrase + 6-digit PIN | ~85 bits | Enhanced security | -| 4-word passphrase + RSA 2048 | ~172 bits | File-based auth | -| 6-word passphrase + PIN + RSA | ~213 bits | Maximum security | +| Reference Photo | Shared image | ~80-256 bits | +| Passphrase | BIP-39 words | ~44 bits (4 words) | +| Static PIN | Numeric (6-9) | ~20 bits (6 digits) | +| RSA Key | Shared key file | ~128 bits | +| Channel Key (v4.0.0) | Deployment isolation | ~256 bits | --- ## Workflow Examples -### Basic Secure Communication +### Team Setup with Channel Key -**Setup (once):** +**Initial setup (team lead):** ```bash -# Both parties generate credentials -stegasoo generate +# Generate team channel key +stegasoo channel generate -q > team_channel.key -# Or share RSA key securely -stegasoo generate --rsa -o shared_key.pem -p "agreedpassword" -# Securely transfer shared_key.pem to recipient +# Distribute to team members securely +# (encrypted email, secure file share, etc.) ``` -**Sender:** +**Team member setup:** ```bash -# For email, file transfer, etc. (no recompression) -stegasoo encode \ - -r our_shared_photo.jpg \ - -c random_meme.png \ - -p "our shared passphrase here" \ - --pin 847293 \ - -m "Meeting moved to 3pm" +# Save received key +stegasoo channel set --file team_channel.key + +# Verify +stegasoo channel show ``` -**Sender (social media):** +**Daily use:** ```bash -# For platforms that may recompress -stegasoo encode \ - -r our_shared_photo.jpg \ - -c random_meme.png \ - -p "our shared passphrase here" \ - --pin 847293 \ - -m "Meeting moved to 3pm" \ - --mode dct \ - --dct-format jpeg +# Channel key is used automatically +stegasoo encode -r ref.jpg -c meme.png -p "phrase" --pin 123456 -m "Team message" +stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 ``` -**Recipient:** -```bash -# Works for both LSB and DCT (auto-detected) -stegasoo decode \ - -r our_shared_photo.jpg \ - -s received_image.png \ - -p "our shared passphrase here" \ - --pin 847293 +### Docker Deployment + +**docker-compose.yml:** +```yaml +x-common-env: &common-env + STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-} + +services: + web: + environment: + <<: *common-env + api: + environment: + <<: *common-env ``` -### Batch Processing - -**Encode multiple messages:** +**.env (gitignored):** ```bash -#!/bin/bash -PASSPHRASE="apple forest thunder mountain" -PIN="123456" -REF="reference.jpg" - -for file in messages/*.txt; do - name=$(basename "$file" .txt) - stegasoo encode \ - -r "$REF" \ - -c "carriers/${name}.png" \ - -p "$PASSPHRASE" \ - --pin "$PIN" \ - -f "$file" \ - -o "output/${name}_stego.png" \ - -q - echo "Encoded: $name" -done +STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 ``` -**Encode for social media (DCT):** +### CI/CD Pipeline + ```bash -#!/bin/bash -for file in messages/*.txt; do - name=$(basename "$file" .txt) - stegasoo encode \ - -r "$REF" \ - -c "carriers/${name}.png" \ - -p "$PASSPHRASE" \ - --pin "$PIN" \ - -f "$file" \ - --mode dct \ - --dct-format jpeg \ - -o "output/${name}_social.jpg" \ - -q - echo "Encoded for social: $name" -done +# Generate key for CI +CHANNEL_KEY=$(stegasoo channel generate -q) + +# Use in pipeline +STEGASOO_CHANNEL_KEY=$CHANNEL_KEY stegasoo encode ... ``` --- ## Piping & Scripting -### Stdin/Stdout Support - -**Encode from pipe:** -```bash -cat secret.txt | stegasoo encode -r ref.jpg -c carrier.png -p "phrase words" --pin 123456 -o out.png -``` - -**Decode to pipe:** -```bash -stegasoo decode -r ref.jpg -s stego.png -p "phrase words" --pin 123456 -q | less -``` - -**Chain with encryption:** -```bash -# Encode GPG-encrypted content -gpg -e -r recipient@email.com secret.txt -cat secret.txt.gpg | base64 | stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 - -# Decode and decrypt -stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | base64 -d | gpg -d -``` - -### JSON Output for Scripts +### Extract channel key for scripts ```bash -# Get credentials as JSON -creds=$(stegasoo generate --json) +# Get just the key +KEY=$(stegasoo channel show --json | jq -r '.key // empty') -# Extract specific fields -passphrase=$(echo "$creds" | jq -r '.passphrase') -pin=$(echo "$creds" | jq -r '.pin') -entropy=$(echo "$creds" | jq -r '.entropy.total') +# Get fingerprint +FINGERPRINT=$(stegasoo channel show --json | jq -r '.fingerprint // "none"') -echo "Passphrase: $passphrase" -echo "PIN: $pin" -echo "Total entropy: $entropy bits" -``` - -### Error Handling in Scripts - -```bash -#!/bin/bash -set -e - -if ! stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q 2>/dev/null; then - echo "Decryption failed - check credentials" - exit 1 +# Check if configured +if stegasoo channel show --json | jq -e '.configured' > /dev/null; then + echo "Channel key is configured" fi ``` +### Generate and use immediately + +```bash +# Generate, save, and use +stegasoo channel generate --save +stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "message" +``` + --- ## Error Handling -### Common Errors +### Channel Key Errors | Error | Cause | Solution | |-------|-------|----------| -| "Must provide --pin or --key" | No security factor given | Add `--pin` or `--key` option | -| "PIN must be 6-9 digits" | Invalid PIN format | Use numeric PIN, 6-9 chars | -| "Payload too large for LSB mode" | Message exceeds capacity | Use larger carrier or shorter message | -| "Payload too large for DCT mode" | DCT has less space | Use LSB mode or shorter message | -| "Decryption failed" | Wrong credentials | Verify passphrase, PIN, ref photo | -| "DCT mode requires scipy" | Missing library | Install: `pip install scipy` | +| "Invalid channel key format" | Key doesn't match pattern | Use `stegasoo channel generate` | +| "Message encoded with channel key but none configured" | Missing channel key | Set key or use `--channel` | +| "Message encoded without channel key" | Used `--no-channel` to encode | Decode with `--no-channel` | +| "Channel key mismatch" | Wrong key | Verify correct key | -### Troubleshooting Decryption Failures +### Troubleshooting -1. **Check passphrase:** Must be exact match (case-sensitive) -2. **Verify reference photo:** Must be the exact same file, not a resized copy -3. **Check stego image:** - - LSB: Ensure it wasn't resized, recompressed, or converted - - DCT: More resilient but not immune to heavy processing -4. **Verify PIN/key:** Must match exactly what was used for encoding +```bash +# Check current channel status +stegasoo channel show -### v3.2.0 Migration Note +# Try decoding with explicit key +stegasoo decode ... --channel XXXX-XXXX-... -If you're trying to decode images created with v3.1.x: -- v3.2.0 **cannot** decode v3.1.x images (date-based key derivation removed) -- Keep v3.1.x installed to access old images -- Re-encode old messages with v3.2.0 for forward compatibility +# Try decoding without channel key +stegasoo decode ... --no-channel +``` --- @@ -1091,38 +746,12 @@ If you're trying to decode images created with v3.1.x: | Variable | Description | |----------|-------------| +| `STEGASOO_CHANNEL_KEY` | Channel key for deployment isolation (v4.0.0) | | `PYTHONPATH` | Include `src/` for development | | `STEGASOO_DEBUG` | Enable debug output (set to `1`) | --- -## Dependencies - -### Core Dependencies - -- `pillow` - Image processing -- `cryptography` - Encryption -- `argon2-cffi` - Key derivation -- `click` - CLI framework - -### DCT Mode Dependencies - -- `scipy` - DCT transformations - -Install DCT dependencies: -```bash -pip install scipy -``` - -Check availability: -```bash -stegasoo modes -# or -python -c "from stegasoo import has_dct_support; print('DCT:', has_dct_support())" -``` - ---- - ## See Also - [API Documentation](API.md) - Python API reference diff --git a/frontends/WEB_UI.md b/frontends/WEB_UI.md index f28e4bb..e62b441 100644 --- a/frontends/WEB_UI.md +++ b/frontends/WEB_UI.md @@ -1,11 +1,11 @@ -# Stegasoo Web UI Documentation (v3.2.0) +# Stegasoo Web UI Documentation (v3.3.0) Complete guide for the Stegasoo web-based steganography interface. ## Table of Contents - [Overview](#overview) -- [What's New in v3.2.0](#whats-new-in-v320) +- [What's New in v3.3.0](#whats-new-in-v330) - [Installation & Setup](#installation--setup) - [Pages & Features](#pages--features) - [Home Page](#home-page) @@ -14,8 +14,8 @@ Complete guide for the Stegasoo web-based steganography interface. - [Decode Message](#decode-message) - [About Page](#about-page) - [Embedding Modes](#embedding-modes) - - [LSB Mode (Default)](#lsb-mode-default) - - [DCT Mode](#dct-mode) + - [DCT Mode (Default)](#dct-mode-default) + - [LSB Mode](#lsb-mode) - [User Interface Guide](#user-interface-guide) - [Workflow Examples](#workflow-examples) - [Security Features](#security-features) @@ -39,38 +39,39 @@ Built with Flask, Bootstrap 5, and a modern dark theme. ### Features - ✅ Drag-and-drop file uploads -- ✅ Image previews +- ✅ Image previews with scan animations - ✅ Native sharing (Web Share API) - ✅ Responsive design (mobile-friendly) - ✅ Password-protected RSA key downloads - ✅ Real-time entropy calculations - ✅ Automatic file cleanup -- ✅ **DCT steganography mode** - Frequency domain embedding +- ✅ **DCT steganography mode** - Now the default for social media resilience - ✅ **Color mode selection** - Preserve carrier colors - ✅ **File embedding** - Hide files, not just text -- ✅ **v3.2.0: No date tracking** - Simplified workflow +- ✅ **QR code RSA keys** - Scan to import keys +- ✅ **v3.3.0: Streamlined UI** - Compact mode selection, improved form flow --- -## What's New in v3.2.0 +## What's New in v3.3.0 -Version 3.2.0 simplifies the user experience significantly: +Version 3.3.0 improves the user interface with a streamlined workflow: -| Change | Before (v3.1) | After (v3.2.0) | +| Change | Before (v3.2) | After (v3.3.0) | |--------|---------------|----------------| -| Credentials | 7 daily phrases | Single passphrase | -| Encode form | Date selection required | No date field | -| Decode form | Date detection/input | No date needed | -| Default words | 3 words | 4 words | -| Field label | "Day Phrase" | "Passphrase" | +| Default mode | LSB | DCT (when available) | +| Mode selection | Large cards with bullet lists | Compact inline buttons with tooltips | +| Mode position | Top of form | After image upload, before payload | +| Mode details | Always visible | Hover tooltip on ⓘ icon | +| Capacity badges | LSB first | DCT first | +| Status labels | "Key Source", "Carrier", "RSA KEY" | "Hash Acquired", "Carrier Loaded"/"Stego Loaded", "KEY LOADED" | **Key benefits:** -- ✅ No need to remember which day a message was encoded -- ✅ Simpler forms with fewer fields -- ✅ True asynchronous communication -- ✅ Stronger default security (4 words = ~44 bits entropy) - -**Breaking Change:** v3.2.0 cannot decode images created with v3.1.x. +- ✅ DCT mode default - Better for social media sharing +- ✅ Logical form flow: Load images → Select mode → Enter payload +- ✅ Cleaner UI with less visual clutter +- ✅ Mode details available on hover without expanding +- ✅ Consistent "Loaded" status indicators --- @@ -232,16 +233,27 @@ For easier sharing, you can also: Hide a secret message or file inside an image. +#### Form Flow (v3.3.0) + +The encode form follows a logical flow: + +1. **Load Images** - Reference photo and carrier image +2. **View Capacity** - Shows available capacity for DCT and LSB modes +3. **Select Mode** - DCT (default) or LSB with inline tooltips +4. **Enter Payload** - Text message or file +5. **Add Security** - Passphrase, PIN, and/or RSA key + #### Input Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | Reference Photo | Image file | ✓ | Your shared secret photo | | Carrier Image | Image file | ✓ | Image to hide message in | +| Embedding Mode | Toggle | ✓ | DCT (default) or LSB | | Payload Type | Toggle | ✓ | Text message or file | | Secret Message | Text | * | Message to hide (max 50KB) | | File to Embed | File | * | File to hide (max 2MB) | -| Passphrase | Text | ✓ | Your passphrase (v3.2.0) | +| Passphrase | Text | ✓ | Your passphrase | | PIN | Number | ** | Your static PIN | | RSA Key | .pem file | ** | Your shared RSA key | | RSA Key QR | Image file | ** | QR code containing RSA key | @@ -250,25 +262,46 @@ Hide a secret message or file inside an image. \* One of message or file required. \*\* At least one security factor (PIN or RSA Key) required. -#### Advanced Options +#### Embedding Mode Selection (v3.3.0) -Expand "Advanced Options" to access embedding mode settings: +The mode selector is now a compact inline toggle: + +``` +┌────────────────────────────────────────────────────────────┐ +│ ◉ 🔊 DCT · Social Media ⓘ │ ○ ⊞ LSB · Email & Files ⓘ │ +└────────────────────────────────────────────────────────────┘ +``` + +- **DCT** - Default, best for social media sharing +- **LSB** - Higher capacity, for lossless channels +- **ⓘ** - Hover for details (capacity, output format, etc.) + +#### DCT Options + +When DCT mode is selected, additional options appear: | Option | Values | Default | Description | |--------|--------|---------|-------------| -| Embedding Mode | LSB / DCT | LSB | Steganography algorithm | -| Output Format | PNG / JPEG | PNG | Output image format (DCT only) | -| Color Mode | Color / Grayscale | Color | Carrier color handling (DCT only) | - -See [Embedding Modes](#embedding-modes) for detailed explanations. +| Output Format | PNG / JPEG | JPEG | Output image format | +| Color Mode | Color / Grayscale | Color | Carrier color handling | #### Drag-and-Drop Upload Both image upload zones support: - Click to browse - Drag and drop files -- Instant image preview -- File name display +- Instant image preview with scan animation +- Status indicators ("Hash Acquired", "Carrier Loaded") + +#### Capacity Info Panel + +After loading a carrier image, a capacity panel appears: + +``` +┌─────────────────────────────────────────────────────────┐ +│ 📏 Carrier: 1920 × 1080 (2.1 MP) DCT: 150 KB LSB: 750 KB │ +└─────────────────────────────────────────────────────────┘ +``` #### Character Counter @@ -281,11 +314,14 @@ Shows warning at 80% capacity. #### Encoding Process -1. Fill in all required fields -2. (Optional) Expand "Advanced Options" for DCT mode -3. Click "Encode Message" -4. Wait for processing (shows spinner) -5. Redirected to result page +1. Upload reference photo and carrier image +2. View capacity info panel +3. Select embedding mode (DCT default) +4. Choose payload type and enter content +5. Enter passphrase and security factors +6. Click "Encode Message" +7. Wait for processing (shows spinner) +8. Redirected to result page #### Result Page @@ -413,8 +449,8 @@ If decryption fails: Information about the Stegasoo project, security model, and credits. Includes: -- Version information (v3.2.0) -- v3.2.0 changes explanation +- Version information (v3.3.0) +- Recent UI improvements - Security model overview - Dependency status (Argon2, QR code support) @@ -424,44 +460,29 @@ Includes: Stegasoo offers two steganography algorithms, each with different trade-offs. -### LSB Mode (Default) +### DCT Mode (Default) -**Least Significant Bit** embedding modifies the least significant bits of pixel values. +**Discrete Cosine Transform** embedding hides data in frequency domain coefficients. This is now the default mode when scipy is available. | Aspect | Details | |--------|---------| -| **Capacity** | ~3 bits/pixel (~375 KB for 1920×1080) | -| **Output Format** | PNG only (lossless required) | -| **Resilience** | ❌ Destroyed by JPEG compression | -| **Best For** | Maximum capacity, controlled sharing | - -**When to use LSB:** -- Sharing via lossless channels (email attachment, file transfer) -- Maximum message capacity needed -- Recipient won't modify the image - -### DCT Mode - -**Discrete Cosine Transform** embedding hides data in frequency domain coefficients. - -| Aspect | Details | -|--------|---------| -| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080 PNG, ~50 KB JPEG) | +| **Capacity** | ~0.5 bits/pixel (~75 KB/MP) | | **Output Formats** | PNG or JPEG | -| **Resilience** | ✅ Better resistance to analysis | -| **Best For** | Stealth requirements, frequency domain hiding | +| **Resilience** | ✅ Survives JPEG recompression | +| **Best For** | Social media, messaging apps | **When to use DCT:** +- Sharing via social media (Instagram, WhatsApp, Telegram) +- When image may be recompressed - When stealth is important - Smaller messages that fit in reduced capacity -- When you want JPEG output for natural appearance #### DCT Output Formats | Format | Pros | Cons | |--------|------|------| +| **JPEG** | Native format, natural, smaller, resilient | Slightly lower capacity | | **PNG** | Lossless, predictable | Larger file | -| **JPEG** | Native format, natural, smaller | Slightly lower capacity | #### DCT Color Modes @@ -470,15 +491,31 @@ Stegasoo offers two steganography algorithms, each with different trade-offs. | **Color** | Embeds in luminance (Y), preserves chrominance | Most images, photos | | **Grayscale** | Converts to grayscale before embedding | Black & white images | +### LSB Mode + +**Least Significant Bit** embedding modifies the least significant bits of pixel values. + +| Aspect | Details | +|--------|---------| +| **Capacity** | ~3 bits/pixel (~375 KB/MP) | +| **Output Format** | PNG only (lossless required) | +| **Resilience** | ❌ Destroyed by JPEG compression | +| **Best For** | Maximum capacity, controlled sharing | + +**When to use LSB:** +- Sharing via lossless channels (email attachment, file transfer, cloud storage) +- Maximum message capacity needed +- Recipient won't modify/recompress the image + ### Capacity Comparison -For a 1920×1080 image: +For a 1920×1080 image (~2 MP): | Mode | Approximate Capacity | |------|---------------------| -| LSB (PNG) | ~375 KB | -| DCT (PNG, Color) | ~65 KB | -| DCT (JPEG) | ~50 KB | +| LSB (PNG) | ~750 KB | +| DCT (PNG, Color) | ~150 KB | +| DCT (JPEG) | ~150 KB | ### Choosing the Right Mode @@ -487,21 +524,22 @@ For a 1920×1080 image: │ Mode Selection Guide │ ├─────────────────────────────────────────────────────────────┤ │ │ -│ Need maximum capacity? │ +│ Sharing via social media / messaging app? │ │ │ │ │ ┌───────┴───────┐ │ │ ▼ ▼ │ │ YES NO │ │ │ │ │ │ ▼ ▼ │ -│ Use LSB Need stealth? │ +│ Use DCT Need maximum capacity? │ │ (default) │ │ │ ┌───────┴───────┐ │ │ ▼ ▼ │ │ YES NO │ │ │ │ │ │ ▼ ▼ │ -│ Use DCT Use LSB │ +│ Use LSB Use DCT │ +│ (default) │ │ │ └─────────────────────────────────────────────────────────────┘ ``` diff --git a/frontends/api/main.py b/frontends/api/main.py index 3307b21..f175c7d 100644 --- a/frontends/api/main.py +++ b/frontends/api/main.py @@ -6,9 +6,10 @@ FastAPI-based REST API for steganography operations. Supports both text messages and file embedding. CHANGES in v4.0.0: -- Updated from v3.2.0 with no functional API changes -- Internal: JPEG normalization for jpegio compatibility -- Internal: Python 3.12 recommended +- Added channel key support for deployment/group isolation +- New /channel endpoints for key management +- channel_key parameter on encode/decode endpoints +- Messages encoded with channel key require same key to decode CHANGES in v3.2.0: - Removed date dependency from all operations @@ -51,6 +52,17 @@ from stegasoo import ( compare_modes, will_fit_by_mode, calculate_capacity_by_mode, + # Channel key functions (v4.0.0) + generate_channel_key, + get_channel_key, + set_channel_key, + clear_channel_key, + has_channel_key, + get_channel_status, + validate_channel_key, + format_channel_key, + get_active_channel_key, + get_channel_fingerprint, ) from stegasoo.constants import ( MIN_PIN_LENGTH, MAX_PIN_LENGTH, @@ -82,8 +94,9 @@ Secure steganography with hybrid authentication. Supports text messages and file ## Version 4.0.0 Changes -- **Python 3.12 recommended** - jpegio compatibility improvements -- **JPEG normalization** - Handles quality=100 images automatically +- **Channel key support** - Deployment/group isolation for messages +- **New /channel endpoints** - Generate, view, and manage channel keys +- **channel_key parameter** - Added to encode/decode endpoints ## Version 3.2.0 Changes @@ -156,12 +169,15 @@ class EncodeRequest(BaseModel): pin: str = "" rsa_key_base64: Optional[str] = None rsa_password: Optional[str] = None - # date_str removed in v3.2.0 + # Channel key (v4.0.0) + channel_key: Optional[str] = Field( + default=None, + description="Channel key for deployment isolation. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key" + ) embed_mode: EmbedModeType = Field( default="lsb", description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)" ) - # NEW in v3.0.1 dct_output_format: DctOutputFormatType = Field( default="png", description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode." @@ -183,12 +199,15 @@ class EncodeFileRequest(BaseModel): pin: str = "" rsa_key_base64: Optional[str] = None rsa_password: Optional[str] = None - # date_str removed in v3.2.0 + # Channel key (v4.0.0) + channel_key: Optional[str] = Field( + default=None, + description="Channel key for deployment isolation. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key" + ) embed_mode: EmbedModeType = Field( default="lsb", description="Embedding mode: 'lsb' (default, color) or 'dct' (requires scipy)" ) - # NEW in v3.0.1 dct_output_format: DctOutputFormatType = Field( default="png", description="DCT output format: 'png' (lossless) or 'jpeg' (smaller). Only applies to DCT mode." @@ -204,7 +223,6 @@ class EncodeResponse(BaseModel): filename: str capacity_used_percent: float embed_mode: str = Field(description="Embedding mode used: 'lsb' or 'dct'") - # NEW in v3.0.1 output_format: str = Field( default="png", description="Output format: 'png' or 'jpeg' (for DCT mode)" @@ -213,6 +231,15 @@ class EncodeResponse(BaseModel): default="color", description="Color mode: 'color' (LSB/DCT color) or 'grayscale' (DCT grayscale)" ) + # Channel key info (v4.0.0) + channel_mode: str = Field( + default="public", + description="Channel mode: 'public' or 'private'" + ) + channel_fingerprint: Optional[str] = Field( + default=None, + description="Channel key fingerprint (if private mode)" + ) # Legacy fields (v3.2.0: no longer used in crypto) date_used: Optional[str] = Field( default=None, @@ -231,6 +258,11 @@ class DecodeRequest(BaseModel): pin: str = "" rsa_key_base64: Optional[str] = None rsa_password: Optional[str] = None + # Channel key (v4.0.0) + channel_key: Optional[str] = Field( + default=None, + description="Channel key for decryption. null=auto (use server config), ''=public mode, 'XXXX-...'=explicit key" + ) embed_mode: ExtractModeType = Field( default="auto", description="Extraction mode: 'auto' (default), 'lsb', or 'dct'" @@ -260,7 +292,6 @@ class ImageInfoResponse(BaseModel): pixels: int capacity_bytes: int = Field(description="LSB mode capacity (for backwards compatibility)") capacity_kb: int = Field(description="LSB mode capacity in KB") - # NEW in v3.0 modes: Optional[dict[str, ModeCapacity]] = Field( default=None, description="Capacity by embedding mode (v3.0+)" @@ -297,10 +328,38 @@ class DctModeInfo(BaseModel): requires: str +class ChannelStatusResponse(BaseModel): + """Response for channel key status (v4.0.0).""" + mode: str = Field(description="'public' or 'private'") + configured: bool = Field(description="Whether a channel key is configured") + fingerprint: Optional[str] = Field(default=None, description="Key fingerprint (partial)") + source: Optional[str] = Field(default=None, description="Where the key comes from") + key: Optional[str] = Field(default=None, description="Full key (only if reveal=true)") + + +class ChannelGenerateResponse(BaseModel): + """Response for channel key generation (v4.0.0).""" + key: str = Field(description="Generated channel key") + fingerprint: str = Field(description="Key fingerprint") + saved: bool = Field(default=False, description="Whether key was saved to config") + save_location: Optional[str] = Field(default=None, description="Where key was saved") + + +class ChannelSetRequest(BaseModel): + """Request to set channel key (v4.0.0).""" + key: str = Field(description="Channel key to set") + location: str = Field(default="user", description="'user' or 'project'") + + class ModesResponse(BaseModel): """Response showing available embedding modes.""" lsb: dict dct: DctModeInfo + # Channel key status (v4.0.0) + channel: Optional[dict] = Field( + default=None, + description="Channel key status (v4.0.0)" + ) class StatusResponse(BaseModel): @@ -310,14 +369,17 @@ class StatusResponse(BaseModel): has_dct: bool max_payload_kb: int available_modes: list[str] - # NEW in v3.0.1 dct_features: Optional[dict] = Field( default=None, description="DCT mode features (v3.0.1+)" ) - # NEW in v3.2.0 + # Channel key status (v4.0.0) + channel: Optional[dict] = Field( + default=None, + description="Channel key status (v4.0.0)" + ) breaking_changes: dict = Field( - description="v3.2.0 breaking changes" + description="v4.0.0 breaking changes" ) @@ -349,6 +411,67 @@ class ErrorResponse(BaseModel): detail: Optional[str] = None +# ============================================================================ +# HELPER: RESOLVE CHANNEL KEY +# ============================================================================ + +def _resolve_channel_key(channel_key: Optional[str]) -> Optional[str]: + """ + Resolve channel key from API parameter. + + Args: + channel_key: API parameter value + - None: Use server-configured key (auto mode) + - "": Public mode (no channel key) + - "XXXX-...": Explicit key + + Returns: + Resolved channel key to pass to encode/decode + + Raises: + HTTPException: If key format is invalid + """ + if channel_key is None: + # Auto mode - use server config + return None + + if channel_key == "": + # Public mode + return "" + + # Explicit key - validate format + if not validate_channel_key(channel_key): + raise HTTPException( + 400, + f"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" + ) + + return channel_key + + +def _get_channel_info(channel_key: Optional[str]) -> tuple[str, Optional[str]]: + """ + Get channel mode and fingerprint for response. + + Returns: + (mode, fingerprint) tuple + """ + if channel_key == "": + return "public", None + + if channel_key is not None: + # Explicit key + fingerprint = f"{channel_key[:4]}-••••-••••-••••-••••-••••-••••-{channel_key[-4:]}" + return "private", fingerprint + + # Auto mode - check server config + if has_channel_key(): + status = get_channel_status() + return "private", status.get('fingerprint') + + return "public", None + + # ============================================================================ # ROUTES - STATUS & INFO # ============================================================================ @@ -368,6 +491,15 @@ async def root(): "default_color_mode": "grayscale", } + # Channel key status (v4.0.0) + channel_status = get_channel_status() + channel_info = { + "mode": channel_status['mode'], + "configured": channel_status['configured'], + "fingerprint": channel_status.get('fingerprint'), + "source": channel_status.get('source'), + } + return StatusResponse( version=__version__, has_argon2=has_argon2(), @@ -376,11 +508,15 @@ async def root(): max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024, available_modes=available_modes, dct_features=dct_features, + channel=channel_info, breaking_changes={ - "date_removed": "No date_str parameter needed - encode/decode anytime", - "passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)", - "format_version": 4, + "v4_channel_key": "Messages encoded with channel key require same key to decode", + "format_version": 5, "backward_compatible": False, + "v3_notes": { + "date_removed": "No date_str parameter needed - encode/decode anytime", + "passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)", + } } ) @@ -390,9 +526,16 @@ async def api_modes(): """ Get available embedding modes and their status. - NEW in v3.0: Shows LSB and DCT mode availability. - NEW in v3.0.1: Shows DCT color modes and output formats. + v4.0.0: Also includes channel key status. """ + # Channel status + channel_status = get_channel_status() + channel_info = { + "mode": channel_status['mode'], + "configured": channel_status['configured'], + "fingerprint": channel_status.get('fingerprint'), + } + return ModesResponse( lsb={ "available": True, @@ -409,16 +552,137 @@ async def api_modes(): color_modes=["grayscale", "color"], capacity_ratio="~20% of LSB", requires="scipy", - ) + ), + channel=channel_info, ) +# ============================================================================ +# ROUTES - CHANNEL KEY (v4.0.0) +# ============================================================================ + +@app.get("/channel/status", response_model=ChannelStatusResponse) +async def api_channel_status( + reveal: bool = Query(False, description="Include full key in response") +): + """ + Get current channel key status. + + v4.0.0: New endpoint for channel key management. + + Returns mode (public/private), fingerprint, and source. + Use reveal=true to include the full key. + """ + status = get_channel_status() + + return ChannelStatusResponse( + mode=status['mode'], + configured=status['configured'], + fingerprint=status.get('fingerprint'), + source=status.get('source'), + key=status.get('key') if reveal and status['configured'] else None, + ) + + +@app.post("/channel/generate", response_model=ChannelGenerateResponse) +async def api_channel_generate( + save: bool = Query(False, description="Save to user config"), + save_project: bool = Query(False, description="Save to project config"), +): + """ + Generate a new channel key. + + v4.0.0: New endpoint for channel key management. + + Optionally saves to user config (~/.stegasoo/channel.key) or + project config (./config/channel.key). + """ + if save and save_project: + raise HTTPException(400, "Cannot use both save and save_project") + + key = generate_channel_key() + fingerprint = f"{key[:4]}-••••-••••-••••-••••-••••-••••-{key[-4:]}" + + saved = False + save_location = None + + if save: + set_channel_key(key, location='user') + saved = True + save_location = "~/.stegasoo/channel.key" + elif save_project: + set_channel_key(key, location='project') + saved = True + save_location = "./config/channel.key" + + return ChannelGenerateResponse( + key=key, + fingerprint=fingerprint, + saved=saved, + save_location=save_location, + ) + + +@app.post("/channel/set") +async def api_channel_set(request: ChannelSetRequest): + """ + Set/save a channel key to config. + + v4.0.0: New endpoint for channel key management. + """ + if not validate_channel_key(request.key): + raise HTTPException( + 400, + "Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" + ) + + if request.location not in ('user', 'project'): + raise HTTPException(400, "location must be 'user' or 'project'") + + set_channel_key(request.key, location=request.location) + + status = get_channel_status() + return { + "success": True, + "location": status.get('source'), + "fingerprint": status.get('fingerprint'), + } + + +@app.delete("/channel") +async def api_channel_clear( + location: str = Query("user", description="'user', 'project', or 'all'") +): + """ + Clear/remove channel key from config. + + v4.0.0: New endpoint for channel key management. + + Note: Does not affect environment variables. + """ + if location == "all": + clear_channel_key(location='user') + clear_channel_key(location='project') + elif location in ('user', 'project'): + clear_channel_key(location=location) + else: + raise HTTPException(400, "location must be 'user', 'project', or 'all'") + + status = get_channel_status() + return { + "success": True, + "mode": status['mode'], + "still_configured": status['configured'], + "remaining_source": status.get('source'), + } + + @app.post("/compare", response_model=CompareModesResponse) async def api_compare_modes(request: CompareModesRequest): """ Compare LSB and DCT embedding modes for a carrier image. - NEW in v3.0: Returns capacity for both modes and recommendation. + Returns capacity for both modes and recommendation. Optionally checks if a specific payload size would fit. """ try: @@ -474,7 +738,7 @@ async def api_will_fit(request: WillFitRequest): """ Check if a payload of given size will fit in the carrier image. - NEW in v3.0: Supports both LSB and DCT modes. + Supports both LSB and DCT modes. """ try: # Validate mode @@ -555,17 +819,16 @@ async def api_generate(request: GenerateRequest): raise HTTPException(400, f"rsa_bits must be one of {VALID_RSA_SIZES}") try: - # v3.2.0: Call with passphrase_words parameter creds = generate_credentials( use_pin=request.use_pin, use_rsa=request.use_rsa, pin_length=request.pin_length, rsa_bits=request.rsa_bits, - passphrase_words=request.words_per_passphrase, # Map API field to library parameter + passphrase_words=request.words_per_passphrase, ) return GenerateResponse( - passphrase=creds.passphrase, # v3.2.0: Single passphrase + passphrase=creds.passphrase, pin=creds.pin, rsa_key_pem=creds.rsa_key_pem, entropy={ @@ -626,15 +889,16 @@ async def api_encode(request: EncodeRequest): Images must be base64-encoded. Returns base64-encoded stego image. + v4.0.0: Added channel_key parameter for deployment isolation. v3.2.0: No date_str parameter needed - encode anytime! - - NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct'). - NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color'). """ # Validate mode if request.embed_mode == "dct" and not has_dct_support(): raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") + # Resolve channel key + resolved_channel_key = _resolve_channel_key(request.channel_key) + try: ref_photo = base64.b64decode(request.reference_photo_base64) carrier = base64.b64decode(request.carrier_image_base64) @@ -647,17 +911,17 @@ async def api_encode(request: EncodeRequest): request.dct_color_mode ) - # v3.2.0: No date_str parameter + # v4.0.0: Include channel_key result = encode( message=request.message, reference_photo=ref_photo, carrier_image=carrier, - passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase + passphrase=request.passphrase, pin=request.pin, rsa_key_data=rsa_key, rsa_password=request.rsa_password, - # date_str removed in v3.2.0 embed_mode=request.embed_mode, + channel_key=resolved_channel_key, **dct_params, ) @@ -669,6 +933,9 @@ async def api_encode(request: EncodeRequest): request.dct_color_mode ) + # Get channel info for response + channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key) + return EncodeResponse( stego_image_base64=stego_b64, filename=result.filename, @@ -676,8 +943,10 @@ async def api_encode(request: EncodeRequest): embed_mode=request.embed_mode, output_format=output_format, color_mode=color_mode, - date_used=None, # v3.2.0: No longer used - day_of_week=None, # v3.2.0: No longer used + channel_mode=channel_mode, + channel_fingerprint=channel_fingerprint, + date_used=None, + day_of_week=None, ) except CapacityError as e: @@ -695,15 +964,16 @@ async def api_encode_file(request: EncodeFileRequest): File data must be base64-encoded. + v4.0.0: Added channel_key parameter for deployment isolation. v3.2.0: No date_str parameter needed - encode anytime! - - NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct'). - NEW in v3.0.1: Supports dct_output_format and dct_color_mode. """ # Validate mode if request.embed_mode == "dct" and not has_dct_support(): raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") + # Resolve channel key + resolved_channel_key = _resolve_channel_key(request.channel_key) + try: file_data = base64.b64decode(request.file_data_base64) ref_photo = base64.b64decode(request.reference_photo_base64) @@ -723,17 +993,17 @@ async def api_encode_file(request: EncodeFileRequest): request.dct_color_mode ) - # v3.2.0: No date_str parameter + # v4.0.0: Include channel_key result = encode( message=payload, reference_photo=ref_photo, carrier_image=carrier, - passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase + passphrase=request.passphrase, pin=request.pin, rsa_key_data=rsa_key, rsa_password=request.rsa_password, - # date_str removed in v3.2.0 embed_mode=request.embed_mode, + channel_key=resolved_channel_key, **dct_params, ) @@ -745,6 +1015,9 @@ async def api_encode_file(request: EncodeFileRequest): request.dct_color_mode ) + # Get channel info for response + channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key) + return EncodeResponse( stego_image_base64=stego_b64, filename=result.filename, @@ -752,8 +1025,10 @@ async def api_encode_file(request: EncodeFileRequest): embed_mode=request.embed_mode, output_format=output_format, color_mode=color_mode, - date_used=None, # v3.2.0: No longer used - day_of_week=None, # v3.2.0: No longer used + channel_mode=channel_mode, + channel_fingerprint=channel_fingerprint, + date_used=None, + day_of_week=None, ) except CapacityError as e: @@ -775,32 +1050,31 @@ async def api_decode(request: DecodeRequest): Returns payload_type to indicate if result is text or file. + v4.0.0: Added channel_key parameter - must match encoding key. v3.2.0: No date_str parameter needed - decode anytime! - - NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct'). - With 'auto' (default), tries LSB first then DCT. - - Note: Extraction works regardless of whether the image was created with - color mode or grayscale mode - both use the same Y channel for data. """ # Validate mode if request.embed_mode == "dct" and not has_dct_support(): raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") + # Resolve channel key + resolved_channel_key = _resolve_channel_key(request.channel_key) + try: stego = base64.b64decode(request.stego_image_base64) ref_photo = base64.b64decode(request.reference_photo_base64) rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None - # v3.2.0: No date_str parameter + # v4.0.0: Include channel_key result = decode( stego_image=stego, reference_photo=ref_photo, - passphrase=request.passphrase, # v3.2.0: Renamed from day_phrase + passphrase=request.passphrase, pin=request.pin, rsa_key_data=rsa_key, rsa_password=request.rsa_password, embed_mode=request.embed_mode, + channel_key=resolved_channel_key, ) if result.is_file: @@ -817,6 +1091,10 @@ async def api_decode(request: DecodeRequest): ) except DecryptionError as e: + # Provide helpful error message for channel key issues + error_msg = str(e) + if 'channel key' in error_msg.lower(): + raise HTTPException(401, error_msg) raise HTTPException(401, "Decryption failed. Check credentials.") except StegasooError as e: raise HTTPException(400, str(e)) @@ -839,9 +1117,9 @@ async def api_encode_multipart( rsa_key: Optional[UploadFile] = File(None), rsa_key_qr: Optional[UploadFile] = File(None), rsa_password: str = Form(""), - # date_str removed in v3.2.0 + # Channel key (v4.0.0) + channel_key: str = Form("auto", description="Channel key: 'auto'=server config, 'none'=public, 'XXXX-...'=explicit"), embed_mode: str = Form("lsb"), - # NEW in v3.0.1 dct_output_format: str = Form("png"), dct_color_mode: str = Form("grayscale"), ): @@ -852,10 +1130,9 @@ async def api_encode_multipart( RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image). Returns the stego image directly with metadata headers. + v4.0.0: Added channel_key parameter for deployment isolation. + Use 'auto' for server config, 'none' for public mode. v3.2.0: No date_str parameter needed - encode anytime! - - NEW in v3.0: Supports embed_mode parameter ('lsb' or 'dct'). - NEW in v3.0.1: Supports dct_output_format ('png' or 'jpeg') and dct_color_mode ('grayscale' or 'color'). """ # Validate mode if embed_mode not in ("lsb", "dct"): @@ -869,6 +1146,15 @@ async def api_encode_multipart( if dct_color_mode not in ("grayscale", "color"): raise HTTPException(400, "dct_color_mode must be 'grayscale' or 'color'") + # Resolve channel key (v4.0.0) + # Form data: "auto" = use server config, "none" = public, otherwise explicit key + if channel_key.lower() == "auto": + resolved_channel_key = None # Auto mode + elif channel_key.lower() == "none": + resolved_channel_key = "" # Public mode + else: + resolved_channel_key = _resolve_channel_key(channel_key) + try: ref_data = await reference_photo.read() carrier_data = await carrier.read() @@ -911,17 +1197,17 @@ async def api_encode_multipart( # Get DCT parameters dct_params = _get_dct_params(embed_mode, dct_output_format, dct_color_mode) - # v3.2.0: No date_str parameter + # v4.0.0: Include channel_key result = encode( message=payload, reference_photo=ref_data, carrier_image=carrier_data, - passphrase=passphrase, # v3.2.0: Renamed from day_phrase + passphrase=passphrase, pin=pin, rsa_key_data=rsa_key_data, rsa_password=effective_password, - # date_str removed in v3.2.0 embed_mode=embed_mode, + channel_key=resolved_channel_key, **dct_params, ) @@ -929,17 +1215,26 @@ async def api_encode_multipart( embed_mode, dct_output_format, dct_color_mode ) + # Get channel info for headers + channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key) + + headers = { + "Content-Disposition": f"attachment; filename={result.filename}", + "X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}", + "X-Stegasoo-Embed-Mode": embed_mode, + "X-Stegasoo-Output-Format": output_format, + "X-Stegasoo-Color-Mode": color_mode, + "X-Stegasoo-Channel-Mode": channel_mode, + "X-Stegasoo-Version": __version__, + } + + if channel_fingerprint: + headers["X-Stegasoo-Channel-Fingerprint"] = channel_fingerprint + return Response( content=result.stego_image, media_type=mime_type, - headers={ - "Content-Disposition": f"attachment; filename={result.filename}", - "X-Stegasoo-Capacity-Percent": f"{result.capacity_percent:.1f}", - "X-Stegasoo-Embed-Mode": embed_mode, - "X-Stegasoo-Output-Format": output_format, - "X-Stegasoo-Color-Mode": color_mode, - "X-Stegasoo-Version": __version__, - } + headers=headers, ) except CapacityError as e: @@ -961,6 +1256,8 @@ async def api_decode_multipart( rsa_key: Optional[UploadFile] = File(None), rsa_key_qr: Optional[UploadFile] = File(None), rsa_password: str = Form(""), + # Channel key (v4.0.0) + channel_key: str = Form("auto", description="Channel key: 'auto'=server config, 'none'=public, 'XXXX-...'=explicit"), embed_mode: str = Form("auto"), ): """ @@ -969,11 +1266,9 @@ async def api_decode_multipart( RSA key can be provided as 'rsa_key' (.pem file) or 'rsa_key_qr' (QR code image). Returns JSON with payload_type indicating text or file. + v4.0.0: Added channel_key parameter - must match what was used for encoding. + Use 'auto' for server config, 'none' for public mode. v3.2.0: No date_str parameter needed - decode anytime! - - NEW in v3.0: Supports embed_mode parameter ('auto', 'lsb', or 'dct'). - - Note: Extraction works the same regardless of color mode used during encoding. """ # Validate mode if embed_mode not in ("auto", "lsb", "dct"): @@ -981,6 +1276,14 @@ async def api_decode_multipart( if embed_mode == "dct" and not has_dct_support(): raise HTTPException(400, "DCT mode requires scipy. Install with: pip install scipy") + # Resolve channel key (v4.0.0) + if channel_key.lower() == "auto": + resolved_channel_key = None # Auto mode + elif channel_key.lower() == "none": + resolved_channel_key = "" # Public mode + else: + resolved_channel_key = _resolve_channel_key(channel_key) + try: ref_data = await reference_photo.read() stego_data = await stego_image.read() @@ -1007,15 +1310,16 @@ async def api_decode_multipart( # QR code keys are never password-protected effective_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None) - # v3.2.0: No date_str parameter + # v4.0.0: Include channel_key result = decode( stego_image=stego_data, reference_photo=ref_data, - passphrase=passphrase, # v3.2.0: Renamed from day_phrase + passphrase=passphrase, pin=pin, rsa_key_data=rsa_key_data, rsa_password=effective_password, embed_mode=embed_mode, + channel_key=resolved_channel_key, ) if result.is_file: @@ -1031,7 +1335,10 @@ async def api_decode_multipart( message=result.message ) - except DecryptionError: + except DecryptionError as e: + error_msg = str(e) + if 'channel key' in error_msg.lower(): + raise HTTPException(401, error_msg) raise HTTPException(401, "Decryption failed. Check credentials.") except StegasooError as e: raise HTTPException(400, str(e)) @@ -1053,7 +1360,7 @@ async def api_image_info( """ Get information about an image's capacity. - NEW in v3.0: Optionally includes capacity for both LSB and DCT modes. + Optionally includes capacity for both LSB and DCT modes. """ try: image_data = await image.read() @@ -1072,7 +1379,6 @@ async def api_image_info( capacity_kb=capacity // 1024 ) - # NEW in v3.0 - include mode comparison if include_modes: comparison = compare_modes(image_data) response.modes = { diff --git a/frontends/cli/main.py b/frontends/cli/main.py index 4f08fa2..875ea13 100644 --- a/frontends/cli/main.py +++ b/frontends/cli/main.py @@ -1,6 +1,11 @@ #!/usr/bin/env python3 """ -Stegasoo CLI - Command-line interface for steganography operations (v3.2.0). +Stegasoo CLI - Command-line interface for steganography operations (v4.0.0). + +CHANGES in v4.0.0: +- Added channel key support for deployment/group isolation +- Messages encoded with a channel key can only be decoded with the same key +- New `channel` command group for key management CHANGES in v3.2.0: - Removed date dependency from all operations @@ -16,6 +21,7 @@ Usage: stegasoo info [OPTIONS] stegasoo compare [OPTIONS] stegasoo modes [OPTIONS] + stegasoo channel [SUBCOMMAND] """ import sys @@ -64,6 +70,18 @@ from stegasoo import ( # Models FilePayload, + + # Channel key functions (v4.0.0) + generate_channel_key, + get_channel_key, + set_channel_key, + clear_channel_key, + has_channel_key, + get_channel_status, + validate_channel_key, + format_channel_key, + get_active_channel_key, + get_channel_fingerprint, ) # Import constants - try main module first, then constants submodule @@ -136,13 +154,13 @@ def cli(): - Reference photo (something you have) - Passphrase (something you know) - Static PIN or RSA key (additional security) + - Channel key (deployment/group isolation) [v4.0.0] \b - Version 3.2.0 Changes: - - No more date parameters - encode/decode anytime! - - Simplified passphrase (no daily rotation) - - Default passphrase increased to 4 words - - True asynchronous communications + Version 4.0.0 Changes: + - Channel key support for group/deployment isolation + - Messages encoded with a channel key require the same key to decode + - New `stegasoo channel` command for key management \b Embedding Modes: @@ -157,6 +175,60 @@ def cli(): pass +# ============================================================================ +# CHANNEL KEY HELPERS +# ============================================================================ + +def resolve_channel_key_option(channel: Optional[str], channel_file: Optional[str], + no_channel: bool) -> Optional[str]: + """ + Resolve channel key from CLI options. + + Returns: + None: Use server-configured key (auto mode) + "": Public mode (no channel key) + str: Explicit channel key + """ + if no_channel: + return "" # Public mode + + if channel_file: + # Load from file + path = Path(channel_file) + if not path.exists(): + raise click.ClickException(f"Channel key file not found: {channel_file}") + key = path.read_text().strip() + if not validate_channel_key(key): + raise click.ClickException(f"Invalid channel key format in file: {channel_file}") + return key + + if channel: + if channel.lower() == 'auto': + return None # Use server config + # Explicit key provided + if not validate_channel_key(channel): + raise click.ClickException( + f"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n" + f"Generate a new key with: stegasoo channel generate" + ) + return channel + + # Default: use server-configured key (auto mode) + return None + + +def format_channel_status_line(quiet: bool = False) -> Optional[str]: + """Get a one-line status for channel key configuration.""" + if quiet: + return None + + status = get_channel_status() + if status['mode'] == 'public': + return None + + return f"Channel: {status['fingerprint']} ({status['source']})" + + # ============================================================================ # GENERATE COMMAND # ============================================================================ @@ -229,7 +301,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): # Pretty output click.echo() click.secho("=" * 60, fg='cyan') - click.secho(" STEGASOO CREDENTIALS (v3.2.0)", fg='cyan', bold=True) + click.secho(" STEGASOO CREDENTIALS (v4.0.0)", fg='cyan', bold=True) click.secho("=" * 60, fg='cyan') click.echo() @@ -269,13 +341,278 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): click.secho(f" + photo entropy: 80-256 bits", dim=True) click.echo() - click.secho("✓ v3.2.0: Use this passphrase anytime - no date needed!", fg='cyan') + # Show channel key status + if has_channel_key(): + status = get_channel_status() + click.secho("─── CHANNEL KEY ───", fg='magenta') + click.echo(f" Status: Private mode") + click.echo(f" Fingerprint: {status['fingerprint']}") + click.secho(f" (configured via {status['source']})", dim=True) + click.echo() + + click.secho("✓ v4.0.0: Use this passphrase anytime - no date needed!", fg='cyan') click.echo() except Exception as e: raise click.ClickException(str(e)) +# ============================================================================ +# CHANNEL COMMAND GROUP (v4.0.0) +# ============================================================================ + +@cli.group() +def channel(): + """ + Manage channel keys for deployment/group isolation. + + Channel keys allow different deployments or groups to use Stegasoo + without being able to read each other's messages, even with identical + credentials. + + \b + Key Storage (checked in order): + 1. Environment variable: STEGASOO_CHANNEL_KEY + 2. Project config: ./config/channel.key + 3. User config: ~/.stegasoo/channel.key + + \b + Subcommands: + generate Create a new channel key + show Display current channel key status + set Save a channel key to config file + clear Remove channel key from config + + \b + Examples: + stegasoo channel generate + stegasoo channel show + stegasoo channel set XXXX-XXXX-... + stegasoo channel clear + """ + pass + + +@channel.command('generate') +@click.option('--save', '-s', is_flag=True, help='Save to user config (~/.stegasoo/channel.key)') +@click.option('--save-project', is_flag=True, help='Save to project config (./config/channel.key)') +@click.option('--env', '-e', is_flag=True, help='Output as environment variable export') +@click.option('--quiet', '-q', is_flag=True, help='Output only the key') +def channel_generate(save, save_project, env, quiet): + """ + Generate a new channel key. + + Creates a cryptographically secure 256-bit channel key in the format: + XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX + + \b + Examples: + # Just display a new key + stegasoo channel generate + + # Save to user config + stegasoo channel generate --save + + # Output for .env file + stegasoo channel generate --env >> .env + + # For scripts + KEY=$(stegasoo channel generate -q) + """ + key = generate_channel_key() + + if save and save_project: + raise click.UsageError("Cannot use both --save and --save-project") + + if save: + set_channel_key(key, location='user') + if not quiet: + click.secho("✓ Channel key saved to ~/.stegasoo/channel.key", fg='green') + click.echo() + + if save_project: + set_channel_key(key, location='project') + if not quiet: + click.secho("✓ Channel key saved to ./config/channel.key", fg='green') + click.echo() + + if env: + click.echo(f"STEGASOO_CHANNEL_KEY={key}") + elif quiet: + click.echo(key) + else: + click.echo() + click.secho("─── NEW CHANNEL KEY ───", fg='cyan', bold=True) + click.echo() + click.secho(f" {key}", fg='bright_yellow', bold=True) + click.echo() + + fingerprint = f"{key[:4]}-••••-••••-••••-••••-••••-••••-{key[-4:]}" + click.echo(f" Fingerprint: {fingerprint}") + click.echo() + + click.secho("Usage:", dim=True) + click.echo(" # Environment variable (recommended)") + click.echo(f" export STEGASOO_CHANNEL_KEY={key}") + click.echo() + click.echo(" # Or save to config") + click.echo(" stegasoo channel generate --save") + click.echo() + click.echo(" # Or add to .env file") + click.echo(" stegasoo channel generate --env >> .env") + click.echo() + + +@channel.command('show') +@click.option('--reveal', '-r', is_flag=True, help='Show full key (not just fingerprint)') +@click.option('--json', 'as_json', is_flag=True, help='Output as JSON') +def channel_show(reveal, as_json): + """ + Display current channel key status. + + Shows whether a channel key is configured and where it comes from. + By default shows only fingerprint; use --reveal to see full key. + + \b + Examples: + stegasoo channel show + stegasoo channel show --reveal + stegasoo channel show --json + """ + status = get_channel_status() + + if as_json: + import json + output = { + 'mode': status['mode'], + 'configured': status['configured'], + 'fingerprint': status.get('fingerprint'), + 'source': status.get('source'), + } + if reveal and status['configured']: + output['key'] = status.get('key') + click.echo(json.dumps(output, indent=2)) + return + + click.echo() + click.secho("─── CHANNEL KEY STATUS ───", fg='cyan', bold=True) + click.echo() + + if status['mode'] == 'public': + click.secho(" Mode: PUBLIC", fg='yellow', bold=True) + click.echo(" No channel key configured.") + click.echo() + click.secho(" Messages can be read by any Stegasoo installation", dim=True) + click.secho(" with matching credentials.", dim=True) + else: + click.secho(" Mode: PRIVATE", fg='green', bold=True) + click.echo(f" Fingerprint: {status['fingerprint']}") + click.echo(f" Source: {status['source']}") + + if reveal: + click.echo() + click.secho(f" Full key: {status['key']}", fg='bright_yellow') + + click.echo() + click.secho(" Messages require this channel key to decode.", dim=True) + + click.echo() + + +@channel.command('set') +@click.argument('key', required=False) +@click.option('--file', '-f', 'key_file', type=click.Path(exists=True), help='Read key from file') +@click.option('--project', '-p', is_flag=True, help='Save to project config instead of user config') +def channel_set(key, key_file, project): + """ + Save a channel key to config file. + + Saves to user config (~/.stegasoo/channel.key) by default, + or project config (./config/channel.key) with --project. + + \b + Examples: + stegasoo channel set XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX + stegasoo channel set --file channel.key + stegasoo channel set XXXX-... --project + """ + if not key and not key_file: + raise click.UsageError("Must provide KEY argument or --file option") + + if key and key_file: + raise click.UsageError("Cannot use both KEY argument and --file option") + + if key_file: + key = Path(key_file).read_text().strip() + + if not validate_channel_key(key): + raise click.ClickException( + f"Invalid channel key format.\n" + f"Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n" + f"Generate a new key with: stegasoo channel generate" + ) + + location = 'project' if project else 'user' + set_channel_key(key, location=location) + + status = get_channel_status() + click.secho(f"✓ Channel key saved", fg='green') + click.echo(f" Location: {status['source']}") + click.echo(f" Fingerprint: {status['fingerprint']}") + + +@channel.command('clear') +@click.option('--project', '-p', is_flag=True, help='Clear project config instead of user config') +@click.option('--all', 'clear_all', is_flag=True, help='Clear both user and project configs') +@click.option('--force', '-f', is_flag=True, help='Skip confirmation') +def channel_clear(project, clear_all, force): + """ + Remove channel key from config. + + Clears user config by default. Use --project for project config, + or --all to clear both. + + Note: This does not affect environment variables. + + \b + Examples: + stegasoo channel clear + stegasoo channel clear --project + stegasoo channel clear --all + """ + if not force: + if clear_all: + msg = "Clear channel key from both user and project configs?" + elif project: + msg = "Clear channel key from project config (./config/channel.key)?" + else: + msg = "Clear channel key from user config (~/.stegasoo/channel.key)?" + + if not click.confirm(msg): + click.echo("Cancelled.") + return + + if clear_all: + clear_channel_key(location='user') + clear_channel_key(location='project') + click.secho("✓ Cleared channel key from user and project configs", fg='green') + elif project: + clear_channel_key(location='project') + click.secho("✓ Cleared channel key from project config", fg='green') + else: + clear_channel_key(location='user') + click.secho("✓ Cleared channel key from user config", fg='green') + + # Show current status + status = get_channel_status() + if status['configured']: + click.echo() + click.secho(f"Note: Channel key still active from {status['source']}", fg='yellow') + click.echo(f" Fingerprint: {status['fingerprint']}") + else: + click.echo(" Mode is now: PUBLIC") + + # ============================================================================ # ENCODE COMMAND # ============================================================================ @@ -291,6 +628,9 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): @click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)') @click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image') @click.option('--key-password', help='RSA key password (for encrypted .pem files)') +@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)') +@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file') +@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)') @click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)') @click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb', help='Embedding mode: lsb (default, color) or dct (requires scipy)') @@ -300,18 +640,23 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): help='DCT color mode: grayscale (default) or color (preserves original colors)') @click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors') def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, key, key_qr, - key_password, output, embed_mode, dct_output_format, dct_color_mode, quiet): + key_password, channel_key, channel_file, no_channel, output, embed_mode, + dct_output_format, dct_color_mode, quiet): """ Encode a secret message or file into an image. Requires a reference photo, carrier image, and passphrase. Must provide either --pin or --key/--key-qr (or both). + v4.0.0: Channel key support for deployment isolation. v3.2.0: No --date parameter needed! Encode and decode anytime. - For text messages, use -m or -f or pipe via stdin. - For binary files, use -e/--embed-file. - RSA key can be provided as a .pem file (--key) or QR code image (--key-qr). + \b + Channel Key Options: + (no option) Use server-configured key (auto mode) + --channel KEY Use explicit channel key + --channel-file F Read channel key from file + --no-channel Force public mode (no isolation) \b Embedding Modes: @@ -324,25 +669,17 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, - Lower capacity (~75 KB/megapixel) - Better resistance to visual analysis - \b - DCT Options: - --dct-format png Lossless output (default) - --dct-format jpeg Smaller file, more natural appearance - - --dct-color grayscale Convert to grayscale (default, traditional) - --dct-color color Preserve original colors (experimental) - \b Examples: - # Text message with PIN (LSB mode, default) - stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" --pin 123456 -m "secret" + # Text message with PIN (auto channel key) + stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret" - # DCT mode - grayscale PNG (traditional) - stegasoo encode -r photo.jpg -c meme.png -p "secure words here now" --pin 123456 -m "secret" --mode dct + # Explicit channel key + stegasoo encode -r photo.jpg -c meme.png -p "words here" --pin 123456 -m "msg" \\ + --channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 - # DCT mode - color JPEG - stegasoo encode -r photo.jpg -c meme.png -p "my strong passphrase" --pin 123456 -m "secret" \\ - --mode dct --dct-color color --dct-format jpeg + # Public mode (no channel key) + stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "msg" --no-channel """ # Check DCT mode availability if embed_mode == 'dct' and not has_dct_support(): @@ -356,6 +693,12 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, if not quiet: click.secho("Note: --dct-format and --dct-color only apply to DCT mode", fg='yellow', dim=True) + # Resolve channel key + try: + resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel) + except click.ClickException: + raise + # Determine what to encode payload = None @@ -431,8 +774,18 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, if embed_mode == 'dct': mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})" click.echo(f"Mode: {mode_desc} ({fit_check['usage_percent']:.1f}% capacity)") + + # Show channel status + channel_status = format_channel_status_line() + if resolved_channel_key == "": + click.echo("Channel: PUBLIC (no isolation)") + elif resolved_channel_key: + fingerprint = f"{resolved_channel_key[:4]}-••••-...-{resolved_channel_key[-4:]}" + click.echo(f"Channel: {fingerprint} (explicit)") + elif channel_status: + click.echo(channel_status) - # v3.2.0: No date_str parameter + # v4.0.0: Include channel_key parameter result = encode( message=payload, reference_photo=ref_photo, @@ -444,6 +797,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, embed_mode=embed_mode, dct_output_format=dct_output_format, dct_color_mode=dct_color_mode, + channel_key=resolved_channel_key, ) # Determine output path @@ -485,43 +839,43 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, @click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)') @click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image') @click.option('--key-password', help='RSA key password (for encrypted .pem files)') +@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)') +@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file') +@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)') @click.option('--output', '-o', type=click.Path(), help='Save decoded content to file') @click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto', help='Extraction mode: auto (default), lsb, or dct') @click.option('--quiet', '-q', is_flag=True, help='Output only the content (for text) or suppress messages (for files)') @click.option('--force', is_flag=True, help='Overwrite existing output file') -def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, embed_mode, quiet, force): +def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, channel_key, channel_file, + no_channel, output, embed_mode, quiet, force): """ Decode a secret message or file from a stego image. Must use the same credentials that were used for encoding. Automatically detects whether content is text or a file. - RSA key can be provided as a .pem file (--key) or QR code image (--key-qr). + v4.0.0: Channel key support - must match what was used for encoding. v3.2.0: No --date parameter needed! Just use your passphrase. - Note: Extraction works the same regardless of whether the image was - created with color mode or grayscale mode - both use the same Y channel. - \b - Extraction Modes: - --mode auto Auto-detect (default) - tries LSB first, then DCT - --mode lsb Only try LSB extraction - --mode dct Only try DCT extraction (requires scipy) + Channel Key Options: + (no option) Use server-configured key (auto mode) + --channel KEY Use explicit channel key + --channel-file F Read channel key from file + --no-channel Force public mode (for images encoded without channel key) \b Examples: - # Decode with PIN (auto-detect mode) - stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" --pin 123456 + # Decode with auto channel key + stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456 - # Explicitly specify DCT mode - stegasoo decode -r photo.jpg -s stego.png -p "my passphrase here" --pin 123456 --mode dct + # Decode with explicit channel key + stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 \\ + --channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 - # Decode with RSA key file - stegasoo decode -r photo.jpg -s stego.png -p "strong words" -k mykey.pem - - # Save output to file - stegasoo decode -r photo.jpg -s stego.png -p "passphrase" --pin 123456 -o output.txt + # Decode public image (no channel key was used) + stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 --no-channel """ # Check DCT mode availability if embed_mode == 'dct' and not has_dct_support(): @@ -529,6 +883,12 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e "DCT mode requires scipy. Install with: pip install scipy" ) + # Resolve channel key + try: + resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel) + except click.ClickException: + raise + # Load key if provided (from .pem file or QR code image) rsa_key_data = None rsa_key_from_qr = False @@ -563,7 +923,7 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e ref_photo = Path(ref).read_bytes() stego_image = Path(stego).read_bytes() - # v3.2.0: No date_str parameter + # v4.0.0: Include channel_key parameter result = decode( stego_image=stego_image, reference_photo=ref_photo, @@ -572,6 +932,7 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e rsa_key_data=rsa_key_data, rsa_password=effective_key_password, embed_mode=embed_mode, + channel_key=resolved_channel_key, ) if result.is_file: @@ -612,6 +973,10 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e click.echo(result.message) except (DecryptionError, ExtractionError) as e: + # Provide helpful hints for channel key mismatches + error_msg = str(e) + if 'channel key' in error_msg.lower(): + raise click.ClickException(error_msg) raise click.ClickException(f"Decryption failed: {e}") except StegasooError as e: raise click.ClickException(str(e)) @@ -631,23 +996,29 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e @click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)') @click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image') @click.option('--key-password', help='RSA key password (for encrypted .pem files)') +@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)') +@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file') +@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)') @click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto', help='Extraction mode: auto (default), lsb, or dct') @click.option('--json', 'as_json', is_flag=True, help='Output as JSON') -def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, as_json): +def verify(ref, stego, passphrase, pin, key, key_qr, key_password, channel_key, channel_file, + no_channel, embed_mode, as_json): """ Verify that a stego image can be decoded without extracting the message. Quick check to validate credentials are correct and data is intact. Does NOT output the actual message content. + v4.0.0: Also verifies channel key matches. + \b Examples: stegasoo verify -r photo.jpg -s stego.png -p "my passphrase" --pin 123456 stegasoo verify -r photo.jpg -s stego.png -p "words here" -k mykey.pem --json - stegasoo verify -r photo.jpg -s stego.png -p "test phrase" --pin 123456 --mode dct + stegasoo verify -r photo.jpg -s stego.png -p "test phrase" --pin 123456 --no-channel """ # Check DCT mode availability if embed_mode == 'dct' and not has_dct_support(): @@ -655,6 +1026,12 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a "DCT mode requires scipy. Install with: pip install scipy" ) + # Resolve channel key + try: + resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel) + except click.ClickException: + raise + # Load key if provided rsa_key_data = None rsa_key_from_qr = False @@ -685,7 +1062,7 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a ref_photo = Path(ref).read_bytes() stego_image = Path(stego).read_bytes() - # Attempt to decode + # Attempt to decode (v4.0.0: with channel_key) result = decode( stego_image=stego_image, reference_photo=ref_photo, @@ -694,51 +1071,44 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a rsa_key_data=rsa_key_data, rsa_password=effective_key_password, embed_mode=embed_mode, + channel_key=resolved_channel_key, ) # Calculate payload size if result.is_file: - payload_size = len(result.file_data) if result.file_data else 0 - payload_type = "file" - payload_desc = result.filename or "unnamed file" - if result.mime_type: - payload_desc += f" ({result.mime_type})" + payload_size = len(result.file_data) + content_type = result.mime_type or 'file' else: - payload_size = len(result.message.encode('utf-8')) if result.message else 0 - payload_type = "text" - payload_desc = f"{payload_size} bytes" + payload_size = len(result.message.encode('utf-8')) + content_type = 'text' if as_json: import json - output_data = { - "valid": True, - "stego_file": stego, - "payload_type": payload_type, - "payload_size": payload_size, + output = { + 'valid': True, + 'content_type': content_type, + 'payload_size': payload_size, + 'filename': result.filename if result.is_file else None, } - if result.is_file: - output_data["filename"] = result.filename - output_data["mime_type"] = result.mime_type - click.echo(json.dumps(output_data, indent=2)) + click.echo(json.dumps(output, indent=2)) else: - click.secho("✓ Valid stego image", fg='green', bold=True) - click.echo(f" Payload: {payload_type} ({payload_desc})") - click.echo(f" Size: {payload_size:,} bytes") + click.secho("✓ Verification successful!", fg='green') + click.echo(f" Content type: {content_type}") + click.echo(f" Payload size: {payload_size:,} bytes") + if result.is_file and result.filename: + click.echo(f" Filename: {result.filename}") except (DecryptionError, ExtractionError) as e: if as_json: import json - output_data = { - "valid": False, - "stego_file": stego, - "error": str(e), + output = { + 'valid': False, + 'error': str(e), } - click.echo(json.dumps(output_data, indent=2)) + click.echo(json.dumps(output, indent=2)) sys.exit(1) else: - click.secho("✗ Verification failed", fg='red', bold=True) - click.echo(f" Error: {e}") - sys.exit(1) + raise click.ClickException(f"Verification failed: {e}") except StegasooError as e: raise click.ClickException(str(e)) except Exception as e: @@ -754,64 +1124,38 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a @click.option('--json', 'as_json', is_flag=True, help='Output as JSON') def info(image, as_json): """ - Show information about an image. + Show information about an image file. - Displays dimensions, capacity for both LSB and DCT modes. + Displays dimensions, format, capacity estimates for different modes, + and whether the image appears suitable as a carrier. + + \b + Examples: + stegasoo info photo.png + stegasoo info carrier.jpg --json """ try: image_data = Path(image).read_bytes() - - result = validate_image(image_data, check_size=False) - if not result.is_valid: - raise click.ClickException(result.error_message) - - # Get capacity comparison - comparison = compare_modes(image_data) + img_info = get_image_info(image_data) if as_json: import json - output_data = { - "file": image, - "width": result.details['width'], - "height": result.details['height'], - "pixels": result.details['pixels'], - "mode": result.details['mode'], - "format": result.details['format'], - "capacity": { - "lsb": { - "bytes": comparison['lsb']['capacity_bytes'], - "kb": round(comparison['lsb']['capacity_kb'], 1), - }, - "dct": { - "bytes": comparison['dct']['capacity_bytes'], - "kb": round(comparison['dct']['capacity_kb'], 1), - "available": comparison['dct']['available'], - "ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1), - "output_formats": ["png", "jpeg"], - "color_modes": ["grayscale", "color"], - }, - }, - } - click.echo(json.dumps(output_data, indent=2)) + click.echo(json.dumps(img_info, indent=2)) return click.echo() - click.secho(f"Image: {image}", bold=True) - click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}") - click.echo(f" Pixels: {result.details['pixels']:,}") - click.echo(f" Mode: {result.details['mode']}") - click.echo(f" Format: {result.details['format']}") - click.echo() + click.secho(f"=== Image Info: {image} ===", fg='cyan', bold=True) + click.echo(f" Format: {img_info.get('format', 'Unknown')}") + click.echo(f" Dimensions: {img_info.get('width', '?')} × {img_info.get('height', '?')}") + click.echo(f" Mode: {img_info.get('mode', '?')}") + click.echo(f" Size: {len(image_data):,} bytes") - click.secho(" Capacity:", bold=True) - click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)") - - dct_status = "✓" if comparison['dct']['available'] else "✗ (scipy not installed)" - click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}") - click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB") - - if comparison['dct']['available']: - click.secho(" DCT options: grayscale/color, png/jpeg", dim=True) + if 'lsb_capacity' in img_info: + click.echo() + click.secho(" Capacity Estimates:", fg='green') + click.echo(f" LSB mode: {img_info['lsb_capacity']:,} bytes") + if 'dct_capacity' in img_info: + click.echo(f" DCT mode: {img_info['dct_capacity']:,} bytes") click.echo() @@ -825,24 +1169,32 @@ def info(image, as_json): @cli.command() @click.argument('image', type=click.Path(exists=True)) -@click.option('--payload-size', '-s', type=int, help='Check if specific payload size fits') +@click.option('--payload', '-p', type=click.Path(exists=True), help='Check if this file would fit') +@click.option('--size', '-s', type=int, help='Check if this many bytes would fit') @click.option('--json', 'as_json', is_flag=True, help='Output as JSON') -def compare(image, payload_size, as_json): +def compare(image, payload, size, as_json): """ - Compare LSB and DCT embedding modes for an image. + Compare embedding mode capacities for an image. - Shows capacity for each mode and recommends which to use. - Optionally checks if a specific payload size would fit. + Shows LSB vs DCT capacity and helps choose the right mode. + Optionally checks if a specific payload would fit. \b Examples: stegasoo compare carrier.png - stegasoo compare carrier.png --payload-size 50000 - stegasoo compare carrier.png --json + stegasoo compare carrier.png --payload secret.pdf + stegasoo compare carrier.png --size 50000 """ try: image_data = Path(image).read_bytes() + # Get payload size if provided + payload_size = None + if payload: + payload_size = len(Path(payload).read_bytes()) + elif size: + payload_size = size + comparison = compare_modes(image_data) if as_json: @@ -1004,7 +1356,7 @@ def modes(): Displays which modes are available and their characteristics. """ click.echo() - click.secho("=== Stegasoo Embedding Modes (v3.2.0) ===", fg='cyan', bold=True) + click.secho("=== Stegasoo Embedding Modes (v4.0.0) ===", fg='cyan', bold=True) click.echo() # LSB Mode @@ -1039,24 +1391,41 @@ def modes(): click.echo(" --dct-color color Preserves original colors") click.echo() - # v3.2.0 Note - click.secho(" v3.2.0 Changes:", fg='cyan', bold=True) - click.echo(" ✓ No date parameters needed") - click.echo(" ✓ Single passphrase (no daily rotation)") - click.echo(" ✓ Default passphrase increased to 4 words") - click.echo(" ✓ True asynchronous communications") + # Channel Key Status (v4.0.0) + click.secho(" Channel Key (v4.0.0)", fg='cyan', bold=True) + status = get_channel_status() + if status['mode'] == 'public': + click.echo(" Status: PUBLIC (no key configured)") + click.echo(" Effect: Messages readable by any installation") + else: + click.echo(" Status: PRIVATE") + click.echo(f" Fingerprint: {status['fingerprint']}") + click.echo(f" Source: {status['source']}") + click.echo(" Effect: Messages isolated to this channel") + click.echo() + click.echo(" CLI flags:") + click.echo(" --channel KEY Use explicit channel key") + click.echo(" --channel-file F Read key from file") + click.echo(" --no-channel Force public mode") + click.echo() + + # v4.0.0 Changes + click.secho(" v4.0.0 Changes:", fg='cyan', bold=True) + click.echo(" ✓ Channel key support for deployment isolation") + click.echo(" ✓ New `stegasoo channel` command group") + click.echo(" ✓ Messages encoded with channel key require same key to decode") click.echo() # Examples click.secho(" Examples:", dim=True) - click.echo(" # Traditional DCT (grayscale PNG)") - click.echo(" stegasoo encode ... --mode dct") + click.echo(" # Generate channel key") + click.echo(" stegasoo channel generate --save") click.echo() - click.echo(" # Color-preserving DCT with JPEG output") - click.echo(" stegasoo encode ... --mode dct --dct-color color --dct-format jpeg") + click.echo(" # Encode with channel isolation") + click.echo(" stegasoo encode ... --channel XXXX-XXXX-...") click.echo() - click.echo(" # Compare modes for an image") - click.echo(" stegasoo compare carrier.png") + click.echo(" # Decode public message (no channel key)") + click.echo(" stegasoo decode ... --no-channel") click.echo() diff --git a/frontends/cli/main.py_old b/frontends/cli/main.py_old new file mode 100644 index 0000000..4f08fa2 --- /dev/null +++ b/frontends/cli/main.py_old @@ -0,0 +1,1073 @@ +#!/usr/bin/env python3 +""" +Stegasoo CLI - Command-line interface for steganography operations (v3.2.0). + +CHANGES in v3.2.0: +- Removed date dependency from all operations +- Renamed day_phrase → passphrase +- No longer need to specify or remember encoding dates +- Default passphrase length increased to 4 words + +Usage: + stegasoo generate [OPTIONS] + stegasoo encode [OPTIONS] + stegasoo decode [OPTIONS] + stegasoo verify [OPTIONS] + stegasoo info [OPTIONS] + stegasoo compare [OPTIONS] + stegasoo modes [OPTIONS] +""" + +import sys +from pathlib import Path +from typing import Optional + +import click + +# Add parent to path for development +sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src')) + +import stegasoo +from stegasoo import ( + # Core operations + encode, decode, + + # Credential generation + generate_credentials, + generate_passphrase, + generate_pin, + export_rsa_key_pem, + load_rsa_key, + + # Validation + validate_image, + + # Image utilities + get_image_info, + compare_capacity, + + # Steganography functions + has_dct_support, + compare_modes, + will_fit_by_mode, + + # Utilities + generate_filename, + + # Version + __version__, + + # Exceptions + StegasooError, + DecryptionError, + ExtractionError, + + # Models + FilePayload, +) + +# Import constants - try main module first, then constants submodule +try: + from stegasoo import ( + EMBED_MODE_LSB, + EMBED_MODE_DCT, + EMBED_MODE_AUTO, + ) +except ImportError: + from stegasoo.constants import ( + EMBED_MODE_LSB, + EMBED_MODE_DCT, + EMBED_MODE_AUTO, + ) + +# Import constants that may not be in main __init__ +try: + from stegasoo.constants import ( + DEFAULT_PASSPHRASE_WORDS, + DEFAULT_PIN_LENGTH, + MIN_PIN_LENGTH, + MAX_PIN_LENGTH, + ) +except ImportError: + # Fallback defaults if constants not available + DEFAULT_PASSPHRASE_WORDS = 4 + DEFAULT_PIN_LENGTH = 6 + MIN_PIN_LENGTH = 6 + MAX_PIN_LENGTH = 9 + +# Optional: strip_image_metadata from utils +try: + from stegasoo.utils import strip_image_metadata + HAS_STRIP_METADATA = True +except ImportError: + HAS_STRIP_METADATA = False + +# QR Code utilities +try: + from stegasoo.qr_utils import ( + extract_key_from_qr_file, + generate_qr_code, + has_qr_read, has_qr_write, + can_fit_in_qr, needs_compression, + ) + HAS_QR = True +except ImportError: + HAS_QR = False + has_qr_read = lambda: False + has_qr_write = lambda: False + + +# ============================================================================ +# CLI SETUP +# ============================================================================ + +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.version_option(__version__, '-v', '--version') +def cli(): + """ + Stegasoo - Secure steganography with hybrid authentication. + + Hide encrypted messages or files in images using a combination of: + + \b + - Reference photo (something you have) + - Passphrase (something you know) + - Static PIN or RSA key (additional security) + + \b + Version 3.2.0 Changes: + - No more date parameters - encode/decode anytime! + - Simplified passphrase (no daily rotation) + - Default passphrase increased to 4 words + - True asynchronous communications + + \b + Embedding Modes: + - LSB mode (default): Full color output, higher capacity + - DCT mode: Frequency domain, ~20% capacity, better stealth + + \b + DCT Options: + - Color mode: grayscale (default) or color (preserves colors) + - Output format: png (lossless) or jpeg (smaller, natural) + """ + pass + + +# ============================================================================ +# GENERATE COMMAND +# ============================================================================ + +@cli.command() +@click.option('--pin/--no-pin', default=True, help='Generate a PIN (default: yes)') +@click.option('--rsa/--no-rsa', default=False, help='Generate an RSA key') +@click.option('--pin-length', type=click.IntRange(6, 9), default=DEFAULT_PIN_LENGTH, + help=f'PIN length (6-9, default: {DEFAULT_PIN_LENGTH})') +@click.option('--rsa-bits', type=click.Choice(['2048', '3072', '4096']), default='2048', + help='RSA key size') +@click.option('--words', type=click.IntRange(3, 12), default=DEFAULT_PASSPHRASE_WORDS, + help=f'Words per passphrase (default: {DEFAULT_PASSPHRASE_WORDS})') +@click.option('--output', '-o', type=click.Path(), help='Save RSA key to file (requires password)') +@click.option('--password', '-p', help='Password for RSA key file') +@click.option('--json', 'as_json', is_flag=True, help='Output as JSON') +def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): + """ + Generate credentials for encoding/decoding. + + Creates a passphrase and optionally a PIN and/or RSA key. + At least one of --pin or --rsa must be enabled. + + v3.2.0: Single passphrase (no more daily rotation!) + Default increased to 4 words for better security. + + \b + Examples: + stegasoo generate + stegasoo generate --words 5 + stegasoo generate --rsa --rsa-bits 4096 + stegasoo generate --rsa -o mykey.pem -p "secretpassword" + stegasoo generate --no-pin --rsa + """ + if not pin and not rsa: + raise click.UsageError("Must enable at least one of --pin or --rsa") + + if output and not password: + raise click.UsageError("--password is required when saving RSA key to file") + + if password and len(password) < 8: + raise click.UsageError("Password must be at least 8 characters") + + try: + creds = generate_credentials( + use_pin=pin, + use_rsa=rsa, + pin_length=pin_length, + rsa_bits=int(rsa_bits), + passphrase_words=words, # v3.2.0: renamed parameter + rsa_password=password if output else None, + ) + + if as_json: + import json + data = { + 'passphrase': creds.passphrase, + 'pin': creds.pin, + 'rsa_key': creds.rsa_key_pem, + 'entropy': { + 'passphrase': creds.passphrase_entropy, + 'pin': creds.pin_entropy, + 'rsa': creds.rsa_entropy, + 'total': creds.total_entropy, + } + } + click.echo(json.dumps(data, indent=2)) + return + + # Pretty output + click.echo() + click.secho("=" * 60, fg='cyan') + click.secho(" STEGASOO CREDENTIALS (v3.2.0)", fg='cyan', bold=True) + click.secho("=" * 60, fg='cyan') + click.echo() + + click.secho("⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW", fg='yellow', bold=True) + click.secho(" Do not screenshot or save to file!", fg='yellow') + click.echo() + + if creds.pin: + click.secho("─── STATIC PIN ───", fg='green') + click.secho(f" {creds.pin}", fg='bright_yellow', bold=True) + click.echo() + + click.secho("─── PASSPHRASE ───", fg='green') + click.secho(f" {creds.passphrase}", fg='bright_white', bold=True) + click.echo() + + if creds.rsa_key_pem: + click.secho("─── RSA KEY ───", fg='green') + if output: + # Save to file + private_key = load_rsa_key(creds.rsa_key_pem.encode()) + encrypted_pem = export_rsa_key_pem(private_key, password) + Path(output).write_bytes(encrypted_pem) + click.secho(f" Saved to: {output}", fg='bright_white') + click.secho(f" Password: {'*' * len(password)}", dim=True) + else: + click.echo(creds.rsa_key_pem) + click.echo() + + click.secho("─── SECURITY ───", fg='green') + click.echo(f" Passphrase entropy: {creds.passphrase_entropy} bits ({words} words)") + if creds.pin: + click.echo(f" PIN entropy: {creds.pin_entropy} bits") + if creds.rsa_key_pem: + click.echo(f" RSA entropy: {creds.rsa_entropy} bits") + click.echo(f" Combined: {creds.total_entropy} bits") + click.secho(f" + photo entropy: 80-256 bits", dim=True) + click.echo() + + click.secho("✓ v3.2.0: Use this passphrase anytime - no date needed!", fg='cyan') + click.echo() + + except Exception as e: + raise click.ClickException(str(e)) + + +# ============================================================================ +# ENCODE COMMAND +# ============================================================================ + +@cli.command() +@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo') +@click.option('--carrier', '-c', required=True, type=click.Path(exists=True), help='Carrier image') +@click.option('--message', '-m', help='Text message to encode') +@click.option('--message-file', '-f', type=click.Path(exists=True), help='Read text message from file') +@click.option('--embed-file', '-e', type=click.Path(exists=True), help='Embed a file (binary)') +@click.option('--passphrase', '-p', required=True, help='Passphrase') +@click.option('--pin', help='Static PIN') +@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)') +@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image') +@click.option('--key-password', help='RSA key password (for encrypted .pem files)') +@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)') +@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb', + help='Embedding mode: lsb (default, color) or dct (requires scipy)') +@click.option('--dct-format', 'dct_output_format', type=click.Choice(['png', 'jpeg']), default='png', + help='DCT output format: png (lossless, default) or jpeg (smaller)') +@click.option('--dct-color', 'dct_color_mode', type=click.Choice(['grayscale', 'color']), default='grayscale', + help='DCT color mode: grayscale (default) or color (preserves original colors)') +@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors') +def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, key, key_qr, + key_password, output, embed_mode, dct_output_format, dct_color_mode, quiet): + """ + Encode a secret message or file into an image. + + Requires a reference photo, carrier image, and passphrase. + Must provide either --pin or --key/--key-qr (or both). + + v3.2.0: No --date parameter needed! Encode and decode anytime. + + For text messages, use -m or -f or pipe via stdin. + For binary files, use -e/--embed-file. + RSA key can be provided as a .pem file (--key) or QR code image (--key-qr). + + \b + Embedding Modes: + --mode lsb Spatial LSB embedding (default) + - Full color output (PNG/BMP) + - Higher capacity (~375 KB/megapixel) + + --mode dct DCT domain embedding (requires scipy) + - Configurable color/grayscale output + - Lower capacity (~75 KB/megapixel) + - Better resistance to visual analysis + + \b + DCT Options: + --dct-format png Lossless output (default) + --dct-format jpeg Smaller file, more natural appearance + + --dct-color grayscale Convert to grayscale (default, traditional) + --dct-color color Preserve original colors (experimental) + + \b + Examples: + # Text message with PIN (LSB mode, default) + stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" --pin 123456 -m "secret" + + # DCT mode - grayscale PNG (traditional) + stegasoo encode -r photo.jpg -c meme.png -p "secure words here now" --pin 123456 -m "secret" --mode dct + + # DCT mode - color JPEG + stegasoo encode -r photo.jpg -c meme.png -p "my strong passphrase" --pin 123456 -m "secret" \\ + --mode dct --dct-color color --dct-format jpeg + """ + # Check DCT mode availability + if embed_mode == 'dct' and not has_dct_support(): + raise click.ClickException( + "DCT mode requires scipy. Install with: pip install scipy" + ) + + # Warn if DCT options used with LSB mode + if embed_mode == 'lsb': + if dct_output_format != 'png' or dct_color_mode != 'grayscale': + if not quiet: + click.secho("Note: --dct-format and --dct-color only apply to DCT mode", fg='yellow', dim=True) + + # Determine what to encode + payload = None + + if embed_file: + # Binary file embedding + payload = FilePayload.from_file(embed_file) + if not quiet: + click.echo(f"Embedding file: {payload.filename} ({len(payload.data):,} bytes)") + elif message: + payload = message + elif message_file: + payload = Path(message_file).read_text() + elif not sys.stdin.isatty(): + payload = sys.stdin.read() + else: + raise click.UsageError("Must provide message via -m, -f, -e, or stdin") + + # Load key if provided (from .pem file or QR code image) + rsa_key_data = None + rsa_key_from_qr = False + + if key and key_qr: + raise click.UsageError("Cannot use both --key and --key-qr. Choose one.") + + if key: + rsa_key_data = Path(key).read_bytes() + elif key_qr: + if not HAS_QR or not has_qr_read(): + raise click.ClickException( + "QR code reading not available. Install: pip install pyzbar\n" + "Also requires system library: sudo apt-get install libzbar0" + ) + key_pem = extract_key_from_qr_file(key_qr) + if not key_pem: + raise click.ClickException(f"Could not extract RSA key from QR code: {key_qr}") + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True + if not quiet: + click.echo(f"Loaded RSA key from QR code: {key_qr}") + + # QR code keys are never password-protected + effective_key_password = None if rsa_key_from_qr else key_password + + # Validate security factors + if not pin and not rsa_key_data: + raise click.UsageError("Must provide --pin or --key/--key-qr (or both)") + + try: + ref_photo = Path(ref).read_bytes() + carrier_image = Path(carrier).read_bytes() + + # Pre-check capacity with selected mode + fit_check = will_fit_by_mode(payload, carrier_image, embed_mode=embed_mode) + if not fit_check['fits']: + # Suggest alternative mode if it would fit + alt_mode = 'lsb' if embed_mode == 'dct' else 'dct' + alt_check = will_fit_by_mode(payload, carrier_image, embed_mode=alt_mode) + + suggestion = "" + if alt_mode == 'lsb' and alt_check['fits']: + suggestion = f"\n Tip: Payload would fit in LSB mode (--mode lsb)" + + raise click.ClickException( + f"Payload too large for {embed_mode.upper()} mode.\n" + f" Payload: {fit_check['payload_size']:,} bytes\n" + f" Capacity: {fit_check['capacity']:,} bytes\n" + f" Shortfall: {-fit_check['headroom']:,} bytes" + f"{suggestion}" + ) + + if not quiet: + mode_desc = embed_mode.upper() + if embed_mode == 'dct': + mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})" + click.echo(f"Mode: {mode_desc} ({fit_check['usage_percent']:.1f}% capacity)") + + # v3.2.0: No date_str parameter + result = encode( + message=payload, + reference_photo=ref_photo, + carrier_image=carrier_image, + passphrase=passphrase, + pin=pin or "", + rsa_key_data=rsa_key_data, + rsa_password=effective_key_password, + embed_mode=embed_mode, + dct_output_format=dct_output_format, + dct_color_mode=dct_color_mode, + ) + + # Determine output path + if output: + out_path = Path(output) + else: + out_path = Path(result.filename) + + # Write output + out_path.write_bytes(result.stego_image) + + if not quiet: + click.secho(f"✓ Encoded successfully!", fg='green') + click.echo(f" Output: {out_path}") + click.echo(f" Size: {len(result.stego_image):,} bytes") + click.echo(f" Capacity used: {result.capacity_percent:.1f}%") + if embed_mode == 'dct': + color_note = "color preserved" if dct_color_mode == 'color' else "grayscale" + format_note = dct_output_format.upper() + click.secho(f" DCT output: {format_note} ({color_note})", dim=True) + + except StegasooError as e: + raise click.ClickException(str(e)) + except click.ClickException: + raise + except Exception as e: + raise click.ClickException(f"Error: {e}") + + +# ============================================================================ +# DECODE COMMAND +# ============================================================================ + +@cli.command() +@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo') +@click.option('--stego', '-s', required=True, type=click.Path(exists=True), help='Stego image') +@click.option('--passphrase', '-p', required=True, help='Passphrase') +@click.option('--pin', help='Static PIN') +@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)') +@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image') +@click.option('--key-password', help='RSA key password (for encrypted .pem files)') +@click.option('--output', '-o', type=click.Path(), help='Save decoded content to file') +@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto', + help='Extraction mode: auto (default), lsb, or dct') +@click.option('--quiet', '-q', is_flag=True, help='Output only the content (for text) or suppress messages (for files)') +@click.option('--force', is_flag=True, help='Overwrite existing output file') +def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, embed_mode, quiet, force): + """ + Decode a secret message or file from a stego image. + + Must use the same credentials that were used for encoding. + Automatically detects whether content is text or a file. + RSA key can be provided as a .pem file (--key) or QR code image (--key-qr). + + v3.2.0: No --date parameter needed! Just use your passphrase. + + Note: Extraction works the same regardless of whether the image was + created with color mode or grayscale mode - both use the same Y channel. + + \b + Extraction Modes: + --mode auto Auto-detect (default) - tries LSB first, then DCT + --mode lsb Only try LSB extraction + --mode dct Only try DCT extraction (requires scipy) + + \b + Examples: + # Decode with PIN (auto-detect mode) + stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" --pin 123456 + + # Explicitly specify DCT mode + stegasoo decode -r photo.jpg -s stego.png -p "my passphrase here" --pin 123456 --mode dct + + # Decode with RSA key file + stegasoo decode -r photo.jpg -s stego.png -p "strong words" -k mykey.pem + + # Save output to file + stegasoo decode -r photo.jpg -s stego.png -p "passphrase" --pin 123456 -o output.txt + """ + # Check DCT mode availability + if embed_mode == 'dct' and not has_dct_support(): + raise click.ClickException( + "DCT mode requires scipy. Install with: pip install scipy" + ) + + # Load key if provided (from .pem file or QR code image) + rsa_key_data = None + rsa_key_from_qr = False + + if key and key_qr: + raise click.UsageError("Cannot use both --key and --key-qr. Choose one.") + + if key: + rsa_key_data = Path(key).read_bytes() + elif key_qr: + if not HAS_QR or not has_qr_read(): + raise click.ClickException( + "QR code reading not available. Install: pip install pyzbar\n" + "Also requires system library: sudo apt-get install libzbar0" + ) + key_pem = extract_key_from_qr_file(key_qr) + if not key_pem: + raise click.ClickException(f"Could not extract RSA key from QR code: {key_qr}") + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True + if not quiet: + click.echo(f"Loaded RSA key from QR code: {key_qr}") + + # QR code keys are never password-protected + effective_key_password = None if rsa_key_from_qr else key_password + + # Validate security factors + if not pin and not rsa_key_data: + raise click.UsageError("Must provide --pin or --key/--key-qr (or both)") + + try: + ref_photo = Path(ref).read_bytes() + stego_image = Path(stego).read_bytes() + + # v3.2.0: No date_str parameter + result = decode( + stego_image=stego_image, + reference_photo=ref_photo, + passphrase=passphrase, + pin=pin or "", + rsa_key_data=rsa_key_data, + rsa_password=effective_key_password, + embed_mode=embed_mode, + ) + + if result.is_file: + # File content + if output: + out_path = Path(output) + elif result.filename: + out_path = Path(result.filename) + else: + out_path = Path("decoded_file") + + if out_path.exists() and not force: + raise click.ClickException( + f"Output file '{out_path}' exists. Use --force to overwrite." + ) + + out_path.write_bytes(result.file_data) + + if not quiet: + click.secho("✓ Decoded file successfully!", fg='green') + click.echo(f" Saved to: {out_path}") + click.echo(f" Size: {len(result.file_data):,} bytes") + if result.mime_type: + click.echo(f" Type: {result.mime_type}") + else: + # Text content + if output: + Path(output).write_text(result.message) + if not quiet: + click.secho("✓ Decoded successfully!", fg='green') + click.echo(f" Saved to: {output}") + else: + if quiet: + click.echo(result.message) + else: + click.secho("✓ Decoded successfully!", fg='green') + click.echo() + click.echo(result.message) + + except (DecryptionError, ExtractionError) as e: + raise click.ClickException(f"Decryption failed: {e}") + except StegasooError as e: + raise click.ClickException(str(e)) + except Exception as e: + raise click.ClickException(f"Error: {e}") + + +# ============================================================================ +# VERIFY COMMAND +# ============================================================================ + +@cli.command() +@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo') +@click.option('--stego', '-s', required=True, type=click.Path(exists=True), help='Stego image') +@click.option('--passphrase', '-p', required=True, help='Passphrase') +@click.option('--pin', help='Static PIN') +@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)') +@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image') +@click.option('--key-password', help='RSA key password (for encrypted .pem files)') +@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto', + help='Extraction mode: auto (default), lsb, or dct') +@click.option('--json', 'as_json', is_flag=True, help='Output as JSON') +def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, as_json): + """ + Verify that a stego image can be decoded without extracting the message. + + Quick check to validate credentials are correct and data is intact. + Does NOT output the actual message content. + + \b + Examples: + stegasoo verify -r photo.jpg -s stego.png -p "my passphrase" --pin 123456 + + stegasoo verify -r photo.jpg -s stego.png -p "words here" -k mykey.pem --json + + stegasoo verify -r photo.jpg -s stego.png -p "test phrase" --pin 123456 --mode dct + """ + # Check DCT mode availability + if embed_mode == 'dct' and not has_dct_support(): + raise click.ClickException( + "DCT mode requires scipy. Install with: pip install scipy" + ) + + # Load key if provided + rsa_key_data = None + rsa_key_from_qr = False + + if key and key_qr: + raise click.UsageError("Cannot use both --key and --key-qr. Choose one.") + + if key: + rsa_key_data = Path(key).read_bytes() + elif key_qr: + if not HAS_QR or not has_qr_read(): + raise click.ClickException( + "QR code reading not available. Install: pip install pyzbar\n" + "Also requires system library: sudo apt-get install libzbar0" + ) + key_pem = extract_key_from_qr_file(key_qr) + if not key_pem: + raise click.ClickException(f"Could not extract RSA key from QR code: {key_qr}") + rsa_key_data = key_pem.encode('utf-8') + rsa_key_from_qr = True + + effective_key_password = None if rsa_key_from_qr else key_password + + if not pin and not rsa_key_data: + raise click.UsageError("Must provide --pin or --key/--key-qr (or both)") + + try: + ref_photo = Path(ref).read_bytes() + stego_image = Path(stego).read_bytes() + + # Attempt to decode + result = decode( + stego_image=stego_image, + reference_photo=ref_photo, + passphrase=passphrase, + pin=pin or "", + rsa_key_data=rsa_key_data, + rsa_password=effective_key_password, + embed_mode=embed_mode, + ) + + # Calculate payload size + if result.is_file: + payload_size = len(result.file_data) if result.file_data else 0 + payload_type = "file" + payload_desc = result.filename or "unnamed file" + if result.mime_type: + payload_desc += f" ({result.mime_type})" + else: + payload_size = len(result.message.encode('utf-8')) if result.message else 0 + payload_type = "text" + payload_desc = f"{payload_size} bytes" + + if as_json: + import json + output_data = { + "valid": True, + "stego_file": stego, + "payload_type": payload_type, + "payload_size": payload_size, + } + if result.is_file: + output_data["filename"] = result.filename + output_data["mime_type"] = result.mime_type + click.echo(json.dumps(output_data, indent=2)) + else: + click.secho("✓ Valid stego image", fg='green', bold=True) + click.echo(f" Payload: {payload_type} ({payload_desc})") + click.echo(f" Size: {payload_size:,} bytes") + + except (DecryptionError, ExtractionError) as e: + if as_json: + import json + output_data = { + "valid": False, + "stego_file": stego, + "error": str(e), + } + click.echo(json.dumps(output_data, indent=2)) + sys.exit(1) + else: + click.secho("✗ Verification failed", fg='red', bold=True) + click.echo(f" Error: {e}") + sys.exit(1) + except StegasooError as e: + raise click.ClickException(str(e)) + except Exception as e: + raise click.ClickException(f"Error: {e}") + + +# ============================================================================ +# INFO COMMAND +# ============================================================================ + +@cli.command() +@click.argument('image', type=click.Path(exists=True)) +@click.option('--json', 'as_json', is_flag=True, help='Output as JSON') +def info(image, as_json): + """ + Show information about an image. + + Displays dimensions, capacity for both LSB and DCT modes. + """ + try: + image_data = Path(image).read_bytes() + + result = validate_image(image_data, check_size=False) + if not result.is_valid: + raise click.ClickException(result.error_message) + + # Get capacity comparison + comparison = compare_modes(image_data) + + if as_json: + import json + output_data = { + "file": image, + "width": result.details['width'], + "height": result.details['height'], + "pixels": result.details['pixels'], + "mode": result.details['mode'], + "format": result.details['format'], + "capacity": { + "lsb": { + "bytes": comparison['lsb']['capacity_bytes'], + "kb": round(comparison['lsb']['capacity_kb'], 1), + }, + "dct": { + "bytes": comparison['dct']['capacity_bytes'], + "kb": round(comparison['dct']['capacity_kb'], 1), + "available": comparison['dct']['available'], + "ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1), + "output_formats": ["png", "jpeg"], + "color_modes": ["grayscale", "color"], + }, + }, + } + click.echo(json.dumps(output_data, indent=2)) + return + + click.echo() + click.secho(f"Image: {image}", bold=True) + click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}") + click.echo(f" Pixels: {result.details['pixels']:,}") + click.echo(f" Mode: {result.details['mode']}") + click.echo(f" Format: {result.details['format']}") + click.echo() + + click.secho(" Capacity:", bold=True) + click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)") + + dct_status = "✓" if comparison['dct']['available'] else "✗ (scipy not installed)" + click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}") + click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB") + + if comparison['dct']['available']: + click.secho(" DCT options: grayscale/color, png/jpeg", dim=True) + + click.echo() + + except Exception as e: + raise click.ClickException(str(e)) + + +# ============================================================================ +# COMPARE COMMAND +# ============================================================================ + +@cli.command() +@click.argument('image', type=click.Path(exists=True)) +@click.option('--payload-size', '-s', type=int, help='Check if specific payload size fits') +@click.option('--json', 'as_json', is_flag=True, help='Output as JSON') +def compare(image, payload_size, as_json): + """ + Compare LSB and DCT embedding modes for an image. + + Shows capacity for each mode and recommends which to use. + Optionally checks if a specific payload size would fit. + + \b + Examples: + stegasoo compare carrier.png + stegasoo compare carrier.png --payload-size 50000 + stegasoo compare carrier.png --json + """ + try: + image_data = Path(image).read_bytes() + + comparison = compare_modes(image_data) + + if as_json: + import json + output_data = { + "file": image, + "width": comparison['width'], + "height": comparison['height'], + "modes": { + "lsb": { + "capacity_bytes": comparison['lsb']['capacity_bytes'], + "capacity_kb": round(comparison['lsb']['capacity_kb'], 1), + "available": True, + "output_format": comparison['lsb']['output'], + }, + "dct": { + "capacity_bytes": comparison['dct']['capacity_bytes'], + "capacity_kb": round(comparison['dct']['capacity_kb'], 1), + "available": comparison['dct']['available'], + "output_formats": ["png", "jpeg"], + "color_modes": ["grayscale", "color"], + "ratio_vs_lsb_percent": round(comparison['dct']['ratio_vs_lsb'], 1), + }, + }, + } + + if payload_size: + output_data["payload_check"] = { + "size_bytes": payload_size, + "fits_lsb": payload_size <= comparison['lsb']['capacity_bytes'], + "fits_dct": payload_size <= comparison['dct']['capacity_bytes'], + } + + click.echo(json.dumps(output_data, indent=2)) + return + + click.echo() + click.secho(f"=== Mode Comparison: {image} ===", fg='cyan', bold=True) + click.echo(f" Dimensions: {comparison['width']} × {comparison['height']}") + click.echo() + + # LSB mode + click.secho(" ┌─── LSB Mode ───", fg='green') + click.echo(f" │ Capacity: {comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)") + click.echo(f" │ Output: {comparison['lsb']['output']}") + click.echo(f" │ Status: ✓ Available") + click.echo(" │") + + # DCT mode + click.secho(" ├─── DCT Mode ───", fg='blue') + click.echo(f" │ Capacity: {comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB)") + click.echo(f" │ Ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB capacity") + if comparison['dct']['available']: + click.echo(f" │ Status: ✓ Available") + click.echo(f" │ Formats: PNG (lossless), JPEG (smaller)") + click.echo(f" │ Colors: Grayscale (default), Color") + else: + click.secho(f" │ Status: ✗ Requires scipy (pip install scipy)", fg='yellow') + click.echo(" │") + + # Payload check + if payload_size: + click.secho(" ├─── Payload Check ───", fg='magenta') + click.echo(f" │ Size: {payload_size:,} bytes") + + fits_lsb = payload_size <= comparison['lsb']['capacity_bytes'] + fits_dct = payload_size <= comparison['dct']['capacity_bytes'] + + lsb_icon = "✓" if fits_lsb else "✗" + dct_icon = "✓" if fits_dct else "✗" + lsb_color = 'green' if fits_lsb else 'red' + dct_color = 'green' if fits_dct else 'red' + + click.echo(f" │ LSB mode: ", nl=False) + click.secho(f"{lsb_icon} {'Fits' if fits_lsb else 'Too large'}", fg=lsb_color) + click.echo(f" │ DCT mode: ", nl=False) + click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color) + click.echo(" │") + + # Recommendation + click.secho(" └─── Recommendation ───", fg='yellow') + if not comparison['dct']['available']: + click.echo(" Use LSB mode (DCT unavailable)") + elif payload_size: + if fits_dct: + click.echo(" DCT mode for better stealth (payload fits both modes)") + click.echo(" Use --dct-color color to preserve original colors") + elif fits_lsb: + click.echo(" LSB mode (payload too large for DCT)") + else: + click.secho(" ✗ Payload too large for both modes!", fg='red') + else: + click.echo(" LSB for larger payloads, DCT for better stealth") + click.echo(" DCT supports color output with --dct-color color") + + click.echo() + + except Exception as e: + raise click.ClickException(str(e)) + + +# ============================================================================ +# STRIP-METADATA COMMAND +# ============================================================================ + +@cli.command('strip-metadata') +@click.argument('image', type=click.Path(exists=True)) +@click.option('--output', '-o', type=click.Path(), help='Output file (default: overwrites input)') +@click.option('--format', '-f', 'output_format', type=click.Choice(['PNG', 'BMP']), default='PNG', + help='Output format') +@click.option('--quiet', '-q', is_flag=True, help='Suppress output') +def strip_metadata_cmd(image, output, output_format, quiet): + """ + Remove all metadata (EXIF, GPS, etc.) from an image. + + Creates a clean image with only pixel data - no camera info, + location data, timestamps, or other potentially sensitive metadata. + + \b + Examples: + stegasoo strip-metadata photo.jpg -o clean.png + stegasoo strip-metadata photo.jpg # Overwrites as PNG + """ + if not HAS_STRIP_METADATA: + raise click.ClickException("strip_image_metadata not available") + + try: + image_data = Path(image).read_bytes() + original_size = len(image_data) + + clean_data = strip_image_metadata(image_data, output_format) + + if output: + out_path = Path(output) + else: + # Replace extension with output format + out_path = Path(image).with_suffix(f'.{output_format.lower()}') + + out_path.write_bytes(clean_data) + + if not quiet: + click.secho("✓ Metadata stripped", fg='green') + click.echo(f" Input: {image} ({original_size:,} bytes)") + click.echo(f" Output: {out_path} ({len(clean_data):,} bytes)") + + except Exception as e: + raise click.ClickException(str(e)) + + +# ============================================================================ +# MODES COMMAND +# ============================================================================ + +@cli.command() +def modes(): + """ + Show available embedding modes and their status. + + Displays which modes are available and their characteristics. + """ + click.echo() + click.secho("=== Stegasoo Embedding Modes (v3.2.0) ===", fg='cyan', bold=True) + click.echo() + + # LSB Mode + click.secho(" LSB Mode (Spatial LSB)", fg='green', bold=True) + click.echo(" Status: ✓ Always available") + click.echo(" Output: PNG/BMP (full color)") + click.echo(" Capacity: ~375 KB per megapixel") + click.echo(" Use case: Larger payloads, color preservation") + click.echo(" CLI flag: --mode lsb (default)") + click.echo() + + # DCT Mode + click.secho(" DCT Mode (Frequency Domain)", fg='blue', bold=True) + if has_dct_support(): + click.echo(" Status: ✓ Available") + else: + click.secho(" Status: ✗ Requires scipy", fg='yellow') + click.echo(" Install: pip install scipy") + click.echo(" Capacity: ~75 KB per megapixel (~20% of LSB)") + click.echo(" Use case: Better stealth, frequency domain hiding") + click.echo(" CLI flag: --mode dct") + click.echo() + + # DCT Options + click.secho(" DCT Options", fg='magenta', bold=True) + click.echo(" Output format:") + click.echo(" --dct-format png Lossless, larger file (default)") + click.echo(" --dct-format jpeg Lossy, smaller, more natural") + click.echo() + click.echo(" Color mode:") + click.echo(" --dct-color grayscale Traditional DCT (default)") + click.echo(" --dct-color color Preserves original colors") + click.echo() + + # v3.2.0 Note + click.secho(" v3.2.0 Changes:", fg='cyan', bold=True) + click.echo(" ✓ No date parameters needed") + click.echo(" ✓ Single passphrase (no daily rotation)") + click.echo(" ✓ Default passphrase increased to 4 words") + click.echo(" ✓ True asynchronous communications") + click.echo() + + # Examples + click.secho(" Examples:", dim=True) + click.echo(" # Traditional DCT (grayscale PNG)") + click.echo(" stegasoo encode ... --mode dct") + click.echo() + click.echo(" # Color-preserving DCT with JPEG output") + click.echo(" stegasoo encode ... --mode dct --dct-color color --dct-format jpeg") + click.echo() + click.echo(" # Compare modes for an image") + click.echo(" stegasoo compare carrier.png") + click.echo() + + +# ============================================================================ +# MAIN +# ============================================================================ + +def main(): + """Entry point.""" + cli() + + +if __name__ == '__main__': + main() diff --git a/frontends/web/app.py b/frontends/web/app.py index 523ff4e..99c17d8 100644 --- a/frontends/web/app.py +++ b/frontends/web/app.py @@ -1,10 +1,16 @@ #!/usr/bin/env python3 """ -Stegasoo Web Frontend (v3.2.0) +Stegasoo Web Frontend (v4.0.0) Flask-based web UI for steganography operations. Supports both text messages and file embedding. +CHANGES in v4.0.0: +- Added channel key support for deployment/group isolation +- New /api/channel/status endpoint +- Channel key selector on encode/decode pages +- Messages encoded with channel key require same key to decode + CHANGES in v3.2.0: - Removed date dependency from all operations - Renamed day_phrase → passphrase @@ -52,6 +58,11 @@ from stegasoo import ( EMBED_MODE_DCT, EMBED_MODE_AUTO, has_dct_support, + # Channel key functions (v4.0.0) + has_channel_key, + get_channel_status, + validate_channel_key, + generate_channel_key, # NOTE: encode, decode, compare_modes, will_fit_by_mode now use subprocess isolation ) from stegasoo.constants import ( @@ -126,6 +137,9 @@ THUMBNAIL_FILES: dict[str, bytes] = {} @app.context_processor def inject_globals(): """Inject global variables into all templates.""" + # Get channel status (v4.0.0) + channel_status = get_channel_status() + return { 'version': __version__, 'max_message_chars': MAX_MESSAGE_CHARS, @@ -140,6 +154,11 @@ def inject_globals(): 'default_passphrase_words': DEFAULT_PASSPHRASE_WORDS, # NEW in v3.0 'has_dct': has_dct_support(), + # NEW in v4.0.0 - Channel key status + 'channel_mode': channel_status['mode'], + 'channel_configured': channel_status['configured'], + 'channel_fingerprint': channel_status.get('fingerprint'), + 'channel_source': channel_status.get('source'), } @@ -154,6 +173,13 @@ try: print(f"DCT support: {has_dct_support()}") print(f"QR code support: write={HAS_QRCODE}, read={HAS_QRCODE_READ}") + # Channel key status (v4.0.0) + channel_status = get_channel_status() + print(f"Channel key: {channel_status['mode']} mode") + if channel_status['configured']: + print(f" Fingerprint: {channel_status.get('fingerprint')}") + print(f" Source: {channel_status.get('source')}") + DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'): @@ -164,6 +190,33 @@ except Exception as e: print(f"Could not override stegasoo limits: {e}") +# ============================================================================ +# CHANNEL KEY HELPER (v4.0.0) +# ============================================================================ + +def resolve_channel_key_form(channel_key_value: str) -> str: + """ + Resolve channel key from form input. + + Args: + channel_key_value: Form value ('auto', 'none', or explicit key) + + Returns: + Value to pass to subprocess_stego ('auto', 'none', or explicit key) + """ + if not channel_key_value or channel_key_value == 'auto': + return 'auto' + elif channel_key_value == 'none': + return 'none' + else: + # Explicit key - validate format + if validate_channel_key(channel_key_value): + return channel_key_value + else: + # Invalid format, fall back to auto + return 'auto' + + def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes: """Generate thumbnail from image data.""" try: @@ -233,6 +286,71 @@ def index(): return render_template('index.html') +# ============================================================================ +# CHANNEL KEY API (v4.0.0) +# ============================================================================ + +@app.route('/api/channel/status') +def api_channel_status(): + """ + Get current channel key status (v4.0.0). + + Returns JSON with mode, fingerprint, and source. + """ + # Use subprocess for isolation + result = subprocess_stego.get_channel_status(reveal=False) + + if result.success: + return jsonify({ + 'success': True, + 'mode': result.mode, + 'configured': result.configured, + 'fingerprint': result.fingerprint, + 'source': result.source, + }) + else: + # Fallback to direct call if subprocess fails + status = get_channel_status() + return jsonify({ + 'success': True, + 'mode': status['mode'], + 'configured': status['configured'], + 'fingerprint': status.get('fingerprint'), + 'source': status.get('source'), + }) + + +@app.route('/api/channel/validate', methods=['POST']) +def api_channel_validate(): + """ + Validate a channel key format (v4.0.0). + + Returns JSON with validation result. + """ + key = request.form.get('key', '') or request.json.get('key', '') if request.is_json else '' + + if not key: + return jsonify({'valid': False, 'error': 'No key provided'}) + + is_valid = validate_channel_key(key) + + if is_valid: + fingerprint = f"{key[:4]}-••••-••••-••••-••••-••••-••••-{key[-4:]}" + return jsonify({ + 'valid': True, + 'fingerprint': fingerprint, + }) + else: + return jsonify({ + 'valid': False, + 'error': 'Invalid format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX', + }) + + +# ============================================================================ +# GENERATE +# ============================================================================ + @app.route('/generate', methods=['GET', 'POST']) def generate(): if request.method == 'POST': @@ -614,6 +732,9 @@ def encode_page(): if dct_color_mode not in ('grayscale', 'color'): dct_color_mode = 'color' + # NEW in v4.0.0 - Channel key + channel_key = resolve_channel_key_form(request.form.get('channel_key', 'auto')) + # Check DCT availability if embed_mode == 'dct' and not has_dct_support(): flash('DCT mode requires scipy. Install with: pip install scipy', 'error') @@ -708,7 +829,7 @@ def encode_page(): flash(result.error_message, 'error') return render_template('encode.html', has_qrcode_read=HAS_QRCODE_READ) - # v3.2.0: No date parameter needed + # v4.0.0: Include channel_key parameter # Use subprocess-isolated encode to prevent crashes if payload_type == 'file' and payload_file and payload_file.filename: encode_result = subprocess_stego.encode( @@ -724,6 +845,7 @@ def encode_page(): embed_mode=embed_mode, dct_output_format=dct_output_format if embed_mode == 'dct' else 'png', dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color', + channel_key=channel_key, # v4.0.0 ) else: encode_result = subprocess_stego.encode( @@ -737,6 +859,7 @@ def encode_page(): embed_mode=embed_mode, dct_output_format=dct_output_format if embed_mode == 'dct' else 'png', dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color', + channel_key=channel_key, # v4.0.0 ) # Check for subprocess errors @@ -772,6 +895,9 @@ def encode_page(): 'output_format': dct_output_format if embed_mode == 'dct' else 'png', 'color_mode': dct_color_mode if embed_mode == 'dct' else None, 'mime_type': output_mime, + # Channel info (v4.0.0) + 'channel_mode': encode_result.channel_mode, + 'channel_fingerprint': encode_result.channel_fingerprint, } return redirect(url_for('encode_result', file_id=file_id)) @@ -812,6 +938,9 @@ def encode_result(file_id): embed_mode=file_info.get('embed_mode', 'lsb'), output_format=file_info.get('output_format', 'png'), color_mode=file_info.get('color_mode'), + # Channel info (v4.0.0) + channel_mode=file_info.get('channel_mode', 'public'), + channel_fingerprint=file_info.get('channel_fingerprint'), ) @@ -901,6 +1030,9 @@ def decode_page(): if embed_mode not in ('auto', 'lsb', 'dct'): embed_mode = 'auto' + # NEW in v4.0.0 - Channel key + channel_key = resolve_channel_key_form(request.form.get('channel_key', 'auto')) + # Check DCT availability if embed_mode == 'dct' and not has_dct_support(): flash('DCT mode requires scipy. Install with: pip install scipy', 'error') @@ -957,7 +1089,7 @@ def decode_page(): flash(result.error_message, 'error') return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) - # v3.2.0: No date_str parameter needed + # v4.0.0: Include channel_key parameter # Use subprocess-isolated decode to prevent crashes decode_result = subprocess_stego.decode( stego_data=stego_data, @@ -967,11 +1099,16 @@ def decode_page(): rsa_key_data=rsa_key_data, rsa_password=key_password, embed_mode=embed_mode, + channel_key=channel_key, # v4.0.0 ) # Check for subprocess errors if not decode_result.success: error_msg = decode_result.error or 'Decoding failed' + # Check for channel key related errors + if 'channel key' in error_msg.lower(): + flash(error_msg, 'error') + return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) if 'decrypt' in error_msg.lower() or decode_result.error_type == 'DecryptionError': raise DecryptionError(error_msg) raise StegasooError(error_msg) @@ -1005,7 +1142,7 @@ def decode_page(): ) except DecryptionError: - flash('Decryption failed. Check your passphrase, PIN, RSA key, and reference photo.', 'error') + flash('Decryption failed. Check your passphrase, PIN, RSA key, reference photo, and channel key.', 'error') return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ) except StegasooError as e: flash(str(e), 'error') diff --git a/frontends/web/static/js/stegasoo.js b/frontends/web/static/js/stegasoo.js index c9773d4..d5d87d2 100644 --- a/frontends/web/static/js/stegasoo.js +++ b/frontends/web/static/js/stegasoo.js @@ -696,6 +696,177 @@ const Stegasoo = { adjust(); }, + // ======================================================================== + // CHANNEL KEY HANDLING (v4.0.0) + // ======================================================================== + + /** + * Generate a random channel key in format XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX + * @returns {string} Generated key + */ + generateChannelKey() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let key = ''; + for (let i = 0; i < 8; i++) { + if (i > 0) key += '-'; + for (let j = 0; j < 4; j++) { + key += chars.charAt(Math.floor(Math.random() * chars.length)); + } + } + return key; + }, + + /** + * Validate channel key format + * @param {string} key - Key to validate + * @returns {boolean} True if valid + */ + validateChannelKey(key) { + const pattern = /^[A-Z0-9]{4}(-[A-Z0-9]{4}){7}$/; + return pattern.test(key); + }, + + /** + * Format channel key input (auto-add dashes, uppercase) + * @param {HTMLInputElement} input - Input element + */ + formatChannelKeyInput(input) { + let value = input.value.toUpperCase(); + const clean = value.replace(/-/g, ''); + + if (clean.length > 0 && clean.length <= 32) { + const formatted = clean.match(/.{1,4}/g)?.join('-') || clean; + if (formatted !== value && formatted.length <= 39) { + input.value = formatted; + } else { + input.value = value; + } + } + + // Validate and show/hide error state + const isValid = this.validateChannelKey(input.value); + input.classList.toggle('is-invalid', input.value.length > 0 && !isValid); + }, + + /** + * Initialize channel key UI for encode/decode pages + * @param {Object} config - Configuration object + * @param {string} config.radioName - Name of radio buttons (default: 'channel_key') + * @param {string} config.customInputId - ID of custom key input container + * @param {string} config.keyInputId - ID of key input field + * @param {string} config.generateBtnId - ID of generate button (optional) + * @param {string} config.customRadioId - ID of custom radio button + * @param {string[]} config.cardIds - Array of card/label IDs for active class toggling + */ + initChannelKey(config = {}) { + const radioName = config.radioName || 'channel_key'; + const customInputId = config.customInputId || 'channelCustomInput'; + const keyInputId = config.keyInputId || 'channelKeyInput'; + const generateBtnId = config.generateBtnId; + const customRadioId = config.customRadioId || 'channelCustom'; + const cardIds = config.cardIds || []; + + const radios = document.querySelectorAll(`input[name="${radioName}"]`); + const customInput = document.getElementById(customInputId); + const keyInput = document.getElementById(keyInputId); + const generateBtn = generateBtnId ? document.getElementById(generateBtnId) : null; + const customRadio = document.getElementById(customRadioId); + + // Toggle active class on mode-btn cards + const updateActiveState = () => { + radios.forEach(radio => { + const card = radio.closest('.mode-btn'); + if (card) { + card.classList.toggle('active', radio.checked); + } + }); + }; + + // Show/hide custom input based on selection + radios.forEach(radio => { + radio.addEventListener('change', () => { + updateActiveState(); + const isCustom = customRadio?.checked; + customInput?.classList.toggle('d-none', !isCustom); + if (isCustom && keyInput) { + keyInput.focus(); + } + }); + }); + + // Initial state + updateActiveState(); + + // Format and validate key input + keyInput?.addEventListener('input', () => { + this.formatChannelKeyInput(keyInput); + }); + + // Generate button (if present) + generateBtn?.addEventListener('click', () => { + if (keyInput) { + keyInput.value = this.generateChannelKey(); + keyInput.classList.remove('is-invalid'); + } + }); + }, + + /** + * Handle form submission with channel key validation + * @param {HTMLFormElement} form - Form element + * @param {string} customRadioId - ID of custom radio button + * @param {string} keyInputId - ID of key input field + * @returns {boolean} True if valid, false to prevent submission + */ + validateChannelKeyOnSubmit(form, customRadioId, keyInputId) { + const customRadio = document.getElementById(customRadioId); + const keyInput = document.getElementById(keyInputId); + + if (customRadio?.checked && keyInput) { + if (!this.validateChannelKey(keyInput.value)) { + keyInput.classList.add('is-invalid'); + keyInput.focus(); + return false; + } + // Set the radio value to the actual key for form submission + customRadio.value = keyInput.value; + } + return true; + }, + + /** + * Initialize standalone channel key generator (for generate page) + * @param {string} inputId - ID of generated key input + * @param {string} generateBtnId - ID of generate button + * @param {string} copyBtnId - ID of copy button + */ + initChannelKeyGenerator(inputId, generateBtnId, copyBtnId) { + const input = document.getElementById(inputId); + const generateBtn = document.getElementById(generateBtnId); + const copyBtn = document.getElementById(copyBtnId); + + generateBtn?.addEventListener('click', () => { + if (input) { + input.value = this.generateChannelKey(); + } + if (copyBtn) { + copyBtn.disabled = false; + } + }); + + copyBtn?.addEventListener('click', () => { + if (input?.value) { + navigator.clipboard.writeText(input.value).then(() => { + const icon = copyBtn.querySelector('i'); + if (icon) { + icon.className = 'bi bi-check'; + setTimeout(() => { icon.className = 'bi bi-clipboard'; }, 2000); + } + }); + } + }); + }, + // ======================================================================== // INITIALIZATION HELPERS // ======================================================================== @@ -707,8 +878,30 @@ const Stegasoo = { this.initClipboardPaste(['input[name="carrier"]', 'input[name="reference_photo"]']); this.initQrCropAnimation('rsaQrInput'); this.initCollapseChevrons(); - this.initFormLoading('encodeForm', 'encodeBtn', 'Encoding...'); this.initPassphraseFontResize(); + + // Channel key (v4.0.0) - uses mode-btn style + this.initChannelKey({ + customInputId: 'channelCustomInput', + keyInputId: 'channelKeyInput', + generateBtnId: 'channelKeyGenerate', + customRadioId: 'channelCustom', + cardIds: ['channelAutoCard', 'channelPublicCard', 'channelCustomCard'] + }); + + // Form submission with channel key validation + const form = document.getElementById('encodeForm'); + const btn = document.getElementById('encodeBtn'); + form?.addEventListener('submit', (e) => { + if (!this.validateChannelKeyOnSubmit(form, 'channelCustom', 'channelKeyInput')) { + e.preventDefault(); + return false; + } + if (btn) { + btn.disabled = true; + btn.innerHTML = 'Encoding...'; + } + }); }, initDecodePage() { @@ -718,13 +911,36 @@ const Stegasoo = { this.initClipboardPaste(['input[name="stego_image"]', 'input[name="reference_photo"]']); this.initQrCropAnimation('rsaKeyQrInput'); this.initCollapseChevrons(); - this.initFormLoading('decodeForm', 'decodeBtn', 'Decoding...'); this.initPassphraseFontResize(); + + // Channel key (v4.0.0) - uses mode-btn style + this.initChannelKey({ + customInputId: 'channelCustomInputDec', + keyInputId: 'channelKeyInputDec', + customRadioId: 'channelCustomDec', + cardIds: ['channelAutoCardDec', 'channelPublicCardDec', 'channelCustomCardDec'] + }); + + // Form submission with channel key validation and mode display + const form = document.getElementById('decodeForm'); + const btn = document.getElementById('decodeBtn'); + form?.addEventListener('submit', (e) => { + if (!this.validateChannelKeyOnSubmit(form, 'channelCustomDec', 'channelKeyInputDec')) { + e.preventDefault(); + return false; + } + const selectedMode = document.querySelector('input[name="embed_mode"]:checked')?.value || 'auto'; + if (btn) { + btn.disabled = true; + btn.innerHTML = `Decoding (${selectedMode.toUpperCase()})...`; + } + }); }, initGeneratePage() { this.initPasswordToggles(); - // Generate page has mostly unique functionality + // Channel key generator (v4.0.0) + this.initChannelKeyGenerator('channelKeyGenerated', 'generateChannelKeyBtn', 'copyChannelKeyBtn'); } }; diff --git a/frontends/web/static/style.css b/frontends/web/static/style.css index da149e9..fa2572c 100644 --- a/frontends/web/static/style.css +++ b/frontends/web/static/style.css @@ -26,6 +26,23 @@ font-weight: 700 !important; } +/* ---------------------------------------------------------------------------- + Channel Card Icons (About page) - Contrast fix for gradient backgrounds + ---------------------------------------------------------------------------- */ +#channel-keys .card-header i.bi { + /* Add outline/shadow for visibility on gradient backgrounds */ + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8)) + drop-shadow(0 0 4px rgba(0, 0, 0, 0.5)); +} + +/* Override green Auto icon to white for better contrast */ +#channel-keys .card-header i.bi-gear-fill.text-success { + color: #ffffff !important; + filter: drop-shadow(0 0 3px rgba(34, 197, 94, 0.8)) + drop-shadow(0 0 6px rgba(34, 197, 94, 0.5)) + drop-shadow(0 0 2px rgba(0, 0, 0, 0.6)); +} + /* ---------------------------------------------------------------------------- Mode Selection Buttons (Compact) ---------------------------------------------------------------------------- */ @@ -34,11 +51,13 @@ border: 2px solid var(--border-light); border-radius: 0.5rem; padding: 0.75rem 1rem; + padding-left: 2.75rem; /* Make room for absolutely positioned radio */ cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; + position: relative; /* For absolute positioning of radio */ } .mode-btn:hover { @@ -52,10 +71,35 @@ } .mode-btn .form-check-input { - margin-top: 0; + position: absolute; + left: 15px; /* Fixed distance from left edge of card */ + top: 50%; + transform: translateY(-50%); + margin: 0; flex-shrink: 0; } +/* Remove ms-2 margin from first icon after radio since radio is now absolute */ +.mode-btn > i.bi:first-of-type { + margin-left: 0 !important; +} + +/* Equal-width mode buttons (ignores content length) */ +.mode-btn.equal-width { + flex: 1 1 0; + min-width: 0; +} + +/* ---------------------------------------------------------------------------- + Security Factor Boxes - Matches drop-zone dashed border style + ---------------------------------------------------------------------------- */ +.security-box { + border: 2px dashed rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + padding: 1rem; + height: 100%; +} + .mode-info-icon { cursor: help; opacity: 0.6; diff --git a/frontends/web/stego_worker.py b/frontends/web/stego_worker.py index eeb224e..0de2084 100644 --- a/frontends/web/stego_worker.py +++ b/frontends/web/stego_worker.py @@ -1,10 +1,14 @@ #!/usr/bin/env python3 """ -Stegasoo Subprocess Worker +Stegasoo Subprocess Worker (v4.0.0) This script runs in a subprocess and handles encode/decode operations. If it crashes due to jpegio/scipy issues, the parent Flask process survives. +CHANGES in v4.0.0: +- Added channel_key support for encode/decode operations +- New channel_status operation + Communication is via JSON over stdin/stdout: - Input: JSON object with operation parameters - Output: JSON object with results or error @@ -24,6 +28,49 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src')) sys.path.insert(0, str(Path(__file__).parent)) +def _resolve_channel_key(channel_key_param): + """ + Resolve channel_key parameter to value for stegasoo. + + Args: + channel_key_param: 'auto', 'none', explicit key, or None + + Returns: + None (auto), "" (public), or explicit key string + """ + if channel_key_param is None or channel_key_param == "auto": + return None # Auto mode - use server config + elif channel_key_param == "none": + return "" # Public mode + else: + return channel_key_param # Explicit key + + +def _get_channel_info(resolved_key): + """ + Get channel mode and fingerprint for response. + + Returns: + (mode, fingerprint) tuple + """ + from stegasoo import has_channel_key, get_channel_status + + if resolved_key == "": + return "public", None + + if resolved_key is not None: + # Explicit key + fingerprint = f"{resolved_key[:4]}-••••-••••-••••-••••-••••-••••-{resolved_key[-4:]}" + return "private", fingerprint + + # Auto mode - check server config + if has_channel_key(): + status = get_channel_status() + return "private", status.get('fingerprint') + + return "public", None + + def encode_operation(params: dict) -> dict: """Handle encode operation.""" from stegasoo import encode, FilePayload @@ -48,6 +95,9 @@ def encode_operation(params: dict) -> dict: else: payload = params.get('message', '') + # Resolve channel key (v4.0.0) + resolved_channel_key = _resolve_channel_key(params.get('channel_key', 'auto')) + # Call encode with correct parameter names result = encode( message=payload, @@ -60,6 +110,7 @@ def encode_operation(params: dict) -> dict: embed_mode=params.get('embed_mode', 'lsb'), dct_output_format=params.get('dct_output_format', 'png'), dct_color_mode=params.get('dct_color_mode', 'color'), + channel_key=resolved_channel_key, # v4.0.0 ) # Build stats dict if available @@ -71,11 +122,16 @@ def encode_operation(params: dict) -> dict: 'bytes_embedded': getattr(result.stats, 'bytes_embedded', 0), } + # Get channel info for response (v4.0.0) + channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key) + return { 'success': True, 'stego_b64': base64.b64encode(result.stego_image).decode('ascii'), 'filename': getattr(result, 'filename', None), 'stats': stats, + 'channel_mode': channel_mode, + 'channel_fingerprint': channel_fingerprint, } @@ -92,6 +148,9 @@ def decode_operation(params: dict) -> dict: if params.get('rsa_key_b64'): rsa_key_data = base64.b64decode(params['rsa_key_b64']) + # Resolve channel key (v4.0.0) + resolved_channel_key = _resolve_channel_key(params.get('channel_key', 'auto')) + # Call decode with correct parameter names result = decode( stego_image=stego_data, @@ -101,6 +160,7 @@ def decode_operation(params: dict) -> dict: rsa_key_data=rsa_key_data, rsa_password=params.get('rsa_password'), embed_mode=params.get('embed_mode', 'auto'), + channel_key=resolved_channel_key, # v4.0.0 ) if result.is_file: @@ -150,6 +210,25 @@ def capacity_check_operation(params: dict) -> dict: } +def channel_status_operation(params: dict) -> dict: + """Handle channel status check (v4.0.0).""" + from stegasoo import get_channel_status + + status = get_channel_status() + reveal = params.get('reveal', False) + + return { + 'success': True, + 'status': { + 'mode': status['mode'], + 'configured': status['configured'], + 'fingerprint': status.get('fingerprint'), + 'source': status.get('source'), + 'key': status.get('key') if reveal and status['configured'] else None, + } + } + + def main(): """Main entry point - read JSON from stdin, write JSON to stdout.""" try: @@ -170,6 +249,8 @@ def main(): output = compare_operation(params) elif operation == 'capacity': output = capacity_check_operation(params) + elif operation == 'channel_status': + output = channel_status_operation(params) else: output = {'success': False, 'error': f'Unknown operation: {operation}'} diff --git a/frontends/web/subprocess_stego.py b/frontends/web/subprocess_stego.py index edd3c1b..b436f36 100644 --- a/frontends/web/subprocess_stego.py +++ b/frontends/web/subprocess_stego.py @@ -1,15 +1,19 @@ """ -Subprocess Steganography Wrapper +Subprocess Steganography Wrapper (v4.0.0) Runs stegasoo operations in isolated subprocesses to prevent crashes from taking down the Flask server. +CHANGES in v4.0.0: +- Added channel_key parameter to encode() and decode() methods +- Channel keys enable deployment/group isolation + Usage: from subprocess_stego import SubprocessStego stego = SubprocessStego() - # Encode + # Encode with channel key result = stego.encode( carrier_data=carrier_bytes, reference_data=ref_bytes, @@ -17,6 +21,7 @@ Usage: passphrase="my passphrase", pin="123456", embed_mode="dct", + channel_key="auto", # or "none", or explicit key ) if result.success: @@ -31,6 +36,7 @@ Usage: reference_data=ref_bytes, passphrase="my passphrase", pin="123456", + channel_key="auto", ) # Compare modes (capacity) @@ -60,6 +66,9 @@ class EncodeResult: stego_data: Optional[bytes] = None filename: Optional[str] = None stats: Optional[Dict[str, Any]] = None + # Channel info (v4.0.0) + channel_mode: Optional[str] = None + channel_fingerprint: Optional[str] = None error: Optional[str] = None error_type: Optional[str] = None @@ -101,6 +110,18 @@ class CapacityResult: error: Optional[str] = None +@dataclass +class ChannelStatusResult: + """Result from channel status check (v4.0.0).""" + success: bool + mode: str = "public" + configured: bool = False + fingerprint: Optional[str] = None + source: Optional[str] = None + key: Optional[str] = None + error: Optional[str] = None + + class SubprocessStego: """ Subprocess-isolated steganography operations. @@ -205,6 +226,8 @@ class SubprocessStego: embed_mode: str = "lsb", dct_output_format: str = "png", dct_color_mode: str = "color", + # Channel key (v4.0.0) + channel_key: Optional[str] = "auto", timeout: Optional[int] = None, ) -> EncodeResult: """ @@ -224,6 +247,7 @@ class SubprocessStego: embed_mode: 'lsb' or 'dct' dct_output_format: 'png' or 'jpeg' (for DCT mode) dct_color_mode: 'grayscale' or 'color' (for DCT mode) + channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0) timeout: Operation timeout in seconds Returns: @@ -239,6 +263,7 @@ class SubprocessStego: 'embed_mode': embed_mode, 'dct_output_format': dct_output_format, 'dct_color_mode': dct_color_mode, + 'channel_key': channel_key, # v4.0.0 } if file_data: @@ -258,6 +283,8 @@ class SubprocessStego: stego_data=base64.b64decode(result['stego_b64']), filename=result.get('filename'), stats=result.get('stats'), + channel_mode=result.get('channel_mode'), + channel_fingerprint=result.get('channel_fingerprint'), ) else: return EncodeResult( @@ -275,6 +302,8 @@ class SubprocessStego: rsa_key_data: Optional[bytes] = None, rsa_password: Optional[str] = None, embed_mode: str = "auto", + # Channel key (v4.0.0) + channel_key: Optional[str] = "auto", timeout: Optional[int] = None, ) -> DecodeResult: """ @@ -288,6 +317,7 @@ class SubprocessStego: rsa_key_data: Optional RSA key PEM bytes rsa_password: RSA key password if encrypted embed_mode: 'auto', 'lsb', or 'dct' + channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0) timeout: Operation timeout in seconds Returns: @@ -300,6 +330,7 @@ class SubprocessStego: 'passphrase': passphrase, 'pin': pin, 'embed_mode': embed_mode, + 'channel_key': channel_key, # v4.0.0 } if rsa_key_data: @@ -411,6 +442,44 @@ class SubprocessStego: success=False, error=result.get('error', 'Unknown error'), ) + + def get_channel_status( + self, + reveal: bool = False, + timeout: Optional[int] = None, + ) -> ChannelStatusResult: + """ + Get current channel key status (v4.0.0). + + Args: + reveal: Include full key in response + timeout: Operation timeout in seconds + + Returns: + ChannelStatusResult with channel info + """ + params = { + 'operation': 'channel_status', + 'reveal': reveal, + } + + result = self._run_worker(params, timeout) + + if result.get('success'): + status = result.get('status', {}) + return ChannelStatusResult( + success=True, + mode=status.get('mode', 'public'), + configured=status.get('configured', False), + fingerprint=status.get('fingerprint'), + source=status.get('source'), + key=status.get('key') if reveal else None, + ) + else: + return ChannelStatusResult( + success=False, + error=result.get('error', 'Unknown error'), + ) # Convenience function for quick usage diff --git a/frontends/web/templates/about.html b/frontends/web/templates/about.html index 9d65c24..85032b6 100644 --- a/frontends/web/templates/about.html +++ b/frontends/web/templates/about.html @@ -11,8 +11,7 @@

- Stegasoo is a steganography tool that hides encrypted messages and files - inside ordinary images using multi-factor authentication. + Stegasoo hides encrypted messages and files inside images using multi-factor authentication.

Features
@@ -22,22 +21,22 @@
  • Text & File Embedding -
    Hide messages or any file type (PDF, ZIP, documents) +
    Any file type: PDF, ZIP, documents
  • Multi-Factor Security -
    Combines photo + passphrase + PIN/RSA key +
    Photo + passphrase + PIN/RSA key
  • AES-256-GCM Encryption -
    Authenticated encryption with integrity verification +
    Authenticated encryption with integrity check
  • - LSB & DCT Modes -
    Choose capacity (LSB) or JPEG resilience (DCT) + DCT & LSB Modes +
    JPEG resilience (DCT) or high capacity (LSB)
  • @@ -46,12 +45,12 @@
  • Random Pixel Embedding -
    Key-derived selection defeats statistical analysis +
    Defeats statistical analysis
  • Large Image Support -
    Up to {{ max_payload_kb }} KB payload, tested with 14MB+ images +
    Up to {{ max_payload_kb }} KB, tested with 14MB+ images
  • @@ -61,7 +60,13 @@
  • QR Code Keys -
    Import/export RSA keys via QR codes +
    Import/export RSA keys via QR +
  • +
  • + + Channel Keys + v4.0 +
    Group/deployment isolation
  • @@ -75,67 +80,61 @@
    Embedding Modes
    -

    - Stegasoo supports two embedding modes, each optimized for different use cases. -

    +

    Two modes optimized for different use cases.

    - -
    -
    -
    - - LSB Mode - Default -
    -
    -

    - LSB (Least Significant Bit) 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. -

    -
      -
    • Capacity: ~375 KB per megapixel
    • -
    • Output: PNG (lossless)
    • -
    • Color: Full color preserved
    • -
    • Speed: Fast (~0.5s)
    • -
    -
    -
    - Email attachments
    - Cloud storage (Dropbox, Drive)
    - Direct file transfer
    - Social media (recompresses) -
    -
    -
    -
    -
    DCT Mode + Default

    - DCT (Discrete Cosine Transform) embeds data in frequency - coefficients rather than raw pixels. This survives JPEG recompression - because coefficients are preserved during re-encoding. + DCT (Discrete Cosine Transform) embeds data in frequency coefficients. Survives JPEG recompression.

      -
    • Capacity: ~75 KB per megapixel
    • +
    • Capacity: ~75 KB/MP
    • Output: JPEG or PNG
    • Color: Color or grayscale
    • -
    • Speed: Slower (~2s)
    • +
    • Speed: ~2s

    Instagram, Facebook
    WhatsApp, Signal, Telegram
    Twitter/X
    - Any platform that recompresses + Any recompressing platform +
    +
    +
    +
    + + +
    +
    +
    + + LSB Mode +
    +
    +

    + LSB (Least Significant Bit) embeds data in the lowest bit of each color channel. Imperceptible to the eye. +

    +
      +
    • Capacity: ~375 KB/MP
    • +
    • Output: PNG (lossless)
    • +
    • Color: Full color
    • +
    • Speed: ~0.5s
    • +
    +
    +
    + Email attachments
    + Cloud storage
    + Direct file transfer
    + Social media
    @@ -149,35 +148,30 @@ Aspect + DCT Mode Default LSB Mode - DCT Mode Capacity (1080p) - ~770 KB ~50 KB + ~770 KB Survives JPEG - ❌ No ✅ Yes + ❌ No Social Media - ❌ Broken ✅ Works + ❌ Broken Detection Resistance - Moderate Better - - - Dependencies - Pillow, NumPy - + scipy, jpegio + Moderate @@ -185,8 +179,7 @@
    - Auto-Detection: When decoding, Stegasoo automatically detects whether - LSB or DCT mode was used. You don't need to specify the mode during decoding. + Auto-Detection: Mode is detected automatically when decoding.
    @@ -196,64 +189,149 @@
    How Security Works
    -

    Stegasoo uses multi-factor authentication to derive encryption keys:

    +

    Multi-factor authentication derives encryption keys:

    -
    -
    +
    +
    Reference Photo
    Something you have
    -
    ~80-256 bits
    +
    ~80-256 bits
    -
    -
    +
    +
    Passphrase
    Something you know
    -
    ~44 bits (4 words)
    +
    ~44 bits (4 words)
    -
    -
    +
    +
    Static PIN
    Something you know
    -
    ~20 bits (6 digits)
    +
    ~20 bits (6 digits)
    -
    -
    +
    +
    RSA Key -
    Something you have (optional)
    -
    ~128 bits
    +
    Optional
    +
    ~128 bits
    - Combined entropy: 144-424+ bits depending on configuration. - For reference, 128 bits is considered computationally infeasible to brute force. + Combined entropy: 144-424+ bits. 128 bits is infeasible to brute force.
    Key Derivation

    {% if has_argon2 %} Argon2id - Using Argon2id with 256MB memory cost — memory-hard KDF that - makes GPU/ASIC attacks infeasible. + 256MB memory cost. Memory-hard KDF defeats GPU/ASIC attacks. {% else %} Argon2 Not Available - Falling back to PBKDF2-SHA512 with 600,000 iterations. + Using PBKDF2-SHA512 with 600k iterations. Install argon2-cffi for stronger security. {% endif %}

    + +
    +
    +
    + Channel Keys + v4.0 +
    +
    +
    +

    + Channel keys provide deployment/group isolation. Messages encoded with one channel key + cannot be decoded with a different key, even if all other credentials match. +

    + +
    + +
    +
    +
    + + Auto +
    +
    +

    Uses server-configured key if available, otherwise public mode.

    +
      +
    • Set via STEGASOO_CHANNEL_KEY env var
    • +
    • Or channel_key in config file
    • +
    • All users share the same channel
    • +
    +
    +
    +
    + + +
    +
    +
    + + Public +
    +
    +

    No channel key. Compatible with other public installations.

    +
      +
    • Default if no server key configured
    • +
    • Anyone can decode (with credentials)
    • +
    • Interoperable between deployments
    • +
    +
    +
    +
    + + +
    +
    +
    + + Custom +
    +
    +

    Your own group key. Share with recipients.

    +
      +
    • Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
    • +
    • 32 chars (128 bits entropy)
    • +
    • Private group communication
    • +
    +
    +
    +
    +
    + + {% if channel_configured %} +
    + + This server has a channel key configured: + {{ channel_fingerprint }} + ({{ channel_source }}) +
    + {% else %} +
    + + This server is running in public mode. + Set STEGASOO_CHANNEL_KEY to enable server-wide channel isolation. +
    + {% endif %} +
    +
    +
    @@ -272,18 +350,18 @@ 4.0.0 - Simplified auth (no date dependency), passphrase replaces day_phrase, - 4-word default, JPEG normalization fix, large image support (14MB+ tested), - subprocess isolation for stability, Python 3.10-3.12 required + Channel keys for group/deployment isolation, + DCT default, simplified auth, passphrase replaces day_phrase, + 4-word default, JPEG fix, large image support, subprocess isolation, Python 3.10-3.12 3.2.0 - Single passphrase (removed day-of-week rotation), increased default words + Single passphrase, more default words 3.0.0 - DCT steganography mode, JPEG output, color preservation option + DCT mode, JPEG output, color preservation 2.2.0 @@ -291,11 +369,11 @@ 2.1.0 - File embedding, compression support + File embedding, compression 2.0.0 - Web UI, REST API, RSA key support + Web UI, REST API, RSA keys 1.0.0 @@ -304,12 +382,6 @@
    - -
    - - Compatibility: v4.0 cannot decode messages from v3.1 or earlier (different format). - Messages encoded with v3.2 should decode correctly. -
    @@ -329,11 +401,11 @@
      -
    1. Both parties agree on a reference photo (shared secretly, never transmitted)
    2. -
    3. Go to Generate and create credentials
    4. -
    5. Memorize the passphrase and PIN
    6. -
    7. If using RSA, download and securely store the key file
    8. -
    9. Share credentials with your contact through a secure channel
    10. +
    11. Agree on a reference photo (never transmitted)
    12. +
    13. Go to Generate to create credentials
    14. +
    15. Memorize passphrase and PIN
    16. +
    17. If using RSA, store the key file securely
    18. +
    19. Share credentials via secure channel
    @@ -343,24 +415,23 @@

    1. Go to Encode
    2. -
    3. Choose your embedding mode: +
    4. Upload reference photo and carrier image
    5. +
    6. Choose mode:
        -
      • LSB – for email, cloud storage, direct transfer
      • -
      • DCT – for social media (Instagram, WhatsApp, etc.)
      • +
      • DCT (default): social media
      • +
      • LSB: email, cloud, direct transfer
    7. -
    8. Upload your reference photo and carrier image
    9. -
    10. Enter your message or select a file to embed
    11. -
    12. Enter your passphrase and PIN/key
    13. -
    14. Download the resulting stego image
    15. -
    16. Send through any channel!
    17. +
    18. Enter message or select file
    19. +
    20. Enter passphrase and PIN/key
    21. +
    22. Download stego image
    @@ -370,22 +441,21 @@

    1. Go to Decode
    2. -
    3. Upload your reference photo (same one used for encoding)
    4. -
    5. Upload the stego image you received
    6. -
    7. Enter your passphrase
    8. -
    9. Enter your PIN and/or RSA key
    10. -
    11. View the decoded message or download the extracted file
    12. +
    13. Upload reference photo
    14. +
    15. Upload stego image
    16. +
    17. Enter passphrase and PIN/key
    18. +
    19. View message or download file
    - Auto-detection: Stegasoo automatically detects LSB vs DCT mode. + Mode is auto-detected.
    @@ -396,67 +466,64 @@
    -
    Limits & Specifications
    +
    Limits & Specs
    - - + + - + - - - - - - + + - + - + + + + + - - + + - + - - + + - - + + - - + + + + + +
    Max text message2 million charactersMax text2M characters
    Max file payloadMax file {{ max_payload_kb }} KB
    Max carrier image24 megapixels (~6000×4000)
    LSB capacity~375 KB/megapixelMax carrier24 MP (~6000x4000)
    DCT capacity~75 KB/megapixel~75 KB/MP
    Max upload sizeLSB capacity~375 KB/MP
    Max upload 30 MB
    Temp file expiry5 minutesFile expiry5 min
    PIN lengthPIN 6-9 digits
    RSA key sizes2048, 3072, 4096 bitsRSA keys2048, 3072, 4096 bit
    Passphrase length3-12 words (BIP-39, recommended: 4+ words)Passphrase3-12 words (BIP-39)
    Python version3.10-3.12 (3.13 not supported)Python Version3.10-3.12
    Built withFlask, Pillow, NumPy, SciPy, jpegio, cryptography, argon2-cffi
    -
    -

    - Stegasoo v{{ version }} • - Open Source • - Built with Python, Flask, and cryptography -

    -
    {% endblock %} diff --git a/frontends/web/templates/decode.html b/frontends/web/templates/decode.html index e6470f1..299175c 100644 --- a/frontends/web/templates/decode.html +++ b/frontends/web/templates/decode.html @@ -266,6 +266,7 @@
    +
    @@ -274,9 +275,11 @@
    If PIN was used during encoding
    +
    +
    @@ -333,9 +336,64 @@
    +
    + +
    +
    + + +
    + + + + + + + + +
    + + + {% if channel_configured %} +
    + + Server: {{ channel_fingerprint }} +
    + {% endif %} + + +
    +
    + + +
    +
    +
    +
    + @@ -355,51 +413,34 @@ v3.0 -
    +
    -
    -
    - - -
    -
    + -
    -
    - - -
    -
    + -
    -
    - - -
    -
    +
    - Auto tries LSB first, then DCT. Use specific mode if you know how it was encoded. + Auto tries LSB first, then DCT. {% if not has_dct %} -
    DCT requires scipy: pip install scipy + DCT requires scipy {% endif %}
    @@ -442,6 +483,10 @@ Format compatibility: v4.0 cannot decode messages from v3.1 or earlier (different format) +
  • + + Channel key: Use the same channel (Auto/Public/Custom) that was used during encoding +
  • If using an RSA key, verify the password is correct (if key is encrypted) @@ -461,34 +506,22 @@ {% block scripts %} {% endblock %} diff --git a/frontends/web/templates/encode.html b/frontends/web/templates/encode.html index 4b0962b..e8390eb 100644 --- a/frontends/web/templates/encode.html +++ b/frontends/web/templates/encode.html @@ -333,17 +333,20 @@
    - -
    - - +
    + +
    + + +
    +
    Static 6-9 digit PIN
    -
    Static 6-9 digit PIN
    +
    @@ -400,9 +403,70 @@
    +
    + +
    +
    + + +
    + + + + + + + + +
    + + + {% if channel_configured %} +
    + + Server: {{ channel_fingerprint }} +
    + {% endif %} + + +
    +
    + + + +
    +
    + Invalid format. Use: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX +
    +
    +
    +
    +
    -
    +
    -
    - - DCT defaults: Color mode + JPEG output for best social media compatibility. -
    - - +
    -
  • Color preserved - extraction works on both color and grayscale
  • {% endif %} {% endif %} + {% if channel_mode == 'private' %} +
  • Recipient needs the same channel key to decode
  • + {% endif %}
    diff --git a/frontends/web/templates/generate.html b/frontends/web/templates/generate.html index ce63b56..476aecd 100644 --- a/frontends/web/templates/generate.html +++ b/frontends/web/templates/generate.html @@ -3,7 +3,7 @@ {% block title %}Generate Credentials - Stegasoo{% endblock %} {% block content %} -
    +
    @@ -74,6 +74,32 @@
    +
    + + +
    + + +
    + + + + +
    +
    For private groups: generate, then use Custom mode when encoding/decoding.
    +
    + diff --git a/frontends/web/templates/index.html b/frontends/web/templates/index.html index 1069615..9312e2c 100644 --- a/frontends/web/templates/index.html +++ b/frontends/web/templates/index.html @@ -19,6 +19,20 @@
    + +{% if channel_configured %} +
    +
    +
    + + Private Channel Active + Messages are isolated to this deployment +
    + {{ channel_fingerprint }} +
    +
    +{% endif %} +
    @@ -81,22 +95,22 @@
    - - LSB Mode + + DCT Mode Default
    - Higher capacity (~375 KB/MP)
    - Best for email & file transfer + Survives JPEG recompression
    + Best for social media
    - - DCT Mode + + LSB Mode
    - Survives JPEG recompression
    - Best for social media + Higher capacity (~375 KB/MP)
    + Best for email & file transfer
    @@ -116,15 +130,15 @@
    • - Reference Photo — shared secret image + Reference Photo: shared secret
    • - Passphrase — 4+ words + Passphrase: 4+ words
    • - PIN — 6-9 digits (and/or RSA key) + PIN: 6-9 digits (or RSA key)
    @@ -143,6 +157,11 @@ Pseudo-random embedding +
  • + + Channel keys for group isolation + v4.0 +
  • diff --git a/src/stegasoo/__init__.py b/src/stegasoo/__init__.py index 80ff686..c95e580 100644 --- a/src/stegasoo/__init__.py +++ b/src/stegasoo/__init__.py @@ -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 from .encode import encode @@ -28,7 +33,19 @@ from .image_utils import ( from .utils import generate_filename # 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 from .steganography import ( @@ -150,6 +167,18 @@ __all__ = [ "export_rsa_key_pem", "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 "get_image_info", "compare_capacity", @@ -183,6 +212,7 @@ __all__ = [ "validate_embed_mode", "validate_dct_output_format", "validate_dct_color_mode", + "validate_channel_key", # Models "ImageInfo", diff --git a/src/stegasoo/channel.py b/src/stegasoo/channel.py index 1f05951..df79fcf 100644 --- a/src/stegasoo/channel.py +++ b/src/stegasoo/channel.py @@ -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. 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 3. None (public mode - compatible with any instance without a channel key) -STATUS: This module is IMPLEMENTED but NOT YET INTEGRATED into crypto.py. - The get_channel_key_hash() function should be mixed into key derivation - in a future release. - -TODO (v3.3.0): -- Integrate get_channel_key_hash() into derive_hybrid_key() in crypto.py -- Add --channel-key option to CLI -- Add channel key display to web UI -- Document channel key feature in README +INTEGRATION STATUS (v4.0.0): +- ✅ get_channel_key_hash() integrated into derive_hybrid_key() in crypto.py +- ✅ get_channel_key_hash() integrated into derive_pixel_key() in crypto.py +- ✅ channel_key parameter added to encode() and decode() functions +- ✅ Header flags indicate whether message was encoded with channel key +- ✅ Helpful error messages for channel key mismatches """ 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. - 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. - NOTE: This function is implemented but not yet integrated into crypto.py. - See TODO at top of file for integration plan. - Args: key: Channel key (if None, reads from config) diff --git a/src/stegasoo/constants.py b/src/stegasoo/constants.py index cc152ca..8402214 100644 --- a/src/stegasoo/constants.py +++ b/src/stegasoo/constants.py @@ -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. 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: - Removed date dependency from cryptographic operations - 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 @@ -28,8 +31,9 @@ MAGIC_HEADER = b'\x89ST3' # FORMAT VERSION HISTORY: # Version 1-3: Date-dependent encryption (v3.0.x - v3.1.x) -# Version 4: Date-independent encryption (v3.2.0+) - BREAKING CHANGE -FORMAT_VERSION = 4 +# Version 4: Date-independent encryption (v3.2.0) +# Version 5: Channel key support (v4.0.0) - adds flags byte to header +FORMAT_VERSION = 5 # Payload type markers PAYLOAD_TEXT = 0x01 diff --git a/src/stegasoo/crypto.py b/src/stegasoo/crypto.py index b3ff338..402ff75 100644 --- a/src/stegasoo/crypto.py +++ b/src/stegasoo/crypto.py @@ -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. 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: - Removed date dependency from key derivation - 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 @@ -46,6 +49,51 @@ except ImportError: 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: """ Compute deterministic hash of photo pixel content. @@ -73,7 +121,8 @@ def derive_hybrid_key( passphrase: str, salt: bytes, pin: str = "", - rsa_key_data: Optional[bytes] = None + rsa_key_data: Optional[bytes] = None, + channel_key: Optional[Union[str, bool]] = None, ) -> bytes: """ Derive encryption key from multiple factors. @@ -83,19 +132,21 @@ def derive_hybrid_key( - Passphrase (something you know) - PIN (something you know, static) - RSA key (something you have) + - Channel key (deployment/group binding) - Salt (random per message) 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: photo_data: Reference photo bytes passphrase: Shared passphrase (recommend 4+ words) salt: Random salt for this message pin: Optional static PIN 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: 32-byte derived key @@ -106,6 +157,10 @@ def derive_hybrid_key( try: photo_hash = hash_photo(photo_data) + # Resolve channel key + channel_hash = _resolve_channel_key(channel_key) + + # Build key material key_material = ( photo_hash + passphrase.lower().encode() + @@ -117,6 +172,10 @@ def derive_hybrid_key( if rsa_key_data: 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: key = hash_secret_raw( secret=key_material, @@ -147,7 +206,8 @@ def derive_pixel_key( photo_data: bytes, passphrase: str, pin: str = "", - rsa_key_data: Optional[bytes] = None + rsa_key_data: Optional[bytes] = None, + channel_key: Optional[Union[str, bool]] = None, ) -> bytes: """ Derive key for pseudo-random pixel selection. @@ -155,19 +215,21 @@ def derive_pixel_key( This key determines which pixels are used for embedding, making the message location unpredictable without the correct inputs. - NOTE: v3.2.0 removed date dependency. - Args: photo_data: Reference photo bytes passphrase: Shared passphrase pin: Optional static PIN rsa_key_data: Optional RSA key bytes + channel_key: Channel key parameter (see derive_hybrid_key) Returns: 32-byte key for pixel selection """ photo_hash = hash_photo(photo_data) + # Resolve channel key + channel_hash = _resolve_channel_key(channel_key) + material = ( photo_hash + passphrase.lower().encode() + @@ -177,6 +239,10 @@ def derive_pixel_key( if rsa_key_data: 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() @@ -284,19 +350,29 @@ def _unpack_payload(data: bytes) -> DecodeResult: 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( message: Union[str, bytes, FilePayload], photo_data: bytes, passphrase: str, pin: str = "", - rsa_key_data: Optional[bytes] = None + rsa_key_data: Optional[bytes] = None, + channel_key: Optional[Union[str, bool]] = None, ) -> bytes: """ 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) - - Version (1 byte) = 4 + - Version (1 byte) = 5 + - Flags (1 byte) - indicates if channel key was used - Salt (32 bytes) - IV (12 bytes) - Auth tag (16 bytes) @@ -308,6 +384,10 @@ def encrypt_message( passphrase: Shared passphrase (recommend 4+ words for good entropy) pin: Optional static PIN 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: Encrypted message bytes @@ -317,9 +397,15 @@ def encrypt_message( """ try: 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) + # Determine flags + flags = 0 + channel_hash = _resolve_channel_key(channel_key) + if channel_hash: + flags |= FLAG_CHANNEL_KEY + # Pack payload with type marker 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)) padded_message = packed_payload + padding + # Build header for AAD + header = MAGIC_HEADER + bytes([FORMAT_VERSION, flags]) + # Encrypt with AES-256-GCM cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend()) encryptor = cipher.encryptor() - encryptor.authenticate_additional_data(MAGIC_HEADER + bytes([FORMAT_VERSION])) + encryptor.authenticate_additional_data(header) ciphertext = encryptor.update(padded_message) + encryptor.finalize() - # v3.2.0: Simplified header without date + # v4.0.0: Header with flags byte return ( - MAGIC_HEADER + - bytes([FORMAT_VERSION]) + + header + salt + iv + encryptor.tag + @@ -354,16 +442,16 @@ def parse_header(encrypted_data: bytes) -> Optional[dict]: """ 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: encrypted_data: Raw encrypted bytes 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 - if len(encrypted_data) < 65 or encrypted_data[:4] != MAGIC_HEADER: + # Min size: Magic(4) + Version(1) + Flags(1) + Salt(32) + IV(12) + Tag(16) = 66 bytes + if len(encrypted_data) < 66 or encrypted_data[:4] != MAGIC_HEADER: return None try: @@ -371,7 +459,9 @@ def parse_header(encrypted_data: bytes) -> Optional[dict]: if version != FORMAT_VERSION: return None - offset = 5 + flags = encrypted_data[5] + + offset = 6 salt = encrypted_data[offset:offset + SALT_SIZE] offset += SALT_SIZE iv = encrypted_data[offset:offset + IV_SIZE] @@ -381,6 +471,9 @@ def parse_header(encrypted_data: bytes) -> Optional[dict]: ciphertext = encrypted_data[offset:] return { + 'version': version, + 'flags': flags, + 'has_channel_key': bool(flags & FLAG_CHANNEL_KEY), 'salt': salt, 'iv': iv, 'tag': tag, @@ -395,10 +488,11 @@ def decrypt_message( photo_data: bytes, passphrase: str, pin: str = "", - rsa_key_data: Optional[bytes] = None + rsa_key_data: Optional[bytes] = None, + channel_key: Optional[Union[str, bool]] = None, ) -> DecodeResult: """ - Decrypt message (v3.2.0 - no date needed). + Decrypt message (v4.0.0 - with channel key support). Args: encrypted_data: Encrypted message bytes @@ -406,6 +500,7 @@ def decrypt_message( passphrase: Shared passphrase pin: Optional static PIN rsa_key_data: Optional RSA key bytes + channel_key: Channel key parameter (see encrypt_message) Returns: DecodeResult with decrypted content @@ -418,18 +513,26 @@ def decrypt_message( if not 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: 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( algorithms.AES(key), modes.GCM(header['iv'], header['tag']), backend=default_backend() ) 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() original_length = struct.unpack('>I', padded_plaintext[-4:])[0] @@ -437,14 +540,25 @@ def decrypt_message( payload_data = padded_plaintext[:original_length] result = _unpack_payload(payload_data) - # Note: No date_encoded field in v3.2.0 - return result except Exception as e: - raise DecryptionError( - "Decryption failed. Check your passphrase, PIN, RSA key, and reference photo." - ) from e + # Provide more helpful error message for channel key issues + if message_has_key and not has_configured_key: + raise DecryptionError( + "Decryption failed. This message was encoded with a channel key, " + "but no channel key is configured. Provide the correct channel key." + ) from e + elif not message_has_key and has_configured_key: + raise DecryptionError( + "Decryption failed. This message was encoded without a channel key, " + "but you have one configured. Try with channel_key='' for public mode." + ) from e + else: + raise DecryptionError( + "Decryption failed. Check your passphrase, PIN, RSA key, " + "reference photo, and channel key." + ) from e def decrypt_message_text( @@ -452,7 +566,8 @@ def decrypt_message_text( photo_data: bytes, passphrase: str, pin: str = "", - rsa_key_data: Optional[bytes] = None + rsa_key_data: Optional[bytes] = None, + channel_key: Optional[Union[str, bool]] = None, ) -> str: """ Decrypt message and return as text string. @@ -465,6 +580,7 @@ def decrypt_message_text( passphrase: Shared passphrase pin: Optional static PIN rsa_key_data: Optional RSA key bytes + channel_key: Channel key parameter Returns: Decrypted message string @@ -472,7 +588,7 @@ def decrypt_message_text( Raises: 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.file_data: @@ -491,3 +607,29 @@ def decrypt_message_text( def has_argon2() -> bool: """Check if Argon2 is available.""" 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() diff --git a/src/stegasoo/debug.py b/src/stegasoo/debug.py index bd05cf4..e1a43f4 100644 --- a/src/stegasoo/debug.py +++ b/src/stegasoo/debug.py @@ -98,8 +98,8 @@ def memory_usage() -> Dict[str, Union[float, str]]: mem_info = process.memory_info() return { - 'rss_mb': mem_info.rss / 1024 / 1024, # Resident Set Size - 'vms_mb': mem_info.vms / 1024 / 1024, # Virtual Memory Size + 'rss_mb': mem_info.rss / 1024 / 1024, + 'vms_mb': mem_info.vms / 1024 / 1024, 'percent': process.memory_percent(), } 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): chunk = data_to_dump[i:i+16] 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) 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) -# Create singleton instance for easy import class Debug: """Debugging utility class.""" diff --git a/src/stegasoo/decode.py b/src/stegasoo/decode.py index 498575e..9f1650d 100644 --- a/src/stegasoo/decode.py +++ b/src/stegasoo/decode.py @@ -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. + +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 .models import DecodeInput, DecodeResult @@ -29,6 +33,7 @@ def decode( rsa_key_data: Optional[bytes] = None, rsa_password: Optional[str] = None, embed_mode: str = EMBED_MODE_AUTO, + channel_key: Optional[Union[str, bool]] = None, ) -> DecodeResult: """ 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_password: Optional RSA key password 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: DecodeResult with message or file data @@ -57,9 +66,19 @@ def decode( ... else: ... with open(result.filename, 'wb') as f: ... 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, " - 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 require_valid_image(stego_image, "Stego image") @@ -71,10 +90,10 @@ def decode( if rsa_key_data: 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 pixel_key = derive_pixel_key( - reference_photo, passphrase, pin, rsa_key_data + reference_photo, passphrase, pin, rsa_key_data, channel_key ) # Extract encrypted data @@ -90,9 +109,9 @@ def decode( debug.print(f"Extracted {len(encrypted)} bytes from image") - # Decrypt + # Decrypt (with channel key) 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}") @@ -108,6 +127,7 @@ def decode_file( rsa_key_data: Optional[bytes] = None, rsa_password: Optional[str] = None, embed_mode: str = EMBED_MODE_AUTO, + channel_key: Optional[Union[str, bool]] = None, ) -> Path: """ 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_password: Optional RSA key password embed_mode: 'auto', 'lsb', or 'dct' + channel_key: Channel key parameter (see decode()) Returns: Path where file was saved @@ -136,6 +157,7 @@ def decode_file( rsa_key_data, rsa_password, embed_mode, + channel_key, ) if not result.is_file: @@ -163,6 +185,7 @@ def decode_text( rsa_key_data: Optional[bytes] = None, rsa_password: Optional[str] = None, embed_mode: str = EMBED_MODE_AUTO, + channel_key: Optional[Union[str, bool]] = None, ) -> str: """ Decode a text message from a stego image. @@ -177,6 +200,7 @@ def decode_text( rsa_key_data: Optional RSA key bytes rsa_password: Optional RSA key password embed_mode: 'auto', 'lsb', or 'dct' + channel_key: Channel key parameter (see decode()) Returns: Decoded message string @@ -192,6 +216,7 @@ def decode_text( rsa_key_data, rsa_password, embed_mode, + channel_key, ) if result.is_file: diff --git a/src/stegasoo/encode.py b/src/stegasoo/encode.py index 8ea6f73..5f98407 100644 --- a/src/stegasoo/encode.py +++ b/src/stegasoo/encode.py @@ -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. + +Changes in v4.0.0: +- Added channel_key parameter for deployment/group isolation """ from typing import Optional, Union @@ -34,6 +37,7 @@ def encode( embed_mode: str = EMBED_MODE_LSB, dct_output_format: str = "png", dct_color_mode: str = "grayscale", + channel_key: Optional[Union[str, bool]] = None, ) -> EncodeResult: """ Encode a message or file into an image. @@ -50,6 +54,10 @@ def encode( embed_mode: 'lsb' (default) or 'dct' dct_output_format: For DCT mode - 'png' or 'jpeg' 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: EncodeResult with stego image and metadata @@ -64,9 +72,20 @@ def encode( ... ) >>> with open('stego.png', 'wb') as f: ... 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, " - 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 require_valid_payload(message) @@ -79,16 +98,16 @@ def encode( if rsa_key_data: require_valid_rsa_key(rsa_key_data, rsa_password) - # Encrypt message + # Encrypt message (with channel key) 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") - # Derive pixel/coefficient selection key + # Derive pixel/coefficient selection key (with channel 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 @@ -114,7 +133,7 @@ def encode( pixels_modified=stats.pixels_modified, total_pixels=stats.total_pixels, 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: # DCT mode stats @@ -141,6 +160,7 @@ def encode_file( embed_mode: str = EMBED_MODE_LSB, dct_output_format: str = "png", dct_color_mode: str = "grayscale", + channel_key: Optional[Union[str, bool]] = None, ) -> EncodeResult: """ Encode a file into an image. @@ -160,6 +180,7 @@ def encode_file( embed_mode: 'lsb' or 'dct' dct_output_format: 'png' or 'jpeg' dct_color_mode: 'grayscale' or 'color' + channel_key: Channel key parameter (see encode()) Returns: EncodeResult @@ -178,6 +199,7 @@ def encode_file( embed_mode=embed_mode, dct_output_format=dct_output_format, dct_color_mode=dct_color_mode, + channel_key=channel_key, ) @@ -195,6 +217,7 @@ def encode_bytes( embed_mode: str = EMBED_MODE_LSB, dct_output_format: str = "png", dct_color_mode: str = "grayscale", + channel_key: Optional[Union[str, bool]] = None, ) -> EncodeResult: """ Encode raw bytes with metadata into an image. @@ -213,6 +236,7 @@ def encode_bytes( embed_mode: 'lsb' or 'dct' dct_output_format: 'png' or 'jpeg' dct_color_mode: 'grayscale' or 'color' + channel_key: Channel key parameter (see encode()) Returns: EncodeResult @@ -231,4 +255,5 @@ def encode_bytes( embed_mode=embed_mode, dct_output_format=dct_output_format, dct_color_mode=dct_color_mode, + channel_key=channel_key, )