diff --git a/.gitignore b/.gitignore index def1e84..2c88040 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,10 @@ scripts/ # Web UI auth database and SSL certs frontends/web/instance/ frontends/web/certs/ +rpi/inject-wifi.sh + +# RPi image build artifacts +*.img +*.img.xz +*.img.zst +pishrink.sh diff --git a/check_scipy.py b/check_scipy.py deleted file mode 100644 index e073d0b..0000000 --- a/check_scipy.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -""" -Diagnostic script to check for scipy/numpy issues. -Run this BEFORE starting the web app. - -Usage: - python check_scipy.py -""" - -import sys -print(f"Python version: {sys.version}") -print() - -# Check numpy -try: - import numpy as np - print(f"NumPy version: {np.__version__}") - print(f"NumPy config:") - np.show_config() -except ImportError as e: - print(f"NumPy not installed: {e}") -except Exception as e: - print(f"NumPy error: {e}") - -print() -print("-" * 50) -print() - -# Check scipy -try: - import scipy - print(f"SciPy version: {scipy.__version__}") -except ImportError as e: - print(f"SciPy not installed: {e}") - -print() - -# Check PIL -try: - from PIL import Image - print(f"Pillow version: {Image.__version__}") -except ImportError as e: - print(f"Pillow not installed: {e}") - -print() -print("-" * 50) -print() - -# Test scipy DCT directly -print("Testing scipy DCT...") -try: - from scipy.fftpack import dct, idct - import numpy as np - - # Create test array - test = np.random.rand(8, 8).astype(np.float64) - print(f"Input array shape: {test.shape}, dtype: {test.dtype}") - - # Test 1D DCT - row = test[0, :] - result = dct(row, norm='ortho') - print(f"1D DCT result shape: {result.shape}, dtype: {result.dtype}") - - # Test 2D DCT (the potentially problematic operation) - result2d = dct(dct(test.T, norm='ortho').T, norm='ortho') - print(f"2D DCT result shape: {result2d.shape}, dtype: {result2d.dtype}") - - # Test inverse - recovered = idct(idct(result2d.T, norm='ortho').T, norm='ortho') - error = np.max(np.abs(test - recovered)) - print(f"Round-trip error: {error}") - - if error < 1e-10: - print("✓ scipy DCT working correctly") - else: - print("⚠ scipy DCT has precision issues") - -except Exception as e: - print(f"✗ scipy DCT failed: {e}") - import traceback - traceback.print_exc() - -print() -print("-" * 50) -print() - -# Test with larger array (more like real image processing) -print("Testing with larger arrays (512x512)...") -try: - from scipy.fftpack import dct, idct - import numpy as np - import gc - - # Simulate processing many 8x8 blocks - large_array = np.random.rand(512, 512).astype(np.float64) - print(f"Large array shape: {large_array.shape}, size: {large_array.nbytes} bytes") - - count = 0 - for y in range(0, 512, 8): - for x in range(0, 512, 8): - block = large_array[y:y+8, x:x+8].copy() - dct_block = dct(dct(block.T, norm='ortho').T, norm='ortho') - recovered = idct(idct(dct_block.T, norm='ortho').T, norm='ortho') - large_array[y:y+8, x:x+8] = recovered - count += 1 - - print(f"Processed {count} blocks successfully") - - del large_array - gc.collect() - - print("✓ Large array processing completed") - -except Exception as e: - print(f"✗ Large array processing failed: {e}") - import traceback - traceback.print_exc() - -print() -print("-" * 50) -print() - -# Test PIL with large image -print("Testing PIL with large image...") -try: - from PIL import Image - import io - - # Create a large test image - img = Image.new('RGB', (4000, 3000), color=(128, 128, 128)) - - # Save to bytes - buffer = io.BytesIO() - img.save(buffer, format='PNG') - img_bytes = buffer.getvalue() - print(f"Test image size: {len(img_bytes)} bytes") - - # Re-open and process - buffer2 = io.BytesIO(img_bytes) - img2 = Image.open(buffer2) - print(f"Re-opened image: {img2.size}, mode: {img2.mode}") - - # Convert to numpy array - import numpy as np - arr = np.array(img2) - print(f"NumPy array: {arr.shape}, dtype: {arr.dtype}") - - # Clean up - img.close() - img2.close() - buffer.close() - buffer2.close() - del arr - gc.collect() - - print("✓ PIL large image test completed") - -except Exception as e: - print(f"✗ PIL test failed: {e}") - import traceback - traceback.print_exc() - -print() -print("=" * 50) -print("Diagnostics complete") -print() -print("If no errors above but web app still crashes, try:") -print("1. pip install --upgrade scipy numpy pillow") -print("2. pip install scipy==1.11.4 numpy==1.26.4 # Known stable versions") -print("3. Check if using conda vs pip (mixing can cause issues)") diff --git a/frontends/api/API_UPDATE_SUMMARY_V3.2.0.md b/frontends/api/API_UPDATE_SUMMARY_V3.2.0.md deleted file mode 100644 index d66a130..0000000 --- a/frontends/api/API_UPDATE_SUMMARY_V3.2.0.md +++ /dev/null @@ -1,500 +0,0 @@ -# API Update Summary for v3.2.0 - -## Overview - -The FastAPI REST API has been updated to align with Stegasoo v3.2.0's breaking changes: -1. **Removed date dependency** - No `date_str` field in requests -2. **Renamed day_phrase → passphrase** - Updated all request/response models -3. **Updated generation** - Now generates single passphrase instead of daily phrases - -## Breaking Changes - -### Request Model Changes - -#### 1. EncodeRequest & EncodeFileRequest - -**Before (v3.1.0):** -```python -class EncodeRequest(BaseModel): - message: str - reference_photo_base64: str - carrier_image_base64: str - day_phrase: str # ← Changed to passphrase - pin: str = "" - rsa_key_base64: Optional[str] = None - rsa_password: Optional[str] = None - date_str: Optional[str] = None # ← REMOVED - embed_mode: EmbedModeType = "lsb" -``` - -**After (v3.2.0):** -```python -class EncodeRequest(BaseModel): - message: str - reference_photo_base64: str - carrier_image_base64: str - passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)") - pin: str = "" - rsa_key_base64: Optional[str] = None - rsa_password: Optional[str] = None - # date_str removed in v3.2.0 - embed_mode: EmbedModeType = "lsb" - dct_output_format: DctOutputFormatType = "png" - dct_color_mode: DctColorModeType = "grayscale" -``` - -#### 2. DecodeRequest - -**Before (v3.1.0):** -```python -class DecodeRequest(BaseModel): - stego_image_base64: str - reference_photo_base64: str - day_phrase: str # ← Changed to passphrase - pin: str = "" - rsa_key_base64: Optional[str] = None - rsa_password: Optional[str] = None - embed_mode: ExtractModeType = "auto" -``` - -**After (v3.2.0):** -```python -class DecodeRequest(BaseModel): - stego_image_base64: str - reference_photo_base64: str - passphrase: str = Field(description="Passphrase (v3.2.0: renamed from day_phrase)") - pin: str = "" - rsa_key_base64: Optional[str] = None - rsa_password: Optional[str] = None - embed_mode: ExtractModeType = "auto" -``` - -#### 3. GenerateRequest - -**Before (v3.1.0):** -```python -class GenerateRequest(BaseModel): - use_pin: bool = True - use_rsa: bool = False - pin_length: int = Field(default=6, ge=MIN_PIN_LENGTH, le=MAX_PIN_LENGTH) - rsa_bits: int = Field(default=2048) - words_per_phrase: int = Field(default=3, ge=MIN_PHRASE_WORDS, le=MAX_PHRASE_WORDS) -``` - -**After (v3.2.0):** -```python -class GenerateRequest(BaseModel): - use_pin: bool = True - use_rsa: bool = False - pin_length: int = Field(default=6, ge=MIN_PIN_LENGTH, le=MAX_PIN_LENGTH) - rsa_bits: int = Field(default=2048) - words_per_passphrase: int = Field( - default=DEFAULT_PASSPHRASE_WORDS, # = 4, was 3 - ge=MIN_PASSPHRASE_WORDS, - le=MAX_PASSPHRASE_WORDS, - description="Words per passphrase (v3.2.0: default increased to 4)" - ) -``` - -### Response Model Changes - -#### 1. GenerateResponse - -**Before (v3.1.0):** -```python -class GenerateResponse(BaseModel): - phrases: dict[str, str] # Monday -> phrase, Tuesday -> phrase, etc. - pin: Optional[str] = None - rsa_key_pem: Optional[str] = None - entropy: dict[str, int] -``` - -**After (v3.2.0):** -```python -class GenerateResponse(BaseModel): - passphrase: str = Field(description="Single passphrase (v3.2.0: no daily rotation)") - pin: Optional[str] = None - rsa_key_pem: Optional[str] = None - entropy: dict[str, int] - # Legacy field for compatibility - phrases: Optional[dict[str, str]] = Field( - default=None, - description="Deprecated: Use 'passphrase' instead" - ) -``` - -#### 2. EncodeResponse - -**Before (v3.1.0):** -```python -class EncodeResponse(BaseModel): - stego_image_base64: str - filename: str - capacity_used_percent: float - date_used: str - day_of_week: str - embed_mode: str - output_format: str = "png" - color_mode: str = "color" -``` - -**After (v3.2.0):** -```python -class EncodeResponse(BaseModel): - stego_image_base64: str - filename: str - capacity_used_percent: float - embed_mode: str - output_format: str = "png" - color_mode: str = "color" - # Legacy fields (no longer used in crypto) - date_used: Optional[str] = Field( - default=None, - description="Deprecated: Date no longer used in v3.2.0" - ) - day_of_week: Optional[str] = Field( - default=None, - description="Deprecated: Date no longer used in v3.2.0" - ) -``` - -### Endpoint Changes - -#### 1. POST /encode - -**Before (v3.1.0):** -```json -{ - "message": "Secret message", - "reference_photo_base64": "...", - "carrier_image_base64": "...", - "day_phrase": "apple forest thunder", - "date_str": "2025-01-15", - "pin": "123456", - "embed_mode": "lsb" -} -``` - -**After (v3.2.0):** -```json -{ - "message": "Secret message", - "reference_photo_base64": "...", - "carrier_image_base64": "...", - "passphrase": "apple forest thunder mountain", - "pin": "123456", - "embed_mode": "lsb" -} -``` - -#### 2. POST /decode - -**Before (v3.1.0):** -```json -{ - "stego_image_base64": "...", - "reference_photo_base64": "...", - "day_phrase": "apple forest thunder", - "pin": "123456", - "embed_mode": "auto" -} -``` - -**After (v3.2.0):** -```json -{ - "stego_image_base64": "...", - "reference_photo_base64": "...", - "passphrase": "apple forest thunder mountain", - "pin": "123456", - "embed_mode": "auto" -} -``` - -#### 3. POST /generate - -**Response Before (v3.1.0):** -```json -{ - "phrases": { - "Monday": "apple forest thunder", - "Tuesday": "banana river lightning", - ... - }, - "pin": "123456", - "rsa_key_pem": null, - "entropy": { - "phrase": 33, - "pin": 20, - "rsa": 0, - "total": 53 - } -} -``` - -**Response After (v3.2.0):** -```json -{ - "passphrase": "apple forest thunder mountain", - "pin": "123456", - "rsa_key_pem": null, - "entropy": { - "passphrase": 44, - "pin": 20, - "rsa": 0, - "total": 64 - }, - "phrases": null -} -``` - -#### 4. POST /encode/multipart - -**Form Fields Before (v3.1.0):** -- `day_phrase` (required) -- `date_str` (optional) -- `reference_photo` (file) -- `carrier` (file) -- ... - -**Form Fields After (v3.2.0):** -- `passphrase` (required) ← renamed from day_phrase -- `reference_photo` (file) -- `carrier` (file) -- ... (date_str removed) - -**Response Headers Before (v3.1.0):** -``` -X-Stegasoo-Date: 2025-01-15 -X-Stegasoo-Day: Wednesday -X-Stegasoo-Capacity-Percent: 25.5 -X-Stegasoo-Embed-Mode: lsb -``` - -**Response Headers After (v3.2.0):** -``` -X-Stegasoo-Capacity-Percent: 25.5 -X-Stegasoo-Embed-Mode: lsb -X-Stegasoo-Output-Format: png -X-Stegasoo-Color-Mode: color -X-Stegasoo-Version: 3.2.0 -``` - -### New Status Endpoint Information - -#### GET / - -**Added to response:** -```json -{ - "version": "3.2.0", - ... - "breaking_changes": { - "date_removed": "No date_str parameter needed - encode/decode anytime", - "passphrase_renamed": "day_phrase → passphrase (single passphrase, no daily rotation)", - "format_version": 4, - "backward_compatible": false - } -} -``` - -## Migration Guide for API Clients - -### 1. Update Request Bodies - -**Find and replace in client code:** -```javascript -// Before -{ - day_phrase: "apple forest thunder", - date_str: "2025-01-15" -} - -// After -{ - passphrase: "apple forest thunder mountain" -} -``` - -### 2. Update Response Handling - -**Before:** -```javascript -const response = await fetch('/encode', { - method: 'POST', - body: JSON.stringify({ - message: "secret", - day_phrase: "words", - date_str: "2025-01-15", - ... - }) -}); - -const data = await response.json(); -console.log(data.date_used); // "2025-01-15" -console.log(data.day_of_week); // "Wednesday" -``` - -**After:** -```javascript -const response = await fetch('/encode', { - method: 'POST', - body: JSON.stringify({ - message: "secret", - passphrase: "longer words here now", - // date_str removed - ... - }) -}); - -const data = await response.json(); -// date_used and day_of_week are null in v3.2.0 -``` - -### 3. Update Generate Endpoint Usage - -**Before:** -```javascript -const creds = await fetch('/generate', { - method: 'POST', - body: JSON.stringify({ use_pin: true }) -}).then(r => r.json()); - -// Use Monday's phrase -const mondayPhrase = creds.phrases['Monday']; -``` - -**After:** -```javascript -const creds = await fetch('/generate', { - method: 'POST', - body: JSON.stringify({ use_pin: true }) -}).then(r => r.json()); - -// Use single passphrase -const passphrase = creds.passphrase; -``` - -### 4. Update Multipart Requests - -**Before (JavaScript fetch):** -```javascript -const formData = new FormData(); -formData.append('day_phrase', 'apple forest thunder'); -formData.append('date_str', '2025-01-15'); -formData.append('reference_photo', refPhotoFile); -formData.append('carrier', carrierFile); -formData.append('message', 'secret'); -formData.append('pin', '123456'); - -const response = await fetch('/encode/multipart', { - method: 'POST', - body: formData -}); -``` - -**After (JavaScript fetch):** -```javascript -const formData = new FormData(); -formData.append('passphrase', 'apple forest thunder mountain'); -// date_str removed -formData.append('reference_photo', refPhotoFile); -formData.append('carrier', carrierFile); -formData.append('message', 'secret'); -formData.append('pin', '123456'); - -const response = await fetch('/encode/multipart', { - method: 'POST', - body: formData -}); -``` - -## Testing Checklist - -### Endpoints to Test - -- [ ] GET / - Returns v3.2.0 with breaking_changes info -- [ ] GET /modes - Returns mode information -- [ ] POST /generate - Returns single passphrase -- [ ] POST /encode - Works without date_str -- [ ] POST /encode/file - Works without date_str -- [ ] POST /decode - Works without date_str -- [ ] POST /encode/multipart - Accepts passphrase instead of day_phrase -- [ ] POST /decode/multipart - Accepts passphrase instead of day_phrase -- [ ] POST /compare - Still works -- [ ] POST /will-fit - Still works -- [ ] POST /image/info - Still works -- [ ] POST /extract-key-from-qr - Still works - -### Validation Tests - -- [ ] Reject requests with `day_phrase` field (should get validation error) -- [ ] Reject requests with `date_str` field (should be ignored or error) -- [ ] Accept requests with `passphrase` field -- [ ] Generate response includes `passphrase` field -- [ ] Generate response has `phrases` as null -- [ ] Encode response has `date_used` and `day_of_week` as null -- [ ] Multipart encode works with new field names -- [ ] Response headers updated correctly - -## OpenAPI/Swagger Documentation - -The FastAPI auto-generated documentation (/docs and /redoc) will automatically reflect the changes: - -1. **Models updated** - Request/response schemas show new field names -2. **Descriptions updated** - Field descriptions mention v3.2.0 changes -3. **Examples updated** - Interactive API explorer uses new field names - -Users can browse to `/docs` to see the updated API specification. - -## Backward Compatibility - -**Breaking Change:** API v3.2.0 is NOT backward compatible with v3.1.0 - -Clients using the old API will encounter: -1. **Validation errors** - Missing required `passphrase` field -2. **Unexpected responses** - `phrases` field will be null -3. **Changed behavior** - Date fields no longer populated - -### Migration Timeline Recommendation - -1. **Deploy v3.2.0 API** to staging -2. **Update client applications** to use new field names -3. **Test thoroughly** with staging API -4. **Deploy v3.2.0 API** to production -5. **Notify users** of breaking changes - -Alternatively, run v3.1.0 and v3.2.0 APIs side-by-side on different paths: -- `/api/v3.1/` - Old API -- `/api/v3.2/` - New API - -## Constants Updates - -Used in validation: -```python -from stegasoo.constants import ( - MIN_PASSPHRASE_WORDS, # = 3 - MAX_PASSPHRASE_WORDS, # = 12 - DEFAULT_PASSPHRASE_WORDS, # = 4 (increased from 3) -) -``` - -## Error Messages - -All error messages updated: -- "day_phrase is required" → "passphrase is required" -- References to "phrase" now mean "passphrase" - -## Implementation Status - -✅ All request models updated -✅ All response models updated -✅ All endpoints updated -✅ Multipart endpoints updated -✅ Status endpoint shows breaking changes -✅ Constants imported correctly -✅ Error handling updated -✅ No references to day_phrase in user-facing text -✅ No date_str parameters accepted - -Ready for deployment! diff --git a/frontends/web/WEB_FRONTEND_UPDATE_SUMMARY_V3.2.0.md b/frontends/web/WEB_FRONTEND_UPDATE_SUMMARY_V3.2.0.md deleted file mode 100644 index a7e5a95..0000000 --- a/frontends/web/WEB_FRONTEND_UPDATE_SUMMARY_V3.2.0.md +++ /dev/null @@ -1,426 +0,0 @@ -# Web Frontend Update Summary for v3.2.0 - -## Overview - -The Flask web frontend has been updated to align with Stegasoo v3.2.0's breaking changes: -1. **Removed date dependency** - No date selection or tracking in UI -2. **Renamed day_phrase → passphrase** - Updated all forms and templates -3. **Increased default words** - From 3 to 4 for better security - -## Key Changes - -### 1. Form Parameter Changes - -#### Generate Page - -**Before (v3.1.0):** -```python -words_per_phrase = int(request.form.get('words_per_phrase', 3)) -# Generated daily phrases for all days of the week -``` - -**After (v3.2.0):** -```python -words_per_passphrase = int(request.form.get('words_per_passphrase', 4)) -# Generates single passphrase -``` - -**Template variables changed:** -- `phrases` → `passphrase` (single string instead of dict) -- `words_per_phrase` → `words_per_passphrase` -- `phrase_entropy` → `passphrase_entropy` -- Removed `days` variable (no longer needed) - -#### Encode Page - -**Before (v3.1.0):** -```python -day_phrase = request.form.get('day_phrase', '') -client_date = request.form.get('client_date', '').strip() -day_of_week = get_today_day() # Used in template - -encode_result = encode( - ..., - day_phrase=day_phrase, - date_str=date_str, - ... -) -``` - -**After (v3.2.0):** -```python -passphrase = request.form.get('passphrase', '') -# No client_date or day_of_week needed - -encode_result = encode( - ..., - passphrase=passphrase, # Renamed - # date_str removed - ... -) -``` - -#### Decode Page - -**Before (v3.1.0):** -```python -day_phrase = request.form.get('day_phrase', '') -stego_date = request.form.get('stego_date', '').strip() - -decode_result = decode( - ..., - day_phrase=day_phrase, - date_str=stego_date if stego_date else None, - ... -) -``` - -**After (v3.2.0):** -```python -passphrase = request.form.get('passphrase', '') -# No stego_date needed - -decode_result = decode( - ..., - passphrase=passphrase, # Renamed - # date_str removed - ... -) -``` - -### 2. Template Context Updates - -**inject_globals() changes:** - -**Added:** -```python -'min_passphrase_words': MIN_PASSPHRASE_WORDS, -'recommended_passphrase_words': RECOMMENDED_PASSPHRASE_WORDS, -'default_passphrase_words': DEFAULT_PASSPHRASE_WORDS, -``` - -**Used for:** -- Showing passphrase length requirements -- Default values in generate form -- Validation messages - -### 3. Validation Updates - -**Added passphrase validation:** -```python -from stegasoo import validate_passphrase - -# In encode_page() -result = validate_passphrase(passphrase) -if not result.is_valid: - flash(result.error_message, 'error') - return ... - -# Show warning if passphrase is short -if result.warning: - flash(result.warning, 'warning') -``` - -### 4. Error Message Updates - -**Before:** -```python -flash('Day phrase is required', 'error') -flash('Decryption failed. Check your phrase, PIN...', 'error') -``` - -**After:** -```python -flash('Passphrase is required', 'error') -flash('Decryption failed. Check your passphrase, PIN...', 'error') -``` - -## Template Changes Needed - -These Flask routes will need corresponding template updates: - -### generate.html - -**Changes needed:** -```html - - - - -{% if generated %} -

