diff --git a/PLAN-4.1.2.md b/PLAN-4.1.2.md index a4e82da..ef3040b 100644 --- a/PLAN-4.1.2.md +++ b/PLAN-4.1.2.md @@ -50,63 +50,27 @@ Polish and UX improvements after the 4.1.1 stability release. ## 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. **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:** - ```python - class StegasooError(Exception): pass - class InvalidMagicBytesError(StegasooError): pass - class DecryptionError(StegasooError): pass - 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` +### Files Modified +- `src/stegasoo/exceptions.py` (new exceptions) +- `src/stegasoo/__init__.py` (exports) +- `src/stegasoo/dct_steganography.py` (raise specific exceptions) +- `frontends/web/app.py` (catch and display) --- @@ -280,6 +244,6 @@ Polish and UX improvements after the 4.1.1 stability release. ## 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) - Test on Pi before release diff --git a/frontends/web/app.py b/frontends/web/app.py index 6a23c0b..709e13e 100644 --- a/frontends/web/app.py +++ b/frontends/web/app.py @@ -93,6 +93,9 @@ from stegasoo import ( CapacityError, DecryptionError, FilePayload, + InvalidHeaderError, + InvalidMagicBytesError, + ReedSolomonError, StegasooError, export_rsa_key_pem, generate_credentials, @@ -1289,10 +1292,28 @@ def decode_page(): 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: flash( - "Decryption failed. Check passphrase, PIN, RSA key, reference photo, and channel key.", - "error", + "Wrong credentials. Double-check your reference photo, passphrase, PIN, and channel key.", + "warning", ) return render_template("decode.html", has_qrcode_read=HAS_QRCODE_READ) except StegasooError as e: diff --git a/src/stegasoo/__init__.py b/src/stegasoo/__init__.py index a0a1be1..f2ab652 100644 --- a/src/stegasoo/__init__.py +++ b/src/stegasoo/__init__.py @@ -112,12 +112,16 @@ from .exceptions import ( ExtractionError, ImageValidationError, InvalidHeaderError, + InvalidMagicBytesError, KeyDerivationError, KeyGenerationError, KeyPasswordError, KeyValidationError, MessageValidationError, + ModeMismatchError, + NoDataFoundError, PinValidationError, + ReedSolomonError, SecurityFactorError, SteganographyError, StegasooError, @@ -232,6 +236,10 @@ __all__ = [ "ExtractionError", "EmbeddingError", "InvalidHeaderError", + "InvalidMagicBytesError", + "ReedSolomonError", + "NoDataFoundError", + "ModeMismatchError", # Constants "FORMAT_VERSION", "MIN_PASSPHRASE_WORDS", diff --git a/src/stegasoo/dct_steganography.py b/src/stegasoo/dct_steganography.py index 2c1727f..c2dc872 100644 --- a/src/stegasoo/dct_steganography.py +++ b/src/stegasoo/dct_steganography.py @@ -54,6 +54,9 @@ except ImportError: HAS_JPEGIO = False jio = None +# Import custom exceptions +from .exceptions import InvalidMagicBytesError, ReedSolomonError as StegasooRSError + # ============================================================================ # CONSTANTS @@ -214,7 +217,7 @@ def _rs_decode(data: bytes) -> bytes: pass # Errors were corrected return bytes(decoded) 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) 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 @@ -461,7 +464,7 @@ def _jpegio_parse_header(header_bytes: bytes) -> tuple[int, int, int]: raise ValueError("Insufficient header data") magic, version, flags, length = struct.unpack(">4sBBI", header_bytes[:HEADER_SIZE]) 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 diff --git a/src/stegasoo/exceptions.py b/src/stegasoo/exceptions.py index 60a0df2..520e56b 100644 --- a/src/stegasoo/exceptions.py +++ b/src/stegasoo/exceptions.py @@ -133,6 +133,30 @@ class InvalidHeaderError(SteganographyError): 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 # ============================================================================