From a001f227ecf2435a47433cf4c110bd4797a0b444 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Thu, 1 Jan 2026 13:40:27 -0500 Subject: [PATCH] Bug fixes, CLI updates, docs. --- frontends/CLI.md | 698 ++++++++++++++++++++---------- frontends/cli/main.py | 187 ++++---- src/main.py | 47 +- src/stegasoo/batch.py | 218 ++++++++-- src/stegasoo/channel.py | 17 +- src/stegasoo/cli.py | 78 ++-- src/stegasoo/dct_steganography.py | 104 ++++- src/stegasoo/keygen.py | 161 +++++-- src/stegasoo/steganography.py | 45 +- 9 files changed, 1110 insertions(+), 445 deletions(-) diff --git a/frontends/CLI.md b/frontends/CLI.md index 1fdfa23..d58a1ba 100644 --- a/frontends/CLI.md +++ b/frontends/CLI.md @@ -1,16 +1,21 @@ -# Stegasoo CLI Documentation +# Stegasoo CLI Documentation (v3.2.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) - [Quick Start](#quick-start) - [Commands](#commands) - [generate](#generate-command) - [encode](#encode-command) - [decode](#decode-command) + - [verify](#verify-command) - [info](#info-command) + - [compare](#compare-command) + - [modes](#modes-command) + - [strip-metadata](#strip-metadata-command) - [Embedding Modes](#embedding-modes) - [Security Factors](#security-factors) - [Workflow Examples](#workflow-examples) @@ -50,22 +55,43 @@ stegasoo --version stegasoo --help # Check DCT support -python -c "from stegasoo.dct_steganography import has_jpegio_support; print('jpegio:', has_jpegio_support())" +python -c "from stegasoo import has_dct_support; print('DCT:', 'available' if has_dct_support() else 'requires scipy')" ``` --- +## What's New in v3.2.0 + +Version 3.2.0 brings major simplifications to the authentication system: + +| 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` | + +**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) + +**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. + +--- + ## Quick Start ```bash # 1. Generate credentials (do this once, memorize results) -stegasoo generate --pin --words 3 +stegasoo generate # 2. Encode a message (LSB mode - default) stegasoo encode \ --ref secret_photo.jpg \ --carrier meme.png \ - --phrase "apple forest thunder" \ + --passphrase "apple forest thunder mountain" \ --pin 123456 \ --message "Meet at midnight" @@ -73,17 +99,17 @@ stegasoo encode \ stegasoo encode \ --ref secret_photo.jpg \ --carrier meme.png \ - --phrase "apple forest thunder" \ + --passphrase "apple forest thunder mountain" \ --pin 123456 \ --message "Meet at midnight" \ --mode dct \ - --format jpeg + --dct-format jpeg # 4. Decode a message (auto-detects mode) stegasoo decode \ --ref secret_photo.jpg \ - --stego stego_abc123_20251227.png \ - --phrase "apple forest thunder" \ + --stego stego_abc123.png \ + --passphrase "apple forest thunder mountain" \ --pin 123456 ``` @@ -93,7 +119,7 @@ stegasoo decode \ ### Generate Command -Generate credentials for encoding/decoding operations. Creates daily passphrases and optionally a PIN and/or RSA key. +Generate credentials for encoding/decoding operations. Creates a passphrase and optionally a PIN and/or RSA key. #### Synopsis @@ -109,7 +135,7 @@ 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 | 3 | Words per daily phrase | +| `--words` | | 3-12 | 4 | Words in passphrase (v3.2.0: default increased to 4) | | `--output` | `-o` | path | | Save RSA key to file | | `--password` | `-p` | string | | Password for RSA key file | | `--json` | | flag | | Output as JSON | @@ -123,9 +149,9 @@ stegasoo generate Output: ``` -═══════════════════════════════════════════════════════════════ - STEGASOO CREDENTIALS -═══════════════════════════════════════════════════════════════ +============================================================ + STEGASOO CREDENTIALS (v3.2.0) +============================================================ ⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW Do not screenshot or save to file! @@ -133,20 +159,21 @@ Output: ─── STATIC PIN ─── 847293 -─── DAILY PHRASES ─── - Monday │ abandon ability able - Tuesday │ actor actress actual - Wednesday │ advice aerobic affair - Thursday │ afraid again age - Friday │ agree ahead aim - Saturday │ airport aisle alarm - Sunday │ album alcohol alert +─── PASSPHRASE ─── + abandon ability able about ─── SECURITY ─── - Phrase entropy: 33 bits - PIN entropy: 19 bits - Combined: 52 bits - + photo entropy: 80-256 bits + 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 +stegasoo generate --words 6 ``` **Generate with RSA key:** @@ -159,14 +186,34 @@ stegasoo generate --rsa --rsa-bits 4096 stegasoo generate --rsa -o mykey.pem -p "mysecretpassword" ``` -**Maximum security (longer phrases + both factors):** +**Maximum security (longer passphrase + both factors):** ```bash stegasoo generate --pin --rsa --words 6 --pin-length 9 ``` **JSON output for scripting:** ```bash -stegasoo generate --json | jq '.phrases.Monday' +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):** @@ -178,7 +225,7 @@ stegasoo generate --no-pin --rsa -o key.pem -p "password123" ### Encode Command -Encode a secret message into an image using steganography. +Encode a secret message or file into an image using steganography. #### Synopsis @@ -192,36 +239,42 @@ stegasoo encode [OPTIONS] |--------|-------|------|----------|---------|-------------| | `--ref` | `-r` | path | ✓ | | Reference photo (shared secret) | | `--carrier` | `-c` | path | ✓ | | Carrier image to hide message in | -| `--phrase` | `-p` | string | ✓ | | Today's passphrase | +| `--passphrase` | `-p` | string | ✓ | | Passphrase (v3.2.0: single, no date needed) | | `--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 | | `--output` | `-o` | path | | | Output filename | -| `--date` | | YYYY-MM-DD | | | Date override | | `--mode` | | choice | | `lsb` | Embedding mode: `lsb` or `dct` | -| `--format` | | choice | | `png` | Output format: `png` or `jpeg` (DCT only) | -| `--color` | | choice | | `color` | Color mode: `color` or `grayscale` (DCT only) | +| `--dct-format` | | choice | | `png` | DCT output: `png` or `jpeg` | +| `--dct-color` | | choice | | `grayscale` | DCT color: `grayscale` or `color` | | `--quiet` | `-q` | flag | | | Suppress output | -\* At least one of `--pin` or `--key` is required. +\* At least one of `--pin`, `--key`, or `--key-qr` is required. #### Message Input Methods 1. **Command line argument:** ```bash - stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "Secret message" + 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 "phrase" --pin 123456 -f message.txt + 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 "phrase" --pin 123456 + 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 ``` #### Examples @@ -231,19 +284,18 @@ stegasoo encode [OPTIONS] stegasoo encode \ --ref photos/vacation.jpg \ --carrier memes/funny_cat.png \ - --phrase "correct horse battery" \ + --passphrase "correct horse battery staple" \ --pin 847293 \ --message "The package arrives Tuesday" ``` Output: ``` +Mode: LSB (12.4% capacity) ✓ Encoded successfully! - Output: a1b2c3d4_20251227.png + Output: a1b2c3d4.png Size: 245,832 bytes - Mode: LSB Capacity used: 12.4% - Date: 2025-12-27 ``` **DCT mode for social media (JPEG output):** @@ -251,48 +303,34 @@ Output: stegasoo encode \ --ref photos/vacation.jpg \ --carrier memes/funny_cat.png \ - --phrase "correct horse battery" \ + --passphrase "correct horse battery staple" \ --pin 847293 \ --message "The package arrives Tuesday" \ --mode dct \ - --format jpeg + --dct-format jpeg ``` Output: ``` +Mode: DCT (grayscale, JPEG) (45.2% capacity) ✓ Encoded successfully! - Output: a1b2c3d4_20251227.jpg + Output: a1b2c3d4.jpg Size: 89,432 bytes - Mode: DCT (color, jpeg) Capacity used: 45.2% - Date: 2025-12-27 - - ⚠️ DCT mode is experimental + DCT output: JPEG (grayscale) ``` -**DCT mode with PNG output (maximum DCT capacity):** +**DCT mode with color preservation:** ```bash stegasoo encode \ -r ref.jpg \ -c carrier.png \ - -p "phrase words here" \ - --pin 123456 \ - -m "Longer message that needs more space" \ - --mode dct \ - --format png \ - --color color -``` - -**DCT grayscale mode:** -```bash -stegasoo encode \ - -r ref.jpg \ - -c bw_photo.png \ - -p "phrase" \ + -p "phrase words here now" \ --pin 123456 \ -m "Message" \ --mode dct \ - --color grayscale + --dct-color color \ + --dct-format png ``` **With RSA key:** @@ -300,7 +338,7 @@ stegasoo encode \ stegasoo encode \ -r reference.jpg \ -c carrier.png \ - -p "apple forest thunder" \ + -p "apple forest thunder mountain" \ -k mykey.pem \ --key-password "secretpassword" \ -m "Encrypted with RSA" @@ -311,7 +349,7 @@ stegasoo encode \ stegasoo encode \ -r ref.jpg \ -c carrier.png \ - -p "word1 word2 word3" \ + -p "word1 word2 word3 word4" \ --pin 123456 \ -k mykey.pem \ --key-password "pass" \ @@ -323,31 +361,20 @@ stegasoo encode \ stegasoo encode \ -r ref.jpg \ -c carrier.png \ - -p "phrase words here" \ + -p "phrase words here now" \ --pin 123456 \ -m "Message" \ -o holiday_photo.png ``` -**Encoding with specific date (for testing):** -```bash -stegasoo encode \ - -r ref.jpg \ - -c carrier.png \ - -p "monday phrase here" \ - --pin 123456 \ - -m "Message" \ - --date 2025-12-29 -``` - -**Long message from file:** +**Embed a binary file:** ```bash stegasoo encode \ -r ref.jpg \ -c large_image.png \ - -p "phrase" \ + -p "secure phrase words here" \ --pin 123456 \ - -f secret_document.txt \ + -e secret_document.pdf \ -o output.png ``` @@ -361,7 +388,7 @@ stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "msg" -q - ### Decode Command -Decode a secret message from a stego image. **Automatically detects LSB vs DCT mode.** +Decode a secret message or file from a stego image. **Automatically detects LSB vs DCT mode.** #### Synopsis @@ -375,12 +402,15 @@ stegasoo decode [OPTIONS] |--------|-------|------|----------|-------------| | `--ref` | `-r` | path | ✓ | Reference photo (same as encoding) | | `--stego` | `-s` | path | ✓ | Stego image to decode | -| `--phrase` | `-p` | string | ✓ | Passphrase for the encoding day | +| `--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. @@ -391,14 +421,13 @@ stegasoo decode [OPTIONS] stegasoo decode \ --ref photos/vacation.jpg \ --stego received_image.png \ - --phrase "correct horse battery" \ + --passphrase "correct horse battery staple" \ --pin 847293 ``` Output: ``` ✓ Decoded successfully! - Mode detected: LSB The package arrives Tuesday ``` @@ -408,24 +437,16 @@ The package arrives Tuesday stegasoo decode \ --ref photos/vacation.jpg \ --stego received_image.jpg \ - --phrase "correct horse battery" \ + --passphrase "correct horse battery staple" \ --pin 847293 ``` -Output: -``` -✓ Decoded successfully! - Mode detected: DCT - -The package arrives Tuesday -``` - **With RSA key:** ```bash stegasoo decode \ -r reference.jpg \ -s stego_image.png \ - -p "apple forest thunder" \ + -p "apple forest thunder mountain" \ -k mykey.pem \ --key-password "secretpassword" ``` @@ -435,7 +456,7 @@ stegasoo decode \ stegasoo decode \ -r ref.jpg \ -s stego.png \ - -p "phrase" \ + -p "passphrase words here now" \ --pin 123456 \ -o decoded_message.txt ``` @@ -461,6 +482,77 @@ The package arrives Tuesday 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 +``` + +--- + +### Verify Command + +Verify that a stego image can be decoded without extracting the actual message content. + +#### Synopsis + +```bash +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 | + +#### Examples + +**Basic verification:** +```bash +stegasoo verify -r photo.jpg -s stego.png -p "my passphrase here" --pin 123456 +``` + +Output: +``` +✓ Valid stego image + Payload: text (142 bytes) + Size: 142 bytes +``` + +**JSON output:** +```bash +stegasoo verify -r photo.jpg -s stego.png -p "words here" --pin 123456 --json +``` + +Output: +```json +{ + "valid": true, + "stego_file": "stego.png", + "payload_type": "text", + "payload_size": 142 +} +``` + +**Failed verification:** +```bash +stegasoo verify -r photo.jpg -s stego.png -p "wrong passphrase" --pin 123456 +``` + +Output: +``` +✗ Verification failed + Error: Decryption failed: Invalid authentication tag +``` + --- ### Info Command @@ -470,14 +562,14 @@ Display information about an image's capacity for both LSB and DCT modes. #### Synopsis ```bash -stegasoo info IMAGE +stegasoo info IMAGE [OPTIONS] ``` -#### Arguments +#### Options -| Argument | Type | Description | -|----------|------|-------------| -| `IMAGE` | path | Path to image file | +| Option | Type | Description | +|--------|------|-------------| +| `--json` | flag | Output as JSON | #### Examples @@ -494,40 +586,222 @@ Image: vacation_photo.png Mode: RGB Format: PNG -Capacity: - LSB Mode: ~776,970 bytes (758 KB) - DCT Mode: ~64,800 bytes (63 KB) [approximate] - - Note: DCT capacity varies based on image content + 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 ``` -**Check stego image (shows encoding date and mode):** +**JSON output:** ```bash -stegasoo info stego_a1b2c3d4_20251227.png +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 + +```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: ``` -Image: stego_a1b2c3d4_20251227.png - Dimensions: 1920 × 1080 - Pixels: 2,073,600 - Mode: RGB - Format: PNG +=== Mode Comparison: carrier.png === + Dimensions: 1920 × 1080 -Stego Info: - Embed date: 2025-12-27 (Saturday) - Embed mode: DCT (detected) + ┌─── 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 +``` -Capacity: - LSB Mode: ~776,970 bytes (758 KB) - DCT Mode: ~64,800 bytes (63 KB) [approximate] +**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 +``` + +--- + +### Strip-Metadata Command + +Remove all metadata (EXIF, GPS, etc.) from an image. + +#### Synopsis + +```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 | + +#### Examples + +```bash +# Strip metadata, save as PNG +stegasoo strip-metadata photo.jpg -o clean.png + +# Overwrite in place (converts to PNG) +stegasoo strip-metadata photo.jpg +``` + +Output: +``` +✓ Metadata stripped + Input: photo.jpg (2,456,789 bytes) + Output: clean.png (1,234,567 bytes) ``` --- ## Embedding Modes -Stegasoo v3.0+ supports two steganography algorithms. +Stegasoo supports two steganography algorithms. ### LSB Mode (Default) @@ -540,58 +814,61 @@ stegasoo encode ... --mode lsb | Aspect | Details | |--------|---------| -| **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) | +| **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 (Experimental) +### DCT Mode **Discrete Cosine Transform** embedding hides data in frequency coefficients. ```bash -stegasoo encode ... --mode dct --format jpeg --color color +stegasoo encode ... --mode dct --dct-format jpeg --dct-color color ``` | Aspect | Details | |--------|---------| | **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080) | | **Output** | PNG or JPEG | -| **Resilience** | ✅ Survives JPEG compression | -| **Best For** | Social media, messaging apps | - -> ⚠️ **Experimental**: DCT mode may have edge cases. Test with your workflow. +| **Resilience** | ✅ Better resistance to analysis | +| **Best For** | Social media, stealth requirements | ### DCT Options | Option | Values | Default | Description | |--------|--------|---------|-------------| -| `--format` | `png`, `jpeg` | `png` | Output image format | -| `--color` | `color`, `grayscale` | `color` | Color processing | +| `--dct-format` | `png`, `jpeg` | `png` | Output image format | +| `--dct-color` | `grayscale`, `color` | `grayscale` | Color processing | ### Choosing the Right Mode ``` -Will the image be recompressed? -(social media, messaging apps, etc.) +Need maximum capacity? │ ┌──────┴──────┐ ▼ ▼ YES NO │ │ ▼ ▼ -Use DCT Use LSB ---mode dct (default) ---format jpeg +Use LSB Need stealth? +(default) │ + ┌──────┴──────┐ + ▼ ▼ + YES NO + │ │ + ▼ ▼ + Use DCT Use LSB + --mode dct (default) ``` ### Capacity Comparison | Mode | 1920×1080 Capacity | |------|-------------------| -| LSB (PNG) | ~770 KB | +| LSB (PNG) | ~375 KB | | DCT (PNG) | ~65 KB | -| DCT (JPEG) | ~30-50 KB | +| DCT (JPEG) | ~50 KB | --- @@ -602,81 +879,81 @@ Stegasoo uses multiple authentication factors: | Factor | Description | Entropy | |--------|-------------|---------| | Reference Photo | A photo both parties have | ~80-256 bits | -| Day Phrase | Changes daily (e.g., 3 BIP-39 words) | ~33 bits (3 words) | -| Static PIN | Same every day (6-9 digits) | ~20 bits (6 digits) | +| 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 -- Day phrase is always required +- Passphrase is always required ### Security Configurations | Configuration | Entropy (excl. photo) | Use Case | |--------------|----------------------|----------| -| 3-word phrase + 6-digit PIN | ~53 bits | Casual use | -| 6-word phrase + 9-digit PIN | ~96 bits | Standard security | -| 3-word phrase + RSA 2048 | ~161 bits | File-based auth | -| 6-word phrase + PIN + RSA | ~224 bits | Maximum security | +| 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 | --- ## Workflow Examples -### Daily Secure Communication +### Basic Secure Communication **Setup (once):** ```bash -# Both parties generate same credentials -stegasoo generate --pin --words 3 +# Both parties generate credentials +stegasoo generate # Or share RSA key securely stegasoo generate --rsa -o shared_key.pem -p "agreedpassword" # Securely transfer shared_key.pem to recipient ``` -**Sender (daily - private channel):** +**Sender:** ```bash # For email, file transfer, etc. (no recompression) stegasoo encode \ -r our_shared_photo.jpg \ -c random_meme.png \ - -p "$TODAY_PHRASE" \ + -p "our shared passphrase here" \ --pin 847293 \ -m "Meeting moved to 3pm" ``` -**Sender (daily - social media):** +**Sender (social media):** ```bash -# For Instagram, Twitter, WhatsApp, etc. +# For platforms that may recompress stegasoo encode \ -r our_shared_photo.jpg \ -c random_meme.png \ - -p "$TODAY_PHRASE" \ + -p "our shared passphrase here" \ --pin 847293 \ -m "Meeting moved to 3pm" \ --mode dct \ - --format jpeg + --dct-format jpeg ``` -**Recipient (daily):** +**Recipient:** ```bash # Works for both LSB and DCT (auto-detected) stegasoo decode \ -r our_shared_photo.jpg \ -s received_image.png \ - -p "monday phrase words" \ + -p "our shared passphrase here" \ --pin 847293 ``` ### Batch Processing -**Encode multiple messages (LSB):** +**Encode multiple messages:** ```bash #!/bin/bash -PHRASE="apple forest thunder" +PASSPHRASE="apple forest thunder mountain" PIN="123456" REF="reference.jpg" @@ -685,7 +962,7 @@ for file in messages/*.txt; do stegasoo encode \ -r "$REF" \ -c "carriers/${name}.png" \ - -p "$PHRASE" \ + -p "$PASSPHRASE" \ --pin "$PIN" \ -f "$file" \ -o "output/${name}_stego.png" \ @@ -702,56 +979,17 @@ for file in messages/*.txt; do stegasoo encode \ -r "$REF" \ -c "carriers/${name}.png" \ - -p "$PHRASE" \ + -p "$PASSPHRASE" \ --pin "$PIN" \ -f "$file" \ --mode dct \ - --format jpeg \ + --dct-format jpeg \ -o "output/${name}_social.jpg" \ -q echo "Encoded for social: $name" done ``` -### Archive with Date Preservation - -```bash -# Encode with specific date for archival -stegasoo encode \ - -r ref.jpg \ - -c carrier.png \ - -p "archive phrase words" \ - --pin 123456 \ - -m "Historical record" \ - --date 2025-01-15 \ - -o archive_2025-01-15.png -``` - -### Testing Mode Compatibility - -```bash -# Encode with DCT -stegasoo encode \ - -r ref.jpg \ - -c carrier.png \ - -p "test phrase" \ - --pin 123456 \ - -m "Test message" \ - --mode dct \ - --format jpeg \ - -o test_dct.jpg - -# Simulate social media recompression -convert test_dct.jpg -quality 85 test_recompressed.jpg - -# Decode (should still work!) -stegasoo decode \ - -r ref.jpg \ - -s test_recompressed.jpg \ - -p "test phrase" \ - --pin 123456 -``` - --- ## Piping & Scripting @@ -760,12 +998,12 @@ stegasoo decode \ **Encode from pipe:** ```bash -cat secret.txt | stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -o out.png +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" --pin 123456 -q | less +stegasoo decode -r ref.jpg -s stego.png -p "phrase words" --pin 123456 -q | less ``` **Chain with encryption:** @@ -785,12 +1023,12 @@ stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | base64 -d creds=$(stegasoo generate --json) # Extract specific fields +passphrase=$(echo "$creds" | jq -r '.passphrase') pin=$(echo "$creds" | jq -r '.pin') -monday=$(echo "$creds" | jq -r '.phrases.Monday') entropy=$(echo "$creds" | jq -r '.entropy.total') +echo "Passphrase: $passphrase" echo "PIN: $pin" -echo "Monday phrase: $monday" echo "Total entropy: $entropy bits" ``` @@ -806,15 +1044,6 @@ if ! stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q 2>/dev/ fi ``` -### Mode Detection in Scripts - -```bash -#!/bin/bash -# Get mode from verbose output -MODE=$(stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 2>&1 | grep "Mode detected" | awk '{print $3}') -echo "Image was encoded with: $MODE mode" -``` - --- ## Error Handling @@ -825,32 +1054,26 @@ echo "Image was encoded with: $MODE mode" |-------|-------|----------| | "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 | -| "PIN cannot start with zero" | Leading zero in PIN | Use PIN starting with 1-9 | -| "Carrier image too small" | Message exceeds capacity | Use larger carrier or LSB mode | -| "Message too long for DCT capacity" | DCT has less space | Shorten message or use LSB | -| "Decryption failed" | Wrong credentials | Verify phrase, PIN, ref photo | -| "Invalid or missing Stegasoo header" | Wrong mode or corruption | Check mode, try other credentials | -| "RSA key is password-protected" | Missing key password | Add `--key-password` option | -| "jpegio not available" | Missing library | Install: `pip install jpegio` | -| "Invalid --format for LSB mode" | JPEG with LSB | Use `--mode dct` for JPEG output | +| "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` | ### Troubleshooting Decryption Failures -1. **Check the encoding date:** The filename often contains the date (e.g., `_20251227`) -2. **Use correct phrase:** The phrase must match the day the message was encoded, not today -3. **Verify reference photo:** Must be the exact same file, not a resized copy -4. **Check stego image:** +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 heavy recompression may still destroy data -5. **Check embedding mode:** The decoder auto-detects, but if issues persist, verify the original was encoded with the expected mode + - DCT: More resilient but not immune to heavy processing +4. **Verify PIN/key:** Must match exactly what was used for encoding -### DCT-Specific Issues +### v3.2.0 Migration Note -| Issue | Cause | Solution | -|-------|-------|----------| -| "Invalid or missing Stegasoo header" after social media | Heavy recompression | Try higher quality original or shorter message | -| JPEG output not working | jpegio not installed | `pip install jpegio` | -| Lower capacity than expected | Normal for DCT | DCT has ~10% of LSB capacity | +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 --- @@ -859,7 +1082,7 @@ echo "Image was encoded with: $MODE mode" | Code | Meaning | |------|---------| | 0 | Success | -| 1 | General error | +| 1 | General error / decryption failed | | 2 | Invalid arguments/options | --- @@ -869,6 +1092,7 @@ echo "Image was encoded with: $MODE mode" | Variable | Description | |----------|-------------| | `PYTHONPATH` | Include `src/` for development | +| `STEGASOO_DEBUG` | Enable debug output (set to `1`) | --- @@ -884,23 +1108,23 @@ echo "Image was encoded with: $MODE mode" ### DCT Mode Dependencies - `scipy` - DCT transformations -- `jpegio` - Native JPEG coefficient access (recommended) Install DCT dependencies: ```bash -pip install scipy jpegio +pip install scipy ``` Check availability: ```bash -python -c "import scipy; print('scipy:', scipy.__version__)" -python -c "import jpegio; print('jpegio: available')" +stegasoo modes +# or +python -c "from stegasoo import has_dct_support; print('DCT:', has_dct_support())" ``` --- ## See Also -- [API Documentation](API.md) - REST API reference +- [API Documentation](API.md) - Python API reference - [Web UI Documentation](WEB_UI.md) - Browser interface guide -- [README](README.md) - Project overview and security model +- [README](../README.md) - Project overview and security model diff --git a/frontends/cli/main.py b/frontends/cli/main.py index f0b3720..9b7f051 100644 --- a/frontends/cli/main.py +++ b/frontends/cli/main.py @@ -6,6 +6,7 @@ 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] @@ -28,26 +29,57 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src')) import stegasoo from stegasoo import ( - encode, encode_file, decode, + # Core operations + encode, decode, + + # Credential generation generate_credentials, - export_rsa_key_pem, load_rsa_key, - validate_image, calculate_capacity, - parse_date_from_filename, # Keep for filename parsing only - __version__, - StegasooError, DecryptionError, ExtractionError, - FilePayload, - will_fit, - strip_image_metadata, - # Embedding modes - EMBED_MODE_LSB, - EMBED_MODE_DCT, - EMBED_MODE_AUTO, + 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, - calculate_capacity_by_mode, + + # Utilities + generate_filename, + + # Version + __version__, + + # Exceptions + StegasooError, + DecryptionError, + ExtractionError, + + # Models + FilePayload, + + # Constants + EMBED_MODE_LSB, + EMBED_MODE_DCT, + EMBED_MODE_AUTO, + DEFAULT_PASSPHRASE_WORDS, + DEFAULT_PIN_LENGTH, ) +# 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 ( @@ -87,6 +119,7 @@ def cli(): 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 @@ -109,9 +142,12 @@ def cli(): @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=6, help='PIN length (6-9)') -@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=4, help='Words per passphrase (default: 4, was 3 in v3.1)') +@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') @@ -122,8 +158,8 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): Creates a passphrase and optionally a PIN and/or RSA key. At least one of --pin or --rsa must be enabled. - v3.2.0: No more daily passphrases - use one strong passphrase! - Default increased to 4 words (from 3) for better security. + v3.2.0: Single passphrase (no more daily rotation!) + Default increased to 4 words for better security. \b Examples: @@ -148,7 +184,8 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): use_rsa=rsa, pin_length=pin_length, rsa_bits=int(rsa_bits), - words_per_passphrase=words + passphrase_words=words, # v3.2.0: renamed parameter + rsa_password=password if output else None, ) if as_json: @@ -174,21 +211,21 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): click.secho("=" * 60, fg='cyan') click.echo() - click.secho(" MEMORIZE THESE AND CLOSE THIS WINDOW", fg='yellow', bold=True) + 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("─── STATIC PIN ───", fg='green') click.secho(f" {creds.pin}", fg='bright_yellow', bold=True) click.echo() - click.secho("--- PASSPHRASE ---", fg='green') + 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') + click.secho("─── RSA KEY ───", fg='green') if output: # Save to file private_key = load_rsa_key(creds.rsa_key_pem.encode()) @@ -200,7 +237,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): click.echo(creds.rsa_key_pem) click.echo() - click.secho("--- SECURITY ---", fg='green') + 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") @@ -210,7 +247,7 @@ 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("NOTE: v3.2.0 removed date dependency - use this passphrase anytime!", fg='cyan') + click.secho("✓ v3.2.0: Use this passphrase anytime - no date needed!", fg='cyan') click.echo() except Exception as e: @@ -227,7 +264,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json): @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 (v3.2.0: no date needed!)') +@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') @@ -282,7 +319,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, 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" \ + 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 @@ -378,7 +415,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, message=payload, reference_photo=ref_photo, carrier_image=carrier_image, - passphrase=passphrase, # Renamed from day_phrase + passphrase=passphrase, pin=pin or "", rsa_key_data=rsa_key_data, rsa_password=effective_key_password, @@ -405,7 +442,6 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, 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) - click.secho(" (v3.2.0: No date needed to decode!)", fg='cyan', dim=True) except StegasooError as e: raise click.ClickException(str(e)) @@ -509,7 +545,7 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e result = decode( stego_image=stego_image, reference_photo=ref_photo, - passphrase=passphrase, # Renamed from day_phrase + passphrase=passphrase, pin=pin or "", rsa_key_data=rsa_key_data, rsa_password=effective_key_password, @@ -631,7 +667,7 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a result = decode( stego_image=stego_image, reference_photo=ref_photo, - passphrase=passphrase, # v3.2.0: Renamed from day_phrase + passphrase=passphrase, pin=pin or "", rsa_key_data=rsa_key_data, rsa_password=effective_key_password, @@ -652,16 +688,16 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a if as_json: import json - output = { + output_data = { "valid": True, "stego_file": stego, "payload_type": payload_type, "payload_size": payload_size, } if result.is_file: - output["filename"] = result.filename - output["mime_type"] = result.mime_type - click.echo(json.dumps(output, indent=2)) + output_data["filename"] = result.filename + output_data["mime_type"] = result.mime_type + click.echo(json.dumps(output_data, indent=2)) else: click.secho("✓ Valid stego image", fg='green', bold=True) click.echo(f" Payload: {payload_type} ({payload_desc})") @@ -670,12 +706,12 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a except (DecryptionError, ExtractionError) as e: if as_json: import json - output = { + output_data = { "valid": False, "stego_file": stego, "error": str(e), } - click.echo(json.dumps(output, indent=2)) + click.echo(json.dumps(output_data, indent=2)) sys.exit(1) else: click.secho("✗ Verification failed", fg='red', bold=True) @@ -712,7 +748,7 @@ def info(image, as_json): if as_json: import json - output = { + output_data = { "file": image, "width": result.details['width'], "height": result.details['height'], @@ -734,12 +770,12 @@ def info(image, as_json): }, }, } - click.echo(json.dumps(output, indent=2)) + 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']} x {result.details['height']}") + 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']}") @@ -789,7 +825,7 @@ def compare(image, payload_size, as_json): if as_json: import json - output = { + output_data = { "file": image, "width": comparison['width'], "height": comparison['height'], @@ -812,43 +848,43 @@ def compare(image, payload_size, as_json): } if payload_size: - output["payload_check"] = { + 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, indent=2)) + 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']} x {comparison['height']}") + 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(" |") + 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") + 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 (v3.0.1)") + 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(" |") + 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") + 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'] @@ -858,27 +894,27 @@ def compare(image, payload_size, as_json): lsb_color = 'green' if fits_lsb else 'red' dct_color = 'green' if fits_dct else 'red' - click.echo(f" | LSB mode: ", nl=False) + 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.echo(f" │ DCT mode: ", nl=False) click.secho(f"{dct_icon} {'Fits' if fits_dct else 'Too large'}", fg=dct_color) - click.echo(" |") + click.echo(" │") # Recommendation - click.secho(" +--- Recommendation ---", fg='yellow') + click.secho(" └─── Recommendation ───", fg='yellow') if not comparison['dct']['available']: - click.echo(" Use LSB mode (DCT unavailable)") + 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") + 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)") + click.echo(" LSB mode (payload too large for DCT)") else: - click.secho(" ✗ Payload too large for both modes!", fg='red') + 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(" LSB for larger payloads, DCT for better stealth") + click.echo(" DCT supports color output with --dct-color color") click.echo() @@ -893,7 +929,8 @@ def compare(image, payload_size, as_json): @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('--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): """ @@ -907,6 +944,9 @@ def strip_metadata_cmd(image, output, output_format, quiet): 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) @@ -967,7 +1007,7 @@ def modes(): click.echo() # DCT Options - click.secho(" DCT Options (v3.0.1)", fg='magenta', bold=True) + 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") @@ -981,6 +1021,7 @@ def modes(): 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() diff --git a/src/main.py b/src/main.py index fcb14ea..4809e26 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,51 @@ #!/usr/bin/env python3 -"""Main entry point.""" +""" +Stegasoo - Main Entry Point + +This module provides the main entry point for the stegasoo package. +It can be run directly or via the installed console script. + +Usage: + python -m stegasoo --help + python src/main.py --help + stegasoo --help (if installed via pip) +""" + +import sys def main(): - """Main function.""" - print("Hello, World!") + """ + Main entry point for Stegasoo CLI. + + Delegates to the CLI module for command parsing and execution. + """ + try: + from stegasoo.cli import main as cli_main + cli_main() + except ImportError as e: + # Provide helpful error if dependencies are missing + print(f"Error: Could not import stegasoo package: {e}", file=sys.stderr) + print("\nMake sure stegasoo is installed:", file=sys.stderr) + print(" pip install -e .", file=sys.stderr) + print("\nOr run from the src directory:", file=sys.stderr) + print(" PYTHONPATH=src python -m stegasoo", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + print("\nInterrupted.", file=sys.stderr) + sys.exit(130) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +def version(): + """Print version and exit.""" + try: + from stegasoo import __version__ + print(f"stegasoo {__version__}") + except ImportError: + print("stegasoo (version unknown)") if __name__ == "__main__": diff --git a/src/stegasoo/batch.py b/src/stegasoo/batch.py index b198bfa..5c7e06d 100644 --- a/src/stegasoo/batch.py +++ b/src/stegasoo/batch.py @@ -1,8 +1,12 @@ """ -Stegasoo Batch Processing Module +Stegasoo Batch Processing Module (v3.2.0) Enables encoding/decoding multiple files in a single operation. Supports parallel processing, progress tracking, and detailed reporting. + +Changes in v3.2.0: +- BatchCredentials: renamed day_phrase → passphrase, removed date_str +- Updated all credential handling to use v3.2.0 API """ import os @@ -64,36 +68,56 @@ class BatchItem: @dataclass class BatchCredentials: """ - Credentials for batch encode/decode operations. + Credentials for batch encode/decode operations (v3.2.0). Provides a structured way to pass authentication factors for batch processing instead of using plain dicts. + Changes in v3.2.0: + - Renamed day_phrase → passphrase + - Removed date_str (no longer used in cryptographic operations) + Example: creds = BatchCredentials( reference_photo=ref_bytes, - day_phrase="apple forest thunder", + passphrase="apple forest thunder mountain", pin="123456" ) result = processor.batch_encode(images, creds, message="secret") """ reference_photo: bytes - day_phrase: str + passphrase: str # v3.2.0: renamed from day_phrase pin: str = "" rsa_key_data: Optional[bytes] = None rsa_password: Optional[str] = None - date_str: Optional[str] = None # YYYY-MM-DD, defaults to today def to_dict(self) -> dict: - """Convert to dictionary for legacy API compatibility.""" + """Convert to dictionary for API compatibility.""" return { "reference_photo": self.reference_photo, - "day_phrase": self.day_phrase, + "passphrase": self.passphrase, "pin": self.pin, "rsa_key_data": self.rsa_key_data, "rsa_password": self.rsa_password, - "date_str": self.date_str, } + + @classmethod + def from_dict(cls, data: dict) -> 'BatchCredentials': + """ + Create BatchCredentials from a dictionary. + + Handles both v3.2.0 format (passphrase) and legacy format (day_phrase). + """ + # Handle legacy 'day_phrase' key + passphrase = data.get('passphrase') or data.get('day_phrase', '') + + return cls( + reference_photo=data['reference_photo'], + passphrase=passphrase, + pin=data.get('pin', ''), + rsa_key_data=data.get('rsa_key_data'), + rsa_password=data.get('rsa_password'), + ) @dataclass @@ -140,23 +164,39 @@ ProgressCallback = Callable[[int, int, BatchItem], None] class BatchProcessor: """ - Handles batch encoding/decoding operations. + Handles batch encoding/decoding operations (v3.2.0). Usage: processor = BatchProcessor(max_workers=4) - # Batch encode + # Batch encode with BatchCredentials + creds = BatchCredentials( + reference_photo=ref_bytes, + passphrase="apple forest thunder mountain", + pin="123456" + ) result = processor.batch_encode( images=['img1.png', 'img2.png'], message="Secret message", output_dir="./encoded/", - credentials={"phrase": "...", "pin": "..."}, + credentials=creds, + ) + + # Batch encode with dict credentials + result = processor.batch_encode( + images=['img1.png', 'img2.png'], + message="Secret message", + credentials={ + "reference_photo": ref_bytes, + "passphrase": "apple forest thunder mountain", + "pin": "123456" + }, ) # Batch decode result = processor.batch_decode( images=['encoded1.png', 'encoded2.png'], - credentials={"phrase": "...", "pin": "..."}, + credentials=creds, ) """ @@ -202,6 +242,26 @@ class BatchProcessor: """Check if path is a valid image file.""" return path.suffix.lower().lstrip('.') in ALLOWED_IMAGE_EXTENSIONS + def _normalize_credentials( + self, + credentials: dict | BatchCredentials | None + ) -> BatchCredentials: + """ + Normalize credentials to BatchCredentials object. + + Handles both dict and BatchCredentials input, and legacy 'day_phrase' key. + """ + if credentials is None: + raise ValueError("Credentials are required") + + if isinstance(credentials, BatchCredentials): + return credentials + + if isinstance(credentials, dict): + return BatchCredentials.from_dict(credentials) + + raise ValueError(f"Invalid credentials type: {type(credentials)}") + def batch_encode( self, images: list[str | Path], @@ -209,7 +269,7 @@ class BatchProcessor: file_payload: Optional[Path] = None, output_dir: Optional[Path] = None, output_suffix: str = "_encoded", - credentials: dict = None, + credentials: dict | BatchCredentials | None = None, compress: bool = True, recursive: bool = False, progress_callback: Optional[ProgressCallback] = None, @@ -224,7 +284,7 @@ class BatchProcessor: file_payload: File to embed (mutually exclusive with message) output_dir: Output directory (default: same as input) output_suffix: Suffix for output files - credentials: Dict with 'phrase', 'pin', and optionally 'private_key' + credentials: BatchCredentials or dict with 'passphrase', 'pin', etc. compress: Enable compression recursive: Search directories recursively progress_callback: Called for each item: callback(current, total, item) @@ -236,8 +296,8 @@ class BatchProcessor: if message is None and file_payload is None: raise ValueError("Either message or file_payload must be provided") - if credentials is None: - raise ValueError("Credentials are required") + # Normalize credentials to BatchCredentials + creds = self._normalize_credentials(credentials) result = BatchResult(operation="encode") image_paths = list(self.find_images(images, recursive)) @@ -274,15 +334,15 @@ class BatchProcessor: output_path=item.output_path, message=message, file_payload=file_payload, - credentials=credentials, + credentials=creds.to_dict(), compress=compress, ) else: - # Placeholder - actual implementation would call stego.encode() - self._mock_encode(item, message, credentials, compress) + # Use stegasoo encode + self._do_encode(item, message, file_payload, creds, compress) item.status = BatchStatus.SUCCESS - item.output_size = item.output_path.stat().st_size if item.output_path.exists() else 0 + item.output_size = item.output_path.stat().st_size if item.output_path and item.output_path.exists() else 0 item.message = f"Encoded to {item.output_path.name}" except Exception as e: @@ -301,7 +361,7 @@ class BatchProcessor: self, images: list[str | Path], output_dir: Optional[Path] = None, - credentials: dict = None, + credentials: dict | BatchCredentials | None = None, recursive: bool = False, progress_callback: Optional[ProgressCallback] = None, decode_func: Callable = None, @@ -312,7 +372,7 @@ class BatchProcessor: Args: images: List of image paths or directories output_dir: Output directory for file payloads (default: same as input) - credentials: Dict with 'phrase', 'pin', and optionally 'private_key' + credentials: BatchCredentials or dict with 'passphrase', 'pin', etc. recursive: Search directories recursively progress_callback: Called for each item: callback(current, total, item) decode_func: Custom decode function (for integration) @@ -320,8 +380,8 @@ class BatchProcessor: Returns: BatchResult with decoded messages in item.message fields """ - if credentials is None: - raise ValueError("Credentials are required") + # Normalize credentials to BatchCredentials + creds = self._normalize_credentials(credentials) result = BatchResult(operation="decode") image_paths = list(self.find_images(images, recursive)) @@ -351,12 +411,12 @@ class BatchProcessor: decoded = decode_func( image_path=item.input_path, output_dir=item.output_path, - credentials=credentials, + credentials=creds.to_dict(), ) item.message = decoded.get('message', '') if isinstance(decoded, dict) else str(decoded) else: - # Placeholder - actual implementation would call stego.decode() - item.message = self._mock_decode(item, credentials) + # Use stegasoo decode + item.message = self._do_decode(item, creds) item.status = BatchStatus.SUCCESS @@ -404,14 +464,112 @@ class BatchProcessor: result.end_time = time.time() - def _mock_encode(self, item: BatchItem, message: str, credentials: dict, compress: bool) -> None: + def _do_encode( + self, + item: BatchItem, + message: Optional[str], + file_payload: Optional[Path], + creds: BatchCredentials, + compress: bool + ) -> None: + """ + Perform actual encoding using stegasoo.encode. + + Override this method to customize encoding behavior. + """ + try: + from .encode import encode, encode_file + from .models import FilePayload + + # Read carrier image + carrier_image = item.input_path.read_bytes() + + if file_payload: + # Encode file + payload = FilePayload.from_file(str(file_payload)) + result = encode( + message=payload, + reference_photo=creds.reference_photo, + carrier_image=carrier_image, + passphrase=creds.passphrase, + pin=creds.pin, + rsa_key_data=creds.rsa_key_data, + rsa_password=creds.rsa_password, + ) + else: + # Encode text message + result = encode( + message=message, + reference_photo=creds.reference_photo, + carrier_image=carrier_image, + passphrase=creds.passphrase, + pin=creds.pin, + rsa_key_data=creds.rsa_key_data, + rsa_password=creds.rsa_password, + ) + + # Write output + if item.output_path: + item.output_path.write_bytes(result.stego_image) + + except ImportError: + # Fallback to mock if stegasoo.encode not available + self._mock_encode(item, message, creds, compress) + + def _do_decode( + self, + item: BatchItem, + creds: BatchCredentials, + ) -> str: + """ + Perform actual decoding using stegasoo.decode. + + Override this method to customize decoding behavior. + """ + try: + from .decode import decode + + # Read stego image + stego_image = item.input_path.read_bytes() + + result = decode( + stego_image=stego_image, + reference_photo=creds.reference_photo, + passphrase=creds.passphrase, + pin=creds.pin, + rsa_key_data=creds.rsa_key_data, + rsa_password=creds.rsa_password, + ) + + if result.is_text: + return result.message or "" + else: + # File payload - save it + if item.output_path and result.file_data: + output_file = item.output_path / (result.filename or "extracted_file") + output_file.write_bytes(result.file_data) + return f"File extracted: {result.filename or 'extracted_file'}" + return f"[File: {result.filename or 'binary data'}]" + + except ImportError: + # Fallback to mock if stegasoo.decode not available + return self._mock_decode(item, creds) + + def _mock_encode( + self, + item: BatchItem, + message: str, + creds: BatchCredentials, + compress: bool + ) -> None: """Mock encode for testing - replace with actual stego.encode()""" # This is a placeholder - in real usage, you'd call your actual encode function # For now, just copy the file to simulate encoding import shutil - shutil.copy(item.input_path, item.output_path) + if item.output_path: + shutil.copy(item.input_path, item.output_path) - def _mock_decode(self, item: BatchItem, credentials: dict) -> str: + def _mock_decode(self, item: BatchItem, creds: BatchCredentials) -> str: """Mock decode for testing - replace with actual stego.decode()""" # This is a placeholder - in real usage, you'd call your actual decode function return "[Decoded message would appear here]" diff --git a/src/stegasoo/channel.py b/src/stegasoo/channel.py index 06e8df5..1f05951 100644 --- a/src/stegasoo/channel.py +++ b/src/stegasoo/channel.py @@ -1,5 +1,5 @@ """ -Channel Key Management for Stegasoo +Channel Key Management for Stegasoo (v3.2.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 @@ -15,6 +15,16 @@ Storage priority: 1. Environment variable: STEGASOO_CHANNEL_KEY 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 """ import os @@ -247,9 +257,12 @@ 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 mixed into the Argon2 key derivation to bind + This hash is designed to be 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/cli.py b/src/stegasoo/cli.py index 761821d..a840b12 100644 --- a/src/stegasoo/cli.py +++ b/src/stegasoo/cli.py @@ -1,7 +1,11 @@ """ -Stegasoo CLI Module +Stegasoo CLI Module (v3.2.0) Command-line interface with batch processing and compression support. + +Changes in v3.2.0: +- Updated to use DEFAULT_PASSPHRASE_WORDS (consistency with v3.2.0 naming) +- Updated help text to use 'passphrase' terminology """ import sys @@ -16,7 +20,7 @@ from .constants import ( MAX_MESSAGE_SIZE, MAX_FILE_PAYLOAD_SIZE, DEFAULT_PIN_LENGTH, - DEFAULT_PHRASE_WORDS, + DEFAULT_PASSPHRASE_WORDS, # v3.2.0: renamed from DEFAULT_PHRASE_WORDS ) from .compression import ( CompressionAlgorithm, @@ -60,8 +64,8 @@ def cli(ctx, json_output): @click.option('-f', '--file', 'file_payload', type=click.Path(exists=True), help='File to embed instead of message') @click.option('-o', '--output', type=click.Path(), help='Output image path') -@click.option('--phrase', prompt=True, hide_input=True, - confirmation_prompt=True, help='Passphrase') +@click.option('--passphrase', prompt=True, hide_input=True, + confirmation_prompt=True, help='Passphrase (recommend 4+ words)') @click.option('--pin', prompt=True, hide_input=True, confirmation_prompt=True, help='PIN code') @click.option('--compress/--no-compress', default=True, @@ -70,14 +74,14 @@ def cli(ctx, json_output): default='zlib', help='Compression algorithm') @click.option('--dry-run', is_flag=True, help='Show capacity usage without encoding') @click.pass_context -def encode(ctx, image, message, file_payload, output, phrase, pin, +def encode(ctx, image, message, file_payload, output, passphrase, pin, compress, algorithm, dry_run): """ Encode a message or file into an image. Examples: - stegasoo encode photo.png -m "Secret message" --phrase --pin + stegasoo encode photo.png -m "Secret message" --passphrase --pin stegasoo encode photo.png -f secret.pdf -o encoded.png """ @@ -109,7 +113,7 @@ def encode(ctx, image, message, file_payload, output, phrase, pin, # Get image capacity with Image.open(image) as img: width, height = img.size - capacity_bytes = (width * height * 3 // 8) - 100 + capacity_bytes = (width * height * 3 // 8) - 69 # v3.2.0: corrected overhead if dry_run: result = { @@ -153,18 +157,18 @@ def encode(ctx, image, message, file_payload, output, phrase, pin, @cli.command() @click.argument('image', type=click.Path(exists=True)) -@click.option('--phrase', prompt=True, hide_input=True, help='Passphrase') +@click.option('--passphrase', prompt=True, hide_input=True, help='Passphrase') @click.option('--pin', prompt=True, hide_input=True, help='PIN code') @click.option('-o', '--output', type=click.Path(), help='Output path for file payloads') @click.pass_context -def decode(ctx, image, phrase, pin, output): +def decode(ctx, image, passphrase, pin, output): """ Decode a message or file from an image. Examples: - stegasoo decode encoded.png --phrase --pin + stegasoo decode encoded.png --passphrase --pin stegasoo decode encoded.png -o ./extracted/ """ @@ -201,8 +205,8 @@ def batch(): @click.option('-o', '--output-dir', type=click.Path(), help='Output directory (default: same as input)') @click.option('--suffix', default='_encoded', help='Output filename suffix') -@click.option('--phrase', prompt=True, hide_input=True, - confirmation_prompt=True, help='Passphrase') +@click.option('--passphrase', prompt=True, hide_input=True, + confirmation_prompt=True, help='Passphrase (recommend 4+ words)') @click.option('--pin', prompt=True, hide_input=True, confirmation_prompt=True, help='PIN code') @click.option('--compress/--no-compress', default=True, @@ -215,13 +219,13 @@ def batch(): @click.option('-v', '--verbose', is_flag=True, help='Show detailed output') @click.pass_context def batch_encode(ctx, images, message, file_payload, output_dir, suffix, - phrase, pin, compress, algorithm, recursive, jobs, verbose): + passphrase, pin, compress, algorithm, recursive, jobs, verbose): """ Encode message into multiple images. Examples: - stegasoo batch encode *.png -m "Secret" --phrase --pin + stegasoo batch encode *.png -m "Secret" --passphrase --pin stegasoo batch encode ./photos/ -r -o ./encoded/ """ @@ -236,7 +240,8 @@ def batch_encode(ctx, images, message, file_payload, output_dir, suffix, status = "✓" if item.status.value == "success" else "✗" click.echo(f"[{current}/{total}] {status} {item.input_path.name}") - credentials = {"phrase": phrase, "pin": pin} + # v3.2.0: Use 'passphrase' key instead of 'phrase' + credentials = {"passphrase": passphrase, "pin": pin} result = processor.batch_encode( images=list(images), @@ -260,20 +265,20 @@ def batch_encode(ctx, images, message, file_payload, output_dir, suffix, @click.argument('images', nargs=-1, required=True, type=click.Path(exists=True)) @click.option('-o', '--output-dir', type=click.Path(), help='Output directory for file payloads') -@click.option('--phrase', prompt=True, hide_input=True, help='Passphrase') +@click.option('--passphrase', prompt=True, hide_input=True, help='Passphrase') @click.option('--pin', prompt=True, hide_input=True, help='PIN code') @click.option('-r', '--recursive', is_flag=True, help='Search directories recursively') @click.option('-j', '--jobs', default=4, help='Parallel workers (default: 4)') @click.option('-v', '--verbose', is_flag=True, help='Show detailed output') @click.pass_context -def batch_decode(ctx, images, output_dir, phrase, pin, recursive, jobs, verbose): +def batch_decode(ctx, images, output_dir, passphrase, pin, recursive, jobs, verbose): """ Decode messages from multiple images. Examples: - stegasoo batch decode encoded*.png --phrase --pin + stegasoo batch decode encoded*.png --passphrase --pin stegasoo batch decode ./encoded/ -r -o ./extracted/ """ @@ -285,7 +290,8 @@ def batch_decode(ctx, images, output_dir, phrase, pin, recursive, jobs, verbose) status = "✓" if item.status.value == "success" else "✗" click.echo(f"[{current}/{total}] {status} {item.input_path.name}") - credentials = {"phrase": phrase, "pin": pin} + # v3.2.0: Use 'passphrase' key instead of 'phrase' + credentials = {"passphrase": passphrase, "pin": pin} result = processor.batch_decode( images=list(images), @@ -348,14 +354,14 @@ def batch_check(ctx, images, recursive): # ============================================================================= @cli.command() -@click.option('--words', default=DEFAULT_PHRASE_WORDS, - help=f'Number of words (default: {DEFAULT_PHRASE_WORDS})') +@click.option('--words', default=DEFAULT_PASSPHRASE_WORDS, + help=f'Number of words in passphrase (default: {DEFAULT_PASSPHRASE_WORDS})') @click.option('--pin-length', default=DEFAULT_PIN_LENGTH, help=f'PIN length (default: {DEFAULT_PIN_LENGTH})') @click.pass_context def generate(ctx, words, pin_length): """ - Generate random credentials (phrase + PIN). + Generate random credentials (passphrase + PIN). Examples: @@ -367,26 +373,36 @@ def generate(ctx, words, pin_length): # Generate PIN pin = ''.join(str(secrets.randbelow(10)) for _ in range(pin_length)) + # Ensure PIN doesn't start with 0 + if pin[0] == '0': + pin = str(secrets.randbelow(9) + 1) + pin[1:] - # Generate phrase (would use BIP-39 wordlist) + # Generate passphrase (would use BIP-39 wordlist) # Placeholder - actual implementation uses constants.get_wordlist() - sample_words = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', - 'golf', 'hotel', 'india', 'juliet', 'kilo', 'lima'] - phrase_words = [secrets.choice(sample_words) for _ in range(words)] - phrase = ' '.join(phrase_words) + try: + from .constants import get_wordlist + wordlist = get_wordlist() + phrase_words = [secrets.choice(wordlist) for _ in range(words)] + except (ImportError, FileNotFoundError): + # Fallback for testing + sample_words = ['alpha', 'bravo', 'charlie', 'delta', 'echo', 'foxtrot', + 'golf', 'hotel', 'india', 'juliet', 'kilo', 'lima'] + phrase_words = [secrets.choice(sample_words) for _ in range(words)] + + passphrase = ' '.join(phrase_words) result = { - "phrase": phrase, + "passphrase": passphrase, "pin": pin, - "phrase_words": words, + "passphrase_words": words, "pin_length": pin_length, } if ctx.obj.get('json'): click.echo(json.dumps(result, indent=2)) else: - click.echo(f"Phrase: {phrase}") - click.echo(f"PIN: {pin}") + click.echo(f"Passphrase: {passphrase}") + click.echo(f"PIN: {pin}") click.echo("\n⚠️ Save these credentials securely - they cannot be recovered!") diff --git a/src/stegasoo/dct_steganography.py b/src/stegasoo/dct_steganography.py index a136a64..abfde7a 100644 --- a/src/stegasoo/dct_steganography.py +++ b/src/stegasoo/dct_steganography.py @@ -1,5 +1,5 @@ """ -DCT Domain Steganography Module (v3.0.2) +DCT Domain Steganography Module (v3.2.0) Embeds data in DCT coefficients with two approaches: 1. PNG output: Scipy-based DCT transform (grayscale or color) @@ -8,11 +8,16 @@ Embeds data in DCT coefficients with two approaches: The JPEG approach is the "correct" way to do JPEG steganography because it directly modifies the already-quantized coefficients without re-encoding. -New in v3.0.2: +Changes in v3.0.2: - jpegio integration for proper JPEG coefficient embedding - Falls back to warning if jpegio not available for JPEG output - Maintains backward compatibility with v3.0.1 +Changes in v3.2.0: +- Fixed color-mode extraction to properly extract from Y channel +- Added _extract_from_y_channel() for accurate color-mode extraction +- Improved extraction robustness for both grayscale and color modes + Requires: scipy (for PNG mode), optionally jpegio (for JPEG mode) """ @@ -83,6 +88,9 @@ JPEGIO_MAGIC = b'JPGS' JPEGIO_MIN_COEF_MAGNITUDE = 2 JPEGIO_EMBED_CHANNEL = 0 # Y channel +# Flag bits for header +FLAG_COLOR_MODE = 0x01 # Set if embedded in color mode (Y channel of YCbCr) + # ============================================================================ # DATA CLASSES @@ -167,6 +175,37 @@ def _to_grayscale(image_data: bytes) -> np.ndarray: return np.array(gray, dtype=np.float64) +def _extract_y_channel(image_data: bytes) -> np.ndarray: + """ + Extract Y (luminance) channel from image for color-mode extraction. + + This uses the same YCbCr conversion as embedding to ensure + accurate extraction from color-mode stego images. + + Args: + image_data: Image file bytes + + Returns: + Y channel as float64 numpy array + """ + img = Image.open(io.BytesIO(image_data)) + + # Convert to RGB if needed + if img.mode != 'RGB': + img = img.convert('RGB') + + rgb_array = np.array(img, dtype=np.float64) + + # Extract Y channel using ITU-R BT.601 (same as embedding) + R = rgb_array[:, :, 0] + G = rgb_array[:, :, 1] + B = rgb_array[:, :, 2] + + Y = 0.299 * R + 0.587 * G + 0.114 * B + + return Y + + def _pad_to_blocks(image: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]: """Pad image dimensions to be divisible by block size.""" h, w = image.shape @@ -376,9 +415,9 @@ def _jpegio_generate_order(num_positions: int, seed: bytes) -> list: return order -def _jpegio_create_header(data_length: int) -> bytes: +def _jpegio_create_header(data_length: int, flags: int = 0) -> bytes: """Create header for jpegio embedding.""" - return struct.pack('>4sBBI', JPEGIO_MAGIC, 1, 0, data_length) + return struct.pack('>4sBBI', JPEGIO_MAGIC, 1, flags, data_length) def _jpegio_parse_header(header_bytes: bytes) -> Tuple[int, int, int]: @@ -549,6 +588,9 @@ def _embed_scipy_dct( img = Image.open(io.BytesIO(carrier_image)) width, height = img.size + # Set flags for header + flags = FLAG_COLOR_MODE if color_mode == 'color' else 0 + if color_mode == 'color' and img.mode in ('RGB', 'RGBA'): # Color mode: convert to YCbCr, embed in Y only, preserve Cb/Cr if img.mode == 'RGBA': @@ -560,8 +602,8 @@ def _embed_scipy_dct( # Pad Y channel Y_padded, original_size = _pad_to_blocks(Y) - # Embed in Y channel - Y_embedded = _embed_in_channel(Y_padded, data, seed, capacity_info) + # Embed in Y channel (with color flag) + Y_embedded = _embed_in_channel(Y_padded, data, seed, capacity_info, flags) # Unpad Y_result = _unpad_image(Y_embedded, original_size) @@ -576,13 +618,13 @@ def _embed_scipy_dct( image = _to_grayscale(carrier_image) padded, original_size = _pad_to_blocks(image) - embedded = _embed_in_channel(padded, data, seed, capacity_info) + embedded = _embed_in_channel(padded, data, seed, capacity_info, flags) result = _unpad_image(embedded, original_size) stego_bytes = _save_stego_image(result, output_format) # Calculate stats - header = _create_header(len(data)) + header = _create_header(len(data), flags) payload = header + data bits = len(payload) * 8 @@ -607,9 +649,10 @@ def _embed_in_channel( data: bytes, seed: bytes, capacity_info: DCTCapacityInfo, + flags: int = 0, ) -> np.ndarray: """Embed data in a single channel using DCT.""" - header = _create_header(len(data)) + header = _create_header(len(data), flags) payload = header + data bits = [] @@ -677,14 +720,14 @@ def _embed_jpegio( input_path = _jpegio_bytes_to_file(carrier_image, suffix='.jpg') output_path = tempfile.mktemp(suffix='.jpg') + # Set flags + flags = FLAG_COLOR_MODE if color_mode == 'color' else 0 + try: # Read JPEG with jpegio jpeg = jio.read(input_path) # Get Y channel coefficients (channel 0) - # For grayscale mode, we could convert to grayscale, but jpegio - # works with the original JPEG which already has color info. - # The color_mode primarily affects the output interpretation. coef_array = jpeg.coef_arrays[JPEGIO_EMBED_CHANNEL] # Find usable positions @@ -693,8 +736,8 @@ def _embed_jpegio( # Generate pseudo-random order order = _jpegio_generate_order(len(all_positions), seed) - # Create payload - header = _jpegio_create_header(len(data)) + # Create payload with flags + header = _jpegio_create_header(len(data), flags) payload = header + data # Convert to bits @@ -764,7 +807,8 @@ def extract_from_dct( """ Extract data from DCT stego image. - Automatically detects whether image uses scipy DCT or jpegio embedding. + Automatically detects whether image uses scipy DCT or jpegio embedding, + and handles both grayscale and color modes. Args: stego_image: Stego image bytes @@ -790,9 +834,28 @@ def extract_from_dct( def _extract_scipy_dct(stego_image: bytes, seed: bytes) -> bytes: - """Extract using scipy DCT (for PNG images).""" - image = _to_grayscale(stego_image) - padded, original_size = _pad_to_blocks(image) + """ + Extract using scipy DCT (for PNG images). + + v3.2.0: Now properly handles both grayscale and color modes by + first trying to detect the mode from header flags, then extracting + from the appropriate channel. + """ + # First, try extracting from grayscale to get header and detect mode + # This works because even color-mode images can be converted to grayscale + # and the Y channel ≈ grayscale for extraction purposes + + # Try Y channel extraction first (works for both color and grayscale) + img = Image.open(io.BytesIO(stego_image)) + + if img.mode in ('RGB', 'RGBA'): + # Extract from Y channel (more accurate for color-mode images) + channel = _extract_y_channel(stego_image) + else: + # Grayscale image + channel = _to_grayscale(stego_image) + + padded, original_size = _pad_to_blocks(channel) h, w = padded.shape blocks_x = w // BLOCK_SIZE @@ -816,7 +879,7 @@ def _extract_scipy_dct(stego_image: bytes, seed: bytes) -> bytes: if len(all_bits) >= HEADER_SIZE * 8: try: - _, _, data_length = _parse_header(all_bits[:HEADER_SIZE * 8]) + _, flags, data_length = _parse_header(all_bits[:HEADER_SIZE * 8]) total_needed = (HEADER_SIZE + data_length) * 8 if len(all_bits) >= total_needed: break @@ -825,6 +888,9 @@ def _extract_scipy_dct(stego_image: bytes, seed: bytes) -> bytes: version, flags, data_length = _parse_header(all_bits) + # Check if color mode flag is set (for informational purposes) + is_color_mode = bool(flags & FLAG_COLOR_MODE) + data_bits = all_bits[HEADER_SIZE * 8:(HEADER_SIZE + data_length) * 8] data = bytes([ diff --git a/src/stegasoo/keygen.py b/src/stegasoo/keygen.py index a633df2..edad6eb 100644 --- a/src/stegasoo/keygen.py +++ b/src/stegasoo/keygen.py @@ -1,7 +1,12 @@ """ -Stegasoo Key Generation +Stegasoo Key Generation (v3.2.0) Generate PINs, passphrases, and RSA keys. + +Changes in v3.2.0: +- generate_credentials() now returns Credentials with single passphrase +- Removed generate_day_phrases() from main API (kept for legacy compatibility) +- Updated to use PASSPHRASE constants """ import secrets @@ -16,7 +21,7 @@ from cryptography.hazmat.backends import default_backend from .constants import ( DAY_NAMES, MIN_PIN_LENGTH, MAX_PIN_LENGTH, DEFAULT_PIN_LENGTH, - MIN_PHRASE_WORDS, MAX_PHRASE_WORDS, DEFAULT_PHRASE_WORDS, + MIN_PASSPHRASE_WORDS, MAX_PASSPHRASE_WORDS, DEFAULT_PASSPHRASE_WORDS, MIN_RSA_BITS, VALID_RSA_SIZES, DEFAULT_RSA_BITS, get_wordlist, ) @@ -57,7 +62,7 @@ def generate_pin(length: int = DEFAULT_PIN_LENGTH) -> str: return pin -def generate_phrase(words_per_phrase: int = DEFAULT_PHRASE_WORDS) -> str: +def generate_phrase(words_per_phrase: int = DEFAULT_PASSPHRASE_WORDS) -> str: """ Generate a random passphrase from BIP-39 wordlist. @@ -68,13 +73,13 @@ def generate_phrase(words_per_phrase: int = DEFAULT_PHRASE_WORDS) -> str: Space-separated phrase Example: - >>> generate_phrase(3) - "apple forest thunder" + >>> generate_phrase(4) + "apple forest thunder mountain" """ - debug.validate(MIN_PHRASE_WORDS <= words_per_phrase <= MAX_PHRASE_WORDS, - f"Words per phrase must be between {MIN_PHRASE_WORDS} and {MAX_PHRASE_WORDS}") + debug.validate(MIN_PASSPHRASE_WORDS <= words_per_phrase <= MAX_PASSPHRASE_WORDS, + f"Words per phrase must be between {MIN_PASSPHRASE_WORDS} and {MAX_PASSPHRASE_WORDS}") - words_per_phrase = max(MIN_PHRASE_WORDS, min(MAX_PHRASE_WORDS, words_per_phrase)) + words_per_phrase = max(MIN_PASSPHRASE_WORDS, min(MAX_PASSPHRASE_WORDS, words_per_phrase)) wordlist = get_wordlist() words = [secrets.choice(wordlist) for _ in range(words_per_phrase)] @@ -83,10 +88,17 @@ def generate_phrase(words_per_phrase: int = DEFAULT_PHRASE_WORDS) -> str: return phrase -def generate_day_phrases(words_per_phrase: int = DEFAULT_PHRASE_WORDS) -> Dict[str, str]: +# Alias for backward compatibility and public API consistency +generate_passphrase = generate_phrase + + +def generate_day_phrases(words_per_phrase: int = DEFAULT_PASSPHRASE_WORDS) -> Dict[str, str]: """ Generate phrases for all days of the week. + DEPRECATED in v3.2.0: Use generate_phrase() for single passphrase. + Kept for legacy compatibility and organizational use cases. + Args: words_per_phrase: Number of words per phrase (3-12) @@ -97,6 +109,14 @@ def generate_day_phrases(words_per_phrase: int = DEFAULT_PHRASE_WORDS) -> Dict[s >>> generate_day_phrases(3) {'Monday': 'apple forest thunder', 'Tuesday': 'banana river lightning', ...} """ + import warnings + warnings.warn( + "generate_day_phrases() is deprecated in v3.2.0. " + "Use generate_phrase() for single passphrase.", + DeprecationWarning, + stacklevel=2 + ) + phrases = {day: generate_phrase(words_per_phrase) for day in DAY_NAMES} debug.print(f"Generated phrases for {len(phrases)} days") return phrases @@ -272,13 +292,89 @@ def generate_credentials( use_rsa: bool = False, pin_length: int = DEFAULT_PIN_LENGTH, rsa_bits: int = DEFAULT_RSA_BITS, - words_per_phrase: int = DEFAULT_PHRASE_WORDS + passphrase_words: int = DEFAULT_PASSPHRASE_WORDS, + rsa_password: Optional[str] = None, ) -> Credentials: """ Generate a complete set of credentials. + v3.2.0: Now generates a single passphrase instead of daily phrases. At least one of use_pin or use_rsa must be True. + Args: + use_pin: Whether to generate a PIN + use_rsa: Whether to generate an RSA key + pin_length: PIN length if generating (default 6) + rsa_bits: RSA key size if generating (default 2048) + passphrase_words: Words in passphrase (default 4) + rsa_password: Optional password for RSA key encryption + + Returns: + Credentials object with passphrase, PIN, and/or RSA key + + Raises: + ValueError: If neither PIN nor RSA is selected + + Example: + >>> creds = generate_credentials(use_pin=True, use_rsa=False) + >>> creds.passphrase + "apple forest thunder mountain" + >>> creds.pin + "812345" + """ + debug.validate(use_pin or use_rsa, + "Must select at least one security factor (PIN or RSA key)") + + if not use_pin and not use_rsa: + raise ValueError("Must select at least one security factor (PIN or RSA key)") + + debug.print(f"Generating credentials: PIN={use_pin}, RSA={use_rsa}, " + f"passphrase_words={passphrase_words}") + + # Generate single passphrase (v3.2.0 - no daily rotation) + passphrase = generate_phrase(passphrase_words) + + # Generate PIN if requested + pin = generate_pin(pin_length) if use_pin else None + + # Generate RSA key if requested + rsa_key_pem = None + if use_rsa: + rsa_key_obj = generate_rsa_key(rsa_bits) + rsa_key_pem = export_rsa_key_pem(rsa_key_obj, rsa_password).decode('utf-8') + + # Create Credentials object (v3.2.0 format with single passphrase) + creds = Credentials( + passphrase=passphrase, + pin=pin, + rsa_key_pem=rsa_key_pem, + rsa_bits=rsa_bits if use_rsa else None, + words_per_passphrase=passphrase_words, + ) + + debug.print(f"Credentials generated: {creds.total_entropy} bits total entropy") + return creds + + +# ============================================================================= +# LEGACY COMPATIBILITY +# ============================================================================= + +def generate_credentials_legacy( + use_pin: bool = True, + use_rsa: bool = False, + pin_length: int = DEFAULT_PIN_LENGTH, + rsa_bits: int = DEFAULT_RSA_BITS, + words_per_phrase: int = DEFAULT_PASSPHRASE_WORDS +) -> dict: + """ + Generate credentials in legacy format (v3.1.0 style with daily phrases). + + DEPRECATED: Use generate_credentials() for v3.2.0 format. + + This function exists only for migration tools that need to work with + old-format credentials. + Args: use_pin: Whether to generate a PIN use_rsa: Whether to generate an RSA key @@ -287,44 +383,33 @@ def generate_credentials( words_per_phrase: Words per daily phrase Returns: - Credentials object - - Raises: - ValueError: If neither PIN nor RSA is selected - - Example: - >>> creds = generate_credentials(use_pin=True, use_rsa=False) - >>> creds.pin - "812345" - >>> creds.phrases['Monday'] - "apple forest thunder" + Dict with 'phrases' (dict), 'pin', 'rsa_key_pem', etc. """ - debug.validate(use_pin or use_rsa, - "Must select at least one security factor (PIN or RSA key)") + import warnings + warnings.warn( + "generate_credentials_legacy() returns v3.1.0 format. " + "Use generate_credentials() for v3.2.0 format.", + DeprecationWarning, + stacklevel=2 + ) if not use_pin and not use_rsa: raise ValueError("Must select at least one security factor (PIN or RSA key)") - debug.print(f"Generating credentials: PIN={use_pin}, RSA={use_rsa}, " - f"words={words_per_phrase}") - - phrases = generate_day_phrases(words_per_phrase) + # Generate daily phrases (old format) + phrases = {day: generate_phrase(words_per_phrase) for day in DAY_NAMES} pin = generate_pin(pin_length) if use_pin else None rsa_key_pem = None - rsa_key_obj = None if use_rsa: rsa_key_obj = generate_rsa_key(rsa_bits) rsa_key_pem = export_rsa_key_pem(rsa_key_obj).decode('utf-8') - creds = Credentials( - phrases=phrases, - pin=pin, - rsa_key_pem=rsa_key_pem, - rsa_bits=rsa_bits if use_rsa else None, - words_per_phrase=words_per_phrase - ) - - debug.print(f"Credentials generated: {creds.total_entropy} bits total entropy") - return creds + return { + 'phrases': phrases, + 'pin': pin, + 'rsa_key_pem': rsa_key_pem, + 'rsa_bits': rsa_bits if use_rsa else None, + 'words_per_phrase': words_per_phrase, + } diff --git a/src/stegasoo/steganography.py b/src/stegasoo/steganography.py index b1fd7fe..ae7049c 100644 --- a/src/stegasoo/steganography.py +++ b/src/stegasoo/steganography.py @@ -1,17 +1,21 @@ """ -Stegasoo Steganography Functions (v3.0.1) +Stegasoo Steganography Functions (v3.2.0) LSB and DCT embedding modes with pseudo-random pixel/coefficient selection. -New in v3.0: +Changes in v3.0: - DCT domain embedding mode (requires scipy) - embed_mode parameter for encode/decode - Auto-detection of embedding mode - Comparison utilities -New in v3.0.1: +Changes in v3.0.1: - dct_output_format parameter for DCT mode ('png' or 'jpeg') - dct_color_mode parameter for DCT mode ('grayscale' or 'color') + +Changes in v3.2.0: +- Fixed HEADER_OVERHEAD constant (65 bytes, not 104 - date field removed) +- Updated ENCRYPTION_OVERHEAD calculation """ import io @@ -51,10 +55,24 @@ EXT_TO_FORMAT = { 'tif': 'TIFF', } -# Overhead constants for capacity estimation -HEADER_OVERHEAD = 104 # Magic + version + date + salt + iv + tag -LENGTH_PREFIX = 4 # 4 bytes for payload length -ENCRYPTION_OVERHEAD = HEADER_OVERHEAD + LENGTH_PREFIX +# ============================================================================= +# OVERHEAD CONSTANTS (v3.2.0 - Updated for date-independent format) +# ============================================================================= +# v3.2.0 Header format (no date field): +# Magic: 4 bytes (\x89ST3) +# Version: 1 byte (4 for v3.2.0) +# Salt: 32 bytes +# IV: 12 bytes +# Tag: 16 bytes +# ----------------- +# Total: 65 bytes +# +# Previous v3.1.0 had date field (10 bytes + 1 byte length) = 76 bytes header +# The old value of 104 was incorrect even for v3.1.0 + +HEADER_OVERHEAD = 65 # v3.2.0: Magic + version + salt + iv + tag +LENGTH_PREFIX = 4 # 4 bytes for payload length in LSB embedding +ENCRYPTION_OVERHEAD = HEADER_OVERHEAD + LENGTH_PREFIX # 69 bytes total # DCT output format options (v3.0.1) DCT_OUTPUT_PNG = 'png' @@ -167,6 +185,9 @@ def will_fit( capacity = calculate_capacity(carrier_image, bits_per_channel) + # Estimate encrypted size with padding + # Padding adds 64-319 bytes, rounded up to 256-byte boundary + # Average case: ~190 bytes padding estimated_padding = 190 estimated_encrypted_size = payload_size + estimated_padding + ENCRYPTION_OVERHEAD @@ -175,7 +196,7 @@ def will_fit( try: import zlib compressed = zlib.compress(payload_data, level=6) - compressed_size = len(compressed) + 9 + compressed_size = len(compressed) + 9 # Compression header if compressed_size < payload_size: compressed_estimate = compressed_size estimated_encrypted_size = compressed_size + estimated_padding + ENCRYPTION_OVERHEAD @@ -301,7 +322,7 @@ def will_fit_by_mode( else: payload_size = len(payload) - estimated_size = payload_size + ENCRYPTION_OVERHEAD + 190 + estimated_size = payload_size + ENCRYPTION_OVERHEAD + 190 # padding estimate dct_mod = _get_dct_module() fits = dct_mod.will_fit_dct(estimated_size, carrier_image) @@ -481,8 +502,8 @@ def embed_in_image( bits_per_channel: int = 1, output_format: Optional[str] = None, embed_mode: str = EMBED_MODE_LSB, - dct_output_format: str = DCT_OUTPUT_PNG, # NEW in v3.0.1 - dct_color_mode: str = 'grayscale', # NEW in v3.0.1: 'grayscale' or 'color' + dct_output_format: str = DCT_OUTPUT_PNG, + dct_color_mode: str = 'grayscale', ) -> Tuple[bytes, Union[EmbedStats, 'DCTEmbedStats'], str]: """ Embed data into an image using specified mode. @@ -535,7 +556,7 @@ def embed_in_image( image_data, pixel_key, output_format=dct_output_format, - color_mode=dct_color_mode, # NEW in v3.0.1 + color_mode=dct_color_mode, ) # Determine extension based on output format