diff --git a/.gitignore b/.gitignore
index def1e84..2c88040 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,3 +70,10 @@ scripts/
# Web UI auth database and SSL certs
frontends/web/instance/
frontends/web/certs/
+rpi/inject-wifi.sh
+
+# RPi image build artifacts
+*.img
+*.img.xz
+*.img.zst
+pishrink.sh
diff --git a/check_scipy.py b/check_scipy.py
deleted file mode 100644
index e073d0b..0000000
--- a/check_scipy.py
+++ /dev/null
@@ -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)")
diff --git a/frontends/api/API_UPDATE_SUMMARY_V3.2.0.md b/frontends/api/API_UPDATE_SUMMARY_V3.2.0.md
deleted file mode 100644
index d66a130..0000000
--- a/frontends/api/API_UPDATE_SUMMARY_V3.2.0.md
+++ /dev/null
@@ -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!
diff --git a/frontends/web/WEB_FRONTEND_UPDATE_SUMMARY_V3.2.0.md b/frontends/web/WEB_FRONTEND_UPDATE_SUMMARY_V3.2.0.md
deleted file mode 100644
index a7e5a95..0000000
--- a/frontends/web/WEB_FRONTEND_UPDATE_SUMMARY_V3.2.0.md
+++ /dev/null
@@ -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
-
-
-
-
-{% if generated %}
-
Daily Phrases
- {% for day in days %}
-
- | {{ day }} |
- {{ phrases[day] }} |
-
- {% endfor %}
-{% endif %}
-
-
-
-
-
-{% if generated %}
- Passphrase
-
-
{{ passphrase }}
-
Use this passphrase to encode and decode messages (no date needed!)
-
-{% endif %}
-```
-
-**Entropy display:**
-```html
-
-Phrase entropy: {{ phrase_entropy }} bits
-
-
-Passphrase entropy: {{ passphrase_entropy }} bits ({{ words_per_passphrase }} words)
-```
-
-### encode.html
-
-**Changes needed:**
-```html
-
-
-
-
-
-
-Defaults to today: {{ day_of_week }}
-
-
-
-
-
- v3.2.0: No date needed! Use your passphrase anytime.
-
-```
-
-### decode.html
-
-**Changes needed:**
-```html
-
-
-
-
-
-
-Will be auto-detected from filename if possible
-
-
-
-
-
-
-
- v3.2.0: No date needed to decode!
-
-
-
-```
-
-### index.html
-
-**Changes needed:**
-```html
-
-Generate daily passphrases and security credentials
-Hide messages using day-specific phrases
-
-
-Generate passphrases and security credentials
-v3.2.0: Simplified - no more daily rotation!
-```
-
-### about.html
-
-**Add v3.2.0 section:**
-```html
-Version 3.2.0 Changes
-
- - No date dependency - Encode and decode anytime without tracking dates
- - Single passphrase - No more daily rotation, just remember one strong passphrase
- - Better security - Default passphrase length increased to 4 words
- - Asynchronous ready - Perfect for dead drops and delayed delivery
-
-```
-
-## 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
-
-
⚠️ v3.2.0 Breaking Changes
-
If you have messages encoded with v3.1.0:
-
- - They cannot be decoded with v3.2.0
- - You need the original v3.1.0 installation to decode them
- - After decoding, you can re-encode with v3.2.0
-
-
-```
-
-## 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
-
-```
-
-## 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
diff --git a/minimal_flask_crash.py b/minimal_flask_crash.py
deleted file mode 100644
index 8ab880e..0000000
--- a/minimal_flask_crash.py
+++ /dev/null
@@ -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)
diff --git a/rpi/BUILD_IMAGE.md b/rpi/BUILD_IMAGE.md
index 3da49a0..d5887f2 100644
--- a/rpi/BUILD_IMAGE.md
+++ b/rpi/BUILD_IMAGE.md
@@ -5,12 +5,12 @@ Quick reference for building a distributable SD card image.
## Step 1: Flash Fresh Raspbian
Use rpi-imager with these settings:
-- **OS**: Raspberry Pi OS (64-bit)
+- **OS**: Raspberry Pi OS Lite (64-bit)
- **Hostname**: `stegasoo`
- **Enable SSH**: Yes (password auth)
-- **Username**: `pi` (or any)
-- **Password**: `raspberry` (temporary)
-- **WiFi**: Skip (use ethernet for clean image)
+- **Username**: `admin`
+- **Password**: `stegasoo`
+- **WiFi**: Configure for your network (sanitize script removes it later)
## Step 2: Boot & SSH In
@@ -43,17 +43,22 @@ curl -k https://localhost:5000
## Step 5: Sanitize for Distribution
```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:
-- WiFi credentials
+- WiFi credentials (unless `--soft`)
+- SSH host keys (regenerate on boot)
- SSH authorized keys
- Bash history
- Stegasoo auth database
- Logs and temp files
-The Pi will shut down when complete.
+The script validates all cleanup steps before finishing.
## Step 6: Copy the Image
@@ -75,18 +80,18 @@ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
chmod +x pishrink.sh
sudo ./pishrink.sh stegasoo-rpi-*.img
-# Compress
-xz -9 -T0 stegasoo-rpi-*.img
+# Compress (zstd is faster than xz with similar ratio)
+zstd -19 -T0 stegasoo-rpi-*.img
```
## Step 8: Distribute
-Upload `.img.xz` to GitHub Releases.
+Upload `.img.zst` to GitHub Releases.
Users can flash with:
```bash
# 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
```
@@ -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
sudo systemctl start stegasoo
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:
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
```
diff --git a/rpi/README.md b/rpi/README.md
index a8fcd2c..83f68c1 100644
--- a/rpi/README.md
+++ b/rpi/README.md
@@ -30,11 +30,21 @@ chmod +x setup.sh
## Requirements
- 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)
- ~2GB free disk space
- 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
### Start the Service
@@ -134,17 +144,23 @@ curl -k https://localhost:5000 # Should return HTML
### 4. Sanitize for Distribution
```bash
-# Download and run sanitize script
-curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash
+# Full sanitize (removes WiFi, shuts down for imaging)
+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:
-- WiFi credentials
+- WiFi credentials (unless `--soft`)
+- SSH host keys (regenerate on boot)
- SSH authorized keys
- Bash history
- Stegasoo auth database (users create their own admin)
- Logs and temp files
+The script validates cleanup and reports any issues.
+
### 5. Create the Image
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
sudo ./pishrink.sh stegasoo-rpi-*.img
-# Compress
-xz -9 -T0 stegasoo-rpi-*.img
+# Compress (zstd is faster than xz with similar compression)
+zstd -19 -T0 stegasoo-rpi-*.img
```
### 6. Distribute
-Upload the `.img.xz` file to GitHub Releases.
+Upload the `.img.zst` file to GitHub Releases.
Users flash with:
```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.
diff --git a/rpi/sanitize-for-image.sh b/rpi/sanitize-for-image.sh
index 1e22448..7fc72bd 100755
--- a/rpi/sanitize-for-image.sh
+++ b/rpi/sanitize-for-image.sh
@@ -4,14 +4,17 @@
# Run this BEFORE creating an image with dd
#
# This script removes:
-# - WiFi credentials
+# - WiFi credentials (unless --soft)
+# - SSH host keys (will regenerate on boot)
# - SSH authorized keys
# - User-specific data
# - Bash history
# - Logs
# - 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
@@ -19,21 +22,38 @@ set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
+CYAN='\033[0;36m'
NC='\033[0m'
+SOFT_RESET=false
+if [ "$1" = "--soft" ] || [ "$1" = "-s" ]; then
+ SOFT_RESET=true
+fi
+
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Error: Must run as root (sudo)${NC}"
exit 1
fi
-echo -e "${YELLOW}"
-echo "╔═══════════════════════════════════════════════════════════════╗"
-echo "║ Sanitize Pi for Image Distribution ║"
-echo "║ ║"
-echo "║ This will remove personal data and prepare for imaging. ║"
-echo "║ The system will shut down when complete. ║"
-echo "╚═══════════════════════════════════════════════════════════════╝"
-echo -e "${NC}"
+if [ "$SOFT_RESET" = true ]; then
+ echo -e "${CYAN}"
+ echo "+-----------------------------------------------------------------+"
+ echo "| Soft Reset (Factory Defaults) |"
+ echo "| |"
+ echo "| WiFi credentials will be KEPT for continued testing. |"
+ echo "| Everything else will be reset to first-boot state. |"
+ echo "+-----------------------------------------------------------------+"
+ 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
echo
@@ -42,9 +62,21 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
-echo -e "${GREEN}[1/9]${NC} Removing WiFi credentials..."
-if [ -f /etc/wpa_supplicant/wpa_supplicant.conf ]; then
- cat > /etc/wpa_supplicant/wpa_supplicant.conf << 'EOF'
+# 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
+ cat > /etc/wpa_supplicant/wpa_supplicant.conf << 'EOF'
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=US
@@ -55,12 +87,22 @@ country=US
# psk="YourPassword"
# }
EOF
- echo " WiFi credentials cleared"
-else
- echo " No wpa_supplicant.conf found"
+ echo " Cleared /etc/wpa_supplicant/wpa_supplicant.conf"
+ fi
+
+ # 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
-echo -e "${GREEN}[2/9]${NC} Removing SSH authorized keys..."
+# =============================================================================
+# Step 2: SSH Authorized Keys
+# =============================================================================
+echo -e "${GREEN}[2/10]${NC} Removing SSH authorized keys..."
for user_home in /home/*; do
if [ -d "$user_home/.ssh" ]; then
rm -f "$user_home/.ssh/authorized_keys"
@@ -70,15 +112,28 @@ for user_home in /home/*; do
done
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
rm -f "$user_home/.bash_history"
rm -f "$user_home/.python_history"
done
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)
rm -rf /home/*/stegasoo/frontends/web/instance/
# Remove SSL certs (will be regenerated)
@@ -87,35 +142,36 @@ rm -rf /home/*/stegasoo/frontends/web/certs/
rm -f /home/*/stegasoo/frontends/web/.env
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
STEGASOO_DIR=$(ls -d /home/*/stegasoo 2>/dev/null | head -1)
-echo " Looking for stegasoo in: $STEGASOO_DIR"
if [ -z "$STEGASOO_DIR" ]; then
- echo -e " ${RED}ERROR: Could not find stegasoo directory in /home/*/stegasoo${NC}"
- echo " Checking common locations..."
- for dir in /home/*/stegasoo /root/stegasoo /opt/stegasoo; do
+ for dir in /root/stegasoo /opt/stegasoo; do
if [ -d "$dir" ]; then
STEGASOO_DIR="$dir"
- echo " Found at: $STEGASOO_DIR"
break
fi
done
fi
STEGASOO_USER=$(stat -c '%U' "$STEGASOO_DIR" 2>/dev/null || echo "pi")
+echo " Stegasoo directory: $STEGASOO_DIR"
echo " Stegasoo user: $STEGASOO_USER"
if [ -n "$STEGASOO_DIR" ] && [ -f "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" ]; then
# Install the profile.d hook
cp "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" /etc/profile.d/stegasoo-wizard.sh
- chmod 755 /etc/profile.d/stegasoo-wizard.sh
- echo " Installed first-boot wizard hook"
+ chmod 644 /etc/profile.d/stegasoo-wizard.sh
+ echo " Installed wizard hook to /etc/profile.d/"
# Create the first-boot flag
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)
cat > /etc/systemd/system/stegasoo.service </dev/null || true
+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
echo " Logs cleared"
-echo -e "${GREEN}[7/9]${NC} Clearing temporary files..."
-rm -rf /tmp/*
-rm -rf /var/tmp/*
+# =============================================================================
+# Step 8: Temporary Files
+# =============================================================================
+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 -e "${GREEN}[8/9]${NC} Clearing package cache..."
-apt-get clean
-rm -rf /var/cache/apt/archives/*
+# =============================================================================
+# Step 9: Package Cache
+# =============================================================================
+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 -e "${GREEN}[9/9]${NC} Final cleanup..."
-# Remove this script's evidence
-rm -f /root/.bash_history
+# =============================================================================
+# Step 10: Final Sync
+# =============================================================================
+echo -e "${GREEN}[10/10]${NC} Final sync..."
+rm -f /root/.bash_history 2>/dev/null || true
sync
+echo " Filesystem synced"
+# =============================================================================
+# Validation
+# =============================================================================
echo ""
-echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}"
-echo -e "${GREEN}║ Sanitization Complete! ║${NC}"
-echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}"
-echo ""
-echo "The system is ready for imaging."
-echo ""
-echo -e "${YELLOW}Next steps:${NC}"
-echo " 1. Shut down: sudo shutdown -h now"
-echo " 2. Remove SD card"
-echo " 3. On another machine, copy with:"
-echo " sudo dd if=/dev/sdX of=stegasoo-rpi.img bs=4M status=progress"
-echo " 4. Compress: xz -9 stegasoo-rpi.img"
-echo ""
-read -p "Shut down now? [y/N] " -n 1 -r
-echo
-if [[ $REPLY =~ ^[Yy]$ ]]; then
- shutdown -h now
+echo -e "${CYAN}Validating sanitization...${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 ""
+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 ""
+ echo -e "${YELLOW}Next steps:${NC}"
+ echo " 1. Shut down: sudo shutdown -h now"
+ echo " 2. Remove SD card"
+ echo " 3. On another machine, copy with:"
+ echo " sudo dd if=/dev/sdX of=stegasoo-rpi.img bs=4M status=progress"
+ echo " 4. Compress: zstd -19 stegasoo-rpi.img"
+ echo ""
+ read -p "Shut down now? [y/N] " -n 1 -r
+ echo
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ shutdown -h now
+ fi
fi
diff --git a/src/stegasoo/COMPLETE_CHANGE_SUMMARY_V3.2.0.md b/src/stegasoo/COMPLETE_CHANGE_SUMMARY_V3.2.0.md
deleted file mode 100644
index 28e29a3..0000000
--- a/src/stegasoo/COMPLETE_CHANGE_SUMMARY_V3.2.0.md
+++ /dev/null
@@ -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
diff --git a/tests/RELEASE_CHECKLIST_V4_0_0.md b/tests/RELEASE_CHECKLIST_V4_0_0.md
deleted file mode 100644
index b494e09..0000000
--- a/tests/RELEASE_CHECKLIST_V4_0_0.md
+++ /dev/null
@@ -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:** _________________