Clean up debug scripts and update RPi docs

- Delete debug/diagnostic scripts (minimal_flask_crash.py, check_scipy.py)
- Delete old version summary markdown files
- Update RPi docs with default creds (admin/stegasoo)
- Add --soft flag documentation for sanitize script
- Switch compression from xz to zstd
- Add RPi image artifacts to .gitignore
- Improve sanitize-for-image.sh with validation and soft reset mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-03 19:55:37 -05:00
parent 0d7b5a14cb
commit e129c38fd8
10 changed files with 269 additions and 2383 deletions

7
.gitignore vendored
View File

@@ -70,3 +70,10 @@ scripts/
# Web UI auth database and SSL certs # Web UI auth database and SSL certs
frontends/web/instance/ frontends/web/instance/
frontends/web/certs/ frontends/web/certs/
rpi/inject-wifi.sh
# RPi image build artifacts
*.img
*.img.xz
*.img.zst
pishrink.sh

View File

@@ -1,170 +0,0 @@
#!/usr/bin/env python3
"""
Diagnostic script to check for scipy/numpy issues.
Run this BEFORE starting the web app.
Usage:
python check_scipy.py
"""
import sys
print(f"Python version: {sys.version}")
print()
# Check numpy
try:
import numpy as np
print(f"NumPy version: {np.__version__}")
print(f"NumPy config:")
np.show_config()
except ImportError as e:
print(f"NumPy not installed: {e}")
except Exception as e:
print(f"NumPy error: {e}")
print()
print("-" * 50)
print()
# Check scipy
try:
import scipy
print(f"SciPy version: {scipy.__version__}")
except ImportError as e:
print(f"SciPy not installed: {e}")
print()
# Check PIL
try:
from PIL import Image
print(f"Pillow version: {Image.__version__}")
except ImportError as e:
print(f"Pillow not installed: {e}")
print()
print("-" * 50)
print()
# Test scipy DCT directly
print("Testing scipy DCT...")
try:
from scipy.fftpack import dct, idct
import numpy as np
# Create test array
test = np.random.rand(8, 8).astype(np.float64)
print(f"Input array shape: {test.shape}, dtype: {test.dtype}")
# Test 1D DCT
row = test[0, :]
result = dct(row, norm='ortho')
print(f"1D DCT result shape: {result.shape}, dtype: {result.dtype}")
# Test 2D DCT (the potentially problematic operation)
result2d = dct(dct(test.T, norm='ortho').T, norm='ortho')
print(f"2D DCT result shape: {result2d.shape}, dtype: {result2d.dtype}")
# Test inverse
recovered = idct(idct(result2d.T, norm='ortho').T, norm='ortho')
error = np.max(np.abs(test - recovered))
print(f"Round-trip error: {error}")
if error < 1e-10:
print("✓ scipy DCT working correctly")
else:
print("⚠ scipy DCT has precision issues")
except Exception as e:
print(f"✗ scipy DCT failed: {e}")
import traceback
traceback.print_exc()
print()
print("-" * 50)
print()
# Test with larger array (more like real image processing)
print("Testing with larger arrays (512x512)...")
try:
from scipy.fftpack import dct, idct
import numpy as np
import gc
# Simulate processing many 8x8 blocks
large_array = np.random.rand(512, 512).astype(np.float64)
print(f"Large array shape: {large_array.shape}, size: {large_array.nbytes} bytes")
count = 0
for y in range(0, 512, 8):
for x in range(0, 512, 8):
block = large_array[y:y+8, x:x+8].copy()
dct_block = dct(dct(block.T, norm='ortho').T, norm='ortho')
recovered = idct(idct(dct_block.T, norm='ortho').T, norm='ortho')
large_array[y:y+8, x:x+8] = recovered
count += 1
print(f"Processed {count} blocks successfully")
del large_array
gc.collect()
print("✓ Large array processing completed")
except Exception as e:
print(f"✗ Large array processing failed: {e}")
import traceback
traceback.print_exc()
print()
print("-" * 50)
print()
# Test PIL with large image
print("Testing PIL with large image...")
try:
from PIL import Image
import io
# Create a large test image
img = Image.new('RGB', (4000, 3000), color=(128, 128, 128))
# Save to bytes
buffer = io.BytesIO()
img.save(buffer, format='PNG')
img_bytes = buffer.getvalue()
print(f"Test image size: {len(img_bytes)} bytes")
# Re-open and process
buffer2 = io.BytesIO(img_bytes)
img2 = Image.open(buffer2)
print(f"Re-opened image: {img2.size}, mode: {img2.mode}")
# Convert to numpy array
import numpy as np
arr = np.array(img2)
print(f"NumPy array: {arr.shape}, dtype: {arr.dtype}")
# Clean up
img.close()
img2.close()
buffer.close()
buffer2.close()
del arr
gc.collect()
print("✓ PIL large image test completed")
except Exception as e:
print(f"✗ PIL test failed: {e}")
import traceback
traceback.print_exc()
print()
print("=" * 50)
print("Diagnostics complete")
print()
print("If no errors above but web app still crashes, try:")
print("1. pip install --upgrade scipy numpy pillow")
print("2. pip install scipy==1.11.4 numpy==1.26.4 # Known stable versions")
print("3. Check if using conda vs pip (mixing can cause issues)")

View File

