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:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
170
check_scipy.py
170
check_scipy.py
@@ -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)")
|
|
||||||
@@ -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!
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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:** _________________
|
|
||||||
Reference in New Issue
Block a user