Release checklist and updated test scripts.

This commit is contained in:
Aaron D. Lee
2026-01-01 14:04:55 -05:00
parent a001f227ec
commit 12929bf326
8 changed files with 1635 additions and 575 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,11 @@
# Stegasoo Web UI Documentation # Stegasoo Web UI Documentation (v3.2.0)
Complete guide for the Stegasoo web-based steganography interface. Complete guide for the Stegasoo web-based steganography interface.
## Table of Contents ## Table of Contents
- [Overview](#overview) - [Overview](#overview)
- [What's New in v3.2.0](#whats-new-in-v320)
- [Installation & Setup](#installation--setup) - [Installation & Setup](#installation--setup)
- [Pages & Features](#pages--features) - [Pages & Features](#pages--features)
- [Home Page](#home-page) - [Home Page](#home-page)
@@ -14,7 +15,7 @@ Complete guide for the Stegasoo web-based steganography interface.
- [About Page](#about-page) - [About Page](#about-page)
- [Embedding Modes](#embedding-modes) - [Embedding Modes](#embedding-modes)
- [LSB Mode (Default)](#lsb-mode-default) - [LSB Mode (Default)](#lsb-mode-default)
- [DCT Mode (Experimental)](#dct-mode-experimental) - [DCT Mode](#dct-mode)
- [User Interface Guide](#user-interface-guide) - [User Interface Guide](#user-interface-guide)
- [Workflow Examples](#workflow-examples) - [Workflow Examples](#workflow-examples)
- [Security Features](#security-features) - [Security Features](#security-features)
@@ -28,9 +29,9 @@ Complete guide for the Stegasoo web-based steganography interface.
The Stegasoo Web UI provides a user-friendly browser-based interface for: The Stegasoo Web UI provides a user-friendly browser-based interface for:
- **Generating** secure credentials (phrases, PINs, RSA keys) - **Generating** secure credentials (passphrase, PINs, RSA keys)
- **Encoding** secret messages into images - **Encoding** secret messages or files into images
- **Decoding** hidden messages from images - **Decoding** hidden messages or files from images
- **Learning** about the security model - **Learning** about the security model
Built with Flask, Bootstrap 5, and a modern dark theme. Built with Flask, Bootstrap 5, and a modern dark theme.
@@ -39,14 +40,37 @@ Built with Flask, Bootstrap 5, and a modern dark theme.
- ✅ Drag-and-drop file uploads - ✅ Drag-and-drop file uploads
- ✅ Image previews - ✅ Image previews
- ✅ Client-side date detection
- ✅ Native sharing (Web Share API) - ✅ Native sharing (Web Share API)
- ✅ Responsive design (mobile-friendly) - ✅ Responsive design (mobile-friendly)
- ✅ Password-protected RSA key downloads - ✅ Password-protected RSA key downloads
- ✅ Real-time entropy calculations - ✅ Real-time entropy calculations
- ✅ Automatic file cleanup - ✅ Automatic file cleanup
-**DCT steganography mode** (v3.0+) - JPEG-resilient embedding -**DCT steganography mode** - Frequency domain embedding
-**Color mode selection** (v3.0.1+) - Preserve carrier colors -**Color mode selection** - Preserve carrier colors
-**File embedding** - Hide files, not just text
-**v3.2.0: No date tracking** - Simplified workflow
---
## What's New in v3.2.0
Version 3.2.0 simplifies the user experience significantly:
| Change | Before (v3.1) | After (v3.2.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" |
**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.
--- ---
@@ -58,7 +82,7 @@ Built with Flask, Bootstrap 5, and a modern dark theme.
pip install stegasoo[web] pip install stegasoo[web]
``` ```
This automatically installs DCT dependencies (scipy, jpegio) for full functionality. This automatically installs DCT dependencies (scipy) for full functionality.
### From Source ### From Source
@@ -92,7 +116,7 @@ docker-compose up web
1. Navigate to http://localhost:5000 1. Navigate to http://localhost:5000
2. Click "Generate" to create your credentials 2. Click "Generate" to create your credentials
3. **Memorize** your phrases and PIN 3. **Memorize** your passphrase and PIN
4. Share credentials securely with your communication partner 4. Share credentials securely with your communication partner
--- ---
@@ -117,7 +141,7 @@ The landing page introduces Stegasoo and provides quick access to all features.
Explains the three key components: Explains the three key components:
1. **Reference Photo** - Shared secret image 1. **Reference Photo** - Shared secret image
2. **Day Phrase** - Changes daily 2. **Passphrase** - Your secret phrase (v3.2.0: same every time!)
3. **Static PIN** - Same every day 3. **Static PIN** - Same every day
--- ---
@@ -132,7 +156,7 @@ Create a new set of credentials for steganography operations.
| Option | Range | Default | Description | | Option | Range | Default | Description |
|--------|-------|---------|-------------| |--------|-------|---------|-------------|
| Words per phrase | 3-12 | 3 | BIP-39 words per daily phrase | | Words per passphrase | 3-12 | 4 | BIP-39 words in passphrase |
| Use PIN | on/off | on | Generate a numeric PIN | | Use PIN | on/off | on | Generate a numeric PIN |
| PIN length | 6-9 | 6 | Digits in the PIN | | PIN length | 6-9 | 6 | Digits in the PIN |
| Use RSA Key | on/off | off | Generate an RSA key pair | | Use RSA Key | on/off | off | Generate an RSA key pair |
@@ -143,12 +167,12 @@ Create a new set of credentials for steganography operations.
The UI displays real-time entropy calculations: The UI displays real-time entropy calculations:
``` ```
Estimated entropy: ~53 bits Estimated entropy: ~63 bits
[==========> ] Good for most use cases [=============> ] Good for most use cases
• Reference photo adds ~80-256 bits more • Reference photo adds ~80-256 bits more
``` ```
#### Generated Output #### Generated Output (v3.2.0)
After clicking "Generate Credentials": After clicking "Generate Credentials":
@@ -157,34 +181,32 @@ After clicking "Generate Credentials":
┌─────────────────────┐ ┌─────────────────────┐
│ 8 4 7 2 9 3 │ │ 8 4 7 2 9 3 │
└─────────────────────┘ └─────────────────────┘
Use this 6-digit PIN every day Use this 6-digit PIN every time
``` ```
**Daily Phrases:** **Passphrase** (v3.2.0: single passphrase, no daily rotation):
``` ```
Day │ Phrase ┌─────────────────────────────────────────┐
───────────────────────────────────────── │ abandon ability able about │
Monday │ abandon ability able │ │
Tuesday │ actor actress actual │ Use this passphrase to encode and │
Wednesday │ advice aerobic affair │ decode messages - no date needed! │
Thursday │ afraid again age └─────────────────────────────────────────┘
Friday │ agree ahead aim
Saturday │ airport aisle alarm
Sunday │ album alcohol alert
``` ```
**RSA Key** (if enabled): **RSA Key** (if enabled):
- Copy to clipboard button - Copy to clipboard button
- Download as password-protected .pem file - Download as password-protected .pem file
- Download as QR code image
**Security Summary:** **Security Summary:**
``` ```
Phrase entropy: 33 bits/phrase Passphrase entropy: 44 bits (4 words)
PIN entropy: 19 bits/PIN PIN entropy: 19 bits
RSA entropy: 128 bits/RSA RSA entropy: 128 bits
───────────────────────────── ─────────────────────────────
Total: 180 bits Total: 191 bits
+ reference photo (~80-256 bits) = 260+ bits combined + reference photo (~80-256 bits) = 271+ bits combined
``` ```
#### RSA Key Download #### RSA Key Download
@@ -195,13 +217,20 @@ Total: 180 bits
4. Save the file securely 4. Save the file securely
5. Share with your communication partner through a secure channel 5. Share with your communication partner through a secure channel
#### RSA Key QR Code
For easier sharing, you can also:
1. Click "Download QR Code"
2. Save the QR code image
3. Your partner can scan it to import the key
--- ---
### Encode Message ### Encode Message
**URL:** `/encode` **URL:** `/encode`
Hide a secret message inside an image. Hide a secret message or file inside an image.
#### Input Fields #### Input Fields
@@ -209,15 +238,19 @@ Hide a secret message inside an image.
|-------|------|----------|-------------| |-------|------|----------|-------------|
| Reference Photo | Image file | ✓ | Your shared secret photo | | Reference Photo | Image file | ✓ | Your shared secret photo |
| Carrier Image | Image file | ✓ | Image to hide message in | | Carrier Image | Image file | ✓ | Image to hide message in |
| Secret Message | Text | ✓ | Message to hide (max 50KB) | | Payload Type | Toggle | ✓ | Text message or file |
| Day Phrase | Text | | Today's passphrase | | Secret Message | Text | * | Message to hide (max 50KB) |
| PIN | Number | * | Your static PIN | | File to Embed | File | * | File to hide (max 2MB) |
| RSA Key | .pem file | * | Your shared RSA key | | Passphrase | Text | | Your passphrase (v3.2.0) |
| PIN | Number | ** | Your static PIN |
| RSA Key | .pem file | ** | Your shared RSA key |
| RSA Key QR | Image file | ** | QR code containing RSA key |
| RSA Key Password | Password | | Password for encrypted key | | RSA Key Password | Password | | Password for encrypted key |
\* At least one security factor (PIN or RSA Key) required. \* One of message or file required.
\*\* At least one security factor (PIN or RSA Key) required.
#### Advanced Options (v3.0+) #### Advanced Options
Expand "Advanced Options" to access embedding mode settings: Expand "Advanced Options" to access embedding mode settings:
@@ -246,13 +279,6 @@ Message: [ ]
Shows warning at 80% capacity. Shows warning at 80% capacity.
#### Day Detection
The page automatically detects your local day of week and updates the label:
```
Saturday's Phrase: [ ]
```
#### Encoding Process #### Encoding Process
1. Fill in all required fields 1. Fill in all required fields
@@ -271,11 +297,11 @@ After successful encoding:
┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐
│ ✓ Message Encoded Successfully! │ │ ✓ Message Encoded Successfully! │
│ │ │ │
│ 📄 a1b2c3d4_20251227.png │ 📄 a1b2c3d4.png
│ Your secret message is hidden │ │ Your secret is hidden
│ in this image │ │ in this image │
│ │ │ │
│ Mode: DCT (Color, JPEG) │ ← v3.0+ shows mode info │ Mode: DCT (Color, JPEG) │
│ Capacity used: 45.2% │ │ Capacity used: 45.2% │
│ │ │ │
│ [ Download Image ] │ │ [ Download Image ] │
@@ -307,7 +333,7 @@ After successful encoding:
**URL:** `/decode` **URL:** `/decode`
Extract a hidden message from a stego image. Extract a hidden message or file from a stego image.
#### Input Fields #### Input Fields
@@ -315,34 +341,28 @@ Extract a hidden message from a stego image.
|-------|------|----------|-------------| |-------|------|----------|-------------|
| Reference Photo | Image file | ✓ | Same photo used for encoding | | Reference Photo | Image file | ✓ | Same photo used for encoding |
| Stego Image | Image file | ✓ | Image containing hidden message | | Stego Image | Image file | ✓ | Image containing hidden message |
| Day Phrase | Text | ✓ | Phrase for the **encoding** day | | Passphrase | Text | ✓ | Same passphrase used for encoding |
| PIN | Number | * | Same PIN used for encoding | | PIN | Number | * | Same PIN used for encoding |
| RSA Key | .pem file | * | Same RSA key used for encoding | | RSA Key | .pem file | * | Same RSA key used for encoding |
| RSA Key QR | Image file | * | QR code containing RSA key |
| RSA Key Password | Password | | Password for encrypted key | | RSA Key Password | Password | | Password for encrypted key |
\* Must match security factors used during encoding. \* Must match security factors used during encoding.
#### Automatic Mode Detection (v3.0+) #### Automatic Mode Detection
The decoder automatically detects whether a stego image uses LSB or DCT mode. You don't need to specify the mode manually—it just works! The decoder automatically detects whether a stego image uses LSB or DCT mode. You don't need to specify the mode manually—it just works!
#### Date Detection from Filename #### Decoding Process (v3.2.0 Simplified)
When you upload a stego image with a date in the filename (e.g., `stego_20251227.png`), the UI: 1. Upload the same reference photo
1. Extracts the date 2. Upload the received stego image
2. Determines the day of week 3. Enter your passphrase (no date needed!)
3. Updates the phrase label: "Saturday's Phrase" 4. Enter your PIN and/or RSA key
5. Click "Decode Message"
6. View decoded message or download decoded file
This helps you use the correct daily phrase. #### Successful Decode (Text)
#### Decoding Process
1. Fill in all required fields
2. Click "Decode Message"
3. Wait for processing
4. View decoded message on same page
#### Successful Decode
``` ```
┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐
@@ -358,13 +378,31 @@ This helps you use the correct daily phrase.
└────────────────────────────────────────┘ └────────────────────────────────────────┘
``` ```
#### Successful Decode (File)
```
┌────────────────────────────────────────┐
│ ✓ File Extracted Successfully! │
│ │
│ 📄 secret_document.pdf │
│ Size: 245 KB │
│ Type: application/pdf │
│ │
│ [ Download File ] │
│ │
│ ⚠️ File expires in 5 minutes. │
│ │
│ [ Decode Another Message ] │
└────────────────────────────────────────┘
```
#### Troubleshooting Tips #### Troubleshooting Tips
If decryption fails: If decryption fails:
1. **Check the date** - Use phrase for encoding day, not today 1. **Check passphrase** - Must be exact match (case-sensitive)
2. **Same reference photo** - Must be identical file 2. **Same reference photo** - Must be identical file
3. **Correct PIN/RSA** - Match what was used for encoding 3. **Correct PIN/RSA** - Match what was used for encoding
4. **Image integrity** - Ensure no resizing/recompression 4. **Image integrity** - Ensure no resizing/recompression (LSB mode)
--- ---
@@ -374,11 +412,17 @@ If decryption fails:
Information about the Stegasoo project, security model, and credits. Information about the Stegasoo project, security model, and credits.
Includes:
- Version information (v3.2.0)
- v3.2.0 changes explanation
- Security model overview
- Dependency status (Argon2, QR code support)
--- ---
## Embedding Modes ## Embedding Modes
Stegasoo v3.0+ offers two steganography algorithms, each with different trade-offs. Stegasoo offers two steganography algorithms, each with different trade-offs.
### LSB Mode (Default) ### LSB Mode (Default)
@@ -386,7 +430,7 @@ Stegasoo v3.0+ offers two steganography algorithms, each with different trade-of
| Aspect | Details | | Aspect | Details |
|--------|---------| |--------|---------|
| **Capacity** | ~3 bits/pixel (~770 KB for 1920×1080) | | **Capacity** | ~3 bits/pixel (~375 KB for 1920×1080) |
| **Output Format** | PNG only (lossless required) | | **Output Format** | PNG only (lossless required) |
| **Resilience** | ❌ Destroyed by JPEG compression | | **Resilience** | ❌ Destroyed by JPEG compression |
| **Best For** | Maximum capacity, controlled sharing | | **Best For** | Maximum capacity, controlled sharing |
@@ -396,31 +440,28 @@ Stegasoo v3.0+ offers two steganography algorithms, each with different trade-of
- Maximum message capacity needed - Maximum message capacity needed
- Recipient won't modify the image - Recipient won't modify the image
### DCT Mode (Experimental) ### DCT Mode
**Discrete Cosine Transform** embedding hides data in frequency domain coefficients. **Discrete Cosine Transform** embedding hides data in frequency domain coefficients.
| Aspect | Details | | Aspect | Details |
|--------|---------| |--------|---------|
| **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080 PNG, ~30-50 KB JPEG) | | **Capacity** | ~0.25 bits/pixel (~65 KB for 1920×1080 PNG, ~50 KB JPEG) |
| **Output Formats** | PNG or JPEG | | **Output Formats** | PNG or JPEG |
| **Resilience** | ✅ Survives JPEG compression | | **Resilience** | ✅ Better resistance to analysis |
| **Best For** | Social media, messaging apps, web sharing | | **Best For** | Stealth requirements, frequency domain hiding |
> ⚠️ **Experimental Feature**: DCT mode is marked experimental and may have edge cases. Test with your specific workflow before relying on it for critical messages.
**When to use DCT:** **When to use DCT:**
- Posting to social media (which recompresses images) - When stealth is important
- Sharing via messaging apps (WhatsApp, Telegram, etc.)
- When channel may apply JPEG compression
- Smaller messages that fit in reduced capacity - Smaller messages that fit in reduced capacity
- When you want JPEG output for natural appearance
#### DCT Output Formats #### DCT Output Formats
| Format | Pros | Cons | | Format | Pros | Cons |
|--------|------|------| |--------|------|------|
| **PNG** | Lossless, predictable | Larger file, obvious if channel expects JPEG | | **PNG** | Lossless, predictable | Larger file |
| **JPEG** | Native format, natural | Slightly lower capacity | | **JPEG** | Native format, natural, smaller | Slightly lower capacity |
#### DCT Color Modes #### DCT Color Modes
@@ -435,9 +476,9 @@ For a 1920×1080 image:
| Mode | Approximate Capacity | | Mode | Approximate Capacity |
|------|---------------------| |------|---------------------|
| LSB (PNG) | ~770 KB | | LSB (PNG) | ~375 KB |
| DCT (PNG, Color) | ~65 KB | | DCT (PNG, Color) | ~65 KB |
| DCT (JPEG) | ~30-50 KB | | DCT (JPEG) | ~50 KB |
### Choosing the Right Mode ### Choosing the Right Mode
@@ -446,19 +487,21 @@ For a 1920×1080 image:
│ Mode Selection Guide │ │ Mode Selection Guide │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ │ │ │
Will the image be recompressed (social media, chat apps)? Need maximum capacity?
│ │ │ │ │ │
┌───────────┴───────────┐ ┌───────┴───────┐
│ ▼ ▼ │ │ ▼ ▼ │
│ YES NO │ │ YES NO │
│ │ │ │ │ │ │ │
│ ▼ ▼ │ │ ▼ ▼ │
Use DCT Mode Use LSB Mode Use LSB Need stealth?
│ (default) │ │
│ ┌───────┴───────┐ │
│ ▼ ▼ │
│ YES NO │
│ │ │ │ │ │ │ │
│ ▼ ▼ │ │ ▼ ▼ │
Output: JPEG (natural) Output: PNG (automatic) Use DCT Use LSB
│ Color: Color (usually) Capacity: ~770 KB │
│ Capacity: ~30-50 KB │
│ │ │ │
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
``` ```
@@ -482,6 +525,9 @@ For a 1920×1080 image:
│ │ │ (Reference) │ │ (Carrier) │ │ │ │ │ │ (Reference) │ │ (Carrier) │ │ │
│ │ └──────────────┘ └──────────────┘ │ │ │ │ └──────────────┘ └──────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ Passphrase: [________________________] │ │
│ │ PIN: [____________] │ │
│ │ │ │
│ │ [Advanced Options ▼] │ │ │ │ [Advanced Options ▼] │ │
│ │ ┌────────────────────────────────────────────┐ │ │ │ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ Embedding Mode: [LSB ▼] │ │ │ │ │ │ Embedding Mode: [LSB ▼] │ │ │
@@ -508,7 +554,6 @@ For a 1920×1080 image:
| Success | Green | Positive actions | | Success | Green | Positive actions |
| Warning | Yellow | Caution messages | | Warning | Yellow | Caution messages |
| Error | Red | Error states | | Error | Red | Error states |
| Experimental | Orange badge | DCT mode indicator |
### Form Validation ### Form Validation
@@ -516,6 +561,7 @@ For a 1920×1080 image:
- Clear error messages in alerts - Clear error messages in alerts
- Required field indicators - Required field indicators
- Input constraints (max length, format) - Input constraints (max length, format)
- Passphrase word count validation (v3.2.0)
### Loading States ### Loading States
@@ -535,7 +581,7 @@ During long operations:
Types: Types:
- Success (green) - Operation completed - Success (green) - Operation completed
- Error (red) - Operation failed - Error (red) - Operation failed
- Warning (yellow) - Caution needed - Warning (yellow) - Caution needed (e.g., short passphrase)
--- ---
@@ -545,14 +591,14 @@ Types:
**Party A:** **Party A:**
1. Go to `/generate` 1. Go to `/generate`
2. Configure: PIN ✓, 3 words, 6 digits 2. Configure: PIN ✓, 4 words, 6 digits
3. Click "Generate Credentials" 3. Click "Generate Credentials"
4. **Write down** phrases and PIN on paper 4. **Write down** passphrase and PIN on paper
5. **Memorize** over the next few days 5. **Memorize** over the next few days
6. Destroy the paper 6. Destroy the paper
**Share with Party B (in person or secure channel):** **Share with Party B (in person or secure channel):**
- The 7 daily phrases - The passphrase (just one phrase now!)
- The PIN - The PIN
- The reference photo file (if not already shared) - The reference photo file (if not already shared)
@@ -562,40 +608,53 @@ Types:
2. Upload your shared reference photo 2. Upload your shared reference photo
3. Upload any carrier image (meme, vacation photo, etc.) 3. Upload any carrier image (meme, vacation photo, etc.)
4. Type your secret message 4. Type your secret message
5. Enter today's phrase (check your memory!) 5. Enter your passphrase
6. Enter your PIN 6. Enter your PIN
7. Click "Encode Message" 7. Click "Encode Message"
8. Download or share the resulting image 8. Download or share the resulting image
9. Send via any channel (email, file transfer) 9. Send via any channel (email, file transfer)
### Sending via Social Media (DCT Mode) ### Sending with DCT Mode
1. Go to `/encode` 1. Go to `/encode`
2. Upload your shared reference photo 2. Upload your shared reference photo
3. Upload carrier image 3. Upload carrier image
4. Type your secret message 4. Type your secret message
5. Enter today's phrase and PIN 5. Enter your passphrase and PIN
6. **Expand "Advanced Options"** 6. **Expand "Advanced Options"**
7. **Select "DCT" embedding mode** 7. **Select "DCT" embedding mode**
8. **Select "JPEG" output format** 8. **Select "JPEG" output format** (optional)
9. Click "Encode Message" 9. Click "Encode Message"
10. Download and post to social media 10. Download and share
The recipient can decode even after the platform recompresses the image! ### Receiving a Secret Message (v3.2.0 Simplified)
### Receiving a Secret Message
1. Receive the stego image through any channel 1. Receive the stego image through any channel
2. Go to `/decode` 2. Go to `/decode`
3. Upload the same reference photo 3. Upload the same reference photo
4. Upload the received stego image 4. Upload the received stego image
5. Note the date in the filename (e.g., `_20251227`) 5. Enter your passphrase (no date needed!)
6. Enter the phrase for **that day** (not today!) 6. Enter your PIN
7. Enter the PIN 7. Click "Decode Message"
8. Click "Decode Message" 8. Read the secret message or download the file
9. Read the secret message
> 💡 Decoding automatically detects LSB vs DCT mode—no configuration needed! ### Embedding a File
1. Go to `/encode`
2. Upload reference photo and carrier image
3. Select "File" as payload type
4. Upload the file to embed (max 2MB)
5. Enter passphrase and PIN
6. Click "Encode Message"
7. Download the stego image
### Extracting a File
1. Go to `/decode`
2. Upload reference photo and stego image
3. Enter passphrase and PIN
4. Click "Decode Message"
5. Click "Download File" to save the extracted file
### Changing Credentials ### Changing Credentials
@@ -613,7 +672,6 @@ To rotate to new credentials:
| Feature | Implementation | | Feature | Implementation |
|---------|----------------| |---------|----------------|
| Local date detection | JavaScript `Date()` object |
| No credential storage | Nothing saved in browser | | No credential storage | Nothing saved in browser |
| Automatic cleanup | Files deleted after 5 minutes | | Automatic cleanup | Files deleted after 5 minutes |
| HTTPS support | Configure at server level | | HTTPS support | Configure at server level |
@@ -643,7 +701,7 @@ To rotate to new credentials:
| Mode | Security Consideration | | Mode | Security Consideration |
|------|----------------------| |------|----------------------|
| LSB | Full capacity, but fragile to modification | | LSB | Full capacity, but fragile to modification |
| DCT | Lower capacity, but survives recompression | | DCT | Lower capacity, frequency domain hiding |
Both modes use the same strong encryption (AES-256-GCM with Argon2id key derivation). Both modes use the same strong encryption (AES-256-GCM with Argon2id key derivation).
@@ -666,7 +724,9 @@ Both modes use the same strong encryption (AES-256-GCM with Argon2id key derivat
| File expiry | 5 minutes | `TEMP_FILE_EXPIRY` | | File expiry | 5 minutes | `TEMP_FILE_EXPIRY` |
| Max image pixels | 4 MP | `stegasoo.constants` | | Max image pixels | 4 MP | `stegasoo.constants` |
| Max message size | 50 KB | `stegasoo.constants` | | Max message size | 50 KB | `stegasoo.constants` |
| Max file payload | 2 MB | `stegasoo.constants` |
| PIN length | 6-9 digits | `stegasoo.constants` | | PIN length | 6-9 digits | `stegasoo.constants` |
| Passphrase words | 3-12 | `stegasoo.constants` |
### Production Deployment ### Production Deployment
@@ -714,7 +774,7 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 768M # Increased for scipy/DCT memory: 768M
reservations: reservations:
memory: 384M memory: 384M
``` ```
@@ -728,18 +788,17 @@ services:
#### "Decryption failed" #### "Decryption failed"
**Causes:** **Causes:**
- Wrong day phrase - Wrong passphrase
- Wrong PIN - Wrong PIN
- Different reference photo - Different reference photo
- Stego image was modified - Stego image was modified
**Solutions:** **Solutions:**
1. Check the date in the stego filename 1. Verify exact passphrase (case-sensitive)
2. Use the phrase for that specific day 2. Verify you're using the original reference photo
3. Verify you're using the original reference photo 3. Ensure the stego image wasn't resized/recompressed (LSB mode)
4. Ensure the stego image wasn't resized/recompressed (LSB mode)
#### "Invalid or missing Stegasoo header" (DCT Mode) #### "Invalid or missing Stegasoo header"
**Causes:** **Causes:**
- Image was heavily recompressed - Image was heavily recompressed
@@ -747,9 +806,9 @@ services:
- Corrupted during transfer - Corrupted during transfer
**Solutions:** **Solutions:**
1. If sharing via lossy channel, ensure DCT mode was used for encoding 1. Verify credentials match
2. Verify credentials match 2. Try obtaining original file
3. Try obtaining original file 3. If using DCT mode, some modification is expected to work
#### "Carrier image too small" #### "Carrier image too small"
@@ -758,8 +817,15 @@ services:
**Solutions:** **Solutions:**
1. Use a larger carrier image (more pixels) 1. Use a larger carrier image (more pixels)
2. Shorten the message 2. Shorten the message
3. Use LSB mode for more capacity (if channel supports it) 3. Use LSB mode for more capacity
4. Check capacity with `/info` command (CLI)
#### "Passphrase should have at least 4 words"
**Cause:** Passphrase too short (v3.2.0 warning)
**Solutions:**
1. Use a longer passphrase for better security
2. Can still proceed with shorter passphrase (warning only)
#### "You must provide at least a PIN or RSA Key" #### "You must provide at least a PIN or RSA Key"
@@ -791,13 +857,13 @@ services:
2. If key is unencrypted, leave password blank 2. If key is unencrypted, leave password blank
3. Re-download or regenerate the key 3. Re-download or regenerate the key
#### DCT mode shows "jpegio not available" #### DCT mode shows "requires scipy"
**Cause:** jpegio library not installed (required for JPEG output) **Cause:** scipy library not installed
**Solution:** **Solution:**
```bash ```bash
pip install jpegio pip install scipy
# Or rebuild Docker image # Or rebuild Docker image
docker-compose build --no-cache docker-compose build --no-cache
``` ```
@@ -883,4 +949,5 @@ The web app can be added to home screen on mobile devices for quick access.
- [CLI Documentation](CLI.md) - Command-line interface - [CLI Documentation](CLI.md) - Command-line interface
- [API Documentation](API.md) - REST API reference - [API Documentation](API.md) - REST API reference
- [README](README.md) - Project overview - [Web Frontend Update Summary](web/WEB_FRONTEND_UPDATE_SUMMARY_V3.2.0.md) - Migration details
- [README](../README.md) - Project overview

View File

@@ -545,12 +545,13 @@ async def api_generate(request: GenerateRequest):
raise HTTPException(400, f"rsa_bits must be one of {VALID_RSA_SIZES}") raise HTTPException(400, f"rsa_bits must be one of {VALID_RSA_SIZES}")
try: try:
# v3.2.0: Call with passphrase_words parameter
creds = generate_credentials( creds = generate_credentials(
use_pin=request.use_pin, use_pin=request.use_pin,
use_rsa=request.use_rsa, use_rsa=request.use_rsa,
pin_length=request.pin_length, pin_length=request.pin_length,
rsa_bits=request.rsa_bits, rsa_bits=request.rsa_bits,
words_per_passphrase=request.words_per_passphrase passphrase_words=request.words_per_passphrase, # Map API field to library parameter
) )
return GenerateResponse( return GenerateResponse(

View File

@@ -240,12 +240,13 @@ def generate():
rsa_bits = 2048 rsa_bits = 2048
try: try:
# v3.2.0 FIX: Use correct parameter name 'passphrase_words'
creds = generate_credentials( creds = generate_credentials(
use_pin=use_pin, use_pin=use_pin,
use_rsa=use_rsa, use_rsa=use_rsa,
pin_length=pin_length, pin_length=pin_length,
rsa_bits=rsa_bits, rsa_bits=rsa_bits,
words_per_passphrase=words_per_passphrase passphrase_words=words_per_passphrase, # FIX: was words_per_passphrase=
) )
# Store RSA key temporarily for QR generation # Store RSA key temporarily for QR generation
@@ -892,11 +893,15 @@ def decode_page():
file_id=file_id, file_id=file_id,
filename=filename, filename=filename,
file_size=format_size(len(decode_result.file_data)), file_size=format_size(len(decode_result.file_data)),
mime_type=decode_result.mime_type mime_type=decode_result.mime_type,
has_qrcode_read=HAS_QRCODE_READ
) )
else: else:
# Text content # Text content
return render_template('decode.html', decoded_message=decode_result.message) return render_template('decode.html',
decoded_message=decode_result.message,
has_qrcode_read=HAS_QRCODE_READ
)
except DecryptionError: except DecryptionError:
flash('Decryption failed. Check your passphrase, PIN, RSA key, and reference photo.', 'error') flash('Decryption failed. Check your passphrase, PIN, RSA key, and reference photo.', 'error')

View File

@@ -47,8 +47,8 @@
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert"> <div class="alert alert-{{ 'danger' if category == 'error' else ('warning' if category == 'warning' else 'success') }} alert-dismissible fade show" role="alert">
<i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' }} me-2"></i> <i class="bi bi-{{ 'exclamation-triangle' if category == 'error' else ('exclamation-circle' if category == 'warning' else 'check-circle') }} me-2"></i>
{{ message }} {{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button> <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div> </div>
@@ -63,7 +63,7 @@
<div class="container text-center text-muted"> <div class="container text-center text-muted">
<small> <small>
<img src="{{ url_for('static', filename='favicon.svg') }}" alt="" height="16" class="me-1" style="vertical-align: text-bottom;"> <img src="{{ url_for('static', filename='favicon.svg') }}" alt="" height="16" class="me-1" style="vertical-align: text-bottom;">
Stegasoo v{{ version }} — Steganography using "Reference Photo Hashing + Day-Phrase + PIN/Key". Stegasoo v{{ version }} — Steganography with Reference Photo + Passphrase + PIN/Key
</small> </small>
</div> </div>
</footer> </footer>

View File

@@ -0,0 +1,525 @@
# Stegasoo v3.2.0 Release Checklist
## Overview
This checklist covers comprehensive functionality testing for the v3.2.0 release, which introduces breaking changes from v3.1.x.
### Breaking Changes in v3.2.0
| Change | v3.1.x | v3.2.0 |
|--------|--------|--------|
| Passphrase model | 7 daily phrases (`day_phrase`) | Single `passphrase` |
| Date parameter | Required `date_str` | Removed |
| Default words | 3 | 4 |
| Format version | 3 | 4 |
| Backward compatible | N/A | ❌ Cannot decode v3.1.x images |
---
## 1. Core Library Tests
### 1.1 Key Generation (`src/stegasoo/keygen.py`)
- [ ] **generate_pin()** - Default 6 digits, no leading zero
- [ ] **generate_pin(length=9)** - Custom length works
- [ ] **generate_phrase(words=4)** - Default 4 words
- [ ] **generate_phrase(words=6)** - Custom word count
- [ ] **generate_credentials(use_pin=True)** - Returns single passphrase
- [ ] **generate_credentials(use_rsa=True)** - RSA key generation
- [ ] **generate_credentials(use_pin=False, use_rsa=False)** - Raises error
- [ ] **Credentials.passphrase** - Single string, not dict
- [ ] **Credentials.passphrase_entropy** - Correct entropy (4 words = 44 bits)
- [ ] **Credentials.total_entropy** - Sum is correct
### 1.2 Encoding (`src/stegasoo/steganography.py`)
- [ ] **encode() with passphrase** - New parameter name works
- [ ] **encode() without date_str** - No date parameter needed
- [ ] **HEADER_OVERHEAD = 65** - Correct constant
- [ ] **LSB mode** - Default, full color PNG output
- [ ] **DCT mode** - Frequency domain embedding
- [ ] **DCT + JPEG output** - Works correctly
- [ ] **DCT + color mode** - Preserves colors
- [ ] **Capacity calculation** - Uses 65-byte overhead
### 1.3 Decoding (`src/stegasoo/steganography.py`)
- [ ] **decode() with passphrase** - New parameter name works
- [ ] **decode() without date_str** - No date parameter needed
- [ ] **Auto mode detection** - LSB vs DCT automatic
- [ ] **Wrong passphrase** - Raises DecryptionError
- [ ] **Wrong PIN** - Raises DecryptionError
- [ ] **Wrong reference photo** - Raises DecryptionError
### 1.4 DCT Steganography (`src/stegasoo/dct_steganography.py`)
- [ ] **Y channel extraction** - Uses correct formula (not just R channel)
- [ ] **Color mode encoding** - YCbCr conversion works
- [ ] **Grayscale mode** - Converts to grayscale
- [ ] **JPEG output** - Quality 95, proper format
- [ ] **PNG output** - Lossless DCT output
### 1.5 Batch Processing (`src/stegasoo/batch.py`)
- [ ] **BatchCredentials.passphrase** - Single field, not dict
- [ ] **BatchCredentials.from_dict()** - Accepts both old and new format
- [ ] **batch_encode()** - Uses passphrase parameter
- [ ] **batch_decode()** - Uses passphrase parameter
### 1.6 Validation
- [ ] **validate_passphrase()** - New function works
- [ ] **validate_passphrase() warning** - Warns if < 4 words
- [ ] **validate_pin()** - 6-9 digits, no leading zero
- [ ] **validate_message()** - Non-empty, within size limits
---
## 2. CLI Frontend Tests (`frontends/cli/main.py`)
### 2.1 Generate Command
```bash
# Test default generation (4 words, PIN)
stegasoo generate --pin
# Test custom word count
stegasoo generate --pin --words 6
# Test RSA generation
stegasoo generate --rsa
# Test JSON output
stegasoo generate --pin --json
```
- [ ] Output shows single `PASSPHRASE:` not daily phrases
- [ ] Default is 4 words
- [ ] JSON has `passphrase` field, not `phrases` dict
- [ ] Entropy shows `passphrase_entropy`
### 2.2 Encode Command
```bash
# Test basic encode
stegasoo encode -r ref.jpg -c carrier.png \
-p "word1 word2 word3 word4" --pin 123456 \
-m "Secret message"
# Test DCT mode
stegasoo encode -r ref.jpg -c carrier.png \
-p "word1 word2 word3 word4" --pin 123456 \
-m "Secret" --mode dct
# Test DCT + JPEG
stegasoo encode -r ref.jpg -c carrier.png \
-p "word1 word2 word3 word4" --pin 123456 \
-m "Secret" --mode dct --dct-format jpeg
```
- [ ] `-p` / `--passphrase` parameter works
- [ ] No `--date` parameter exists
- [ ] LSB mode produces PNG
- [ ] DCT mode works
- [ ] DCT + JPEG output works
- [ ] Output filename has no date suffix
### 2.3 Decode Command
```bash
# Test basic decode
stegasoo decode -r ref.jpg -s stego.png \
-p "word1 word2 word3 word4" --pin 123456
# Test auto mode detection
stegasoo decode -r ref.jpg -s stego.png \
-p "word1 word2 word3 word4" --pin 123456 --mode auto
```
- [ ] `-p` / `--passphrase` parameter works
- [ ] No `--date` parameter exists
- [ ] Auto-detects LSB vs DCT
- [ ] Outputs decoded message
### 2.4 Other Commands
```bash
# Verify command
stegasoo verify -s stego.png
# Compare command
stegasoo compare original.png stego.png
# Modes command
stegasoo modes
# Capacity command
stegasoo capacity carrier.png
```
- [ ] All commands work without errors
- [ ] No references to "day phrase" or dates
---
## 3. API Frontend Tests (`frontends/api/main.py`)
### 3.1 Status Endpoint
```bash
curl http://localhost:8000/
```
- [ ] Returns `version: "3.2.0"`
- [ ] Includes `breaking_changes` object
- [ ] No `day_names` field
### 3.2 Generate Endpoint
```bash
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": true, "words_per_passphrase": 4}'
```
- [ ] Parameter is `words_per_passphrase` (not `words_per_phrase`)
- [ ] Response has `passphrase` string field
- [ ] Response has `phrases: null`
- [ ] Entropy field is `passphrase` not `phrase`
### 3.3 Encode Endpoint
```bash
curl -X POST http://localhost:8000/encode \
-H "Content-Type: application/json" \
-d '{
"message": "Secret",
"passphrase": "word1 word2 word3 word4",
"pin": "123456",
"reference_photo_base64": "...",
"carrier_image_base64": "..."
}'
```
- [ ] Parameter is `passphrase` (not `day_phrase`)
- [ ] No `date_str` parameter accepted
- [ ] Response has `date_used: null`
- [ ] Response has `day_of_week: null`
### 3.4 Decode Endpoint
```bash
curl -X POST http://localhost:8000/decode \
-H "Content-Type: application/json" \
-d '{
"passphrase": "word1 word2 word3 word4",
"pin": "123456",
"stego_image_base64": "...",
"reference_photo_base64": "..."
}'
```
- [ ] Parameter is `passphrase` (not `day_phrase`)
- [ ] No `date_str` parameter needed
- [ ] Auto-detects embedding mode
### 3.5 Multipart Endpoints
```bash
# Encode multipart
curl -X POST http://localhost:8000/encode/multipart \
-F "passphrase=word1 word2 word3 word4" \
-F "pin=123456" \
-F "message=Secret" \
-F "reference_photo=@ref.jpg" \
-F "carrier=@carrier.png"
# Decode multipart
curl -X POST http://localhost:8000/decode/multipart \
-F "passphrase=word1 word2 word3 word4" \
-F "pin=123456" \
-F "reference_photo=@ref.jpg" \
-F "stego_image=@stego.png"
```
- [ ] Form field is `passphrase` (not `day_phrase`)
- [ ] No `date_str` field
- [ ] Headers include `X-Stegasoo-Version: 3.2.0`
- [ ] No date headers in response
---
## 4. Web Frontend Tests (`frontends/web/app.py`)
### 4.1 Generate Page (`/generate`)
- [ ] Form field is `words_per_passphrase`
- [ ] Default slider value is 4
- [ ] Output shows single passphrase, not 7 daily phrases
- [ ] Memory aid works with single passphrase
- [ ] Entropy display shows `passphrase_entropy`
- [ ] v3.2.0 badge visible
### 4.2 Encode Page (`/encode`)
- [ ] Form field is `passphrase`
- [ ] No date selection field
- [ ] v3.2.0 badge on passphrase label
- [ ] Passphrase validation warning works (< 4 words)
- [ ] DCT mode options work
- [ ] Success result shows no date info
### 4.3 Decode Page (`/decode`)
- [ ] Form field is `passphrase`
- [ ] No date input field
- [ ] No date detection from filename JavaScript
- [ ] Troubleshooting mentions v3.2.0 compatibility
- [ ] Auto mode detection works
### 4.4 Other Pages
- [ ] **Home** (`/`) - Shows v3.2.0 badge, passphrase terminology
- [ ] **About** (`/about`) - Updated terminology, v3.2.0 features
- [ ] **Footer** - Says "Passphrase" not "Day-Phrase"
---
## 5. Integration Tests
### 5.1 Full Roundtrip Tests
```bash
# Generate → Encode → Decode (LSB)
stegasoo generate --pin > creds.json
stegasoo encode -r ref.jpg -c carrier.png -p "..." --pin 123456 -m "Test" -o stego.png
stegasoo decode -r ref.jpg -s stego.png -p "..." --pin 123456
# Generate → Encode → Decode (DCT)
stegasoo encode -r ref.jpg -c carrier.png -p "..." --pin 123456 -m "Test" --mode dct -o stego_dct.png
stegasoo decode -r ref.jpg -s stego_dct.png -p "..." --pin 123456
```
- [ ] LSB roundtrip works
- [ ] DCT roundtrip works
- [ ] DCT + JPEG roundtrip works
- [ ] File embedding roundtrip works
### 5.2 Cross-Frontend Tests
- [ ] Encode via CLI, decode via API
- [ ] Encode via API, decode via Web
- [ ] Encode via Web, decode via CLI
### 5.3 Error Handling
- [ ] Wrong passphrase shows clear error
- [ ] Wrong PIN shows clear error
- [ ] Wrong reference photo shows clear error
- [ ] Capacity exceeded shows clear error
- [ ] Invalid image shows clear error
---
## 6. Documentation Tests
### 6.1 CLI Documentation (`frontends/CLI.md`)
- [ ] "What's New in v3.2.0" section exists
- [ ] All examples use 4-word passphrases
- [ ] No `--date` parameter in examples
- [ ] Command reference is complete
- [ ] Migration notes for v3.1.x users
### 6.2 API Documentation (`frontends/API.md`)
- [ ] "What's New in v3.2.0" section exists
- [ ] All request examples use `passphrase`
- [ ] No `date_str` in request models
- [ ] Response models show `date_used: null`
- [ ] Code examples updated
### 6.3 Web UI Documentation (`frontends/WEB_UI.md`)
- [ ] "What's New in v3.2.0" section exists
- [ ] Workflow examples use passphrase
- [ ] No date selection in screenshots/descriptions
- [ ] Troubleshooting updated
---
## 7. Backward Compatibility Tests
### 7.1 v3.1.x Image Decoding
- [ ] Attempting to decode v3.1.x image with v3.2.0 fails gracefully
- [ ] Error message mentions version incompatibility
- [ ] Suggests using v3.1.x for old images
### 7.2 Migration Path
- [ ] `BatchCredentials.from_dict()` accepts old `day_phrase` key
- [ ] `generate_credentials_legacy()` available if needed
- [ ] Documentation explains migration steps
---
## 8. Unit Test Updates
### 8.1 Test Files to Update
- [ ] `tests/test_stegasoo.py` - Use `passphrase` parameter
- [ ] `tests/test_batch.py` - Use `passphrase` in credentials
- [ ] `tests/test_compression.py` - No changes needed (compression unchanged)
### 8.2 New Tests Needed
- [ ] Test single passphrase generation
- [ ] Test `passphrase_words` parameter
- [ ] Test `validate_passphrase()` function
- [ ] Test DCT Y channel extraction
- [ ] Test 65-byte header overhead
---
## 9. Release Artifacts
### 9.1 Version Bumps
- [ ] `src/stegasoo/constants.py` - `__version__ = "3.2.0"`
- [ ] `pyproject.toml` or `setup.py` - version updated
- [ ] `CHANGELOG.md` - v3.2.0 section added
### 9.2 Documentation
- [ ] `README.md` - Updated for v3.2.0
- [ ] `frontends/CLI.md` - Complete
- [ ] `frontends/API.md` - Complete
- [ ] `frontends/WEB_UI.md` - Complete
### 9.3 Git
- [ ] All changes committed
- [ ] Tag created: `v3.2.0`
- [ ] Release notes written
---
## 10. Quick Smoke Test Script
```bash
#!/bin/bash
# v3.2.0 Smoke Test
set -e
echo "=== Stegasoo v3.2.0 Smoke Test ==="
# Check version
echo "1. Checking version..."
python -c "import stegasoo; print(f'Version: {stegasoo.__version__}')"
# Generate credentials
echo "2. Generating credentials..."
python -c "
from stegasoo import generate_credentials
creds = generate_credentials(use_pin=True, passphrase_words=4)
print(f'Passphrase: {creds.passphrase}')
print(f'PIN: {creds.pin}')
print(f'Entropy: {creds.total_entropy} bits')
assert ' ' in creds.passphrase, 'Passphrase should have spaces'
assert len(creds.passphrase.split()) == 4, 'Should have 4 words'
print('✓ Credentials OK')
"
# Test encode/decode roundtrip
echo "3. Testing encode/decode roundtrip..."
python -c "
from stegasoo import encode, decode
from PIL import Image
import io
# Create test image
img = Image.new('RGB', (200, 200), color='blue')
buf = io.BytesIO()
img.save(buf, format='PNG')
test_image = buf.getvalue()
# Encode
result = encode(
message='Hello v3.2.0!',
reference_photo=test_image,
carrier_image=test_image,
passphrase='test phrase four words',
pin='123456'
)
print(f'Encoded: {result.filename}')
# Decode
decoded = decode(
stego_image=result.stego_image,
reference_photo=test_image,
passphrase='test phrase four words',
pin='123456'
)
assert decoded.message == 'Hello v3.2.0!', 'Message mismatch'
print(f'Decoded: {decoded.message}')
print('✓ Roundtrip OK')
"
# Test DCT mode
echo "4. Testing DCT mode..."
python -c "
from stegasoo import encode, decode, has_dct_support
if has_dct_support():
from PIL import Image
import io
img = Image.new('RGB', (200, 200), color='green')
buf = io.BytesIO()
img.save(buf, format='PNG')
test_image = buf.getvalue()
result = encode(
message='DCT test',
reference_photo=test_image,
carrier_image=test_image,
passphrase='dct test phrase here',
pin='123456',
embed_mode='dct'
)
decoded = decode(
stego_image=result.stego_image,
reference_photo=test_image,
passphrase='dct test phrase here',
pin='123456'
)
assert decoded.message == 'DCT test'
print('✓ DCT Mode OK')
else:
print('⚠ DCT mode not available (scipy not installed)')
"
echo ""
echo "=== All smoke tests passed! ==="
```
---
## Sign-Off
| Area | Tested By | Date | Status |
|------|-----------|------|--------|
| Core Library | | | ☐ |
| CLI Frontend | | | ☐ |
| API Frontend | | | ☐ |
| Web Frontend | | | ☐ |
| Documentation | | | ☐ |
| Integration | | | ☐ |
**Release Approved:**
**Released By:** _________________
**Release Date:** _________________

View File

@@ -1,5 +1,10 @@
""" """
Tests for Stegasoo batch processing module. Tests for Stegasoo batch processing module (v3.2.0).
Updated for v3.2.0:
- Uses 'passphrase' instead of 'phrase' in credentials dict
- No date_str parameter
- BatchCredentials.passphrase is a single string
""" """
import pytest import pytest
@@ -13,6 +18,7 @@ from stegasoo.batch import (
BatchResult, BatchResult,
BatchItem, BatchItem,
BatchStatus, BatchStatus,
BatchCredentials,
batch_capacity_check, batch_capacity_check,
print_batch_result, print_batch_result,
) )
@@ -41,6 +47,15 @@ def sample_images(temp_dir):
return images return images
@pytest.fixture
def sample_credentials():
"""Create sample v3.2.0 credentials dict."""
return {
"passphrase": "test phrase four words", # v3.2.0: single passphrase
"pin": "123456"
}
class TestBatchItem: class TestBatchItem:
"""Tests for BatchItem dataclass.""" """Tests for BatchItem dataclass."""
@@ -90,6 +105,50 @@ class TestBatchResult:
assert result.duration == 10.0 assert result.duration == 10.0
class TestBatchCredentials:
"""Tests for BatchCredentials dataclass (v3.2.0)."""
def test_from_dict_new_format(self):
"""Should parse v3.2.0 format with 'passphrase' key."""
data = {
"passphrase": "test phrase four words",
"pin": "123456"
}
creds = BatchCredentials.from_dict(data)
assert creds.passphrase == "test phrase four words"
assert creds.pin == "123456"
def test_from_dict_legacy_format(self):
"""Should parse legacy format with 'day_phrase' key for migration."""
data = {
"day_phrase": "legacy phrase here", # Old key name
"pin": "123456"
}
creds = BatchCredentials.from_dict(data)
# Should accept old key and map to passphrase
assert creds.passphrase == "legacy phrase here"
assert creds.pin == "123456"
def test_to_dict(self):
"""Should serialize to v3.2.0 format."""
creds = BatchCredentials(
passphrase="test phrase four words",
pin="123456"
)
result = creds.to_dict()
assert result['passphrase'] == "test phrase four words"
assert result['pin'] == "123456"
assert 'day_phrase' not in result # Old key should not be present
def test_passphrase_is_string(self):
"""Passphrase should be a string, not a dict."""
creds = BatchCredentials(
passphrase="test phrase four words",
pin="123456"
)
assert isinstance(creds.passphrase, str)
class TestBatchProcessor: class TestBatchProcessor:
"""Tests for BatchProcessor class.""" """Tests for BatchProcessor class."""
@@ -145,13 +204,13 @@ class TestBatchProcessor:
results = list(processor.find_images([temp_dir], recursive=True)) results = list(processor.find_images([temp_dir], recursive=True))
assert any(p.name == "nested.png" for p in results) assert any(p.name == "nested.png" for p in results)
def test_batch_encode_requires_message_or_file(self, sample_images): def test_batch_encode_requires_message_or_file(self, sample_images, sample_credentials):
"""Should raise if neither message nor file provided.""" """Should raise if neither message nor file provided."""
processor = BatchProcessor() processor = BatchProcessor()
with pytest.raises(ValueError, match="message or file_payload"): with pytest.raises(ValueError, match="message or file_payload"):
processor.batch_encode( processor.batch_encode(
images=sample_images, images=sample_images,
credentials={"phrase": "test", "pin": "123456"}, credentials=sample_credentials,
) )
def test_batch_encode_requires_credentials(self, sample_images): def test_batch_encode_requires_credentials(self, sample_images):
@@ -163,14 +222,28 @@ class TestBatchProcessor:
message="test", message="test",
) )
def test_batch_encode_creates_result(self, sample_images, temp_dir): def test_batch_encode_accepts_passphrase_credentials(self, sample_images, temp_dir, sample_credentials):
"""Should accept v3.2.0 format credentials with passphrase."""
processor = BatchProcessor()
result = processor.batch_encode(
images=sample_images,
message="Test message",
output_dir=temp_dir / "output",
credentials=sample_credentials, # Uses 'passphrase' key
)
assert isinstance(result, BatchResult)
assert result.operation == "encode"
assert result.total == 3
def test_batch_encode_creates_result(self, sample_images, temp_dir, sample_credentials):
"""Should return BatchResult with correct structure.""" """Should return BatchResult with correct structure."""
processor = BatchProcessor() processor = BatchProcessor()
result = processor.batch_encode( result = processor.batch_encode(
images=sample_images, images=sample_images,
message="Test message", message="Test message",
output_dir=temp_dir / "output", output_dir=temp_dir / "output",
credentials={"phrase": "test phrase", "pin": "123456"}, credentials=sample_credentials,
) )
assert isinstance(result, BatchResult) assert isinstance(result, BatchResult)
@@ -184,19 +257,31 @@ class TestBatchProcessor:
with pytest.raises(ValueError, match="Credentials"): with pytest.raises(ValueError, match="Credentials"):
processor.batch_decode(images=sample_images) processor.batch_decode(images=sample_images)
def test_batch_decode_creates_result(self, sample_images): def test_batch_decode_accepts_passphrase_credentials(self, sample_images, sample_credentials):
"""Should return BatchResult with correct structure.""" """Should accept v3.2.0 format credentials with passphrase."""
processor = BatchProcessor() processor = BatchProcessor()
result = processor.batch_decode( result = processor.batch_decode(
images=sample_images, images=sample_images,
credentials={"phrase": "test phrase", "pin": "123456"}, credentials=sample_credentials, # Uses 'passphrase' key
) )
assert isinstance(result, BatchResult) assert isinstance(result, BatchResult)
assert result.operation == "decode" assert result.operation == "decode"
assert result.total == 3 assert result.total == 3
def test_progress_callback_called(self, sample_images): def test_batch_decode_creates_result(self, sample_images, sample_credentials):
"""Should return BatchResult with correct structure."""
processor = BatchProcessor()
result = processor.batch_decode(
images=sample_images,
credentials=sample_credentials,
)
assert isinstance(result, BatchResult)
assert result.operation == "decode"
assert result.total == 3
def test_progress_callback_called(self, sample_images, sample_credentials):
"""Progress callback should be called for each item.""" """Progress callback should be called for each item."""
processor = BatchProcessor() processor = BatchProcessor()
callback = Mock() callback = Mock()
@@ -204,13 +289,13 @@ class TestBatchProcessor:
processor.batch_encode( processor.batch_encode(
images=sample_images, images=sample_images,
message="Test", message="Test",
credentials={"phrase": "test", "pin": "123456"}, credentials=sample_credentials,
progress_callback=callback, progress_callback=callback,
) )
assert callback.call_count == 3 assert callback.call_count == 3
def test_custom_encode_func(self, sample_images, temp_dir): def test_custom_encode_func(self, sample_images, temp_dir, sample_credentials):
"""Should use custom encode function if provided.""" """Should use custom encode function if provided."""
processor = BatchProcessor() processor = BatchProcessor()
encode_mock = Mock() encode_mock = Mock()
@@ -219,7 +304,7 @@ class TestBatchProcessor:
images=sample_images, images=sample_images,
message="Test", message="Test",
output_dir=temp_dir / "output", output_dir=temp_dir / "output",
credentials={"phrase": "test", "pin": "123456"}, credentials=sample_credentials,
encode_func=encode_mock, encode_func=encode_mock,
) )
@@ -289,3 +374,36 @@ class TestPrintBatchResult:
captured = capsys.readouterr() captured = capsys.readouterr()
assert "test.png" in captured.out assert "test.png" in captured.out
class TestCredentialsMigration:
"""Tests for v3.1.x to v3.2.0 credentials migration."""
def test_old_phrase_key_accepted(self):
"""Old 'phrase' key should be accepted for migration."""
old_format = {
"phrase": "old style phrase",
"pin": "123456"
}
# Should not raise
creds = BatchCredentials.from_dict(old_format)
assert creds.passphrase == "old style phrase"
def test_old_day_phrase_key_accepted(self):
"""Old 'day_phrase' key should be accepted for migration."""
old_format = {
"day_phrase": "old day phrase",
"pin": "123456"
}
creds = BatchCredentials.from_dict(old_format)
assert creds.passphrase == "old day phrase"
def test_new_passphrase_key_preferred(self):
"""New 'passphrase' key should take precedence if both present."""
mixed_format = {
"passphrase": "new style passphrase",
"day_phrase": "old day phrase",
"pin": "123456"
}
creds = BatchCredentials.from_dict(mixed_format)
assert creds.passphrase == "new style passphrase"

View File

@@ -1,7 +1,12 @@
""" """
Stegasoo Tests Stegasoo Tests (v3.2.0)
Tests for key generation, validation, encoding/decoding, and output formats. Tests for key generation, validation, encoding/decoding, and output formats.
Updated for v3.2.0:
- Single passphrase instead of daily phrases
- No date_str parameter
- passphrase_words parameter (default 4)
""" """
import pytest import pytest
@@ -15,13 +20,13 @@ from stegasoo import (
generate_credentials, generate_credentials,
validate_pin, validate_pin,
validate_message, validate_message,
validate_passphrase,
encode, encode,
decode, decode,
decode_text, decode_text,
DAY_NAMES,
__version__, __version__,
) )
from stegasoo.steganography import get_output_format from stegasoo.steganography import get_output_format, HEADER_OVERHEAD
# ============================================================================= # =============================================================================
@@ -38,6 +43,16 @@ def png_image():
return buf.getvalue() return buf.getvalue()
@pytest.fixture
def large_png_image():
"""Create a larger test PNG image for DCT mode."""
img = Image.new('RGB', (400, 400), color='blue')
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
return buf.getvalue()
@pytest.fixture @pytest.fixture
def bmp_image(): def bmp_image():
"""Create a test BMP image.""" """Create a test BMP image."""
@@ -69,100 +84,162 @@ def gif_image():
# ============================================================================= # =============================================================================
# Key Generation Tests # Key Generation Tests (v3.2.0 Updated)
# ============================================================================= # =============================================================================
class TestKeygen: class TestKeygen:
"""Tests for key generation functions."""
def test_generate_pin_default(self): def test_generate_pin_default(self):
"""Default PIN should be 6 digits, no leading zero."""
pin = generate_pin() pin = generate_pin()
assert len(pin) == 6 assert len(pin) == 6
assert pin.isdigit() assert pin.isdigit()
assert pin[0] != '0' assert pin[0] != '0'
def test_generate_pin_lengths(self): def test_generate_pin_lengths(self):
"""PIN generation should work for all valid lengths."""
for length in [6, 7, 8, 9]: for length in [6, 7, 8, 9]:
pin = generate_pin(length) pin = generate_pin(length)
assert len(pin) == length assert len(pin) == length
assert pin.isdigit() assert pin.isdigit()
def test_generate_phrase_default(self): def test_generate_phrase_default(self):
"""Default phrase should have 4 words (v3.2.0 change)."""
phrase = generate_phrase() phrase = generate_phrase()
words = phrase.split() words = phrase.split()
assert len(words) == 3 assert len(words) == 4 # Changed from 3 in v3.1.x
def test_generate_phrase_lengths(self): def test_generate_phrase_custom_length(self):
for length in [3, 4, 5, 6]: """Phrase generation should work for custom lengths."""
for length in [3, 4, 5, 6, 8, 12]:
phrase = generate_phrase(length) phrase = generate_phrase(length)
words = phrase.split() words = phrase.split()
assert len(words) == length assert len(words) == length
def test_generate_credentials_pin_only(self): def test_generate_credentials_pin_only(self):
"""PIN-only credentials should have single passphrase."""
creds = generate_credentials(use_pin=True, use_rsa=False) creds = generate_credentials(use_pin=True, use_rsa=False)
assert creds.pin is not None assert creds.pin is not None
assert creds.rsa_key_pem is None assert creds.rsa_key_pem is None
assert len(creds.phrases) == 7 # v3.2.0: Single passphrase instead of 7 daily phrases
assert creds.passphrase is not None
assert isinstance(creds.passphrase, str)
assert ' ' in creds.passphrase # Should have multiple words
def test_generate_credentials_rsa_only(self): def test_generate_credentials_rsa_only(self):
"""RSA-only credentials should have single passphrase."""
creds = generate_credentials(use_pin=False, use_rsa=True) creds = generate_credentials(use_pin=False, use_rsa=True)
assert creds.pin is None assert creds.pin is None
assert creds.rsa_key_pem is not None assert creds.rsa_key_pem is not None
assert creds.passphrase is not None
def test_generate_credentials_both(self): def test_generate_credentials_both(self):
"""Both PIN and RSA should work together."""
creds = generate_credentials(use_pin=True, use_rsa=True) creds = generate_credentials(use_pin=True, use_rsa=True)
assert creds.pin is not None assert creds.pin is not None
assert creds.rsa_key_pem is not None assert creds.rsa_key_pem is not None
assert creds.passphrase is not None
def test_generate_credentials_neither_fails(self): def test_generate_credentials_neither_fails(self):
"""Test that generating credentials with neither PIN nor RSA fails.""" """Generating with neither PIN nor RSA should fail."""
# Code raises AssertionError from debug.validate before ValueError
with pytest.raises((ValueError, AssertionError)): with pytest.raises((ValueError, AssertionError)):
generate_credentials(use_pin=False, use_rsa=False) generate_credentials(use_pin=False, use_rsa=False)
def test_entropy_calculation(self): def test_generate_credentials_custom_words(self):
creds = generate_credentials(use_pin=True, use_rsa=False) """Custom passphrase_words parameter should work."""
creds = generate_credentials(use_pin=True, passphrase_words=6)
words = creds.passphrase.split()
assert len(words) == 6
def test_generate_credentials_default_words(self):
"""Default should be 4 words (v3.2.0)."""
creds = generate_credentials(use_pin=True)
words = creds.passphrase.split()
assert len(words) == 4
def test_passphrase_entropy_calculation(self):
"""Passphrase entropy should be calculated correctly."""
creds = generate_credentials(use_pin=True, passphrase_words=4)
# 4 words × 11 bits/word = 44 bits
assert creds.passphrase_entropy == 44
def test_total_entropy_calculation(self):
"""Total entropy should sum all components."""
creds = generate_credentials(use_pin=True, use_rsa=False, passphrase_words=4)
# 44 bits (passphrase) + ~20 bits (PIN)
assert creds.total_entropy > 0 assert creds.total_entropy > 0
assert creds.total_entropy >= creds.passphrase_entropy
# ============================================================================= # =============================================================================
# Validation Tests # Validation Tests (v3.2.0 Updated)
# ============================================================================= # =============================================================================
class TestValidation: class TestValidation:
"""Tests for validation functions."""
def test_validate_pin_valid(self): def test_validate_pin_valid(self):
"""Valid PIN should pass validation."""
result = validate_pin("123456") result = validate_pin("123456")
assert result.is_valid assert result.is_valid
def test_validate_pin_empty_ok(self): def test_validate_pin_empty_ok(self):
# Empty PIN is valid (RSA key might be used instead) """Empty PIN should be valid (RSA key might be used instead)."""
result = validate_pin("") result = validate_pin("")
assert result.is_valid assert result.is_valid
def test_validate_pin_too_short(self): def test_validate_pin_too_short(self):
"""PIN shorter than 6 digits should fail."""
result = validate_pin("12345") result = validate_pin("12345")
assert not result.is_valid assert not result.is_valid
def test_validate_pin_too_long(self): def test_validate_pin_too_long(self):
"""PIN longer than 9 digits should fail."""
result = validate_pin("1234567890") result = validate_pin("1234567890")
assert not result.is_valid assert not result.is_valid
def test_validate_pin_leading_zero(self): def test_validate_pin_leading_zero(self):
"""PIN with leading zero should fail."""
result = validate_pin("012345") result = validate_pin("012345")
assert not result.is_valid assert not result.is_valid
def test_validate_pin_non_digits(self): def test_validate_pin_non_digits(self):
"""PIN with non-digit characters should fail."""
result = validate_pin("12345a") result = validate_pin("12345a")
assert not result.is_valid assert not result.is_valid
def test_validate_message_valid(self): def test_validate_message_valid(self):
"""Valid message should pass validation."""
result = validate_message("Hello, World!") result = validate_message("Hello, World!")
assert result.is_valid assert result.is_valid
def test_validate_message_empty(self): def test_validate_message_empty(self):
"""Empty message should fail validation."""
result = validate_message("") result = validate_message("")
assert not result.is_valid assert not result.is_valid
# Note: validate_message doesn't have a max length check by default def test_validate_passphrase_valid(self):
# This test is removed as it doesn't match the actual validation behavior """Valid passphrase should pass validation."""
result = validate_passphrase("word1 word2 word3 word4")
assert result.is_valid
def test_validate_passphrase_empty(self):
"""Empty passphrase should fail validation."""
result = validate_passphrase("")
assert not result.is_valid
def test_validate_passphrase_short_warning(self):
"""Short passphrase should have warning but still be valid."""
result = validate_passphrase("word1 word2 word3") # Only 3 words
assert result.is_valid
assert result.warning is not None # Should warn about short passphrase
def test_validate_passphrase_recommended_no_warning(self):
"""Recommended length passphrase should have no warning."""
result = validate_passphrase("word1 word2 word3 word4") # 4 words
assert result.is_valid
# May or may not have warning depending on implementation
# ============================================================================= # =============================================================================
@@ -170,53 +247,76 @@ class TestValidation:
# ============================================================================= # =============================================================================
class TestOutputFormat: class TestOutputFormat:
"""Tests for output format handling."""
def test_png_stays_png(self): def test_png_stays_png(self):
"""PNG input should produce PNG output."""
fmt, ext = get_output_format('PNG') fmt, ext = get_output_format('PNG')
assert fmt == 'PNG' assert fmt == 'PNG'
assert ext == 'png' assert ext == 'png'
def test_bmp_stays_bmp(self): def test_bmp_stays_bmp(self):
"""BMP input should produce BMP output."""
fmt, ext = get_output_format('BMP') fmt, ext = get_output_format('BMP')
assert fmt == 'BMP' assert fmt == 'BMP'
assert ext == 'bmp' assert ext == 'bmp'
def test_jpeg_becomes_png(self): def test_jpeg_becomes_png(self):
"""JPEG input should produce PNG output (lossless)."""
fmt, ext = get_output_format('JPEG') fmt, ext = get_output_format('JPEG')
assert fmt == 'PNG' assert fmt == 'PNG'
assert ext == 'png' assert ext == 'png'
def test_gif_becomes_png(self): def test_gif_becomes_png(self):
"""GIF input should produce PNG output."""
fmt, ext = get_output_format('GIF') fmt, ext = get_output_format('GIF')
assert fmt == 'PNG' assert fmt == 'PNG'
assert ext == 'png' assert ext == 'png'
def test_none_becomes_png(self): def test_none_becomes_png(self):
"""None format should default to PNG."""
fmt, ext = get_output_format(None) fmt, ext = get_output_format(None)
assert fmt == 'PNG' assert fmt == 'PNG'
assert ext == 'png' assert ext == 'png'
def test_unknown_becomes_png(self): def test_unknown_becomes_png(self):
"""Unknown format should default to PNG."""
fmt, ext = get_output_format('UNKNOWN') fmt, ext = get_output_format('UNKNOWN')
assert fmt == 'PNG' assert fmt == 'PNG'
assert ext == 'png' assert ext == 'png'
# ============================================================================= # =============================================================================
# Encode/Decode Tests # Header Overhead Test (v3.2.0)
# =============================================================================
class TestConstants:
"""Tests for constants and configuration."""
def test_header_overhead_value(self):
"""Header overhead should be 65 bytes (v3.2.0 fix)."""
assert HEADER_OVERHEAD == 65
# =============================================================================
# Encode/Decode Tests (v3.2.0 Updated)
# ============================================================================= # =============================================================================
class TestEncodeDecode: class TestEncodeDecode:
"""Tests for encoding and decoding functions."""
def test_encode_decode_roundtrip(self, png_image): def test_encode_decode_roundtrip(self, png_image):
"""Test full encode/decode cycle.""" """Full encode/decode cycle should work."""
message = "Secret message!" message = "Secret message!"
phrase = "apple forest thunder" passphrase = "apple forest thunder mountain" # 4 words
pin = "123456" pin = "123456"
# v3.2.0: Use passphrase parameter, no date_str
result = encode( result = encode(
message=message, message=message,
reference_photo=png_image, reference_photo=png_image,
carrier_image=png_image, carrier_image=png_image,
day_phrase=phrase, passphrase=passphrase,
pin=pin pin=pin
) )
@@ -224,27 +324,27 @@ class TestEncodeDecode:
assert len(result.stego_image) > 0 assert len(result.stego_image) > 0
assert result.filename.endswith('.png') assert result.filename.endswith('.png')
# v3.2.0: Use passphrase parameter, no date_str
decoded = decode( decoded = decode(
stego_image=result.stego_image, stego_image=result.stego_image,
reference_photo=png_image, reference_photo=png_image,
day_phrase=phrase, passphrase=passphrase,
pin=pin pin=pin
) )
# decode() returns DecodeResult, not string
assert decoded.message == message assert decoded.message == message
def test_decode_text_roundtrip(self, png_image): def test_decode_text_roundtrip(self, png_image):
"""Test decode_text convenience function.""" """decode_text convenience function should work."""
message = "Secret message!" message = "Secret message!"
phrase = "apple forest thunder" passphrase = "apple forest thunder mountain"
pin = "123456" pin = "123456"
result = encode( result = encode(
message=message, message=message,
reference_photo=png_image, reference_photo=png_image,
carrier_image=png_image, carrier_image=png_image,
day_phrase=phrase, passphrase=passphrase,
pin=pin pin=pin
) )
@@ -252,56 +352,56 @@ class TestEncodeDecode:
decoded_text = decode_text( decoded_text = decode_text(
stego_image=result.stego_image, stego_image=result.stego_image,
reference_photo=png_image, reference_photo=png_image,
day_phrase=phrase, passphrase=passphrase,
pin=pin pin=pin
) )
assert decoded_text == message assert decoded_text == message
def test_png_carrier_produces_png(self, png_image): def test_png_carrier_produces_png(self, png_image):
"""Test that PNG carrier produces PNG output.""" """PNG carrier should produce PNG output."""
result = encode( result = encode(
message="Test", message="Test",
reference_photo=png_image, reference_photo=png_image,
carrier_image=png_image, carrier_image=png_image,
day_phrase="test phrase", passphrase="test phrase here now",
pin="123456" pin="123456"
) )
assert result.filename.endswith('.png') assert result.filename.endswith('.png')
def test_bmp_carrier_produces_bmp(self, bmp_image, png_image): def test_bmp_carrier_produces_bmp(self, bmp_image, png_image):
"""Test that BMP carrier produces BMP output.""" """BMP carrier should produce BMP output."""
result = encode( result = encode(
message="Test", message="Test",
reference_photo=png_image, reference_photo=png_image,
carrier_image=bmp_image, carrier_image=bmp_image,
day_phrase="test phrase", passphrase="test phrase here now",
pin="123456" pin="123456"
) )
assert result.filename.endswith('.bmp') assert result.filename.endswith('.bmp')
def test_jpeg_carrier_produces_png(self, jpeg_image, png_image): def test_jpeg_carrier_produces_png(self, jpeg_image, png_image):
"""Test that JPEG carrier produces PNG output (lossless).""" """JPEG carrier should produce PNG output (lossless)."""
result = encode( result = encode(
message="Test", message="Test",
reference_photo=png_image, reference_photo=png_image,
carrier_image=jpeg_image, carrier_image=jpeg_image,
day_phrase="test phrase", passphrase="test phrase here now",
pin="123456" pin="123456"
) )
assert result.filename.endswith('.png') assert result.filename.endswith('.png')
def test_bmp_roundtrip(self, bmp_image, png_image): def test_bmp_roundtrip(self, bmp_image, png_image):
"""Test full encode/decode cycle with BMP.""" """Full encode/decode cycle with BMP should work."""
message = "BMP test message!" message = "BMP test message!"
phrase = "test phrase words" passphrase = "test phrase words here"
pin = "123456" pin = "123456"
result = encode( result = encode(
message=message, message=message,
reference_photo=png_image, reference_photo=png_image,
carrier_image=bmp_image, carrier_image=bmp_image,
day_phrase=phrase, passphrase=passphrase,
pin=pin pin=pin
) )
assert result.filename.endswith('.bmp') assert result.filename.endswith('.bmp')
@@ -309,65 +409,202 @@ class TestEncodeDecode:
decoded = decode( decoded = decode(
stego_image=result.stego_image, stego_image=result.stego_image,
reference_photo=png_image, reference_photo=png_image,
day_phrase=phrase, passphrase=passphrase,
pin=pin pin=pin
) )
# decode() returns DecodeResult, not string
assert decoded.message == message assert decoded.message == message
def test_wrong_pin_fails(self, png_image): def test_wrong_pin_fails(self, png_image):
"""Test that wrong PIN fails to decode.""" """Wrong PIN should fail to decode."""
result = encode( result = encode(
message="Secret", message="Secret",
reference_photo=png_image, reference_photo=png_image,
carrier_image=png_image, carrier_image=png_image,
day_phrase="test phrase here", passphrase="test phrase here now",
pin="123456" pin="123456"
) )
# Wrong PIN means wrong pixel key, so extraction fails before decryption
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)): with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
decode( decode(
stego_image=result.stego_image, stego_image=result.stego_image,
reference_photo=png_image, reference_photo=png_image,
day_phrase="test phrase here", passphrase="test phrase here now",
pin="654321" # Wrong PIN pin="654321" # Wrong PIN
) )
def test_wrong_phrase_fails(self, png_image): def test_wrong_passphrase_fails(self, png_image):
"""Test that wrong phrase fails to decode.""" """Wrong passphrase should fail to decode."""
result = encode( result = encode(
message="Secret", message="Secret",
reference_photo=png_image, reference_photo=png_image,
carrier_image=png_image, carrier_image=png_image,
day_phrase="correct phrase here", passphrase="correct phrase here now",
pin="123456" pin="123456"
) )
# Wrong phrase means wrong pixel key, so extraction fails before decryption
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)): with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
decode( decode(
stego_image=result.stego_image, stego_image=result.stego_image,
reference_photo=png_image, reference_photo=png_image,
day_phrase="wrong phrase here", passphrase="wrong phrase here now", # Wrong passphrase
pin="123456" pin="123456"
) )
def test_unicode_message(self, png_image):
"""Unicode messages should encode/decode correctly."""
message = "Hello, 世界! 🎉 Émojis and ümlauts"
passphrase = "unicode test phrase here"
pin = "123456"
result = encode(
message=message,
reference_photo=png_image,
carrier_image=png_image,
passphrase=passphrase,
pin=pin
)
decoded = decode(
stego_image=result.stego_image,
reference_photo=png_image,
passphrase=passphrase,
pin=pin
)
assert decoded.message == message
def test_filename_has_no_date(self, png_image):
"""v3.2.0: Output filename should not have date suffix."""
result = encode(
message="Test",
reference_photo=png_image,
carrier_image=png_image,
passphrase="test phrase here now",
pin="123456"
)
# Filename should be like "a1b2c3d4.png", not "a1b2c3d4_20251227.png"
# Check that there's no underscore followed by 8 digits
import re
assert not re.search(r'_\d{8}\.', result.filename)
# =============================================================================
# DCT Mode Tests (v3.2.0)
# =============================================================================
class TestDCTMode:
"""Tests for DCT steganography mode."""
@pytest.fixture
def skip_if_no_dct(self):
"""Skip test if DCT support not available."""
if not stegasoo.has_dct_support():
pytest.skip("DCT support not available (scipy not installed)")
def test_dct_encode_decode_roundtrip(self, large_png_image, skip_if_no_dct):
"""DCT mode encode/decode should work."""
message = "DCT test"
passphrase = "dct test phrase here"
pin = "123456"
result = encode(
message=message,
reference_photo=large_png_image,
carrier_image=large_png_image,
passphrase=passphrase,
pin=pin,
embed_mode='dct'
)
assert result.stego_image is not None
decoded = decode(
stego_image=result.stego_image,
reference_photo=large_png_image,
passphrase=passphrase,
pin=pin
)
assert decoded.message == message
def test_dct_auto_detection(self, large_png_image, skip_if_no_dct):
"""Auto mode should detect DCT encoding."""
message = "Auto detect DCT"
passphrase = "auto detect test here"
pin = "123456"
result = encode(
message=message,
reference_photo=large_png_image,
carrier_image=large_png_image,
passphrase=passphrase,
pin=pin,
embed_mode='dct'
)
# Decode with auto mode (default)
decoded = decode(
stego_image=result.stego_image,
reference_photo=large_png_image,
passphrase=passphrase,
pin=pin,
embed_mode='auto'
)
assert decoded.message == message
# ============================================================================= # =============================================================================
# Version Tests # Version Tests
# ============================================================================= # =============================================================================
class TestVersion: class TestVersion:
"""Tests for version information."""
def test_version_exists(self): def test_version_exists(self):
"""Version string should exist and be valid."""
assert hasattr(stegasoo, '__version__') assert hasattr(stegasoo, '__version__')
# Version should be a valid semver string
parts = stegasoo.__version__.split('.') parts = stegasoo.__version__.split('.')
assert len(parts) >= 2 assert len(parts) >= 2
assert all(p.isdigit() for p in parts[:2]) assert all(p.isdigit() for p in parts[:2])
def test_day_names(self): def test_version_is_3_2_0(self):
assert len(DAY_NAMES) == 7 """Version should be 3.2.0 or higher."""
assert 'Monday' in DAY_NAMES parts = stegasoo.__version__.split('.')
assert 'Sunday' in DAY_NAMES major = int(parts[0])
minor = int(parts[1])
assert major >= 3
if major == 3:
assert minor >= 2
# =============================================================================
# Backward Compatibility Tests
# =============================================================================
class TestBackwardCompatibility:
"""Tests for backward compatibility handling."""
def test_old_day_phrase_parameter_raises(self, png_image):
"""Using old day_phrase parameter should raise TypeError."""
with pytest.raises(TypeError):
encode(
message="Test",
reference_photo=png_image,
carrier_image=png_image,
day_phrase="old style phrase", # Old parameter name
pin="123456"
)
def test_old_date_str_parameter_raises(self, png_image):
"""Using old date_str parameter should raise TypeError."""
with pytest.raises(TypeError):
encode(
message="Test",
reference_photo=png_image,
carrier_image=png_image,
passphrase="test phrase here now",
pin="123456",
date_str="2025-01-01" # Removed parameter
)