@@ -1,500 +0,0 @@
# API Update Summary for v3.2.0
## Overview
The FastAPI REST API has been updated to align with Stegasoo v3.2.0's breaking changes:
1. **Removed date dependency** - No `date_str` field in requests
2. **Renamed day_phrase → passphrase** - Updated all request/response models
3. **Updated generation** - Now generates single passphrase instead of daily phrases
## Breaking Changes
### Request Model Changes
#### 1. EncodeRequest & EncodeFileRequest
**Before (v3.1.0):**
```python
class EncodeRequest(BaseModel):
message: str
reference_photo_base64: str
carrier_image_base64: str
day_phrase: str # ← Changed to passphrase
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
date_str: Optional[str] = None # ← REMOVED
embed_mode: EmbedModeType = "lsb"
```
**After (v3.2.0):**
```python
class EncodeRequest(BaseModel):
message: str
reference_photo_base64: str
carrier_image_base64: str
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
# date_str removed in v3.2.0
embed_mode: EmbedModeType = "lsb"
dct_output_format: DctOutputFormatType = "png"
dct_color_mode: DctColorModeType = "grayscale"
```
#### 2. DecodeRequest
**Before (v3.1.0):**
```python
class DecodeRequest(BaseModel):
stego_image_base64: str
reference_photo_base64: str
day_phrase: str # ← Changed to passphrase
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
embed_mode: ExtractModeType = "auto"
```
**After (v3.2.0):**
```python
class DecodeRequest(BaseModel):
stego_image_base64: str
reference_photo_base64: str
passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)")
pin: str = ""
rsa_key_base64: Optional[str] = None
rsa_password: Optional[str] = None
embed_mode: ExtractModeType = "auto"
```
#### 3. GenerateRequest
**Before (v3.1.0):**
```python
class GenerateRequest(BaseModel):
use_pin: bool = True
use_rsa: bool = False
pin_length: int = Field(default=6, ge=MIN_PIN_LENGTH, le=MAX_PIN_LENGTH)
rsa_bits: int = Field(default=2048)
words_per_phrase: int = Field(default=3, ge=MIN_PHRASE_WORDS, le=MAX_PHRASE_WORDS)
```
**After (v3.2.0):**
```python
class GenerateRequest(BaseModel):
use_pin: bool = True
use_rsa: bool = False
pin_length: int = Field(default=6, ge=MIN_PIN_LENGTH, le=MAX_PIN_LENGTH)
rsa_bits: int = Field(default=2048)
words_per_passphrase: int = Field(
default=DEFAULT_PASSPHRASE_WORDS, # = 4, was 3
ge=MIN_PASSPHRASE_WORDS,
le=MAX_PASSPHRASE_WORDS,
description="Words per passphrase (v3.2.0: default increased to 4)"
)
```
### Response Model Changes
#### 1. GenerateResponse
**Before (v3.1.0):**
```python
class GenerateResponse(BaseModel):
phrases: dict[str, str] # Monday -> phrase, Tuesday -> phrase, etc.
pin: Optional[str] = None
rsa_key_pem: Optional[str] = None
entropy: dict[str, int]
```
**After (v3.2.0):**
```python
class GenerateResponse(BaseModel):
passphrase: str = Field(description="Single passphrase (v3.2.0: no daily rotation)")
pin: Optional[str] = None
rsa_key_pem: Optional[str] = None
entropy: dict[str, int]
# Legacy field for compatibility
phrases: Optional[dict[str, str]] = Field(
default=None,
description="Deprecated: Use 'passphrase' instead"
)
```
#### 2. EncodeResponse
**Before (v3.1.0):**
```python
class EncodeResponse(BaseModel):
stego_image_base64: str
filename: str
capacity_used_percent: float
date_used: str
day_of_week: str
embed_mode: str
output_format: str = "png"
color_mode: str = "color"
```
**After (v3.2.0):**
```python
class EncodeResponse(BaseModel):
stego_image_base64: str
filename: str
capacity_used_percent: float
embed_mode: str
output_format: str = "png"
color_mode: str = "color"
# Legacy fields (no longer used in crypto)
date_used: Optional[str] = Field(
default=None,
description="Deprecated: Date no longer used in v3.2.0"
)
day_of_week: Optional[str] = Field(
default=None,
description="Deprecated: Date no longer used in v3.2.0"
)
```
### Endpoint Changes
#### 1. POST /encode
**Before (v3.1.0):**
```json
{
"message": "Secret message",
"reference_photo_base64": "...",
"carrier_image_base64": "...",
"day_phrase": "apple forest thunder",
"date_str": "2025-01-15",
"pin": "123456",
"embed_mode": "lsb"
}
```
**After (v3.2.0):**
```json
{
"message": "Secret message",
"reference_photo_base64": "...",
"carrier_image_base64": "...",
"passphrase": "apple forest thunder mountain",
"pin": "123456",
"embed_mode": "lsb"
}
```
#### 2. POST /decode
**Before (v3.1.0):**
```json
{
"stego_image_base64": "...",
"reference_photo_base64": "...",
"day_phrase": "apple forest thunder",
"pin": "123456",
"embed_mode": "auto"
}
```
**After (v3.2.0):**
```json
{
"stego_image_base64": "...",
"reference_photo_base64": "...",
"passphrase": "apple forest thunder mountain",
"pin": "123456",
"embed_mode": "auto"
}
```
#### 3. POST /generate
**Response Before (v3.1.0):**
```json
{
"phrases": {
"Monday": "apple forest thunder",
"Tuesday": "banana river lightning",
...
},
"pin": "123456",
"rsa_key_pem": null,
"entropy": {
"phrase": 33,
"pin": 20,
"rsa": 0,
"total": 53
}
}
```
**Response After (v3.2.0):**
```json
{
"passphrase": "apple forest thunder mountain",
"pin": "123456",
"rsa_key_pem": null,
"entropy": {
"passphrase": 44,
"pin": 20,
"rsa": 0,
"total": 64
},
"phrases": null
}
```
#### 4. POST /encode/multipart
**Form Fields Before (v3.1.0):**
- `day_phrase` (required)
- `date_str` (optional)
- `reference_photo` (file)
- `carrier` (file)
- ...
**Form Fields After (v3.2.0):**
- `passphrase` (required) ← renamed from day_phrase
- `reference_photo` (file)
- `carrier` (file)
- ... (date_str removed)
**Response Headers Before (v3.1.0):**
```
X-Stegasoo-Date: 2025-01-15
X-Stegasoo-Day: Wednesday
X-Stegasoo-Capacity-Percent: 25.5
X-Stegasoo-Embed-Mode: lsb
```
**Response Headers After (v3.2.0):**
```
X-Stegasoo-Capacity-Percent: 25.5
X-Stegasoo-Embed-Mode: lsb
X-Stegasoo-Output-Format: png
X-Stegasoo-Color-Mode: color
X-Stegasoo-Version: 3.2.0
```
### New Status Endpoint Information
#### GET /
**Added to response:**
```json
{
"version": "3.2.0",
...
"breaking_changes": {
"date_removed": "No date_str parameter needed - encode/decode anytime",
"passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)",
"format_version": 4,
"backward_compatible": false
}
}
```
## Migration Guide for API Clients
### 1. Update Request Bodies
**Find and replace in client code:**
```javascript
// Before
{
day_phrase: "apple forest thunder",
date_str: "2025-01-15"
}
// After
{
passphrase: "apple forest thunder mountain"
}
```
### 2. Update Response Handling
**Before:**
```javascript
const response = await fetch('/encode', {
method: 'POST',
body: JSON.stringify({
message: "secret",
day_phrase: "words",
date_str: "2025-01-15",
...
})
});
const data = await response.json();
console.log(data.date_used); // "2025-01-15"
console.log(data.day_of_week); // "Wednesday"
```
**After:**
```javascript
const response = await fetch('/encode', {
method: 'POST',
body: JSON.stringify({
message: "secret",
passphrase: "longer words here now",
// date_str removed
...
})
});
const data = await response.json();
// date_used and day_of_week are null in v3.2.0
```
### 3. Update Generate Endpoint Usage
**Before:**
```javascript
const creds = await fetch('/generate', {
method: 'POST',
body: JSON.stringify({ use_pin: true })
}).then(r => r.json());
// Use Monday's phrase
const mondayPhrase = creds.phrases['Monday'];
```
**After:**
```javascript
const creds = await fetch('/generate', {
method: 'POST',
body: JSON.stringify({ use_pin: true })
}).then(r => r.json());
// Use single passphrase
const passphrase = creds.passphrase;
```
### 4. Update Multipart Requests
**Before (JavaScript fetch):**
```javascript
const formData = new FormData();
formData.append('day_phrase', 'apple forest thunder');
formData.append('date_str', '2025-01-15');
formData.append('reference_photo', refPhotoFile);
formData.append('carrier', carrierFile);
formData.append('message', 'secret');
formData.append('pin', '123456');
const response = await fetch('/encode/multipart', {
method: 'POST',
body: formData
});
```
**After (JavaScript fetch):**
```javascript
const formData = new FormData();
formData.append('passphrase', 'apple forest thunder mountain');
// date_str removed
formData.append('reference_photo', refPhotoFile);
formData.append('carrier', carrierFile);
formData.append('message', 'secret');
formData.append('pin', '123456');
const response = await fetch('/encode/multipart', {
method: 'POST',
body: formData
});
```
## Testing Checklist
### Endpoints to Test
- [ ] GET / - Returns v3.2.0 with breaking_changes info
- [ ] GET /modes - Returns mode information
- [ ] POST /generate - Returns single passphrase
- [ ] POST /encode - Works without date_str
- [ ] POST /encode/file - Works without date_str
- [ ] POST /decode - Works without date_str
- [ ] POST /encode/multipart - Accepts passphrase instead of day_phrase
- [ ] POST /decode/multipart - Accepts passphrase instead of day_phrase
- [ ] POST /compare - Still works
- [ ] POST /will-fit - Still works
- [ ] POST /image/info - Still works
- [ ] POST /extract-key-from-qr - Still works
### Validation Tests
- [ ] Reject requests with `day_phrase` field (should get validation error)
- [ ] Reject requests with `date_str` field (should be ignored or error)
- [ ] Accept requests with `passphrase` field
- [ ] Generate response includes `passphrase` field
- [ ] Generate response has `phrases` as null
- [ ] Encode response has `date_used` and `day_of_week` as null
- [ ] Multipart encode works with new field names
- [ ] Response headers updated correctly
## OpenAPI/Swagger Documentation
The FastAPI auto-generated documentation (/docs and /redoc) will automatically reflect the changes:
1. **Models updated** - Request/response schemas show new field names
2. **Descriptions updated** - Field descriptions mention v3.2.0 changes
3. **Examples updated** - Interactive API explorer uses new field names
Users can browse to `/docs` to see the updated API specification.
## Backward Compatibility
**Breaking Change:** API v3.2.0 is NOT backward compatible with v3.1.0
Clients using the old API will encounter:
1. **Validation errors** - Missing required `passphrase` field
2. **Unexpected responses** - `phrases` field will be null
3. **Changed behavior** - Date fields no longer populated
### Migration Timeline Recommendation
1. **Deploy v3.2.0 API** to staging
2. **Update client applications** to use new field names
3. **Test thoroughly** with staging API
4. **Deploy v3.2.0 API** to production
5. **Notify users** of breaking changes
Alternatively, run v3.1.0 and v3.2.0 APIs side-by-side on different paths:
- `/api/v3.1/` - Old API
- `/api/v3.2/` - New API
## Constants Updates
Used in validation:
```python
from stegasoo.constants import (
MIN_PASSPHRASE_WORDS, # = 3
MAX_PASSPHRASE_WORDS, # = 12
DEFAULT_PASSPHRASE_WORDS, # = 4 (increased from 3)
)
```
## Error Messages
All error messages updated:
- "day_phrase is required" → "passphrase is required"
- References to "phrase" now mean "passphrase"
## Implementation Status
✅ All request models updated
✅ All response models updated
✅ All endpoints updated
✅ Multipart endpoints updated
✅ Status endpoint shows breaking changes
✅ Constants imported correctly
✅ Error handling updated
✅ No references to day_phrase in user-facing text
✅ No date_str parameters accepted
Ready for deployment!

View File

