Implement granular decode error messages (#2)
New exceptions for specific decode failures: - InvalidMagicBytesError: wrong mode or not a Stegasoo image - ReedSolomonError: image too corrupted to recover - NoDataFoundError, ModeMismatchError: additional clarity Web UI now shows specific, actionable error messages: - "Try a different mode (LSB/DCT)" - "Image too corrupted, may have been re-saved" - "Wrong credentials - check reference photo..." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,63 +50,27 @@ Polish and UX improvements after the 4.1.1 stability release.
|
|||||||
|
|
||||||
## 2. Granular Decode Error Messages
|
## 2. Granular Decode Error Messages
|
||||||
|
|
||||||
**Status:** Planned
|
**Status:** Done
|
||||||
|
|
||||||
**Problem:** Decode failures show generic "Decryption failed" - users don't know if it's wrong photo, wrong passphrase, wrong PIN, corrupted image, or format mismatch.
|
**Problem:** Decode failures show generic "Decryption failed" - users don't know if it's wrong photo, wrong passphrase, wrong PIN, corrupted image, or format mismatch.
|
||||||
|
|
||||||
**Solution:** Bubble up specific error types from library to UI
|
**Solution:** Bubble up specific error types from library to UI
|
||||||
|
|
||||||
### Library Level (`src/stegasoo/`)
|
### Implementation
|
||||||
|
- Added new exceptions: InvalidMagicBytesError, ReedSolomonError, NoDataFoundError, ModeMismatchError
|
||||||
|
- DCT decode now raises InvalidMagicBytesError for wrong magic bytes
|
||||||
|
- DCT decode now raises ReedSolomonError (renamed from reedsolo's) for corruption
|
||||||
|
- app.py catches specific exceptions with user-friendly messages:
|
||||||
|
- Invalid magic → "Try a different mode (LSB/DCT)"
|
||||||
|
- RS error → "Image too corrupted, may have been re-saved"
|
||||||
|
- Invalid header → "Image may have been modified"
|
||||||
|
- Decryption error → "Wrong credentials"
|
||||||
|
|
||||||
1. **Custom exception classes:**
|
### Files Modified
|
||||||
```python
|
- `src/stegasoo/exceptions.py` (new exceptions)
|
||||||
class StegasooError(Exception): pass
|
- `src/stegasoo/__init__.py` (exports)
|
||||||
class InvalidMagicBytesError(StegasooError): pass
|
- `src/stegasoo/dct_steganography.py` (raise specific exceptions)
|
||||||
class DecryptionError(StegasooError): pass
|
- `frontends/web/app.py` (catch and display)
|
||||||
class ReedSolomonError(StegasooError): pass
|
|
||||||
class PayloadTooLargeError(StegasooError): pass
|
|
||||||
class InvalidHeaderError(StegasooError): pass
|
|
||||||
class NoDataFoundError(StegasooError): pass
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Raise specific exceptions** in decode paths:
|
|
||||||
- Magic bytes mismatch → "Not a Stegasoo image or wrong mode (LSB/DCT)"
|
|
||||||
- RS decode failure → "Image corrupted beyond repair"
|
|
||||||
- AES-GCM auth fail → "Wrong credentials (photo/passphrase/PIN)"
|
|
||||||
- Header parse fail → "Invalid or corrupted header"
|
|
||||||
- No stego data → "No hidden data found in image"
|
|
||||||
|
|
||||||
3. **Error codes** for programmatic handling:
|
|
||||||
```python
|
|
||||||
class ErrorCode(Enum):
|
|
||||||
INVALID_MAGIC = "invalid_magic"
|
|
||||||
DECRYPTION_FAILED = "decryption_failed"
|
|
||||||
RS_FAILED = "rs_failed"
|
|
||||||
# etc.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Web UI Level (`frontends/web/`)
|
|
||||||
|
|
||||||
1. **app.py** - Catch specific exceptions, return error type:
|
|
||||||
```python
|
|
||||||
except InvalidMagicBytesError:
|
|
||||||
flash("This doesn't appear to be a Stegasoo image, or mode mismatch", "danger")
|
|
||||||
except DecryptionError:
|
|
||||||
flash("Wrong credentials - check reference photo, passphrase, and PIN", "warning")
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **decode.html** - Error-specific help text:
|
|
||||||
- Wrong credentials → "Double-check your reference photo matches exactly"
|
|
||||||
- Corrupted → "Image may have been re-saved or compressed"
|
|
||||||
- Mode mismatch → "Try switching between Auto/DCT/LSB"
|
|
||||||
|
|
||||||
### Files to Modify
|
|
||||||
- `src/stegasoo/__init__.py` (export exceptions)
|
|
||||||
- `src/stegasoo/exceptions.py` (new file)
|
|
||||||
- `src/stegasoo/dct_steganography.py`
|
|
||||||
- `src/stegasoo/steganography.py` (LSB)
|
|
||||||
- `frontends/web/app.py`
|
|
||||||
- `frontends/web/templates/decode.html`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -280,6 +244,6 @@ Polish and UX improvements after the 4.1.1 stability release.
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Keep 4.1.2 focused - 9 features (4 done)
|
- Keep 4.1.2 focused - 9 features (5 done)
|
||||||
- Don't break DCT compatibility (4.1.1 RS format is stable)
|
- Don't break DCT compatibility (4.1.1 RS format is stable)
|
||||||
- Test on Pi before release
|
- Test on Pi before release
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ from stegasoo import (
|
|||||||
CapacityError,
|
CapacityError,
|
||||||
DecryptionError,
|
DecryptionError,
|
||||||
FilePayload,
|
FilePayload,
|
||||||
|
InvalidHeaderError,
|
||||||
|
InvalidMagicBytesError,
|
||||||
|
ReedSolomonError,
|
||||||
StegasooError,
|
StegasooError,
|
||||||
export_rsa_key_pem,
|
export_rsa_key_pem,
|
||||||
generate_credentials,
|
generate_credentials,
|
||||||
@@ -1289,10 +1292,28 @@ def decode_page():
|
|||||||
has_qrcode_read=HAS_QRCODE_READ,
|
has_qrcode_read=HAS_QRCODE_READ,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except InvalidMagicBytesError:
|
||||||
|
flash(
|
||||||
|
"This doesn't appear to be a Stegasoo image. Try a different mode (LSB/DCT).",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
|
return render_template("decode.html", has_qrcode_read=HAS_QRCODE_READ)
|
||||||
|
except ReedSolomonError:
|
||||||
|
flash(
|
||||||
|
"Image too corrupted to decode. It may have been re-saved or compressed.",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
return render_template("decode.html", has_qrcode_read=HAS_QRCODE_READ)
|
||||||
|
except InvalidHeaderError:
|
||||||
|
flash(
|
||||||
|
"Invalid or corrupted header. The image may have been modified.",
|
||||||
|
"error",
|
||||||
|
)
|
||||||
|
return render_template("decode.html", has_qrcode_read=HAS_QRCODE_READ)
|
||||||
except DecryptionError:
|
except DecryptionError:
|
||||||
flash(
|
flash(
|
||||||
"Decryption failed. Check passphrase, PIN, RSA key, reference photo, and channel key.",
|
"Wrong credentials. Double-check your reference photo, passphrase, PIN, and channel key.",
|
||||||
"error",
|
"warning",
|
||||||
)
|
)
|
||||||
return render_template("decode.html", has_qrcode_read=HAS_QRCODE_READ)
|
return render_template("decode.html", has_qrcode_read=HAS_QRCODE_READ)
|
||||||
except StegasooError as e:
|
except StegasooError as e:
|
||||||
|
|||||||
@@ -112,12 +112,16 @@ from .exceptions import (
|
|||||||
ExtractionError,
|
ExtractionError,
|
||||||
ImageValidationError,
|
ImageValidationError,
|
||||||
InvalidHeaderError,
|
InvalidHeaderError,
|
||||||
|
InvalidMagicBytesError,
|
||||||
KeyDerivationError,
|
KeyDerivationError,
|
||||||
KeyGenerationError,
|
KeyGenerationError,
|
||||||
KeyPasswordError,
|
KeyPasswordError,
|
||||||
KeyValidationError,
|
KeyValidationError,
|
||||||
MessageValidationError,
|
MessageValidationError,
|
||||||
|
ModeMismatchError,
|
||||||
|
NoDataFoundError,
|
||||||
PinValidationError,
|
PinValidationError,
|
||||||
|
ReedSolomonError,
|
||||||
SecurityFactorError,
|
SecurityFactorError,
|
||||||
SteganographyError,
|
SteganographyError,
|
||||||
StegasooError,
|
StegasooError,
|
||||||
@@ -232,6 +236,10 @@ __all__ = [
|
|||||||
"ExtractionError",
|
"ExtractionError",
|
||||||
"EmbeddingError",
|
"EmbeddingError",
|
||||||
"InvalidHeaderError",
|
"InvalidHeaderError",
|
||||||
|
"InvalidMagicBytesError",
|
||||||
|
"ReedSolomonError",
|
||||||
|
"NoDataFoundError",
|
||||||
|
"ModeMismatchError",
|
||||||
# Constants
|
# Constants
|
||||||
"FORMAT_VERSION",
|
"FORMAT_VERSION",
|
||||||
"MIN_PASSPHRASE_WORDS",
|
"MIN_PASSPHRASE_WORDS",
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ except ImportError:
|
|||||||
HAS_JPEGIO = False
|
HAS_JPEGIO = False
|
||||||
jio = None
|
jio = None
|
||||||
|
|
||||||
|
# Import custom exceptions
|
||||||
|
from .exceptions import InvalidMagicBytesError, ReedSolomonError as StegasooRSError
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONSTANTS
|
# CONSTANTS
|
||||||
@@ -214,7 +217,7 @@ def _rs_decode(data: bytes) -> bytes:
|
|||||||
pass # Errors were corrected
|
pass # Errors were corrected
|
||||||
return bytes(decoded)
|
return bytes(decoded)
|
||||||
except ReedSolomonError as e:
|
except ReedSolomonError as e:
|
||||||
raise ValueError(f"Reed-Solomon decoding failed: {e}") from e
|
raise StegasooRSError(f"Image corrupted beyond repair: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -410,7 +413,7 @@ def _parse_header(header_bits: list) -> tuple[int, int, int]:
|
|||||||
magic, version, flags, length = struct.unpack(">4sBBI", header_bytes)
|
magic, version, flags, length = struct.unpack(">4sBBI", header_bytes)
|
||||||
|
|
||||||
if magic != DCT_MAGIC:
|
if magic != DCT_MAGIC:
|
||||||
raise ValueError("Invalid DCT stego magic bytes")
|
raise InvalidMagicBytesError("Not a Stegasoo image or wrong mode (try LSB instead of DCT)")
|
||||||
|
|
||||||
return version, flags, length
|
return version, flags, length
|
||||||
|
|
||||||
@@ -461,7 +464,7 @@ def _jpegio_parse_header(header_bytes: bytes) -> tuple[int, int, int]:
|
|||||||
raise ValueError("Insufficient header data")
|
raise ValueError("Insufficient header data")
|
||||||
magic, version, flags, length = struct.unpack(">4sBBI", header_bytes[:HEADER_SIZE])
|
magic, version, flags, length = struct.unpack(">4sBBI", header_bytes[:HEADER_SIZE])
|
||||||
if magic != JPEGIO_MAGIC:
|
if magic != JPEGIO_MAGIC:
|
||||||
raise ValueError(f"Invalid JPEG stego magic: {magic}")
|
raise InvalidMagicBytesError("Not a Stegasoo JPEG or wrong mode")
|
||||||
return version, flags, length
|
return version, flags, length
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,30 @@ class InvalidHeaderError(SteganographyError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMagicBytesError(SteganographyError):
|
||||||
|
"""Magic bytes don't match - not a Stegasoo image or wrong mode."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ReedSolomonError(SteganographyError):
|
||||||
|
"""Reed-Solomon error correction failed - image too corrupted."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoDataFoundError(SteganographyError):
|
||||||
|
"""No hidden data found in image."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ModeMismatchError(SteganographyError):
|
||||||
|
"""Wrong steganography mode (LSB vs DCT)."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# FILE ERRORS
|
# FILE ERRORS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user