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
|
||||
frontends/web/instance/
|
||||
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
|
||||
|
||||
Use rpi-imager with these settings:
|
||||
- **OS**: Raspberry Pi OS (64-bit)
|
||||
- **OS**: Raspberry Pi OS Lite (64-bit)
|
||||
- **Hostname**: `stegasoo`
|
||||
- **Enable SSH**: Yes (password auth)
|
||||
- **Username**: `pi` (or any)
|
||||
- **Password**: `raspberry` (temporary)
|
||||
- **WiFi**: Skip (use ethernet for clean image)
|
||||
- **Username**: `admin`
|
||||
- **Password**: `stegasoo`
|
||||
- **WiFi**: Configure for your network (sanitize script removes it later)
|
||||
|
||||
## Step 2: Boot & SSH In
|
||||
|
||||
@@ -43,17 +43,22 @@ curl -k https://localhost:5000
|
||||
## Step 5: Sanitize for Distribution
|
||||
|
||||
```bash
|
||||
curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash
|
||||
# Full sanitize (for final image - removes WiFi, shuts down)
|
||||
sudo ~/stegasoo/rpi/sanitize-for-image.sh
|
||||
|
||||
# Or soft reset (for testing - keeps WiFi, reboots)
|
||||
sudo ~/stegasoo/rpi/sanitize-for-image.sh --soft
|
||||
```
|
||||
|
||||
This removes:
|
||||
- WiFi credentials
|
||||
- WiFi credentials (unless `--soft`)
|
||||
- SSH host keys (regenerate on boot)
|
||||
- SSH authorized keys
|
||||
- Bash history
|
||||
- Stegasoo auth database
|
||||
- Logs and temp files
|
||||
|
||||
The Pi will shut down when complete.
|
||||
The script validates all cleanup steps before finishing.
|
||||
|
||||
## Step 6: Copy the Image
|
||||
|
||||
@@ -75,18 +80,18 @@ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
|
||||
chmod +x pishrink.sh
|
||||
sudo ./pishrink.sh stegasoo-rpi-*.img
|
||||
|
||||
# Compress
|
||||
xz -9 -T0 stegasoo-rpi-*.img
|
||||
# Compress (zstd is faster than xz with similar ratio)
|
||||
zstd -19 -T0 stegasoo-rpi-*.img
|
||||
```
|
||||
|
||||
## Step 8: Distribute
|
||||
|
||||
Upload `.img.xz` to GitHub Releases.
|
||||
Upload `.img.zst` to GitHub Releases.
|
||||
|
||||
Users can flash with:
|
||||
```bash
|
||||
# Linux
|
||||
xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
|
||||
# Or use rpi-imager "Use custom" option
|
||||
```
|
||||
@@ -100,9 +105,9 @@ xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/setup.sh | bash
|
||||
sudo systemctl start stegasoo
|
||||
curl -k https://localhost:5000
|
||||
curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash
|
||||
sudo ~/stegasoo/rpi/sanitize-for-image.sh
|
||||
|
||||
# On your machine:
|
||||
sudo dd if=/dev/sdX of=stegasoo-rpi-$(date +%Y%m%d).img bs=4M status=progress
|
||||
xz -9 -T0 stegasoo-rpi-*.img
|
||||
zstd -19 -T0 stegasoo-rpi-*.img
|
||||
```
|
||||
|
||||
@@ -30,11 +30,21 @@ chmod +x setup.sh
|
||||
## Requirements
|
||||
|
||||
- Raspberry Pi 4 or 5
|
||||
- Raspberry Pi OS (64-bit) - Bookworm or later
|
||||
- Raspberry Pi OS Lite (64-bit) - Bookworm or later
|
||||
- 4GB+ RAM recommended (2GB minimum)
|
||||
- ~2GB free disk space
|
||||
- Internet connection
|
||||
|
||||
## Pre-built Image Defaults
|
||||
|
||||
If using a pre-built image from GitHub Releases:
|
||||
|
||||
- **Default login**: `admin` / `stegasoo`
|
||||
- **Hostname**: `stegasoo.local`
|
||||
- **First boot**: A setup wizard runs on first SSH login
|
||||
|
||||
> **Security note**: Change the default password after setup with `passwd`
|
||||
|
||||
## After Installation
|
||||
|
||||
### Start the Service
|
||||
@@ -134,17 +144,23 @@ curl -k https://localhost:5000 # Should return HTML
|
||||
### 4. Sanitize for Distribution
|
||||
|
||||
```bash
|
||||
# Download and run sanitize script
|
||||
curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash
|
||||
# Full sanitize (removes WiFi, shuts down for imaging)
|
||||
sudo ~/stegasoo/rpi/sanitize-for-image.sh
|
||||
|
||||
# Or soft reset (keeps WiFi for testing, reboots)
|
||||
sudo ~/stegasoo/rpi/sanitize-for-image.sh --soft
|
||||
```
|
||||
|
||||
This removes:
|
||||
- WiFi credentials
|
||||
- WiFi credentials (unless `--soft`)
|
||||
- SSH host keys (regenerate on boot)
|
||||
- SSH authorized keys
|
||||
- Bash history
|
||||
- Stegasoo auth database (users create their own admin)
|
||||
- Logs and temp files
|
||||
|
||||
The script validates cleanup and reports any issues.
|
||||
|
||||
### 5. Create the Image
|
||||
|
||||
After Pi shuts down, remove SD card and on another Linux machine:
|
||||
@@ -161,17 +177,17 @@ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
|
||||
chmod +x pishrink.sh
|
||||
sudo ./pishrink.sh stegasoo-rpi-*.img
|
||||
|
||||
# Compress
|
||||
xz -9 -T0 stegasoo-rpi-*.img
|
||||
# Compress (zstd is faster than xz with similar compression)
|
||||
zstd -19 -T0 stegasoo-rpi-*.img
|
||||
```
|
||||
|
||||
### 6. Distribute
|
||||
|
||||
Upload the `.img.xz` file to GitHub Releases.
|
||||
Upload the `.img.zst` file to GitHub Releases.
|
||||
|
||||
Users flash with:
|
||||
```bash
|
||||
xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress
|
||||
```
|
||||
|
||||
Or use rpi-imager's "Use custom" option.
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
# Run this BEFORE creating an image with dd
|
||||
#
|
||||
# This script removes:
|
||||
# - WiFi credentials
|
||||
# - WiFi credentials (unless --soft)
|
||||
# - SSH host keys (will regenerate on boot)
|
||||
# - SSH authorized keys
|
||||
# - User-specific data
|
||||
# - Bash history
|
||||
# - Logs
|
||||
# - Stegasoo auth database (users will create their own admin)
|
||||
#
|
||||
# Usage: sudo ./sanitize-for-image.sh
|
||||
# Usage:
|
||||
# sudo ./sanitize-for-image.sh # Full sanitize for image distribution
|
||||
# sudo ./sanitize-for-image.sh --soft # Soft reset (keeps WiFi for testing)
|
||||
#
|
||||
|
||||
set -e
|
||||
@@ -19,21 +22,38 @@ set -e
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
SOFT_RESET=false
|
||||
if [ "$1" = "--soft" ] || [ "$1" = "-s" ]; then
|
||||
SOFT_RESET=true
|
||||
fi
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}Error: Must run as root (sudo)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}"
|
||||
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Sanitize Pi for Image Distribution ║"
|
||||
echo "║ ║"
|
||||
echo "║ This will remove personal data and prepare for imaging. ║"
|
||||
echo "║ The system will shut down when complete. ║"
|
||||
echo "╚═══════════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
if [ "$SOFT_RESET" = true ]; then
|
||||
echo -e "${CYAN}"
|
||||
echo "+-----------------------------------------------------------------+"
|
||||
echo "| Soft Reset (Factory Defaults) |"
|
||||
echo "| |"
|
||||
echo "| WiFi credentials will be KEPT for continued testing. |"
|
||||
echo "| Everything else will be reset to first-boot state. |"
|
||||
echo "+-----------------------------------------------------------------+"
|
||||
echo -e "${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}"
|
||||
echo "+-----------------------------------------------------------------+"
|
||||
echo "| Sanitize Pi for Image Distribution |"
|
||||
echo "| |"
|
||||
echo "| This will remove ALL personal data for imaging. |"
|
||||
echo "| The system will shut down when complete. |"
|
||||
echo "+-----------------------------------------------------------------+"
|
||||
echo -e "${NC}"
|
||||
fi
|
||||
|
||||
read -p "Continue? This cannot be undone! [y/N] " -n 1 -r
|
||||
echo
|
||||
@@ -42,9 +62,21 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[1/9]${NC} Removing WiFi credentials..."
|
||||
if [ -f /etc/wpa_supplicant/wpa_supplicant.conf ]; then
|
||||
cat > /etc/wpa_supplicant/wpa_supplicant.conf << 'EOF'
|
||||
# Track validation results
|
||||
VALIDATION_ERRORS=0
|
||||
|
||||
# =============================================================================
|
||||
# Step 1: WiFi Credentials
|
||||
# =============================================================================
|
||||
if [ "$SOFT_RESET" = true ]; then
|
||||
echo -e "${GREEN}[1/10]${NC} Keeping WiFi credentials (soft reset)..."
|
||||
echo " WiFi config preserved"
|
||||
else
|
||||
echo -e "${GREEN}[1/10]${NC} Removing WiFi credentials..."
|
||||
|
||||
# Remove from rootfs
|
||||
if [ -f /etc/wpa_supplicant/wpa_supplicant.conf ]; then
|
||||
cat > /etc/wpa_supplicant/wpa_supplicant.conf << 'EOF'
|
||||
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
|
||||
update_config=1
|
||||
country=US
|
||||
@@ -55,12 +87,22 @@ country=US
|
||||
# psk="YourPassword"
|
||||
# }
|
||||
EOF
|
||||
echo " WiFi credentials cleared"
|
||||
else
|
||||
echo " No wpa_supplicant.conf found"
|
||||
echo " Cleared /etc/wpa_supplicant/wpa_supplicant.conf"
|
||||
fi
|
||||
|
||||
# Remove from boot partition (headless setup file)
|
||||
BOOT_PART=$(findmnt -n -o SOURCE /boot/firmware 2>/dev/null || findmnt -n -o SOURCE /boot 2>/dev/null || echo "")
|
||||
if [ -n "$BOOT_PART" ]; then
|
||||
BOOT_MOUNT=$(findmnt -n -o TARGET "$BOOT_PART" 2>/dev/null || echo "/boot")
|
||||
rm -f "$BOOT_MOUNT/wpa_supplicant.conf" 2>/dev/null || true
|
||||
echo " Removed boot partition WiFi config"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[2/9]${NC} Removing SSH authorized keys..."
|
||||
# =============================================================================
|
||||
# Step 2: SSH Authorized Keys
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[2/10]${NC} Removing SSH authorized keys..."
|
||||
for user_home in /home/*; do
|
||||
if [ -d "$user_home/.ssh" ]; then
|
||||
rm -f "$user_home/.ssh/authorized_keys"
|
||||
@@ -70,15 +112,28 @@ for user_home in /home/*; do
|
||||
done
|
||||
rm -f /root/.ssh/authorized_keys /root/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}[3/9]${NC} Clearing bash history..."
|
||||
# =============================================================================
|
||||
# Step 3: SSH Host Keys
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[3/10]${NC} Removing SSH host keys (will regenerate on first boot)..."
|
||||
rm -f /etc/ssh/ssh_host_*
|
||||
echo " SSH host keys removed"
|
||||
|
||||
# =============================================================================
|
||||
# Step 4: Bash History
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[4/10]${NC} Clearing bash history..."
|
||||
for user_home in /home/*; do
|
||||
rm -f "$user_home/.bash_history"
|
||||
rm -f "$user_home/.python_history"
|
||||
done
|
||||
rm -f /root/.bash_history /root/.python_history 2>/dev/null || true
|
||||
history -c
|
||||
history -c 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}[4/9]${NC} Removing Stegasoo user data..."
|
||||
# =============================================================================
|
||||
# Step 5: Stegasoo User Data
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[5/10]${NC} Removing Stegasoo user data..."
|
||||
# Remove auth database (users create their own admin on first run)
|
||||
rm -rf /home/*/stegasoo/frontends/web/instance/
|
||||
# Remove SSL certs (will be regenerated)
|
||||
@@ -87,35 +142,36 @@ rm -rf /home/*/stegasoo/frontends/web/certs/
|
||||
rm -f /home/*/stegasoo/frontends/web/.env
|
||||
echo " Stegasoo instance data cleared"
|
||||
|
||||
echo -e "${GREEN}[5/9]${NC} Setting up first-boot wizard..."
|
||||
# =============================================================================
|
||||
# Step 6: First-Boot Wizard Setup
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[6/10]${NC} Setting up first-boot wizard..."
|
||||
|
||||
# Find stegasoo install directory
|
||||
STEGASOO_DIR=$(ls -d /home/*/stegasoo 2>/dev/null | head -1)
|
||||
echo " Looking for stegasoo in: $STEGASOO_DIR"
|
||||
|
||||
if [ -z "$STEGASOO_DIR" ]; then
|
||||
echo -e " ${RED}ERROR: Could not find stegasoo directory in /home/*/stegasoo${NC}"
|
||||
echo " Checking common locations..."
|
||||
for dir in /home/*/stegasoo /root/stegasoo /opt/stegasoo; do
|
||||
for dir in /root/stegasoo /opt/stegasoo; do
|
||||
if [ -d "$dir" ]; then
|
||||
STEGASOO_DIR="$dir"
|
||||
echo " Found at: $STEGASOO_DIR"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
STEGASOO_USER=$(stat -c '%U' "$STEGASOO_DIR" 2>/dev/null || echo "pi")
|
||||
echo " Stegasoo directory: $STEGASOO_DIR"
|
||||
echo " Stegasoo user: $STEGASOO_USER"
|
||||
|
||||
if [ -n "$STEGASOO_DIR" ] && [ -f "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" ]; then
|
||||
# Install the profile.d hook
|
||||
cp "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" /etc/profile.d/stegasoo-wizard.sh
|
||||
chmod 755 /etc/profile.d/stegasoo-wizard.sh
|
||||
echo " Installed first-boot wizard hook"
|
||||
chmod 644 /etc/profile.d/stegasoo-wizard.sh
|
||||
echo " Installed wizard hook to /etc/profile.d/"
|
||||
|
||||
# Create the first-boot flag
|
||||
touch /etc/stegasoo-first-boot
|
||||
echo " Created first-boot flag"
|
||||
echo " Created /etc/stegasoo-first-boot flag"
|
||||
|
||||
# Reset systemd service to defaults (wizard will reconfigure)
|
||||
cat > /etc/systemd/system/stegasoo.service <<EOF
|
||||
@@ -140,64 +196,153 @@ RestartSec=5
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
echo " Reset 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
|
||||
echo -e " ${RED}✗ Wizard files missing after setup!${NC}"
|
||||
fi
|
||||
echo " Reset systemd service to defaults"
|
||||
else
|
||||
echo -e " ${RED}ERROR: Could not set up wizard${NC}"
|
||||
echo -e " ${RED}ERROR: Could not find wizard script${NC}"
|
||||
echo " STEGASOO_DIR: $STEGASOO_DIR"
|
||||
echo " Wizard script exists: $([ -f "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" ] && echo 'yes' || echo 'NO')"
|
||||
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/"
|
||||
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}[6/9]${NC} Clearing logs..."
|
||||
journalctl --rotate
|
||||
journalctl --vacuum-time=1s
|
||||
rm -rf /var/log/*.log /var/log/*.gz /var/log/*.[0-9]
|
||||
rm -rf /var/log/apt/*
|
||||
rm -rf /var/log/journal/*
|
||||
# =============================================================================
|
||||
# Step 7: Logs
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[7/10]${NC} Clearing logs..."
|
||||
journalctl --rotate 2>/dev/null || true
|
||||
journalctl --vacuum-time=1s 2>/dev/null || true
|
||||
rm -rf /var/log/*.log /var/log/*.gz /var/log/*.[0-9] 2>/dev/null || true
|
||||
rm -rf /var/log/apt/* 2>/dev/null || true
|
||||
rm -rf /var/log/journal/* 2>/dev/null || true
|
||||
find /var/log -type f -name "*.log" -delete 2>/dev/null || true
|
||||
echo " Logs cleared"
|
||||
|
||||
echo -e "${GREEN}[7/9]${NC} Clearing temporary files..."
|
||||
rm -rf /tmp/*
|
||||
rm -rf /var/tmp/*
|
||||
# =============================================================================
|
||||
# Step 8: Temporary Files
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[8/10]${NC} Clearing temporary files..."
|
||||
rm -rf /tmp/* 2>/dev/null || true
|
||||
rm -rf /var/tmp/* 2>/dev/null || true
|
||||
echo " Temp files cleared"
|
||||
|
||||
echo -e "${GREEN}[8/9]${NC} Clearing package cache..."
|
||||
apt-get clean
|
||||
rm -rf /var/cache/apt/archives/*
|
||||
# =============================================================================
|
||||
# Step 9: Package Cache
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[9/10]${NC} Clearing package cache..."
|
||||
apt-get clean 2>/dev/null || true
|
||||
rm -rf /var/cache/apt/archives/* 2>/dev/null || true
|
||||
echo " Package cache cleared"
|
||||
|
||||
echo -e "${GREEN}[9/9]${NC} Final cleanup..."
|
||||
# Remove this script's evidence
|
||||
rm -f /root/.bash_history
|
||||
# =============================================================================
|
||||
# Step 10: Final Sync
|
||||
# =============================================================================
|
||||
echo -e "${GREEN}[10/10]${NC} Final sync..."
|
||||
rm -f /root/.bash_history 2>/dev/null || true
|
||||
sync
|
||||
echo " Filesystem synced"
|
||||
|
||||
# =============================================================================
|
||||
# Validation
|
||||
# =============================================================================
|
||||
echo ""
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ Sanitization Complete! ║${NC}"
|
||||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo "The system is ready for imaging."
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next steps:${NC}"
|
||||
echo " 1. Shut down: sudo shutdown -h now"
|
||||
echo " 2. Remove SD card"
|
||||
echo " 3. On another machine, copy with:"
|
||||
echo " sudo dd if=/dev/sdX of=stegasoo-rpi.img bs=4M status=progress"
|
||||
echo " 4. Compress: xz -9 stegasoo-rpi.img"
|
||||
echo ""
|
||||
read -p "Shut down now? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
shutdown -h now
|
||||
echo -e "${CYAN}Validating sanitization...${NC}"
|
||||
|
||||
# Check first-boot flag
|
||||
if [ -f /etc/stegasoo-first-boot ]; then
|
||||
echo -e " ${GREEN}[PASS]${NC} First-boot flag exists"
|
||||
else
|
||||
echo -e " ${RED}[FAIL]${NC} First-boot flag missing"
|
||||
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
|
||||
fi
|
||||
|
||||
# Check profile.d hook
|
||||
if [ -f /etc/profile.d/stegasoo-wizard.sh ]; then
|
||||
echo -e " ${GREEN}[PASS]${NC} Wizard hook installed"
|
||||
else
|
||||
echo -e " ${RED}[FAIL]${NC} Wizard hook missing"
|
||||
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
|
||||
fi
|
||||
|
||||
# Check SSH host keys removed
|
||||
if ls /etc/ssh/ssh_host_* 1>/dev/null 2>&1; then
|
||||
echo -e " ${RED}[FAIL]${NC} SSH host keys still present"
|
||||
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
|
||||
else
|
||||
echo -e " ${GREEN}[PASS]${NC} SSH host keys removed"
|
||||
fi
|
||||
|
||||
# Check Stegasoo instance data removed
|
||||
if ls /home/*/stegasoo/frontends/web/instance/*.db 1>/dev/null 2>&1; then
|
||||
echo -e " ${RED}[FAIL]${NC} Stegasoo database still present"
|
||||
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
|
||||
else
|
||||
echo -e " ${GREEN}[PASS]${NC} Stegasoo database removed"
|
||||
fi
|
||||
|
||||
# Check WiFi (only for full sanitize)
|
||||
if [ "$SOFT_RESET" = false ]; then
|
||||
if grep -q "psk=" /etc/wpa_supplicant/wpa_supplicant.conf 2>/dev/null; then
|
||||
echo -e " ${RED}[FAIL]${NC} WiFi credentials still present"
|
||||
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
|
||||
else
|
||||
echo -e " ${GREEN}[PASS]${NC} WiFi credentials cleared"
|
||||
fi
|
||||
else
|
||||
echo -e " ${YELLOW}[SKIP]${NC} WiFi check (soft reset mode)"
|
||||
fi
|
||||
|
||||
# Check authorized_keys removed
|
||||
AUTH_KEYS_FOUND=false
|
||||
for user_home in /home/*; do
|
||||
if [ -f "$user_home/.ssh/authorized_keys" ]; then
|
||||
AUTH_KEYS_FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$AUTH_KEYS_FOUND" = true ]; then
|
||||
echo -e " ${RED}[FAIL]${NC} SSH authorized_keys still present"
|
||||
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
|
||||
else
|
||||
echo -e " ${GREEN}[PASS]${NC} SSH authorized_keys removed"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Summary
|
||||
# =============================================================================
|
||||
echo ""
|
||||
if [ $VALIDATION_ERRORS -eq 0 ]; then
|
||||
echo -e "${GREEN}+-----------------------------------------------------------------+${NC}"
|
||||
echo -e "${GREEN}| Sanitization Complete! |${NC}"
|
||||
echo -e "${GREEN}| All validation checks passed. |${NC}"
|
||||
echo -e "${GREEN}+-----------------------------------------------------------------+${NC}"
|
||||
else
|
||||
echo -e "${RED}+-----------------------------------------------------------------+${NC}"
|
||||
echo -e "${RED}| Sanitization Complete with Errors |${NC}"
|
||||
echo -e "${RED}| $VALIDATION_ERRORS validation check(s) failed |${NC}"
|
||||
echo -e "${RED}+-----------------------------------------------------------------+${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [ "$SOFT_RESET" = true ]; then
|
||||
echo -e "${CYAN}Soft reset complete.${NC}"
|
||||
echo "You can now reboot to test the first-boot wizard."
|
||||
echo ""
|
||||
read -p "Reboot now? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
reboot
|
||||
fi
|
||||
else
|
||||
echo "The system is ready for imaging."
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next steps:${NC}"
|
||||
echo " 1. Shut down: sudo shutdown -h now"
|
||||
echo " 2. Remove SD card"
|
||||
echo " 3. On another machine, copy with:"
|
||||
echo " sudo dd if=/dev/sdX of=stegasoo-rpi.img bs=4M status=progress"
|
||||
echo " 4. Compress: zstd -19 stegasoo-rpi.img"
|
||||
echo ""
|
||||
read -p "Shut down now? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
shutdown -h now
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -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