@@ -1,426 +0,0 @@
# Web Frontend Update Summary for v3.2.0
## Overview
The Flask web frontend has been updated to align with Stegasoo v3.2.0's breaking changes:
1. **Removed date dependency** - No date selection or tracking in UI
2. **Renamed day_phrase → passphrase** - Updated all forms and templates
3. **Increased default words** - From 3 to 4 for better security
## Key Changes
### 1. Form Parameter Changes
#### Generate Page
**Before (v3.1.0):**
```python
words_per_phrase = int(request.form.get('words_per_phrase', 3))
# Generated daily phrases for all days of the week
```
**After (v3.2.0):**
```python
words_per_passphrase = int(request.form.get('words_per_passphrase', 4))
# Generates single passphrase
```
**Template variables changed:**
- `phrases``passphrase` (single string instead of dict)
- `words_per_phrase``words_per_passphrase`
- `phrase_entropy``passphrase_entropy`
- Removed `days` variable (no longer needed)
#### Encode Page
**Before (v3.1.0):**
```python
day_phrase = request.form.get('day_phrase', '')
client_date = request.form.get('client_date', '').strip()
day_of_week = get_today_day() # Used in template
encode_result = encode(
...,
day_phrase=day_phrase,
date_str=date_str,
...
)
```
**After (v3.2.0):**
```python
passphrase = request.form.get('passphrase', '')
# No client_date or day_of_week needed
encode_result = encode(
...,
passphrase=passphrase, # Renamed
# date_str removed
...
)
```
#### Decode Page
**Before (v3.1.0):**
```python
day_phrase = request.form.get('day_phrase', '')
stego_date = request.form.get('stego_date', '').strip()
decode_result = decode(
...,
day_phrase=day_phrase,
date_str=stego_date if stego_date else None,
...
)
```
**After (v3.2.0):**
```python
passphrase = request.form.get('passphrase', '')
# No stego_date needed
decode_result = decode(
...,
passphrase=passphrase, # Renamed
# date_str removed
...
)
```
### 2. Template Context Updates
**inject_globals() changes:**
**Added:**
```python
'min_passphrase_words': MIN_PASSPHRASE_WORDS,
'recommended_passphrase_words': RECOMMENDED_PASSPHRASE_WORDS,
'default_passphrase_words': DEFAULT_PASSPHRASE_WORDS,
```
**Used for:**
- Showing passphrase length requirements
- Default values in generate form
- Validation messages
### 3. Validation Updates
**Added passphrase validation:**
```python
from stegasoo import validate_passphrase
# In encode_page()
result = validate_passphrase(passphrase)
if not result.is_valid:
flash(result.error_message, 'error')
return ...
# Show warning if passphrase is short
if result.warning:
flash(result.warning, 'warning')
```
### 4. Error Message Updates
**Before:**
```python
flash('Day phrase is required', 'error')
flash('Decryption failed. Check your phrase, PIN...', 'error')
```
**After:**
```python
flash('Passphrase is required', 'error')
flash('Decryption failed. Check your passphrase, PIN...', 'error')
```
## Template Changes Needed
These Flask routes will need corresponding template updates:
### generate.html
**Changes needed:**
```html
<!-- Before -->
<label for="words_per_phrase">Words per phrase</label>
<input type="number" name="words_per_phrase" value="3">
{% if generated %}
<h3>Daily Phrases</h3>
{% for day in days %}
<tr>
<td>{{ day }}</td>
<td>{{ phrases[day] }}</td>
</tr>
{% endfor %}
{% endif %}
<!-- After -->
<label for="words_per_passphrase">Words per passphrase</label>
<input type="number" name="words_per_passphrase" value="{{ default_passphrase_words }}">
{% if generated %}
<h3>Passphrase</h3>
<div class="passphrase-display">
<code>{{ passphrase }}</code>
<p class="help-text">Use this passphrase to encode and decode messages (no date needed!)</p>
</div>
{% endif %}
```
**Entropy display:**
```html
<!-- Before -->
<li>Phrase entropy: {{ phrase_entropy }} bits</li>
<!-- After -->
<li>Passphrase entropy: {{ passphrase_entropy }} bits ({{ words_per_passphrase }} words)</li>
```
### encode.html
**Changes needed:**
```html
<!-- Before -->
<label for="day_phrase">Day Phrase</label>
<input type="text" name="day_phrase" required>
<label for="client_date">Encoding Date (Optional)</label>
<input type="date" name="client_date">
<p class="help-text">Defaults to today: {{ day_of_week }}</p>
<!-- After -->
<label for="passphrase">Passphrase</label>
<input type="text" name="passphrase" required
placeholder="Enter at least {{ recommended_passphrase_words }} words">
<p class="help-text">
v3.2.0: No date needed! Use your passphrase anytime.
</p>
```
### decode.html
**Changes needed:**
```html
<!-- Before -->
<label for="day_phrase">Day Phrase</label>
<input type="text" name="day_phrase" required>
<label for="stego_date">Encoding Date</label>
<input type="date" name="stego_date" id="stego_date">
<p class="help-text">Will be auto-detected from filename if possible</p>
<script>
// Auto-detect date from filename
stegoInput.addEventListener('change', function() {
const filename = this.files[0]?.name || '';
const dateMatch = filename.match(/_(\d{4})(\d{2})(\d{2})/);
if (dateMatch) {
document.getElementById('stego_date').value =
`${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
}
});
</script>
<!-- After -->
<label for="passphrase">Passphrase</label>
<input type="text" name="passphrase" required
placeholder="Enter your passphrase">
<p class="help-text">
v3.2.0: No date needed to decode!
</p>
<!-- Remove date detection script -->
```
### index.html
**Changes needed:**
```html
<!-- Before -->
<p>Generate daily passphrases and security credentials</p>
<p>Hide messages using day-specific phrases</p>
<!-- After -->
<p>Generate passphrases and security credentials</p>
<p>v3.2.0: Simplified - no more daily rotation!</p>
```
### about.html
**Add v3.2.0 section:**
```html
<h2>Version 3.2.0 Changes</h2>
<ul>
<li><strong>No date dependency</strong> - Encode and decode anytime without tracking dates</li>
<li><strong>Single passphrase</strong> - No more daily rotation, just remember one strong passphrase</li>
<li><strong>Better security</strong> - Default passphrase length increased to 4 words</li>
<li><strong>Asynchronous ready</strong> - Perfect for dead drops and delayed delivery</li>
</ul>
```
## JavaScript Changes Needed
### Remove date-related code:
```javascript
// REMOVE THIS (date detection from filename)
function detectDateFromFilename(filename) {
const match = filename.match(/_(\d{4})(\d{2})(\d{2})/);
if (match) {
return `${match[1]}-${match[2]}-${match[3]}`;
}
return null;
}
// REMOVE THIS (day-of-week display)
function updateDayOfWeek() {
const dateInput = document.getElementById('client_date');
const dayDisplay = document.getElementById('day_display');
// ...
}
```
### Update validation:
```javascript
// Before
const dayPhrase = document.getElementById('day_phrase').value;
if (!dayPhrase || dayPhrase.trim().length === 0) {
alert('Day phrase is required');
return false;
}
// After
const passphrase = document.getElementById('passphrase').value;
if (!passphrase || passphrase.trim().length === 0) {
alert('Passphrase is required');
return false;
}
// Add word count validation
const words = passphrase.trim().split(/\s+/);
if (words.length < {{ min_passphrase_words }}) {
alert(`Passphrase should have at least {{ recommended_passphrase_words }} words`);
return false;
}
```
## CSS Updates
Add styling for passphrase warnings:
```css
.passphrase-display {
background: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
}
.passphrase-display code {
font-size: 1.2em;
color: #2c3e50;
word-break: break-word;
}
.help-text.v3-2-0 {
color: #3498db;
font-weight: bold;
}
.flash.warning {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
color: #856404;
}
```
## Migration Notes for Users
Add to templates:
```html
<div class="alert alert-info">
<h4>⚠️ v3.2.0 Breaking Changes</h4>
<p>If you have messages encoded with v3.1.0:</p>
<ul>
<li>They cannot be decoded with v3.2.0</li>
<li>You need the original v3.1.0 installation to decode them</li>
<li>After decoding, you can re-encode with v3.2.0</li>
</ul>
</div>
```
## Form Field Summary
### Changed Field Names
| Old Name (v3.1.0) | New Name (v3.2.0) | Type |
|-------------------|-------------------|------|
| `day_phrase` | `passphrase` | text input |
| `words_per_phrase` | `words_per_passphrase` | number input |
| `client_date` | (removed) | date input |
| `stego_date` | (removed) | date input |
### New Validation Attributes
```html
<input type="text" name="passphrase"
required
minlength="{{ min_passphrase_words * 4 }}"
placeholder="Enter at least {{ recommended_passphrase_words }} words"
pattern="^\s*\S+(\s+\S+){3,}.*$"
title="Please enter at least 4 words">
```
## Testing Checklist
- [ ] Generate page creates single passphrase
- [ ] Generate page shows correct entropy (4 words = 44 bits)
- [ ] Generate page doesn't show day names
- [ ] Encode page accepts passphrase (not day_phrase)
- [ ] Encode page doesn't have date selection
- [ ] Encode page shows v3.2.0 help text
- [ ] Decode page accepts passphrase
- [ ] Decode page doesn't have date input
- [ ] Decode page doesn't auto-detect date from filename
- [ ] Error messages say "passphrase" not "day phrase"
- [ ] Validation shows warnings for short passphrases
- [ ] QR code functionality still works
- [ ] DCT mode options still work
- [ ] All flash messages updated
## Implementation Status
✅ Flask routes updated
✅ Form parameter names changed
✅ Function calls updated
✅ Validation added for passphrases
✅ Error messages updated
✅ Template context updated
⏳ Templates need updating (generate.html, encode.html, decode.html, index.html, about.html)
⏳ JavaScript needs updating
⏳ CSS styling for v3.2.0 features
## Quick Reference
**To test the Flask app:**
```bash
cd frontends/web
python app.py
# Visit http://localhost:5000
```
**Key user-facing changes:**
1. Generate: Shows one passphrase, not 7 daily phrases
2. Encode: No date selection, just passphrase
3. Decode: No date needed, just passphrase
**Benefits to highlight:**
- ✅ Simpler UI (fewer fields)
- ✅ No date tracking needed
- ✅ Encode today, decode anytime
- ✅ Perfect for asynchronous communications

View File

@@ -1,289 +0,0 @@
#!/usr/bin/env python3
"""
Minimal Flask app to isolate the crash.
Run with: python minimal_flask_crash.py
Then test with:
curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test1
curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test2
curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test3
"""
import io
import gc
import os
import sys
import tempfile
# Minimal imports first
from flask import Flask, request, jsonify
from PIL import Image
import numpy as np
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
# Check for jpegio
try:
import jpegio as jio
HAS_JPEGIO = True
print("jpegio: available")
except ImportError:
HAS_JPEGIO = False
print("jpegio: NOT available")
@app.route('/test1', methods=['POST'])
def test1_pil_only():
"""Test 1: PIL only, no jpegio, no scipy"""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier'}), 400
data = carrier.read()
print(f"[test1] Read {len(data)} bytes")
img = Image.open(io.BytesIO(data))
width, height = img.size
fmt = img.format
img.close()
print(f"[test1] Image: {width}x{height} {fmt}")
gc.collect()
print("[test1] Returning response...")
return jsonify({
'test': 'pil_only',
'width': width,
'height': height,
'format': fmt,
})
@app.route('/test2', methods=['POST'])
def test2_multiple_opens():
"""Test 2: Open image multiple times like compare_modes does"""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier'}), 400
data = carrier.read()
print(f"[test2] Read {len(data)} bytes")
# First open
img1 = Image.open(io.BytesIO(data))
width, height = img1.size
img1.close()
print(f"[test2] Open 1: {width}x{height}")
# Second open
img2 = Image.open(io.BytesIO(data))
pixels = img2.size[0] * img2.size[1]
img2.close()
print(f"[test2] Open 2: {pixels} pixels")
# Third open
img3 = Image.open(io.BytesIO(data))
blocks = (img3.size[0] // 8) * (img3.size[1] // 8)
img3.close()
print(f"[test2] Open 3: {blocks} blocks")
gc.collect()
print("[test2] Returning response...")
return jsonify({
'test': 'multiple_opens',
'width': width,
'height': height,
'pixels': pixels,
'blocks': blocks,
})
@app.route('/test3', methods=['POST'])
def test3_with_jpegio():
"""Test 3: Include jpegio operations"""
if not HAS_JPEGIO:
return jsonify({'error': 'jpegio not available'}), 501
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier'}), 400
data = carrier.read()
print(f"[test3] Read {len(data)} bytes")
# Check if JPEG
img = Image.open(io.BytesIO(data))
is_jpeg = img.format == 'JPEG'
width, height = img.size
img.close()
print(f"[test3] Image: {width}x{height}, JPEG: {is_jpeg}")
if not is_jpeg:
return jsonify({'error': 'Not a JPEG'}), 400
# Write to temp file
fd, temp_path = tempfile.mkstemp(suffix='.jpg')
os.write(fd, data)
os.close(fd)
print(f"[test3] Temp file: {temp_path}")
try:
# Read with jpegio
jpeg = jio.read(temp_path)
print(f"[test3] jpegio.read() OK")
coef = jpeg.coef_arrays[0]
coef_shape = coef.shape
print(f"[test3] Coef shape: {coef_shape}")
# Count positions like the real code does
positions = 0
h, w = coef.shape
for row in range(h):
for col in range(w):
if (row % 8 == 0) and (col % 8 == 0):
continue
if abs(coef[row, col]) >= 2:
positions += 1
print(f"[test3] Usable positions: {positions}")
# Cleanup
del coef
del jpeg
print(f"[test3] Deleted jpegio objects")
finally:
os.unlink(temp_path)
print(f"[test3] Removed temp file")
gc.collect()
print("[test3] Returning response...")
return jsonify({
'test': 'with_jpegio',
'width': width,
'height': height,
'coef_shape': list(coef_shape),
'positions': positions,
})
@app.route('/test4', methods=['POST'])
def test4_numpy_array_from_pil():
"""Test 4: Create numpy array from PIL image (like DCT does)"""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier'}), 400
data = carrier.read()
print(f"[test4] Read {len(data)} bytes")
img = Image.open(io.BytesIO(data))
width, height = img.size
print(f"[test4] Image: {width}x{height}")
# Convert to grayscale and numpy array
gray = img.convert('L')
arr = np.array(gray, dtype=np.float64, copy=True)
print(f"[test4] Array: {arr.shape} {arr.dtype}")
# Close PIL images
gray.close()
img.close()
print(f"[test4] PIL closed")
# Do some numpy operations
mean_val = float(np.mean(arr))
std_val = float(np.std(arr))
print(f"[test4] Stats: mean={mean_val:.2f}, std={std_val:.2f}")
# Clear array
del arr
gc.collect()
print("[test4] Returning response...")
return jsonify({
'test': 'numpy_from_pil',
'width': width,
'height': height,
'mean': mean_val,
'std': std_val,
})
@app.route('/test5', methods=['POST'])
def test5_file_read_keep_reference():
"""Test 5: Keep reference to file data in request scope"""
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'error': 'No carrier'}), 400
# Don't read into local variable - read directly each time
# This mimics potential issues with Flask's file handling
print(f"[test5] File object: {carrier}")
# Read once
carrier.seek(0)
data1 = carrier.read()
print(f"[test5] First read: {len(data1)} bytes")
img = Image.open(io.BytesIO(data1))
width, height = img.size
img.close()
# Try to read again (should be empty or need seek)
data2 = carrier.read()
print(f"[test5] Second read (no seek): {len(data2)} bytes")
carrier.seek(0)
data3 = carrier.read()
print(f"[test5] Third read (after seek): {len(data3)} bytes")
gc.collect()
print("[test5] Returning response...")
return jsonify({
'test': 'file_handling',
'width': width,
'height': height,
'read1': len(data1),
'read2': len(data2),
'read3': len(data3),
})
@app.after_request
def after_request(response):
"""Log after each request"""
print(f"[after_request] Response status: {response.status}")
return response
@app.teardown_request
def teardown_request(exception):
"""Log during teardown"""
if exception:
print(f"[teardown] Exception: {exception}")
else:
print("[teardown] Clean teardown")
gc.collect()
if __name__ == '__main__':
print("\n" + "=" * 60)
print("MINIMAL FLASK CRASH TEST")
print("=" * 60)
print("\nTest endpoints:")
print(" /test1 - PIL only")
print(" /test2 - Multiple PIL opens")
print(" /test3 - With jpegio")
print(" /test4 - NumPy array from PIL")
print(" /test5 - File handling test")
print("\nUsage:")
print(' curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test1')
print("=" * 60 + "\n")
app.run(host='0.0.0.0', port=5001, debug=False, threaded=False)

View File

@@ -5,12 +5,12 @@ Quick reference for building a distributable SD card image.
## Step 1: Flash Fresh Raspbian ## Step 1: Flash Fresh Raspbian
Use rpi-imager with these settings: Use rpi-imager with these settings:
- **OS**: Raspberry Pi OS (64-bit) - **OS**: Raspberry Pi OS Lite (64-bit)
- **Hostname**: `stegasoo` - **Hostname**: `stegasoo`
- **Enable SSH**: Yes (password auth) - **Enable SSH**: Yes (password auth)
- **Username**: `pi` (or any) - **Username**: `admin`
- **Password**: `raspberry` (temporary) - **Password**: `stegasoo`
- **WiFi**: Skip (use ethernet for clean image) - **WiFi**: Configure for your network (sanitize script removes it later)
## Step 2: Boot & SSH In ## Step 2: Boot & SSH In
@@ -43,17 +43,22 @@ curl -k https://localhost:5000
## Step 5: Sanitize for Distribution ## Step 5: Sanitize for Distribution
```bash ```bash
curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash # Full sanitize (for final image - removes WiFi, shuts down)
sudo ~/stegasoo/rpi/sanitize-for-image.sh
# Or soft reset (for testing - keeps WiFi, reboots)
sudo ~/stegasoo/rpi/sanitize-for-image.sh --soft
``` ```
This removes: This removes:
- WiFi credentials - WiFi credentials (unless `--soft`)
- SSH host keys (regenerate on boot)
- SSH authorized keys - SSH authorized keys
- Bash history - Bash history
- Stegasoo auth database - Stegasoo auth database
- Logs and temp files - Logs and temp files
The Pi will shut down when complete. The script validates all cleanup steps before finishing.
## Step 6: Copy the Image ## Step 6: Copy the Image
@@ -75,18 +80,18 @@ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
chmod +x pishrink.sh chmod +x pishrink.sh
sudo ./pishrink.sh stegasoo-rpi-*.img sudo ./pishrink.sh stegasoo-rpi-*.img
# Compress # Compress (zstd is faster than xz with similar ratio)
xz -9 -T0 stegasoo-rpi-*.img zstd -19 -T0 stegasoo-rpi-*.img
``` ```
## Step 8: Distribute ## Step 8: Distribute
Upload `.img.xz` to GitHub Releases. Upload `.img.zst` to GitHub Releases.
Users can flash with: Users can flash with:
```bash ```bash
# Linux # Linux
xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress
# Or use rpi-imager "Use custom" option # Or use rpi-imager "Use custom" option
``` ```
@@ -100,9 +105,9 @@ xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress
curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/setup.sh | bash curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/setup.sh | bash
sudo systemctl start stegasoo sudo systemctl start stegasoo
curl -k https://localhost:5000 curl -k https://localhost:5000
curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash sudo ~/stegasoo/rpi/sanitize-for-image.sh
# On your machine: # On your machine:
sudo dd if=/dev/sdX of=stegasoo-rpi-$(date +%Y%m%d).img bs=4M status=progress sudo dd if=/dev/sdX of=stegasoo-rpi-$(date +%Y%m%d).img bs=4M status=progress
xz -9 -T0 stegasoo-rpi-*.img zstd -19 -T0 stegasoo-rpi-*.img
``` ```

View File

@@ -30,11 +30,21 @@ chmod +x setup.sh
## Requirements ## Requirements
- Raspberry Pi 4 or 5 - Raspberry Pi 4 or 5
- Raspberry Pi OS (64-bit) - Bookworm or later - Raspberry Pi OS Lite (64-bit) - Bookworm or later
- 4GB+ RAM recommended (2GB minimum) - 4GB+ RAM recommended (2GB minimum)
- ~2GB free disk space - ~2GB free disk space
- Internet connection - Internet connection
## Pre-built Image Defaults
If using a pre-built image from GitHub Releases:
- **Default login**: `admin` / `stegasoo`
- **Hostname**: `stegasoo.local`
- **First boot**: A setup wizard runs on first SSH login
> **Security note**: Change the default password after setup with `passwd`
## After Installation ## After Installation
### Start the Service ### Start the Service
@@ -134,17 +144,23 @@ curl -k https://localhost:5000 # Should return HTML
### 4. Sanitize for Distribution ### 4. Sanitize for Distribution
```bash ```bash
# Download and run sanitize script # Full sanitize (removes WiFi, shuts down for imaging)
curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash sudo ~/stegasoo/rpi/sanitize-for-image.sh
# Or soft reset (keeps WiFi for testing, reboots)
sudo ~/stegasoo/rpi/sanitize-for-image.sh --soft
``` ```
This removes: This removes:
- WiFi credentials - WiFi credentials (unless `--soft`)
- SSH host keys (regenerate on boot)
- SSH authorized keys - SSH authorized keys
- Bash history - Bash history
- Stegasoo auth database (users create their own admin) - Stegasoo auth database (users create their own admin)
- Logs and temp files - Logs and temp files
The script validates cleanup and reports any issues.
### 5. Create the Image ### 5. Create the Image
After Pi shuts down, remove SD card and on another Linux machine: After Pi shuts down, remove SD card and on another Linux machine:
@@ -161,17 +177,17 @@ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
chmod +x pishrink.sh chmod +x pishrink.sh
sudo ./pishrink.sh stegasoo-rpi-*.img sudo ./pishrink.sh stegasoo-rpi-*.img
# Compress # Compress (zstd is faster than xz with similar compression)
xz -9 -T0 stegasoo-rpi-*.img zstd -19 -T0 stegasoo-rpi-*.img
``` ```
### 6. Distribute ### 6. Distribute
Upload the `.img.xz` file to GitHub Releases. Upload the `.img.zst` file to GitHub Releases.
Users flash with: Users flash with:
```bash ```bash
xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress
``` ```
Or use rpi-imager's "Use custom" option. Or use rpi-imager's "Use custom" option.

View File

@@ -4,14 +4,17 @@
# Run this BEFORE creating an image with dd # Run this BEFORE creating an image with dd
# #
# This script removes: # This script removes:
# - WiFi credentials # - WiFi credentials (unless --soft)
# - SSH host keys (will regenerate on boot)
# - SSH authorized keys # - SSH authorized keys
# - User-specific data # - User-specific data
# - Bash history # - Bash history
# - Logs # - Logs
# - Stegasoo auth database (users will create their own admin) # - Stegasoo auth database (users will create their own admin)
# #
# Usage: sudo ./sanitize-for-image.sh # Usage:
# sudo ./sanitize-for-image.sh # Full sanitize for image distribution
# sudo ./sanitize-for-image.sh --soft # Soft reset (keeps WiFi for testing)
# #
set -e set -e
@@ -19,21 +22,38 @@ set -e
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' NC='\033[0m'
SOFT_RESET=false
if [ "$1" = "--soft" ] || [ "$1" = "-s" ]; then
SOFT_RESET=true
fi
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Error: Must run as root (sudo)${NC}" echo -e "${RED}Error: Must run as root (sudo)${NC}"
exit 1 exit 1
fi fi
echo -e "${YELLOW}" if [ "$SOFT_RESET" = true ]; then
echo "╔═══════════════════════════════════════════════════════════════╗" echo -e "${CYAN}"
echo "║ Sanitize Pi for Image Distribution ║" echo "+-----------------------------------------------------------------+"
echo " " echo "| Soft Reset (Factory Defaults) |"
echo " This will remove personal data and prepare for imaging. ║" echo "| |"
echo " The system will shut down when complete. ║" echo "| WiFi credentials will be KEPT for continued testing. |"
echo "╚═══════════════════════════════════════════════════════════════╝" echo "| Everything else will be reset to first-boot state. |"
echo "+-----------------------------------------------------------------+"
echo -e "${NC}" echo -e "${NC}"
else
echo -e "${YELLOW}"
echo "+-----------------------------------------------------------------+"
echo "| Sanitize Pi for Image Distribution |"
echo "| |"
echo "| This will remove ALL personal data for imaging. |"
echo "| The system will shut down when complete. |"
echo "+-----------------------------------------------------------------+"
echo -e "${NC}"
fi
read -p "Continue? This cannot be undone! [y/N] " -n 1 -r read -p "Continue? This cannot be undone! [y/N] " -n 1 -r
echo echo
@@ -42,7 +62,19 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1 exit 1
fi fi
echo -e "${GREEN}[1/9]${NC} Removing WiFi credentials..." # Track validation results
VALIDATION_ERRORS=0
# =============================================================================
# Step 1: WiFi Credentials
# =============================================================================
if [ "$SOFT_RESET" = true ]; then
echo -e "${GREEN}[1/10]${NC} Keeping WiFi credentials (soft reset)..."
echo " WiFi config preserved"
else
echo -e "${GREEN}[1/10]${NC} Removing WiFi credentials..."
# Remove from rootfs
if [ -f /etc/wpa_supplicant/wpa_supplicant.conf ]; then if [ -f /etc/wpa_supplicant/wpa_supplicant.conf ]; then
cat > /etc/wpa_supplicant/wpa_supplicant.conf << 'EOF' cat > /etc/wpa_supplicant/wpa_supplicant.conf << 'EOF'
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
@@ -55,12 +87,22 @@ country=US
# psk="YourPassword" # psk="YourPassword"
# } # }
EOF EOF
echo " WiFi credentials cleared" echo " Cleared /etc/wpa_supplicant/wpa_supplicant.conf"
else
echo " No wpa_supplicant.conf found"
fi fi
echo -e "${GREEN}[2/9]${NC} Removing SSH authorized keys..." # Remove from boot partition (headless setup file)
BOOT_PART=$(findmnt -n -o SOURCE /boot/firmware 2>/dev/null || findmnt -n -o SOURCE /boot 2>/dev/null || echo "")
if [ -n "$BOOT_PART" ]; then
BOOT_MOUNT=$(findmnt -n -o TARGET "$BOOT_PART" 2>/dev/null || echo "/boot")
rm -f "$BOOT_MOUNT/wpa_supplicant.conf" 2>/dev/null || true
echo " Removed boot partition WiFi config"
fi
fi
# =============================================================================
# Step 2: SSH Authorized Keys
# =============================================================================
echo -e "${GREEN}[2/10]${NC} Removing SSH authorized keys..."
for user_home in /home/*; do for user_home in /home/*; do
if [ -d "$user_home/.ssh" ]; then if [ -d "$user_home/.ssh" ]; then
rm -f "$user_home/.ssh/authorized_keys" rm -f "$user_home/.ssh/authorized_keys"
@@ -70,15 +112,28 @@ for user_home in /home/*; do
done done
rm -f /root/.ssh/authorized_keys /root/.ssh/known_hosts 2>/dev/null || true rm -f /root/.ssh/authorized_keys /root/.ssh/known_hosts 2>/dev/null || true
echo -e "${GREEN}[3/9]${NC} Clearing bash history..." # =============================================================================
# Step 3: SSH Host Keys
# =============================================================================
echo -e "${GREEN}[3/10]${NC} Removing SSH host keys (will regenerate on first boot)..."
rm -f /etc/ssh/ssh_host_*
echo " SSH host keys removed"
# =============================================================================
# Step 4: Bash History
# =============================================================================
echo -e "${GREEN}[4/10]${NC} Clearing bash history..."
for user_home in /home/*; do for user_home in /home/*; do
rm -f "$user_home/.bash_history" rm -f "$user_home/.bash_history"
rm -f "$user_home/.python_history" rm -f "$user_home/.python_history"
done done
rm -f /root/.bash_history /root/.python_history 2>/dev/null || true rm -f /root/.bash_history /root/.python_history 2>/dev/null || true
history -c history -c 2>/dev/null || true
echo -e "${GREEN}[4/9]${NC} Removing Stegasoo user data..." # =============================================================================
# Step 5: Stegasoo User Data
# =============================================================================
echo -e "${GREEN}[5/10]${NC} Removing Stegasoo user data..."
# Remove auth database (users create their own admin on first run) # Remove auth database (users create their own admin on first run)
rm -rf /home/*/stegasoo/frontends/web/instance/ rm -rf /home/*/stegasoo/frontends/web/instance/
# Remove SSL certs (will be regenerated) # Remove SSL certs (will be regenerated)
@@ -87,35 +142,36 @@ rm -rf /home/*/stegasoo/frontends/web/certs/
rm -f /home/*/stegasoo/frontends/web/.env rm -f /home/*/stegasoo/frontends/web/.env
echo " Stegasoo instance data cleared" echo " Stegasoo instance data cleared"
echo -e "${GREEN}[5/9]${NC} Setting up first-boot wizard..." # =============================================================================
# Step 6: First-Boot Wizard Setup
# =============================================================================
echo -e "${GREEN}[6/10]${NC} Setting up first-boot wizard..."
# Find stegasoo install directory # Find stegasoo install directory
STEGASOO_DIR=$(ls -d /home/*/stegasoo 2>/dev/null | head -1) STEGASOO_DIR=$(ls -d /home/*/stegasoo 2>/dev/null | head -1)
echo " Looking for stegasoo in: $STEGASOO_DIR"
if [ -z "$STEGASOO_DIR" ]; then if [ -z "$STEGASOO_DIR" ]; then
echo -e " ${RED}ERROR: Could not find stegasoo directory in /home/*/stegasoo${NC}" for dir in /root/stegasoo /opt/stegasoo; do
echo " Checking common locations..."
for dir in /home/*/stegasoo /root/stegasoo /opt/stegasoo; do
if [ -d "$dir" ]; then if [ -d "$dir" ]; then
STEGASOO_DIR="$dir" STEGASOO_DIR="$dir"
echo " Found at: $STEGASOO_DIR"
break break
fi fi
done done
fi fi
STEGASOO_USER=$(stat -c '%U' "$STEGASOO_DIR" 2>/dev/null || echo "pi") STEGASOO_USER=$(stat -c '%U' "$STEGASOO_DIR" 2>/dev/null || echo "pi")
echo " Stegasoo directory: $STEGASOO_DIR"
echo " Stegasoo user: $STEGASOO_USER" echo " Stegasoo user: $STEGASOO_USER"
if [ -n "$STEGASOO_DIR" ] && [ -f "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" ]; then if [ -n "$STEGASOO_DIR" ] && [ -f "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" ]; then
# Install the profile.d hook # Install the profile.d hook
cp "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" /etc/profile.d/stegasoo-wizard.sh cp "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" /etc/profile.d/stegasoo-wizard.sh
chmod 755 /etc/profile.d/stegasoo-wizard.sh chmod 644 /etc/profile.d/stegasoo-wizard.sh
echo " Installed first-boot wizard hook" echo " Installed wizard hook to /etc/profile.d/"
# Create the first-boot flag # Create the first-boot flag
touch /etc/stegasoo-first-boot touch /etc/stegasoo-first-boot
echo " Created first-boot flag" echo " Created /etc/stegasoo-first-boot flag"
# Reset systemd service to defaults (wizard will reconfigure) # Reset systemd service to defaults (wizard will reconfigure)
cat > /etc/systemd/system/stegasoo.service <<EOF cat > /etc/systemd/system/stegasoo.service <<EOF
@@ -140,53 +196,141 @@ RestartSec=5
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
systemctl daemon-reload systemctl daemon-reload
echo " Reset service to defaults" echo " Reset systemd service to defaults"
# Verify files were created
if [ -f /etc/stegasoo-first-boot ] && [ -f /etc/profile.d/stegasoo-wizard.sh ]; then
echo -e " ${GREEN}✓ Wizard setup verified${NC}"
else else
echo -e " ${RED}✗ Wizard files missing after setup!${NC}" echo -e " ${RED}ERROR: Could not find wizard script${NC}"
fi
else
echo -e " ${RED}ERROR: Could not set up wizard${NC}"
echo " STEGASOO_DIR: $STEGASOO_DIR" echo " STEGASOO_DIR: $STEGASOO_DIR"
echo " Wizard script exists: $([ -f "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" ] && echo 'yes' || echo 'NO')" VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
echo ""
echo " You may need to manually run:"
echo " sudo touch /etc/stegasoo-first-boot"
echo " sudo cp $STEGASOO_DIR/rpi/stegasoo-wizard.sh /etc/profile.d/"
fi fi
echo -e "${GREEN}[6/9]${NC} Clearing logs..." # =============================================================================
journalctl --rotate # Step 7: Logs
journalctl --vacuum-time=1s # =============================================================================
rm -rf /var/log/*.log /var/log/*.gz /var/log/*.[0-9] echo -e "${GREEN}[7/10]${NC} Clearing logs..."
rm -rf /var/log/apt/* journalctl --rotate 2>/dev/null || true
rm -rf /var/log/journal/* journalctl --vacuum-time=1s 2>/dev/null || true
rm -rf /var/log/*.log /var/log/*.gz /var/log/*.[0-9] 2>/dev/null || true
rm -rf /var/log/apt/* 2>/dev/null || true
rm -rf /var/log/journal/* 2>/dev/null || true
find /var/log -type f -name "*.log" -delete 2>/dev/null || true find /var/log -type f -name "*.log" -delete 2>/dev/null || true
echo " Logs cleared" echo " Logs cleared"
echo -e "${GREEN}[7/9]${NC} Clearing temporary files..." # =============================================================================
rm -rf /tmp/* # Step 8: Temporary Files
rm -rf /var/tmp/* # =============================================================================
echo -e "${GREEN}[8/10]${NC} Clearing temporary files..."
rm -rf /tmp/* 2>/dev/null || true
rm -rf /var/tmp/* 2>/dev/null || true
echo " Temp files cleared" echo " Temp files cleared"
echo -e "${GREEN}[8/9]${NC} Clearing package cache..." # =============================================================================
apt-get clean # Step 9: Package Cache
rm -rf /var/cache/apt/archives/* # =============================================================================
echo -e "${GREEN}[9/10]${NC} Clearing package cache..."
apt-get clean 2>/dev/null || true
rm -rf /var/cache/apt/archives/* 2>/dev/null || true
echo " Package cache cleared" echo " Package cache cleared"
echo -e "${GREEN}[9/9]${NC} Final cleanup..." # =============================================================================
# Remove this script's evidence # Step 10: Final Sync
rm -f /root/.bash_history # =============================================================================
echo -e "${GREEN}[10/10]${NC} Final sync..."
rm -f /root/.bash_history 2>/dev/null || true
sync sync
echo " Filesystem synced"
# =============================================================================
# Validation
# =============================================================================
echo "" echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}Validating sanitization...${NC}"
echo -e "${GREEN}║ Sanitization Complete! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}" # Check first-boot flag
if [ -f /etc/stegasoo-first-boot ]; then
echo -e " ${GREEN}[PASS]${NC} First-boot flag exists"
else
echo -e " ${RED}[FAIL]${NC} First-boot flag missing"
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
fi
# Check profile.d hook
if [ -f /etc/profile.d/stegasoo-wizard.sh ]; then
echo -e " ${GREEN}[PASS]${NC} Wizard hook installed"
else
echo -e " ${RED}[FAIL]${NC} Wizard hook missing"
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
fi
# Check SSH host keys removed
if ls /etc/ssh/ssh_host_* 1>/dev/null 2>&1; then
echo -e " ${RED}[FAIL]${NC} SSH host keys still present"
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
else
echo -e " ${GREEN}[PASS]${NC} SSH host keys removed"
fi
# Check Stegasoo instance data removed
if ls /home/*/stegasoo/frontends/web/instance/*.db 1>/dev/null 2>&1; then
echo -e " ${RED}[FAIL]${NC} Stegasoo database still present"
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
else
echo -e " ${GREEN}[PASS]${NC} Stegasoo database removed"
fi
# Check WiFi (only for full sanitize)
if [ "$SOFT_RESET" = false ]; then
if grep -q "psk=" /etc/wpa_supplicant/wpa_supplicant.conf 2>/dev/null; then
echo -e " ${RED}[FAIL]${NC} WiFi credentials still present"
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
else
echo -e " ${GREEN}[PASS]${NC} WiFi credentials cleared"
fi
else
echo -e " ${YELLOW}[SKIP]${NC} WiFi check (soft reset mode)"
fi
# Check authorized_keys removed
AUTH_KEYS_FOUND=false
for user_home in /home/*; do
if [ -f "$user_home/.ssh/authorized_keys" ]; then
AUTH_KEYS_FOUND=true
break
fi
done
if [ "$AUTH_KEYS_FOUND" = true ]; then
echo -e " ${RED}[FAIL]${NC} SSH authorized_keys still present"
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
else
echo -e " ${GREEN}[PASS]${NC} SSH authorized_keys removed"
fi
# =============================================================================
# Summary
# =============================================================================
echo "" echo ""
if [ $VALIDATION_ERRORS -eq 0 ]; then
echo -e "${GREEN}+-----------------------------------------------------------------+${NC}"
echo -e "${GREEN}| Sanitization Complete! |${NC}"
echo -e "${GREEN}| All validation checks passed. |${NC}"
echo -e "${GREEN}+-----------------------------------------------------------------+${NC}"
else
echo -e "${RED}+-----------------------------------------------------------------+${NC}"
echo -e "${RED}| Sanitization Complete with Errors |${NC}"
echo -e "${RED}| $VALIDATION_ERRORS validation check(s) failed |${NC}"
echo -e "${RED}+-----------------------------------------------------------------+${NC}"
fi
echo ""
if [ "$SOFT_RESET" = true ]; then
echo -e "${CYAN}Soft reset complete.${NC}"
echo "You can now reboot to test the first-boot wizard."
echo ""
read -p "Reboot now? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
reboot
fi
else
echo "The system is ready for imaging." echo "The system is ready for imaging."
echo "" echo ""
echo -e "${YELLOW}Next steps:${NC}" echo -e "${YELLOW}Next steps:${NC}"
@@ -194,10 +338,11 @@ echo " 1. Shut down: sudo shutdown -h now"
echo " 2. Remove SD card" echo " 2. Remove SD card"
echo " 3. On another machine, copy with:" echo " 3. On another machine, copy with:"
echo " sudo dd if=/dev/sdX of=stegasoo-rpi.img bs=4M status=progress" echo " sudo dd if=/dev/sdX of=stegasoo-rpi.img bs=4M status=progress"
echo " 4. Compress: xz -9 stegasoo-rpi.img" echo " 4. Compress: zstd -19 stegasoo-rpi.img"
echo "" echo ""
read -p "Shut down now? [y/N] " -n 1 -r read -p "Shut down now? [y/N] " -n 1 -r
echo echo
if [[ $REPLY =~ ^[Yy]$ ]]; then if [[ $REPLY =~ ^[Yy]$ ]]; then
shutdown -h now shutdown -h now
fi fi
fi

View File

@@ -1,374 +0,0 @@
# Stegasoo v3.2.0 - Complete Change Summary
## Overview
This update makes two major breaking changes to Stegasoo:
1. **Remove date dependency** - Date no longer used in cryptographic operations
2. **Rename day_phrase → passphrase** - Reflects removal of daily rotation requirement
## Version Information
- **Previous**: v3.1.0 (date-dependent, day_phrase)
- **Current**: v3.2.0 (date-independent, passphrase)
- **Format Version**: 3 → 4 (breaking change)
- **Compatibility**: NOT backward compatible with v3.1.0
## Files Modified
### Core Files (MUST UPDATE)
1. **crypto.py** ✅ Updated
- Removed `date_str` parameter from all functions
- Renamed `day_phrase``passphrase` in all functions
- Removed date from key derivation material
- Simplified header format (no date field)
- Updated error messages
2. **constants.py** ✅ Updated
- Version: `__version__ = "3.2.0"`
- Format: `FORMAT_VERSION = 4`
- Added passphrase constants:
- `MIN_PASSPHRASE_WORDS = 3`
- `MAX_PASSPHRASE_WORDS = 12`
- `DEFAULT_PASSPHRASE_WORDS = 4` (increased from 3)
- `RECOMMENDED_PASSPHRASE_WORDS = 4`
- Kept legacy aliases for transition
3. **models.py** ✅ Updated
- `Credentials`: Changed from `phrases: dict``passphrase: str`
- `EncodeInput`: Renamed `day_phrase``passphrase`, removed `date_str`
- `DecodeInput`: Renamed `day_phrase``passphrase`
- `EncodeResult`: Made `date_used` optional (cosmetic only)
- `DecodeResult`: `date_encoded` always None in v3.2.0
- `ValidationResult`: Added `warning` field
4. **validation.py** ✅ Updated
- Renamed `validate_phrase()``validate_passphrase()`
- Added word count validation with warnings
- Recommends 4+ words for good security
- Updated error messages
### Files Needing Updates
5. **__init__.py** - Public API
- [ ] `encode()`: Remove `date_str`, rename `day_phrase``passphrase`
- [ ] `encode_file()`: Same changes
- [ ] `encode_bytes()`: Same changes
- [ ] `decode()`: Remove `date_str`, rename `day_phrase``passphrase`
- [ ] `decode_text()`: Same changes
- [ ] Update all docstrings
6. **keygen.py** - Key generation
- [ ] `generate_day_phrases()``generate_passphrases()` or keep with new implementation
- [ ] `generate_credentials()`: Update to use single passphrase
- [ ] Update `Credentials` creation
7. **batch.py** - Batch operations
- [ ] `BatchCredentials`: Rename `day_phrase``passphrase`
- [ ] Update all batch functions
8. **cli.py** - Command line
- [ ] `--phrase``--passphrase` (or keep `--phrase` for simplicity)
- [ ] Update help text
- [ ] Update credentials dict creation
9. **steganography.py** - No changes needed
- Uses keys from crypto module, doesn't directly handle phrases/dates
10. **dct_steganography.py** - No changes needed
- Uses keys from crypto module
### Optional/Documentation Files
11. **utils.py** - Keep as-is (organizational functions)
12. **debug.py** - No changes needed
13. **exceptions.py** - No changes needed
14. **compression.py** - No changes needed
15. **qr_utils.py** - No changes needed
## Key Changes Breakdown
### 1. Function Signatures
**Before (v3.1.0):**
```python
def derive_hybrid_key(
photo_data: bytes,
day_phrase: str,
date_str: str,
salt: bytes,
pin: str = "",
rsa_key_data: Optional[bytes] = None
) -> bytes:
```
**After (v3.2.0):**
```python
def derive_hybrid_key(
photo_data: bytes,
passphrase: str,
salt: bytes,
pin: str = "",
rsa_key_data: Optional[bytes] = None
) -> bytes:
```
### 2. Key Derivation Material
**Before:**
```python
key_material = (
photo_hash +
day_phrase.lower().encode() +
pin.encode() +
date_str.encode() + # ← REMOVED
salt
)
```
**After:**
```python
key_material = (
photo_hash +
passphrase.lower().encode() +
pin.encode() +
salt
)
```
### 3. Header Format
**Before (v3.1.0):** 66+ bytes
```
[Magic:4][Version:1][DateLen:1][Date:10][Salt:32][IV:12][Tag:16][Ciphertext]
```
**After (v3.2.0):** 65 bytes
```
[Magic:4][Version:1][Salt:32][IV:12][Tag:16][Ciphertext]
```
### 4. Public API
**Before:**
```python
# Encoding
result = encode(
message="Secret",
reference_photo=photo,
carrier_image=carrier,
day_phrase="apple forest thunder",
pin="123456",
date_str="2025-01-15"
)
# Decoding
decoded = decode(
stego_image=stego,
reference_photo=photo,
day_phrase="apple forest thunder",
pin="123456",
date_str="2025-01-15"
)
```
**After:**
```python
# Encoding
result = encode(
message="Secret",
reference_photo=photo,
carrier_image=carrier,
passphrase="apple forest thunder mountain",
pin="123456"
)
# Decoding
decoded = decode(
stego_image=stego,
reference_photo=photo,
passphrase="apple forest thunder mountain",
pin="123456"
)
```
## Migration Path
### For Users with v3.1.0 Messages
1. **Before upgrading**, decode all messages with v3.1.0:
```bash
# Using v3.1.0
python decode_all.py
```
2. Save the decoded content
3. Upgrade to v3.2.0
4. Re-encode with v3.2.0 if needed
### For Developers
1. Update the 4 core files: crypto.py, constants.py, models.py, validation.py
2. Update remaining files in order:
- `__init__.py` (public API - critical)
- `keygen.py` (credential generation)
- `batch.py` (batch operations)
- `cli.py` (command line)
3. Run tests to verify:
```bash
pytest tests/ -v
```
4. Update documentation and examples
## Benefits
### Simplicity
- ❌ Before: 3 parameters (day_phrase, pin, date)
- ✅ After: 2 parameters (passphrase, pin)
### User Experience
- ❌ Before: "What date did I encode this?" "Which day's phrase?"
- ✅ After: Just use your passphrase
### Asynchronous Ready
- ❌ Before: Must know encoding date
- ✅ After: Decode anytime
### Less Metadata
- ❌ Before: Date stored in header
- ✅ After: No temporal metadata
## Security Considerations
### Entropy Comparison
**v3.1.0:**
- Photo hash: ~128 bits
- Day phrase (3 words): ~33 bits
- PIN (6 digits): ~20 bits
- Date: ~33 bits (10 digits)
- **Total: ~214 bits**
**v3.2.0:**
- Photo hash: ~128 bits
- Passphrase (4 words): ~44 bits
- PIN (6 digits): ~20 bits
- **Total: ~192 bits**
**Mitigation:** Recommend longer passphrases (4-5 words vs 3)
### Best Practices for v3.2.0
1. **Use 4+ word passphrases** (increased from 3)
2. **Keep using PINs** (additional 20 bits)
3. **Protect reference photo** (still critical)
4. **Consider RSA keys** for highest security
## Testing Checklist
- [ ] Unit tests pass
- [ ] Integration tests pass
- [ ] Encode/decode round-trip works
- [ ] File payloads work
- [ ] LSB mode works
- [ ] DCT mode works
- [ ] Batch operations work
- [ ] CLI commands work
- [ ] Error messages are clear
- [ ] Validation works correctly
- [ ] No references to "day_phrase" remain
- [ ] No date parameters remain (except cosmetic)
## Documentation Updates Needed
- [ ] README.md - Update all examples
- [ ] API documentation - Update function signatures
- [ ] Tutorials - Remove date parameters
- [ ] CHANGELOG.md - Add v3.2.0 entry
- [ ] Migration guide - How to upgrade from v3.1.0
- [ ] Examples directory - Update all scripts
## Backward Compatibility Strategy
### Option 1: Clean Break (Recommended)
- No compatibility code
- Clear version separation
- Users must migrate manually
### Option 2: Temporary Wrapper
```python
def encode(
message,
reference_photo,
carrier_image,
passphrase: str = None,
day_phrase: str = None, # Deprecated
date_str: str = None, # Deprecated
pin: str = "",
...
):
if day_phrase and not passphrase:
import warnings
warnings.warn("day_phrase deprecated, use passphrase", DeprecationWarning)
passphrase = day_phrase
if date_str:
warnings.warn("date_str no longer used", DeprecationWarning)
# ... rest of function
```
## Release Checklist
- [ ] All files updated
- [ ] Tests passing
- [ ] Documentation updated
- [ ] Migration guide written
- [ ] CHANGELOG.md updated
- [ ] Version bumped to 3.2.0
- [ ] Git tag created: v3.2.0
- [ ] PyPI package published
- [ ] Release notes published
- [ ] Users notified of breaking changes
## Quick Reference
### Search and Replace Patterns
Safe to replace globally:
- `day_phrase` → `passphrase`
- `day phrase` → `passphrase`
- `Day phrase` → `Passphrase`
- `DEFAULT_PHRASE_WORDS` → `DEFAULT_PASSPHRASE_WORDS`
Do NOT replace:
- `DAY_NAMES` (keep for utilities)
- `get_day_from_date` (keep for utilities)
- `generate_day_phrases` (rename function itself)
### Error Message Updates
- "Day phrase is required" → "Passphrase is required"
- "Check your phrase, PIN" → "Check your passphrase, PIN"
- "the day's phrase" → "the passphrase"
- "today's passphrase" → "passphrase"
## Support
For issues or questions during migration:
1. Check the migration guide
2. Review the comparison document
3. Look at updated examples
4. File an issue on GitHub
---
**Status:**
✅ Core files updated (crypto, constants, models, validation)
⏳ Remaining files need updates (__init__, keygen, batch, cli)
📝 Documentation updates pending

View File

@@ -1,528 +0,0 @@
# Stegasoo v4.0.0 Release Checklist
## Overview
This checklist covers functionality testing for the v4.0.0 release.
### Changes in v4.0.0
| Change | v3.2.0 | v4.0.0 |
|--------|--------|--------|
| Python version | 3.10-3.12 | 3.10-3.12 (3.13 NOT supported) |
| JPEG handling | Could crash on quality=100 | Normalized before jpegio |
| Header size | 65 bytes | 65 bytes (unchanged) |
| API | passphrase, no date_str | Same (no breaking changes) |
| Format version | 4 | 4 (compatible with v3.2.0) |
### Key Points
- **No breaking API changes from v3.2.0**
- **v4.0 CAN decode v3.2.0 images** (same format version)
- **v4.0 CANNOT decode v3.1.x or earlier images**
- **Python 3.13 is NOT supported** (jpegio C extension ABI incompatibility)
---
## 1. Pre-Release Checks
### 1.1 Python Version
```bash
python --version # Must be 3.10, 3.11, or 3.12
```
- [ ] Python version is 3.10, 3.11, or 3.12
- [ ] NOT Python 3.13 (jpegio will crash)
### 1.2 Dependencies
```bash
pip list | grep -E "jpegio|scipy|pillow|argon2"
```
- [ ] jpegio installed (for DCT JPEG support)
- [ ] scipy installed (for DCT mode)
- [ ] pillow installed
- [ ] argon2-cffi installed
---
## 2. Core Library Tests
### 2.1 Run Unit Tests
```bash
cd /path/to/stegasoo
pytest tests/ -v
```
- [ ] All tests pass
- [ ] No deprecation warnings for removed parameters
### 2.2 JPEG Normalization Test (NEW in v4.0)
```bash
python -c "
from PIL import Image
import io
from stegasoo import encode, decode
# Create quality=100 JPEG (triggers normalization)
img = Image.new('RGB', (400, 400), 'red')
buf = io.BytesIO()
img.save(buf, format='JPEG', quality=100)
jpeg_data = buf.getvalue()
# This should NOT crash (v3.2.0 would crash here)
result = encode(
message='Test quality 100',
reference_photo=jpeg_data,
carrier_image=jpeg_data,
passphrase='test phrase four words',
pin='123456',
embed_mode='dct'
)
print('✓ Quality=100 JPEG encode OK')
decoded = decode(
stego_image=result.stego_image,
reference_photo=jpeg_data,
passphrase='test phrase four words',
pin='123456'
)
assert decoded.message == 'Test quality 100'
print('✓ Quality=100 JPEG decode OK')
"
```
- [ ] Quality=100 JPEG encoding works (no crash)
- [ ] Quality=100 JPEG decoding works
### 2.3 Large Image Test (NEW in v4.0)
```bash
python -c "
from PIL import Image
import io
from stegasoo import encode, decode
# Create large image (similar to 14MB real photo)
img = Image.new('RGB', (4000, 3000), 'blue')
buf = io.BytesIO()
img.save(buf, format='PNG')
large_image = buf.getvalue()
print(f'Test image size: {len(large_image) / 1024 / 1024:.1f} MB')
result = encode(
message='Large image test',
reference_photo=large_image,
carrier_image=large_image,
passphrase='large image test phrase',
pin='123456'
)
print('✓ Large image encode OK')
decoded = decode(
stego_image=result.stego_image,
reference_photo=large_image,
passphrase='large image test phrase',
pin='123456'
)
assert decoded.message == 'Large image test'
print('✓ Large image decode OK')
"
```
- [ ] Large image (12MP+) encoding works
- [ ] Large image decoding works
---
## 3. Docker Build Tests
### 3.1 Base Image Build
```bash
# Build base image (one-time, 5-10 min)
sudo docker build -f Dockerfile.base -t stegasoo-base:latest .
```
- [ ] Base image builds successfully
- [ ] jpegio + scipy + numpy verification passes
### 3.2 Application Build
```bash
# Fast build using base image
sudo docker-compose build
```
- [ ] Web container builds
- [ ] API container builds
### 3.3 Container Startup
```bash
sudo docker-compose up -d
sudo docker-compose logs
```
- [ ] Web container starts without errors
- [ ] API container starts without errors
- [ ] No import errors in logs
---
## 4. Web UI Tests (`http://localhost:5000`)
### 4.1 Home Page
- [ ] v4.0 badge visible
- [ ] "Learn More" button is white/visible
- [ ] No references to "day phrase" or dates
### 4.2 Generate Page (`/generate`)
- [ ] Default is 4 words
- [ ] Single passphrase generated (not 7 daily)
- [ ] PIN toggle shows/hides digits
- [ ] Memory aid generator works
### 4.3 Encode Page (`/encode`)
- [ ] Passphrase field has blue glow on focus
- [ ] PIN field has orange glow on focus
- [ ] PIN box is 180px wide (fits LastPass icon)
- [ ] Passphrase font shrinks for long input (stepped)
- [ ] RSA .pem/QR toggle works
- [ ] QR image preview shows when selected
- [ ] DCT mode options appear when selected
- [ ] Encoding works (LSB mode)
- [ ] Encoding works (DCT mode)
### 4.4 Decode Page (`/decode`)
- [ ] Same styling as encode (glowing inputs)
- [ ] RSA .pem/QR toggle works (matches encode layout)
- [ ] QR image preview shows when selected
- [ ] Copy button is below message (not overlapping)
- [ ] Decoding works (LSB mode)
- [ ] Decoding works (DCT mode)
- [ ] Auto mode detection works
### 4.5 About Page (`/about`)
- [ ] Version history table present
- [ ] v4.0.0 entry in table
- [ ] Python 3.10-3.12 requirement noted
- [ ] No marketing language ("military-grade" removed)
---
## 5. API Tests (`http://localhost:8000`)
### 5.1 Status Endpoint
```bash
curl http://localhost:8000/
```
- [ ] Returns version "4.0.0"
- [ ] No import errors
### 5.2 Generate Endpoint
```bash
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": true}'
```
- [ ] Returns single `passphrase` string
- [ ] Returns 4 words by default
### 5.3 OpenAPI Docs
- [ ] `/docs` loads (Swagger UI)
- [ ] `/redoc` loads (ReDoc)
- [ ] All endpoints documented
---
## 6. CLI Tests
### 6.1 Version
```bash
stegasoo --version
```
- [ ] Shows 4.0.0
### 6.2 Generate
```bash
stegasoo generate --pin --words 4
```
- [ ] Single passphrase output
- [ ] 4 words generated
### 6.3 Encode/Decode Roundtrip
```bash
# Generate test image
python -c "from PIL import Image; Image.new('RGB', (200,200), 'red').save('/tmp/test.png')"
# Encode
stegasoo encode \
-r /tmp/test.png \
-c /tmp/test.png \
-p "cli test phrase here" \
--pin 123456 \
-m "CLI roundtrip test" \
-o /tmp/stego.png
# Decode
stegasoo decode \
-r /tmp/test.png \
-s /tmp/stego.png \
-p "cli test phrase here" \
--pin 123456
```
- [ ] Encode succeeds
- [ ] Decode returns correct message
---
## 7. Cross-Version Compatibility
### 7.1 v3.2.0 Compatibility
- [ ] v4.0 can decode v3.2.0 images (same format version 4)
### 7.2 v3.1.x Incompatibility
- [ ] v4.0 fails gracefully on v3.1.x images
- [ ] Error message is clear
---
## 8. Documentation Review
### 8.1 Updated Files
- [ ] README.md - v4.0 references
- [ ] INSTALL.md - Python 3.13 warning prominent
- [ ] SECURITY.md - v4.0 changes documented
- [ ] UNDER_THE_HOOD.md - JPEG normalization section
### 8.2 Template Updates
- [ ] All 7 templates updated
- [ ] No v3.x badges remaining
- [ ] Version history in About page
---
## 9. Quick Smoke Test Script
```bash
#!/bin/bash
# v4.0.0 Smoke Test
set -e
echo "=== Stegasoo v4.0.0 Smoke Test ==="
# Check version
echo "1. Checking version..."
python -c "import stegasoo; assert stegasoo.__version__.startswith('4.'), f'Wrong version: {stegasoo.__version__}'; print(f'✓ Version: {stegasoo.__version__}')"
# Check Python version
echo "2. Checking Python version..."
python -c "
import sys
v = sys.version_info
assert v.major == 3 and 10 <= v.minor <= 12, f'Python {v.major}.{v.minor} not supported'
print(f'✓ Python {v.major}.{v.minor}.{v.micro}')
"
# Check DCT support
echo "3. Checking DCT support..."
python -c "
from stegasoo import has_dct_support
from stegasoo.dct_steganography import has_jpegio_support
print(f' DCT (scipy): {has_dct_support()}')
print(f' JPEG native (jpegio): {has_jpegio_support()}')
assert has_dct_support(), 'DCT not available'
print('✓ DCT support OK')
"
# Test encode/decode roundtrip
echo "4. Testing encode/decode roundtrip..."
python -c "
from stegasoo import encode, decode
from PIL import Image
import io
img = Image.new('RGB', (200, 200), color='blue')
buf = io.BytesIO()
img.save(buf, format='PNG')
test_image = buf.getvalue()
result = encode(
message='Hello v4.0.0!',
reference_photo=test_image,
carrier_image=test_image,
passphrase='test phrase four words',
pin='123456'
)
decoded = decode(
stego_image=result.stego_image,
reference_photo=test_image,
passphrase='test phrase four words',
pin='123456'
)
assert decoded.message == 'Hello v4.0.0!', f'Got: {decoded.message}'
print('✓ LSB roundtrip OK')
"
# Test DCT mode
echo "5. Testing DCT mode..."
python -c "
from stegasoo import encode, decode
from PIL import Image
import io
img = Image.new('RGB', (400, 400), color='green')
buf = io.BytesIO()
img.save(buf, format='PNG')
test_image = buf.getvalue()
result = encode(
message='DCT v4.0 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 v4.0 test'
print('✓ DCT roundtrip OK')
"
# Test JPEG quality=100 (v4.0 fix)
echo "6. Testing JPEG quality=100 handling..."
python -c "
from stegasoo import encode, decode
from PIL import Image
import io
img = Image.new('RGB', (400, 400), color='red')
buf = io.BytesIO()
img.save(buf, format='JPEG', quality=100)
jpeg_q100 = buf.getvalue()
result = encode(
message='Quality 100 test',
reference_photo=jpeg_q100,
carrier_image=jpeg_q100,
passphrase='jpeg quality test here',
pin='123456',
embed_mode='dct'
)
decoded = decode(
stego_image=result.stego_image,
reference_photo=jpeg_q100,
passphrase='jpeg quality test here',
pin='123456'
)
assert decoded.message == 'Quality 100 test'
print('✓ JPEG quality=100 OK (v4.0 fix working)')
"
echo ""
echo "=== All smoke tests passed! ==="
echo "Ready for release."
```
---
## 10. Release Steps
### 10.1 Final Checks
- [ ] All tests pass
- [ ] All Docker containers work
- [ ] Documentation updated
- [ ] Version bumped in `constants.py` and `pyproject.toml`
### 10.2 Git
```bash
git add -A
git status # Review changes
git commit -m "v4.0.0: JPEG normalization, Python 3.12, UI polish"
git tag v4.0.0
git push origin main --tags
```
- [ ] Changes committed
- [ ] Tag created
- [ ] Pushed to remote
### 10.3 Release Notes
```markdown
## v4.0.0
### What's New
- **JPEG Normalization**: Quality=100 JPEGs now work with DCT mode
- **Python 3.12**: Recommended version (3.13 NOT supported due to jpegio)
- **UI Polish**: Glowing input fields, better layout, version history
### Fixes
- Fixed jpegio crash on quality=100 JPEG images
- Fixed QR code input on decode page
- Fixed passphrase font sizing (stepped instead of smooth)
### Breaking Changes
- Python 3.13 is NOT supported
### Compatibility
- v4.0 can decode v3.2.0 images (same format)
- v4.0 CANNOT decode v3.1.x or earlier
```
---
## Sign-Off
| Area | Tested By | Date | Status |
|------|-----------|------|--------|
| Python/Dependencies | | | ☐ |
| Unit Tests | | | ☐ |
| Docker Build | | | ☐ |
| Web UI | | | ☐ |
| API | | | ☐ |
| CLI | | | ☐ |
| Documentation | | | ☐ |
**Release Approved:**
**Released By:** _________________
**Release Date:** _________________