Release checklist and updated test scripts.
This commit is contained in:
849
frontends/API.md
849
frontends/API.md
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
525
tests/RELEASE_CHECKLIST_V3.2.0.md
Normal file
525
tests/RELEASE_CHECKLIST_V3.2.0.md
Normal 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:** _________________
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user