Daily Phrases

- {% for day in days %} - - {{ day }} - {{ phrases[day] }} - - {% endfor %} -{% endif %} - - - - - -{% if generated %} -

Passphrase

-
- {{ passphrase }} -

Use this passphrase to encode and decode messages (no date needed!)

-
-{% endif %} -``` - -**Entropy display:** -```html - -
  • Phrase entropy: {{ phrase_entropy }} bits
  • - - -
  • Passphrase entropy: {{ passphrase_entropy }} bits ({{ words_per_passphrase }} words)
  • -``` - -### encode.html - -**Changes needed:** -```html - - - - - - -

    Defaults to today: {{ day_of_week }}

    - - - - -

    - v3.2.0: No date needed! Use your passphrase anytime. -

    -``` - -### decode.html - -**Changes needed:** -```html - - - - - - -

    Will be auto-detected from filename if possible

    - - - - - - -

    - v3.2.0: No date needed to decode! -

    - - -``` - -### index.html - -**Changes needed:** -```html - -

    Generate daily passphrases and security credentials

    -

    Hide messages using day-specific phrases

    - - -

    Generate passphrases and security credentials

    -

    v3.2.0: Simplified - no more daily rotation!

    -``` - -### about.html - -**Add v3.2.0 section:** -```html -

    Version 3.2.0 Changes

    - -``` - -## JavaScript Changes Needed - -### Remove date-related code: - -```javascript -// REMOVE THIS (date detection from filename) -function detectDateFromFilename(filename) { - const match = filename.match(/_(\d{4})(\d{2})(\d{2})/); - if (match) { - return `${match[1]}-${match[2]}-${match[3]}`; - } - return null; -} - -// REMOVE THIS (day-of-week display) -function updateDayOfWeek() { - const dateInput = document.getElementById('client_date'); - const dayDisplay = document.getElementById('day_display'); - // ... -} -``` - -### Update validation: - -```javascript -// Before -const dayPhrase = document.getElementById('day_phrase').value; -if (!dayPhrase || dayPhrase.trim().length === 0) { - alert('Day phrase is required'); - return false; -} - -// After -const passphrase = document.getElementById('passphrase').value; -if (!passphrase || passphrase.trim().length === 0) { - alert('Passphrase is required'); - return false; -} - -// Add word count validation -const words = passphrase.trim().split(/\s+/); -if (words.length < {{ min_passphrase_words }}) { - alert(`Passphrase should have at least {{ recommended_passphrase_words }} words`); - return false; -} -``` - -## CSS Updates - -Add styling for passphrase warnings: - -```css -.passphrase-display { - background: #f5f5f5; - padding: 15px; - border-radius: 5px; - margin: 10px 0; -} - -.passphrase-display code { - font-size: 1.2em; - color: #2c3e50; - word-break: break-word; -} - -.help-text.v3-2-0 { - color: #3498db; - font-weight: bold; -} - -.flash.warning { - background-color: #fff3cd; - border-left: 4px solid #ffc107; - color: #856404; -} -``` - -## Migration Notes for Users - -Add to templates: - -```html -
    -

    ⚠️ v3.2.0 Breaking Changes

    -

    If you have messages encoded with v3.1.0:

    - -
    -``` - -## Form Field Summary - -### Changed Field Names - -| Old Name (v3.1.0) | New Name (v3.2.0) | Type | -|-------------------|-------------------|------| -| `day_phrase` | `passphrase` | text input | -| `words_per_phrase` | `words_per_passphrase` | number input | -| `client_date` | (removed) | date input | -| `stego_date` | (removed) | date input | - -### New Validation Attributes - -```html - -``` - -## Testing Checklist - -- [ ] Generate page creates single passphrase -- [ ] Generate page shows correct entropy (4 words = 44 bits) -- [ ] Generate page doesn't show day names -- [ ] Encode page accepts passphrase (not day_phrase) -- [ ] Encode page doesn't have date selection -- [ ] Encode page shows v3.2.0 help text -- [ ] Decode page accepts passphrase -- [ ] Decode page doesn't have date input -- [ ] Decode page doesn't auto-detect date from filename -- [ ] Error messages say "passphrase" not "day phrase" -- [ ] Validation shows warnings for short passphrases -- [ ] QR code functionality still works -- [ ] DCT mode options still work -- [ ] All flash messages updated - -## Implementation Status - -✅ Flask routes updated -✅ Form parameter names changed -✅ Function calls updated -✅ Validation added for passphrases -✅ Error messages updated -✅ Template context updated -⏳ Templates need updating (generate.html, encode.html, decode.html, index.html, about.html) -⏳ JavaScript needs updating -⏳ CSS styling for v3.2.0 features - -## Quick Reference - -**To test the Flask app:** -```bash -cd frontends/web -python app.py -# Visit http://localhost:5000 -``` - -**Key user-facing changes:** -1. Generate: Shows one passphrase, not 7 daily phrases -2. Encode: No date selection, just passphrase -3. Decode: No date needed, just passphrase - -**Benefits to highlight:** -- ✅ Simpler UI (fewer fields) -- ✅ No date tracking needed -- ✅ Encode today, decode anytime -- ✅ Perfect for asynchronous communications diff --git a/minimal_flask_crash.py b/minimal_flask_crash.py deleted file mode 100644 index 8ab880e..0000000 --- a/minimal_flask_crash.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env python3 -""" -Minimal Flask app to isolate the crash. -Run with: python minimal_flask_crash.py - -Then test with: - curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test1 - curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test2 - curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test3 -""" - -import io -import gc -import os -import sys -import tempfile - -# Minimal imports first -from flask import Flask, request, jsonify -from PIL import Image -import numpy as np - -app = Flask(__name__) -app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB - -# Check for jpegio -try: - import jpegio as jio - HAS_JPEGIO = True - print("jpegio: available") -except ImportError: - HAS_JPEGIO = False - print("jpegio: NOT available") - - -@app.route('/test1', methods=['POST']) -def test1_pil_only(): - """Test 1: PIL only, no jpegio, no scipy""" - carrier = request.files.get('carrier') - if not carrier: - return jsonify({'error': 'No carrier'}), 400 - - data = carrier.read() - print(f"[test1] Read {len(data)} bytes") - - img = Image.open(io.BytesIO(data)) - width, height = img.size - fmt = img.format - img.close() - print(f"[test1] Image: {width}x{height} {fmt}") - - gc.collect() - print("[test1] Returning response...") - - return jsonify({ - 'test': 'pil_only', - 'width': width, - 'height': height, - 'format': fmt, - }) - - -@app.route('/test2', methods=['POST']) -def test2_multiple_opens(): - """Test 2: Open image multiple times like compare_modes does""" - carrier = request.files.get('carrier') - if not carrier: - return jsonify({'error': 'No carrier'}), 400 - - data = carrier.read() - print(f"[test2] Read {len(data)} bytes") - - # First open - img1 = Image.open(io.BytesIO(data)) - width, height = img1.size - img1.close() - print(f"[test2] Open 1: {width}x{height}") - - # Second open - img2 = Image.open(io.BytesIO(data)) - pixels = img2.size[0] * img2.size[1] - img2.close() - print(f"[test2] Open 2: {pixels} pixels") - - # Third open - img3 = Image.open(io.BytesIO(data)) - blocks = (img3.size[0] // 8) * (img3.size[1] // 8) - img3.close() - print(f"[test2] Open 3: {blocks} blocks") - - gc.collect() - print("[test2] Returning response...") - - return jsonify({ - 'test': 'multiple_opens', - 'width': width, - 'height': height, - 'pixels': pixels, - 'blocks': blocks, - }) - - -@app.route('/test3', methods=['POST']) -def test3_with_jpegio(): - """Test 3: Include jpegio operations""" - if not HAS_JPEGIO: - return jsonify({'error': 'jpegio not available'}), 501 - - carrier = request.files.get('carrier') - if not carrier: - return jsonify({'error': 'No carrier'}), 400 - - data = carrier.read() - print(f"[test3] Read {len(data)} bytes") - - # Check if JPEG - img = Image.open(io.BytesIO(data)) - is_jpeg = img.format == 'JPEG' - width, height = img.size - img.close() - print(f"[test3] Image: {width}x{height}, JPEG: {is_jpeg}") - - if not is_jpeg: - return jsonify({'error': 'Not a JPEG'}), 400 - - # Write to temp file - fd, temp_path = tempfile.mkstemp(suffix='.jpg') - os.write(fd, data) - os.close(fd) - print(f"[test3] Temp file: {temp_path}") - - try: - # Read with jpegio - jpeg = jio.read(temp_path) - print(f"[test3] jpegio.read() OK") - - coef = jpeg.coef_arrays[0] - coef_shape = coef.shape - print(f"[test3] Coef shape: {coef_shape}") - - # Count positions like the real code does - positions = 0 - h, w = coef.shape - for row in range(h): - for col in range(w): - if (row % 8 == 0) and (col % 8 == 0): - continue - if abs(coef[row, col]) >= 2: - positions += 1 - print(f"[test3] Usable positions: {positions}") - - # Cleanup - del coef - del jpeg - print(f"[test3] Deleted jpegio objects") - - finally: - os.unlink(temp_path) - print(f"[test3] Removed temp file") - - gc.collect() - print("[test3] Returning response...") - - return jsonify({ - 'test': 'with_jpegio', - 'width': width, - 'height': height, - 'coef_shape': list(coef_shape), - 'positions': positions, - }) - - -@app.route('/test4', methods=['POST']) -def test4_numpy_array_from_pil(): - """Test 4: Create numpy array from PIL image (like DCT does)""" - carrier = request.files.get('carrier') - if not carrier: - return jsonify({'error': 'No carrier'}), 400 - - data = carrier.read() - print(f"[test4] Read {len(data)} bytes") - - img = Image.open(io.BytesIO(data)) - width, height = img.size - print(f"[test4] Image: {width}x{height}") - - # Convert to grayscale and numpy array - gray = img.convert('L') - arr = np.array(gray, dtype=np.float64, copy=True) - print(f"[test4] Array: {arr.shape} {arr.dtype}") - - # Close PIL images - gray.close() - img.close() - print(f"[test4] PIL closed") - - # Do some numpy operations - mean_val = float(np.mean(arr)) - std_val = float(np.std(arr)) - print(f"[test4] Stats: mean={mean_val:.2f}, std={std_val:.2f}") - - # Clear array - del arr - gc.collect() - print("[test4] Returning response...") - - return jsonify({ - 'test': 'numpy_from_pil', - 'width': width, - 'height': height, - 'mean': mean_val, - 'std': std_val, - }) - - -@app.route('/test5', methods=['POST']) -def test5_file_read_keep_reference(): - """Test 5: Keep reference to file data in request scope""" - carrier = request.files.get('carrier') - if not carrier: - return jsonify({'error': 'No carrier'}), 400 - - # Don't read into local variable - read directly each time - # This mimics potential issues with Flask's file handling - - print(f"[test5] File object: {carrier}") - - # Read once - carrier.seek(0) - data1 = carrier.read() - print(f"[test5] First read: {len(data1)} bytes") - - img = Image.open(io.BytesIO(data1)) - width, height = img.size - img.close() - - # Try to read again (should be empty or need seek) - data2 = carrier.read() - print(f"[test5] Second read (no seek): {len(data2)} bytes") - - carrier.seek(0) - data3 = carrier.read() - print(f"[test5] Third read (after seek): {len(data3)} bytes") - - gc.collect() - print("[test5] Returning response...") - - return jsonify({ - 'test': 'file_handling', - 'width': width, - 'height': height, - 'read1': len(data1), - 'read2': len(data2), - 'read3': len(data3), - }) - - -@app.after_request -def after_request(response): - """Log after each request""" - print(f"[after_request] Response status: {response.status}") - return response - - -@app.teardown_request -def teardown_request(exception): - """Log during teardown""" - if exception: - print(f"[teardown] Exception: {exception}") - else: - print("[teardown] Clean teardown") - gc.collect() - - -if __name__ == '__main__': - print("\n" + "=" * 60) - print("MINIMAL FLASK CRASH TEST") - print("=" * 60) - print("\nTest endpoints:") - print(" /test1 - PIL only") - print(" /test2 - Multiple PIL opens") - print(" /test3 - With jpegio") - print(" /test4 - NumPy array from PIL") - print(" /test5 - File handling test") - print("\nUsage:") - print(' curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test1') - print("=" * 60 + "\n") - - app.run(host='0.0.0.0', port=5001, debug=False, threaded=False) diff --git a/rpi/BUILD_IMAGE.md b/rpi/BUILD_IMAGE.md index 3da49a0..d5887f2 100644 --- a/rpi/BUILD_IMAGE.md +++ b/rpi/BUILD_IMAGE.md @@ -5,12 +5,12 @@ Quick reference for building a distributable SD card image. ## Step 1: Flash Fresh Raspbian Use rpi-imager with these settings: -- **OS**: Raspberry Pi OS (64-bit) +- **OS**: Raspberry Pi OS Lite (64-bit) - **Hostname**: `stegasoo` - **Enable SSH**: Yes (password auth) -- **Username**: `pi` (or any) -- **Password**: `raspberry` (temporary) -- **WiFi**: Skip (use ethernet for clean image) +- **Username**: `admin` +- **Password**: `stegasoo` +- **WiFi**: Configure for your network (sanitize script removes it later) ## Step 2: Boot & SSH In @@ -43,17 +43,22 @@ curl -k https://localhost:5000 ## Step 5: Sanitize for Distribution ```bash -curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash +# Full sanitize (for final image - removes WiFi, shuts down) +sudo ~/stegasoo/rpi/sanitize-for-image.sh + +# Or soft reset (for testing - keeps WiFi, reboots) +sudo ~/stegasoo/rpi/sanitize-for-image.sh --soft ``` This removes: -- WiFi credentials +- WiFi credentials (unless `--soft`) +- SSH host keys (regenerate on boot) - SSH authorized keys - Bash history - Stegasoo auth database - Logs and temp files -The Pi will shut down when complete. +The script validates all cleanup steps before finishing. ## Step 6: Copy the Image @@ -75,18 +80,18 @@ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh chmod +x pishrink.sh sudo ./pishrink.sh stegasoo-rpi-*.img -# Compress -xz -9 -T0 stegasoo-rpi-*.img +# Compress (zstd is faster than xz with similar ratio) +zstd -19 -T0 stegasoo-rpi-*.img ``` ## Step 8: Distribute -Upload `.img.xz` to GitHub Releases. +Upload `.img.zst` to GitHub Releases. Users can flash with: ```bash # Linux -xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress +zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress # Or use rpi-imager "Use custom" option ``` @@ -100,9 +105,9 @@ xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/setup.sh | bash sudo systemctl start stegasoo curl -k https://localhost:5000 -curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash +sudo ~/stegasoo/rpi/sanitize-for-image.sh # On your machine: sudo dd if=/dev/sdX of=stegasoo-rpi-$(date +%Y%m%d).img bs=4M status=progress -xz -9 -T0 stegasoo-rpi-*.img +zstd -19 -T0 stegasoo-rpi-*.img ``` diff --git a/rpi/README.md b/rpi/README.md index a8fcd2c..83f68c1 100644 --- a/rpi/README.md +++ b/rpi/README.md @@ -30,11 +30,21 @@ chmod +x setup.sh ## Requirements - Raspberry Pi 4 or 5 -- Raspberry Pi OS (64-bit) - Bookworm or later +- Raspberry Pi OS Lite (64-bit) - Bookworm or later - 4GB+ RAM recommended (2GB minimum) - ~2GB free disk space - Internet connection +## Pre-built Image Defaults + +If using a pre-built image from GitHub Releases: + +- **Default login**: `admin` / `stegasoo` +- **Hostname**: `stegasoo.local` +- **First boot**: A setup wizard runs on first SSH login + +> **Security note**: Change the default password after setup with `passwd` + ## After Installation ### Start the Service @@ -134,17 +144,23 @@ curl -k https://localhost:5000 # Should return HTML ### 4. Sanitize for Distribution ```bash -# Download and run sanitize script -curl -sSL https://raw.githubusercontent.com/adlee-was-taken/stegasoo/main/rpi/sanitize-for-image.sh | sudo bash +# Full sanitize (removes WiFi, shuts down for imaging) +sudo ~/stegasoo/rpi/sanitize-for-image.sh + +# Or soft reset (keeps WiFi for testing, reboots) +sudo ~/stegasoo/rpi/sanitize-for-image.sh --soft ``` This removes: -- WiFi credentials +- WiFi credentials (unless `--soft`) +- SSH host keys (regenerate on boot) - SSH authorized keys - Bash history - Stegasoo auth database (users create their own admin) - Logs and temp files +The script validates cleanup and reports any issues. + ### 5. Create the Image After Pi shuts down, remove SD card and on another Linux machine: @@ -161,17 +177,17 @@ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh chmod +x pishrink.sh sudo ./pishrink.sh stegasoo-rpi-*.img -# Compress -xz -9 -T0 stegasoo-rpi-*.img +# Compress (zstd is faster than xz with similar compression) +zstd -19 -T0 stegasoo-rpi-*.img ``` ### 6. Distribute -Upload the `.img.xz` file to GitHub Releases. +Upload the `.img.zst` file to GitHub Releases. Users flash with: ```bash -xzcat stegasoo-rpi-*.img.xz | sudo dd of=/dev/sdX bs=4M status=progress +zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress ``` Or use rpi-imager's "Use custom" option. diff --git a/rpi/sanitize-for-image.sh b/rpi/sanitize-for-image.sh index 1e22448..7fc72bd 100755 --- a/rpi/sanitize-for-image.sh +++ b/rpi/sanitize-for-image.sh @@ -4,14 +4,17 @@ # Run this BEFORE creating an image with dd # # This script removes: -# - WiFi credentials +# - WiFi credentials (unless --soft) +# - SSH host keys (will regenerate on boot) # - SSH authorized keys # - User-specific data # - Bash history # - Logs # - Stegasoo auth database (users will create their own admin) # -# Usage: sudo ./sanitize-for-image.sh +# Usage: +# sudo ./sanitize-for-image.sh # Full sanitize for image distribution +# sudo ./sanitize-for-image.sh --soft # Soft reset (keeps WiFi for testing) # set -e @@ -19,21 +22,38 @@ set -e RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' +CYAN='\033[0;36m' NC='\033[0m' +SOFT_RESET=false +if [ "$1" = "--soft" ] || [ "$1" = "-s" ]; then + SOFT_RESET=true +fi + if [ "$EUID" -ne 0 ]; then echo -e "${RED}Error: Must run as root (sudo)${NC}" exit 1 fi -echo -e "${YELLOW}" -echo "╔═══════════════════════════════════════════════════════════════╗" -echo "║ Sanitize Pi for Image Distribution ║" -echo "║ ║" -echo "║ This will remove personal data and prepare for imaging. ║" -echo "║ The system will shut down when complete. ║" -echo "╚═══════════════════════════════════════════════════════════════╝" -echo -e "${NC}" +if [ "$SOFT_RESET" = true ]; then + echo -e "${CYAN}" + echo "+-----------------------------------------------------------------+" + echo "| Soft Reset (Factory Defaults) |" + echo "| |" + echo "| WiFi credentials will be KEPT for continued testing. |" + echo "| Everything else will be reset to first-boot state. |" + echo "+-----------------------------------------------------------------+" + echo -e "${NC}" +else + echo -e "${YELLOW}" + echo "+-----------------------------------------------------------------+" + echo "| Sanitize Pi for Image Distribution |" + echo "| |" + echo "| This will remove ALL personal data for imaging. |" + echo "| The system will shut down when complete. |" + echo "+-----------------------------------------------------------------+" + echo -e "${NC}" +fi read -p "Continue? This cannot be undone! [y/N] " -n 1 -r echo @@ -42,9 +62,21 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi -echo -e "${GREEN}[1/9]${NC} Removing WiFi credentials..." -if [ -f /etc/wpa_supplicant/wpa_supplicant.conf ]; then - cat > /etc/wpa_supplicant/wpa_supplicant.conf << 'EOF' +# Track validation results +VALIDATION_ERRORS=0 + +# ============================================================================= +# Step 1: WiFi Credentials +# ============================================================================= +if [ "$SOFT_RESET" = true ]; then + echo -e "${GREEN}[1/10]${NC} Keeping WiFi credentials (soft reset)..." + echo " WiFi config preserved" +else + echo -e "${GREEN}[1/10]${NC} Removing WiFi credentials..." + + # Remove from rootfs + if [ -f /etc/wpa_supplicant/wpa_supplicant.conf ]; then + cat > /etc/wpa_supplicant/wpa_supplicant.conf << 'EOF' ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 country=US @@ -55,12 +87,22 @@ country=US # psk="YourPassword" # } EOF - echo " WiFi credentials cleared" -else - echo " No wpa_supplicant.conf found" + echo " Cleared /etc/wpa_supplicant/wpa_supplicant.conf" + fi + + # Remove from boot partition (headless setup file) + BOOT_PART=$(findmnt -n -o SOURCE /boot/firmware 2>/dev/null || findmnt -n -o SOURCE /boot 2>/dev/null || echo "") + if [ -n "$BOOT_PART" ]; then + BOOT_MOUNT=$(findmnt -n -o TARGET "$BOOT_PART" 2>/dev/null || echo "/boot") + rm -f "$BOOT_MOUNT/wpa_supplicant.conf" 2>/dev/null || true + echo " Removed boot partition WiFi config" + fi fi -echo -e "${GREEN}[2/9]${NC} Removing SSH authorized keys..." +# ============================================================================= +# Step 2: SSH Authorized Keys +# ============================================================================= +echo -e "${GREEN}[2/10]${NC} Removing SSH authorized keys..." for user_home in /home/*; do if [ -d "$user_home/.ssh" ]; then rm -f "$user_home/.ssh/authorized_keys" @@ -70,15 +112,28 @@ for user_home in /home/*; do done rm -f /root/.ssh/authorized_keys /root/.ssh/known_hosts 2>/dev/null || true -echo -e "${GREEN}[3/9]${NC} Clearing bash history..." +# ============================================================================= +# Step 3: SSH Host Keys +# ============================================================================= +echo -e "${GREEN}[3/10]${NC} Removing SSH host keys (will regenerate on first boot)..." +rm -f /etc/ssh/ssh_host_* +echo " SSH host keys removed" + +# ============================================================================= +# Step 4: Bash History +# ============================================================================= +echo -e "${GREEN}[4/10]${NC} Clearing bash history..." for user_home in /home/*; do rm -f "$user_home/.bash_history" rm -f "$user_home/.python_history" done rm -f /root/.bash_history /root/.python_history 2>/dev/null || true -history -c +history -c 2>/dev/null || true -echo -e "${GREEN}[4/9]${NC} Removing Stegasoo user data..." +# ============================================================================= +# Step 5: Stegasoo User Data +# ============================================================================= +echo -e "${GREEN}[5/10]${NC} Removing Stegasoo user data..." # Remove auth database (users create their own admin on first run) rm -rf /home/*/stegasoo/frontends/web/instance/ # Remove SSL certs (will be regenerated) @@ -87,35 +142,36 @@ rm -rf /home/*/stegasoo/frontends/web/certs/ rm -f /home/*/stegasoo/frontends/web/.env echo " Stegasoo instance data cleared" -echo -e "${GREEN}[5/9]${NC} Setting up first-boot wizard..." +# ============================================================================= +# Step 6: First-Boot Wizard Setup +# ============================================================================= +echo -e "${GREEN}[6/10]${NC} Setting up first-boot wizard..." + # Find stegasoo install directory STEGASOO_DIR=$(ls -d /home/*/stegasoo 2>/dev/null | head -1) -echo " Looking for stegasoo in: $STEGASOO_DIR" if [ -z "$STEGASOO_DIR" ]; then - echo -e " ${RED}ERROR: Could not find stegasoo directory in /home/*/stegasoo${NC}" - echo " Checking common locations..." - for dir in /home/*/stegasoo /root/stegasoo /opt/stegasoo; do + for dir in /root/stegasoo /opt/stegasoo; do if [ -d "$dir" ]; then STEGASOO_DIR="$dir" - echo " Found at: $STEGASOO_DIR" break fi done fi STEGASOO_USER=$(stat -c '%U' "$STEGASOO_DIR" 2>/dev/null || echo "pi") +echo " Stegasoo directory: $STEGASOO_DIR" echo " Stegasoo user: $STEGASOO_USER" if [ -n "$STEGASOO_DIR" ] && [ -f "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" ]; then # Install the profile.d hook cp "$STEGASOO_DIR/rpi/stegasoo-wizard.sh" /etc/profile.d/stegasoo-wizard.sh - chmod 755 /etc/profile.d/stegasoo-wizard.sh - echo " Installed first-boot wizard hook" + chmod 644 /etc/profile.d/stegasoo-wizard.sh + echo " Installed wizard hook to /etc/profile.d/" # Create the first-boot flag touch /etc/stegasoo-first-boot - echo " Created first-boot flag" + echo " Created /etc/stegasoo-first-boot flag" # Reset systemd service to defaults (wizard will reconfigure) cat > /etc/systemd/system/stegasoo.service </dev/null || true +journalctl --vacuum-time=1s 2>/dev/null || true +rm -rf /var/log/*.log /var/log/*.gz /var/log/*.[0-9] 2>/dev/null || true +rm -rf /var/log/apt/* 2>/dev/null || true +rm -rf /var/log/journal/* 2>/dev/null || true find /var/log -type f -name "*.log" -delete 2>/dev/null || true echo " Logs cleared" -echo -e "${GREEN}[7/9]${NC} Clearing temporary files..." -rm -rf /tmp/* -rm -rf /var/tmp/* +# ============================================================================= +# Step 8: Temporary Files +# ============================================================================= +echo -e "${GREEN}[8/10]${NC} Clearing temporary files..." +rm -rf /tmp/* 2>/dev/null || true +rm -rf /var/tmp/* 2>/dev/null || true echo " Temp files cleared" -echo -e "${GREEN}[8/9]${NC} Clearing package cache..." -apt-get clean -rm -rf /var/cache/apt/archives/* +# ============================================================================= +# Step 9: Package Cache +# ============================================================================= +echo -e "${GREEN}[9/10]${NC} Clearing package cache..." +apt-get clean 2>/dev/null || true +rm -rf /var/cache/apt/archives/* 2>/dev/null || true echo " Package cache cleared" -echo -e "${GREEN}[9/9]${NC} Final cleanup..." -# Remove this script's evidence -rm -f /root/.bash_history +# ============================================================================= +# Step 10: Final Sync +# ============================================================================= +echo -e "${GREEN}[10/10]${NC} Final sync..." +rm -f /root/.bash_history 2>/dev/null || true sync +echo " Filesystem synced" +# ============================================================================= +# Validation +# ============================================================================= echo "" -echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════╗${NC}" -echo -e "${GREEN}║ Sanitization Complete! ║${NC}" -echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════╝${NC}" -echo "" -echo "The system is ready for imaging." -echo "" -echo -e "${YELLOW}Next steps:${NC}" -echo " 1. Shut down: sudo shutdown -h now" -echo " 2. Remove SD card" -echo " 3. On another machine, copy with:" -echo " sudo dd if=/dev/sdX of=stegasoo-rpi.img bs=4M status=progress" -echo " 4. Compress: xz -9 stegasoo-rpi.img" -echo "" -read -p "Shut down now? [y/N] " -n 1 -r -echo -if [[ $REPLY =~ ^[Yy]$ ]]; then - shutdown -h now +echo -e "${CYAN}Validating sanitization...${NC}" + +# Check first-boot flag +if [ -f /etc/stegasoo-first-boot ]; then + echo -e " ${GREEN}[PASS]${NC} First-boot flag exists" +else + echo -e " ${RED}[FAIL]${NC} First-boot flag missing" + VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1)) +fi + +# Check profile.d hook +if [ -f /etc/profile.d/stegasoo-wizard.sh ]; then + echo -e " ${GREEN}[PASS]${NC} Wizard hook installed" +else + echo -e " ${RED}[FAIL]${NC} Wizard hook missing" + VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1)) +fi + +# Check SSH host keys removed +if ls /etc/ssh/ssh_host_* 1>/dev/null 2>&1; then + echo -e " ${RED}[FAIL]${NC} SSH host keys still present" + VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1)) +else + echo -e " ${GREEN}[PASS]${NC} SSH host keys removed" +fi + +# Check Stegasoo instance data removed +if ls /home/*/stegasoo/frontends/web/instance/*.db 1>/dev/null 2>&1; then + echo -e " ${RED}[FAIL]${NC} Stegasoo database still present" + VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1)) +else + echo -e " ${GREEN}[PASS]${NC} Stegasoo database removed" +fi + +# Check WiFi (only for full sanitize) +if [ "$SOFT_RESET" = false ]; then + if grep -q "psk=" /etc/wpa_supplicant/wpa_supplicant.conf 2>/dev/null; then + echo -e " ${RED}[FAIL]${NC} WiFi credentials still present" + VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1)) + else + echo -e " ${GREEN}[PASS]${NC} WiFi credentials cleared" + fi +else + echo -e " ${YELLOW}[SKIP]${NC} WiFi check (soft reset mode)" +fi + +# Check authorized_keys removed +AUTH_KEYS_FOUND=false +for user_home in /home/*; do + if [ -f "$user_home/.ssh/authorized_keys" ]; then + AUTH_KEYS_FOUND=true + break + fi +done +if [ "$AUTH_KEYS_FOUND" = true ]; then + echo -e " ${RED}[FAIL]${NC} SSH authorized_keys still present" + VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1)) +else + echo -e " ${GREEN}[PASS]${NC} SSH authorized_keys removed" +fi + +# ============================================================================= +# Summary +# ============================================================================= +echo "" +if [ $VALIDATION_ERRORS -eq 0 ]; then + echo -e "${GREEN}+-----------------------------------------------------------------+${NC}" + echo -e "${GREEN}| Sanitization Complete! |${NC}" + echo -e "${GREEN}| All validation checks passed. |${NC}" + echo -e "${GREEN}+-----------------------------------------------------------------+${NC}" +else + echo -e "${RED}+-----------------------------------------------------------------+${NC}" + echo -e "${RED}| Sanitization Complete with Errors |${NC}" + echo -e "${RED}| $VALIDATION_ERRORS validation check(s) failed |${NC}" + echo -e "${RED}+-----------------------------------------------------------------+${NC}" +fi +echo "" + +if [ "$SOFT_RESET" = true ]; then + echo -e "${CYAN}Soft reset complete.${NC}" + echo "You can now reboot to test the first-boot wizard." + echo "" + read -p "Reboot now? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + reboot + fi +else + echo "The system is ready for imaging." + echo "" + echo -e "${YELLOW}Next steps:${NC}" + echo " 1. Shut down: sudo shutdown -h now" + echo " 2. Remove SD card" + echo " 3. On another machine, copy with:" + echo " sudo dd if=/dev/sdX of=stegasoo-rpi.img bs=4M status=progress" + echo " 4. Compress: zstd -19 stegasoo-rpi.img" + echo "" + read -p "Shut down now? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + shutdown -h now + fi fi diff --git a/src/stegasoo/COMPLETE_CHANGE_SUMMARY_V3.2.0.md b/src/stegasoo/COMPLETE_CHANGE_SUMMARY_V3.2.0.md deleted file mode 100644 index 28e29a3..0000000 --- a/src/stegasoo/COMPLETE_CHANGE_SUMMARY_V3.2.0.md +++ /dev/null @@ -1,374 +0,0 @@ -# Stegasoo v3.2.0 - Complete Change Summary - -## Overview - -This update makes two major breaking changes to Stegasoo: -1. **Remove date dependency** - Date no longer used in cryptographic operations -2. **Rename day_phrase → passphrase** - Reflects removal of daily rotation requirement - -## Version Information - -- **Previous**: v3.1.0 (date-dependent, day_phrase) -- **Current**: v3.2.0 (date-independent, passphrase) -- **Format Version**: 3 → 4 (breaking change) -- **Compatibility**: NOT backward compatible with v3.1.0 - -## Files Modified - -### Core Files (MUST UPDATE) - -1. **crypto.py** ✅ Updated - - Removed `date_str` parameter from all functions - - Renamed `day_phrase` → `passphrase` in all functions - - Removed date from key derivation material - - Simplified header format (no date field) - - Updated error messages - -2. **constants.py** ✅ Updated - - Version: `__version__ = "3.2.0"` - - Format: `FORMAT_VERSION = 4` - - Added passphrase constants: - - `MIN_PASSPHRASE_WORDS = 3` - - `MAX_PASSPHRASE_WORDS = 12` - - `DEFAULT_PASSPHRASE_WORDS = 4` (increased from 3) - - `RECOMMENDED_PASSPHRASE_WORDS = 4` - - Kept legacy aliases for transition - -3. **models.py** ✅ Updated - - `Credentials`: Changed from `phrases: dict` → `passphrase: str` - - `EncodeInput`: Renamed `day_phrase` → `passphrase`, removed `date_str` - - `DecodeInput`: Renamed `day_phrase` → `passphrase` - - `EncodeResult`: Made `date_used` optional (cosmetic only) - - `DecodeResult`: `date_encoded` always None in v3.2.0 - - `ValidationResult`: Added `warning` field - -4. **validation.py** ✅ Updated - - Renamed `validate_phrase()` → `validate_passphrase()` - - Added word count validation with warnings - - Recommends 4+ words for good security - - Updated error messages - -### Files Needing Updates - -5. **__init__.py** - Public API - - [ ] `encode()`: Remove `date_str`, rename `day_phrase` → `passphrase` - - [ ] `encode_file()`: Same changes - - [ ] `encode_bytes()`: Same changes - - [ ] `decode()`: Remove `date_str`, rename `day_phrase` → `passphrase` - - [ ] `decode_text()`: Same changes - - [ ] Update all docstrings - -6. **keygen.py** - Key generation - - [ ] `generate_day_phrases()` → `generate_passphrases()` or keep with new implementation - - [ ] `generate_credentials()`: Update to use single passphrase - - [ ] Update `Credentials` creation - -7. **batch.py** - Batch operations - - [ ] `BatchCredentials`: Rename `day_phrase` → `passphrase` - - [ ] Update all batch functions - -8. **cli.py** - Command line - - [ ] `--phrase` → `--passphrase` (or keep `--phrase` for simplicity) - - [ ] Update help text - - [ ] Update credentials dict creation - -9. **steganography.py** - No changes needed - - Uses keys from crypto module, doesn't directly handle phrases/dates - -10. **dct_steganography.py** - No changes needed - - Uses keys from crypto module - -### Optional/Documentation Files - -11. **utils.py** - Keep as-is (organizational functions) -12. **debug.py** - No changes needed -13. **exceptions.py** - No changes needed -14. **compression.py** - No changes needed -15. **qr_utils.py** - No changes needed - -## Key Changes Breakdown - -### 1. Function Signatures - -**Before (v3.1.0):** -```python -def derive_hybrid_key( - photo_data: bytes, - day_phrase: str, - date_str: str, - salt: bytes, - pin: str = "", - rsa_key_data: Optional[bytes] = None -) -> bytes: -``` - -**After (v3.2.0):** -```python -def derive_hybrid_key( - photo_data: bytes, - passphrase: str, - salt: bytes, - pin: str = "", - rsa_key_data: Optional[bytes] = None -) -> bytes: -``` - -### 2. Key Derivation Material - -**Before:** -```python -key_material = ( - photo_hash + - day_phrase.lower().encode() + - pin.encode() + - date_str.encode() + # ← REMOVED - salt -) -``` - -**After:** -```python -key_material = ( - photo_hash + - passphrase.lower().encode() + - pin.encode() + - salt -) -``` - -### 3. Header Format - -**Before (v3.1.0):** 66+ bytes -``` -[Magic:4][Version:1][DateLen:1][Date:10][Salt:32][IV:12][Tag:16][Ciphertext] -``` - -**After (v3.2.0):** 65 bytes -``` -[Magic:4][Version:1][Salt:32][IV:12][Tag:16][Ciphertext] -``` - -### 4. Public API - -**Before:** -```python -# Encoding -result = encode( - message="Secret", - reference_photo=photo, - carrier_image=carrier, - day_phrase="apple forest thunder", - pin="123456", - date_str="2025-01-15" -) - -# Decoding -decoded = decode( - stego_image=stego, - reference_photo=photo, - day_phrase="apple forest thunder", - pin="123456", - date_str="2025-01-15" -) -``` - -**After:** -```python -# Encoding -result = encode( - message="Secret", - reference_photo=photo, - carrier_image=carrier, - passphrase="apple forest thunder mountain", - pin="123456" -) - -# Decoding -decoded = decode( - stego_image=stego, - reference_photo=photo, - passphrase="apple forest thunder mountain", - pin="123456" -) -``` - -## Migration Path - -### For Users with v3.1.0 Messages - -1. **Before upgrading**, decode all messages with v3.1.0: - ```bash - # Using v3.1.0 - python decode_all.py - ``` - -2. Save the decoded content - -3. Upgrade to v3.2.0 - -4. Re-encode with v3.2.0 if needed - -### For Developers - -1. Update the 4 core files: crypto.py, constants.py, models.py, validation.py - -2. Update remaining files in order: - - `__init__.py` (public API - critical) - - `keygen.py` (credential generation) - - `batch.py` (batch operations) - - `cli.py` (command line) - -3. Run tests to verify: - ```bash - pytest tests/ -v - ``` - -4. Update documentation and examples - -## Benefits - -### Simplicity -- ❌ Before: 3 parameters (day_phrase, pin, date) -- ✅ After: 2 parameters (passphrase, pin) - -### User Experience -- ❌ Before: "What date did I encode this?" "Which day's phrase?" -- ✅ After: Just use your passphrase - -### Asynchronous Ready -- ❌ Before: Must know encoding date -- ✅ After: Decode anytime - -### Less Metadata -- ❌ Before: Date stored in header -- ✅ After: No temporal metadata - -## Security Considerations - -### Entropy Comparison - -**v3.1.0:** -- Photo hash: ~128 bits -- Day phrase (3 words): ~33 bits -- PIN (6 digits): ~20 bits -- Date: ~33 bits (10 digits) -- **Total: ~214 bits** - -**v3.2.0:** -- Photo hash: ~128 bits -- Passphrase (4 words): ~44 bits -- PIN (6 digits): ~20 bits -- **Total: ~192 bits** - -**Mitigation:** Recommend longer passphrases (4-5 words vs 3) - -### Best Practices for v3.2.0 - -1. **Use 4+ word passphrases** (increased from 3) -2. **Keep using PINs** (additional 20 bits) -3. **Protect reference photo** (still critical) -4. **Consider RSA keys** for highest security - -## Testing Checklist - -- [ ] Unit tests pass -- [ ] Integration tests pass -- [ ] Encode/decode round-trip works -- [ ] File payloads work -- [ ] LSB mode works -- [ ] DCT mode works -- [ ] Batch operations work -- [ ] CLI commands work -- [ ] Error messages are clear -- [ ] Validation works correctly -- [ ] No references to "day_phrase" remain -- [ ] No date parameters remain (except cosmetic) - -## Documentation Updates Needed - -- [ ] README.md - Update all examples -- [ ] API documentation - Update function signatures -- [ ] Tutorials - Remove date parameters -- [ ] CHANGELOG.md - Add v3.2.0 entry -- [ ] Migration guide - How to upgrade from v3.1.0 -- [ ] Examples directory - Update all scripts - -## Backward Compatibility Strategy - -### Option 1: Clean Break (Recommended) -- No compatibility code -- Clear version separation -- Users must migrate manually - -### Option 2: Temporary Wrapper -```python -def encode( - message, - reference_photo, - carrier_image, - passphrase: str = None, - day_phrase: str = None, # Deprecated - date_str: str = None, # Deprecated - pin: str = "", - ... -): - if day_phrase and not passphrase: - import warnings - warnings.warn("day_phrase deprecated, use passphrase", DeprecationWarning) - passphrase = day_phrase - - if date_str: - warnings.warn("date_str no longer used", DeprecationWarning) - - # ... rest of function -``` - -## Release Checklist - -- [ ] All files updated -- [ ] Tests passing -- [ ] Documentation updated -- [ ] Migration guide written -- [ ] CHANGELOG.md updated -- [ ] Version bumped to 3.2.0 -- [ ] Git tag created: v3.2.0 -- [ ] PyPI package published -- [ ] Release notes published -- [ ] Users notified of breaking changes - -## Quick Reference - -### Search and Replace Patterns - -Safe to replace globally: -- `day_phrase` → `passphrase` -- `day phrase` → `passphrase` -- `Day phrase` → `Passphrase` -- `DEFAULT_PHRASE_WORDS` → `DEFAULT_PASSPHRASE_WORDS` - -Do NOT replace: -- `DAY_NAMES` (keep for utilities) -- `get_day_from_date` (keep for utilities) -- `generate_day_phrases` (rename function itself) - -### Error Message Updates - -- "Day phrase is required" → "Passphrase is required" -- "Check your phrase, PIN" → "Check your passphrase, PIN" -- "the day's phrase" → "the passphrase" -- "today's passphrase" → "passphrase" - -## Support - -For issues or questions during migration: -1. Check the migration guide -2. Review the comparison document -3. Look at updated examples -4. File an issue on GitHub - ---- - -**Status:** -✅ Core files updated (crypto, constants, models, validation) -⏳ Remaining files need updates (__init__, keygen, batch, cli) -📝 Documentation updates pending diff --git a/tests/RELEASE_CHECKLIST_V4_0_0.md b/tests/RELEASE_CHECKLIST_V4_0_0.md deleted file mode 100644 index b494e09..0000000 --- a/tests/RELEASE_CHECKLIST_V4_0_0.md +++ /dev/null @@ -1,528 +0,0 @@ -# Stegasoo v4.0.0 Release Checklist - -## Overview - -This checklist covers functionality testing for the v4.0.0 release. - -### Changes in v4.0.0 - -| Change | v3.2.0 | v4.0.0 | -|--------|--------|--------| -| Python version | 3.10-3.12 | 3.10-3.12 (3.13 NOT supported) | -| JPEG handling | Could crash on quality=100 | Normalized before jpegio | -| Header size | 65 bytes | 65 bytes (unchanged) | -| API | passphrase, no date_str | Same (no breaking changes) | -| Format version | 4 | 4 (compatible with v3.2.0) | - -### Key Points -- **No breaking API changes from v3.2.0** -- **v4.0 CAN decode v3.2.0 images** (same format version) -- **v4.0 CANNOT decode v3.1.x or earlier images** -- **Python 3.13 is NOT supported** (jpegio C extension ABI incompatibility) - ---- - -## 1. Pre-Release Checks - -### 1.1 Python Version - -```bash -python --version # Must be 3.10, 3.11, or 3.12 -``` - -- [ ] Python version is 3.10, 3.11, or 3.12 -- [ ] NOT Python 3.13 (jpegio will crash) - -### 1.2 Dependencies - -```bash -pip list | grep -E "jpegio|scipy|pillow|argon2" -``` - -- [ ] jpegio installed (for DCT JPEG support) -- [ ] scipy installed (for DCT mode) -- [ ] pillow installed -- [ ] argon2-cffi installed - ---- - -## 2. Core Library Tests - -### 2.1 Run Unit Tests - -```bash -cd /path/to/stegasoo -pytest tests/ -v -``` - -- [ ] All tests pass -- [ ] No deprecation warnings for removed parameters - -### 2.2 JPEG Normalization Test (NEW in v4.0) - -```bash -python -c " -from PIL import Image -import io -from stegasoo import encode, decode - -# Create quality=100 JPEG (triggers normalization) -img = Image.new('RGB', (400, 400), 'red') -buf = io.BytesIO() -img.save(buf, format='JPEG', quality=100) -jpeg_data = buf.getvalue() - -# This should NOT crash (v3.2.0 would crash here) -result = encode( - message='Test quality 100', - reference_photo=jpeg_data, - carrier_image=jpeg_data, - passphrase='test phrase four words', - pin='123456', - embed_mode='dct' -) -print('✓ Quality=100 JPEG encode OK') - -decoded = decode( - stego_image=result.stego_image, - reference_photo=jpeg_data, - passphrase='test phrase four words', - pin='123456' -) -assert decoded.message == 'Test quality 100' -print('✓ Quality=100 JPEG decode OK') -" -``` - -- [ ] Quality=100 JPEG encoding works (no crash) -- [ ] Quality=100 JPEG decoding works - -### 2.3 Large Image Test (NEW in v4.0) - -```bash -python -c " -from PIL import Image -import io -from stegasoo import encode, decode - -# Create large image (similar to 14MB real photo) -img = Image.new('RGB', (4000, 3000), 'blue') -buf = io.BytesIO() -img.save(buf, format='PNG') -large_image = buf.getvalue() -print(f'Test image size: {len(large_image) / 1024 / 1024:.1f} MB') - -result = encode( - message='Large image test', - reference_photo=large_image, - carrier_image=large_image, - passphrase='large image test phrase', - pin='123456' -) -print('✓ Large image encode OK') - -decoded = decode( - stego_image=result.stego_image, - reference_photo=large_image, - passphrase='large image test phrase', - pin='123456' -) -assert decoded.message == 'Large image test' -print('✓ Large image decode OK') -" -``` - -- [ ] Large image (12MP+) encoding works -- [ ] Large image decoding works - ---- - -## 3. Docker Build Tests - -### 3.1 Base Image Build - -```bash -# Build base image (one-time, 5-10 min) -sudo docker build -f Dockerfile.base -t stegasoo-base:latest . -``` - -- [ ] Base image builds successfully -- [ ] jpegio + scipy + numpy verification passes - -### 3.2 Application Build - -```bash -# Fast build using base image -sudo docker-compose build -``` - -- [ ] Web container builds -- [ ] API container builds - -### 3.3 Container Startup - -```bash -sudo docker-compose up -d -sudo docker-compose logs -``` - -- [ ] Web container starts without errors -- [ ] API container starts without errors -- [ ] No import errors in logs - ---- - -## 4. Web UI Tests (`http://localhost:5000`) - -### 4.1 Home Page - -- [ ] v4.0 badge visible -- [ ] "Learn More" button is white/visible -- [ ] No references to "day phrase" or dates - -### 4.2 Generate Page (`/generate`) - -- [ ] Default is 4 words -- [ ] Single passphrase generated (not 7 daily) -- [ ] PIN toggle shows/hides digits -- [ ] Memory aid generator works - -### 4.3 Encode Page (`/encode`) - -- [ ] Passphrase field has blue glow on focus -- [ ] PIN field has orange glow on focus -- [ ] PIN box is 180px wide (fits LastPass icon) -- [ ] Passphrase font shrinks for long input (stepped) -- [ ] RSA .pem/QR toggle works -- [ ] QR image preview shows when selected -- [ ] DCT mode options appear when selected -- [ ] Encoding works (LSB mode) -- [ ] Encoding works (DCT mode) - -### 4.4 Decode Page (`/decode`) - -- [ ] Same styling as encode (glowing inputs) -- [ ] RSA .pem/QR toggle works (matches encode layout) -- [ ] QR image preview shows when selected -- [ ] Copy button is below message (not overlapping) -- [ ] Decoding works (LSB mode) -- [ ] Decoding works (DCT mode) -- [ ] Auto mode detection works - -### 4.5 About Page (`/about`) - -- [ ] Version history table present -- [ ] v4.0.0 entry in table -- [ ] Python 3.10-3.12 requirement noted -- [ ] No marketing language ("military-grade" removed) - ---- - -## 5. API Tests (`http://localhost:8000`) - -### 5.1 Status Endpoint - -```bash -curl http://localhost:8000/ -``` - -- [ ] Returns version "4.0.0" -- [ ] No import errors - -### 5.2 Generate Endpoint - -```bash -curl -X POST http://localhost:8000/generate \ - -H "Content-Type: application/json" \ - -d '{"use_pin": true}' -``` - -- [ ] Returns single `passphrase` string -- [ ] Returns 4 words by default - -### 5.3 OpenAPI Docs - -- [ ] `/docs` loads (Swagger UI) -- [ ] `/redoc` loads (ReDoc) -- [ ] All endpoints documented - ---- - -## 6. CLI Tests - -### 6.1 Version - -```bash -stegasoo --version -``` - -- [ ] Shows 4.0.0 - -### 6.2 Generate - -```bash -stegasoo generate --pin --words 4 -``` - -- [ ] Single passphrase output -- [ ] 4 words generated - -### 6.3 Encode/Decode Roundtrip - -```bash -# Generate test image -python -c "from PIL import Image; Image.new('RGB', (200,200), 'red').save('/tmp/test.png')" - -# Encode -stegasoo encode \ - -r /tmp/test.png \ - -c /tmp/test.png \ - -p "cli test phrase here" \ - --pin 123456 \ - -m "CLI roundtrip test" \ - -o /tmp/stego.png - -# Decode -stegasoo decode \ - -r /tmp/test.png \ - -s /tmp/stego.png \ - -p "cli test phrase here" \ - --pin 123456 -``` - -- [ ] Encode succeeds -- [ ] Decode returns correct message - ---- - -## 7. Cross-Version Compatibility - -### 7.1 v3.2.0 Compatibility - -- [ ] v4.0 can decode v3.2.0 images (same format version 4) - -### 7.2 v3.1.x Incompatibility - -- [ ] v4.0 fails gracefully on v3.1.x images -- [ ] Error message is clear - ---- - -## 8. Documentation Review - -### 8.1 Updated Files - -- [ ] README.md - v4.0 references -- [ ] INSTALL.md - Python 3.13 warning prominent -- [ ] SECURITY.md - v4.0 changes documented -- [ ] UNDER_THE_HOOD.md - JPEG normalization section - -### 8.2 Template Updates - -- [ ] All 7 templates updated -- [ ] No v3.x badges remaining -- [ ] Version history in About page - ---- - -## 9. Quick Smoke Test Script - -```bash -#!/bin/bash -# v4.0.0 Smoke Test - -set -e - -echo "=== Stegasoo v4.0.0 Smoke Test ===" - -# Check version -echo "1. Checking version..." -python -c "import stegasoo; assert stegasoo.__version__.startswith('4.'), f'Wrong version: {stegasoo.__version__}'; print(f'✓ Version: {stegasoo.__version__}')" - -# Check Python version -echo "2. Checking Python version..." -python -c " -import sys -v = sys.version_info -assert v.major == 3 and 10 <= v.minor <= 12, f'Python {v.major}.{v.minor} not supported' -print(f'✓ Python {v.major}.{v.minor}.{v.micro}') -" - -# Check DCT support -echo "3. Checking DCT support..." -python -c " -from stegasoo import has_dct_support -from stegasoo.dct_steganography import has_jpegio_support -print(f' DCT (scipy): {has_dct_support()}') -print(f' JPEG native (jpegio): {has_jpegio_support()}') -assert has_dct_support(), 'DCT not available' -print('✓ DCT support OK') -" - -# Test encode/decode roundtrip -echo "4. Testing encode/decode roundtrip..." -python -c " -from stegasoo import encode, decode -from PIL import Image -import io - -img = Image.new('RGB', (200, 200), color='blue') -buf = io.BytesIO() -img.save(buf, format='PNG') -test_image = buf.getvalue() - -result = encode( - message='Hello v4.0.0!', - reference_photo=test_image, - carrier_image=test_image, - passphrase='test phrase four words', - pin='123456' -) - -decoded = decode( - stego_image=result.stego_image, - reference_photo=test_image, - passphrase='test phrase four words', - pin='123456' -) - -assert decoded.message == 'Hello v4.0.0!', f'Got: {decoded.message}' -print('✓ LSB roundtrip OK') -" - -# Test DCT mode -echo "5. Testing DCT mode..." -python -c " -from stegasoo import encode, decode -from PIL import Image -import io - -img = Image.new('RGB', (400, 400), color='green') -buf = io.BytesIO() -img.save(buf, format='PNG') -test_image = buf.getvalue() - -result = encode( - message='DCT v4.0 test', - reference_photo=test_image, - carrier_image=test_image, - passphrase='dct test phrase here', - pin='123456', - embed_mode='dct' -) - -decoded = decode( - stego_image=result.stego_image, - reference_photo=test_image, - passphrase='dct test phrase here', - pin='123456' -) - -assert decoded.message == 'DCT v4.0 test' -print('✓ DCT roundtrip OK') -" - -# Test JPEG quality=100 (v4.0 fix) -echo "6. Testing JPEG quality=100 handling..." -python -c " -from stegasoo import encode, decode -from PIL import Image -import io - -img = Image.new('RGB', (400, 400), color='red') -buf = io.BytesIO() -img.save(buf, format='JPEG', quality=100) -jpeg_q100 = buf.getvalue() - -result = encode( - message='Quality 100 test', - reference_photo=jpeg_q100, - carrier_image=jpeg_q100, - passphrase='jpeg quality test here', - pin='123456', - embed_mode='dct' -) - -decoded = decode( - stego_image=result.stego_image, - reference_photo=jpeg_q100, - passphrase='jpeg quality test here', - pin='123456' -) - -assert decoded.message == 'Quality 100 test' -print('✓ JPEG quality=100 OK (v4.0 fix working)') -" - -echo "" -echo "=== All smoke tests passed! ===" -echo "Ready for release." -``` - ---- - -## 10. Release Steps - -### 10.1 Final Checks - -- [ ] All tests pass -- [ ] All Docker containers work -- [ ] Documentation updated -- [ ] Version bumped in `constants.py` and `pyproject.toml` - -### 10.2 Git - -```bash -git add -A -git status # Review changes -git commit -m "v4.0.0: JPEG normalization, Python 3.12, UI polish" -git tag v4.0.0 -git push origin main --tags -``` - -- [ ] Changes committed -- [ ] Tag created -- [ ] Pushed to remote - -### 10.3 Release Notes - -```markdown -## v4.0.0 - -### What's New -- **JPEG Normalization**: Quality=100 JPEGs now work with DCT mode -- **Python 3.12**: Recommended version (3.13 NOT supported due to jpegio) -- **UI Polish**: Glowing input fields, better layout, version history - -### Fixes -- Fixed jpegio crash on quality=100 JPEG images -- Fixed QR code input on decode page -- Fixed passphrase font sizing (stepped instead of smooth) - -### Breaking Changes -- Python 3.13 is NOT supported - -### Compatibility -- v4.0 can decode v3.2.0 images (same format) -- v4.0 CANNOT decode v3.1.x or earlier -``` - ---- - -## Sign-Off - -| Area | Tested By | Date | Status | -|------|-----------|------|--------| -| Python/Dependencies | | | ☐ | -| Unit Tests | | | ☐ | -| Docker Build | | | ☐ | -| Web UI | | | ☐ | -| API | | | ☐ | -| CLI | | | ☐ | -| Documentation | | | ☐ | - -**Release Approved:** ☐ - -**Released By:** _________________ - -**Release Date:** _________________