426 Commits
2_2_1 ... main

Author SHA1 Message Date
adlee-was-taken
c970261e53 Merge branch 'main' of github.com:adlee-was-taken/stegasoo
Some checks failed
Lint / lint (push) Failing after 1m42s
Lint / typecheck (push) Failing after 30s
Tests / test (3.11) (push) Failing after 1m13s
Tests / test (3.12) (push) Failing after 42s
Tests / test (3.13) (push) Failing after 41s
2026-04-04 16:29:34 -04:00
adlee-was-taken
4607ff27dd Minor fixes 2026-04-04 16:29:20 -04:00
Aaron D. Lee
70b941d55a Pre-consolidation snapshot: backends, steganalysis, platform presets, and WIP changes
Snapshot of all uncommitted work before merging stegasoo into soosef monorepo.
Includes: pluggable backends registry, steganalysis detection, platform presets,
and various in-progress modifications across core modules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:56:36 -04:00
Aaron D. Lee
14fce4d3ed More video work, planning, etc. -- Need to mark things EXPERIMENTAL. 2026-03-24 16:00:30 -04:00
adlee-was-taken
05382c4081 Merge carrier type toggle into Step 1 accordion, reduce to 3 steps
Remove the dedicated Carrier Type accordion step and merge the
Image/Audio toggle into the Carrier & Mode step. The toggle now sits
in a two-column row aligned with the embedding mode buttons. Steps
renumbered from 4 to 3, carrier label changed to "Carrier File",
mode hint updates on carrier type switch via dispatched change events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:27:02 -05:00
adlee-was-taken
ef5a9ce9cb Add per-channel hybrid audio spread spectrum and env feature toggles
Spread spectrum v2: independent per-channel embedding with round-robin
bit distribution, preserving spatial stereo/surround mix. Adaptive chip
tiers (256/512/1024) trade capacity for lossy codec robustness. LFE
channel skipped for 5.1+ layouts. v2 header (20B) with backward-
compatible v0 decode fallback.

Environment toggles (STEGASOO_AUDIO, STEGASOO_VIDEO) gate audio/video
features for minimal builds (e.g. Raspberry Pi image-only). Values:
auto (default, detect deps), 1/true (force on), 0/false (force off).

Web UI fixes: accordion defaults to step 1 on load, chevron arrow
styling, required attribute toggling for audio carrier type switch,
"Images & Mode" renamed to "Reference, Carrier, Mode".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 11:58:40 -05:00
adlee-was-taken
0248bec813 Add audio steganography with LSB and spread spectrum modes
Implement two audio embedding modes following the same multi-factor
authentication pipeline as image steganography (passphrase + PIN +
optional RSA key + optional channel key):

- audio_lsb: High-capacity LSB embedding in PCM samples for lossless
  formats (WAV/FLAC). Uses ChaCha20-keyed sample index selection.
- audio_spread: Direct-sequence spread spectrum (DSSS) with ChaCha20-
  keyed bipolar chip codes, Reed-Solomon error correction, and 3-copy
  majority-voted length headers. Designed to survive lossy compression.

New files:
- audio_steganography.py: LSB embed/extract on PCM samples
- spread_steganography.py: Spread spectrum embed/extract
- audio_utils.py: Format detection, transcoding, validation helpers
- tests/test_audio.py: 22 tests covering both modes end-to-end

Updated encode.py, decode.py, cli.py (audio-encode/audio-decode
commands), constants.py, models.py, exceptions.py, validation.py,
__init__.py, and pyproject.toml ([audio] extra).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:26:07 -05:00
adlee-was-taken
7aeb26e003 Add CLAUDE.md project guide and worktree documentation
CLAUDE.md gives Claude Code instant context on architecture, commands,
conventions, security-critical modules, and public API surface.
docs/CLAUDE_WORKTREES.md is a beginner-friendly guide to using Claude
Code with git worktrees for isolated feature work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:45:09 -05:00
adlee-was-taken
1630d044aa Update WebUI screenshots for Encode/Decode pages
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 00:51:45 -05:00
adlee-was-taken
c2a0a731d7 Add READMEs for AUR CLI and API packages
- aur-cli: Lightweight CLI-only usage documentation
- aur-api: REST API with HTTPS/TLS configuration docs
- aur: Updated to reflect Python 3.11-3.14 support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 01:17:39 -05:00
adlee-was-taken
89de839fd8 Improve AUR post-install messages with service details
Add port numbers, HTTPS configuration instructions, and
systemctl enable commands to help users get started.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 01:15:15 -05:00
adlee-was-taken
49566292ba Fix channel badge centering when navbar collapses
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 01:13:17 -05:00
adlee-was-taken
9f0e0afeb6 Fix BIP-39 wordlist not found when installed via pip/AUR
Move bip39-words.txt into package data directory and use
importlib.resources for reliable path resolution. The wordlist
was previously only included in sdist but not in wheel builds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 01:12:45 -05:00
Aaron D. Lee
398a359778 Add Windows Docker/WSL2 docs, update Python version to 3.11-3.14
- Windows: Add Docker Desktop and WSL2 options (recommended)
- Windows: Keep native install as advanced option
- Update Python badge and requirements tables

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:21:58 -05:00
Aaron D. Lee
86aa5cbddf Add pypi environment to release workflow
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:10:13 -05:00
Aaron D. Lee
2f54f80214 Update release notes for v4.2.1
Some checks failed
Release / test (push) Failing after 35s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:00:20 -05:00
Aaron D. Lee
1cd2656e60 Update CI to Python 3.11-3.13 (drop 3.10 support)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:56:50 -05:00
Aaron D. Lee
ce728cec6e Update .SRCINFO files for v4.2.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:23:05 -05:00
Aaron D. Lee
555735a4fd Add Docker artifacts to gitignore
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:21:31 -05:00
Aaron D. Lee
08b70043e4 Update PKGBUILD versions and add AUR test scripts
- Update pkgver fallback to 4.2.1 in all PKGBUILDs
- Add test-aur-build.sh for Docker-based testing
- Add test-aur-nspawn.sh for systemd-nspawn testing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:31:00 -05:00
Aaron D. Lee
d395e5731e Fix CLI import paths for installed packages
The CLI api commands were using a hardcoded path to find frontends/
which didn't work when installed as a package. Now tries both:
- Development: .../stegasoo/frontends
- Installed: .../site-packages/frontends

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:29:06 -05:00
Aaron D. Lee
110b160e68 Bump version to 4.2.1
Release highlights:
- API key authentication (X-API-Key header)
- TLS with self-signed certificates
- CLI tools: compress, rotate, convert
- jpegtran lossless JPEG rotation
- AUR packages: stegasoo-cli-git, stegasoo-api-git
- Bug fixes: DCT rotation, jpegtran -trim, CLI output format

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:18:13 -05:00
Aaron D. Lee
b09f607d34 Add stegasoo-api AUR package
New package in aur-api/ for API-only installation:
- Installs [api,cli,compression] extras
- Has fastapi/uvicorn for REST API
- No flask/gunicorn (web UI deps)
- 74MB package size
- Systemd service with TLS enabled by default

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:13:26 -05:00
Aaron D. Lee
34ede3815f Add API key authentication and TLS support
API Authentication (v4.2.1):
- API key auth via X-API-Key header
- Keys hashed (SHA-256) and stored in ~/.stegasoo/api_keys.json
- Auth disabled when no keys configured
- Protected endpoints: encode, decode, generate, channel/*, compare, etc.
- Public endpoints: /, /docs, /modes, /auth/status, /channel/status

TLS Support:
- Auto-generates self-signed certs on first run
- Certs include localhost, local IPs, hostname.local
- Stored in ~/.stegasoo/certs/

CLI Commands:
- stegasoo api keys list/create/delete
- stegasoo api tls generate/info
- stegasoo api serve (starts with TLS by default)

Updated systemd service to use TLS.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:03:51 -05:00
Aaron D. Lee
3b5ab41ce9 Add stegasoo-cli AUR package (CLI-only, no web deps)
New package in aur-cli/ for CLI-only installation:
- Installs [cli,dct,compression] extras only
- No flask/gunicorn/fastapi/uvicorn/pyzbar dependencies
- 68MB vs 79MB for full package
- Conflicts with stegasoo-git (can't install both)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 17:19:31 -05:00
Aaron D. Lee
525bcec3c9 Add compress, rotate, convert tools to CLI
Port Web UI image tools to CLI for parity:
- compress: JPEG compression with size reduction stats
- rotate: Rotation and flip with jpegtran for JPEGs (DCT-safe)
- convert: Format conversion between PNG, JPG, BMP, WebP

Rotate tool supports flip-only operations without rotation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 17:10:55 -05:00
Aaron D. Lee
afc8c93923 Fix CLI encode format detection and jpegtran -trim bug
CLI encode:
- Auto-detect output format from extension (.jpg → DCT mode, .png → LSB)
- Default to JPEG output for JPEG carriers (preserves DCT benefits)
- Pass embed_mode and dct_output_format to encode function

jpegtran fix (critical for rotation fallback):
- Remove -trim flag which was dropping edge blocks and destroying stego data
- Remove -perfect flag which fails on non-MCU-aligned images
- Plain jpegtran without flags works correctly for lossless rotation

This enables: encode → external rotation → decode to work correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 17:06:53 -05:00
Aaron D. Lee
38bef32750 Redesign EXIF viewer and compact tools UI
EXIF Viewer:
- Card-based grid layout with categories (Camera, Image, Date/Time, Exposure, GPS, Other)
- Icons for each category
- Truncation for long values with full value on hover

Tools UI:
- Reduced padding from 1.25rem to 0.5rem on all tool panels
- Smaller fonts for labels (0.55rem) and values (0.7rem)
- Compact headers and action buttons
- Tighter grid gaps and card padding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 16:50:59 -05:00
Aaron D. Lee
4e3acfca20 Add jpegtran lossless rotation and EXIF orientation handling
DCT steganography improvements:
- Add _apply_exif_orientation() to fix portrait photos encoding rotated
- Add _jpegtran_rotate() for lossless JPEG rotation preserving DCT data
- Add rotation fallback in extract_from_dct() - tries 0°, 90°, 180°, 270°
- Quick header validation to skip invalid rotations efficiently
- Fix: wrap debug.print in try/except to prevent extraction failures

Web UI rotate tool:
- Use jpegtran for JPEGs (lossless, preserves DCT steganography)
- Fall back to PIL for non-JPEGs
- Dynamic UI shows "DCT Safe" for JPEGs, warning for other formats

This enables the workflow: encode → compress → rotate → decode
Rotated stego JPEGs can now be decoded by trying all orientations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 16:36:52 -05:00
Aaron D. Lee
2ebc42f2cd Fix EXIF viewer breaking on binary MakerNote fields
Some checks failed
Release / test (push) Failing after 43s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
Pentax and other cameras have binary EXIF fields (MakerNote, etc.) that
contain raw bytes. The previous code used errors="replace" which still
produced strings with replacement characters that broke JSON parsing.

Now properly detect non-printable binary data and display as
"<N bytes binary>" instead.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:40:12 -05:00
Aaron D. Lee
1e07630b49 Update docs and comments for jpeglib migration (v4.2.0)
- Replace jpegio references with jpeglib in comments/docstrings
- Update sanitize-for-image.sh to use system Python 3.11+ (no pyenv)
- Update rpi/patches/README.md for jpeglib world
- Add AUR build artifacts to .gitignore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 11:19:16 -05:00
Aaron D. Lee
67037ae196 RPi: Standardize tarball naming to stegasoo-rpi-venv-arm64.tar.zst
- Update remote-build-pi.sh to use new naming
- Rewrite build-runtime-tarball.sh for pyenv-free world (system Python)
- Removed pyenv references, now just tarballs the venv

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 10:59:07 -05:00
Aaron D. Lee
5a68840725 RPi: Fix jpeglib ARM64 build - skip turbo/mozjpeg correctly
Only remove dictionary entries for turbo/mozjpeg versions in setup.py
(they need cmake-generated headers). Keep the if blocks intact - they
safely evaluate to False for standard libjpeg versions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 23:15:34 -05:00
Aaron D. Lee
ebc999b2b3 RPi: Simpler approach - filter lines containing turbo/mozjpeg 2026-01-10 23:10:10 -05:00
Aaron D. Lee
f46ef01f5f RPi: Fix setup.py patching (use Python regex instead of sed) 2026-01-10 23:06:17 -05:00
Aaron D. Lee
0d76780deb RPi: Skip turbo/mozjpeg (need cmake-generated headers) 2026-01-10 23:03:28 -05:00
Aaron D. Lee
d34919e32f RPi: Download exact header version for each libjpeg 2026-01-10 22:42:59 -05:00
Aaron D. Lee
a4038589b0 RPi: Download matching libjpeg headers for each version
APIs changed between libjpeg versions, so each version directory
needs its matching headers:
- 6b gets 6b headers
- 7 gets 7 headers
- 8-8d get 8d headers
- 9-9f get 9f headers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:37:27 -05:00
Aaron D. Lee
db763f1464 Fix jpeglib tag (1.0.2 not v1.0.2) 2026-01-10 22:33:15 -05:00
Aaron D. Lee
27c5b08d41 RPi: Fix jpeglib include path (symlink src/jpeglib -> jpeglib)
The setup.py has broken include_dirs that reference 'jpeglib/cjpeglib'
but the source files are in 'src/jpeglib/cjpeglib'. Create a symlink
to fix the include path resolution.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:30:59 -05:00
Aaron D. Lee
28cb9bb9b3 RPi: Clone jpeglib from GitHub (PyPI tarball incomplete)
The PyPI source tarball is missing both jpeglib's own headers
(cjpeglib_common.h, etc.) and libjpeg headers. Clone from GitHub
which has the jpeglib headers, then download libjpeg headers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:30:21 -05:00
Aaron D. Lee
889df881ba RPi: Fix jpeglib ARM64 build (missing headers)
jpeglib has no pre-built ARM64 wheel and the source tarball is missing
libjpeg header files. This adds a workaround that downloads the official
libjpeg headers before building.

- Add rpi/patches/jpeglib/install-jpeglib-arm64.sh helper script
- Update setup.sh to download headers when building from source
- Downloads headers for libjpeg 6b, 7-9f, turbo, and mozjpeg versions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:20:40 -05:00
Aaron D. Lee
c058d116b8 RPi: Remove pyenv, use system Python 3.11+
- Rewrite setup.sh to use system Python instead of pyenv
- Add Python version check (3.11-3.14 supported)
- Remove jpegio build steps (jpeglib installs cleanly via pip)
- Simplify prebuilt tarball (just venv, no pyenv)
- Reduce install time: 5-10 min from source (was 15-20 min)
- Update README.md and BUILD_IMAGE.md accordingly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:35:50 -05:00
Aaron D. Lee
fae86887e2 Update RPi scripts and docs for v4.2.0
- Default branch: 4.1 → 4.2
- Update prebuilt URL to v4.2.0
- Update example filenames to 4.2.0
- Remove jpegio references (now using jpeglib)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:25:07 -05:00
Aaron D. Lee
5e45b2c5c1 AUR: Add zbar dependency, create temp_files directories
- Move zbar from optdepends to depends (required for Web UI QR reading)
- Create temp_files directories for web and api frontends

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 20:59:48 -05:00
Aaron D. Lee
71088989f3 Migrate from jpegio to jpeglib for Python 3.13+ support
- Replace jpegio with jpeglib (jpeglib.to_jpegio compatibility layer)
- Update Python requirement to >=3.11, add 3.13/3.14 classifiers
- AUR: Add install script for user creation and permissions
- AUR: Install frontends to site-packages, create Flask instance dir
- AUR: Use dynamic ${pyver} for systemd WorkingDirectory

Tested: CLI, Web UI (Gunicorn), API (Uvicorn), DCT jpeglib roundtrip

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 20:09:52 -05:00
Aaron D. Lee
530e5debef Include frontends in wheel, fix systemd WorkingDirectory 2026-01-10 16:58:16 -05:00
Aaron D. Lee
3b062458e3 Update Web UI screenshots and fix script for HTTPS 2026-01-10 16:15:20 -05:00
Aaron D. Lee
5e65035ca4 Fix AUR venv shebang paths 2026-01-10 16:06:08 -05:00
Aaron D. Lee
de9d1de881 Fix AUR package build for Python 3.12 2026-01-10 15:21:44 -05:00
Aaron D. Lee
8d90a888cf Add AUR package (stegasoo-git)
- Uses python312 from AUR for jpegio 3.13 compatibility
- Self-contained venv in /opt/stegasoo
- Includes systemd service files for web and API
- CLI symlinked to /usr/bin/stegasoo

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 12:55:20 -05:00
Aaron D. Lee
b0914778e3 Add zstandard to Docker base image
- Added zstandard>=0.22.0 to base image dependencies
- Updated verification to check zstd import
- Bumped base image version label to 4.2.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 00:07:18 -05:00
Aaron D. Lee
7e5462ea6e Add rebuild option to build.sh for complete no-cache builds
- rebuild: cleans everything, rebuilds base and services with --no-cache
- Updated help text to clarify full vs rebuild

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 00:04:00 -05:00
Aaron D. Lee
e085a8ffe9 Update release notes for v4.2.0
Added documentation for:
- Zstd default compression
- QR code generation (CLI and API)
- RSA 3072 cap, file expiry, progress bar UX
- Updated summary table with QR compression improvement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:46:44 -05:00
Aaron D. Lee
2d7fbd1e0d Add QR code generation to CLI and API
CLI generate command:
- --qr <file.png|jpg> to save RSA key as QR image
- --qr-ascii to print ASCII QR code to terminal

API endpoints:
- POST /generate-key-qr - generate QR from key_pem
  - Supports png, jpg, and ascii output formats
  - Uses zstd compression by default
- Added has_qrcode_write to /capabilities

Core:
- generate_qr_code() now supports jpg/jpeg output format
- New generate_qr_ascii() for terminal display

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:38:51 -05:00
Aaron D. Lee
32842f6b73 Move zstandard to core dependencies
zstd is now the default compression algorithm across all frontends,
so it should always be installed with the base package.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:32:49 -05:00
Aaron D. Lee
3fd3204552 Cap RSA at 3072 bits, add zstd compression for QR codes
- RSA key size capped at 3072 bits (4096 too large for QR codes)
- Added zstd compression for QR code RSA keys (better ratio than zlib)
- New prefix STEGASOO-ZS: for zstd, backward compatible with STEGASOO-Z: (zlib)
- Added zstandard dependency to web/api/compression extras
- Updated all docs, CLI options, and web UI to reflect 3072 max
- Version bump to 4.2.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:30:31 -05:00
Aaron D. Lee
175362ce4c Fix async encode returning HTML errors instead of JSON
When encode form was submitted in async mode, validation errors
returned HTML (render_template) instead of JSON, causing
"Unexpected token '<'" parse errors in the browser.

Added _error_response() helper that returns JSON in async mode
and HTML flash in sync mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:17:51 -05:00
Aaron D. Lee
2ed108f3a0 Fix decode progress - add Argon2 phase tracking
- decode.py now writes 20% "initializing" before Argon2
- decode.py writes 25% "extracting" after Argon2 completes
- DCT extraction scales from 25-70% (was 5-70%)
- Removed duplicate "loading" writes that caused backwards jumps

Progress flow: 15% reading -> 20% initializing (Argon2) -> 25-70% extracting -> 75-95% decoding -> 100% complete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:14:10 -05:00
Aaron D. Lee
167e1a6ff5 Add optional REST API systemd service for Pi
- Create stegasoo-api.service for FastAPI on port 8000
- Prompt user during setup with security warning (no auth)
- Default to disabled (recommended)
- Update help text and start commands for both services

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:09:48 -05:00
Aaron D. Lee
f2f3e2eefc Fix decode progress - pass progress_file to library
Worker was writing 25% then calling decode() without progress_file,
so library couldn't update progress. Now passes progress_file through
so library's extraction/RS-decode progress updates work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:03:07 -05:00
Aaron D. Lee
5c685cba67 Fix decode progress getting stuck at 25%
- Reduce PROGRESS_INTERVAL from 2000 to 500 for responsive updates
- Scale extraction progress to 5-70% range
- Add progress updates before/after RS decode (75% and 95%)
- RS decode is the slow part, now visible in progress

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:48:53 -05:00
Aaron D. Lee
4e819b80cc Improve progress phase messages for key derivation
Show "Deriving keys (may take a moment)..." during Argon2 phase
to set user expectations on slower devices

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:45:16 -05:00
Aaron D. Lee
ea86216648 Bump pyproject.toml version to 4.2.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:42:14 -05:00
Aaron D. Lee
8de5659fa6 Increase temp file expiry from 5 to 10 minutes
- Update TEMP_FILE_EXPIRY constant (300 -> 600 seconds)
- Update all UI references to the new 10 minute expiry

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:41:09 -05:00
Aaron D. Lee
de0bf2410d Improve progress bar UX for encode/decode
- Add indeterminate (barber pole) animation during Argon2/initializing phase
- Prevent progress from jumping backwards (fixes flash-to-zero bug)
- Initial progress write at 5% when embedding actually starts
- Reset progress tracking on new operations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:38:42 -05:00
Aaron D. Lee
8b948d00a4 Update about page version history for v4.2.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:22:41 -05:00
Aaron D. Lee
6d88453b69 Update release notes for v4.2.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:16:34 -05:00
Aaron D. Lee
ea57bdf302 Make API encode/decode endpoints async with thread pool
- Added run_in_thread() helper using asyncio.to_thread()
- /encode, /encode/file, /decode use thread pool for CPU-bound ops
- /encode/multipart, /decode/multipart also updated
- Server can now handle concurrent requests without blocking
- Updated version header to v4.2.0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:13:57 -05:00
Aaron D. Lee
55d54717f8 Use float32 instead of float64 for DCT operations
Reduces peak memory usage by ~50%:
- ENCODE: 211 MB -> 107 MB
- DECODE: 104 MB -> 52 MB

float32 provides sufficient precision for 8-bit images
(DCT roundtrip error ~1.8e-7, well under 0.5 threshold).

Significant improvement for Pi deployments with limited RAM.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:10:20 -05:00
Aaron D. Lee
c0fe85ac83 Add progress_file support to DCT extraction
- Added progress_file parameter to extract_from_dct, _extract_scipy_dct_safe, _extract_jpegio
- Progress writes at key phases: loading, extracting, decoding, complete
- Updated extract_from_image and _extract_dct to pass through progress_file
- Updated decode(), decode_file(), decode_text() with progress_file param
- Progress JSON format: {current, total, percent, phase}

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:01:20 -05:00
Aaron D. Lee
e9e4d1aab9 Vectorize DCT encode/decode for ~14x speedup
- Use scipy.fft.dctn/idctn with axes=(1,2) to process 500 blocks at once
- Extract bits in batch using numpy array indexing
- Vectorized QIM embedding with array operations
- Tests pass, roundtrip verified

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 21:52:51 -05:00
Aaron D. Lee
1acb5a3dcc Update release notes for v4.1.7
Some checks failed
Release / test (push) Failing after 30s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 21:38:14 -05:00
Aaron D. Lee
14a73c63ac Add reedsolo to Docker, update docs for docker/ paths
- Add reedsolo>=1.7.0 to Dockerfile and Dockerfile.base for DCT
  error correction (fixes DCT decode failures in container)
- Update all documentation to use docker/docker-compose.yml paths

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 21:20:52 -05:00
Aaron D. Lee
3d53282738 Move pishrink.sh to rpi/tools/
Update .gitignore and .dockerignore paths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 18:08:19 -05:00
Aaron D. Lee
e831ae4884 Move Docker files to docker/ directory
- Move Dockerfile, Dockerfile.base, docker-compose.yml to docker/
- Update docker-compose.yml with correct context paths
- Update scripts/build.sh to use new paths
- Update DOCKER_QUICKSTART.md with new commands
- Add scripts/build.sh to tracked files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 18:01:15 -05:00
Aaron D. Lee
4751d05e9f Add Pi runtime tarball build script
Run on Pi after from-source build to create:
stegasoo-rpi-runtime-env-arm64.tar.zst (~50-60MB)

Contains pyenv + Python 3.12 + venv with all deps.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 17:59:08 -05:00
Aaron D. Lee
d15bcb8df4 Change 'Undetectable' to 'Covertly Embedded'
Less definitive claim for the encode page footer.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 17:54:22 -05:00
Aaron D. Lee
6ec7de5604 Add Docker quickstart guide
Concise guide covering:
- Build commands
- Basic and production run examples
- Environment variables table
- Custom SSL certs (own, mkcert, Let's Encrypt)
- Volumes and troubleshooting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 17:48:10 -05:00
Aaron D. Lee
1cdb2aca91 Bump mobile PIN digit font-size to 1.15rem
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:27:03 -05:00
Aaron D. Lee
46de371c42 Shrink PIN digit boxes on mobile for 9-digit support
Reduce box width, height, font-size, gap, and container padding.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:24:44 -05:00
Aaron D. Lee
11c0d45548 Make decode mode selector buttons wider
Remove btn-sm for regular-sized buttons on decode page.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:21:51 -05:00
Aaron D. Lee
7bb1029c0f Add text-nowrap and icons to decode mode selector
Match encode page styling with icons on all buttons.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:19:27 -05:00
Aaron D. Lee
e3f7f36e5e Prevent DCT/LSB button text from wrapping
Add text-nowrap to keep icon and text together.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:17:47 -05:00
Aaron D. Lee
f200737088 Improve DCT/LSB selector with icons and divider
- Add grid icon to LSB button to match DCT soundwave icon
- Add divider between mode and output options (hidden on mobile)
- Wraps cleanly on small screens

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:15:56 -05:00
Aaron D. Lee
6def318ba7 Left-align collapsed navbar menu on mobile
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:09:47 -05:00
Aaron D. Lee
e203af6a73 Add redacted dots to channel key preview in header
Shows ABCD-••••-3456 instead of ABCD...3456 to indicate
the key is longer and has been redacted.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:07:38 -05:00
Aaron D. Lee
6ba135098b Use consistent button group style for mode selectors
Convert DCT/LSB (encode) and Auto/LSB/DCT (decode) to use
Bootstrap btn-group style matching Color/Gray and JPEG/PNG.
Better mobile layout - all options on one line.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:03:08 -05:00
Aaron D. Lee
903739c055 Remove divider between color/format options for mobile
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:01:18 -05:00
Aaron D. Lee
30fbb5016e Shorten channel fingerprint in navbar for mobile
Display ABCD...3456 instead of full masked fingerprint.
Full fingerprint still visible in tooltip on hover.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 17:00:07 -05:00
Aaron D. Lee
041148e8fe Bump version to 4.1.7
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 16:54:12 -05:00
Aaron D. Lee
90bedce379 Add channel key loading option to first-boot wizard
Step 3 now offers three choices:
- Skip (public mode)
- Generate new key
- Enter existing key (for joining team deployments)

Validates entered keys using Python channel module before accepting.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 16:48:10 -05:00
Aaron D. Lee
021265f3cf Add screenshot capture script for documentation
- Capture main UI pages (Encode, Decode, Generate, Tools, About)
- Capture auth pages (Login, Setup, Account, Recover)
- Auto-convert PNG to WebP for smaller file sizes
- Update .gitignore to track this script

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 16:02:34 -05:00
Aaron D. Lee
ff42398509 Simplify wipe - remove dd zero, just use wipefs 2026-01-08 13:45:20 -05:00
Aaron D. Lee
a30ec33b98 Fix SD card flashing progress display
- Remove pv (showed read progress, not write progress)
- Use dd status=progress for actual write progress
- Reduce block size to 1M (better for slow SD cards)
- Remove conv=fsync (sync at end instead, faster)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:40:31 -05:00
Aaron D. Lee
252efbec7e Add filesystem validation after flashing
Run fsck.vfat on boot partition and e2fsck on root partition
after flashing to catch and fix any corruption.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:00:37 -05:00
Aaron D. Lee
6e906d5981 Run growpart/resize2fs directly without gum spin 2026-01-08 12:40:46 -05:00
Aaron D. Lee
df6125d098 Use growpart before resize2fs to expand full disk
resize2fs only fills the partition. Need growpart first to
expand the partition to fill the disk, then resize2fs to
expand the filesystem.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 12:36:06 -05:00
Aaron D. Lee
3d4a340305 Add prompt for filesystem expansion in wizard
Show current size and ask user before expanding, matching
the style of other wizard prompts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 12:30:39 -05:00
Aaron D. Lee
0decb39b17 Move filesystem expansion to first-boot wizard
Instead of a hidden systemd service, expand the filesystem
visibly during the first-boot wizard so users can see it
happening.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 12:00:47 -05:00
Aaron D. Lee
4291dfad38 Remove rpi-imager, use dd directly
rpi-imager was doing something that prevented the auto-expand
service from working. Simplify to just dd with optional pv
for progress.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 11:33:03 -05:00
Aaron D. Lee
ddee3583e8 Defer wipe until after final confirmation
Move the partition wipe to after user types 'yes' so they can
still abort without having already wiped the device.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 11:21:41 -05:00
Aaron D. Lee
3e2307cbcf Fix auto-expand service creation (add sudo)
The script runs as non-root but needs sudo to write to the
mounted rootfs partition.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 11:15:21 -05:00
Aaron D. Lee
cc745fbdfa Add auto-expand service in pull-image.sh
Create a systemd oneshot service that expands the rootfs on first boot
after flashing. The service self-destructs after running.

This ensures release images fill the SD card while keeping the
download size small (16GB shrunk image).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 11:02:01 -05:00
Aaron D. Lee
3027706d49 Keep auto-expand enabled in release images
The shrinking is only for faster image downloads. After flashing,
the image should auto-expand to fill the SD card.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 10:57:31 -05:00
Aaron D. Lee
39fbd617e6 Remove unused compression options, add man page installation
- Remove --compress/--algorithm CLI options (not wired to encode flow)
- Add man page installation to rpi/setup.sh
- Document man page installation in README.md and CLI.md
- Update man page to remove compression options

Compression will be properly implemented in v4.1.8.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 00:28:15 -05:00
Aaron D. Lee
de4cb0b3be Add stegasoo(1) man page
Comprehensive documentation covering all commands, options,
and usage examples for the CLI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 00:13:02 -05:00
Aaron D. Lee
add3951003 Remove color from channel fingerprint display
The color codes weren't displaying properly in all terminal
environments. Keep it simple with plain text.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 00:09:52 -05:00
Aaron D. Lee
3858e234da Fix channel fingerprint color using Click's native style API
Use click.style() with bright_yellow and color=True to ensure
the channel fingerprint displays in color across different
terminal environments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 00:07:24 -05:00
Aaron D. Lee
03e8e3a840 Try bold yellow for channel fingerprint color
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 00:04:36 -05:00
Aaron D. Lee
55e78d0503 Change channel fingerprint color to orange
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 00:03:39 -05:00
Aaron D. Lee
b13a9fcd3f Add cyan color to channel fingerprint in CLI info
Private channel fingerprints now display in cyan to stand out.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 00:02:23 -05:00
Aaron D. Lee
96b49c68ec Fix get_channel_status() to decrypt stored keys
The function was trying to format encrypted keys directly,
causing ValueError when reading ENC: prefixed stored keys.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 00:00:30 -05:00
Aaron D. Lee
be8744179d Encrypt stored channel keys with machine identity
Channel keys saved to config files are now encrypted using the
machine's identity (/etc/machine-id), so:
- Not stored in plaintext
- Tied to specific machine (can't copy file to another device)
- Legacy plaintext keys still work (auto-detected)

Changes:
- Added _encrypt_for_storage() and _decrypt_from_storage()
- set_channel_key() now encrypts before writing
- get_channel_key() decrypts when reading (handles legacy plaintext)
- Pi setup saves encrypted key to ~/.stegasoo/channel.key
- CLI `stegasoo info` now shows channel status correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:54:23 -05:00
Aaron D. Lee
f971b75d7e Add mkcert support for browser-trusted HTTPS certificates
No more browser warnings! mkcert creates locally-trusted certs.

Pi Setup:
- Auto-install mkcert during setup
- Generate trusted certs when HTTPS enabled
- Copy CA to /static/ca/rootCA.pem for easy device setup
- New devices can download CA via HTTP and install it

Docker:
- docker-entrypoint.sh checks for mkcert, falls back to openssl
- Shows instructions for CA distribution to other devices

Scripts:
- Added setup-trusted-certs.sh helper for local dev
- Installs mkcert, generates certs, shows device setup instructions

To trust on new devices:
1. Download: http://stegasoo.local/static/ca/rootCA.pem
2. Install as trusted CA in browser/OS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:49:38 -05:00
Aaron D. Lee
455c6dfd01 Docker HTTPS by default, smoke test improvements
Docker:
- HTTPS enabled by default (generates self-signed cert)
- Added docker-entrypoint.sh for SSL cert generation
- Gunicorn now starts with --certfile/--keyfile when HTTPS enabled
- Install curl/openssl in web container for healthcheck and certs
- Updated docs to reflect HTTPS default

Smoke Test:
- Moved from rpi/ to scripts/ (works for Pi, Docker, and dev)
- Updated header and examples
- Added to .gitignore exceptions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:45:44 -05:00
Aaron D. Lee
a00a154a1a Add Pi smoke test script
Comprehensive test suite for Pi deployments:
- Connectivity check
- Auto-setup if first boot (admin/stegasoo)
- Login and session handling
- All page accessibility
- LSB encode/decode round trip
- DCT encode/decode round trip
- Tools API (capacity, EXIF)

Usage: ./rpi/smoke-test.sh [host] [port] [user] [pass]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:25:45 -05:00
Aaron D. Lee
8b3b331843 Fix: run update-ca-certificates after install
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:10:59 -05:00
Aaron D. Lee
10c874374f Fix QR key loader: remove conflicting CSS, proper centering
- Remove duplicate qr-crop-container styles from encode/decode templates
- Use only qr-scan-container from style.css (flex centering + object-fit)
- Fix rsaQrSection to use align-items: center for horizontal centering
- Darken channel key fingerprint in header (#f0c674 → #c9a860)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:05:07 -05:00
Aaron D. Lee
0c1e87c7c0 Replace kickoff-pi-test.sh with remote-build-pi.sh
Simplified Pi build script:
- No imaging step (assumes SD card already flashed)
- Waits for Pi to be reachable via SSH
- Installs deps (including ca-certificates for git SSL)
- Clones from main branch (has updated tarball name)
- Copies pre-built tarball if available
- Runs setup and tests

Usage: ./remote-build-pi.sh [host] [user] [pass]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 22:36:35 -05:00
Aaron D. Lee
d517a4dc8b Accordion chevrons: less orange, more muted gold
Reduced saturation (10→2), hue-rotate (15→5), brightness (1.5→1.2)
for a subtler gold that matches the toned-down color scheme.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 22:15:02 -05:00
Aaron D. Lee
6d59f3edfc UX polish: toned gold, cleaner labels, dropdown chevrons
- Toned down gold colors for better cross-monitor consistency
  - Header gold: #fee862 → #e5d058
  - Form labels: #ffe699 → #d9c580
- Removed text-shadow/outline from form labels (was smudgy)
- Removed background from nav floating labels
- More subtle nav hover background (halved opacity)
- Gold chevron on all dropdown selects for clarity
- Removed (environment) tag from channel key display
- Simplified channel key config text in about page
- Generate page: icon-only button for channel key

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 22:13:32 -05:00
Aaron D. Lee
17d0406be2 Homepage icons: clean white with gold hover
Removed gold outline/stroke from default state - too harsh on
some monitors. Now simple white icons that turn gold on hover
like the header nav, with lift effect and drop shadow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 21:55:25 -05:00
Aaron D. Lee
ef73280015 Homepage polish: tagline styling, icon spacing, tooltips
- Tagline: smaller font, drop shadow, 3px offset, 3px left padding
- Icons: reduced gap from gap-5 to gap-4
- Channel badge tooltips: descriptive hover text for private/public

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 21:50:12 -05:00
Aaron D. Lee
6338d6aab4 v4.1.6 UX polish: homepage, navbar, tools consistency
Homepage:
- Minimal floating icons with gold hover effect
- Larger Stegasoo title (display-5)
- v4.1 badge repositioned to bottom-left of logo
- Tighter 8px gap between logo and title

Navbar:
- Container-fluid for fixed left positioning
- Reduced left padding, proper logo/badge spacing
- Channel fingerprint in gold, shield icon brighter

Tools page:
- Consistent font styling (0.62rem, weight 500, 1px spacing)
- Wider buttons (64px) with more gap
- Bolder text on hover (weight 600)

Typography consistency across nav, homepage, and tools.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 21:35:42 -05:00
Aaron D. Lee
b9d0fac535 Homepage icons: stronger shadow and dark outline for pop
Added dark outline via text-shadow and increased drop-shadow
opacity to make white icons stand out against dark background.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 20:51:14 -05:00
Aaron D. Lee
5c0a5bbba7 Homepage icons: gold on hover like tools page
Icons and labels turn gold on hover, matching the tools page
button styling. Combined with lift effect for visual pop.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 20:49:59 -05:00
Aaron D. Lee
ba1a77f00b Homepage icons: white with lift/shadow hover effect
Removed colored icons per user preference. Now using clean white
icons with subtle drop shadow that lifts and deepens on hover for
a "pop" visual cue without glow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 20:49:05 -05:00
Aaron D. Lee
5e587df545 Show 'Public Channel' badge when no channel key configured 2026-01-07 20:44:20 -05:00
Aaron D. Lee
23456ac1e4 Vibrant colored icons: blue/green/gold, glow on hover 2026-01-07 20:43:13 -05:00
Aaron D. Lee
8be512ad7b Reduce left padding on channel badge 2026-01-07 20:42:00 -05:00
Aaron D. Lee
f129500202 Channel badge: thinner key text, brighter shield, more spacing 2026-01-07 20:41:35 -05:00
Aaron D. Lee
c37d743b3e Compact navbar: small logo only, gold fingerprint, v4.1 badge on homepage 2026-01-07 20:40:29 -05:00
Aaron D. Lee
5bdb625059 Move channel indicator to navbar header (shield + fingerprint) 2026-01-07 20:38:14 -05:00
Aaron D. Lee
231ba97fde Pull channel banner tight under navbar with negative margins 2026-01-07 20:37:05 -05:00
Aaron D. Lee
a70e88625f Pin channel banner full-width under navbar 2026-01-07 20:35:17 -05:00
Aaron D. Lee
b6770c46e5 Restore compact alert-style channel banner (320px max) 2026-01-07 20:34:26 -05:00
Aaron D. Lee
9f4318cc0f Move channel status above hero, same width 2026-01-07 20:34:01 -05:00
Aaron D. Lee
91dc665a77 Ultra-minimal homepage: floating icons, hover labels, compact hero 2026-01-07 20:33:39 -05:00
Aaron D. Lee
6066df391b Clean minimal homepage - hero, 3 action cards, quick info footer 2026-01-07 20:26:58 -05:00
Aaron D. Lee
be5c95b59d Bright gold chevrons matching indicator line 2026-01-07 20:16:20 -05:00
Aaron D. Lee
09b1abddc7 Brighter gold chevrons on accordions 2026-01-07 20:15:16 -05:00
Aaron D. Lee
0c9ea0e3f2 Accordion headers: gold left border, warmer tint, visible chevrons 2026-01-07 20:14:02 -05:00
Aaron D. Lee
aebfb20dfc Gold hover effect on tools page buttons (Capacity, EXIF, etc.) 2026-01-07 20:07:55 -05:00
Aaron D. Lee
b935c474af Drop shadow at 25% opacity 2026-01-07 20:04:33 -05:00
Aaron D. Lee
73b34ba8b5 Subtle 10% drop shadow on gold labels and toggles 2026-01-07 20:04:12 -05:00
Aaron D. Lee
89d8fee5da Gold toggle text with outline and drop shadow like labels 2026-01-07 20:03:09 -05:00
Aaron D. Lee
0e270dadb3 Just gold text on selected toggle, keep purple/blue styling 2026-01-07 20:02:31 -05:00
Aaron D. Lee
e2002b6026 Force gold styling with !important for toggle buttons 2026-01-07 20:01:54 -05:00
Aaron D. Lee
66ed11fb97 Gold styling for Text Message / File toggle buttons 2026-01-07 20:00:16 -05:00
Aaron D. Lee
9cbb4600f8 Remove label animation, keep static gold styling 2026-01-07 19:58:52 -05:00
Aaron D. Lee
c1c850c593 Normal font weight (400) for labels 2026-01-07 19:58:15 -05:00
Aaron D. Lee
e029f00d66 Bold labels again (font-weight: 600) 2026-01-07 19:57:51 -05:00
Aaron D. Lee
34e417fb55 Dark gold outline (0.3px, dark goldenrod) 2026-01-07 19:57:33 -05:00
Aaron D. Lee
e7954c63e4 Lighter font weight for labels (300) 2026-01-07 19:56:46 -05:00
Aaron D. Lee
446789a16f Add thin gold outline to labels (pulses with shimmer) 2026-01-07 19:56:19 -05:00
Aaron D. Lee
2538126573 Darker drop shadow on gold labels 2026-01-07 19:54:39 -05:00
Aaron D. Lee
a91d127ed7 Add subtle pulsing drop shadow to gold labels 2026-01-07 19:53:48 -05:00
Aaron D. Lee
a0781b1cf7 Faster gold shimmer cycle (1.5s) 2026-01-07 19:53:04 -05:00
Aaron D. Lee
5e32ecb35a More subtle gold shimmer 2026-01-07 19:52:33 -05:00
Aaron D. Lee
3e5de98f60 Gold shimmer: just color pulse, no outer glow 2026-01-07 19:52:16 -05:00
Aaron D. Lee
c8956b9e43 More pronounced gold shimmer: color + glow pulse 2026-01-07 19:45:13 -05:00
Aaron D. Lee
a8f15f87c6 Use file-earmark-image icon for Carrier (matches Stego) 2026-01-07 19:43:49 -05:00
Aaron D. Lee
8a64db9fcc Fix icons inside form labels (reset background-clip) 2026-01-07 19:43:20 -05:00
Aaron D. Lee
ab450955fe Gold shimmer: static gradient with brightness pulse on first half 2026-01-07 19:43:04 -05:00
Aaron D. Lee
afd502dbf3 Simpler shimmering gold labels - gradient text, no pseudo-elements 2026-01-07 19:41:23 -05:00
Aaron D. Lee
3f02e55ffd Fix squished icons in form labels (use inline-flex) 2026-01-07 19:40:03 -05:00
Aaron D. Lee
2ee824b02b Label shine: use mix-blend-mode overlay to mask to text 2026-01-07 19:39:24 -05:00
Aaron D. Lee
189620e4fb Label shine: narrower, subtler, shorter path with pauses 2026-01-07 19:38:05 -05:00
Aaron D. Lee
ecad88e859 Clip label shine to text bounds (no more eyebrows) 2026-01-07 19:36:50 -05:00
Aaron D. Lee
62bd31d0aa Form labels: shine sweeps across top edge only 2026-01-07 19:36:08 -05:00
Aaron D. Lee
241cdadd25 Form labels: gold with moving shine sweep effect 2026-01-07 19:34:48 -05:00
Aaron D. Lee
85309a2044 Form labels: pale gold with subtle shimmer animation 2026-01-07 19:33:48 -05:00
Aaron D. Lee
a81a20f8ee Style form labels with Stegasoo gold color 2026-01-07 19:31:58 -05:00
Aaron D. Lee
9c88f53cd0 Reset DCT options to Color+JPEG when switching from LSB
Some checks failed
Release / test (push) Failing after 34s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
2026-01-07 19:29:29 -05:00
Aaron D. Lee
3f8c2a6957 Compact mode UI with smart output options
Encode page:
- Inline mode buttons: DCT | LSB | Color | Gray | JPEG | PNG
- LSB mode auto-selects Color+PNG and disables Gray/JPEG
- Dynamic hint text with icons below mode buttons

Decode page:
- Compact inline mode buttons: Auto | LSB | DCT
- Dynamic hints that change per mode selection

CSS:
- Disabled btn-check styling for dimmed unavailable options

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 19:28:38 -05:00
Aaron D. Lee
22cf27d7f6 Security: Password-protect channel key export, add audit plan
Channel Key Protection:
- Hide channel key by default in admin settings
- Require password re-authentication to view/export key
- Add /admin/settings/unlock API endpoint for verification
- Key re-locks on page navigation (per-page-load only)

QR Print Sheet Refinements:
- Key split above/below QR image
- 10pt bold font, 1.6in QR size
- Zero gap between tiles, minimal margins
- No page header/footer for clean printing

Security Audit Plan:
- Comprehensive checklist covering auth, crypto, input validation
- Steganography-specific security considerations
- Air-gap deployment focus with known limitations documented
- Penetration testing checklist and automated tool recommendations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 19:16:24 -05:00
Aaron D. Lee
4d8575ce33 Web UI v4.1.6: Admin settings, nav icons, air-gap ready
Admin System Settings page:
- New /admin/settings route with channel key config
- QR code export with tiled print sheet (4x5 on US Letter)
- Server config display (HTTPS, port, auth, DCT/QR status)
- Environment info (version, Python, platform, KDF)

Navigation improvements:
- Icon-only nav with floating labels on hover
- Gold labels slide down below icons
- Gradient pill background on hover

Air-gap ready:
- All vendor libs now local (Bootstrap CSS/JS, Icons, html5-qrcode)
- QRious library for QR generation
- No external CDN dependencies

Other changes:
- Moved About link from nav to footer
- Channel QR export moved from about.html to admin/settings.html
- Print sheet button for QR codes (tiled US Letter output)
- Dev runner script (dev_run.sh) with r/q hotkeys
- Fixed navbar dropdown z-index

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 18:36:33 -05:00
Aaron D. Lee
28b539bcd9 Remove instance/ from tracking, fix ruff lint errors
Some checks failed
Release / test (push) Failing after 30s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
Security:
- Remove instance/.secret_key and instance/stegasoo.db from git
- Add instance/ to .gitignore (was only ignoring frontends/web/instance/)

Lint fixes:
- Remove unused imports in temp_storage.py (os, shutil)
- Sort imports and fix f-string placeholders in cli.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 14:28:07 -05:00
Aaron D. Lee
6b82069dc8 Rename Pi tarball to stegasoo-rpi-runtime-env-arm64.tar.zst
More descriptive name for the pre-built pyenv + venv bundle.
Updated all scripts and docs to use new filename.
Also bumped PREBUILT_URL to v4.1.5.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 01:21:43 -05:00
Aaron D. Lee
52e1a3dfbf Release v4.1.5: Clean up docs, update release notes
- Remove old PLAN-4.1.x.md and RELEASE-4.1.1.md files
- Update RELEASE_NOTES.md for v4.1.5
- Highlights: dev docs, pull-image.sh auto-resize, 16GB images

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 01:17:25 -05:00
Aaron D. Lee
4a27d0c182 Add host-requirements.txt for Pi scripts
Lists all host machine dependencies needed to run:
- pull-image.sh (parted, e2fsprogs, zstd, zip, bc, pv)
- flash-image.sh (unzip, zstd, pv, jq)
- kickoff-pi-test.sh (sshpass, avahi-utils)

Includes quick install command for Debian/Ubuntu.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 01:08:54 -05:00
Aaron D. Lee
36931518ce Docs: Update Pi image workflow, 16GB+ requirement
- rpi/README.md: 16GB+ SD card requirement, use pull-image.sh
- rpi/BUILD_IMAGE.md: Simplified steps using pull-image.sh
- pull-image.sh: Optional .zst.zip wrapper for GitHub releases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 01:01:24 -05:00
Aaron D. Lee
f79c63428b pull-image: Auto-resize rootfs to 16GB before pull
- Unmounts and resizes partition to exactly 16GB
- Handles both shrinking (large cards) and expanding (small cards)
- Disables Pi OS auto-expand service
- Consistent image size regardless of source SD card
- 16GB = minimum disk requirement = image size

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 00:58:55 -05:00
Aaron D. Lee
cc29de4200 Add update instructions to Pi README
Documents easy 3-command update process for existing installations.
Most updates just need git pull + systemctl restart since we use
editable pip installs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 00:16:30 -05:00
Aaron D. Lee
c14f3f75cb Bump version to 4.1.5
Developer documentation release:
- Educational comments throughout core modules
- Pi test automation script
- MOTD improvements with dynamic emojis
- v4.2 wishlist

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 00:08:59 -05:00
Aaron D. Lee
aa99a258f4 Document CLI and Web UI architecture for future devs
CLI module now explains:
- Click command group hierarchy (tree diagram)
- JSON output pattern for scriptability
- Secure input handling (hide_input, confirmation_prompt)
- Dry-run mode pattern
- Batch processing with variadic args and progress callbacks

Web UI now explains:
- Flask architecture overview with ASCII diagram
- Subprocess isolation pattern (why we run stegasoo in subprocesses)
- Async job management with polling flow diagram
- Context processors for template globals
- Secret key persistence for session survival
- Environment-based configuration (12-factor style)

If you're reading this code trying to learn Flask/Click patterns,
these comments should actually teach you something useful.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 23:58:59 -05:00
Aaron D. Lee
93420704e8 Add personality to the codebase (comments that don't suck)
The code now explains itself like a friend teaching you crypto:
- DCT module: Why mid-frequency? What's QIM? Why is scipy being weird?
- Steganography: How LSB actually works with visual examples
- Crypto: The multi-factor security model with ASCII art diagrams

Also adds kickoff-pi-test.sh - one command to flash, wait, setup, test.
No more manual steps between flashing and seeing if it works.

Comments should teach, not just describe. If you're reading the code
trying to understand how DCT steganography works, these comments
should actually help. Novel concept, I know.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 23:53:26 -05:00
Aaron D. Lee
6e4eb5464e Fix MOTD: Remove escaped vars, shorten Debian boilerplate
- Fix TEMP_NUM/TEMP_EMOJI variables (no escaping in quoted heredoc)
- Shorten /etc/motd to one-liner with license path reference

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 23:14:19 -05:00
Aaron D. Lee
d04670e352 MOTD: Use globe emoji for URL
🚀 Stegasoo running     🌐 https://...

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:55:47 -05:00
Aaron D. Lee
fda1cdad51 MOTD: Dynamic temp emoji based on temperature
- 🧊 ice cube: < 50°C (cool)
- 😎 cool face: 50-70°C (warm)
- 🔥 fire: > 70°C (hot)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:54:52 -05:00
Aaron D. Lee
b48ccc5d16 MOTD: Adjust thermometer spacing
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:51:27 -05:00
Aaron D. Lee
15ed63cafa MOTD: Use link emoji for URL
🚀 Stegasoo running    🔗 https://...

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:48:17 -05:00
Aaron D. Lee
869d7ee8e3 MOTD: Replace bullet with rocket emoji
🚀 Stegasoo running    🚀 https://...

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:47:55 -05:00
Aaron D. Lee
3ee8c1d22a Fix MOTD temperature line alignment
Adjust spacing between MHz and thermometer emoji for proper
column alignment in terminal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:46:22 -05:00
Aaron D. Lee
b96564358a Add v4.2 wishlist with GPU decode idea
Blue sky document for capturing future feature ideas.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:33:41 -05:00
Aaron D. Lee
01afb3da66 Refactor: Extract banner template to shared banner.sh
- Create rpi/banner.sh with print_banner, print_gradient_line,
  print_logo, print_starfield, print_complete_banner functions
- Update setup.sh to source banner.sh (with inline fallback for curl)
- Update first-boot-wizard.sh to use banner functions
- Update sanitize-for-image.sh to use banner functions
- Fix MOTD thermometer spacing alignment

Single source of truth for ASCII banner styling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:33:12 -05:00
Aaron D. Lee
a98df5f9a0 SSL cert: Use actual hostname instead of 'localhost' default
When STEGASOO_HOSTNAME env var is not set, use socket.gethostname()
to get the actual machine hostname for certificate generation.

This ensures the cert includes proper hostname.local SAN.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:16:23 -05:00
Aaron D. Lee
70da348bce SSL certs: Include .local hostname and local IPs in SANs
The auto-generated SSL certificate now includes:
- hostname.local for mDNS browser access
- All detected local network IPs

This fixes browser access via stegasoo.local when HTTPS is enabled.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 22:15:24 -05:00
Aaron D. Lee
90ba8543a7 Remove trailing period from wizard intro 2026-01-06 21:56:37 -05:00
Aaron D. Lee
da3aea992c Polish first-boot-wizard intro text formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 21:55:17 -05:00
Aaron D. Lee
ae47ff4932 Show mDNS hostname alongside IP in RPi scripts
- flash-stock-img.sh: Show stegasoo.local URL and SSH command
- setup.sh: Display both .local and IP URLs
- first-boot-wizard.sh: Prioritize .local URL, IP as fallback
- Clean up service file path display

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 21:52:51 -05:00
Aaron D. Lee
eb16eb1db2 v4.1.5: Accordion UI, webcam QR scanning, Pi image fix
Encode/Decode UI:
- New accordion layout with 3 steps (encode) / 2 steps (decode)
- Gold step numbers with checkmarks on completion
- Dynamic right-aligned summaries as fields are filled
- Subtle gradient highlight on active accordion step

Webcam QR Scanning:
- Camera button for RSA key QR codes on encode/decode pages
- Camera button for channel key scanning
- 3-2-1 countdown capture for dense QR codes
- Proper scanner stop/restart on retry
- Backend decompression for STEGASOO-Z: compressed keys

RSA Key Print:
- Removed identifying text from QR print output
- Now prints plain QR code for discretion

Pi Image Script:
- Fixed 16GB resize to detect expand vs shrink
- Fresh images now properly EXPAND to 16GB
- Already-expanded images properly SHRINK to 16GB

UI Polish:
- Removed PIN helper text for compactness
- Fixed QR drop zone centering
- Fixed decode page element IDs for JS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 21:31:11 -05:00
Aaron D. Lee
c65d9e6682 Finalize 4.1.4 release prep
Some checks failed
Release / test (push) Failing after 34s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
- BUILD_IMAGE.md: Clarify docs are for devs, not end users
- Add 4.1.5 plan with decode progress bar feature
- Update .gitignore for release assets (.zip, .img)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 17:51:00 -05:00
Aaron D. Lee
eeb44eae94 Update BUILD_IMAGE.md with SCP tarball step
- Split clone and setup into separate steps
- Add Step 5: Copy pre-built tarball from host
- Renumber remaining steps (7-11)
- Update quick command summary with full workflow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 17:12:01 -05:00
Aaron D. Lee
26d4b82c91 MOTD: Show configured overclock freq, not idle freq
Read arm_freq from config.txt instead of vcgencmd live reading.
Previously showed 600 MHz at idle, now shows actual configured max.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 16:58:36 -05:00
Aaron D. Lee
7efeaf02e8 Bundle pyenv Python with pre-built tarball for zero-compile installs
- Combined tarball includes pyenv Python 3.12 + venv with all deps
- Downloads from GitHub releases by default (~50MB)
- Reduces install time from 20+ min to ~2 min
- Add --no-prebuilt / --from-source flags to force compile
- Update BUILD_IMAGE.md with tarball creation instructions
- Rename tarball: stegasoo-pi-arm64.tar.zst (was venv-only)

Fresh Pi installs no longer need to compile Python or jpegio.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 15:37:44 -05:00
Aaron D. Lee
925fb05cbd Default to pre-built venv for Pi setup
- USE_PREBUILT=true by default, downloads from GitHub releases
- Add --no-prebuilt / --from-source flags for manual builds
- Update estimated time: ~2 min vs 15-20 min from source
- Update help text with new options

Fresh Pi installs now download pre-built venv automatically,
cutting install time from 20+ minutes to ~2 minutes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 15:29:52 -05:00
Aaron D. Lee
29a02265a1 Restructure setup.sh for optimized tarball detection
- Move repo clone to step 4 (before pyenv) to check for tarball early
- Set USE_PREBUILT flag immediately after cloning
- Python compile only happens if not already installed (fallback)
- Better progress messages during install

This ensures pre-built venv detection happens before expensive
Python compile step, and skips compile entirely on repeat installs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 15:24:54 -05:00
Aaron D. Lee
d58f3c6fb6 4.1.4: QR sharing, venv tarball, flash script improvements
QR Channel Key Sharing:
- Admin-only QR generator in about.html (was visible to all)
- QR button for saved keys on account page
- Fixed about() route missing channel status vars (bug)

Pi Build Optimization:
- Pre-built venv tarball support (39MB zstd, skips 20+ min compile)
- setup.sh auto-detects and extracts tarball if present
- Strip __pycache__/tests before tarball (295MB → 208MB)

Flash Script Improvements:
- flash-image.sh now uses config.json for headless WiFi setup
- Consistent wipe prompt on both flash scripts
- pull-image.sh re-enables auto-expand before shrinking

Build Docs:
- Added zstd and jq to pre-setup apt-get
- Documented fast build option with pre-built venv

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 15:03:46 -05:00
Aaron D. Lee
cc46993d80 Add stegasoo info command and update docs for v4.1
- Enhanced `stegasoo info` with fastfetch-style output
  - Service status, URL, channel key, DCT support
  - System stats with --full (CPU, temp, uptime, disk)
- Updated UNDER_THE_HOOD.md for v4.1
  - Added v4.1 changes table (channel keys, docker, Pi wizard)
  - Updated architecture diagram
  - Added channel module to responsibilities

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 13:05:17 -05:00
Aaron D. Lee
893a044eaa Build tooling improvements for 4.1.4
- Rename flash-pi.sh → flash-stock-img.sh for clarity
- Add 16GB partition sizing option (faster imaging)
- Disable Pi OS auto-expand to preserve partition size
- Add pip-audit security check to release validation
- Add config.json.example, gitignore actual config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 12:59:59 -05:00
Aaron D. Lee
9f03b69408 Add 4.1.4 planning doc and release notes 2026-01-06 12:45:48 -05:00
adlee-was-taken
cce2007c6e Merge pull request #11 from adlee-was-taken/main
Simple doc fixes.
2026-01-06 12:19:33 -05:00
adlee-was-taken
52f43d3a86 Fix formatting in UNDER_THE_HOOD.md 2026-01-06 12:16:15 -05:00
adlee-was-taken
85a7092d55 Fix formatting inconsistencies in UNDER_THE_HOOD.md 2026-01-06 12:14:24 -05:00
adlee-was-taken
4b37a81087 Fix formatting in STEGASOO ARCHITECTURE diagram 2026-01-06 12:12:47 -05:00
adlee-was-taken
31941dc3f5 Update email for reporting security vulnerabilities
Updated the email address for reporting vulnerabilities.
2026-01-06 11:58:26 -05:00
adlee-was-taken
9a7e4ddce7 Change security vulnerability reporting email
Updated contact email for reporting vulnerabilities.
2026-01-06 11:58:16 -05:00
adlee-was-taken
0424dd34d5 Update security policy for version support 2026-01-06 11:57:38 -05:00
adlee-was-taken
2127b916f3 Revise notes for supported versions
Updated notes for supported versions in SECURITY.md.
2026-01-06 11:56:26 -05:00
adlee-was-taken
f8e65890e5 Update supported versions in SECURITY.md
Removed the note about EOL for version 4.1.x.
2026-01-06 11:55:42 -05:00
adlee-was-taken
5861ab0e1e Update supported versions and EOL notes in SECURITY.md 2026-01-06 11:55:28 -05:00
adlee-was-taken
5309a08aaf Update Python version requirement to 3.10 - 3.12 2026-01-06 11:46:40 -05:00
Aaron D. Lee
d8fb95b68e Add optional partition wipe to flash-pi.sh
Some checks failed
Release / test (push) Failing after 29s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
Prompts user to wipe partition table before flashing,
helpful when SD card has corrupted partitions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 00:59:38 -05:00
Aaron D. Lee
c0b6865790 Add headless Pi flash script with NetworkManager WiFi
- Reads config from rpi/config.json
- Flashes image with dd (supports .xz and .zst)
- Configures SSH, user/password, hostname on boot partition
- Creates NetworkManager connection file on rootfs for WiFi
- Works with Trixie/Bookworm (no more wpa_supplicant)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 23:48:59 -05:00
Aaron D. Lee
6e7ae0d6f9 Docker improvements and decode loading state
- Fix .dockerignore to exclude *.img.xz files (was 2.3GB context)
- Remove deprecated 'version' attribute from docker-compose.yml
- Increase container memory limits to 2GB/1GB (prevent OOM on DCT)
- Add loading spinner to decode button during form submission

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:53:39 -05:00
Aaron D. Lee
6a5b12f98e Fix multi-worker temp file issue with file-based storage
- New temp_storage.py module stores files on disk instead of in-memory
- Multiple Gunicorn workers can now share temp files
- Startup cleanup removes leftover files from previous runs
- Dockerfile creates temp_files directory
- Added temp_files/ to .gitignore

Previously encode preview worked but download failed with "File expired"
because each worker had its own in-memory TEMP_FILES dict.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:40:42 -05:00
Aaron D. Lee
d8eb7b0160 Bump version to 4.1.3
- Version bump from 4.1.2 to 4.1.3
- Updated CHANGELOG with SSL cert fix as highlight
- Added *.img.zst.zip to .gitignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:20:26 -05:00
Aaron D. Lee
962c04084b Fix SSL certificate generation for HTTPS mode
- wizard/setup now generate certs when HTTPS enabled
- app.py has proper error handling for cert failures
- Add custom SSL certificate documentation to INSTALL.md
- Include SANs for hostname, localhost, and local IP

Previously HTTPS could be enabled but certs weren't generated,
causing SSL_ERROR_RX_RECORD_TOO_LONG browser errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:16:12 -05:00
Aaron D. Lee
597a9c6411 Prepare 4.1.2 release documentation
Some checks failed
Release / test (push) Failing after 38s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
- Add 4.1.2 changelog: Docker, Pi wizard, unit tests, validation script
- Add Raspberry Pi section to README with first-boot wizard info
- Document new features: TUI setup, overclock presets, sanitize scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:10:02 -05:00
Aaron D. Lee
67b25a43a6 Update RPi banner styling: purple→blue gradient + gold logo
- Horizontal borders: deep purple (93) → light blue (117) gradient
- STEGASOO ASCII logo: gold (220) to match web UI
- Applied to all RPi scripts: first-boot-wizard, setup, sanitize

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:06:35 -05:00
Aaron D. Lee
65a663fe3b Add Docker deployment documentation
- New DOCKER.md with comprehensive Docker setup guide
- Added Docker quick start section to README.md
- Documents environment variables, volumes, build process
- Includes production deployment and troubleshooting tips

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:00:01 -05:00
Aaron D. Lee
fc6e4eb805 Add comprehensive pytest unit tests for stegasoo library
Tests cover:
- Version info
- Credential generation (passphrase, PIN, channel key)
- Validation functions (passphrase, PIN, message, image)
- LSB encode/decode roundtrip and failure cases
- DCT encode/decode roundtrip and JPEG output
- Channel key encode/decode and wrong key rejection
- Compression of long messages
- Edge cases: Unicode, special chars, minimum passphrase

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:53:42 -05:00
Aaron D. Lee
50f07a0ce9 Adjust banner alignment - logo +1, tagline +5 spaces
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:32:41 -05:00
Aaron D. Lee
7accd26821 Standardized the ASCII banners in pi scripts
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:30:35 -05:00
Aaron D. Lee
075e10792c Simplify wizard banners - no side borders, pale pink lines
- Removed side borders from logo/sparkle sections
- Use horizontal lines only (no corner chars)
- Changed to pale pink (256-color 218) for softer look
- Centered "First Boot Wizard" and "Setup Complete!" text
- Both banners now identical except bottom text

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:27:21 -05:00
Aaron D. Lee
9a790de5c3 Fix wizard banner alignment - indent sparkles and logo
- Added 4 extra spaces to sparkle lines
- Added 2 extra spaces to logo lines
- Both banners now properly aligned within the border

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:23:28 -05:00
Aaron D. Lee
3c91c92a4d Fix wizard banner alignment and use light pink border
- Fixed logo alignment to be consistent across all lines
- Changed border from 0;35 (magenta) to 1;35 (light pink)
- Updated sparkle pattern for better visual consistency
- Both welcome and Setup Complete banners now match

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:21:31 -05:00
Aaron D. Lee
9d1bc7f829 Tighten wizard banners - remove extra blank line
Both welcome and Setup Complete banners now have consistent
design with sparkles directly above the bottom text line.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:19:46 -05:00
Aaron D. Lee
d8118d688b Fix wizard welcome banner to match Setup Complete style
Same cyan (0;36), pink border, and indentation as the completion banner

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:17:02 -05:00
Aaron D. Lee
b6acee1acb Add bright cyan STEGASOO logo to wizard welcome banner
Pink border, bright cyan logo, gray sparkles, white 'First Boot Wizard'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:15:22 -05:00
Aaron D. Lee
b9baf35dfa Show CPU speed and temp in MOTD when overclocked
Displays MHz and temperature when arm_freq or over_voltage is set in config.txt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:13:16 -05:00
Aaron D. Lee
561f03ffde Full bordered Setup Complete banner with colored text
Pink border, cyan logo, gray sparkles, green 'Setup Complete!'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:12:12 -05:00
Aaron D. Lee
038347a505 Add pink border lines to Setup Complete banner
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:11:37 -05:00
Aaron D. Lee
e026d1a4db Update about.html version history, fix API exports
About page:
- Version history now shows v4.1.2 prominently with accordion for older versions
- Shortened 'Error Correction Reed-Solomon' to 'DCT ECC / RS Code'
- Removed v4.1 badges from established features

API fixes:
- Export MAX_FILE_PAYLOAD_SIZE from constants
- Export calculate_capacity_by_mode from steganography

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:09:28 -05:00
Aaron D. Lee
3f93e7a752 Add sparkly banner to first-boot wizard completion
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:07:56 -05:00
Aaron D. Lee
cdc7ffd3bf Fix gum --inline flag not supported in first-boot wizard
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 21:02:12 -05:00
Aaron D. Lee
6c3bc995f1 Mobile polish, release validation script, bump to v4.1.2
Mobile-responsive CSS improvements:
- Larger touch targets for drop zones and buttons (56px min)
- Touch feedback with active states for touch devices
- Camera hint text on mobile ("Tap to take photo or choose file")
- Mode buttons stack vertically on small screens
- Full-width download buttons on mobile
- Navbar doesn't stick on mobile to save screen space

Release validation script (scripts/validate-release.sh):
- Automated pre-release checks: ruff, imports, encode/decode sanity
- Optional Docker build/test (--docker flag)
- Optional Pi smoke test via SSH (--pi flag)
- Pass/fail summary with exit codes

Other:
- Version bump to 4.1.2 (pyproject.toml, constants.py, __init__.py)
- Fixed ruff import sorting in cli.py
- Updated PLAN-4.1.2.md (all 9 features complete)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 20:34:23 -05:00
Aaron D. Lee
2d3ed8a79a Add progress bars, fix DCT decode, sparkly MOTD
Progress bar support (v4.1.2):
- Web frontend: Real-time progress during encode with phase display
- CLI: --progress flag with rich library for encode command
- Backend: progress_file parameter for async progress reporting

DCT decode bug fix:
- Fixed InvalidMagicBytesError not being caught in early-exit check
- RS-protected format (v4.1.0+) has length prefix first, not magic bytes
- Exception handler now catches both ValueError and InvalidMagicBytesError

MOTD update:
- Added sparkly header to setup.sh MOTD (matches other rpi scripts)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 20:25:33 -05:00
Aaron D. Lee
040c44fec6 Remove duplicate MOTD, source bashrc after install
- System MOTD already shows banner, bashrc one was redundant
- Source bashrc immediately after copying for instant effect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 19:32:20 -05:00
Aaron D. Lee
832d8be025 Fix jpegio ARM64 patch for CRLF line endings
- Convert CRLF to LF before patching (jpegio uses Windows line endings)
- Update patch context to match current jpegio setup.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 19:30:32 -05:00
Aaron D. Lee
7088623d2c adlee themed cli becuase I can.
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 19:25:23 -05:00
Aaron D. Lee
44a3ca8a0f Compact first-boot-wizard output for smaller terminals
- Remove sparkle decoration lines from banner
- Reduce padding and margins on boxes
- Condense first steps to single line
- Condense commands to single line
- Simplify restart notice (no bordered box)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 19:20:49 -05:00
Aaron D. Lee
7a35ac3df7 Update plan: mark #6 Smoke Test Benchmarking as done
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 19:15:05 -05:00
Aaron D. Lee
f69475b406 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>
2026-01-05 18:34:05 -05:00
Aaron D. Lee
559dcd3dcf Implement forced first-login setup and dropzone UX fixes
#4 Forced First-Login Setup:
- Add before_request hook to redirect to /setup if no users exist
- Skip redirect for static files and setup routes

#5 Dropzone UX Fixes:
- Make preview images clickable to replace file
- Make entire drop zone clickable
- QR zone resets after 2s on error, allowing retry
- Clear file input on error so same file can be re-selected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 18:28:28 -05:00
Aaron D. Lee
b1ddfaa75b flash-image.sh: prefer rpi-imager, fallback to dd
- Try rpi-imager first (native .zst support, faster)
- Fall back to dd if rpi-imager unavailable or fails
- pv now optional (uses dd status=progress without it)
- Handles .zst.zip GitHub wrapper automatically

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 18:14:01 -05:00
Aaron D. Lee
4843ec8c22 Add rpi-imager CLI option to flash docs
- rpi-imager --cli supports .zst.zip directly
- Also document flash-image.sh option
- Keep manual dd as fallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 18:11:13 -05:00
Aaron D. Lee
ac08011236 Clean up repo structure
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 18:05:06 -05:00
Aaron D. Lee
12c4b091fb Move smoke-test.sh to tests/, make it local-only
- Move from rpi/ to tests/ directory
- Add to .gitignore (local tool, not part of distribution)
- Pytest unit tests remain tracked

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 18:03:28 -05:00
Aaron D. Lee
c2c2c924e1 Add Docker support to smoke test, add inject-wifi.sh
Smoke test improvements:
- Add --docker flag for testing Docker containers
- Skip SSH/systemd checks in Docker mode
- Docker health check verifies HTTP response
- Show "Docker Smoke Test" header in Docker mode

inject-wifi.sh:
- Add to repo (was gitignored)
- Add cleanup trap for robustness
- Supports NetworkManager (Bookworm) and wpa_supplicant (legacy)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 18:01:36 -05:00
Aaron D. Lee
df7ad06a08 Update flash-image.sh: add .zst.zip support
- Add support for .zst.zip wrapper (GitHub releases workaround)
- Update examples to use .zst format (current default)
- Update usage to show all supported formats

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 17:50:52 -05:00
Aaron D. Lee
166b936ee5 Fix smoke test NEEDS_SETUP detection and login checks
- Check /login redirect to /setup instead of homepage redirect
- Use logout link presence to verify login success (encode/decode are public)
- Add -c flag to save cookies during homepage check

The smoke test was passing login even when not logged in because
encode/decode links are visible to everyone.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 16:43:09 -05:00
Aaron D. Lee
7138455f8d Update docs: cd /opt before git clone 2026-01-05 16:08:16 -05:00
Aaron D. Lee
9ab3260298 Update 4.1.2 plan: Docker cleanup done, add smoke test Docker support 2026-01-05 16:00:07 -05:00
Aaron D. Lee
763f7bf603 Fix Docker build: add .dockerignore, fix permissions
- Add .dockerignore to exclude instance/, test_data/, rpi/, etc.
- Create instance/certs dirs in Dockerfile for volume mounts
- Ensures stego user can write to mounted volumes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 15:59:32 -05:00
Aaron D. Lee
1059e17f4e Add release validation script to 4.1.2 plan 2026-01-05 15:12:18 -05:00
Aaron D. Lee
7cb42e189a Add release checklist for Pi and Docker validation 2026-01-05 15:11:56 -05:00
Aaron D. Lee
8c283bc4e5 Add 4.1.1 release notes 2026-01-05 14:50:37 -05:00
Aaron D. Lee
664362bea5 Add Pi 4 performance note to README
Some checks failed
Release / test (push) Failing after 30s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
2026-01-05 14:48:58 -05:00
Aaron D. Lee
4733e3b4dd Add dropzone UX fixes to 4.1.2 plan 2026-01-05 14:44:06 -05:00
Aaron D. Lee
24aec00613 Add smoke test benchmarking to 4.1.2 plan 2026-01-05 14:37:58 -05:00
Aaron D. Lee
0e0aa996bc Reset port 443 redirect during sanitize
Clear iptables-restore service and rules so wizard can reconfigure
fresh. Fixes MOTD showing wrong port after re-running wizard.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 14:01:31 -05:00
Aaron D. Lee
255ae4f30d Fix MOTD port 443 detection (check service not iptables)
iptables requires root to read NAT rules. Instead check if the
iptables-restore service is enabled, which indicates 443 redirect
was configured.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 13:56:02 -05:00
Aaron D. Lee
7647ca11d1 Add login banner showing Stegasoo URL
On SSH login, shows ASCII logo and active URL if service is running,
or instructions to start if not.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 13:44:09 -05:00
Aaron D. Lee
01e9e5af0a Rename carrier2.JPG to carrier2.jpg 2026-01-05 13:39:30 -05:00
Aaron D. Lee
39e5daa022 Show /setup URL in setup scripts
Direct users to the admin account creation page instead of root URL.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 13:38:36 -05:00
Aaron D. Lee
54e097c050 Fix pyenv Python path resolution in setup.sh
Use `pyenv which python` instead of hardcoded path to handle
version mapping (3.12 -> 3.12.12).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 13:30:30 -05:00
Aaron D. Lee
a3ff8dace1 Fix Pi setup: use /opt/stegasoo, flexible Python version
- Change .python-version from 3.12.0 to 3.12 (matches any 3.12.x)
- Update docs to use /opt/stegasoo instead of ~/stegasoo
- Add pre-setup steps: chown /opt, install git
- Renumber BUILD_IMAGE.md steps (now 9 steps)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 13:16:31 -05:00
Aaron D. Lee
e4cf96bb7c Add 4.1.3 plan stub (heavier features)
Reserved for:
1. DCT performance optimizations
2. User management UI (admin CRUD)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 00:29:08 -05:00
Aaron D. Lee
597c95070c Add 4.1.2 release plan
Three focused features:
1. Real progress bar for encode/decode (polling + progress file)
2. Granular decode error messages (custom exceptions, specific UI feedback)
3. Mobile-responsive polish (touch targets, stacked layouts, camera hints)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 00:28:47 -05:00
Aaron D. Lee
dba5a08476 Add --443 flag to smoke test for HTTPS on port 443
- Better argument parsing for IP, --https, --443, --port=N
- Port 443 omits port from URL for cleaner output
- Ignore unknown flags instead of treating as IP

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 00:15:17 -05:00
Aaron D. Lee
6ceda6c287 Add overclock removal to sanitize script
- New step 10/11 removes overclock settings from /boot/firmware/config.txt
- Removes over_voltage, arm_freq, gpu_freq lines
- Skipped in soft reset mode (preserves for testing)
- Distributable image should let users configure via wizard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 00:01:24 -05:00
Aaron D. Lee
c2575f973b Redesign Limits & Specs section with key stats and accordion
- Show 6 key specs prominently as cards (Payload, Carrier, DCT/LSB capacity, Encryption, Error Correction)
- Add Reed-Solomon error correction info with v4.1 badge
- Move secondary specs to collapsible accordion
- Add reedsolo to "Built with" list

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 23:52:46 -05:00
Aaron D. Lee
8208ec2955 Add btop, overclock wizard option, click-to-copy decode UI
- setup.sh: Add btop to apt install for temp monitoring
- first-boot-wizard: Add Step 4 for overclock configuration
  - Detects Pi 4/5 model
  - Asks about active cooling
  - Offers appropriate overclock settings (2.0GHz Pi4, 2.8GHz Pi5)
  - Prompts for restart if enabled
- decode.html: Make message box click-to-copy, remove separate button
  - Shows "(click to copy)" hint
  - Visual feedback on hover and copy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 23:48:59 -05:00
Aaron D. Lee
909dc14a92 Fix setup.sh jpegio build order and add python3-dev
- Clone stegasoo BEFORE building jpegio (need patch script)
- Create venv with explicit pyenv Python path
- Build jpegio INTO venv (not globally)
- Add python3-dev to apt dependencies
- Update step count from 9 to 11

This fixes the issue where jpegio was built globally, then pip
tried to reinstall unpatched jpegio from PyPI into the venv.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 23:43:10 -05:00
Aaron D. Lee
bb91e41d3d Update default branch from main to 4.1 in Pi docs
Branch 4.1 includes Reed-Solomon error correction for DCT
steganography which is required for reliable operation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 22:59:32 -05:00
Aaron D. Lee
c54a96894c Bump version to 4.1.1
- Reed-Solomon error correction for DCT mode
- Elapsed time counter on encode/decode buttons
- Increased timeout to 300s for slow devices

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 22:45:05 -05:00
Aaron D. Lee
da044017d7 Add elapsed time counter to encode/decode buttons
Shows running timer (e.g., "Encoding... 1:23") so users know
the operation is still working and not frozen.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 22:44:05 -05:00
Aaron D. Lee
d0ec99d5b5 Add Reed-Solomon error correction to DCT steganography
- Add reedsolo library for RS error correction (32 symbols = 16 byte correction per 223-byte chunk)
- Protect entire payload (header + data) with RS encoding
- Store 3 copies of length header with majority voting for robustness
- Handle RS chunking overhead (varies based on data size)
- Update capacity calculation to account for RS overhead (24 bytes prefix + variable RS overhead)
- Add RS to dct, web, and api optional dependencies
- Update about.html with v4.1.0 Reed-Solomon feature
- Update module docstring

This fixes DCT decode failures with certain carrier images that have
uniform areas causing unstable DCT coefficients.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 22:28:58 -05:00
Aaron D. Lee
aac8037c04 Fix DCT steganography for non-8-aligned images and set color mode default
- Fix block calculation mismatch in DCT extract (use original dimensions)
- Change default dct_color_mode from "grayscale" to "color"
- Update DCT test to use noise image instead of solid color
- Remove debug logging from encode/decode paths

The block calculation fix ensures extract uses the same block positions
as embed for images whose dimensions aren't divisible by 8. This was
causing decode failures on the Pi web UI with 1195x671 images.

Color mode is now the default since it preserves the original image
colors. The test fixture now uses a random noise image because solid
color images cause coefficient drift during YCbCr/RGB conversion that
can corrupt embedded data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 21:36:59 -05:00
Aaron D. Lee
7a5092b945 Add embed_mode to debug logging 2026-01-04 21:12:20 -05:00
Aaron D. Lee
e52a709080 Forward worker stderr to main process for debugging 2026-01-04 21:06:04 -05:00
Aaron D. Lee
70fe8fce62 Add worker debug logging for channel key resolution 2026-01-04 21:00:32 -05:00
Aaron D. Lee
d44575deec Add encode channel_key debug logging 2026-01-04 20:57:08 -05:00
Aaron D. Lee
d0d48236ff Add decode result and channel_key debug logging 2026-01-04 20:52:13 -05:00
Aaron D. Lee
5891285493 Fix missing hashlib import in download route 2026-01-04 20:49:11 -05:00
Aaron D. Lee
5501c7e0ba Add more debug logging for stego file tracking
Log stego file size and hash at:
- Encode result storage
- Download time

This will help identify if files are corrupted during
download/upload cycle.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 20:47:12 -05:00
Aaron D. Lee
038fd6ceac Fix decode debug logging to use stderr for systemd journal
Debug prints need file=sys.stderr to appear in journalctl output.
Encode route was fixed but decode was still using plain print().

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 20:42:24 -05:00
Aaron D. Lee
8622f1a850 Add debug logging for encode/decode file hashes
Temporary debug output to help trace reference photo byte mismatches.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 20:38:13 -05:00
Aaron D. Lee
710b3a6a98 Implement CLI encode/decode with reference photo support
- Add required -r/--reference option to encode command
- Add required -r/--reference option to decode command
- Replace stub implementations with actual library calls
- CLI now properly encodes and decodes messages/files
- Fix smoke test form field names and add proper redirect handling

The CLI encode/decode were stubs that didn't actually work.
Now they properly use the stegasoo library functions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 20:25:59 -05:00
Aaron D. Lee
c965a5f8da Fix channel_fingerprint None check in templates
Handle case where channel_fingerprint is None when no channel
key is configured, preventing TypeError on encode/decode pages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 20:01:30 -05:00
Aaron D. Lee
00cda4d929 Enhance smoke test and fix banner alignment
Smoke test now includes:
- Admin user creation and login
- Regular user creation and workflow
- Encode/decode tests for both user types
- Password recovery QR test
- System health checks

Also fixes Setup Complete banner alignment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 19:53:59 -05:00
Aaron D. Lee
05e2286d02 Add smoke test script for Pi image validation
Automated tests for fresh Pi images:
- Web UI accessibility
- Admin user creation
- Login authentication
- Encode/decode functionality
- Service health via SSH

Usage: ./smoke-test.sh [ip] [--https]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 19:48:49 -05:00
Aaron D. Lee
46cbf98a23 Small fix 2026-01-04 19:36:06 -05:00
Aaron D. Lee
58673c04fe Add gum TUI toolkit installation to setup.sh
Required for first-boot wizard TUI. Adds Charm repo and installs gum.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 19:30:11 -05:00
Aaron D. Lee
dd07972014 Align sparkle banners consistently across all RPI scripts
- 4 spaces before sparkle lines
- 3 spaces before logo top line
- 2 spaces before logo body

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 19:28:03 -05:00
Aaron D. Lee
1f40eeff9e Add option to skip compression in pull-image.sh
Use .img extension to skip zstd compression:
  ./pull-image.sh stegasoo.img

Use .img.zst to compress (default behavior):
  ./pull-image.sh stegasoo.img.zst

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 17:27:26 -05:00
Aaron D. Lee
dc09bac489 Fix logo top line alignment (3 spaces for ASCII art)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 17:22:17 -05:00
Aaron D. Lee
46489dd276 Fix bottom sparkle line alignment
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 17:21:40 -05:00
Aaron D. Lee
9088caa23d Fix top sparkle line alignment in setup and sanitize
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 17:19:40 -05:00
Aaron D. Lee
75b6203525 Make PATH hook a check/fix step in sanitize
Now checks if /etc/profile.d/stegasoo-path.sh exists and creates
it if missing, rather than always overwriting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 17:03:13 -05:00
Aaron D. Lee
404d7885f4 Add PATH hook to sanitize script for pre-built images
Ensures stegasoo CLI and rpi scripts are in PATH for images
created with sanitize-for-image.sh.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 17:02:28 -05:00
Aaron D. Lee
a8db991052 Add stegasoo CLI and rpi scripts to PATH
Creates /etc/profile.d/stegasoo-path.sh to add:
- /opt/stegasoo/venv/bin (stegasoo CLI)
- /opt/stegasoo/rpi (setup.sh, sanitize-for-image.sh, etc)

Users can now run 'stegasoo' and the rpi scripts from anywhere.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:57:02 -05:00
Aaron D. Lee
ea2948e5d2 Add spacing between logo and bottom sparkle row
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:54:24 -05:00
Aaron D. Lee
05278ca55f Fix ASCII banner alignment - remove extra leading space
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:53:42 -05:00
Aaron D. Lee
c551078c37 Add sparkle ASCII banners to all RPI scripts
Replace the complex dot pattern with cleaner sparkle-style banners
using . * . patterns above and below the STEGASOO logo. Also fixes
duplicate logo lines that were present in setup.sh and sanitize.sh.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:52:01 -05:00
Aaron D. Lee
b7d86201ca Use bright green (46) for buttons
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:46:07 -05:00
Aaron D. Lee
07b0bc0b75 Use terminal green (34) instead of lime (82)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:45:44 -05:00
Aaron D. Lee
d8b8e4f5c2 Add bold text for selected buttons
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:43:20 -05:00
Aaron D. Lee
143a8bdc65 Fix button text contrast - dark text on lime, white on gray
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:42:39 -05:00
Aaron D. Lee
ac92fa36b5 Style gum confirm buttons with lime green
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:41:39 -05:00
Aaron D. Lee
c82dcf26f2 Fix jpegio build directory handling
- Use fixed path instead of mktemp
- Remove dir before clone to ensure clean state
- chown to user before pip install
- Check clone success before proceeding

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:31:56 -05:00
Aaron D. Lee
65a496a9d4 Add jpegio ARM64 patch to venv rebuild
Clone and patch jpegio to remove -m64 flag on ARM64 before
installing. Also install build deps (cython, numpy) first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:29:03 -05:00
Aaron D. Lee
25a432fcf3 Use Python 3.12 for venv rebuild
Check for pyenv Python 3.12 first, then system python3.12,
then fall back to python3 with warning.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:28:17 -05:00
Aaron D. Lee
a58dd54ba8 Add venv repair step to sanitize script
Check if venv is broken or missing stegasoo module and rebuild
if needed. Venv paths break when directory is moved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:26:48 -05:00
Aaron D. Lee
05c542d808 Use full venv python path for channel key generation
Don't rely on source activate - use direct path to venv python.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:25:55 -05:00
Aaron D. Lee
5e5d6e60de Fix channel key generation and show errors
- Capture stderr separately to show error details
- Validate key format before accepting
- Wait for user confirmation on error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:22:18 -05:00
Aaron D. Lee
d898f6d7b1 Replace emoji with ASCII art for terminal compatibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:21:24 -05:00
Aaron D. Lee
00dd15b8fb Rewrite first-boot wizard using gum TUI
Replace whiptail with gum (Charm.sh) for beautiful, modern TUI.
Features spinners, styled boxes, colored text, and reliable prompts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:16:57 -05:00
Aaron D. Lee
419b491737 Rewrite first-boot wizard using whiptail TUI
Replace manual read prompts with whiptail dialogs for reliable
user input. Whiptail is pre-installed on Raspberry Pi OS.

Features:
- Modal dialogs that properly wait for input
- Progress gauge for key generation and setup
- Cleaner, more professional look
- No more input buffer issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:11:48 -05:00
Aaron D. Lee
b568026253 Add comments explaining input buffer flush
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:06:59 -05:00
Aaron D. Lee
127d3e54a6 Fix input buffer issues in first-boot wizard
Add input buffer flush before all read prompts to prevent
leftover keystrokes from auto-answering wizard questions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 16:06:04 -05:00
Aaron D. Lee
de41c0731e Fix input prompt dropping issue in sanitize script
Add input buffer flush before each read prompt to prevent
leftover keystrokes from auto-answering prompts. Also add
small delay before reboot/shutdown prompts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 15:51:05 -05:00
Aaron D. Lee
f3d5699e15 Add config file support and help to RPi scripts
- Add stegasoo.conf.example with all configurable options
- setup.sh: Add -h/--help, load config from /etc and ~/.config
- setup.sh: Support STEGASOO_BRANCH for non-main branches
- sanitize-for-image.sh: Add -h/--help with usage examples

Config files are loaded in order:
1. /etc/stegasoo.conf (system-wide)
2. ~/.config/stegasoo/stegasoo.conf (per-user)
3. Environment variables (highest priority)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 15:45:31 -05:00
Aaron D. Lee
298f387c9a Move default install location to /opt/stegasoo
- setup.sh: Install to /opt/stegasoo with proper permissions
- first-boot-wizard.sh: Use /opt/stegasoo
- stegasoo-wizard.sh: Check /opt first, fallback to home dirs
- sanitize-for-image.sh: Handle both /opt and home locations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 15:43:42 -05:00
Aaron D. Lee
fcb71303df Update version badges from v4.0 to v4.1
- Update all v4.0 badges to v4.1 across templates
- Add cute v4.1 badge under Stegasoo title in navbar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 15:35:36 -05:00
Aaron D. Lee
abcff74dd4 Fix hover gradient direction on big buttons
- Align hover gradient with flipped eggplant→blue direction
- Update box-shadow to use eggplant purple

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 15:24:41 -05:00
Aaron D. Lee
355a988405 Add dynamic channel selector feedback with pulse highlight
- Channel select shows contextual info: Auto (server key), Public (no key), Custom (hidden)
- Gold pulse highlight on custom channel input when selected
- Smooth 0.4s animation with subtle glow effect
- Updated encode.html and decode.html with data-fingerprint attributes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 15:21:02 -05:00
Aaron D. Lee
fb55878727 Polish UI styling across site
- Flip gradient to purple→blue (eggplant #4a2860 → blue #5570d4)
- Add gold title styling (.title-gold) for Stegasoo branding
- Style two-choice toggles with gradient-matched purple/blue colors
- Equal-width toggle buttons with hover highlight
- Tools page: green→amber tab gradient with dark background
- Dashed separator between toggle options

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 14:56:08 -05:00
Aaron D. Lee
81d3f37f09 Refine Tools page tab colors and site-wide styling
- Green→lime→yellow gradient across tool tabs (#55df85→#c4f26a→#fdde4a)
- Complements blue→purple header gradient
- Fine-tuned header gold (#fee862) with subtle drop shadow
- Apply gold styling to .text-warning.small site-wide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:45:40 -05:00
Aaron D. Lee
3537e8cdf9 Redesign Tools page UI and refine site-wide styling
- Consolidate Tools into single card with tab toggle (Capacity/EXIF/Strip)
- Remove non-functional Peek feature (requires keys due to PRNG scattering)
- Add lime green (#a3e635) tool tab styling
- Add light straw gold (#fee862) card header text site-wide
- Add subtle drop shadow to headers and warning text
- Match Tools page styling to Encode/Decode pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:24:12 -05:00
Aaron D. Lee
d71f615d66 Improve EXIF tool error handling and UX
- Add loading spinner feedback for Clear All and Save buttons
- Show error alerts when requests fail instead of silent failure
- Detect session expiration and redirect to login
- Update UI to show empty state after clearing metadata
- Fix download by properly appending anchor to DOM

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 12:23:27 -05:00
Aaron D. Lee
ed1d230b4e Add template specification documentation
docs/TEMPLATES.md - Quick reference for all Jinja2 templates,
their routes, form fields, and JS dependencies.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 03:09:02 -05:00
Aaron D. Lee
13f145c3d5 Reduce toast notification delay to 10 seconds
Quick and snappy UX - 10s is plenty to read a notification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 02:30:02 -05:00
Aaron D. Lee
80dc22f150 Add Admin Recovery System with multiple backup options
- Recovery key generation (32-char alphanumeric, dashed format)
- Multiple backup methods: text file, QR code, stego image
- QR codes obfuscated with XOR (RECOVERY_OBFUSCATION_KEY constant)
- Stego backup hides key in image using Stegasoo itself
- CLI: `stegasoo admin recover --db path/to/db`
- Web routes: /recover, /account/recovery/regenerate
- Toast notifications now auto-dismiss after 20s with fade
- Updated WEB_UI.md and CLI.md documentation for v4.1.0

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 02:27:06 -05:00
Aaron D. Lee
01f0173dd4 Add EXIF Editor, consolidate channel key resolution
EXIF Editor (Library → CLI → API → WebUI):
- src/stegasoo/utils.py: read_image_exif(), write_image_exif()
- CLI: stegasoo tools exif [--clear|--set Field=Value]
- API: /api/tools/exif, /api/tools/exif/update, /api/tools/exif/clear
- WebUI: EXIF Editor tab with inline editing, clear all, save/download

Architectural consolidation:
- Moved resolve_channel_key() to src/stegasoo/channel.py (was duplicated in 3 frontends)
- Added get_channel_response_info() for consistent API/WebUI responses
- Frontends now use thin wrappers that translate exceptions

DCT improvements:
- Added will_fit_by_mode() pre-check to WebUI encode (fail fast)
- Suggests LSB mode when DCT capacity exceeded

Dependencies:
- Added piexif>=1.1.0 for EXIF editing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 01:16:33 -05:00
Aaron D. Lee
5df9b9dac8 Add Image Security Toolkit (tools)
Library:
- Add peek_image() to detect Stegasoo headers without decrypting

CLI:
- stegasoo tools capacity <image> - show LSB/DCT capacity
- stegasoo tools strip <image> - remove EXIF metadata
- stegasoo tools peek <image> - detect hidden data

API:
- POST /api/tools/capacity
- POST /api/tools/strip-metadata
- POST /api/tools/peek

WebUI:
- /tools page with tabbed interface (login required)
- Basic implementation - needs polish (dropzones, better results)

Architecture: Library -> CLI -> API -> WebUI pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 00:54:40 -05:00
Aaron D. Lee
2f1ac3a747 Switch flash messages to toast notifications
- Simple single-line toasts in top-right corner
- Positioned below navbar (70px from top)
- Auto-dismiss after 4 seconds
- Color-coded: green success, yellow warning, red error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 00:27:32 -05:00
Aaron D. Lee
8e5f01754f Improve user creation UX with modal dialog
- Replace redirect flow with AJAX + modal popup
- Show credentials side-by-side (username | password)
- Compact warning message and right-aligned action buttons
- Add Another resets form, Done returns to user list
- Narrow flash messages to match card width

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 00:10:48 -05:00
Aaron D. Lee
823b8824ea Add saved channel keys feature for Web UI users
- Database: Add user_channel_keys table with CASCADE delete
- Auth: Add CRUD functions for channel key management (10 keys/user limit)
- Routes: Add key save/delete/rename endpoints and JSON API
- Account page: Add saved keys section with add/rename/delete UI
- Encode/Decode: Add saved keys to channel key dropdown (optgroup)
- About page: Add Channel Key QR generator for sharing keys
- Track last_used_at when saved keys are used

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 23:47:59 -05:00
Aaron D. Lee
f4c1aa1912 Refactor: Extract inline JS to external files
New JS files:
- auth.js: Password toggle, confirmation validation, copy, regenerate
- generate.js: Form controls, credential display, memory story generation

Updated templates to use external JS:
- login.html, setup.html, account.html
- admin/user_new.html, user_created.html, password_reset.html
- generate.html (now uses generate.js + minimal Jinja-dependent inline)

Core stegasoo.js (943 lines) unchanged - already handles encode/decode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 23:33:17 -05:00
Aaron D. Lee
e502f42fb8 Add netplan WiFi cleanup to sanitize script
Some RPi OS variants store WiFi credentials in /etc/netplan/*.yaml
files, particularly NetworkManager-generated configs (90-NM-*.yaml).

- Remove netplan WiFi configs during sanitization
- Update validation to check netplan location
- Covers wpa_supplicant, NetworkManager, and netplan now

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 23:22:09 -05:00
Aaron D. Lee
08e42719ee Fix WiFi sanitization for NetworkManager (RPi OS Bookworm+)
Modern Raspberry Pi OS uses NetworkManager instead of wpa_supplicant.
WiFi connections are stored in /etc/NetworkManager/system-connections/.

- Add removal of NetworkManager WiFi connections
- Update validation to check both locations
- Fixes WiFi credentials being baked into distributable images

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 23:10:50 -05:00
Aaron D. Lee
21023099b0 Add CLI channel command group for channel key management
New commands:
- stegasoo channel generate [--save|--save-user]
- stegasoo channel show [--key KEY]
- stegasoo channel status
- stegasoo channel qr [--key KEY] [-o FILE] [--format ascii|png]
- stegasoo channel clear [--project|--user]

Features:
- ASCII QR code output for terminal display
- PNG QR code export for sharing
- JSON output mode (--json flag)
- Explicit key override for all commands

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 23:09:07 -05:00
Aaron D. Lee
8a41796d1b Update plan: mark multi-user support as completed
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:53:08 -05:00
Aaron D. Lee
7b33501495 Add multi-user support with admin user management
- Rewrite auth.py for multi-user schema (users table with roles)
- Auto-migrate from single-user admin_user table to new schema
- Add @admin_required decorator for protected routes
- Admin routes: /admin/users, /admin/users/new, delete, reset-password
- New templates: admin/users.html, user_new.html, user_created.html, password_reset.html
- Update login.html for username field, base.html and account.html for admin nav
- Max 16 users + 1 admin, session invalidation on delete/password reset

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:52:39 -05:00
Aaron D. Lee
a8f6ae1dd2 Add 4.1.0 feature plan
- Multi-user support (16+1 admin)
- Channel key QR codes (web + CLI)
- Advanced tools: capacity calc, metadata stripper, stego detector,
  image compare, header peek, batch mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:21:23 -05:00
Aaron D. Lee
b199f03f83 Add --reboot flag to sanitize script for full automation
Skips all prompts when passed, auto-reboots (soft reset) or
auto-shutdowns (full sanitize) when complete.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:50:43 -05:00
Aaron D. Lee
b97622956c Fix read prompts and reboot/shutdown in sanitize script
- Add </dev/tty to read commands for reliable terminal input
- Use exec for reboot/shutdown to prevent returning to shell

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:47:01 -05:00
Aaron D. Lee
3044c08fe3 Replace tail/head labels with ~~~~ in banners
Keep the wave decorations but remove the stegosaurus labels.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:43:22 -05:00
Aaron D. Lee
5042c7d555 Add ASCII banner to setup.sh and sanitize-for-image.sh
Consistent branding across all RPi scripts with the stegosaurus
plate banner, gray dots, and cyan accents.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:38:40 -05:00
Aaron D. Lee
aa8788168e Banner tweak (manual) 2026-01-03 21:36:04 -05:00
Aaron D. Lee
899d043892 Swap dot pattern after 2-space padding shift
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:34:11 -05:00
Aaron D. Lee
6fb63edc61 Add 2-space padding before trailing dots in banner
Consistent spacing between letter content and dot pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:33:33 -05:00
Aaron D. Lee
e74f12c24d Fix dot pattern direction - continue from left side
Lines starting with · should end with · pattern,
lines starting with . should end with . pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:32:22 -05:00
Aaron D. Lee
272d0e6ef0 Fix dot alignment on right side of ASCII banner
Consistent spacing between letter content and trailing dots.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:30:56 -05:00
Aaron D. Lee
f38bf4a1c6 Fix escape sequences in ASCII banner
Double backslashes needed to prevent \033 from being escaped
by preceding backslash characters in echo -e output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:27:57 -05:00
Aaron D. Lee
fee3133f9c Double up letter lines in ASCII banner for bolder look
Each row of STEGASOO letters is now duplicated for a thicker,
CRT scanline-style effect.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:25:19 -05:00
Aaron D. Lee
b058d8bf66 Refine ASCII banner: gray dots, cyan accents, 2-row plates
- Dots now gray with STEGASOO letters, plates, and labels in cyan
- Diamond plates simplified to 2 rows (/\ \/)
- Cleaner visual hierarchy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:21:32 -05:00
Aaron D. Lee
916a2e0e7b Fix SSH key regeneration service hanging on boot
Remove ExecStartPost that was calling systemctl restart ssh, which
caused a deadlock. The Before=ssh.service ordering ensures keys are
generated before ssh starts - no restart needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:15:41 -05:00
Aaron D. Lee
cccb40dc3a Update RPi scripts with new ASCII art banner and simpler headers
- Add stegosaurus-themed ASCII art with diamond plates and halftone dots
- Replace box-drawing characters with simple dashes for headers
- Consistent styling across first-boot-wizard.sh, setup.sh, sanitize-for-image.sh

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:13:38 -05:00
Aaron D. Lee
b60880c8b3 Add SSH key regeneration service to sanitize script
Creates a systemd service that regenerates SSH host keys on first boot,
fixing the issue where SSH would fail after sanitization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 20:37:39 -05:00
Aaron D. Lee
c96c595c78 Add robust jpegio ARM64 patching system
- Create rpi/patches/ directory with multi-strategy patching
- Patch tries: patch file → sed → Python regex → already-patched detection
- Fix jpegio patch to handle multiple -m64 occurrences
- Update docs to use wget instead of curl|bash (stdin conflict with read)
- Update SSH examples to use admin@stegasoo.local

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 20:22:55 -05:00
Aaron D. Lee
e129c38fd8 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>
2026-01-03 19:55:37 -05:00
Aaron D. Lee
0d7b5a14cb Improve RPi image scripts
- flash-image.sh: Add optional device argument to bypass auto-detection
- flash-image.sh/pull-image.sh: Remove bc dependency, use bash integer math
- sanitize-for-image.sh: Add better debugging and verification for wizard setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 03:16:01 -05:00
Aaron D. Lee
45b99d2c5e Switch image scripts to zstd compression
- pull-image.sh now uses zstd -19 instead of xz -9 (much faster, similar ratio)
- flash-image.sh supports .zst, .xz, and .gz formats
- Default output is now .img.zst

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 00:56:41 -05:00
Aaron D. Lee
c6f816d61f Add pull-image.sh and flash-image.sh helper scripts
- pull-image.sh: Auto-detects SD card, copies with pv progress, runs pishrink, compresses with xz
- flash-image.sh: Auto-detects SD card, flashes .img.xz/.img with pv progress
- Both scripts auto-detect 8-128GB USB drives and skip root filesystem
- Safety confirmations before destructive operations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 00:49:07 -05:00
Aaron D. Lee
83e9bd6fa1 Fix XSS vulnerability, request parsing bug, and session persistence
- Fix XSS in stegasoo.js: use textContent instead of innerHTML for filenames
- Fix operator precedence in channel key parsing (form data was ignored)
- Persist Flask secret key to instance/.secret_key so sessions survive restarts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 00:08:06 -05:00
Aaron D. Lee
5188492c77 Fix bold text escape codes in first-boot wizard 2026-01-02 23:26:44 -05:00
Aaron D. Lee
8bb70e5667 Add first-boot wizard for pre-built RPi images
- Create first-boot-wizard.sh with interactive step-by-step setup
  - Step 1: HTTPS configuration
  - Step 2: Port 443 configuration (if HTTPS enabled)
  - Step 3: Channel key generation
  - ASCII art banner and clear summaries
- Create stegasoo-wizard.sh profile.d hook to trigger wizard on SSH login
- Update sanitize-for-image.sh to:
  - Install wizard hook in /etc/profile.d/
  - Create first-boot flag file
  - Reset service to defaults for fresh config

Users who flash a pre-built image will see the wizard on first SSH login.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 23:24:20 -05:00
Aaron D. Lee
82ac1dcda4 Add interactive configuration prompts to RPi setup script
- Prompt for HTTPS enable/disable
- Prompt for port 443 with iptables redirect
- Prompt for channel key generation
- Offer to start service immediately
- Show summary with configured URL and channel key

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 23:20:32 -05:00
Aaron D. Lee
464e13567d Add STEGASOO_PORT env var, improve RPi setup output, channel key accordion
- Add STEGASOO_PORT environment variable support (default: 5000)
- Update .env.example with port and fix channel key format docs
- Move channel key generation to collapsible accordion in Generate page
- Improve RPi setup.sh output with HTTPS and channel key instructions
- Add rpi/BUILD_IMAGE.md workflow documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 23:11:04 -05:00
Aaron D. Lee
0b19a41b5e Add sanitize script for distributable Pi images
- rpi/sanitize-for-image.sh: Removes personal data before imaging
  - Clears WiFi credentials
  - Removes SSH keys
  - Clears Stegasoo auth database
  - Removes logs, history, temp files
- Updated rpi/README.md with full image building workflow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:06:14 -05:00
Aaron D. Lee
61c5178752 Fix channel key generation to use correct format
Use generate_channel_key() from channel module instead of hex
Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 22:02:33 -05:00
Aaron D. Lee
6b1b306f61 Add --channel-key flag to generate command
- stegasoo generate --channel-key now outputs a 256-bit hex key
- Also added .env.example template for Web UI configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 21:56:02 -05:00
Aaron D. Lee
267547caba Add Raspberry Pi setup script and documentation
- rpi/setup.sh: One-command install for Pi 4/5
  - Installs pyenv + Python 3.12
  - Patches and builds jpegio for ARM
  - Creates systemd service for auto-start
- rpi/README.md: Usage instructions

Install with: curl -sSL https://raw.githubusercontent.com/.../setup.sh | bash

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 21:45:31 -05:00
Aaron D. Lee
2ff28034f5 Add comprehensive Raspberry Pi installation instructions
- Step-by-step guide for Pi 4/5 deployment
- pyenv setup for Python 3.12 (Pi OS ships with 3.13)
- jpegio ARM build patch (sed one-liner for -m64 flag)
- Full verification steps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 21:42:27 -05:00
Aaron D. Lee
4cba75fe06 Move dev scripts to scripts/ directory
Consolidated all local dev scripts into scripts/ subdirectory.
Updated .gitignore to ignore entire scripts/ folder.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 20:28:09 -05:00
Aaron D. Lee
d03b3dea4b Update Web UI screenshots for v4.0.2
Refreshed all README screenshots with current UI styling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 20:16:01 -05:00
Aaron D. Lee
cf247d207f v4.0.2: Add Web UI authentication and optional HTTPS
Some checks failed
Release / test (push) Failing after 43s
Release / publish (push) Has been skipped
Release / github-release (push) Has been skipped
- Add single-admin login with SQLite3 user storage
- First-run setup wizard for admin account creation
- Account management page for password changes
- Optional HTTPS with auto-generated self-signed certificates
- Configurable via STEGASOO_AUTH_ENABLED, STEGASOO_HTTPS_ENABLED env vars
- UI improvements: larger QR previews, consistent panel styling
- Update docker-compose.yml with auth config and persistent volumes
- Update all documentation for v4.0.2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 20:00:47 -05:00
Aaron D. Lee
28d77957eb Bit of project management stuff. 2026-01-02 18:44:00 -05:00
Aaron D. Lee
89b4809489 Streamline README to focus on current features
Reduced from 433 to 123 lines by removing:
- Version history (now in CHANGELOG.md)
- Upgrade guides (now in CHANGELOG.md)
- Breaking changes sections
- Redundant examples
- Verbose project structure

README now focuses on: features, quick start, interfaces, security model.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 18:33:46 -05:00
Aaron D. Lee
79ab165b95 Add professional project structure and documentation
New files:
- LICENSE (MIT) - Required legal file
- CHANGELOG.md - Version history following Keep a Changelog
- CONTRIBUTING.md - Contributor guidelines
- CODE_OF_CONDUCT.md - Community standards
- .github/ISSUE_TEMPLATE/ - Bug report and feature request forms
- .github/PULL_REQUEST_TEMPLATE.md - PR checklist
- src/stegasoo/py.typed - PEP 561 type hint marker
- examples/ - Usage examples (basic, file embedding, channel keys)

Updated:
- README.md - Added CI status badges

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 18:23:08 -05:00
Aaron D. Lee
4194d6923a Remove backup files and add pattern to .gitignore
Deleted stale backup files:
- frontends/cli/main.py_old
- src/stegasoo/dct_steganography.py_old

Added gitignore patterns for common backup extensions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 18:13:04 -05:00
Aaron D. Lee
08e19a3bfd Remove obsolete debug/diagnostic scripts
Deleted one-off debugging scripts that are no longer needed:
- debug_jpegio.py - DCT/jpegio extraction debugger
- test_compare_capacity_flow.py - API flow crash diagnostic
- test_dct_crash.py - DCT crash diagnostic tool

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 18:10:29 -05:00
Aaron D. Lee
dea7472018 Remove build.sh from repo and update .gitignore
Dev convenience script should not be tracked.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 18:08:15 -05:00
Aaron D. Lee
e8863d15d7 Remove dev scripts from repo (already in .gitignore)
These are local dev convenience scripts that should not be tracked.
- quick_web.sh - Flask dev server launcher
- rbld_containers.sh - Docker rebuild helper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 18:06:45 -05:00
Aaron D. Lee
e4256cd037 Catch ValueError in has_dct_support() for numpy incompatibility
The jpegio package raises ValueError when compiled against numpy 2.x
but numpy 1.x is installed at runtime. This catches the error gracefully
so tests don't fail on Python 3.10 environments with mismatched numpy.

Also removes stale steganography.py_old backup file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 17:55:26 -05:00
Aaron D. Lee
948a582e5d Fix numpy binary incompatibility on Python 3.10
Add explicit numpy>=2.0.0 constraint to dct, web, and api extras.
Scipy/jpegio wheels are built against numpy 2.x, so we need to ensure
numpy 2.x is installed to avoid dtype size mismatch errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 17:47:44 -05:00
Aaron D. Lee
afa88bc73b Apply black formatter to all Python files
Reformatted 29 files for consistent code style and CI compliance.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 17:44:41 -05:00
Aaron D. Lee
221678d934 Moved all docs to root. 2026-01-02 17:41:27 -05:00
Aaron D. Lee
faf3efac0b Update documentation to v4.0.1
- README.md: Add v4.0.1 to version history
- API.md: Update title and version in examples to v4.0.1
- CLI.md: Update title to v4.0.1
- WEB_UI.md: Update to v4.0.1, document channel key dropdown and LED indicators

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 17:36:24 -05:00
Aaron D. Lee
9c45e0d0f8 Fix BatchCredentials tests: add required reference_photo
- Add sample_reference_photo fixture for test data
- Update sample_credentials fixture to include reference_photo
- Update all BatchCredentials test dicts to include reference_photo
- Add 'phrase' as legacy key in BatchCredentials.from_dict()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 17:28:14 -05:00
Aaron D. Lee
6b21190f97 Lint cleanup: ruff fixes across entire codebase
- Strip trailing whitespace from all Python files
- Fix import sorting (I001) across all modules
- Convert Optional[X] to X | None syntax (UP045)
- Remove unused imports (F401)
- Convert lambda assignments to def functions (E731)
- Add TYPE_CHECKING import for forward references
- Update pyproject.toml ruff config:
  - Move select/ignore to [tool.ruff.lint] section
  - Add per-file ignores for DCT colorspace naming (N803/N806)
  - Add per-file ignores for __init__.py import structure (E402)
  - Exclude defunct test_routes.py
- Remove frontends/web/test_routes.py (defunct debug snippet)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 17:17:38 -05:00
Aaron D. Lee
d94ee7be90 Bump version to 4.0.1 with Web UI improvements
- Update version to 4.0.1 across constants.py, __init__.py, pyproject.toml, README
- Refactor channel key UI from radio buttons to select dropdown
- Add LED indicator and key capsule CSS styles
- Reorganize encode/decode forms: RSA key section moved up, PIN + Channel in row
- Streamline channel key JavaScript for dropdown-based selection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 16:43:25 -05:00
Aaron D. Lee
6fa4b447db More snazzy 4.0 Web UI improvements. 2026-01-02 15:45:43 -05:00
Aaron D. Lee
1bb3589baf Lots of snazzy ui updates. 2026-01-02 13:18:58 -05:00
Aaron D. Lee
cfd1d8fb66 Snazzy ui updates. 2026-01-01 22:52:06 -05:00
Aaron D. Lee
c1beaf3611 Snazzy ui updates. 2026-01-01 22:51:53 -05:00
Aaron D. Lee
ef7478b30a A whoooole lotta 4.0.x fixes. 2026-01-01 22:18:13 -05:00
Aaron D. Lee
12929bf326 Release checklist and updated test scripts. 2026-01-01 14:04:55 -05:00
Aaron D. Lee
a001f227ec Bug fixes, CLI updates, docs. 2026-01-01 13:40:27 -05:00
Aaron D. Lee
3898031480 WebUI Fixes for 3.2 2026-01-01 03:39:44 -05:00
Aaron D. Lee
657cae0ae6 3.2.0 Big revamp 2026-01-01 03:14:35 -05:00
Aaron D. Lee
11fc8aab27 3.2.0 Big revamp 2026-01-01 03:14:27 -05:00
Aaron D. Lee
6d64c69f08 Home/about revamps. 2025-12-31 18:39:14 -05:00
Aaron D. Lee
6bd38ccf57 Added UNDER_THE_HOOD.md doc to explain the stego process. 2025-12-31 17:33:09 -05:00
Aaron D. Lee
d8fe25a121 Clean up old files. 2025-12-31 17:27:09 -05:00
Aaron D. Lee
3869391336 Updated screenshot for README.md 2025-12-31 17:24:23 -05:00
Aaron D. Lee
1b914f0409 Updated screenshot for README.md 2025-12-31 17:23:06 -05:00
Aaron D. Lee
2b7abc52c1 Updated screenshot for README.md 2025-12-31 17:21:07 -05:00
Aaron D. Lee
66f7d54db5 Updated encode page to not hide DCT/LSB selector, format tweaks. 2025-12-31 17:16:51 -05:00
Aaron D. Lee
34376b2dfe Version 3.0.2 full expirimental DCT support, jpegio for better jpg manipulation, etc. 2025-12-31 15:43:29 -05:00
Aaron D. Lee
4eefc946c4 Version 3.1.0 now with experimental DCT support. 2025-12-31 13:11:34 -05:00
218 changed files with 54940 additions and 9571 deletions

5
.claude/settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"church@church": true
}
}

52
.dockerignore Normal file
View File

@@ -0,0 +1,52 @@
# Git
.git
.gitignore
# Python
__pycache__
*.py[cod]
*.egg-info
.eggs
venv/
.venv/
# Instance data (user creates fresh)
frontends/web/instance/
frontends/web/certs/
instance/
# Test data
test_data/
tests/
# Pi-specific
rpi/
*.img
*.img.xz
*.img.zst
*.img.zst.zip
# Docs
*.md
docs/
# IDE
.vscode/
.idea/
# Misc
*.log
*.tmp
.DS_Store
# Dev scripts and old files
scripts/
old_files/
*_old
*_old.*
*.bak
*.orig
# Temp files
frontends/web/temp_files/
*.db

98
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
name: Bug Report
description: Report a bug or unexpected behavior
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug! Please fill out the form below.
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is.
placeholder: Describe the bug...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Run command '...'
2. Upload image '...'
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened?
validations:
required: true
- type: dropdown
id: interface
attributes:
label: Interface
description: Which interface are you using?
options:
- CLI
- Web UI
- REST API
- Python Library
validations:
required: true
- type: input
id: version
attributes:
label: Stegasoo Version
description: Run `stegasoo --version` or check the web UI footer
placeholder: "4.0.1"
validations:
required: true
- type: input
id: python-version
attributes:
label: Python Version
description: Run `python --version`
placeholder: "3.11.0"
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
placeholder: "Ubuntu 22.04 / Windows 11 / macOS 14"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Error Logs
description: Paste any relevant error messages or tracebacks.
render: shell
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context, screenshots, or files here.

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Security Vulnerability
url: https://github.com/adlee-was-taken/stegasoo/security/advisories/new
about: Report security vulnerabilities privately
- name: Documentation
url: https://github.com/adlee-was-taken/stegasoo#readme
about: Check the documentation before opening an issue

View File

@@ -0,0 +1,62 @@
name: Feature Request
description: Suggest a new feature or enhancement
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a feature! Please fill out the form below.
- type: textarea
id: problem
attributes:
label: Problem Statement
description: Is your feature request related to a problem? Describe it.
placeholder: I'm always frustrated when...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like.
placeholder: I would like to be able to...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Describe any alternative solutions or features you've considered.
- type: dropdown
id: interface
attributes:
label: Affected Interface(s)
description: Which interface(s) would this feature affect?
multiple: true
options:
- CLI
- Web UI
- REST API
- Python Library
- Core Library
- type: dropdown
id: priority
attributes:
label: Priority
description: How important is this feature to you?
options:
- Nice to have
- Would improve my workflow
- Critical for my use case
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context, mockups, or examples here.

46
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,46 @@
## Description
<!-- Describe your changes in detail -->
## Type of Change
<!-- Mark the relevant option with an 'x' -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
- [ ] Refactoring (no functional changes)
- [ ] CI/CD or build changes
## Related Issues
<!-- Link any related issues here -->
Fixes #
## Testing
<!-- Describe how you tested your changes -->
- [ ] I have added tests that prove my fix/feature works
- [ ] Existing tests pass locally with my changes
- [ ] I have tested manually (describe below)
### Manual Testing Steps
<!-- If applicable, describe manual testing performed -->
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation accordingly
- [ ] I have updated CHANGELOG.md (if user-facing changes)
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published
## Screenshots
<!-- If applicable, add screenshots to help explain your changes -->

View File

@@ -37,7 +37,8 @@ jobs:
publish:
needs: test # Only run if tests pass
runs-on: ubuntu-latest
environment: pypi
# Required for PyPI trusted publishing (recommended)
permissions:
id-token: write

View File

@@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false # Don't cancel other jobs if one fails
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.11", "3.12", "3.13"]
steps:
# 1. Get the code

55
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# Embedded repos (AUR packaging)
aur-cli-upload/
# Python
__pycache__/
*.py[cod]
@@ -35,6 +38,12 @@ old_files/
*.swp
*.swo
# Backup files
*_old
*_old.*
*.bak
*.orig
# Testing
.pytest_cache/
.coverage
@@ -48,7 +57,7 @@ htmlcov/
# Environment
.env
.env.*
.env.local
*.log
# Distribution
@@ -58,6 +67,44 @@ htmlcov/
# Output test files.
test_data/*.png
#Project root scripts.
rbld_containers.sh
quick_web.sh
# Dev scripts (local convenience scripts - except these)
scripts/*
!scripts/validate-release.sh
!scripts/smoke-test.sh
!scripts/setup-trusted-certs.sh
!scripts/screenshots.sh
!scripts/build.sh
# Web UI auth database and SSL certs
instance/
frontends/web/instance/
frontends/web/certs/
# Tests (private)
tests/
# RPi image build artifacts
*.img
*.img.xz
*.img.zst
*.img.zst.zip
rpi/tools/pishrink.sh
# Temp file storage
frontends/web/temp_files/
rpi/config.json
# Pre-built Pi tarballs and images (release assets, too large for git)
rpi/*.tar.zst
rpi/*.tar.zst.zip
rpi/*.img
rpi/*.img.zst
rpi/*.img.zst.zip
# AUR build artifacts
aur-upload/
aur/.SRCINFO
aur/*.pkg.tar.zst
# Docker pre-built images and deps (release assets, too large for git)
docker/*.tar.zst

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12

882
API.md Normal file
View File

@@ -0,0 +1,882 @@
# Stegasoo REST API Documentation (v4.0.2)
Complete REST API reference for Stegasoo steganography operations.
## Table of Contents
- [Overview](#overview)
- [What's New in v4.0.0](#whats-new-in-v400)
- [Installation](#installation)
- [Base URL](#base-url)
- [Endpoints](#endpoints)
- [GET /](#get--status)
- [GET /modes](#get-modes)
- [GET /channel/status](#get-channelstatus)
- [POST /channel/generate](#post-channelgenerate)
- [POST /channel/set](#post-channelset)
- [DELETE /channel](#delete-channel)
- [POST /generate](#post-generate)
- [POST /encode](#post-encode-json)
- [POST /encode/file](#post-encodefile)
- [POST /encode/multipart](#post-encodemultipart)
- [POST /decode](#post-decode-json)
- [POST /decode/multipart](#post-decodemultipart)
- [POST /compare](#post-compare)
- [POST /will-fit](#post-will-fit)
- [POST /image/info](#post-imageinfo)
- [Channel Keys](#channel-keys)
- [Data Models](#data-models)
- [Error Handling](#error-handling)
- [Code Examples](#code-examples)
---
## Overview
The Stegasoo REST API provides programmatic access to all steganography operations:
- **Generate** credentials (passphrase, PINs, RSA keys)
- **Encode** messages or files into images (LSB or DCT mode)
- **Decode** messages or files from images (auto-detects mode)
- **Channel keys** for deployment/group isolation (v4.0.0)
- **Analyze** image capacity and compare modes
The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads).
---
## What's New in v4.0.0
Version 4.0.0 adds **channel key** support for deployment/group isolation:
| Feature | Description |
|---------|-------------|
| Channel keys | 256-bit keys that isolate message groups |
| New endpoints | `/channel/status`, `/channel/generate`, `/channel/set`, `DELETE /channel` |
| Encode/decode param | `channel_key` parameter on all encode/decode endpoints |
| Response headers | `X-Stegasoo-Channel-Mode` and `X-Stegasoo-Channel-Fingerprint` |
**Key benefits:**
- ✅ Isolate messages between teams, deployments, or groups
- ✅ Same credentials can't decode messages from different channels
- ✅ Backward compatible (public mode = no channel key)
**Breaking change:** v4.0.0 messages (with channel key) cannot be decoded by v3.x installations.
---
## Installation
### From PyPI
```bash
pip install stegasoo[api]
```
### Running the Server
**Development:**
```bash
cd frontends/api
python main.py
```
**Production:**
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
```
**Docker with channel key:**
```bash
STEGASOO_CHANNEL_KEY=XXXX-XXXX-... docker-compose -f docker/docker-compose.yml up api
```
---
## Base URL
| Environment | URL |
|-------------|-----|
| Local Development | `http://localhost:8000` |
| Docker | `http://localhost:8000` |
| Production | Configure as needed |
---
## Endpoints
### GET / (Status)
Check API status and configuration.
#### Response
```json
{
"version": "4.0.2",
"has_argon2": true,
"has_qrcode_read": true,
"has_dct": true,
"max_payload_kb": 500,
"available_modes": ["lsb", "dct"],
"dct_features": {
"output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"]
},
"channel": {
"mode": "private",
"configured": true,
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456",
"source": "~/.stegasoo/channel.key"
},
"breaking_changes": {
"v4_channel_key": "Messages encoded with channel key require same key to decode",
"format_version": 5,
"backward_compatible": false
}
}
```
---
### GET /modes
Get available embedding modes and channel status.
#### Response
```json
{
"lsb": {
"available": true,
"name": "Spatial LSB",
"description": "Embed in pixel LSBs, outputs PNG/BMP",
"output_format": "PNG (color)",
"capacity_ratio": "100%"
},
"dct": {
"available": true,
"name": "DCT Domain",
"output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"],
"capacity_ratio": "~20% of LSB",
"requires": "scipy"
},
"channel": {
"mode": "private",
"configured": true,
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456"
}
}
```
---
### GET /channel/status
Get current channel key status. **New in v4.0.0.**
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `reveal` | boolean | `false` | Include full key in response |
#### Response
```json
{
"mode": "private",
"configured": true,
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456",
"source": "~/.stegasoo/channel.key",
"key": null
}
```
With `reveal=true`:
```json
{
"mode": "private",
"configured": true,
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456",
"source": "~/.stegasoo/channel.key",
"key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456"
}
```
#### cURL Example
```bash
# Show status
curl http://localhost:8000/channel/status
# Reveal full key
curl "http://localhost:8000/channel/status?reveal=true"
```
---
### POST /channel/generate
Generate a new channel key. **New in v4.0.0.**
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `save` | boolean | `false` | Save to user config |
| `save_project` | boolean | `false` | Save to project config |
#### Response
```json
{
"key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456",
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456",
"saved": true,
"save_location": "~/.stegasoo/channel.key"
}
```
#### cURL Examples
```bash
# Just generate (don't save)
curl -X POST http://localhost:8000/channel/generate
# Generate and save to user config
curl -X POST "http://localhost:8000/channel/generate?save=true"
# Generate and save to project config
curl -X POST "http://localhost:8000/channel/generate?save_project=true"
```
---
### POST /channel/set
Set/save a channel key to config. **New in v4.0.0.**
#### Request Body
```json
{
"key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456",
"location": "user"
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `key` | string | required | Channel key |
| `location` | string | `"user"` | `"user"` or `"project"` |
#### Response
```json
{
"success": true,
"location": "~/.stegasoo/channel.key",
"fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456"
}
```
---
### DELETE /channel
Clear channel key from config. **New in v4.0.0.**
#### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `location` | string | `"user"` | `"user"`, `"project"`, or `"all"` |
#### Response
```json
{
"success": true,
"mode": "public",
"still_configured": false,
"remaining_source": null
}
```
#### cURL Example
```bash
# Clear user config
curl -X DELETE http://localhost:8000/channel
# Clear project config
curl -X DELETE "http://localhost:8000/channel?location=project"
# Clear all
curl -X DELETE "http://localhost:8000/channel?location=all"
```
---
### POST /generate
Generate credentials for encoding/decoding.
#### Request Body
```json
{
"use_pin": true,
"use_rsa": false,
"pin_length": 6,
"rsa_bits": 2048,
"words_per_passphrase": 4
}
```
#### Response
```json
{
"passphrase": "abandon ability able about",
"pin": "847293",
"rsa_key_pem": null,
"entropy": {
"passphrase": 44,
"pin": 19,
"rsa": 0,
"total": 63
}
}
```
---
### POST /encode (JSON)
Encode a text message into an image.
#### Request Body
```json
{
"message": "Secret message here",
"reference_photo_base64": "iVBORw0KGgo...",
"carrier_image_base64": "iVBORw0KGgo...",
"passphrase": "apple forest thunder mountain",
"pin": "123456",
"rsa_key_base64": null,
"rsa_password": null,
"channel_key": null,
"embed_mode": "lsb",
"dct_output_format": "png",
"dct_color_mode": "grayscale"
}
```
#### Channel Key Parameter (v4.0.0)
| Value | Effect |
|-------|--------|
| `null` | Auto mode - use server-configured key |
| `""` (empty string) | Public mode - no channel isolation |
| `"XXXX-XXXX-..."` | Explicit key - use this specific key |
#### Response
```json
{
"stego_image_base64": "iVBORw0KGgo...",
"filename": "a1b2c3d4.png",
"capacity_used_percent": 12.4,
"embed_mode": "lsb",
"output_format": "png",
"color_mode": "color",
"channel_mode": "private",
"channel_fingerprint": "ABCD-••••-••••-••••-••••-••••-••••-3456"
}
```
---
### POST /encode/file
Encode a file into an image (JSON with base64).
Same parameters as `/encode`, plus:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file_data_base64` | string | ✓ | Base64-encoded file data |
| `filename` | string | ✓ | Original filename |
| `mime_type` | string | | MIME type |
---
### POST /encode/multipart
Encode using multipart form data (file uploads).
#### Form Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `passphrase` | string | ✓ | Passphrase |
| `reference_photo` | file | ✓ | Reference photo |
| `carrier` | file | ✓ | Carrier image |
| `message` | string | * | Text message |
| `payload_file` | file | * | Binary file to embed |
| `pin` | string | | Static PIN |
| `rsa_key` | file | | RSA key (.pem) |
| `rsa_key_qr` | file | | RSA key (QR code image) |
| `rsa_password` | string | | RSA key password |
| `channel_key` | string | | `"auto"` (default), `"none"=public`, or explicit key |
| `embed_mode` | string | | `"lsb"` or `"dct"` |
| `dct_output_format` | string | | `"png"` or `"jpeg"` |
| `dct_color_mode` | string | | `"grayscale"` or `"color"` |
\* Provide either `message` or `payload_file`
#### Channel Key in Multipart
For form data, the channel_key field uses strings:
| Value | Effect |
|-------|--------|
| `"auto"` | Use server config (default) |
| `"none"` | Public mode |
| `"XXXX-XXXX-..."` | Explicit key |
#### Response
Returns the stego image directly with headers:
```http
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: attachment; filename=a1b2c3d4.png
X-Stegasoo-Capacity-Percent: 12.4
X-Stegasoo-Embed-Mode: lsb
X-Stegasoo-Channel-Mode: private
X-Stegasoo-Channel-Fingerprint: ABCD-••••-...-3456
X-Stegasoo-Version: 4.0.2
<binary image data>
```
#### cURL Examples
```bash
# Encode with auto channel key (default)
curl -X POST http://localhost:8000/encode/multipart \
-F "passphrase=apple forest thunder mountain" \
-F "pin=123456" \
-F "message=Secret message" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
# Encode with explicit channel key
curl -X POST http://localhost:8000/encode/multipart \
-F "passphrase=words here" \
-F "pin=123456" \
-F "message=Team message" \
-F "channel_key=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
# Encode in public mode (no channel isolation)
curl -X POST http://localhost:8000/encode/multipart \
-F "passphrase=words here" \
-F "pin=123456" \
-F "message=Public message" \
-F "channel_key=none" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
```
---
### POST /decode (JSON)
Decode a message or file from a stego image.
#### Request Body
```json
{
"stego_image_base64": "iVBORw0KGgo...",
"reference_photo_base64": "iVBORw0KGgo...",
"passphrase": "apple forest thunder mountain",
"pin": "123456",
"rsa_key_base64": null,
"rsa_password": null,
"channel_key": null,
"embed_mode": "auto"
}
```
#### Response (Text)
```json
{
"payload_type": "text",
"message": "Secret message here",
"file_data_base64": null,
"filename": null,
"mime_type": null
}
```
#### Response (File)
```json
{
"payload_type": "file",
"message": null,
"file_data_base64": "UEsDBBQAAAA...",
"filename": "document.pdf",
"mime_type": "application/pdf"
}
```
---
### POST /decode/multipart
Decode using multipart form data.
#### Form Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `passphrase` | string | ✓ | Passphrase |
| `reference_photo` | file | ✓ | Reference photo |
| `stego_image` | file | ✓ | Stego image to decode |
| `pin` | string | | Static PIN |
| `rsa_key` | file | | RSA key (.pem) |
| `rsa_key_qr` | file | | RSA key (QR code image) |
| `rsa_password` | string | | RSA key password |
| `channel_key` | string | | `"auto"` (default), `"none"=public`, or explicit key |
| `embed_mode` | string | | `"auto"`, `"lsb"`, or `"dct"` |
---
## Channel Keys
### Overview
Channel keys provide **deployment/group isolation**. Messages encoded with a channel key can only be decoded with the same key.
### Key Format
```
ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
└──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘
8 groups of 4 alphanumeric characters (256 bits)
```
### Storage Locations
Keys are checked in order:
| Priority | Location | Best For |
|----------|----------|----------|
| 1 | `STEGASOO_CHANNEL_KEY` env var | Docker, CI/CD |
| 2 | `./config/channel.key` | Project-specific |
| 3 | `~/.stegasoo/channel.key` | User default |
### API Parameter Values
#### JSON Endpoints (`/encode`, `/decode`)
| Value | Effect |
|-------|--------|
| `null` | Auto - use server config |
| `""` | Public mode |
| `"XXXX-..."` | Explicit key |
#### Multipart Endpoints (`/encode/multipart`, `/decode/multipart`)
| Value | Effect |
|-------|--------|
| `"auto"` | Use server config (default) |
| `"none"` | Public mode |
| `"XXXX-..."` | Explicit key |
### Workflow Example
```bash
# 1. Generate a channel key for the team
KEY=$(curl -s -X POST http://localhost:8000/channel/generate | jq -r '.key')
echo "Team key: $KEY"
# 2. Distribute to team members (securely!)
# 3. Each deployment sets the key
export STEGASOO_CHANNEL_KEY=$KEY
# 4. Encode - automatically uses server key
curl -X POST http://localhost:8000/encode/multipart \
-F "passphrase=team passphrase" \
-F "pin=123456" \
-F "message=Team secret" \
-F "reference_photo=@ref.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
# 5. Decode - automatically uses server key
curl -X POST http://localhost:8000/decode/multipart \
-F "passphrase=team passphrase" \
-F "pin=123456" \
-F "reference_photo=@ref.jpg" \
-F "stego_image=@stego.png"
```
---
## Data Models
### ChannelStatusResponse
```json
{
"mode": "private",
"configured": true,
"fingerprint": "ABCD-••••-...-3456",
"source": "~/.stegasoo/channel.key",
"key": "ABCD-1234-..."
}
```
### EncodeResponse (v4.0.0)
```json
{
"stego_image_base64": "string",
"filename": "string",
"capacity_used_percent": 12.4,
"embed_mode": "lsb",
"output_format": "png",
"color_mode": "color",
"channel_mode": "private",
"channel_fingerprint": "ABCD-••••-...-3456"
}
```
### DecodeResponse
```json
{
"payload_type": "text",
"message": "string",
"file_data_base64": null,
"filename": null,
"mime_type": null
}
```
---
## Error Handling
### HTTP Status Codes
| Code | Meaning | Use Case |
|------|---------|----------|
| 200 | OK | Successful operation |
| 400 | Bad Request | Invalid input, capacity error, invalid channel key |
| 401 | Unauthorized | Decryption failed, channel key mismatch |
| 500 | Internal Error | Unexpected server error |
| 501 | Not Implemented | Feature unavailable |
### Channel Key Errors
| Status | Error | Cause |
|--------|-------|-------|
| 400 | "Invalid channel key format" | Key doesn't match `XXXX-XXXX-...` pattern |
| 401 | "Message encoded with channel key but none configured" | Need to provide channel key |
| 401 | "Message encoded without channel key" | Use `channel_key=""` or `"none"` |
---
## Code Examples
### Python
```python
import requests
BASE_URL = "http://localhost:8000"
# Check channel status
status = requests.get(f"{BASE_URL}/channel/status").json()
print(f"Channel mode: {status['mode']}")
print(f"Fingerprint: {status.get('fingerprint', 'N/A')}")
# Generate channel key
response = requests.post(f"{BASE_URL}/channel/generate?save=true")
key_info = response.json()
print(f"Generated: {key_info['fingerprint']}")
# Encode with channel key (auto from server)
with open("ref.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
response = requests.post(f"{BASE_URL}/encode/multipart", files={
"reference_photo": ref,
"carrier": carrier,
}, data={
"message": "Team secret",
"passphrase": "apple forest thunder",
"pin": "123456",
# channel_key defaults to "auto" (use server config)
})
with open("stego.png", "wb") as f:
f.write(response.content)
print(f"Channel mode: {response.headers.get('X-Stegasoo-Channel-Mode')}")
# Encode with explicit channel key
with open("ref.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
response = requests.post(f"{BASE_URL}/encode/multipart", files={
"reference_photo": ref,
"carrier": carrier,
}, data={
"message": "Using explicit key",
"passphrase": "words here",
"pin": "123456",
"channel_key": "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456",
})
# Decode
with open("ref.jpg", "rb") as ref, open("stego.png", "rb") as stego:
response = requests.post(f"{BASE_URL}/decode/multipart", files={
"reference_photo": ref,
"stego_image": stego,
}, data={
"passphrase": "apple forest thunder",
"pin": "123456",
# channel_key defaults to "auto"
})
result = response.json()
print(f"Decoded: {result.get('message')}")
```
### JavaScript
```javascript
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const BASE_URL = 'http://localhost:8000';
async function main() {
// Check channel status
const status = await axios.get(`${BASE_URL}/channel/status`);
console.log('Channel:', status.data.mode);
// Encode with auto channel key
const form = new FormData();
form.append('passphrase', 'apple forest thunder');
form.append('pin', '123456');
form.append('message', 'Secret');
form.append('reference_photo', fs.createReadStream('ref.jpg'));
form.append('carrier', fs.createReadStream('carrier.png'));
// channel_key defaults to "auto" (use server config)
const response = await axios.post(`${BASE_URL}/encode/multipart`, form, {
headers: form.getHeaders(),
responseType: 'arraybuffer'
});
fs.writeFileSync('stego.png', response.data);
console.log('Channel mode:', response.headers['x-stegasoo-channel-mode']);
}
main();
```
### cURL / Bash
```bash
#!/bin/bash
BASE_URL="http://localhost:8000"
# Check channel status
echo "Channel status:"
curl -s "$BASE_URL/channel/status" | jq .
# Generate and save channel key
echo "Generating channel key..."
curl -s -X POST "$BASE_URL/channel/generate?save=true" | jq .
# Encode (channel_key defaults to "auto")
echo "Encoding..."
curl -s -X POST "$BASE_URL/encode/multipart" \
-F "passphrase=apple forest thunder" \
-F "pin=123456" \
-F "message=Secret message" \
-F "reference_photo=@ref.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
echo "Encoded to stego.png"
# Decode
echo "Decoding..."
curl -s -X POST "$BASE_URL/decode/multipart" \
-F "passphrase=apple forest thunder" \
-F "pin=123456" \
-F "reference_photo=@ref.jpg" \
-F "stego_image=@stego.png" | jq .
```
---
## Docker Configuration
### docker/docker-compose.yml
```yaml
x-common-env: &common-env
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
services:
api:
build:
context: .
target: api
ports:
- "8000:8000"
environment:
<<: *common-env
```
### .env (gitignored)
```bash
STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
```
### Generate key for .env
```bash
curl -s -X POST http://localhost:8000/channel/generate | \
jq -r '"STEGASOO_CHANNEL_KEY=\(.key)"' >> .env
```
---
## See Also
- [CLI Documentation](CLI.md) - Command-line interface
- [Web UI Documentation](WEB_UI.md) - Browser interface
- [README](../README.md) - Project overview

234
CHANGELOG.md Normal file
View File

@@ -0,0 +1,234 @@
# Changelog
All notable changes to Stegasoo will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org).
## [4.3.0] - 2026-02-27
### Added
- **Audio Steganography** — Hide messages in audio files (WAV, FLAC, MP3, OGG, AAC, M4A)
- LSB mode: Direct least-significant-bit embedding in audio samples
- Spread Spectrum mode: Noise-resistant encoding using pseudo-random spreading
- Automatic format transcoding to WAV for embedding
- Full CLI support: `stegasoo audio-encode`, `audio-decode`, `audio-info`
- REST API endpoints: `/audio/encode`, `/audio/decode`, `/audio/info`
- Web UI: Unified encode/decode pages with carrier type selector (Image/Audio)
- New `AudioCapacityInfo`, `AudioEmbedStats`, `AudioInfo` model classes
- Audio-specific exceptions: `AudioError`, `AudioValidationError`, `AudioCapacityError`, `AudioExtractionError`, `AudioTranscodeError`, `UnsupportedAudioFormatError`
- Subprocess isolation for audio operations (crash protection)
- `debug.py` module for structured logging across all steganography operations
### Changed
- Encode/Decode web pages now have a "Carrier Type" step to switch between Image and Audio
- Version bumped to 4.3.0
## [4.1.5] - 2026-01-07
### Added
- **Developer Documentation**: Educational comments throughout core modules
- DCT module: zig-zag diagrams, QIM explanation, Reed-Solomon deep dive
- LSB module: visual bit embedding examples, ChaCha20 pixel selection
- Crypto module: multi-factor KDF flow diagrams, Argon2id reasoning
- CLI module: Click patterns (groups, JSON output, secure input)
- Web UI module: Flask architecture, subprocess isolation, async jobs
- **Pi Test Automation**: `rpi/kickoff-pi-test.sh` script
- One command to flash, wait for boot, setup, and smoke test
- Self-contained (no dotfile dependencies)
- **v4.2 Wishlist**: `WISHLIST-4.2.md` for blue-sky ideas (GPU acceleration)
### Changed
- **Pi MOTD Improvements**:
- Dynamic temperature emoji (ice/cool/fire based on temp)
- Rocket emoji for service status, globe emoji for URL
- Shortened Debian boilerplate message
- Fixed escaped variable syntax in heredoc
## [4.1.3] - 2026-01-05
### Added
- **Docker Deployment**: Production-ready containerization
- `docker-compose.yml` for Web UI (port 5000) and REST API (port 8000)
- Multi-stage builds with base image for faster rebuilds
- Health checks, resource limits (768MB), and volume persistence
- Comprehensive `DOCKER.md` documentation
- **Raspberry Pi First-Boot Wizard**: Interactive TUI setup experience
- `gum` TUI toolkit for styled prompts and spinners
- WiFi configuration, HTTPS setup, channel key generation
- Overclock presets (Pi 5: 2.8/3.0 GHz with cooling recommendations)
- Port 443 redirect option for clean HTTPS URLs
- Styled banners with purple→blue gradient and gold logo
- **Pi Image Distribution**: Scripts for SD card imaging
- `sanitize-for-image.sh` removes credentials, SSH keys, user data
- Soft reset mode for testing without clearing WiFi
- Auto-validates sanitization before imaging
- **Unit Tests**: Comprehensive pytest test suite
- Tests for encode/decode, LSB/DCT modes, channel keys
- Validation, generation, compression, edge cases
- 29 tests covering core library functionality
- **Release Validation**: `scripts/validate-release.sh` for pre-release checks
- **Custom SSL Documentation**: Guide for replacing certs, Let's Encrypt setup
### Changed
- Pi MOTD shows CPU speed and temperature when overclocked
- Mobile UI polish and responsive improvements
- Standardized ASCII banners across all Pi scripts
- Setup script uses pyenv for Python 3.12 (Pi OS ships 3.13)
### Fixed
- **SSL certificate generation**: Wizard and setup now generate certs when HTTPS enabled
- DCT decode reliability improvements
- Fixed `gum --inline` flag compatibility (not supported in all versions)
- Wizard banner alignment and spacing issues
- Better error handling in app.py for SSL failures
## [4.1.0] - 2026-01-04
### Added
- **Admin Recovery System**: Password reset for locked-out admins
- Recovery key generated during setup (32-char alphanumeric)
- Multiple backup options: text file, QR code, stego image
- QR codes obfuscated (XOR'd with magic header hash)
- Stego backups hide key in an image using Stegasoo itself
- CLI: `stegasoo admin recover --db path/to/db`
- **EXIF Editor**: Full metadata editing in Tools page
- View all EXIF fields from uploaded image
- Inline editing of individual fields
- Clear all metadata with one click
- Download cleaned image
- CLI: `stegasoo tools exif image.jpg [--clear] [--set Field=Value]`
- **Multi-User Support**: Admin can create up to 16 additional users
- Role-based access control (admin/user)
- Admin user management page
- Temp password generation for new users
- **Saved Channel Keys**: Users can save/manage channel keys in account page
### Changed
- **Architecture**: Consolidated `resolve_channel_key()` to library layer
- Single source of truth in `src/stegasoo/channel.py`
- CLI, API, WebUI now use thin wrappers
- **DCT Pre-Check**: Fail fast with helpful error before expensive encoding
- **Toast Notifications**: Auto-dismiss after 20 seconds with fade animation
- `RECOVERY_OBFUSCATION_KEY` constant added to `constants.py`
### Fixed
- DCT payload size error now caught early with clear message
## [4.0.2] - 2026-01-02
### Added
- **Web UI Authentication**: Single-admin login with SQLite3 user storage
- First-run setup wizard for admin account creation
- Account management page for password changes
- `@login_required` decorator protects encode/decode/generate routes
- Argon2id password hashing (lighter 64MB for fast login)
- **Optional HTTPS**: Auto-generated self-signed certificates for home network deployment
- Configurable via `STEGASOO_HTTPS_ENABLED` environment variable
- Certificates stored in `frontends/web/certs/`
- New environment variables: `STEGASOO_AUTH_ENABLED`, `STEGASOO_HTTPS_ENABLED`, `STEGASOO_HOSTNAME`
### Changed
- PIN entry column widened in encode/decode forms (col-md-4 → col-md-6)
- Channel options column narrowed (col-md-8 → col-md-6)
- QR preview panels enlarged for better text readability
- Consistent font sizing across all preview panel banners (0.7rem filename, 0.6rem data, 0.65rem badges)
### Fixed
- QR preview text too small to read in encode/decode templates
- Inconsistent label sizes between reference/carrier/stego panels
## [4.0.1] - 2025-01-02
### Fixed
- Fixed numpy binary incompatibility on Python 3.10 (jpegio/scipy)
- Fixed BatchCredentials test failures with missing `reference_photo` parameter
- Graceful handling when DCT dependencies have version mismatches
### Changed
- Applied `ruff` linter fixes across entire codebase (~400 issues)
- Applied `black` formatter to all Python files
- Modernized type hints: `Optional[X]``X | None`
- Updated ruff config to use `[tool.ruff.lint]` section
- Moved documentation files to repository root
### Removed
- Removed obsolete debug/diagnostic scripts
- Cleaned up backup files and dev scripts
## [4.0.0] - 2024-12-29
### Added
- Refreshed Web UI with modern, snazzy interface
- Improved user experience across all pages
### Changed
- Major version bump for breaking API changes
- Simplified passphrase handling (single passphrase instead of day-based)
- Removed date_str parameter from encoding
### Fixed
- Various bug fixes for Web UI
- CLI updates and improvements
## [3.2.0] - 2024-12-28
### Added
- Big revamp of the encoding system
- Home and about page improvements
- UNDER_THE_HOOD.md documentation
### Changed
- Renamed `phrase``passphrase` in API
- Updated Web UI styling
## [3.0.2] - 2024-12-27
### Added
- Full experimental DCT steganography support
- jpegio integration for better JPEG manipulation
- DCT/LSB mode selector in Web UI
## [3.0.0] - 2024-12-25
### Added
- DCT (Discrete Cosine Transform) steganography mode
- Support for JPEG carriers without quality loss
- Channel key feature for private messaging
### Changed
- Complete rewrite of steganography engine
- New hybrid authentication system
## [2.0.0] - 2024-12-20
### Added
- Web UI frontend
- REST API (FastAPI)
- Batch processing support
- RSA key authentication option
### Changed
- Migrated to hybrid photo + passphrase + PIN authentication
## [1.0.0] - 2024-12-15
### Added
- Initial release
- LSB steganography
- AES-256-GCM encryption
- CLI interface
- Basic PIN authentication
[4.3.0]: https://github.com/adlee-was-taken/stegasoo/compare/v4.2.1...v4.3.0
[4.1.5]: https://github.com/adlee-was-taken/stegasoo/compare/v4.1.3...v4.1.5
[4.1.3]: https://github.com/adlee-was-taken/stegasoo/compare/v4.1.0...v4.1.3
[4.1.0]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.2...v4.1.0
[4.0.2]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.1...v4.0.2
[4.0.1]: https://github.com/adlee-was-taken/stegasoo/compare/v4.0.0...v4.0.1
[4.0.0]: https://github.com/adlee-was-taken/stegasoo/compare/v3.2.0...v4.0.0
[3.2.0]: https://github.com/adlee-was-taken/stegasoo/compare/v3.0.2...v3.2.0
[3.0.2]: https://github.com/adlee-was-taken/stegasoo/compare/v3.0.0...v3.0.2
[3.0.0]: https://github.com/adlee-was-taken/stegasoo/compare/v2.0.0...v3.0.0
[2.0.0]: https://github.com/adlee-was-taken/stegasoo/compare/v1.0.0...v2.0.0
[1.0.0]: https://github.com/adlee-was-taken/stegasoo/releases/tag/v1.0.0

114
CLAUDE.md Normal file
View File

@@ -0,0 +1,114 @@
# Stegasoo — Claude Code Project Guide
Stegasoo is a secure steganography toolkit with hybrid photo + passphrase + PIN authentication.
Version 4.3.0 · Python >=3.11 · MIT License
## Quick commands
```bash
pip install -e ".[dev]" # Install for development (includes all extras)
pytest # Run tests (coverage reported automatically)
black src/ tests/ frontends/ # Format code
ruff check src/ tests/ frontends/ --fix # Lint (auto-fix)
mypy src/ # Type check
pre-commit run --all-files # Run all pre-commit hooks
PYTHONPATH=src python -m stegasoo.cli # Run CLI directly without install
```
## Architecture
```
src/stegasoo/ Core library
crypto.py Argon2id / PBKDF2 key derivation + AES-256-GCM encryption
steganography.py LSB spatial embedding
dct_steganography.py DCT domain embedding (JPEG-safe, needs [dct] extras)
validation.py Input validation for all security factors
constants.py All magic numbers, crypto params, limits
models.py Dataclasses (EncodeResult, DecodeResult, etc.)
encode.py / decode.py High-level encode/decode orchestration
channel.py Channel key management (v4.0+)
audio_steganography.py LSB audio embedding/extraction (v4.3.0)
spread_steganography.py Spread spectrum audio embedding (v4.3.0)
audio_utils.py Audio format detection, validation, transcoding (v4.3.0)
debug.py Structured logging for operations (v4.3.0)
compression.py Zstandard / zlib / lz4 payload compression
cli.py Click CLI entry point
generate.py Credential generation (passphrase, PIN, RSA keys)
exceptions.py Exception hierarchy (all inherit StegasooError)
__init__.py Public API surface (__all__)
frontends/web/ Flask web UI (entry: app.py)
frontends/api/ FastAPI REST API (entry: main.py)
frontends/cli/ CLI extras
tests/ Pytest suite
test_stegasoo.py Single test file covering core library
```
### Entry points
| Interface | Entry point | Install extra |
|-----------|-------------|---------------|
| CLI | `stegasoo.cli:main` (`stegasoo` command) | `[cli]` |
| Web UI | `frontends/web/app.py` | `[web]` |
| REST API | `frontends/api/main.py` | `[api]` |
## Code conventions
- **Formatter**: Black, 100-char line length
- **Linter**: Ruff — rules E, F, I, N, W, UP (E501 ignored). N803/N806 suppressed in `dct_steganography.py` for colorspace variable names
- **Type hints**: Required on all new code. `mypy` with `ignore_missing_imports = true`
- **Pre-commit hooks**: ruff, ruff-format, trailing-whitespace, end-of-file-fixer, check-yaml, check-toml, check-added-large-files (1MB), check-merge-conflict, debug-statements, bandit (excludes tests/)
- **Branch naming**: `feature/`, `fix/`, `docs/`, `refactor/`
- **Commits**: Imperative mood, clear subject line. Include what + why
## Security-critical modules
These files implement the cryptographic and steganographic core. Changes require extra care, thorough test coverage, and careful review:
- **`crypto.py`** — Argon2id KDF (256 MB / 4 iterations / 4 parallelism) + PBKDF2 fallback (600K iterations) → AES-256-GCM authenticated encryption
- **`steganography.py`** — LSB spatial embedding/extraction
- **`dct_steganography.py`** — DCT domain embedding with Reed-Solomon error correction
- **`validation.py`** — Input validation for all security factors (PIN, passphrase, image, RSA key, channel key)
- **`constants.py`** — Crypto parameters (salt sizes, iteration counts, Argon2 memory cost, format versions). Do not change these casually — they affect backward compatibility and security margins
## Public API
`src/stegasoo/__init__.py` defines the full public API surface via `__all__`. Any new public function must be:
1. Imported in `__init__.py`
2. Added to the `__all__` list
## Testing
- Single test file: `tests/test_stegasoo.py`
- Requires `pip install -e ".[dev]"` (includes DCT dependencies)
- Coverage is reported automatically via pytest config (`--cov=stegasoo --cov-report=term-missing`)
- Run: `pytest` (no extra flags needed)
## Worktree workflow
When working on features or fixes that touch multiple files, prefer using a git worktree for isolation:
```bash
# Claude Code can create worktrees automatically via /worktree or EnterWorktree
# Manual creation:
git worktree add .claude/worktrees/<name> -b <branch-name>
```
### Guidelines for worktree usage
- **Use worktrees for**: multi-file refactors, experimental changes, anything that might need to be discarded
- **Worktree location**: `.claude/worktrees/` (gitignored by Claude Code)
- **Branch from**: always branch from `main` unless working on a version branch (e.g., `4.2`)
- **Naming**: use the same conventions as branches — `feature/description`, `fix/description`, etc.
- **Cleanup**: worktrees in `.claude/worktrees/` are ephemeral. Remove with `git worktree remove <path>` when done
- **Testing in worktrees**: run `pip install -e ".[dev]"` inside the worktree before running tests, since the editable install points to the worktree's source
- **Merging back**: create a PR from the worktree branch, or merge locally into `main`
## Useful context
- BIP-39 wordlist lives at `src/stegasoo/data/bip39-words.txt` (used for passphrase generation)
- Docker support: `src/stegasoo/Dockerfile` + `docs/DOCKER_QUICKSTART.md`
- Raspberry Pi builds: `rpi/` directory
- AUR packages: `aur/`, `aur-cli/`, `aur-api/`
- Version is defined in both `pyproject.toml` and `src/stegasoo/__init__.py` — keep them in sync

921
CLI.md Normal file
View File

@@ -0,0 +1,921 @@
# Stegasoo CLI Documentation (v4.1.0)
Complete command-line interface reference for Stegasoo steganography operations.
## Table of Contents
- [Installation](#installation)
- [What's New in v4.1.0](#whats-new-in-v410)
- [Quick Start](#quick-start)
- [Commands](#commands)
- [generate](#generate-command)
- [encode](#encode-command)
- [decode](#decode-command)
- [verify](#verify-command)
- [channel](#channel-command)
- [admin](#admin-command)
- [tools](#tools-command)
- [info](#info-command)
- [compare](#compare-command)
- [modes](#modes-command)
- [Channel Keys](#channel-keys)
- [Embedding Modes](#embedding-modes)
- [Security Factors](#security-factors)
- [Workflow Examples](#workflow-examples)
- [Piping & Scripting](#piping--scripting)
- [Error Handling](#error-handling)
- [Exit Codes](#exit-codes)
---
## Installation
### From PyPI
```bash
# CLI only
pip install stegasoo[cli]
# CLI with DCT support
pip install stegasoo[cli,dct]
# With all extras
pip install stegasoo[all]
```
### From Source
```bash
git clone https://github.com/example/stegasoo.git
cd stegasoo
pip install -e ".[cli,dct]"
```
### Verify Installation
```bash
stegasoo --version
stegasoo --help
# Check DCT support
python -c "from stegasoo import has_dct_support; print('DCT:', 'available' if has_dct_support() else 'requires scipy')"
# Check channel key status
stegasoo channel show
```
### Man Page
```bash
# Install man page
sudo mkdir -p /usr/local/share/man/man1
sudo cp docs/stegasoo.1 /usr/local/share/man/man1/
sudo mandb
# View
man stegasoo
```
---
## What's New in v4.1.0
Version 4.1.0 adds **admin recovery** and **tools** commands:
| Feature | Description |
|---------|-------------|
| Admin recovery | Reset admin password using recovery key |
| EXIF tools | View, edit, and strip image metadata |
| Peek tool | Quick stego detection check |
| Strip tool | Remove hidden data from images |
**New commands:**
- `stegasoo admin recover` - Reset admin password with recovery key
- `stegasoo tools exif` - View/edit EXIF metadata
- `stegasoo tools peek` - Check for hidden data
- `stegasoo tools strip` - Remove stego data from image
---
## What's New in v4.0.0
Version 4.0.0 added **channel key** support for deployment/group isolation:
| Feature | Description |
|---------|-------------|
| Channel keys | 256-bit keys that isolate message groups |
| Deployment isolation | Different deployments can't read each other's messages |
| CLI management | New `stegasoo channel` command group |
| Flexible override | Use server config, explicit key, or public mode |
---
## Quick Start
```bash
# 1. Generate credentials (do this once, memorize results)
stegasoo generate
# 2. (Optional) Set up channel key for deployment isolation
stegasoo channel generate --save
# 3. Encode a message (uses configured channel key automatically)
stegasoo encode \
--ref secret_photo.jpg \
--carrier meme.png \
--passphrase "apple forest thunder mountain" \
--pin 123456 \
--message "Meet at midnight"
# 4. Decode a message (uses same channel key)
stegasoo decode \
--ref secret_photo.jpg \
--stego stego_abc123.png \
--passphrase "apple forest thunder mountain" \
--pin 123456
# 5. Decode without channel key (public mode)
stegasoo decode \
--ref secret_photo.jpg \
--stego public_stego.png \
--passphrase "words here now" \
--pin 123456 \
--no-channel
```
---
## Commands
### Generate Command
Generate credentials for encoding/decoding operations.
#### Synopsis
```bash
stegasoo generate [OPTIONS]
```
#### Options
| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--pin/--no-pin` | | flag | `--pin` | Generate a PIN |
| `--rsa/--no-rsa` | | flag | `--no-rsa` | Generate an RSA key |
| `--pin-length` | | 6-9 | 6 | PIN length in digits |
| `--rsa-bits` | | choice | 2048 | RSA key size (2048, 3072) |
| `--words` | | 3-12 | 4 | Words in passphrase |
| `--output` | `-o` | path | | Save RSA key to file |
| `--password` | `-p` | string | | Password for RSA key file |
| `--json` | | flag | | Output as JSON |
#### Examples
```bash
# Basic generation with PIN (default)
stegasoo generate
# Generate with more words for higher security
stegasoo generate --words 6
# Generate with RSA key
stegasoo generate --rsa --rsa-bits 3072
# Save RSA key to encrypted file
stegasoo generate --rsa -o mykey.pem -p "mysecretpassword"
```
---
### Encode Command
Encode a secret message or file into an image.
#### Synopsis
```bash
stegasoo encode [OPTIONS]
```
#### Options
| Option | Short | Type | Required | Default | Description |
|--------|-------|------|----------|---------|-------------|
| `--ref` | `-r` | path | ✓ | | Reference photo |
| `--carrier` | `-c` | path | ✓ | | Carrier image |
| `--passphrase` | `-p` | string | ✓ | | Passphrase |
| `--message` | `-m` | string | | | Message to encode |
| `--message-file` | `-f` | path | | | Read message from file |
| `--embed-file` | `-e` | path | | | Embed a binary file |
| `--pin` | | string | * | | Static PIN (6-9 digits) |
| `--key` | `-k` | path | * | | RSA key file |
| `--key-qr` | | path | * | | RSA key from QR code |
| `--key-password` | | string | | | RSA key password |
| `--channel` | | string | | auto | Channel key (v4.0.0) |
| `--channel-file` | | path | | | Read channel key from file |
| `--no-channel` | | flag | | | Force public mode |
| `--output` | `-o` | path | | | Output filename |
| `--mode` | | choice | | `lsb` | Embedding mode |
| `--dct-format` | | choice | | `png` | DCT output format |
| `--dct-color` | | choice | | `grayscale` | DCT color mode |
| `--quiet` | `-q` | flag | | | Suppress output |
\* At least one of `--pin`, `--key`, or `--key-qr` is required.
#### Channel Key Options
| Option | Effect |
|--------|--------|
| *(none)* | Use server-configured key (auto mode) |
| `--channel KEY` | Use explicit channel key |
| `--channel auto` | Same as no option |
| `--channel-file F` | Read channel key from file |
| `--no-channel` | Force public mode (no isolation) |
#### Examples
```bash
# Basic encoding (uses server channel key if configured)
stegasoo encode \
-r photo.jpg -c meme.png \
-p "correct horse battery staple" \
--pin 847293 \
-m "The package arrives Tuesday"
# With explicit channel key
stegasoo encode \
-r photo.jpg -c meme.png \
-p "correct horse battery staple" \
--pin 847293 \
-m "Secret message" \
--channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
# Public mode (no channel isolation)
stegasoo encode \
-r photo.jpg -c meme.png \
-p "correct horse battery staple" \
--pin 847293 \
-m "Public message" \
--no-channel
# DCT mode for social media
stegasoo encode \
-r photo.jpg -c meme.png \
-p "words here" --pin 847293 \
-m "Secret" \
--mode dct --dct-format jpeg
```
---
### Decode Command
Decode a secret message or file from a stego image.
#### Synopsis
```bash
stegasoo decode [OPTIONS]
```
#### Options
| Option | Short | Type | Required | Default | Description |
|--------|-------|------|----------|---------|-------------|
| `--ref` | `-r` | path | ✓ | | Reference photo |
| `--stego` | `-s` | path | ✓ | | Stego image |
| `--passphrase` | `-p` | string | ✓ | | Passphrase |
| `--pin` | | string | * | | Static PIN |
| `--key` | `-k` | path | * | | RSA key file |
| `--key-qr` | | path | * | | RSA key from QR code |
| `--key-password` | | string | | | RSA key password |
| `--channel` | | string | | auto | Channel key (v4.0.0) |
| `--channel-file` | | path | | | Read channel key from file |
| `--no-channel` | | flag | | | Force public mode |
| `--output` | `-o` | path | | | Save output to file |
| `--mode` | | choice | | `auto` | Extraction mode |
| `--quiet` | `-q` | flag | | | Minimal output |
| `--force` | | flag | | | Overwrite existing file |
#### Examples
```bash
# Basic decoding (uses server channel key)
stegasoo decode \
-r photo.jpg -s stego.png \
-p "correct horse battery staple" \
--pin 847293
# With explicit channel key
stegasoo decode \
-r photo.jpg -s stego.png \
-p "words here" --pin 847293 \
--channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
# Decode public image (no channel key was used)
stegasoo decode \
-r photo.jpg -s stego.png \
-p "words here" --pin 847293 \
--no-channel
# Save to file
stegasoo decode \
-r photo.jpg -s stego.png \
-p "words" --pin 123456 \
-o decoded.txt
```
---
### Verify Command
Verify credentials without extracting the message.
#### Synopsis
```bash
stegasoo verify [OPTIONS]
```
#### Options
Same as `decode`, minus `--output` and `--force`. Adds `--json` for JSON output.
#### Examples
```bash
# Quick verification
stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456
# With explicit channel key
stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456 \
--channel ABCD-1234-...
# JSON output
stegasoo verify -r photo.jpg -s stego.png -p "phrase" --pin 123456 --json
```
---
### Channel Command
Manage channel keys for deployment/group isolation.
#### Subcommands
| Subcommand | Description |
|------------|-------------|
| `generate` | Create a new channel key |
| `show` | Display current channel key status |
| `set` | Save a channel key to config |
| `clear` | Remove channel key from config |
#### channel generate
```bash
stegasoo channel generate [OPTIONS]
```
| Option | Short | Description |
|--------|-------|-------------|
| `--save` | `-s` | Save to user config (~/.stegasoo/channel.key) |
| `--save-project` | | Save to project config (./config/channel.key) |
| `--env` | `-e` | Output as environment variable export |
| `--quiet` | `-q` | Output only the key |
**Examples:**
```bash
# Just display a new key
stegasoo channel generate
# Save to user config
stegasoo channel generate --save
# Add to .env file
stegasoo channel generate --env >> .env
# For scripts
KEY=$(stegasoo channel generate -q)
```
#### channel show
```bash
stegasoo channel show [OPTIONS]
```
| Option | Short | Description |
|--------|-------|-------------|
| `--reveal` | `-r` | Show full key (not just fingerprint) |
| `--json` | | Output as JSON |
**Examples:**
```bash
# Show status (fingerprint only)
stegasoo channel show
# Reveal full key
stegasoo channel show --reveal
# JSON for scripts
stegasoo channel show --json
```
**Output:**
```
─── CHANNEL KEY STATUS ───
Mode: PRIVATE
Fingerprint: ABCD-••••-••••-••••-••••-••••-••••-3456
Source: ~/.stegasoo/channel.key
Messages require this channel key to decode.
```
#### channel set
```bash
stegasoo channel set [KEY] [OPTIONS]
```
| Option | Short | Description |
|--------|-------|-------------|
| `--file` | `-f` | Read key from file |
| `--project` | `-p` | Save to project config instead of user |
**Examples:**
```bash
# Set from command line
stegasoo channel set ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
# Set from file
stegasoo channel set --file channel.key
# Set in project config
stegasoo channel set XXXX-... --project
```
#### channel clear
```bash
stegasoo channel clear [OPTIONS]
```
| Option | Short | Description |
|--------|-------|-------------|
| `--project` | `-p` | Clear project config |
| `--all` | | Clear both user and project configs |
| `--force` | `-f` | Skip confirmation |
**Examples:**
```bash
# Clear user config (with confirmation)
stegasoo channel clear
# Clear project config
stegasoo channel clear --project
# Clear all configs without confirmation
stegasoo channel clear --all --force
```
---
### Info Command
Show information about an image file.
```bash
stegasoo info IMAGE [OPTIONS]
```
---
### Compare Command
Compare embedding mode capacities for an image.
```bash
stegasoo compare IMAGE [OPTIONS]
```
---
### Modes Command
Show available embedding modes and their status.
```bash
stegasoo modes
```
Now also displays channel key status.
---
### Admin Command
Manage Web UI admin accounts and recovery.
#### Subcommands
| Subcommand | Description |
|------------|-------------|
| `recover` | Reset admin password using recovery key |
#### admin recover
Reset the admin password for a Web UI database.
```bash
stegasoo admin recover --db PATH [OPTIONS]
```
| Option | Short | Type | Required | Description |
|--------|-------|------|----------|-------------|
| `--db` | `-d` | path | ✓ | Path to stegasoo.db file |
| `--key` | `-k` | string | | Recovery key (prompted if not provided) |
| `--password` | `-p` | string | | New password (prompted if not provided) |
**Examples:**
```bash
# Interactive mode (prompts for key and password)
stegasoo admin recover --db frontends/web/instance/stegasoo.db
# Non-interactive mode
stegasoo admin recover \
--db /path/to/stegasoo.db \
--key "XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" \
--password "NewSecurePassword123"
```
**Recovery process:**
1. The recovery key is verified against the database hash
2. If valid, the admin password is reset
3. User can now log in with the new password
**Note:** Recovery keys are instance-bound. A key from one database won't work on another.
---
### Tools Command
Image utilities and analysis tools.
#### Subcommands
| Subcommand | Description |
|------------|-------------|
| `exif` | View/edit EXIF metadata |
| `peek` | Check for hidden data |
| `strip` | Remove stego data from image |
#### tools exif
View and edit EXIF metadata in images.
```bash
stegasoo tools exif IMAGE [OPTIONS]
```
| Option | Type | Description |
|--------|------|-------------|
| `--clear` | flag | Remove all EXIF metadata |
| `--set FIELD=VALUE` | string | Set a specific EXIF field |
| `--output` / `-o` | path | Output filename (default: overwrites input) |
| `--json` | flag | Output as JSON |
**Examples:**
```bash
# View all EXIF data
stegasoo tools exif photo.jpg
# View as JSON
stegasoo tools exif photo.jpg --json
# Clear all metadata
stegasoo tools exif photo.jpg --clear -o clean.jpg
# Set specific fields
stegasoo tools exif photo.jpg \
--set "Artist=John Doe" \
--set "Copyright=2026" \
-o tagged.jpg
# Remove GPS data only
stegasoo tools exif photo.jpg \
--set "GPSLatitude=" \
--set "GPSLongitude=" \
-o no-gps.jpg
```
#### tools peek
Check if an image contains hidden Stegasoo data.
```bash
stegasoo tools peek IMAGE [OPTIONS]
```
| Option | Type | Description |
|--------|------|-------------|
| `--json` | flag | Output as JSON |
| `--quiet` / `-q` | flag | Exit code only (0=found, 1=not found) |
**Examples:**
```bash
# Check for hidden data
stegasoo tools peek suspicious.png
# Script-friendly check
if stegasoo tools peek image.png -q; then
echo "Contains hidden data"
fi
```
#### tools strip
Remove hidden stego data from an image (destructive).
```bash
stegasoo tools strip IMAGE [OPTIONS]
```
| Option | Type | Description |
|--------|------|-------------|
| `--output` / `-o` | path | Output filename |
| `--force` / `-f` | flag | Overwrite without confirmation |
**Examples:**
```bash
# Strip and save to new file
stegasoo tools strip stego.png -o clean.png
# Strip in place (with confirmation)
stegasoo tools strip stego.png
```
---
## Channel Keys
Channel keys provide **deployment/group isolation** - messages encoded with a channel key can only be decoded by systems with the same key.
### Key Format
```
ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
└──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘
8 groups of 4 alphanumeric characters (256 bits)
```
### Storage Locations
Channel keys are checked in this order:
| Priority | Location | Best For |
|----------|----------|----------|
| 1 | `STEGASOO_CHANNEL_KEY` env var | Docker, CI/CD |
| 2 | `./config/channel.key` | Project-specific |
| 3 | `~/.stegasoo/channel.key` | User default |
### Modes
| Mode | Description | CLI Option |
|------|-------------|------------|
| **Auto** | Use server-configured key | *(default)* |
| **Explicit** | Use specific key | `--channel KEY` |
| **Public** | No channel isolation | `--no-channel` |
### Fingerprints
For security, full keys aren't displayed by default. Instead, a fingerprint is shown:
```
Full key: ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
Fingerprint: ABCD-••••-••••-••••-••••-••••-••••-3456
```
### Use Cases
**Team isolation:**
```bash
# Team A
export STEGASOO_CHANNEL_KEY=AAAA-1111-...
# Team B
export STEGASOO_CHANNEL_KEY=BBBB-2222-...
# Messages from Team A can only be decoded by Team A
```
**Development vs Production:**
```bash
# Development
./config/channel.key contains DEV-KEY-...
# Production
STEGASOO_CHANNEL_KEY=PROD-KEY-... in Docker
# Dev messages can't be decoded in production
```
**Public messages:**
```bash
# Anyone with credentials can decode
stegasoo encode ... --no-channel
stegasoo decode ... --no-channel
```
---
## Embedding Modes
### LSB Mode (Default)
```bash
stegasoo encode ... --mode lsb
```
| Aspect | Details |
|--------|---------|
| **Capacity** | ~375 KB for 1920×1080 |
| **Output** | PNG only |
| **Best For** | Maximum capacity |
### DCT Mode
```bash
stegasoo encode ... --mode dct --dct-format jpeg --dct-color color
```
| Aspect | Details |
|--------|---------|
| **Capacity** | ~65 KB for 1920×1080 |
| **Output** | PNG or JPEG |
| **Best For** | Social media, stealth |
---
## Security Factors
| Factor | Description | Entropy |
|--------|-------------|---------|
| Reference Photo | Shared image | ~80-256 bits |
| Passphrase | BIP-39 words | ~44 bits (4 words) |
| Static PIN | Numeric (6-9) | ~20 bits (6 digits) |
| RSA Key | Shared key file | ~128 bits |
| Channel Key (v4.0.0) | Deployment isolation | ~256 bits |
---
## Workflow Examples
### Team Setup with Channel Key
**Initial setup (team lead):**
```bash
# Generate team channel key
stegasoo channel generate -q > team_channel.key
# Distribute to team members securely
# (encrypted email, secure file share, etc.)
```
**Team member setup:**
```bash
# Save received key
stegasoo channel set --file team_channel.key
# Verify
stegasoo channel show
```
**Daily use:**
```bash
# Channel key is used automatically
stegasoo encode -r ref.jpg -c meme.png -p "phrase" --pin 123456 -m "Team message"
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456
```
### Docker Deployment
**docker/docker-compose.yml:**
```yaml
x-common-env: &common-env
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
services:
web:
environment:
<<: *common-env
api:
environment:
<<: *common-env
```
**.env (gitignored):**
```bash
STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
```
### CI/CD Pipeline
```bash
# Generate key for CI
CHANNEL_KEY=$(stegasoo channel generate -q)
# Use in pipeline
STEGASOO_CHANNEL_KEY=$CHANNEL_KEY stegasoo encode ...
```
---
## Piping & Scripting
### Extract channel key for scripts
```bash
# Get just the key
KEY=$(stegasoo channel show --json | jq -r '.key // empty')
# Get fingerprint
FINGERPRINT=$(stegasoo channel show --json | jq -r '.fingerprint // "none"')
# Check if configured
if stegasoo channel show --json | jq -e '.configured' > /dev/null; then
echo "Channel key is configured"
fi
```
### Generate and use immediately
```bash
# Generate, save, and use
stegasoo channel generate --save
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "message"
```
---
## Error Handling
### Channel Key Errors
| Error | Cause | Solution |
|-------|-------|----------|
| "Invalid channel key format" | Key doesn't match pattern | Use `stegasoo channel generate` |
| "Message encoded with channel key but none configured" | Missing channel key | Set key or use `--channel` |
| "Message encoded without channel key" | Used `--no-channel` to encode | Decode with `--no-channel` |
| "Channel key mismatch" | Wrong key | Verify correct key |
### Troubleshooting
```bash
# Check current channel status
stegasoo channel show
# Try decoding with explicit key
stegasoo decode ... --channel XXXX-XXXX-...
# Try decoding without channel key
stegasoo decode ... --no-channel
```
---
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error / decryption failed |
| 2 | Invalid arguments/options |
---
## Environment Variables
| Variable | Description |
|----------|-------------|
| `STEGASOO_CHANNEL_KEY` | Channel key for deployment isolation (v4.0.0) |
| `PYTHONPATH` | Include `src/` for development |
| `STEGASOO_DEBUG` | Enable debug output (set to `1`) |
---
## See Also
- [API Documentation](API.md) - Python API reference
- [Web UI Documentation](WEB_UI.md) - Browser interface guide
- [README](../README.md) - Project overview and security model

54
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,54 @@
# Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior:
* The use of sexualized language or imagery, and sexual attention or advances
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information without explicit permission
* Other conduct which could reasonably be considered inappropriate
## Enforcement Responsibilities
Project maintainers are responsible for clarifying and enforcing our standards
of acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the project maintainers. All complaints will be reviewed and
investigated promptly and fairly.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
version 2.0.

165
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,165 @@
# Contributing to Stegasoo
Thank you for your interest in contributing to Stegasoo! This document provides guidelines and information for contributors.
## Getting Started
### Prerequisites
- Python 3.10 - 3.12
- Git
- Docker (optional, for container testing)
### Development Setup
1. **Clone the repository**
```bash
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
```
2. **Create a virtual environment**
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. **Install development dependencies**
```bash
pip install -e ".[dev]"
```
4. **Install pre-commit hooks**
```bash
pre-commit install
```
## Development Workflow
### Code Style
We use the following tools to maintain code quality:
- **Black** - Code formatting (line length: 100)
- **Ruff** - Linting
- **MyPy** - Type checking
Run all checks before committing:
```bash
black src/ tests/ frontends/
ruff check src/ tests/ frontends/
mypy src/
```
### Running Tests
```bash
# Run all tests
pytest
# Run with coverage
pytest --cov=stegasoo --cov-report=term-missing
# Run specific test file
pytest tests/test_stegasoo.py
```
### Type Hints
All new code should include type hints:
```python
def encode_message(
message: str,
carrier_image: bytes,
passphrase: str,
pin: str = "",
) -> EncodeResult:
...
```
## Making Changes
### Branch Naming
- `feature/description` - New features
- `fix/description` - Bug fixes
- `docs/description` - Documentation updates
- `refactor/description` - Code refactoring
### Commit Messages
Write clear, concise commit messages:
```
Add channel key validation for private messaging
- Implement validate_channel_key() function
- Add tests for valid/invalid key formats
- Update CLI to support --channel-key flag
```
### Pull Request Process
1. **Create a feature branch** from `main`
2. **Make your changes** with appropriate tests
3. **Ensure all checks pass** (tests, linting, formatting)
4. **Submit a PR** with a clear description
5. **Address review feedback** promptly
### PR Checklist
- [ ] Tests added/updated for changes
- [ ] Documentation updated if needed
- [ ] CHANGELOG.md updated for user-facing changes
- [ ] All CI checks passing
- [ ] No merge conflicts with `main`
## Project Structure
```
stegasoo/
├── src/stegasoo/ # Core library
│ ├── crypto.py # Encryption/decryption
│ ├── steganography.py # LSB embedding
│ ├── dct_steganography.py # DCT embedding
│ └── ...
├── frontends/
│ ├── cli/ # Command-line interface
│ ├── web/ # Flask web UI
│ └── api/ # FastAPI REST API
├── tests/ # Test suite
└── examples/ # Usage examples
```
## Reporting Issues
### Bug Reports
Please include:
- Python version and OS
- Stegasoo version (`stegasoo --version`)
- Minimal reproduction steps
- Expected vs actual behavior
- Error messages/tracebacks
### Feature Requests
Please include:
- Use case description
- Proposed solution (if any)
- Alternatives considered
## Security
If you discover a security vulnerability, please see [SECURITY.md](SECURITY.md) for responsible disclosure guidelines. **Do not open a public issue for security vulnerabilities.**
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
## Questions?
Feel free to open a discussion or issue if you have questions about contributing.
Thank you for helping make Stegasoo better!

156
DOCKER.md Normal file
View File

@@ -0,0 +1,156 @@
# Docker Deployment
Stegasoo provides Docker images for both the Web UI and REST API.
## Quick Start
```bash
# Build and start all services
docker-compose -f docker/docker-compose.yml up -d
# Check status
docker-compose -f docker/docker-compose.yml ps
```
Access:
- **Web UI**: https://localhost:5000 (HTTPS with self-signed cert)
- **REST API**: http://localhost:8000
## Services
| Service | Port | Description |
|---------|------|-------------|
| `web` | 5000 | Flask Web UI with authentication |
| `api` | 8000 | FastAPI REST API |
## Configuration
### Environment Variables
Create a `.env` file or set these variables:
```bash
# Channel key for private group communication (optional)
STEGASOO_CHANNEL_KEY=XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
# Web UI authentication (default: enabled)
STEGASOO_AUTH_ENABLED=true
# HTTPS support (default: enabled, generates self-signed cert)
STEGASOO_HTTPS_ENABLED=true
STEGASOO_HOSTNAME=localhost
# To disable HTTPS:
# STEGASOO_HTTPS_ENABLED=false
```
### Volume Mounts
Persistent data is stored in Docker volumes:
| Volume | Purpose |
|--------|---------|
| `stegasoo-web-data` | User database, session data |
| `stegasoo-web-certs` | SSL certificates (if HTTPS enabled) |
## Building
### Standard Build (Recommended)
Uses a pre-built base image with all dependencies:
```bash
# First time only: build the base image
docker build -f docker/Dockerfile.base -t stegasoo-base:latest .
# Build services (fast - only copies app code)
docker-compose -f docker/docker-compose.yml build
```
### Full Build (No Base Image)
If you don't have the base image, the Dockerfile will build all dependencies (slower):
```bash
docker-compose -f docker/docker-compose.yml build
```
## Commands
```bash
# Start services
docker-compose -f docker/docker-compose.yml up -d
# View logs
docker-compose -f docker/docker-compose.yml logs -f
# Stop services
docker-compose -f docker/docker-compose.yml down
# Rebuild after code changes
docker-compose -f docker/docker-compose.yml build && docker-compose -f docker/docker-compose.yml up -d
# Full rebuild (no cache)
docker-compose -f docker/docker-compose.yml build --no-cache
```
## Resource Limits
Each container is configured with:
- **Memory limit**: 768 MB
- **Memory reservation**: 384 MB
This accounts for Argon2id's 256 MB RAM requirement during key derivation.
## Health Checks
Both services include health checks:
- Interval: 30 seconds
- Timeout: 10 seconds
- Start period: 5 seconds
- Retries: 3
Check health status:
```bash
docker-compose -f docker/docker-compose.yml ps
```
## Production Deployment
For production, consider:
1. **Enable HTTPS**:
```bash
STEGASOO_HTTPS_ENABLED=true
STEGASOO_HOSTNAME=your-domain.com
```
2. **Use secrets for channel key**:
```bash
# Don't commit .env files with secrets
export STEGASOO_CHANNEL_KEY=your-key
docker-compose -f docker/docker-compose.yml up -d
```
3. **Reverse proxy**: Put behind nginx/traefik for TLS termination
4. **Backup volumes**:
```bash
docker run --rm -v stegasoo-web-data:/data -v $(pwd):/backup \
alpine tar czf /backup/stegasoo-backup.tar.gz /data
```
## Troubleshooting
### Container won't start
```bash
# Check logs
docker-compose -f docker/docker-compose.yml logs web
docker-compose -f docker/docker-compose.yml logs api
```
### Out of memory
Increase Docker's memory allocation or reduce worker count in `docker/Dockerfile`.
### Permission errors
The containers run as non-root user `stego` (UID 1000). Ensure volume permissions match.

950
INSTALL.md Normal file
View File

@@ -0,0 +1,950 @@
# Stegasoo Installation Guide
Complete installation instructions for all platforms and deployment methods.
## Table of Contents
- [Requirements](#requirements)
- [Quick Install](#quick-install)
- [Installation Methods](#installation-methods)
- [From Source (Development)](#from-source-development)
- [From PyPI](#from-pypi)
- [Docker](#docker)
- [Docker Compose](#docker-compose)
- [Optional Dependencies](#optional-dependencies)
- [Platform-Specific Notes](#platform-specific-notes)
- [Verification](#verification)
- [Troubleshooting](#troubleshooting)
---
## Requirements
### Python Version Requirements
| Python Version | Status | Notes |
|----------------|--------|-------|
| 3.10 | ❌ Not Supported | Dropped in v4.2.1 |
| 3.11 | ✅ Supported | Minimum version |
| 3.12 | ✅ Supported | Recommended |
| 3.13 | ✅ Supported | |
| 3.14 | ✅ Supported | Tested on Arch |
**Note:** v4.2.1 switched from `jpegio` to `jpeglib` for DCT steganography, enabling Python 3.11-3.14 support.
### Minimum Requirements
| Requirement | Value |
|-------------|-------|
| Python | 3.11-3.14 |
| RAM | 512 MB minimum (256MB for Argon2) |
| Disk | ~100 MB |
### System Dependencies
**Linux (Debian/Ubuntu):**
```bash
sudo apt-get update
sudo apt-get install -y \
python3.12 \
python3.12-venv \
python3-pip \
python3-dev \
libzbar0 \
libjpeg-dev \
build-essential
```
**Linux (Arch):**
```bash
# Use pyenv for Python version management
curl https://pyenv.run | bash
pyenv install 3.12
pyenv local 3.12
sudo pacman -S zbar libjpeg-turbo base-devel
```
**macOS:**
```bash
brew install python@3.12 zbar jpeg
xcode-select --install # For compilation
```
**Windows:**
- Install Python 3.12 from [python.org](https://python.org)
- Install Visual Studio Build Tools for compilation
---
## Quick Install
```bash
# Clone and install everything
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
# Create venv with Python 3.12 (critical!)
python3.12 -m venv venv
source venv/bin/activate # Linux/macOS
# or: venv\Scripts\activate # Windows
# Install all dependencies
pip install -e ".[all]"
# Verify
stegasoo --version
python -c "from stegasoo import has_dct_support; print(f'DCT: {has_dct_support()}')"
```
---
## Installation Methods
### From Source (Development)
Best for development or customization.
```bash
# Clone the repository
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
# Create virtual environment with Python 3.12 (recommended)
python3.12 -m venv venv
source venv/bin/activate # Linux/macOS
# or: venv\Scripts\activate # Windows
# Verify Python version
python -V # Should show 3.12.x
# Install core library only
pip install -e .
# Install with specific extras
pip install -e ".[cli]" # Command-line interface
pip install -e ".[web]" # Flask web UI + DCT support
pip install -e ".[api]" # FastAPI REST API + DCT support
pip install -e ".[dct]" # DCT steganography only
pip install -e ".[compression]" # LZ4 compression
# Install everything
pip install -e ".[all]"
# Install with development tools
pip install -e ".[dev]"
```
### From PyPI
```bash
# Core only
pip install stegasoo
# With extras
pip install stegasoo[cli]
pip install stegasoo[web]
pip install stegasoo[api]
pip install stegasoo[all]
```
### Docker
Build and run individual containers.
#### Build Images
```bash
# From project root - build all targets
docker build -t stegasoo-web --target web -f docker/Dockerfile .
docker build -t stegasoo-api --target api -f docker/Dockerfile .
docker build -t stegasoo-cli --target cli -f docker/Dockerfile .
```
#### Run Web UI
```bash
docker run -d \
--name stegasoo-web \
-p 5000:5000 \
--memory=768m \
stegasoo-web
# Visit http://localhost:5000
```
#### Run REST API
```bash
docker run -d \
--name stegasoo-api \
-p 8000:8000 \
--memory=768m \
stegasoo-api
# Docs at http://localhost:8000/docs
```
#### Run CLI
```bash
# Interactive shell
docker run -it --rm stegasoo-cli /bin/bash
# Run commands directly
docker run --rm stegasoo-cli --help
docker run --rm stegasoo-cli generate --pin --words 4
# With volume for files
docker run --rm \
-v $(pwd)/images:/data \
stegasoo-cli encode \
-r /data/ref.jpg \
-c /data/carrier.png \
-p "passphrase words here more" \
--pin 123456 \
-m "Secret message" \
-o /data/stego.png
```
### Docker Compose
The easiest way to run all services.
#### Start All Services
```bash
# Start in background
docker-compose -f docker/docker-compose.yml up -d
# Start specific service
docker-compose -f docker/docker-compose.yml up -d web
docker-compose -f docker/docker-compose.yml up -d api
# View logs
docker-compose -f docker/docker-compose.yml logs -f
# Stop all
docker-compose -f docker/docker-compose.yml down
```
#### Authentication Configuration (v4.0.2)
The Web UI supports optional authentication. Configure via environment variables:
```bash
# .env file (create in project root)
STEGASOO_AUTH_ENABLED=true # Enable login (default: true)
STEGASOO_HTTPS_ENABLED=false # Enable HTTPS (default: false)
STEGASOO_HOSTNAME=localhost # Hostname for SSL cert
STEGASOO_CHANNEL_KEY= # Optional channel key
# Then run
docker-compose -f docker/docker-compose.yml up -d web
```
On first access, you'll be prompted to create an admin account. The database and SSL certs are persisted in Docker volumes.
#### Services
| Service | URL | Description |
|---------|-----|-------------|
| `web` | http://localhost:5000 | Flask Web UI |
| `api` | http://localhost:8000 | FastAPI REST API |
#### Build and Start
```bash
# Build images and start
docker-compose -f docker/docker-compose.yml up -d --build
# Force rebuild (no cache)
docker-compose -f docker/docker-compose.yml build --no-cache
docker-compose -f docker/docker-compose.yml up -d
```
#### Resource Configuration
The `docker/docker-compose.yml` includes resource limits:
```yaml
services:
web:
deploy:
resources:
limits:
memory: 768M # For Argon2 + scipy
reservations:
memory: 384M
```
Adjust based on your available RAM:
| Available RAM | Recommended Limit | Workers |
|---------------|-------------------|---------|
| 2 GB | 768M | 2 |
| 4 GB | 1G | 3 |
| 8 GB+ | 1.5G | 4 |
---
## Optional Dependencies
### DCT Steganography (scipy + jpegio)
DCT mode enables JPEG-resilient steganography. It's automatically included with `[web]`, `[api]`, and `[all]` extras.
#### Install via pip
```bash
# scipy is straightforward
pip install scipy numpy
# jpegio - MUST use Python 3.12 or earlier!
pip install jpegio
# If pip fails, build from source
pip install cython numpy
git clone https://github.com/dwgoon/jpegio.git
cd jpegio
python setup.py install
```
#### Linux Build Dependencies
```bash
sudo apt-get install -y \
build-essential \
python3-dev \
libjpeg-dev \
cython3
```
#### macOS Build Dependencies
```bash
brew install jpeg cython
```
#### Verify DCT Support
```python
from stegasoo import has_dct_support
from stegasoo.dct_steganography import has_jpegio_support
print(f"DCT support (scipy): {has_dct_support()}")
print(f"JPEG native (jpegio): {has_jpegio_support()}")
```
Expected output:
```
DCT support (scipy): True
JPEG native (jpegio): True
```
### Compression (lz4)
Optional LZ4 compression for messages:
```bash
pip install lz4
```
---
## Platform-Specific Notes
### Linux
Most straightforward installation. Use your package manager for system dependencies.
**Ubuntu/Debian:**
```bash
sudo apt-get install python3.12 python3.12-venv python3-dev libzbar0 libjpeg-dev
python3.12 -m venv venv
source venv/bin/activate
pip install stegasoo[all]
```
**Fedora/RHEL:**
```bash
sudo dnf install python3.12 python3-devel zbar libjpeg-devel
python3.12 -m venv venv
source venv/bin/activate
pip install stegasoo[all]
```
**Arch (using pyenv):**
```bash
# Install pyenv
curl https://pyenv.run | bash
# Add to ~/.bashrc or ~/.zshrc
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
# Install Python 3.12
pyenv install 3.12
cd ~/Sources/stegasoo
pyenv local 3.12
# Create venv and install
python -m venv venv
source venv/bin/activate
pip install stegasoo[all]
```
### macOS
```bash
# Install Homebrew if needed
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install dependencies
brew install python@3.12 zbar jpeg
# Create venv
python3.12 -m venv venv
source venv/bin/activate
# Install Stegasoo
pip install stegasoo[all]
```
**Apple Silicon (M1/M2/M3):**
jpegio may need native compilation:
```bash
# Ensure you have native Python
arch -arm64 brew install python@3.12
arch -arm64 python3.12 -m venv venv
source venv/bin/activate
pip install jpegio
```
### Windows
Windows users have three options, listed from easiest to most complex:
#### Option 1: Docker Desktop (Recommended)
The easiest way to run Stegasoo on Windows. No Python installation needed.
1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
2. Enable WSL2 backend when prompted
3. Clone and run:
```powershell
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
docker-compose -f docker/docker-compose.yml up -d web
```
Access at http://localhost:5000
#### Option 2: WSL2 (Windows Subsystem for Linux)
Run the Linux version natively on Windows.
```powershell
# Install WSL2 with Ubuntu
wsl --install -d Ubuntu
# Open Ubuntu terminal, then follow Linux instructions:
sudo apt-get update
sudo apt-get install -y python3.12 python3.12-venv libzbar0 libjpeg-dev
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
python3.12 -m venv venv
source venv/bin/activate
pip install -e ".[all]"
stegasoo --version
```
#### Option 3: Native Windows (Advanced)
Native Windows installation requires Visual Studio Build Tools for compiling C extensions.
1. Install Python 3.11 or 3.12 from [python.org](https://python.org)
2. Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with "Desktop development with C++"
3. Install from pip:
```powershell
python -m venv venv
.\venv\Scripts\activate
pip install stegasoo[cli] # CLI only (easiest)
# or
pip install stegasoo[all] # Full install (may require additional setup)
```
**Note:** Native Windows installation may have issues with `jpegio` (DCT mode). Docker or WSL2 is recommended for full functionality.
### Raspberry Pi
Stegasoo works on Raspberry Pi 4/5 (4GB+ RAM recommended for Web UI).
#### Step 1: Install System Dependencies
```bash
sudo apt-get update
sudo apt-get install -y \
build-essential \
git \
libssl-dev \
zlib1g-dev \
libbz2-dev \
libreadline-dev \
libsqlite3-dev \
libncursesw5-dev \
xz-utils \
tk-dev \
libxml2-dev \
libxmlsec1-dev \
libffi-dev \
liblzma-dev \
libzbar0 \
libjpeg-dev
```
#### Step 2: Install Python 3.12 via pyenv
Raspberry Pi OS ships with Python 3.13, which is **not compatible** with jpegio. Install Python 3.12:
```bash
# Install pyenv
curl https://pyenv.run | bash
# Add to ~/.bashrc
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init - bash)"' >> ~/.bashrc
source ~/.bashrc
# Install Python 3.12 (takes ~10 minutes on Pi 5)
pyenv install 3.12
pyenv global 3.12
```
#### Step 3: Build jpegio for ARM
The upstream jpegio has x86-specific build flags. Patch and build from source:
```bash
# Clone jpegio
git clone https://github.com/dwgoon/jpegio.git
cd jpegio
# Patch for ARM (removes x86-specific -m64 flag)
sed -i "s/cargs.append('-m64')/pass # ARM fix/" setup.py
# Build and install
pip install .
cd ..
```
#### Step 4: Install Stegasoo
```bash
# Clone Stegasoo
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
# Create venv with Python 3.12
~/.pyenv/versions/3.12.*/bin/python -m venv venv
source venv/bin/activate
# Install (jpegio already installed, skip it)
pip install -e ".[web]" --no-deps
pip install argon2-cffi cryptography pillow flask gunicorn scipy numpy pyzbar qrcode
```
#### Step 5: Run the Web UI
```bash
cd frontends/web
# Optional: Enable authentication
export STEGASOO_AUTH_ENABLED=true
# Optional: Enable HTTPS for local network security
export STEGASOO_HTTPS_ENABLED=true
export STEGASOO_HOSTNAME=raspberrypi.local
# Start server
python app.py
# Access at http://<pi-ip>:5000
```
#### Verify Installation
```bash
python -c "
import stegasoo
from stegasoo.dct_steganography import has_jpegio_support
print(f'Stegasoo: {stegasoo.__version__}')
print(f'Argon2: {stegasoo.has_argon2()}')
print(f'DCT: {stegasoo.has_dct_support()}')
print(f'jpegio: {has_jpegio_support()}')
"
# Expected: All True
```
#### Notes
- **RAM**: Web UI needs ~768MB free for Argon2 + scipy operations
- **Performance**: Argon2 operations take 3-5 seconds on Pi 5 (vs ~2s on desktop)
- **Python 3.13**: Not supported due to jpegio C extension incompatibility
- **First run**: Will prompt you to create an admin account
- **HTTPS**: Generates self-signed certificate (browsers will warn)
---
## Custom SSL Certificates
By default, Stegasoo generates a self-signed certificate for HTTPS. To use your own certificate (e.g., from Let's Encrypt or your organization's CA):
### Replace Self-Signed Certificates
```bash
# Stop the service
sudo systemctl stop stegasoo
# Backup existing certs (optional)
mv /opt/stegasoo/frontends/web/certs /opt/stegasoo/frontends/web/certs.bak
# Create new certs directory
mkdir -p /opt/stegasoo/frontends/web/certs
# Copy your certificates (adjust paths as needed)
cp /path/to/your/certificate.crt /opt/stegasoo/frontends/web/certs/server.crt
cp /path/to/your/private.key /opt/stegasoo/frontends/web/certs/server.key
# Set permissions (key must be readable by service user)
chmod 600 /opt/stegasoo/frontends/web/certs/server.key
chown -R $(whoami):$(whoami) /opt/stegasoo/frontends/web/certs
# Start the service
sudo systemctl start stegasoo
```
### Generate New Self-Signed Certificate
If your certificate expires or you need to regenerate:
```bash
# Stop service
sudo systemctl stop stegasoo
# Generate new cert with SANs
CERT_DIR="/opt/stegasoo/frontends/web/certs"
LOCAL_IP=$(hostname -I | awk '{print $1}')
HOSTNAME=$(hostname)
openssl req -x509 -newkey rsa:2048 \
-keyout "$CERT_DIR/server.key" \
-out "$CERT_DIR/server.crt" \
-days 365 -nodes \
-subj "/O=Stegasoo/CN=$HOSTNAME" \
-addext "subjectAltName=DNS:$HOSTNAME,DNS:$HOSTNAME.local,DNS:localhost,IP:$LOCAL_IP,IP:127.0.0.1"
chmod 600 "$CERT_DIR/server.key"
# Start service
sudo systemctl start stegasoo
```
### Let's Encrypt with Certbot
For publicly accessible servers:
```bash
# Install certbot
sudo apt install certbot
# Get certificate (standalone mode)
sudo certbot certonly --standalone -d yourdomain.com
# Copy to Stegasoo
sudo cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /opt/stegasoo/frontends/web/certs/server.crt
sudo cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /opt/stegasoo/frontends/web/certs/server.key
sudo chown $(whoami):$(whoami) /opt/stegasoo/frontends/web/certs/*
sudo chmod 600 /opt/stegasoo/frontends/web/certs/server.key
# Restart
sudo systemctl restart stegasoo
```
**Note:** Set up a cron job or systemd timer to copy renewed certificates and restart Stegasoo.
---
## Verification
### Check Installation
```bash
# CLI version
stegasoo --version
# Python import
python -c "import stegasoo; print(stegasoo.__version__)"
# Check Python version (must be 3.10-3.12)
python -V
```
### Check All Features
```python
#!/usr/bin/env python3
"""Verify Stegasoo installation."""
import sys
def check_feature(name, check_fn):
try:
result = check_fn()
status = "" if result else ""
print(f" {status} {name}: {result}")
return result
except Exception as e:
print(f"{name}: Error - {e}")
return False
print("Stegasoo Installation Check")
print("=" * 40)
# Python version check
py_version = sys.version_info
print(f"\nPython: {py_version.major}.{py_version.minor}.{py_version.micro}")
if py_version >= (3, 13):
print(" ⚠️ WARNING: Python 3.13+ not supported!")
print(" jpegio will not work. Use Python 3.12.")
elif py_version >= (3, 10):
print(" ✓ Python version OK")
else:
print(" ✗ Python 3.10+ required")
# Core
import stegasoo
print(f"\nStegasoo Version: {stegasoo.__version__}")
print("\nCore Features:")
check_feature("Argon2", lambda: stegasoo.has_argon2())
check_feature("Pillow", lambda: True) # Required, would fail import
print("\nOptional Features:")
check_feature("DCT (scipy)", stegasoo.has_dct_support)
try:
from stegasoo.dct_steganography import has_jpegio_support
check_feature("JPEG native (jpegio)", has_jpegio_support)
except ImportError:
print(" ✗ JPEG native (jpegio): Not installed")
try:
import lz4
check_feature("Compression (lz4)", lambda: True)
except ImportError:
print(" - Compression (lz4): Not installed (optional)")
try:
import pyzbar
check_feature("QR codes (pyzbar)", lambda: True)
except ImportError:
print(" - QR codes (pyzbar): Not installed (optional)")
print("\nInterfaces:")
try:
import click
check_feature("CLI", lambda: True)
except ImportError:
print(" ✗ CLI: Not installed")
try:
import flask
check_feature("Web UI", lambda: True)
except ImportError:
print(" - Web UI: Not installed")
try:
import fastapi
check_feature("REST API", lambda: True)
except ImportError:
print(" - REST API: Not installed")
print("\n" + "=" * 40)
print("Installation check complete!")
```
Save as `check_install.py` and run:
```bash
python check_install.py
```
### Test Encoding/Decoding
```bash
# Quick test with CLI
stegasoo generate --pin --words 4 --json > /tmp/creds.json
# Create test image
python -c "
from PIL import Image
img = Image.new('RGB', (256, 256), 'blue')
img.save('/tmp/test_carrier.png')
img.save('/tmp/test_ref.jpg')
"
# Encode
stegasoo encode \
-r /tmp/test_ref.jpg \
-c /tmp/test_carrier.png \
-p "test phrase words here" \
--pin 123456 \
-m "Hello, Stegasoo!" \
-o /tmp/test_stego.png
# Decode
stegasoo decode \
-r /tmp/test_ref.jpg \
-s /tmp/test_stego.png \
-p "test phrase words here" \
--pin 123456
```
---
## Troubleshooting
### Common Issues
#### "jpegio crashes" / "free(): invalid size" / Core dump
**This is the #1 issue!** You're using Python 3.13.
```bash
# Check your Python version
python -V
# If it shows 3.13, you need to use 3.12
# Option 1: Use pyenv
pyenv install 3.12
pyenv local 3.12
# Option 2: Use system Python 3.12
python3.12 -m venv venv
source venv/bin/activate
pip install -e ".[all]"
```
#### "No module named 'stegasoo'"
```bash
# Ensure you're in the right environment
which python
pip list | grep stegasoo
# Reinstall
pip install -e ".[all]"
```
#### "Argon2 not available"
```bash
# Install argon2-cffi
pip install argon2-cffi
# On Linux, may need:
sudo apt-get install libffi-dev
pip install --force-reinstall argon2-cffi
```
#### "jpegio not available" (not crash, just missing)
```bash
# Install build dependencies first
sudo apt-get install libjpeg-dev # Linux
brew install jpeg # macOS
# Then install jpegio
pip install cython numpy
pip install jpegio
# If still fails, build from source
git clone https://github.com/dwgoon/jpegio.git
cd jpegio
python setup.py install
```
#### "libzbar not found" (QR codes)
```bash
# Linux
sudo apt-get install libzbar0
# macOS
brew install zbar
# Then reinstall pyzbar
pip install --force-reinstall pyzbar
```
#### Docker: "Cannot allocate memory"
Argon2 needs 256MB per operation. Increase container memory:
```bash
# Docker run
docker run --memory=768m ...
# Docker Compose - edit docker/docker-compose.yml
deploy:
resources:
limits:
memory: 768M
```
#### Slow performance
- **Argon2 is intentionally slow** - This is a security feature
- Expected encode/decode time: 2-5 seconds
- DCT mode adds ~1-2 seconds for transforms
- Large images (10MB+) may take 15-30 seconds
#### "Carrier image too small"
- LSB needs ~3 bits per pixel
- DCT needs ~0.25 bits per pixel
- For 50KB message: LSB needs ~136K pixels, DCT needs ~1.6M pixels
- Use larger carrier images or shorter messages
### Getting Help
1. Check the documentation:
- [README.md](README.md)
- [CLI.md](CLI.md)
- [API.md](API.md)
- [WEB_UI.md](WEB_UI.md)
2. Check existing issues on GitHub
3. Open a new issue with:
- Python version (`python --version`)
- OS and version
- Installation method
- Full error message
- Steps to reproduce
---
## Next Steps
After installation:
1. **Generate credentials**: `stegasoo generate --pin --words 4`
2. **Read the CLI docs**: [CLI.md](CLI.md)
3. **Try the Web UI**: `cd frontends/web && python app.py`
4. **Explore the API**: `cd frontends/api && python main.py`
Happy steganography! 🦕

View File

@@ -0,0 +1,294 @@
# Stegasoo Ideas Scout — Implementation Plans (2026-03-24)
Baseline: v4.3.0, Python >=3.11, FORMAT_VERSION 5, no existing users (no backward compat constraints).
---
## Tier 1 — Quick Wins
### 1. Platform-Calibrated DCT Presets
**Description**: `--platform telegram|discord|signal|whatsapp` flag for DCT encode. Bakes in each platform's known recompression parameters. Pre-verifies payload survives before outputting.
**Implementation approach**:
- New file `src/stegasoo/platform_presets.py``PlatformPreset` dataclass + `PRESETS` dict mapping platform → tuned `quant_step`, `jpeg_quality`, `embed_positions`, `max_dimension`, `recompress_quality`
- `dct_steganography.py`: `_embed_scipy_dct_safe()` / `_embed_jpegio()` accept optional preset overrides for `QUANT_STEP`, `DEFAULT_EMBED_POSITIONS`, output quality
- New `pre_verify_survival()` function: encode → re-save at platform quality → extract → pass/fail
- Thread `platform` param through `encode.py``steganography.py` → DCT functions
- `cli.py`: add `--platform` as `click.Choice` + `--verify/--no-verify` (pre-verification doubles encode time)
- LSB + `--platform` should error early — LSB data is destroyed by any JPEG recompression
**Known platform params** (from research):
| Platform | Quality | Max Dimension | Notes |
|----------|---------|---------------|-------|
| Telegram | ~82 | 2560×2560 | ~81KB embeddable |
| Discord | ~85 | Varies (Nitro) | |
| Signal | ~80 | Aggressive | |
| WhatsApp | ~70 | 1600×1600 | Most lossy |
**Go/No-Go metrics**:
- >95% payload survival rate per platform at 1KB message size in automated tests
- Pre-verification correctly predicts real platform behavior (manual validation per platform at least once)
**Complexity**: **M** — new file + parameter threading through 4-5 functions
**Risks**: Platform params change without notice. Add version/date stamps to presets and a `stegasoo tools verify-platform` test command.
---
### 2. Steganalysis Self-Check (`stegasoo check`)
**Description**: New CLI command running chi-square and RS (Regular-Singular) statistical analysis on stego images. Outputs detectability risk level (low/medium/high).
**Implementation approach**:
- New file `src/stegasoo/steganalysis.py`:
- `chi_square_analysis(image_data) -> float` — chi-square statistic on LSB distribution per channel
- `rs_analysis(image_data) -> float` — Regular-Singular groups analysis (requires numpy)
- `assess_risk(chi_p, rs_estimate) -> str` — maps to "low"/"medium"/"high"
- `check_image(image_data) -> dict` — orchestrator
- `cli.py`: new `@cli.command("check")` with `IMAGE` arg, `--json`, `--mode lsb|dct|auto`
- `constants.py`: threshold constants for chi-square p-value and RS boundaries
- `__init__.py`: export `check_image` in `__all__`
- Start LSB-only; DCT steganalysis (calibration attack) deferred
**Go/No-Go metrics**:
- Clean images → consistently "low risk"
- Naive sequential LSB → "high risk"
- Stegasoo LSB at <50% capacity → "low" or "medium"
**Complexity**: **M** — ~150 lines numpy per test, straightforward CLI integration
---
### 3. Python 3.13 DCT Cleanup
**Description**: The `jpegio``jpeglib` migration is already done in code. Remaining work: rename stale `jpegio` references and verify on 3.13.
**Implementation approach**:
- `dct_steganography.py`: rename `HAS_JPEGIO``HAS_JPEGLIB`, `_jpegio_*` functions → `_jpeglib_*`, update constant names (`JPEGIO_MAGIC``JPEGLIB_MAGIC`, etc.)
- Verify `jpeglib.to_jpegio()` compatibility shim — if jpeglib plans to deprecate it, migrate to native API
- Run full test suite on Python 3.13
**Go/No-Go metrics**:
- All DCT tests pass on Python 3.13
- No deprecation warnings from jpeglib
**Complexity**: **S** — renaming and verification only
---
## Tier 2 — Strategic
### 4. Content-Adaptive Embedding (S-UNIWARD/WOW-inspired)
**Description**: Replace uniform-random pixel selection with texture-weighted cost functions. Embed preferentially in busy/textured regions where changes are least detectable. 3-5x harder to detect statistically.
**Implementation approach**:
- New file `src/stegasoo/adaptive_cost.py`:
- `compute_cost_map(image_data) -> np.ndarray` — per-pixel distortion cost via directional high-pass filters (Daubechets wavelet bank / KB filter)
- `select_pixels_by_cost(cost_map, pixel_key, num_needed) -> list[int]` — weighted sampling, still ChaCha20-seeded for determinism
- `steganography.py`:
- `generate_pixel_indices()`: add `cost_map` param, use weighted sampling when provided
- `_embed_lsb()`: compute cost map when adaptive mode enabled
- `_extract_lsb()`: must compute identical cost map to find same pixels
- `dct_steganography.py`: adapt `DEFAULT_EMBED_POSITIONS` per-block based on block texture energy
- Thread `adaptive: bool` through `encode.py`/`decode.py`
- `constants.py`: add `EMBED_MODE_ADAPTIVE_LSB`, filter kernels, cost thresholds
**Go/No-Go metrics**:
- Chi-square test (Feature 2) shows measurable improvement vs uniform-random
- **Critical**: cost map computation is deterministic across platforms (quantize to fixed-point integers)
- Round-trip decode succeeds on Linux x86, Linux ARM, macOS
**Complexity**: **L** — novel algorithm, cross-platform determinism requirement, touches core embedding
**Risks**: Floating-point differences in wavelet computation could break extraction. Mitigate with integer quantization. Increases encode/decode time ~2-3x.
---
### 5. Per-Message Forward Secrecy via HKDF
**Description**: Derive ephemeral per-message encryption keys using HKDF expansion from the Argon2id root key + random nonce. Compromising one message doesn't reveal others.
**Implementation approach**:
- `crypto.py`:
- Add `from cryptography.hazmat.primitives.kdf.hkdf import HKDFExpand`
- `derive_message_key(root_key, nonce) -> bytes` — HKDF-Expand with SHA-256
- `encrypt_message()`: generate 16-byte random nonce, derive per-message key, embed nonce in header
- `decrypt_message()`: extract nonce, derive same key
- Also derive pixel selection key via HKDF with different `info` param
- `constants.py`:
- Bump `FORMAT_VERSION` to 6
- `HKDF_INFO_ENCRYPTION = b"stegasoo-v6-encrypt"`, `HKDF_INFO_PIXEL = b"stegasoo-v6-pixel"`
- `MESSAGE_NONCE_SIZE = 16`
- Header grows from 66 → 82 bytes: add `message_nonce(16)` field
- Update `HEADER_OVERHEAD` / `ENCRYPTION_OVERHEAD` in `steganography.py`
**Go/No-Go metrics**:
- Two messages with identical credentials produce different ciphertexts and different pixel locations
- `cryptography` library HKDF works with existing Argon2id output
**Complexity**: **M** — well-defined crypto change, touches security-critical header format
---
### 6. PWA Mobile Interface
**Description**: Convert Flask Web UI to Progressive Web App. Mobile-optimized, installable, offline-capable static pages.
**Implementation approach**:
- New files in `frontends/web/static/`: `manifest.json`, `sw.js`, icon set (192×192, 512×512)
- Base template: add manifest link, theme-color meta, viewport meta, service worker registration
- `app.py`: serve manifest with correct MIME, add cache headers for static assets
- Responsive CSS for encode/decode accordion forms
- Camera capture: `<input type="file" accept="image/*" capture="environment">` for reference photo
- Service worker caches static assets only — NOT encode/decode API endpoints
**Go/No-Go metrics**:
- Lighthouse PWA score >= 90
- Installable on Android Chrome and iOS Safari
- Offline: static pages load, encode/decode shows graceful "offline" message
**Complexity**: **M** — frontend only, no core library changes
**Risks**: Camera capture requires HTTPS (already supported via `ssl_utils.py`).
---
## Tier 3 — Moonshot
### 7. Plausible Deniability / Dual-Payload Mode
**Description**: Two independent encrypted payloads in one carrier, each with different credentials. Reveal decoy under coercion; real payload stays hidden.
**Implementation approach**:
- New file `src/stegasoo/dual_payload.py`:
- `encode_dual(message_a, message_b, carrier, creds_a, creds_b)`
- Partition available pixels into two disjoint pools using different seeds
- **Critical**: ALL images (single or dual) must fill unused pixel pool with random data so single-payload and dual-payload images are indistinguishable
- `steganography.py`: `generate_pixel_indices()` gets `exclude_indices` param
- `decode.py`: each credential set finds a different valid payload; wrong credentials produce garbage
- CLI + Web UI: dual-payload encode workflow
**Go/No-Go metrics**:
- Single-payload and dual-payload images are statistically indistinguishable (chi-square can't differentiate)
- Each payload decodes independently
- Wrong credentials for one payload don't reveal other payload's existence
**Complexity**: **XL** — novel design, halves capacity per payload, challenging UX, needs rigorous security analysis
**Dependencies**: Feature 2 (validation), Feature 4 (detectability reduction)
---
## Architectural Improvements
### 8. EmbeddingBackend Protocol
**Description**: Typed plugin interface for all embedding algorithms. Replace if/elif dispatch in `steganography.py` with a registry.
**Implementation approach**:
- New package `src/stegasoo/backends/`:
- `protocol.py``EmbeddingBackend(Protocol)` with `embed()`, `extract()`, `calculate_capacity()`, `is_available()`
- `lsb.py`, `dct.py` — wrap existing functions
- `registry.py``BackendRegistry` mapping mode strings to backends
- `steganography.py`: `embed_in_image()` / `extract_from_image()` dispatch via registry
- `__init__.py`: export protocol and `register_backend()`
**Complexity**: **M** — implement before Features 4 and 7 (they become new backends)
---
### 9. HKDF Key Separation
Subsumed by Feature 5. The HKDF expansion provides:
- Encryption key: `HKDF-Expand(root_key, info="stegasoo-encrypt", nonce)`
- Pixel selection key: `HKDF-Expand(root_key, info="stegasoo-pixel", nonce)`
- Future: MAC key, padding key, etc.
---
### 10. `[core]` Extra with Minimal Deps
**Description**: Move Pillow to `[image]` extra, base deps = `cryptography` + `argon2-cffi` + `zstandard` only.
**Complexity**: **S** — but Pillow is used in `crypto.py` for photo hashing (core to security model). Only worth it with a concrete headless use case. **Low priority.**
---
## Ecosystem Features
### 11. Aletheia Integration
Optional `--engine aletheia` backend for Feature 2's `stegasoo check`. BSD-licensed, provides SPA/RS/WS attacks + ML classifiers. **Complexity: S** (after Feature 2). **Depends on**: Feature 2.
### 12. C2PA/AI Provenance Watermarking
Embed C2PA metadata alongside stego payloads. **Complexity: L** — C2PA is a complex standard. Potentially conflicts with stego goals (adds detectable metadata). Research-heavy.
### 13. Signal/Matrix Bot
Bot that decodes stego images in a channel using configured channel key. **Complexity: M** — integration work, uses existing `decode()` API.
### 14. Homebrew Tap + Nix Flake
Package distribution for macOS/NixOS. **Complexity: S** — packaging only, no code changes.
---
## Summary Table
| # | Feature | Tier | Size | Dependencies | Primary Files |
|---|---------|------|------|-------------|---------------|
| 1 | Platform DCT Presets | T1 | M | — | new `platform_presets.py`, `dct_steganography.py`, `encode.py`, `cli.py` |
| 2 | Steganalysis Self-Check | T1 | M | — | new `steganalysis.py`, `cli.py`, `constants.py` |
| 3 | Python 3.13 DCT Cleanup | T1 | S | — | `dct_steganography.py` |
| 4 | Content-Adaptive Embedding | T2 | L | numpy, #2 | new `adaptive_cost.py`, `steganography.py`, `constants.py` |
| 5 | HKDF Forward Secrecy | T2 | M | — | `crypto.py`, `constants.py`, `steganography.py` |
| 6 | PWA Mobile Interface | T2 | M | — | `frontends/web/` templates + static |
| 7 | Dual-Payload Mode | T3 | XL | #2, #4 | new `dual_payload.py`, `steganography.py`, `cli.py` |
| 8 | EmbeddingBackend Protocol | Arch | M | — | new `backends/` package, `steganography.py` |
| 9 | HKDF Key Separation | Arch | — | Included in #5 | `crypto.py` |
| 10 | `[core]` Extra | Arch | S | — | `pyproject.toml` |
| 11 | Aletheia Integration | Eco | S | #2 | `steganalysis.py` |
| 12 | C2PA Watermarking | Eco | L | — | new module |
| 13 | Signal/Matrix Bot | Eco | M | — | new `bots/` package |
| 14 | Homebrew + Nix | Eco | S | — | packaging files only |
---
## Suggested Roadmap
### Phase 1 — Foundations (v4.4.0)
1. **#3** Python 3.13 DCT Cleanup (S) — unblocks CI on 3.13
2. **#8** EmbeddingBackend Protocol (M) — architectural cleanup before new embedding work
3. **#2** Steganalysis Self-Check (M) — validation tooling for everything that follows
### Phase 2 — Security & Robustness (v4.5.0)
4. **#5** HKDF Forward Secrecy (M) — FORMAT_VERSION bump to 6, improved crypto
5. **#1** Platform-Calibrated DCT Presets (M) — high user value for social media
6. **#14** Homebrew + Nix (S) — distribution expansion
### Phase 3 — Advanced Steganography (v5.0.0)
7. **#4** Content-Adaptive Embedding (L) — major security improvement
8. **#6** PWA Mobile Interface (M) — parallel frontend work stream
### Phase 4 — Moonshot (v5.x+)
9. **#7** Dual-Payload Mode (XL) — after #2 and #4 are solid
10. **#12** C2PA Watermarking (L) — research-heavy
11. **#13** Signal/Matrix Bot (M) — community-driven
---
## Additional Ideas (Backlog)
- **Animated GIF steganography** — LSB in GIF frames, natural multi-media extension
- **PDF steganography** — whitespace/font metric/embedded image payloads
- **Batch encode** — `stegasoo batch-encode --dir /photos/` with auto carrier selection (BATCH_* constants suggest this was planned)
- **Stego identification** — `stegasoo identify image.png` probes for known stego signatures
- **Per-device credential sync via QR** — channel key as stego image of reference photo
- **`stegasoo verify`** — decode + confirm message matches expected hash without revealing contents

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024-2025 Aaron D. Lee
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

97
PLAN-4.1.6.md Normal file
View File

@@ -0,0 +1,97 @@
# Stegasoo v4.1.6 Planning
## UI Tweaks
### 1. Revamp Tron Lines Animation (Carrier/Stego Image)
**Current state:**
- 6-8 snake paths, each with 3-5 segments (~24-40 total lines)
- 2px thick lines
- 30-60px length per segment
- Starting points spread across 80% of image area
- Colors: yellow, cyan, purple, blue with glow
**Target improvements:**
- [x] Thinner lines (1px instead of 2px)
- [x] More numerous (20-40 paths via 5x4 grid, ~60-200 segments total)
- [x] Better distribution across entire image (grid-based seeding)
- [x] Shorter segments (12-30px) for denser "circuit board" look
**Files:**
- `frontends/web/static/style.css` (~881-979) - `.embed-trace` styling
- `frontends/web/static/js/stegasoo.js` (~333-390) - `generateEmbedTraces()`
---
## Tools Page Expansion
### Analysis Tools
- [x] **JPEG Compression Tester** - Preview image at different quality levels (10-100%), show file size delta. Useful for understanding stego survivability.
- [ ] **LSB Plane Viewer** - Visualize least significant bit plane(s) of RGB channels. Classic stego analysis tool.
- [ ] **Histogram Viewer** - Color distribution graph per channel. Anomalies can indicate hidden data.
- [ ] **Image Diff** - Compare two images side-by-side with pixel difference highlighting. Great for original vs stego comparison.
- [ ] **Noise Analysis** - Chi-square or similar statistical analysis for detecting LSB embedding.
### Transform Tools
- [x] **Rotate/Flip** - 90°/180°/270° rotation, horizontal/vertical flip
- [ ] **Resize** - Scale with aspect ratio lock, common presets (50%, 25%, etc.)
- [ ] **Crop** - Basic rectangular crop with preview
- [x] **Format Convert** - PNG ↔ JPEG ↔ WebP with quality slider
### Existing Tools (already done)
- [x] Capacity Calculator
- [x] EXIF Viewer
- [x] EXIF Strip
- [x] Image Peek (header analysis)
### Tools UI/UX Overhaul
**Final Layout: Office-style Ribbon + Two-Panel**
```
┌─────────────────────────────────────────────────────────────┐
│ 📏 📋 👁️ 📊 ┃ ✂️ 🔄 📐 🔀 Image Tools │ ← Icon toolbar
├────────────────────────────────────────┬────────────────────┤
│ [Format: PNG ▼] [Quality: 85] │ │
├────────────────────────────────────────┤ Capacity │
│ │ Calculator │
│ ┌────────────────────────────┐ │ ────────────── │
│ │ │ │ │
│ │ Drop image here │ │ Dimensions: │
│ │ or click │ │ 1920 × 1080 │
│ │ │ │ │
│ └────────────────────────────┘ │ LSB Capacity: │
│ │ 245 KB │
│ [image.jpg] │ │
│ │ ────────────── │
│ │ [Clear] [Export] │
└────────────────────────────────────────┴────────────────────┘
Options + dropzone/preview Results sidebar
```
- Top ribbon: Icon buttons grouped by category (Analyze | Transform)
- Left panel: Tool options + dropzone/preview (INPUT)
- Right panel: Tool name + results/metadata + actions (OUTPUT)
- Flow: Left → Right (input → output)
**Implementation Tasks:**
- [x] Move inline CSS to style.css
- [x] Build icon toolbar ribbon
- [x] Build two-panel layout structure
- [x] Migrate existing tools (Capacity, EXIF, Strip)
- [x] Add new tools (Rotate, Compress, Convert)
- [ ] Loading spinner on all async operations
- [ ] Toast notifications instead of alerts
- [ ] Consistent color coding (green=analysis, amber=transform)
- [ ] Mobile: stack panels vertically
---
## CLI Improvements
### (Add items here)
---
## Other UI Tweaks
### (Add items here)

295
README.md
View File

@@ -1,226 +1,167 @@
# Stegasoo
A secure steganography system for hiding encrypted messages in images using hybrid authentication.
A secure steganography system for hiding encrypted messages in images and audio using hybrid authentication.
![Python](https://img.shields.io/badge/Python-3.10+-blue)
![License](https://img.shields.io/badge/License-MIT-green)
[![Tests](https://github.com/adlee-was-taken/stegasoo/actions/workflows/test.yml/badge.svg)](https://github.com/adlee-was-taken/stegasoo/actions/workflows/test.yml)
[![Lint](https://github.com/adlee-was-taken/stegasoo/actions/workflows/lint.yml/badge.svg)](https://github.com/adlee-was-taken/stegasoo/actions/workflows/lint.yml)
![Python](https://img.shields.io/badge/Python-3.11--3.14-blue)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
![Security](https://img.shields.io/badge/Security-AES--256--GCM-red)
## Features
- 🔐 **AES-256-GCM** authenticated encryption
- 🧠 **Argon2id** memory-hard key derivation (256MB RAM requirement)
- 🎲 **Pseudo-random pixel selection** defeats steganalysis
- 📅 **Daily key rotation** with BIP-39 passphrases
- 🔑 **Multi-factor authentication**: PIN, RSA key, or both
- 🖼️ **Reference photo** as "something you have"
- 🌐 **Multiple interfaces**: CLI, Web UI, REST API
- 📁 **File embedding** - Hide any file type (PDF, ZIP, documents)
- 📱 **QR code support** - Encode/decode RSA keys via QR codes
- **AES-256-GCM** authenticated encryption
- **Argon2id** memory-hard key derivation (256MB RAM requirement)
- **Pseudo-random pixel selection** defeats steganalysis
- **Multi-factor authentication**: Reference photo + passphrase + PIN/RSA key
- **Multiple interfaces**: CLI, Web UI, REST API
- **File embedding**: Hide any file type (PDF, ZIP, documents)
- **DCT steganography**: JPEG-resilient embedding for social media
- **Audio steganography**: Hide messages in WAV, FLAC, MP3, OGG, AAC, M4A files (LSB and Spread Spectrum modes)
- **Channel keys**: Private group communication channels
## Embedding Modes
## WebUI Preview
### Image Modes
Front Page | Encode | Decode | Generate |
:-------------------------:|:-------------------------:|:------------------------:|:--------:|
![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Encode.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Decode.webp) | ![Screenshot](https://github.com/adlee-was-taken/stegasoo/blob/main/data/WebUI_Generate.webp)
| Mode | Capacity (1080p) | JPEG Resilient | Best For |
|------|------------------|----------------|----------|
| **DCT** (default) | ~150 KB | Yes | Social media, messaging apps |
| **LSB** | ~750 KB | No | Email, direct file transfer |
### Audio Modes
## Installation
| Mode | Capacity (5 min WAV) | Noise Resistant | Best For |
|------|---------------------|-----------------|----------|
| **LSB** | ~1.3 MB | No | Direct file transfer |
| **Spread Spectrum** | ~160 KB | Yes | Shared files, light processing |
### From Source
## Web UI
| Home | Encode | Decode | Generate |
|:----:|:------:|:------:|:--------:|
| ![Home](data/WebUI.webp) | ![Encode](data/WebUI_Encode.webp) | ![Decode](data/WebUI_Decode.webp) | ![Generate](data/WebUI_Generate.webp) |
## Quick Start
```bash
# Clone the repository
git clone https://github.com/adlee-was-taken/stegasoo.git
cd stegasoo
# Install core library
pip install -e .
# Install with CLI
pip install -e ".[cli]"
# Install with Web UI
pip install -e ".[web]"
# Install with REST API
pip install -e ".[api]"
# Install everything
# Install (Python 3.10-3.12)
pip install -e ".[all]"
```
### CLI Usage
```bash
# Generate credentials
stegasoo generate --pin --words 3
stegasoo generate --pin --words 4
# With RSA key
stegasoo generate --rsa --rsa-bits 4096 -o mykey.pem -p "secretpassword"
# Encode
# Encode a message
stegasoo encode \
--ref photo.jpg \
--carrier meme.png \
--phrase "apple forest thunder" \
--ref my_photo.jpg \
--carrier meme.jpg \
--passphrase "apple forest thunder mountain" \
--pin 123456 \
--message "Secret message"
# Decode
stegasoo decode \
--ref photo.jpg \
--stego stego.png \
--phrase "apple forest thunder" \
--ref my_photo.jpg \
--stego stego_image.png \
--passphrase "apple forest thunder mountain" \
--pin 123456
# Pipe-friendly
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 > stego.png
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q
```
### Web UI
## Interfaces
```bash
# Development
cd frontends/web
python app.py
# Production
gunicorn --bind 0.0.0.0:5000 app:app
```
Visit http://localhost:5000
### REST API
```bash
# Development
cd frontends/api
python main.py
# Production
uvicorn main:app --host 0.0.0.0 --port 8000
```
API docs at http://localhost:8000/docs
#### Example API Calls
```bash
# Generate credentials
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": true, "use_rsa": false}'
# Encode (multipart)
curl -X POST http://localhost:8000/encode/multipart \
-F "message=Secret" \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "reference_photo=@photo.jpg" \
-F "carrier=@meme.png" \
--output stego.png
# Decode (multipart)
curl -X POST http://localhost:8000/decode/multipart \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "reference_photo=@photo.jpg" \
-F "stego_image=@stego.png"
```
| Interface | Start Command | Documentation |
|-----------|---------------|---------------|
| **CLI** | `stegasoo --help` | [CLI.md](CLI.md) |
| **Web UI** | `cd frontends/web && python app.py` | [WEB_UI.md](WEB_UI.md) |
| **REST API** | `cd frontends/api && uvicorn main:app` | [API.md](API.md) |
## Security Model
| Component | Entropy | Purpose |
|-----------|---------|---------|
| Reference Photo | ~80-256 bits | Something you have |
| Day Phrase (3-12 words) | ~33-100+ bits | Something you know (rotates daily) |
| PIN (6-9 digits) | ~20+ bits | Something you know (static) |
| RSA Key (2048-bit) | ~128 bits | Something you have |
| **Combined** | **~133-400+ bits** | **Beyond brute force** |
### Attack Resistance
| Attack | Protection |
|--------|------------|
| Brute force | 2^133+ combinations |
| Rainbow tables | Random salt per message |
| Steganalysis | Random pixel selection |
| GPU cracking | Argon2id requires 256MB RAM per attempt |
| Side-channel | Constant-time operations in crypto |
## Project Structure
```
stegasoo/
├── src/stegasoo/ # Core library
├── __init__.py # Public API
│ ├── constants.py # Configuration
│ ├── crypto.py # Encryption/decryption
├── steganography.py # Image embedding
│ ├── keygen.py # Credential generation
│ ├── validation.py # Input validation
├── models.py # Data classes
│ ├── exceptions.py # Custom exceptions
│ └── utils.py # Utilities
├── frontends/
│ ├── web/ # Flask web UI
│ ├── cli/ # Command-line interface
│ └── api/ # FastAPI REST API
├── data/
│ └── bip39-words.txt # BIP-39 wordlist
├── pyproject.toml # Package configuration
├── Dockerfile # Multi-stage Docker build
└── docker-compose.yml # Container orchestration
Reference Photo ──┐
(~80-256 bits) │
├──► Argon2id KDF ──► AES-256-GCM Key
Passphrase ───────┤ (256MB RAM)
(~43-132 bits) │
PIN ──────────────┤
(~20-30 bits) │
RSA Key ──────────┘
(optional)
```
## Configuration
| Configuration | Entropy | Use Case |
|--------------|---------|----------|
| 4-word passphrase + 6-digit PIN | ~153 bits | Standard security |
| 4-word passphrase + PIN + RSA | ~280+ bits | Maximum security |
### Environment Variables
## Requirements
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASK_ENV` | production | Flask environment |
| `PYTHONPATH` | - | Include src/ for development |
### Limits
| Limit | Value |
|-------|-------|
| Max image size | 4 megapixels |
| Max message size | 50 KB |
| Max file upload | 5 MB |
| PIN length | 6-9 digits |
| Phrase length | 3-12 words |
| RSA key sizes | 2048, 3072, 4096 bits |
| Requirement | Version |
|-------------|---------|
| Python | 3.10-3.12 |
| RAM | 512 MB+ |
## Development
```bash
# Install dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Format code
black src/ frontends/
ruff check src/ frontends/
# Type checking
mypy src/
black src/ tests/ frontends/
ruff check src/ tests/ frontends/
```
## Docker
```bash
# Quick start (HTTPS enabled by default)
docker-compose -f docker/docker-compose.yml up -d
# Access
# Web UI: https://localhost:5000 (self-signed cert)
# REST API: http://localhost:8000
# Disable HTTPS if needed:
STEGASOO_HTTPS_ENABLED=false docker-compose -f docker/docker-compose.yml up -d
```
See [DOCKER.md](DOCKER.md) and [docs/DOCKER_QUICKSTART.md](docs/DOCKER_QUICKSTART.md) for full documentation.
## Raspberry Pi
Pre-built SD card images available for Pi 4/5:
```bash
# Flash image (download from GitHub Releases)
zstdcat stegasoo-rpi-*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress
# First boot runs interactive setup wizard:
# - WiFi configuration
# - HTTPS with port 443
# - Channel key generation
# - Optional overclocking
```
See [rpi/README.md](rpi/README.md) for manual installation.
## Documentation
- [INSTALL.md](INSTALL.md) - Installation guide
- [DOCKER.md](DOCKER.md) - Docker deployment
- [CLI.md](CLI.md) - Command-line reference
- [API.md](API.md) - REST API documentation
- [WEB_UI.md](WEB_UI.md) - Web interface guide
- [SECURITY.md](SECURITY.md) - Security model details
- [UNDER_THE_HOOD.md](UNDER_THE_HOOD.md) - Technical deep-dive
- [CHANGELOG.md](CHANGELOG.md) - Version history
- [CONTRIBUTING.md](CONTRIBUTING.md) - Contributor guide
- `man stegasoo` - Man page (install: `sudo cp docs/stegasoo.1 /usr/local/share/man/man1/ && sudo mandb`)
## License
MIT License - Use responsibly.
MIT License - see [LICENSE](LICENSE). Use responsibly.
## ⚠️ Disclaimer
This tool is for educational and legitimate privacy purposes only. Users are responsible for complying with applicable laws in their jurisdiction.
---
*This tool is for educational and legitimate privacy purposes. Users are responsible for complying with applicable laws.*

44
RELEASE_CHECKLIST.md Normal file
View File

@@ -0,0 +1,44 @@
# Stegasoo Release Checklist
Pre-release validation checklist. Complete all items before tagging a release.
## Code Quality
- [ ] All tests pass: `./venv/bin/pytest tests/ -v`
- [ ] No lint errors: `./venv/bin/ruff check src/`
- [ ] Version bumped in `pyproject.toml`
- [ ] CHANGELOG.md updated
## Pi Image Validation
- [ ] Fresh Pi OS install with setup.sh works
- [ ] First-boot wizard completes successfully
- [ ] MOTD shows correct URL on SSH login
- [ ] Smoke test passes: `./rpi/smoke-test.sh --443 <PI_IP>`
- [ ] Encode/decode works on large image (10MB+)
- [ ] Sanitize script runs cleanly
- [ ] Image created and compressed
## Docker Validation
- [ ] Base image builds: `docker build -f docker/Dockerfile.base -t stegasoo-base:latest .`
- [ ] Web image builds: `docker-compose -f docker/docker-compose.yml build web`
- [ ] Container starts: `docker-compose -f docker/docker-compose.yml up -d web`
- [ ] Web UI accessible at http://localhost:5000
- [ ] Encode/decode works in container
- [ ] Container stops cleanly: `docker-compose -f docker/docker-compose.yml down`
## Release Process
- [ ] Merge feature branch to main
- [ ] Create annotated tag: `git tag -a vX.Y.Z -m "message"`
- [ ] Push tag: `git push origin vX.Y.Z`
- [ ] Create GitHub Release with release notes
- [ ] Upload Pi image (.img.zst.zip)
- [ ] Verify download links work
## Post-Release
- [ ] Delete old/obsolete releases if needed
- [ ] Update any external documentation
- [ ] Announce release (if applicable)

173
RELEASE_NOTES.md Normal file
View File

@@ -0,0 +1,173 @@
# v4.3.0 — Audio Steganography
**Release Date:** 2026-02-27
## Highlights
Stegasoo can now hide messages in audio files! This release adds full audio steganography support with two embedding modes:
- **LSB (Least Significant Bit)**: Embeds data directly in audio sample LSBs. High capacity, best for direct file transfers.
- **Spread Spectrum**: Spreads data across audio frequencies using pseudo-random sequences. Lower capacity but more resistant to noise and light processing.
## What's New
### Audio Steganography
- Support for WAV, FLAC, MP3, OGG, AAC, and M4A input formats
- Automatic transcoding to WAV (16-bit PCM) for embedding
- Same security model: reference photo + passphrase + PIN/RSA + channel key
- Full CLI, REST API, and Web UI support
### Unified Web UI
- Encode and Decode pages now feature a "Carrier Type" selector
- Switch between Image and Audio modes without leaving the page
- Audio capacity display shows LSB and Spread Spectrum capacities
- Audio preview player on encode result page
### New Modules
- `audio_steganography.py` — LSB audio embedding/extraction
- `spread_steganography.py` — Spread spectrum embedding/extraction
- `audio_utils.py` — Audio format detection, validation, transcoding
- `debug.py` — Structured logging for all operations
## Upgrade Notes
Audio steganography requires `numpy` and `soundfile` packages. Install with:
```bash
pip install stegasoo[audio]
```
For full audio format support (MP3, AAC, etc.), install FFmpeg on your system.
---
## Stegasoo v4.2.1
### API Security
**API Key Authentication**
- All protected endpoints require `X-API-Key` header
- Keys stored hashed (SHA-256) in `~/.stegasoo/api_keys.json`
- Auth disabled when no keys configured (easy onboarding)
**TLS Support**
- Self-signed certificates auto-generated on first run
- Certs valid for localhost, all local IPs, hostname.local
- CLI: `stegasoo api tls generate` to pre-generate
### CLI Improvements
**New API Management Commands**
```bash
stegasoo api keys create NAME # Create new key
stegasoo api keys list # List API keys
stegasoo api tls generate # Generate TLS cert
stegasoo api serve # Start server with TLS
```
**New Image Tools**
```bash
stegasoo tools compress IMG -q 75 # JPEG compression
stegasoo tools rotate IMG -r 90 # Lossless rotation
stegasoo tools convert IMG -f png # Format conversion
```
### Bug Fixes
- **DCT rotation**: Portrait photos no longer export rotated 90°
- **jpegtran**: Removed `-trim` flag that destroyed DCT stego data
- **CLI encode**: Now outputs JPEG when carrier is JPEG (was always PNG)
- **Import paths**: Fixed for installed packages (AUR/pip)
### Installation
**AUR (Arch Linux)**
```bash
yay -S stegasoo-git # Full (Web + API + CLI)
yay -S stegasoo-cli-git # CLI only
```
**Docker**
```bash
docker-compose -f docker/docker-compose.yml up -d
```
**Raspberry Pi**
Flash `stegasoo-rpi-4.2.1.img.zst.zip` to SD card.
Default login: `admin` / `stegasoo`
### Requirements
- Python 3.11 - 3.14 (dropped 3.10 support)
### Release Assets
| File | Description |
|------|-------------|
| `stegasoo-rpi-4.2.1.img.zst.zip` | Raspberry Pi SD card image |
| `stegasoo-docker-base-4.2.1.tar.zst` | Docker base image |
| Source code (zip/tar.gz) | Auto-generated |
---
## Stegasoo v4.2.0
### Performance Optimizations
Major performance improvements for Raspberry Pi and resource-constrained deployments.
#### DCT Vectorization (~14x faster)
- Batch DCT processing using `scipy.fft.dctn` with `axes=(1,2)`
- Processes 500 blocks at once instead of one-by-one
- Decode time reduced from ~2.6s to ~0.8s on 1MB images
#### Memory Optimization (50% reduction)
- Switched from `float64` to `float32` for all DCT operations
- Peak RAM: 211 MB → 107 MB for encode, 104 MB → 52 MB for decode
- Critical for Pi 3/4 avoiding swap thrashing
#### Progress Callbacks for Decode
- `progress_file` parameter added to `decode()` and extraction functions
- UI can now show decode progress (phases: loading, extracting, decoding, complete)
- JSON format: `{"current": 80, "total": 100, "percent": 80.0, "phase": "decoding"}`
#### Async API Endpoints
- Encode/decode operations now run in thread pool via `asyncio.to_thread()`
- API server can handle concurrent requests without blocking
- Essential for multi-user Pi deployments
### Compression
#### Zstd Default Compression
- `zstandard` is now a core dependency (always installed)
- Better compression ratio than zlib for QR code RSA keys
- New `STEGASOO-ZS:` prefix for zstd, backward compatible with `STEGASOO-Z:` (zlib)
### QR Code Generation
#### CLI Support
- `stegasoo generate --rsa --qr key.png` - save RSA key as QR image (PNG/JPG)
- `stegasoo generate --rsa --qr-ascii` - print ASCII QR to terminal
#### API Support
- `POST /generate-key-qr` - generate QR from RSA key
- Supports `png`, `jpg`, and `ascii` output formats
- Uses zstd compression by default
### Other Changes
- RSA key size capped at 3072 bits (4096 too large for QR codes)
- File auto-expire increased to 10 minutes
- Progress bar "candy cane" animation during Argon2 key derivation
- Optional API service in Pi setup (with security warning)
### Summary
| Metric | v4.1.7 | v4.2.0 | Improvement |
|--------|--------|--------|-------------|
| Decode (1MB) | ~2.6s | ~0.8s | **70% faster** |
| Peak RAM | 211 MB | 107 MB | **50% less** |
| Concurrent API | No | Yes | check |
| QR Compression | zlib | zstd | **~15% smaller** |
### Full Changelog
See [CHANGELOG.md](CHANGELOG.md) for complete version history.

View File

@@ -2,16 +2,18 @@
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.x.x | :white_check_mark: |
| 1.x.x | :x: |
| Version | Supported | Notes |
| ------- | ------------------ | ----- |
| 4.1.x | Current Version | What you SHOULD be using. |
| 4.x.x | ⚠️ Security fixes only | Upgrade (EOL soon) |
| <= 3.x.x | ❌ End of life | |
## Reporting a Vulnerability
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please email: **security@example.com** (replace with your email)
Instead, please email: **adlee-was-taken@proton.me**
Include:
- Description of the vulnerability
@@ -34,7 +36,7 @@ Stegasoo is designed to hide the **existence** of a secret message within an ord
| Goal | How It's Achieved |
|------|-------------------|
| **Confidentiality** | AES-256-GCM encryption with Argon2id key derivation |
| **Steganography** | LSB embedding with pseudo-random pixel selection |
| **Steganography** | LSB/DCT embedding with pseudo-random pixel/coefficient selection |
| **Authentication** | Multi-factor: reference photo + passphrase + PIN (or RSA key) |
| **Integrity** | GCM authentication tag detects tampering |
@@ -43,20 +45,43 @@ Stegasoo is designed to hide the **existence** of a secret message within an ord
Stegasoo combines multiple authentication factors:
```
┌─────────────────────────────────────────────────────────────┐
│ Key Derivation │
│ │
│ Reference Photo ─────
│ (something you have)
│ ├──► Argon2id ──► AES-256 Key │
Day Passphrase ──────┤ (256MB RAM) │
│ (something you know)
│ PIN or RSA Key ──────
│ (second factor) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────
│ Key Derivation
│ Reference Photo ───────┐
│ (something you have)
├──► Argon2id ──► AES-256 Key
│ Passphrase ────────────┤ (256MB RAM)
│ (something you know)
│ PIN or RSA Key ────────┘
│ (second factor)
└─────────────────────────────────────────────────────────────────
```
## Changes in v4.0
### Removed: Date-Based Key Rotation
**Previous versions (v3.x and earlier):**
- Required a date parameter for encode/decode
- Keys rotated daily based on "day phrase"
- Users had to remember which date they used
**Version 4.0:**
- No date dependency
- Single passphrase (no rotation)
- Simpler but slightly reduced entropy per-message
**Security Impact:**
- Minimal - the date only added ~10 bits of entropy
- Passphrase default increased from 3 to 4 words to compensate (+11 bits)
- Overall entropy remains similar or higher with 4-word default
### Renamed: day_phrase → passphrase
Terminology change only. No security impact.
## What Stegasoo Does NOT Protect Against
### 1. Statistical Steganalysis
@@ -68,7 +93,9 @@ Stegasoo combines multiple authentication factors:
- RS analysis
- Machine learning classifiers
**Mitigation:** Stegasoo uses pseudo-random pixel selection (not sequential), which helps but doesn't eliminate detectability.
**DCT mode is more resilient** but not undetectable.
**Mitigation:** Stegasoo uses pseudo-random pixel/coefficient selection, which helps but doesn't eliminate detectability.
**Recommendation:** Don't rely on Stegasoo if your adversary has:
- Access to the original carrier image
@@ -113,24 +140,28 @@ Stegasoo combines multiple authentication factors:
**Recommendation:**
- Use 8+ digit PINs
- Use 4+ word passphrases
- Use 4+ word passphrases (v4.0 default)
- Consider RSA keys for high-security use cases
### 5. Image Modification
**Risk:** Lossy compression destroys hidden data.
**Data is destroyed by:**
**LSB mode - data is destroyed by:**
- JPEG compression
- Resizing
- Filters/effects
- Screenshots
- Social media upload (Instagram, Twitter, etc.)
- Social media upload
**DCT mode - more resilient but not immune:**
- Survives moderate JPEG recompression
- May fail with aggressive compression (quality < 70)
- Still destroyed by resizing, filters, screenshots
**Recommendation:**
- Always use lossless formats (PNG, BMP)
- Transfer files directly (email, Signal, USB)
- Never upload stego images to social media
- LSB: Always use lossless formats (PNG, BMP), direct transfer
- DCT: Use for social media, but test with your specific platform
### 6. Metadata Leakage
@@ -165,49 +196,52 @@ Stegasoo combines multiple authentication factors:
| Encryption | AES-256-GCM | 12-byte IV, 16-byte tag |
| Photo Hash | SHA-256 | Full image bytes |
### Pixel Selection
### Pixel/Coefficient Selection
Pixels are selected pseudo-randomly using a key derived from:
Selection key is derived from:
```
pixel_key = SHA256(photo_hash || passphrase || date || pin/rsa_signature)
selection_key = SHA256(photo_hash || passphrase || pin/rsa_signature)
```
This prevents:
- Sequential embedding patterns
- Statistical detection of modified regions
### Format
### Message Format (v4.0)
```
┌──────────────────────────────────────────────────────────────┐
│ Magic (4B) │ Version (1B) │ Date (10B) │ Salt (32B) │ IV (12B) │
├──────────────────────────────────────────────────────────────┤
│ Encrypted Payload (AES-256-GCM) │
│ ├── Type (1B): 0x01=text, 0x02=file │
│ ├── Length (4B) │
│ ├── Data (variable) │
│ └── [Filename if file] (variable) │
├──────────────────────────────────────────────────────────────┤
│ GCM Auth Tag (16B) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────
│ Magic (4B) │ Version (1B) │ Salt (32B) │ IV (12B)
├──────────────────────────────────────────────────────────────────
│ Encrypted Payload (AES-256-GCM)
│ ├── Type (1B): 0x01=text, 0x02=file
│ ├── Length (4B)
│ ├── Data (variable)
│ └── [Filename if file] (variable)
├──────────────────────────────────────────────────────────────────
│ GCM Auth Tag (16B)
└──────────────────────────────────────────────────────────────────
```
**Note:** v4.0 removed the date field from the header, reducing overhead by 10 bytes.
## Best Practices
### For Maximum Security
1. **Use RSA keys** instead of PINs for authentication
2. **Use unique reference photos** not available online
3. **Use long passphrases** (4+ random words)
3. **Use long passphrases** (4+ random words, recommend 6+)
4. **Transfer via secure channels** (Signal, encrypted email)
5. **Delete stego images** after message is read
6. **Keep software updated** for security fixes
7. **Use DCT mode** for social media sharing
### For Casual Privacy
1. **6-digit PIN** is sufficient for non-adversarial use
2. **3-word passphrase** provides reasonable security
3. **PNG format** always for output
2. **4-word passphrase** provides reasonable security (v4.0 default)
3. **PNG format** for LSB mode output
4. **Direct file transfer** (email attachment, AirDrop)
## Known Limitations
@@ -216,8 +250,8 @@ This prevents:
|------------|--------|--------|
| LSB is detectable | Statistical analysis can detect hidden data | By design (tradeoff for capacity) |
| No forward secrecy | Compromised key decrypts all messages | Use different keys per message for high security |
| Date in header | Reveals when message was encoded | By design (enables day-specific passphrases) |
| No deniability | Single password = single message | Future: plausible deniability layers |
| Python 3.13 incompatible | jpegio C extension crashes | Use Python 3.12 or earlier |
## Security Audit Status
@@ -231,6 +265,9 @@ If you're a security researcher interested in auditing Stegasoo, please reach ou
| Version | Security Changes |
|---------|------------------|
| 4.0.0 | Removed date dependency, increased default passphrase to 4 words, added JPEG normalization |
| 3.2.0 | DCT color mode added |
| 3.0.0 | Added DCT steganography mode |
| 2.2.0 | Added compression (no security impact) |
| 2.1.0 | Upgraded to Argon2id, increased iterations |
| 2.0.0 | Added RSA key support |

284
SECURITY_AUDIT_PLAN.md Normal file
View File

@@ -0,0 +1,284 @@
# Stegasoo Security Audit Plan
> **Target Audience**: Developers, security reviewers, and deployment administrators
> **Scope**: Web UI, REST API, CLI, and cryptographic core
> **Deployment Model**: Air-gapped / private LAN (primary), Internet-facing (secondary)
---
## Overview
Stegasoo is a steganography tool designed for **air-gapped deployments** on private networks. While the primary threat model assumes a trusted local network, this audit plan covers security best practices for both isolated and potentially exposed deployments.
### Known Limitations (By Design)
- **Self-signed certificates**: HTTPS uses self-signed certs; users must add exceptions or deploy their own CA
- **No rate limiting**: Assumes trusted users on private network
- **Single-node**: No distributed session store; sessions are per-instance
- **Air-gap focus**: External security (firewalls, network isolation) is user's responsibility
---
## 1. Authentication & Authorization
### 1.1 Password Security
- [ ] Passwords hashed with Argon2id (preferred) or PBKDF2 fallback
- [ ] Minimum password length enforced (8+ characters)
- [ ] Password not logged or exposed in error messages
- [ ] Password change requires current password verification
- [ ] Admin re-authentication required for sensitive operations (channel key export)
### 1.2 Session Management
- [ ] Session tokens are cryptographically random
- [ ] Session cookies have `HttpOnly` flag
- [ ] Session cookies have `Secure` flag (when HTTPS enabled)
- [ ] Session cookies have `SameSite` attribute
- [ ] Sessions invalidated on logout
- [ ] Sessions invalidated on password change
- [ ] Session timeout configured appropriately
### 1.3 Authorization
- [ ] Admin-only routes protected by `@admin_required` decorator
- [ ] User-only routes protected by `@login_required` decorator
- [ ] Users cannot access other users' saved channel keys
- [ ] Users cannot modify other users' accounts
- [ ] Role escalation not possible through API manipulation
---
## 2. Cryptographic Implementation
### 2.1 Key Derivation
- [ ] KDF uses Argon2id with appropriate parameters (memory, iterations, parallelism)
- [ ] PBKDF2 fallback uses sufficient iterations (600,000+)
- [ ] Salt is cryptographically random and unique per operation
- [ ] PIN/passphrase combined securely before KDF
### 2.2 Encryption
- [ ] AES-256-GCM used for payload encryption
- [ ] Nonce/IV is unique per encryption operation
- [ ] Authentication tag verified before decryption
- [ ] No padding oracle vulnerabilities
### 2.3 Channel Keys
- [ ] Channel keys are 128-bit (32 hex chars)
- [ ] Channel key derivation uses HKDF or similar
- [ ] Channel isolation prevents cross-channel decryption
- [ ] Fingerprint reveals no information about full key
### 2.4 Random Number Generation
- [ ] All random values use `secrets` module or OS CSPRNG
- [ ] No use of `random` module for security-sensitive operations
---
## 3. Input Validation & Injection Prevention
### 3.1 Web UI
- [ ] All user input sanitized before rendering (XSS prevention)
- [ ] Jinja2 auto-escaping enabled
- [ ] No `| safe` filter on user-controlled content
- [ ] Content-Security-Policy header configured
- [ ] X-Content-Type-Options: nosniff
### 3.2 File Uploads
- [ ] File size limits enforced server-side
- [ ] File type validation (magic bytes, not just extension)
- [ ] Uploaded files not executed
- [ ] Filenames sanitized (path traversal prevention)
- [ ] Temporary files cleaned up after processing
### 3.3 API Inputs
- [ ] JSON schema validation on API endpoints
- [ ] Integer overflow checks on size parameters
- [ ] No SQL injection (parameterized queries only)
- [ ] No command injection (no shell=True with user input)
---
## 4. Steganography-Specific Security
### 4.1 Carrier Image Handling
- [ ] Malformed images don't crash the server (PIL/jpegio hardening)
- [ ] DCT mode subprocess isolation for crash protection
- [ ] Memory limits on image processing
- [ ] No arbitrary code execution from image metadata
### 4.2 Payload Security
- [ ] Payload size limits enforced
- [ ] Encrypted payload indistinguishable from random noise
- [ ] No metadata leakage in output images
- [ ] Reference photo required (prevents dictionary attacks)
### 4.3 Capacity Reporting
- [ ] Capacity calculation doesn't leak information about encoding method
- [ ] Failed decodes don't reveal why (wrong key vs no data vs corrupted)
---
## 5. Network & Transport Security
### 5.1 HTTPS Configuration
- [ ] TLS 1.2+ only (no SSLv3, TLS 1.0/1.1)
- [ ] Strong cipher suites configured
- [ ] Certificate generation uses 2048+ bit RSA or P-256 EC
- [ ] Private key file permissions restricted (600)
### 5.2 Headers
- [ ] X-Frame-Options: DENY (clickjacking prevention)
- [ ] X-Content-Type-Options: nosniff
- [ ] Referrer-Policy: same-origin
- [ ] Permissions-Policy configured
### 5.3 CORS (if applicable)
- [ ] CORS not enabled (or restricted to specific origins)
- [ ] Credentials not allowed cross-origin
---
## 6. Error Handling & Logging
### 6.1 Error Messages
- [ ] Stack traces not exposed to users in production
- [ ] Error messages don't reveal sensitive paths or config
- [ ] Failed login doesn't reveal if username exists
### 6.2 Logging
- [ ] Passwords never logged
- [ ] Channel keys never logged
- [ ] Passphrases never logged
- [ ] Log files have appropriate permissions
- [ ] Sensitive operations logged for audit trail (optional)
---
## 7. Dependency Security
### 7.1 Python Dependencies
- [ ] All dependencies pinned to specific versions
- [ ] No known vulnerabilities in dependencies (run `pip-audit` or `safety`)
- [ ] Dependencies from trusted sources only (PyPI)
### 7.2 Frontend Dependencies
- [ ] All JS/CSS served locally (air-gap ready)
- [ ] No CDN dependencies
- [ ] Bootstrap and libraries are official releases
- [ ] Subresource integrity considered for any external loads
---
## 8. Deployment Security
### 8.1 File Permissions
- [ ] Database file not world-readable (600 or 640)
- [ ] SSL certificates/keys not world-readable
- [ ] Config files with secrets protected
- [ ] Instance directory not in web root
### 8.2 Docker Deployment
- [ ] Container runs as non-root user
- [ ] No unnecessary capabilities
- [ ] Resource limits configured
- [ ] Health checks don't expose sensitive info
### 8.3 Raspberry Pi Deployment
- [ ] Default passwords changed
- [ ] SSH key-only authentication (recommended)
- [ ] Unnecessary services disabled
- [ ] Firewall configured (UFW/iptables)
---
## 9. Air-Gap Specific Considerations
### 9.1 Network Isolation
- [ ] Document expected network topology
- [ ] No phone-home or telemetry
- [ ] No external API calls
- [ ] Works fully offline after deployment
### 9.2 Key Distribution
- [ ] QR code export for channel keys (offline transfer)
- [ ] Print sheet for physical key backup
- [ ] No cloud sync or external key servers
### 9.3 Updates
- [ ] Document offline update procedure
- [ ] Signed releases (future consideration)
- [ ] Checksum verification for downloads
---
## 10. Penetration Testing Checklist
### 10.1 Authentication Attacks
- [ ] Brute force login (note: no rate limiting by design)
- [ ] Session fixation
- [ ] Session hijacking
- [ ] Password reset flow abuse
### 10.2 Injection Attacks
- [ ] SQL injection on all inputs
- [ ] XSS (stored, reflected, DOM-based)
- [ ] Command injection
- [ ] Path traversal
- [ ] SSTI (Server-Side Template Injection)
### 10.3 Business Logic
- [ ] Access control bypass
- [ ] IDOR (Insecure Direct Object Reference)
- [ ] Race conditions
- [ ] Integer overflow in capacity calculations
### 10.4 Cryptographic Attacks
- [ ] Known-plaintext attacks on stego output
- [ ] Timing attacks on password verification
- [ ] Padding oracle attacks
- [ ] Key reuse vulnerabilities
---
## Tools for Automated Testing
```bash
# Dependency vulnerability scan
pip-audit
safety check
# Static analysis
bandit -r stegasoo/ frontends/
# Web security scan (if exposed)
nikto -h https://localhost:5000
OWASP ZAP (manual)
# SSL/TLS configuration
testssl.sh https://localhost:5000
# Python code quality
ruff check .
mypy stegasoo/
```
---
## Audit Schedule
| Phase | Focus Area | Priority |
|-------|-----------|----------|
| Pre-release | Crypto implementation, auth flow | Critical |
| Post-release | Dependency scan, static analysis | High |
| Quarterly | Full penetration test | Medium |
| Ongoing | CVE monitoring for dependencies | High |
---
## Notes
- This plan assumes **trusted users on a private network** as the primary deployment model
- Internet-facing deployments should add rate limiting, fail2ban, and reverse proxy hardening
- For high-security deployments, consider external security audit by professionals
---
*Last updated: 2026-01-07*

54
TODO-4.2.1.md Normal file
View File

@@ -0,0 +1,54 @@
# Stegasoo 4.2.1 Plan
## Bugs
- [x] Fix EXIF viewer panel not loading metadata in Web UI
- Redesigned with card-based grid layout and categories
- Compact styling for better space usage
- [x] DCT mode: portrait photos export rotated 90° (EXIF orientation not handled)
- Added `_apply_exif_orientation()` to apply EXIF rotation before embedding
- [x] DCT mode: add rotation fallback (try as-is, rotate 90°, retry on failure)
- Added rotation fallback in `extract_from_dct()` with quick header validation
- [x] Rotate tool: use jpegtran for lossless JPEG rotation (preserves DCT stego!)
- Web UI rotate tool now uses jpegtran for JPEGs
- DCT decode rotation fallback now uses jpegtran for JPEGs
- Dynamic UI shows "DCT Safe" for JPEGs, warning for other formats
## Tools Audit
- [x] Web UI tools - full shakedown and fixes
- Compress, Rotate, Strip, EXIF viewer all working
- Rotate uses jpegtran for lossless JPEG rotation
- Compact UI styling
- [x] CLI tools - full shakedown and fixes
- Fixed encode to output JPEG when carrier is JPEG (was always PNG)
- Fixed jpegtran -trim flag destroying DCT stego data
- Added compress, rotate, convert tools (matching Web UI)
- Rotate uses jpegtran for JPEGs, supports flip-only operations
## AUR Packages
- [x] `stegasoo-cli` - standalone CLI package (no web dependencies)
- Created aur-cli/PKGBUILD with [cli,dct,compression] extras only
- No flask/gunicorn/fastapi/uvicorn/pyzbar deps
- 68MB vs 79MB for full package
- [x] `stegasoo-api` - REST API package
- Created aur-api/PKGBUILD with [api,cli,compression] extras
- Has fastapi/uvicorn, no flask/gunicorn
- 74MB package size
- Includes systemd service with TLS
## API Auth Work
- [x] API key authentication (simpler than OAuth2 for personal use)
- `frontends/api/auth.py` - key generation, hashing, validation
- Keys stored in `~/.stegasoo/api_keys.json` (hashed)
- `X-API-Key` header for authentication
- Auth disabled when no keys configured
- [x] TLS with self-signed certificates
- Auto-generates certs on first run
- CLI: `stegasoo api tls generate`
- Certs stored in `~/.stegasoo/certs/`
- [x] CLI commands for API management
- `stegasoo api keys list/create/delete`
- `stegasoo api tls generate/info`
- `stegasoo api serve` (starts with TLS by default)
## API Documentation
- [ ] Postman collection (with environment templates)

818
UNDER_THE_HOOD.md Normal file
View File

@@ -0,0 +1,818 @@
# Stegasoo Technical Deep Dive: Encoding & Decoding
A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work under the hood.
**Version 4.1** - Updated for channel keys and deployment isolation
---
## Table of Contents
1. [High-Level Overview](#high-level-overview)
2. [The Encoding Pipeline](#the-encoding-pipeline)
3. [The Decoding Pipeline](#the-decoding-pipeline)
4. [LSB Mode Deep Dive](#lsb-mode-deep-dive)
5. [DCT Mode Deep Dive](#dct-mode-deep-dive)
6. [Comparison Table](#comparison-table)
7. [Security Considerations](#security-considerations)
---
## High-Level Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEGASOO ARCHITECTURE (v4.1) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ INPUTS PROCESSING OUTPUT │
│ ─────── ────────── ────── │
│ │
│ Reference Photo ─┐ │
│ Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key │
│ PIN/RSA Key ─────┤ │ │
│ Channel Key ─────┘ (v4.1) ▼ │
│ Message/File ────────────────────────► AES-256-GCM ──► Ciphertext │
│ Encryption │ │
│ ▼ │
│ Carrier Image ───────────────────────────────────────► Embedding ─► Stego │
│ (LSB/DCT) Image │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### v4.0 Changes
| Change | v3.x | v4.0 |
|--------|------|------|
| Authentication | day_phrase + date | passphrase (no date) |
| Default words | 3 | 4 |
| Header size | 75 bytes | 65 bytes (no date field) |
| Python support | 3.10+ | 3.10-3.12 only |
### v4.1 Changes
| Change | v4.0 | v4.1 |
|--------|------|------|
| Channel keys | None | 32-byte deployment isolation |
| Key derivation | passphrase + ref + pin | passphrase + ref + pin + channel |
| Web auth | Session-based | Session + admin/user roles |
| Raspberry Pi | Manual setup | First-boot wizard with gum |
| Docker | Basic | Production-ready compose |
**Channel Keys** provide deployment isolation - messages encoded on one Stegasoo instance cannot be decoded by another instance with a different channel key, even with the same passphrase/PIN/reference photo.
### Module Responsibilities
| Module | File | Purpose |
|--------|------|---------|
| **Crypto** | `crypto.py` | Key derivation (Argon2id), AES-256-GCM encryption/decryption |
| **Channel** | `channel.py` | Channel key management, deployment isolation (v4.1) |
| **Steganography** | `steganography.py` | LSB pixel manipulation, capacity calculation |
| **DCT Steganography** | `dct_steganography.py` | Frequency-domain embedding, jpegio integration |
| **Compression** | `compression.py` | Optional LZ4 compression of payload |
| **Validation** | `validation.py` | Input validation, size limits |
| **Utils** | `utils.py` | Image hashing, format detection |
---
## The Encoding Pipeline
### Step 1: Input Collection & Validation
```python
# validation.py
def validate_encode_inputs(reference_photo, carrier, message, passphrase, pin, rsa_key):
# Check image dimensions (max 24 megapixels)
# Validate PIN format (6-9 digits)
# Validate passphrase (3-12 words from BIP-39)
# Check payload size vs carrier capacity
# Ensure reference != carrier (security)
```
### Step 2: Reference Photo Processing
```python
# utils.py
def get_image_hash(image_bytes: bytes) -> bytes:
"""
Generate deterministic hash from reference photo.
This is the 'something you have' factor.
"""
# Resize to 256x256 (normalize different resolutions)
# Convert to grayscale (normalize color variations)
# Apply slight blur (reduce JPEG artifact sensitivity)
# SHA-256 hash of processed pixels
return hashlib.sha256(processed_pixels).digest() # 32 bytes
```
**Why process the image?** Minor variations (JPEG recompression, slight crops) in the reference photo between sender and receiver would produce different hashes, breaking decryption. The preprocessing makes the hash more resilient.
### Step 3: Key Derivation (Argon2id)
```python
# crypto.py
def derive_key(reference_hash: bytes, passphrase: str, pin: str,
rsa_signature: bytes = None) -> bytes:
"""
Combine all authentication factors into one AES key.
v4.0: No date parameter - simplified authentication.
"""
# Concatenate all factors
key_material = reference_hash + passphrase.encode() + pin.encode()
if rsa_signature:
key_material += rsa_signature
# Argon2id parameters (memory-hard to resist GPU attacks)
# - Memory: 256 MB
# - Iterations: 4
# - Parallelism: 4
# - Output: 32 bytes (256 bits)
key = argon2.hash_password_raw(
password=key_material,
salt=random_salt, # 16 bytes, stored with ciphertext
time_cost=4,
memory_cost=262144, # 256 MB
parallelism=4,
hash_len=32,
type=argon2.Type.ID
)
return key # 32-byte AES-256 key
```
**Why Argon2id?**
- **Memory-hard**: Requires 256MB RAM per attempt, defeating GPU/ASIC attacks
- **Time-hard**: ~2-3 seconds per derivation
- **Side-channel resistant**: ID variant protects against timing attacks
### Step 4: Payload Preparation
```python
# compression.py (optional)
def prepare_payload(data: bytes, filename: str = None) -> bytes:
"""
Prepare the payload with metadata header.
"""
# Header format (variable length):
# [1 byte] - Flags (compression, file mode, etc.)
# [4 bytes] - Original data length (big-endian)
# [2 bytes] - Filename length (if file mode)
# [N bytes] - Filename (if file mode)
# [N bytes] - Data (possibly compressed)
header = struct.pack('>BI', flags, len(data))
if filename:
header += struct.pack('>H', len(filename)) + filename.encode()
# Optional LZ4 compression
if should_compress(data):
data = lz4.frame.compress(data)
flags |= FLAG_COMPRESSED
return header + data
```
### Step 5: AES-256-GCM Encryption
```python
# crypto.py
def encrypt(plaintext: bytes, key: bytes) -> bytes:
"""
Encrypt payload with AES-256-GCM.
Returns: salt + nonce + ciphertext + tag
"""
salt = os.urandom(16) # Random salt for key derivation
nonce = os.urandom(12) # Random nonce for GCM
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
# Final encrypted blob:
# [16 bytes] Salt
# [12 bytes] Nonce
# [16 bytes] Auth Tag
# [N bytes] Ciphertext
return salt + nonce + tag + ciphertext
```
**Why GCM?**
- **Authenticated encryption**: Detects tampering
- **No padding oracle**: Stream cipher mode
- **Built-in integrity**: 128-bit authentication tag
### Step 6: Stego Header Construction
```python
# steganography.py / dct_steganography.py
def build_stego_header(encrypted_data: bytes, mode: str) -> bytes:
"""
Build the header that precedes embedded data.
v4.0: Simplified header (no date field)
"""
# Header format:
# [4 bytes] - Magic number: "STGO" (v4)
# [1 byte] - Version (0x04)
# [1 byte] - Mode (0x01=LSB, 0x02=DCT)
# [4 bytes] - Payload length
# [N bytes] - Encrypted payload
if mode == 'lsb':
magic = b'STGO\x04\x01' # v4, mode 1 (LSB)
else:
magic = b'STGO\x04\x02' # v4, mode 2 (DCT)
length = struct.pack('>I', len(encrypted_data))
return magic + length + encrypted_data
```
### Step 7: Embedding (Mode-Specific)
This is where LSB and DCT diverge. See detailed sections below.
---
## The Decoding Pipeline
### Step 1: Mode Detection
```python
def detect_mode(stego_image: bytes) -> str:
"""
Detect which embedding mode was used.
Checks format and magic bytes.
"""
img = Image.open(io.BytesIO(stego_image))
# JPEG images with JPGS magic = DCT mode with jpegio
if img.format == 'JPEG':
# Check for jpegio magic
return 'dct'
# PNG/BMP: Read first few bytes from LSB
# Check for STGO or DCTS magic
magic = extract_header_lsb(stego_image, 6)
if magic.startswith(b'STGO'):
mode_byte = magic[5]
return 'lsb' if mode_byte == 0x01 else 'dct'
elif magic.startswith(b'DCTS'):
return 'dct'
return 'lsb' # Default fallback
```
### Step 2: Key Re-derivation
```python
# Same process as encoding
def derive_key_for_decode(reference_hash, passphrase, pin, rsa_signature=None):
# Must use SAME parameters as encoding
# No date parameter in v4.0
return derive_key(reference_hash, passphrase, pin, rsa_signature)
```
### Step 3: Data Extraction
```python
def extract_data(stego_image: bytes, mode: str) -> bytes:
"""
Extract raw bytes from stego image.
Mode-specific extraction.
"""
if mode == 'dct':
return extract_from_dct(stego_image, pixel_key)
else:
return extract_from_lsb(stego_image, pixel_key)
```
### Step 4: Decryption & Payload Recovery
```python
def decrypt_and_recover(encrypted_data: bytes, key: bytes) -> Union[str, bytes]:
"""
Decrypt and extract original message/file.
"""
# Parse header
salt = encrypted_data[:16]
nonce = encrypted_data[16:28]
tag = encrypted_data[28:44]
ciphertext = encrypted_data[44:]
# Decrypt
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
# Decompress if needed
if plaintext[0] & FLAG_COMPRESSED:
plaintext = lz4.frame.decompress(plaintext[5:])
# Extract payload
return parse_payload(plaintext)
```
---
## LSB Mode Deep Dive
### How LSB Embedding Works
LSB (Least Significant Bit) embedding modifies the lowest bit of each color channel in selected pixels.
```
Original Pixel (RGB):
R: 11010110 G: 01101001 B: 10110100
↓ ↓ ↓
└─────────┴─────────┘
3 bits available
After embedding "101":
R: 1101011[1] G: 0110100[0] B: 1011010[1]
↑ ↑ ↑
modified modified modified
```
### Pixel Selection Algorithm
```python
def select_pixels(carrier_shape, num_bits, seed: bytes) -> List[Tuple[int, int, int]]:
"""
Generate pseudo-random pixel coordinates.
Distributes modifications across entire image.
"""
height, width, channels = carrier_shape
total_positions = height * width * 3 # RGB channels
# Use seed to generate reproducible random order
rng = np.random.RandomState(int.from_bytes(seed[:4], 'big'))
all_positions = np.arange(total_positions)
rng.shuffle(all_positions)
# Convert flat indices to (y, x, channel)
selected = []
for idx in all_positions[:num_bits]:
y = idx // (width * 3)
x = (idx % (width * 3)) // 3
c = idx % 3
selected.append((y, x, c))
return selected
```
### Embedding Process
```python
def embed_lsb(carrier: np.ndarray, data: bytes, seed: bytes) -> np.ndarray:
"""
Embed data using LSB substitution.
"""
bits = bytes_to_bits(data)
positions = select_pixels(carrier.shape, len(bits), seed)
stego = carrier.copy()
for i, (y, x, c) in enumerate(positions):
# Clear LSB and set to our bit
stego[y, x, c] = (stego[y, x, c] & 0xFE) | bits[i]
return stego
```
### Capacity Calculation
```python
def calculate_lsb_capacity(width: int, height: int) -> int:
"""
Calculate maximum payload size for LSB mode.
"""
total_bits = width * height * 3 # 3 bits per pixel (RGB)
header_bits = 10 * 8 # 10-byte stego header
available_bits = total_bits - header_bits
return available_bits // 8 # Convert to bytes
```
**Example capacities:**
- 1920×1080: ~770 KB
- 4000×3000: ~4.5 MB
- 800×600: ~180 KB
---
## DCT Mode Deep Dive
### How DCT Embedding Works
DCT (Discrete Cosine Transform) mode embeds data in the frequency-domain coefficients, making it resilient to JPEG compression.
```
Image Block (8×8 pixels)
DCT Transform
DCT Coefficients (8×8)
┌────────────────────┐
│ DC AC₁ AC₂ AC₃ ...│ ← Lower frequencies (top-left)
│ AC₄ AC₅ AC₆ ... │
│ ... ... │ ← Mid frequencies (embed here)
│ ... ... │
│ AC₆₃ ────│ ← Higher frequencies (bottom-right)
└────────────────────┘
Modify select ACs
IDCT Transform
Modified Image Block
```
### Coefficient Selection
```python
# dct_steganography.py
EMBED_POSITIONS = [
(0, 1), (1, 0), (2, 0), (1, 1), (0, 2), (0, 3), (1, 2), (2, 1), (3, 0),
(4, 0), (3, 1), (2, 2), (1, 3), (0, 4), (0, 5), (1, 4), (2, 3), (3, 2),
(4, 1), (5, 0), (5, 1), (4, 2), (3, 3), (2, 4), (1, 5), (0, 6), (0, 7),
(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0),
]
# Use positions 4-20 (mid-frequency, good balance)
DEFAULT_EMBED_POSITIONS = EMBED_POSITIONS[4:20] # 16 positions per block
```
**Why mid-frequency?**
- DC coefficient (0,0): Too visible, contains brightness
- Low AC: Visible changes, but survives compression
- Mid AC: Best balance of invisibility + resilience
- High AC: Invisible but destroyed by compression
### Block Processing
```python
def embed_in_block(block: np.ndarray, bits: List[int]) -> np.ndarray:
"""
Embed bits in a single 8×8 block.
"""
# Forward DCT
dct_block = dct_2d(block)
# Embed using quantization
for i, pos in enumerate(DEFAULT_EMBED_POSITIONS):
if i >= len(bits):
break
coef = dct_block[pos[0], pos[1]]
# Quantize and modify LSB
quantized = round(coef / QUANT_STEP)
if (quantized % 2) != bits[i]:
quantized += 1 if coef > 0 else -1
dct_block[pos[0], pos[1]] = quantized * QUANT_STEP
# Inverse DCT
return idct_2d(dct_block)
```
### jpegio Integration (Native JPEG Output)
```python
def embed_jpegio(data: bytes, carrier_jpeg: bytes, seed: bytes) -> bytes:
"""
Embed directly in JPEG DCT coefficients using jpegio.
Preserves JPEG structure perfectly.
Note: Requires Python 3.12 or earlier (jpegio incompatible with 3.13)
"""
import jpegio as jio
# Normalize problematic JPEGs (quality=100 causes crashes)
carrier_jpeg = normalize_jpeg_for_jpegio(carrier_jpeg)
# Read existing JPEG coefficients
jpeg = jio.read(temp_file_from_bytes(carrier_jpeg))
coef_array = jpeg.coef_arrays[0] # Y channel
# Find usable coefficients (magnitude >= 2, non-DC)
positions = get_usable_positions(coef_array)
order = generate_order(len(positions), seed)
# Embed by modifying coefficient LSBs
bits = bytes_to_bits(data)
for i, pos_idx in enumerate(order[:len(bits)]):
row, col = positions[pos_idx]
coef = coef_array[row, col]
if (coef & 1) != bits[i]:
# Flip LSB while preserving sign
if coef > 0:
coef_array[row, col] = coef - 1 if (coef & 1) else coef + 1
else:
coef_array[row, col] = coef + 1 if (coef & 1) else coef - 1
# Write modified JPEG
jio.write(jpeg, output_path)
return read_bytes(output_path)
```
### JPEG Normalization (v4.0)
```python
def normalize_jpeg_for_jpegio(image_data: bytes) -> bytes:
"""
Normalize problematic JPEGs before jpegio processing.
JPEGs with quality=100 have quantization tables with all values=1,
which causes jpegio to crash. Re-save at quality 95.
"""
img = Image.open(io.BytesIO(image_data))
if img.format != 'JPEG':
return image_data
# Check if any quantization table has all values <= 1
needs_normalization = False
if hasattr(img, 'quantization'):
for table in img.quantization.values():
if max(table) <= 1:
needs_normalization = True
break
if not needs_normalization:
return image_data
# Re-save at safe quality
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=95, subsampling=0)
return buffer.getvalue()
```
### DCT Capacity Calculation
```python
def calculate_dct_capacity(width: int, height: int) -> int:
"""
Calculate maximum payload for DCT mode.
"""
blocks_x = width // 8
blocks_y = height // 8
total_blocks = blocks_x * blocks_y
bits_per_block = len(DEFAULT_EMBED_POSITIONS) # 16
total_bits = total_blocks * bits_per_block
header_bits = 10 * 8 # Stego header
available_bits = total_bits - header_bits
return available_bits // 8
```
**Example capacities:**
- 1920×1080: ~64 KB
- 4000×3000: ~375 KB
- 800×600: ~14 KB
### Why DCT Survives JPEG Compression
```
Original JPEG: Stego JPEG: Re-compressed:
DCT coefficients Modified DCT Coefficients
preserved in coefficients re-quantized
file format still valid
│ │ │
▼ ▼ ▼
[DCT] ──────► [Modified] ──────► [Still
[coefs] [DCT coefs] Modified!]
LSB changes survive because they're embedded in
the frequency domain, not spatial pixel values.
```
### DCT Advantages
| Advantage | Description |
|-----------|-------------|
| **JPEG resilient** | Survives social media upload |
| **Better steganalysis resistance** | Harder to detect statistically |
| **Natural-looking output** | JPEG artifacts expected |
### DCT Limitations
| Limitation | Description |
|------------|-------------|
| **Lower capacity** | ~10% of LSB capacity |
| **Slower processing** | DCT transforms are compute-intensive |
| **Requires scipy/jpegio** | Additional dependencies |
| **Quality-dependent** | Heavy recompression still degrades data |
| **Python version** | jpegio requires Python 3.12 or earlier |
---
## Comparison Table
| Aspect | LSB Mode | DCT Mode |
|--------|----------|----------|
| **Capacity (1080p)** | ~770 KB | ~50 KB |
| **Output Format** | PNG only | PNG or JPEG |
| **Survives JPEG** | ❌ No | ✅ Yes |
| **Social Media** | ❌ Broken | ✅ Works |
| **Processing Speed** | Fast (~0.5s) | Slower (~2s) |
| **Dependencies** | Pillow, NumPy | + scipy, jpegio |
| **Color Support** | Full color | Color or Grayscale |
| **Detection Resistance** | Moderate | Better |
| **Best For** | Email, cloud storage | Social media, messaging |
| **Max Tested Image** | 14MB+ | 14MB+ |
---
## Security Considerations
### What Makes Stegasoo Secure?
```
MULTI-FACTOR AUTHENTICATION (v4.0)
──────────────────────────────────
Factor 1: Reference Photo ─┐
• 80-256 bits entropy │
• "Something you have" │
├──► Combined entropy: 133-400+ bits
Factor 2: Passphrase │ (Beyond brute force)
• 43-132 bits entropy │
• "Something you know" │
• 4 words default (v4.0) │
Factor 3: PIN │
• 20-30 bits entropy │
• "Something you know" │
Factor 4: RSA Key (optional) ─┘
• 112-128 bits entropy
• "Something you have"
MEMORY-HARD KDF (Argon2id)
──────────────────────────
• 256 MB RAM per attempt
• ~3 seconds per attempt
• Defeats GPU/ASIC attacks
• 10 attempts = 30 seconds, not 0.00001 seconds
AUTHENTICATED ENCRYPTION (AES-256-GCM)
──────────────────────────────────────
• 256-bit key (unbreakable)
• Built-in integrity check
• Detects tampering
• No padding oracle attacks
```
### Attack Surface Analysis
| Attack | LSB Protection | DCT Protection |
|--------|----------------|----------------|
| Visual inspection | ✅ Imperceptible | ✅ Imperceptible |
| File size analysis | ⚠️ PNG larger | ✅ JPEG natural |
| Histogram analysis | ⚠️ Slight anomalies | ✅ Normal JPEG |
| Chi-square attack | ⚠️ Detectable at scale | ✅ Resistant |
| RS steganalysis | ⚠️ Detectable | ✅ Resistant |
| JPEG recompression | ❌ Destroyed | ✅ Survives |
### Threat Model
**Stegasoo protects against:**
- ✅ Passive eavesdropping
- ✅ Casual inspection of images
- ✅ Basic forensic analysis
- ✅ Brute force key guessing
- ✅ JPEG recompression (DCT mode)
**Stegasoo does NOT protect against:**
- ⚠️ Targeted forensic analysis with original carrier
- ⚠️ Nation-state level steganalysis
- ⚠️ Rubber hose cryptanalysis (coercion)
- ⚠️ Compromise of reference photo or credentials
---
## Data Flow Diagrams
### Complete Encode Flow (v4.0)
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ ENCODE FLOW (v4.0) │
└──────────────────────────────────────────────────────────────────────────────┘
User Inputs Processing Output
─────────── ────────── ──────
Reference Photo ──────┐
├──► get_image_hash() ──► ref_hash (32 bytes)
│ │
Passphrase ───────────┤ ▼
├──► derive_key() ──────► aes_key (32 bytes)
PIN ──────────────────┤ (Argon2id) │
│ │
RSA Key (optional) ───┘ │
Message/File ──────────► prepare_payload() ──► encrypt() ──► ciphertext
(compress, header) (AES-GCM) │
build_stego_header()
(magic + length)
Carrier Image ─────────────────────────────────────────► embed()
│ │
┌───────────┴─────┴────────────┐
│ │
LSB Mode DCT Mode
│ │
▼ ▼
embed_lsb() embed_in_dct()
(pixel LSBs) (DCT coefficients)
│ │
▼ ▼
PNG Output PNG or JPEG
│ │
└──────────┬───────────────────┘
Stego Image
(downloadable)
```
### Complete Decode Flow (v4.0)
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ DECODE FLOW (v4.0) │
└──────────────────────────────────────────────────────────────────────────────┘
User Inputs Processing Output
─────────── ────────── ──────
Reference Photo ──────┐
├──► get_image_hash() ──► ref_hash (32 bytes)
│ │
Passphrase ───────────┤ ▼
├──► derive_key() ──────► aes_key (32 bytes)
PIN ──────────────────┤ (Argon2id) │
│ (MUST MATCH!) │
RSA Key (optional) ───┘ │
Stego Image ──────────► detect_mode() ──────► extract()
(read magic) │ │
│ ┌─────────┴─────┴──────────┐
│ │ │
│ LSB Mode DCT Mode
│ │ │
│ ▼ ▼
│ extract_lsb() extract_from_dct()
│ │ │
│ └────────┬─────────────────┘
│ │
│ ▼
│ parse_stego_header()
│ (magic, length)
│ │
│ ▼
└────────► decrypt()
(AES-GCM)
decompress()
(if compressed)
extract_payload()
(handle file/text)
Original Message
or File
```
---
## Summary
**LSB Mode** is simpler, faster, and higher capacity - perfect for controlled channels where images won't be modified.
**DCT Mode** is more complex but survives real-world image processing - essential for social media and messaging apps.
Both modes share the same cryptographic foundation (Argon2id + AES-256-GCM) and multi-factor authentication, ensuring security regardless of embedding method.
The choice comes down to your use case:
- **Public platform?** → DCT (maximum compatibility)
- **Private channel?** → LSB (maximum capacity)
### v4.0 Simplifications
- **No more date tracking** - encode/decode anytime without remembering dates
- **Single passphrase** - no daily rotation to manage
- **Default 4 words** - better security out of the box
- **JPEG normalization** - handles quality=100 images automatically
- **Large image support** - tested with 14MB+ images

1333
WEB_UI.md Normal file

File diff suppressed because it is too large Load Diff

42
WISHLIST-4.2.md Normal file
View File

@@ -0,0 +1,42 @@
# Stegasoo v4.2 Wishlist
Blue sky ideas for future development. No timeline - just capturing thoughts.
---
## Performance
### GPU-Accelerated DCT Encoding/Decoding
- **Idea**: Leverage GPU for JPEG DCT coefficient manipulation
- **Potential Approaches**:
- OpenCL/CUDA for parallel DCT operations
- Raspberry Pi VideoCore IV/VI GPU compute
- WebGPU for browser-based acceleration
- **Challenges**:
- jpegio library is CPU-bound (C extension)
- Would need custom DCT implementation
- Memory transfer overhead may negate gains for small images
- **Research**:
- libjpeg-turbo uses SIMD but not GPU
- nvJPEG (NVIDIA) does GPU-accelerated JPEG
- Could potentially use GPU for the embedding math, not JPEG decode
---
## Features
(Add ideas here)
---
## Infrastructure
(Add ideas here)
---
## Notes
- This is a living document - add ideas anytime
- Not all ideas will be implemented
- Feasibility research needed before committing to roadmap

30
agentstuff/pyproject.toml Normal file
View File

@@ -0,0 +1,30 @@
[project]
name = "sentiment-agent"
version = "0.1.0"
description = "AI agent for gathering data and performing sentiment analysis"
requires-python = ">=3.11"
dependencies = [
"claude-agent-sdk",
"anyio",
"httpx",
]
[project.optional-dependencies]
dev = [
"pytest",
"ruff",
"mypy",
]
[project.scripts]
sentiment-agent = "sentiment_agent.main:main"
[tool.ruff]
line-length = 100
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.mypy]
python_version = "3.11"
ignore_missing_imports = true

View File

@@ -0,0 +1,3 @@
"""Sentiment analysis agent powered by Claude Agent SDK."""
__version__ = "0.1.0"

View File

@@ -0,0 +1,115 @@
"""Core sentiment analysis agent using Claude Agent SDK."""
from __future__ import annotations
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
ResultMessage,
TextBlock,
)
from sentiment_agent.config import SafetyConfig
from sentiment_agent.tools import create_social_tools_server
SYSTEM_PROMPT = """\
You are a sentiment analysis agent. Your job is to gather data from multiple \
platforms and produce a structured, evidence-based sentiment report.
## Rules — you MUST follow these
1. **Budget awareness.** You have a limited API call budget. Call \
`get_api_budget_status` before starting and after every few tool calls. \
Stop gathering data when you have <5 calls remaining and begin your analysis.
2. **Credibility first.** Every tool result includes credibility scores and \
bot/disinfo flags. You MUST:
- NEVER quote or cite posts marked `likely_inauthentic` (score < 0.3).
- Flag posts marked `suspicious` (score 0.30.5) with a warning when citing them.
- Give more weight to `likely_authentic` posts (score ≥ 0.7).
- If coordination warnings appear (copy-paste campaigns, burst posting), \
call them out prominently in your report.
3. **Platform diversity.** Gather from at least 2 different platforms before \
analyzing. Do not over-index on a single source.
4. **No fabrication.** Only report on data you actually retrieved. If a tool \
call fails or returns no results, say so — do not invent data.
5. **Structured output.** Your final report MUST include these sections:
- **Data Quality Summary**: platforms queried, posts analyzed vs excluded, \
coordination warnings
- **Overall Sentiment**: score (-1.0 to +1.0) and label \
(very negative / negative / mixed / neutral / positive / very positive)
- **Platform Breakdown**: sentiment per platform with sample size
- **Key Themes**: top 3-5 themes with sentiment direction
- **Credibility Concerns**: any bot networks, disinfo patterns, or \
coordinated campaigns detected
- **Notable Quotes**: 3-5 representative quotes (authentic sources only, \
with credibility score noted)
- **Confidence Assessment**: how confident you are in the analysis given \
data quality and volume
6. **Scope discipline.** Stay focused on the requested topic. Do not expand \
scope, follow tangents, or analyze adjacent topics unless explicitly asked.
7. **No side effects.** Do not write files, run commands, or take any action \
beyond reading data and producing your report.
"""
async def run_sentiment_analysis(
topic: str,
sources: list[str] | None = None,
config: SafetyConfig | None = None,
) -> str:
"""Run the sentiment analysis agent on a given topic.
Args:
topic: The topic or subject to analyze sentiment for.
sources: Optional list of URLs or data sources to analyze.
config: Safety configuration. Defaults to SafetyConfig.from_env().
Returns:
The agent's sentiment analysis report.
"""
config = config or SafetyConfig.from_env()
source_instructions = ""
if sources:
source_list = "\n".join(f"- {s}" for s in sources)
source_instructions = f"\n\nAlso analyze these specific sources:\n{source_list}"
prompt = (
f"Perform a sentiment analysis on the following topic: {topic}\n\n"
"Start by calling `get_api_budget_status` to check your budget, then "
"gather data from multiple platforms (Reddit, Hacker News, Bluesky if "
"configured, and web search). Pay close attention to credibility scores "
"and coordination warnings in the results."
f"{source_instructions}"
)
social_server = create_social_tools_server(config)
options = ClaudeAgentOptions(
# Only allow read-only tools — no Write/Bash to prevent side effects
allowed_tools=["WebSearch", "WebFetch", "Read"],
max_turns=config.max_turns,
max_budget_usd=config.max_budget_usd,
mcp_servers={"social": social_server},
system_prompt=SYSTEM_PROMPT,
)
result_text = ""
async with ClaudeSDKClient(options=options) as client:
await client.query(prompt)
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text, end="", flush=True)
if isinstance(message, ResultMessage):
result_text = message.result
return result_text

View File

@@ -0,0 +1 @@
"""API clients for social media and forum data sources."""

View File

@@ -0,0 +1,166 @@
"""Bluesky client using the AT Protocol API.
Search requires authentication. Set BLUESKY_HANDLE and BLUESKY_APP_PASSWORD
env vars. Create an app password at: https://bsky.app/settings/app-passwords
Thread fetching works without auth via the public API.
"""
import os
import httpx
BSKY_PUBLIC_API = "https://public.api.bsky.app"
BSKY_AUTH_API = "https://bsky.social"
async def _get_session() -> dict | None:
"""Authenticate with Bluesky and return session tokens, or None if no creds."""
handle = os.environ.get("BLUESKY_HANDLE")
app_password = os.environ.get("BLUESKY_APP_PASSWORD")
if not handle or not app_password:
return None
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.post(
f"{BSKY_AUTH_API}/xrpc/com.atproto.server.createSession",
json={"identifier": handle, "password": app_password},
)
resp.raise_for_status()
return resp.json()
def _format_post(post_view: dict) -> dict:
"""Extract relevant fields from an AT Protocol post view."""
post = post_view.get("post", post_view)
record = post.get("record", {})
author = post.get("author", {})
return {
"text": record.get("text", ""),
"author_handle": author.get("handle", ""),
"author_display_name": author.get("displayName", ""),
"created_at": record.get("createdAt", ""),
"like_count": post.get("likeCount", 0),
"repost_count": post.get("repostCount", 0),
"reply_count": post.get("replyCount", 0),
"uri": post.get("uri", ""),
"cid": post.get("cid", ""),
"url": _uri_to_url(post.get("uri", ""), author.get("handle", "")),
}
def _uri_to_url(uri: str, handle: str) -> str:
"""Convert an at:// URI to a bsky.app URL."""
# at://did:plc:xxx/app.bsky.feed.post/rkey -> https://bsky.app/profile/handle/post/rkey
if not uri.startswith("at://"):
return ""
parts = uri.split("/")
if len(parts) >= 5:
rkey = parts[-1]
return f"https://bsky.app/profile/{handle}/post/{rkey}"
return ""
async def search_posts(query: str, limit: int = 25, sort: str = "top") -> list[dict]:
"""Search Bluesky for posts matching a query.
Requires BLUESKY_HANDLE and BLUESKY_APP_PASSWORD env vars.
Args:
query: Search terms.
limit: Max results (capped at 100).
sort: "top" (most liked) or "latest" (chronological).
Returns:
List of post dicts with: text, author_handle, author_display_name,
created_at, like_count, repost_count, reply_count, uri, url.
Raises:
RuntimeError: If Bluesky credentials are not configured.
"""
session = await _get_session()
if not session:
raise RuntimeError(
"Bluesky search requires authentication. "
"Set BLUESKY_HANDLE and BLUESKY_APP_PASSWORD environment variables. "
"Create an app password at: https://bsky.app/settings/app-passwords"
)
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(
f"{BSKY_AUTH_API}/xrpc/app.bsky.feed.searchPosts",
params={
"q": query,
"limit": min(limit, 100),
"sort": sort,
},
headers={"Authorization": f"Bearer {session['accessJwt']}"},
)
resp.raise_for_status()
data = resp.json()
return [_format_post(p) for p in data.get("posts", [])]
async def get_thread(uri: str, depth: int = 6) -> dict:
"""Fetch a Bluesky thread by AT URI or bsky.app URL.
Args:
uri: Either an at:// URI or a https://bsky.app/profile/.../post/... URL.
depth: How many levels of replies to fetch (max 1000).
Returns:
Dict with "post" (the root post) and "replies" (list of reply post dicts).
"""
# Convert bsky.app URL to AT URI if needed
if uri.startswith("https://bsky.app/"):
uri = await _resolve_url_to_uri(uri)
headers = {}
session = await _get_session()
if session:
headers["Authorization"] = f"Bearer {session['accessJwt']}"
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(
f"{BSKY_PUBLIC_API}/xrpc/app.bsky.feed.getPostThread",
params={"uri": uri, "depth": min(depth, 1000)},
headers=headers,
)
resp.raise_for_status()
data = resp.json()
thread = data.get("thread", {})
root_post = _format_post(thread) if "post" in thread else {}
replies = []
for reply in thread.get("replies", []):
if "post" in reply:
replies.append(_format_post(reply))
# Include nested replies one level deep
for nested in reply.get("replies", []):
if "post" in nested:
replies.append(_format_post(nested))
return {"post": root_post, "replies": replies}
async def _resolve_url_to_uri(url: str) -> str:
"""Convert a bsky.app URL to an AT URI by resolving the handle."""
# https://bsky.app/profile/handle.bsky.social/post/rkey
parts = url.rstrip("/").split("/")
if len(parts) < 6:
raise ValueError(f"Invalid Bluesky URL: {url}")
handle = parts[4] # profile/{handle}
rkey = parts[6] # post/{rkey}
# Resolve handle to DID
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(
f"{BSKY_PUBLIC_API}/xrpc/com.atproto.identity.resolveHandle",
params={"handle": handle},
)
resp.raise_for_status()
did = resp.json()["did"]
return f"at://{did}/app.bsky.feed.post/{rkey}"

View File

@@ -0,0 +1,78 @@
"""Hacker News client using the Algolia HN Search API.
No authentication required. Docs: https://hn.algolia.com/api
"""
import httpx
HN_API_BASE = "https://hn.algolia.com/api/v1"
async def search_stories(query: str, limit: int = 25) -> list[dict]:
"""Search HN for stories matching a query.
Returns a list of story dicts with: title, url, author, points,
num_comments, created_at, objectID, story_text.
"""
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(
f"{HN_API_BASE}/search",
params={
"query": query,
"tags": "story",
"hitsPerPage": min(limit, 50),
},
)
resp.raise_for_status()
data = resp.json()
results = []
for hit in data.get("hits", []):
results.append(
{
"title": hit.get("title", ""),
"url": hit.get("url", ""),
"author": hit.get("author", ""),
"points": hit.get("points", 0),
"num_comments": hit.get("num_comments", 0),
"created_at": hit.get("created_at", ""),
"object_id": hit.get("objectID", ""),
"story_text": hit.get("story_text") or "",
"hn_url": f"https://news.ycombinator.com/item?id={hit.get('objectID', '')}",
}
)
return results
async def search_comments(query: str, limit: int = 25) -> list[dict]:
"""Search HN for comments matching a query.
Returns a list of comment dicts with: comment_text, author, points,
created_at, story_title, story_url.
"""
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(
f"{HN_API_BASE}/search",
params={
"query": query,
"tags": "comment",
"hitsPerPage": min(limit, 50),
},
)
resp.raise_for_status()
data = resp.json()
results = []
for hit in data.get("hits", []):
results.append(
{
"comment_text": hit.get("comment_text", ""),
"author": hit.get("author", ""),
"points": hit.get("points", 0),
"created_at": hit.get("created_at", ""),
"story_title": hit.get("story_title", ""),
"story_url": hit.get("story_url", ""),
"hn_url": f"https://news.ycombinator.com/item?id={hit.get('objectID', '')}",
}
)
return results

View File

@@ -0,0 +1,117 @@
"""Reddit client using the public JSON API.
No authentication required for read-only search. Reddit requires a descriptive
User-Agent header — requests with generic UAs get 429'd.
"""
import httpx
REDDIT_BASE = "https://www.reddit.com"
USER_AGENT = "sentiment-agent/0.1.0 (research; sentiment analysis tool)"
async def search_posts(
query: str,
subreddit: str = "all",
sort: str = "relevance",
time_filter: str = "month",
limit: int = 25,
) -> list[dict]:
"""Search Reddit for posts matching a query.
Args:
query: Search terms.
subreddit: Subreddit to search within, or "all" for site-wide.
sort: One of "relevance", "hot", "top", "new", "comments".
time_filter: One of "hour", "day", "week", "month", "year", "all".
limit: Max results (capped at 100 by Reddit).
Returns:
List of post dicts with: title, selftext, author, score,
num_comments, subreddit, url, permalink, created_utc.
"""
url = f"{REDDIT_BASE}/r/{subreddit}/search.json"
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
resp = await client.get(
url,
params={
"q": query,
"sort": sort,
"t": time_filter,
"limit": min(limit, 100),
"restrict_sr": "on" if subreddit != "all" else "off",
},
headers={"User-Agent": USER_AGENT},
)
resp.raise_for_status()
data = resp.json()
results = []
for child in data.get("data", {}).get("children", []):
post = child.get("data", {})
results.append(
{
"title": post.get("title", ""),
"selftext": (post.get("selftext") or "")[:2000],
"author": post.get("author", "[deleted]"),
"score": post.get("score", 0),
"upvote_ratio": post.get("upvote_ratio", 0),
"num_comments": post.get("num_comments", 0),
"subreddit": post.get("subreddit", ""),
"url": post.get("url", ""),
"permalink": f"https://reddit.com{post.get('permalink', '')}",
"created_utc": post.get("created_utc", 0),
"is_self": post.get("is_self", False),
}
)
return results
async def get_post_comments(
permalink: str,
sort: str = "top",
limit: int = 25,
) -> list[dict]:
"""Fetch top-level comments for a Reddit post.
Args:
permalink: The post's permalink path (e.g., "/r/python/comments/abc123/title/").
sort: Comment sort order: "top", "best", "new", "controversial".
limit: Max comments to return.
Returns:
List of comment dicts with: body, author, score, created_utc.
"""
# Strip domain if full URL was passed
if permalink.startswith("https://"):
permalink = permalink.replace("https://reddit.com", "")
permalink = permalink.replace("https://www.reddit.com", "")
url = f"{REDDIT_BASE}{permalink}.json"
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
resp = await client.get(
url,
params={"sort": sort, "limit": limit},
headers={"User-Agent": USER_AGENT},
)
resp.raise_for_status()
data = resp.json()
# Reddit returns [post_listing, comments_listing]
if not isinstance(data, list) or len(data) < 2:
return []
results = []
for child in data[1].get("data", {}).get("children", []):
if child.get("kind") != "t1":
continue
comment = child.get("data", {})
results.append(
{
"body": (comment.get("body") or "")[:2000],
"author": comment.get("author", "[deleted]"),
"score": comment.get("score", 0),
"created_utc": comment.get("created_utc", 0),
}
)
return results

View File

@@ -0,0 +1,70 @@
"""Configuration and safety limits for the sentiment agent.
All guardrails are centralized here so they can be tuned from one place
or overridden via CLI flags / env vars.
"""
from __future__ import annotations
import os
from dataclasses import dataclass, field
@dataclass(frozen=True)
class RateLimitConfig:
"""Per-platform rate limiting."""
requests_per_minute: int = 10
burst_size: int = 3 # max concurrent requests
cooldown_after_429: float = 30.0 # seconds to wait after a 429
@dataclass(frozen=True)
class SafetyConfig:
"""Top-level safety rails for the agent."""
# --- Agent-level limits ---
max_turns: int = 20
max_budget_usd: float = 0.50 # hard cap on Claude API spend per run
max_total_api_calls: int = 50 # across ALL platforms combined
max_results_per_call: int = 50 # cap the `limit` param sent to any API
# --- Per-platform rate limits ---
bluesky_rate: RateLimitConfig = field(default_factory=lambda: RateLimitConfig(
requests_per_minute=10, burst_size=2,
))
reddit_rate: RateLimitConfig = field(default_factory=lambda: RateLimitConfig(
requests_per_minute=10, burst_size=2,
))
hackernews_rate: RateLimitConfig = field(default_factory=lambda: RateLimitConfig(
requests_per_minute=15, burst_size=3, # HN Algolia is more generous
))
# --- Content size limits ---
max_post_text_chars: int = 2000 # truncate individual posts beyond this
max_total_content_bytes: int = 500_000 # ~500KB total data gathered before agent stops
# --- Timeout ---
api_timeout_seconds: float = 15.0
# --- Credibility thresholds ---
min_credibility_score: float = 0.3 # posts below this are flagged/excluded
flag_bot_threshold: float = 0.5 # posts between min and this are flagged but included
@classmethod
def from_env(cls) -> SafetyConfig:
"""Build config with env var overrides.
Env vars: SENTIMENT_MAX_TURNS, SENTIMENT_MAX_BUDGET_USD,
SENTIMENT_MAX_API_CALLS, SENTIMENT_MIN_CREDIBILITY.
"""
kwargs: dict = {}
if v := os.environ.get("SENTIMENT_MAX_TURNS"):
kwargs["max_turns"] = int(v)
if v := os.environ.get("SENTIMENT_MAX_BUDGET_USD"):
kwargs["max_budget_usd"] = float(v)
if v := os.environ.get("SENTIMENT_MAX_API_CALLS"):
kwargs["max_total_api_calls"] = int(v)
if v := os.environ.get("SENTIMENT_MIN_CREDIBILITY"):
kwargs["min_credibility_score"] = float(v)
return cls(**kwargs)

View File

@@ -0,0 +1,398 @@
"""Credibility scoring and bot/disinfo detection.
Assigns a 0.01.0 credibility score to each post based on heuristic signals.
Posts below the configured threshold are excluded or flagged so they don't
pollute the sentiment analysis.
Signals are platform-aware — each platform has different indicators of
inauthentic behavior.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from datetime import datetime, timezone
@dataclass
class CredibilityResult:
"""Credibility assessment for a single post."""
score: float # 0.0 (likely bot/disinfo) to 1.0 (likely authentic)
flags: list[str] = field(default_factory=list) # human-readable reasons
is_excluded: bool = False # below min_credibility_score
is_flagged: bool = False # between min and flag threshold
@property
def label(self) -> str:
if self.score >= 0.7:
return "likely_authentic"
if self.score >= 0.5:
return "uncertain"
if self.score >= 0.3:
return "suspicious"
return "likely_inauthentic"
# --- Shared heuristics ---
# Common bot patterns in text
_BOT_TEXT_PATTERNS = [
# Crypto/scam spam
re.compile(r"(?i)(dm me|check my bio|link in bio|click here|free giveaway)"),
re.compile(r"(?i)(join my|subscribe to|follow me for|🔥.*🔥.*🔥)"),
# Astroturfing phrases
re.compile(r"(?i)(i (just )?(discovered|found|tried) this (amazing|incredible|awesome))"),
re.compile(r"(?i)(game.?changer|life.?changing|you won'?t believe)"),
# Excessive hashtags (5+)
re.compile(r"(#\w+\s*){5,}"),
# Walls of emojis (10+ consecutive)
re.compile(r"[\U0001F300-\U0001FAFF]{10,}"),
# Repetitive characters (spammy emphasis)
re.compile(r"(.)\1{9,}"),
]
# Coordinated campaign indicators: identical or near-identical text
# This is checked at the batch level, not per-post
def _check_text_patterns(text: str) -> list[str]:
"""Check text against common bot/spam patterns."""
flags = []
for pattern in _BOT_TEXT_PATTERNS:
if pattern.search(text):
flags.append(f"bot_text_pattern: {pattern.pattern[:60]}")
if len(text) < 15:
flags.append("very_short_text")
return flags
def _engagement_ratio_score(
likes: int, reposts: int, replies: int
) -> tuple[float, list[str]]:
"""Score based on engagement ratios.
Authentic posts tend to have a mix of likes, replies, and reposts.
Bot-amplified posts often have inflated likes with very few replies,
or massive repost counts with no discussion.
"""
flags = []
total = likes + reposts + replies
if total == 0:
return 0.5, ["no_engagement"]
# High repost-to-reply ratio suggests amplification without discussion
if reposts > 0 and replies == 0 and reposts > 10:
flags.append(f"high_repost_no_replies: {reposts} reposts, 0 replies")
return 0.3, flags
# Extremely high like count with zero replies is suspicious
if likes > 100 and replies == 0:
flags.append(f"high_likes_no_replies: {likes} likes, 0 replies")
return 0.4, flags
# Normal engagement
return min(1.0, 0.5 + (replies / max(total, 1)) * 0.5), flags
# --- Platform-specific scoring ---
def score_bluesky_post(post: dict) -> CredibilityResult:
"""Score a Bluesky post for credibility."""
score = 1.0
flags: list[str] = []
text = post.get("text", "")
handle = post.get("author_handle", "")
display_name = post.get("author_display_name", "")
likes = post.get("like_count", 0)
reposts = post.get("repost_count", 0)
replies = post.get("reply_count", 0)
# Text pattern checks
text_flags = _check_text_patterns(text)
if text_flags:
score -= 0.15 * len(text_flags)
flags.extend(text_flags)
# Handle heuristics
# Randomly generated handles (long hex/number strings)
if re.match(r"^[a-f0-9]{8,}\.", handle):
flags.append(f"random_handle: {handle}")
score -= 0.3
# No display name set
if not display_name or display_name == handle:
flags.append("no_display_name")
score -= 0.1
# Engagement ratio
eng_score, eng_flags = _engagement_ratio_score(likes, reposts, replies)
flags.extend(eng_flags)
score = score * 0.6 + eng_score * 0.4
return CredibilityResult(score=max(0.0, min(1.0, score)), flags=flags)
def score_reddit_post(post: dict) -> CredibilityResult:
"""Score a Reddit post for credibility."""
score = 1.0
flags: list[str] = []
text = post.get("selftext", "") or post.get("title", "")
author = post.get("author", "")
upvote_ratio = post.get("upvote_ratio", 0.5)
post_score = post.get("score", 0)
num_comments = post.get("num_comments", 0)
# Text patterns
text_flags = _check_text_patterns(text)
if text_flags:
score -= 0.15 * len(text_flags)
flags.extend(text_flags)
# Deleted author
if author in ("[deleted]", "[removed]"):
flags.append("deleted_author")
score -= 0.2
# Suspicious username patterns (random alphanumeric + numbers)
if re.match(r"^[A-Za-z]+[-_]?\d{4,}$", author):
flags.append(f"auto_generated_username: {author}")
score -= 0.15
# Very controversial ratio (lots of up AND down votes)
if upvote_ratio < 0.4 and post_score > 0:
flags.append(f"highly_controversial: {upvote_ratio:.0%} upvote ratio")
score -= 0.1
# High score but zero comments = potential vote manipulation
if post_score > 100 and num_comments == 0:
flags.append(f"high_score_no_comments: {post_score} score, 0 comments")
score -= 0.2
# Low-effort cross-post spam: very short title, external link, no selftext
if (
len(post.get("title", "")) < 20
and not post.get("is_self", True)
and not post.get("selftext")
):
flags.append("possible_link_spam")
score -= 0.1
return CredibilityResult(score=max(0.0, min(1.0, score)), flags=flags)
def score_reddit_comment(comment: dict) -> CredibilityResult:
"""Score a Reddit comment for credibility."""
score = 1.0
flags: list[str] = []
body = comment.get("body", "")
author = comment.get("author", "")
comment_score = comment.get("score", 0)
text_flags = _check_text_patterns(body)
if text_flags:
score -= 0.15 * len(text_flags)
flags.extend(text_flags)
if author in ("[deleted]", "[removed]"):
flags.append("deleted_author")
score -= 0.2
if re.match(r"^[A-Za-z]+[-_]?\d{4,}$", author):
flags.append(f"auto_generated_username: {author}")
score -= 0.15
# Heavily downvoted
if comment_score < -5:
flags.append(f"heavily_downvoted: {comment_score}")
score -= 0.15
return CredibilityResult(score=max(0.0, min(1.0, score)), flags=flags)
def score_hackernews_post(post: dict) -> CredibilityResult:
"""Score a HN story for credibility.
HN is generally higher-signal than social media, but we still check
for low-effort submissions and spammy patterns.
"""
score = 1.0
flags: list[str] = []
title = post.get("title", "")
text = post.get("story_text", "") or title
points = post.get("points", 0)
num_comments = post.get("num_comments", 0)
text_flags = _check_text_patterns(text)
if text_flags:
score -= 0.1 * len(text_flags)
flags.extend(text_flags)
# Zero points = the community didn't find it valuable
if points == 0:
flags.append("zero_points")
score -= 0.1
# HN is generally more credible, start with a bonus
score = min(1.0, score + 0.1)
return CredibilityResult(score=max(0.0, min(1.0, score)), flags=flags)
def score_hackernews_comment(comment: dict) -> CredibilityResult:
"""Score a HN comment for credibility."""
score = 1.0
flags: list[str] = []
text = comment.get("comment_text", "")
text_flags = _check_text_patterns(text)
if text_flags:
score -= 0.1 * len(text_flags)
flags.extend(text_flags)
# HN comments are generally higher quality
score = min(1.0, score + 0.1)
return CredibilityResult(score=max(0.0, min(1.0, score)), flags=flags)
# --- Batch-level coordination detection ---
def detect_coordination(posts: list[dict], text_key: str = "text") -> list[str]:
"""Detect coordinated inauthentic behavior across a batch of posts.
Looks for:
- Duplicate or near-duplicate text (copy-paste campaigns)
- Burst posting (many posts in a very short window)
- Same talking points with minor variations
Returns a list of warning strings.
"""
warnings: list[str] = []
texts = [p.get(text_key, "") for p in posts if p.get(text_key)]
if not texts:
return warnings
# Exact duplicates
seen: dict[str, int] = {}
for t in texts:
normalized = t.strip().lower()
seen[normalized] = seen.get(normalized, 0) + 1
duplicates = {text: count for text, count in seen.items() if count > 1}
if duplicates:
total_dupes = sum(duplicates.values())
warnings.append(
f"COORDINATION WARNING: {len(duplicates)} duplicate texts found "
f"({total_dupes} total copies). Possible copy-paste campaign."
)
# Near-duplicates: check if many posts share a long common substring
# (simplified: check if >30% of posts start with the same 50+ chars)
if len(texts) >= 5:
prefixes: dict[str, int] = {}
for t in texts:
prefix = t.strip().lower()[:80]
if len(prefix) >= 50:
prefixes[prefix] = prefixes.get(prefix, 0) + 1
for prefix, count in prefixes.items():
if count >= len(texts) * 0.3:
warnings.append(
f"COORDINATION WARNING: {count}/{len(texts)} posts share "
f"a common prefix ({prefix[:50]}...). Possible template campaign."
)
# Burst detection: if timestamps are available
timestamps = []
for p in posts:
created = p.get("created_at") or p.get("created_utc")
if isinstance(created, str):
try:
timestamps.append(datetime.fromisoformat(created.replace("Z", "+00:00")))
except (ValueError, TypeError):
pass
elif isinstance(created, (int, float)):
timestamps.append(datetime.fromtimestamp(created, tz=timezone.utc))
if len(timestamps) >= 5:
timestamps.sort()
# Check if >50% of posts landed within a 5-minute window
window_seconds = 300
for i in range(len(timestamps) - 2):
window_end = timestamps[i] + __import__("datetime").timedelta(seconds=window_seconds)
in_window = sum(1 for t in timestamps if timestamps[i] <= t <= window_end)
if in_window >= len(timestamps) * 0.5:
warnings.append(
f"COORDINATION WARNING: {in_window}/{len(timestamps)} posts "
f"appeared within a 5-minute window. Possible coordinated posting."
)
break
return warnings
def filter_and_annotate(
posts: list[dict],
scorer,
min_score: float = 0.3,
flag_threshold: float = 0.5,
) -> tuple[list[dict], dict]:
"""Score all posts, filter out low-credibility ones, and annotate the rest.
Args:
posts: List of post dicts from any platform.
scorer: A scoring function (e.g., score_reddit_post).
min_score: Posts below this are excluded.
flag_threshold: Posts between min_score and this are flagged.
Returns:
Tuple of (filtered_posts, stats_dict).
Each post in filtered_posts gets a "_credibility" key added.
"""
filtered = []
stats = {
"total": len(posts),
"excluded": 0,
"flagged": 0,
"authentic": 0,
"excluded_reasons": [],
}
for post in posts:
result = scorer(post)
result.is_excluded = result.score < min_score
result.is_flagged = min_score <= result.score < flag_threshold
if result.is_excluded:
stats["excluded"] += 1
stats["excluded_reasons"].append(
{"score": round(result.score, 2), "flags": result.flags}
)
continue
post["_credibility"] = {
"score": round(result.score, 2),
"label": result.label,
"flags": result.flags,
"is_flagged": result.is_flagged,
}
if result.is_flagged:
stats["flagged"] += 1
else:
stats["authentic"] += 1
filtered.append(post)
return filtered, stats

View File

@@ -0,0 +1,66 @@
"""CLI entry point for the sentiment analysis agent."""
import argparse
import anyio
from sentiment_agent.agent import run_sentiment_analysis
from sentiment_agent.config import SafetyConfig
async def async_main(args: argparse.Namespace) -> None:
config = SafetyConfig(
max_turns=args.max_turns,
max_budget_usd=args.max_budget,
max_total_api_calls=args.max_api_calls,
min_credibility_score=args.min_credibility,
flag_bot_threshold=args.flag_threshold,
)
result = await run_sentiment_analysis(
topic=args.topic,
sources=args.sources,
config=config,
)
print("\n" + "=" * 60)
print("SENTIMENT ANALYSIS REPORT")
print("=" * 60)
print(result)
def main() -> None:
parser = argparse.ArgumentParser(
description="Run sentiment analysis on a topic with bot/disinfo detection",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("topic", help="The topic to analyze sentiment for")
parser.add_argument(
"--sources", nargs="*", help="Specific URLs or sources to also analyze"
)
safety = parser.add_argument_group("safety limits")
safety.add_argument(
"--max-turns", type=int, default=20, help="Max agent turns"
)
safety.add_argument(
"--max-budget", type=float, default=0.50, help="Max Claude API spend (USD)"
)
safety.add_argument(
"--max-api-calls", type=int, default=50, help="Max total API calls across all platforms"
)
credibility = parser.add_argument_group("credibility filtering")
credibility.add_argument(
"--min-credibility", type=float, default=0.3,
help="Posts below this score are excluded (0.0-1.0)",
)
credibility.add_argument(
"--flag-threshold", type=float, default=0.5,
help="Posts between min and this are flagged but included (0.0-1.0)",
)
args = parser.parse_args()
anyio.run(async_main, args)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,169 @@
"""Rate limiter and API call budget tracker.
Enforces per-platform rate limits and a global call budget so the agent
can't hammer APIs or run up unbounded costs.
"""
from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass, field
from sentiment_agent.config import RateLimitConfig
class BudgetExhaustedError(Exception):
"""Raised when the global API call budget is spent."""
class RateLimitExceededError(Exception):
"""Raised when a platform's rate limit is hit and cooldown hasn't elapsed."""
@dataclass
class _PlatformState:
"""Tracks call timestamps and active request count for one platform."""
config: RateLimitConfig
call_timestamps: list[float] = field(default_factory=list)
active_requests: int = 0
last_429_at: float = 0.0
class RateLimiter:
"""Manages rate limiting across all platforms + a global call budget.
Usage:
limiter = RateLimiter(max_total_calls=50)
limiter.register_platform("reddit", RateLimitConfig(...))
async with limiter.acquire("reddit"):
await do_reddit_call()
"""
def __init__(self, max_total_calls: int = 50):
self._max_total = max_total_calls
self._total_calls = 0
self._platforms: dict[str, _PlatformState] = {}
self._lock = asyncio.Lock()
@property
def total_calls(self) -> int:
return self._total_calls
@property
def remaining_calls(self) -> int:
return max(0, self._max_total - self._total_calls)
def register_platform(self, name: str, config: RateLimitConfig) -> None:
self._platforms[name] = _PlatformState(config=config)
def acquire(self, platform: str) -> _AcquireContext:
"""Context manager that enforces rate limits before allowing a call."""
return _AcquireContext(self, platform)
async def _acquire(self, platform: str) -> None:
async with self._lock:
if self._total_calls >= self._max_total:
raise BudgetExhaustedError(
f"Global API call budget exhausted ({self._max_total} calls). "
"Increase max_total_api_calls in SafetyConfig to allow more."
)
state = self._platforms.get(platform)
if not state:
raise ValueError(f"Platform '{platform}' not registered with rate limiter")
now = time.monotonic()
# Check 429 cooldown
if state.last_429_at:
elapsed = now - state.last_429_at
if elapsed < state.config.cooldown_after_429:
remaining = state.config.cooldown_after_429 - elapsed
raise RateLimitExceededError(
f"Platform '{platform}' is in cooldown after 429. "
f"Try again in {remaining:.0f}s."
)
state.last_429_at = 0.0
# Check burst limit
if state.active_requests >= state.config.burst_size:
raise RateLimitExceededError(
f"Platform '{platform}' burst limit reached "
f"({state.config.burst_size} concurrent). Wait for a request to finish."
)
# Check RPM: discard timestamps older than 60s, then check count
cutoff = now - 60.0
state.call_timestamps = [t for t in state.call_timestamps if t > cutoff]
if len(state.call_timestamps) >= state.config.requests_per_minute:
oldest = state.call_timestamps[0]
wait_time = 60.0 - (now - oldest)
raise RateLimitExceededError(
f"Platform '{platform}' rate limit: {state.config.requests_per_minute}/min. "
f"Try again in {wait_time:.0f}s."
)
# All clear — record the call
state.call_timestamps.append(now)
state.active_requests += 1
self._total_calls += 1
async def _release(self, platform: str) -> None:
async with self._lock:
state = self._platforms.get(platform)
if state:
state.active_requests = max(0, state.active_requests - 1)
def record_429(self, platform: str) -> None:
"""Call this when an API returns 429 to trigger cooldown."""
state = self._platforms.get(platform)
if state:
state.last_429_at = time.monotonic()
def get_stats(self) -> dict:
"""Return current usage stats for logging/reporting."""
stats: dict = {
"total_calls": self._total_calls,
"remaining_calls": self.remaining_calls,
"platforms": {},
}
for name, state in self._platforms.items():
now = time.monotonic()
cutoff = now - 60.0
recent = [t for t in state.call_timestamps if t > cutoff]
stats["platforms"][name] = {
"calls_last_60s": len(recent),
"active_requests": state.active_requests,
"rpm_limit": state.config.requests_per_minute,
"in_cooldown": bool(
state.last_429_at
and (now - state.last_429_at) < state.config.cooldown_after_429
),
}
return stats
class _AcquireContext:
"""Async context manager for rate-limited API calls."""
def __init__(self, limiter: RateLimiter, platform: str):
self._limiter = limiter
self._platform = platform
async def __aenter__(self) -> None:
await self._limiter._acquire(self._platform)
async def __aexit__(self, *exc_info) -> None:
# Check if the call got a 429
if exc_info[0] is not None:
import httpx
exc = exc_info[1]
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 429:
self._limiter.record_429(self._platform)
await self._limiter._release(self._platform)

View File

@@ -0,0 +1,352 @@
"""Custom MCP tools for social media and forum data gathering.
Each tool wraps an API client, enforces rate limits, runs credibility
scoring, and returns MCP-formatted results with bot/disinfo annotations.
"""
from __future__ import annotations
import json
import traceback
from claude_agent_sdk import tool, create_sdk_mcp_server
from sentiment_agent.clients import bluesky, reddit, hackernews
from sentiment_agent.config import SafetyConfig
from sentiment_agent.credibility import (
detect_coordination,
filter_and_annotate,
score_bluesky_post,
score_hackernews_comment,
score_hackernews_post,
score_reddit_comment,
score_reddit_post,
)
from sentiment_agent.ratelimit import BudgetExhaustedError, RateLimiter
# Module-level state — initialized by create_social_tools_server()
_limiter: RateLimiter | None = None
_config: SafetyConfig | None = None
def _get_limiter() -> RateLimiter:
if _limiter is None:
raise RuntimeError("Tools not initialized — call create_social_tools_server() first")
return _limiter
def _get_config() -> SafetyConfig:
if _config is None:
return SafetyConfig()
return _config
def _text_result(text: str) -> dict:
return {"content": [{"type": "text", "text": text}]}
def _error_result(error: str) -> dict:
return {"content": [{"type": "text", "text": f"Error: {error}"}], "isError": True}
def _clamp_limit(requested: int) -> int:
"""Enforce max results per call."""
return min(requested, _get_config().max_results_per_call)
def _format_with_stats(
posts: list[dict],
stats: dict,
coordination_warnings: list[str],
platform: str,
) -> str:
"""Format results with credibility stats prepended."""
header_parts = [
f"Platform: {platform}",
f"Results: {stats['authentic']} authentic, {stats['flagged']} flagged, "
f"{stats['excluded']} excluded (of {stats['total']} total)",
]
if coordination_warnings:
header_parts.append("--- COORDINATION ALERTS ---")
header_parts.extend(coordination_warnings)
header_parts.append("---")
limiter = _get_limiter()
header_parts.append(f"API budget remaining: {limiter.remaining_calls} calls")
header = "\n".join(header_parts)
body = json.dumps(posts, indent=2, default=str)
return f"{header}\n\n{body}"
# --- Bluesky tools ---
@tool(
"search_bluesky",
"Search Bluesky for posts about a topic. Returns posts with text, author, "
"engagement metrics, credibility scores, and bot/disinfo flags. "
"Requires BLUESKY_HANDLE and BLUESKY_APP_PASSWORD env vars.",
{"query": str, "limit": int, "sort": str},
)
async def search_bluesky(args: dict) -> dict:
try:
limiter = _get_limiter()
config = _get_config()
async with limiter.acquire("bluesky"):
posts = await bluesky.search_posts(
query=args["query"],
limit=_clamp_limit(args.get("limit", 25)),
sort=args.get("sort", "top"),
)
if not posts:
return _text_result(f"No Bluesky posts found for: {args['query']}")
coordination = detect_coordination(posts, text_key="text")
filtered, stats = filter_and_annotate(
posts, score_bluesky_post,
min_score=config.min_credibility_score,
flag_threshold=config.flag_bot_threshold,
)
return _text_result(_format_with_stats(filtered, stats, coordination, "Bluesky"))
except BudgetExhaustedError as e:
return _error_result(str(e))
except Exception as e:
return _error_result(f"Bluesky search failed: {e}\n{traceback.format_exc()}")
@tool(
"get_bluesky_thread",
"Fetch a Bluesky thread/post and its replies with credibility scoring. "
"Accepts an at:// URI or https://bsky.app/... URL.",
{"uri": str, "depth": int},
)
async def get_bluesky_thread(args: dict) -> dict:
try:
limiter = _get_limiter()
config = _get_config()
async with limiter.acquire("bluesky"):
thread = await bluesky.get_thread(
uri=args["uri"],
depth=args.get("depth", 6),
)
# Score replies
if thread.get("replies"):
coordination = detect_coordination(thread["replies"], text_key="text")
filtered_replies, stats = filter_and_annotate(
thread["replies"], score_bluesky_post,
min_score=config.min_credibility_score,
flag_threshold=config.flag_bot_threshold,
)
thread["replies"] = filtered_replies
thread["_reply_credibility_stats"] = stats
thread["_coordination_warnings"] = coordination
# Score root post
if thread.get("post"):
result = score_bluesky_post(thread["post"])
thread["post"]["_credibility"] = {
"score": round(result.score, 2),
"label": result.label,
"flags": result.flags,
}
return _text_result(json.dumps(thread, indent=2, default=str))
except BudgetExhaustedError as e:
return _error_result(str(e))
except Exception as e:
return _error_result(f"Bluesky thread fetch failed: {e}\n{traceback.format_exc()}")
# --- Reddit tools ---
@tool(
"search_reddit",
"Search Reddit for posts about a topic. Returns posts with credibility scores "
"and bot/disinfo flags. Posts below the credibility threshold are auto-excluded. "
"Use subreddit='all' for site-wide or specify a subreddit name.",
{"query": str, "subreddit": str, "sort": str, "time_filter": str, "limit": int},
)
async def search_reddit_tool(args: dict) -> dict:
try:
limiter = _get_limiter()
config = _get_config()
async with limiter.acquire("reddit"):
posts = await reddit.search_posts(
query=args["query"],
subreddit=args.get("subreddit", "all"),
sort=args.get("sort", "relevance"),
time_filter=args.get("time_filter", "month"),
limit=_clamp_limit(args.get("limit", 25)),
)
if not posts:
return _text_result(f"No Reddit posts found for: {args['query']}")
coordination = detect_coordination(posts, text_key="title")
filtered, stats = filter_and_annotate(
posts, score_reddit_post,
min_score=config.min_credibility_score,
flag_threshold=config.flag_bot_threshold,
)
return _text_result(_format_with_stats(filtered, stats, coordination, "Reddit"))
except BudgetExhaustedError as e:
return _error_result(str(e))
except Exception as e:
return _error_result(f"Reddit search failed: {e}\n{traceback.format_exc()}")
@tool(
"get_reddit_comments",
"Fetch comments for a Reddit post with credibility scoring. "
"Pass the permalink path or full URL.",
{"permalink": str, "sort": str, "limit": int},
)
async def get_reddit_comments(args: dict) -> dict:
try:
limiter = _get_limiter()
config = _get_config()
async with limiter.acquire("reddit"):
comments = await reddit.get_post_comments(
permalink=args["permalink"],
sort=args.get("sort", "top"),
limit=_clamp_limit(args.get("limit", 25)),
)
if not comments:
return _text_result("No comments found for this post.")
coordination = detect_coordination(comments, text_key="body")
filtered, stats = filter_and_annotate(
comments, score_reddit_comment,
min_score=config.min_credibility_score,
flag_threshold=config.flag_bot_threshold,
)
return _text_result(_format_with_stats(filtered, stats, coordination, "Reddit Comments"))
except BudgetExhaustedError as e:
return _error_result(str(e))
except Exception as e:
return _error_result(f"Reddit comments fetch failed: {e}\n{traceback.format_exc()}")
# --- Hacker News tools ---
@tool(
"search_hackernews",
"Search Hacker News for stories with credibility scoring. "
"No authentication required.",
{"query": str, "limit": int},
)
async def search_hackernews_tool(args: dict) -> dict:
try:
limiter = _get_limiter()
config = _get_config()
async with limiter.acquire("hackernews"):
stories = await hackernews.search_stories(
query=args["query"],
limit=_clamp_limit(args.get("limit", 25)),
)
if not stories:
return _text_result(f"No HN stories found for: {args['query']}")
coordination = detect_coordination(stories, text_key="title")
filtered, stats = filter_and_annotate(
stories, score_hackernews_post,
min_score=config.min_credibility_score,
flag_threshold=config.flag_bot_threshold,
)
return _text_result(_format_with_stats(filtered, stats, coordination, "Hacker News"))
except BudgetExhaustedError as e:
return _error_result(str(e))
except Exception as e:
return _error_result(f"HN search failed: {e}\n{traceback.format_exc()}")
@tool(
"search_hackernews_comments",
"Search Hacker News comments for opinions and discussions with credibility scoring.",
{"query": str, "limit": int},
)
async def search_hackernews_comments(args: dict) -> dict:
try:
limiter = _get_limiter()
config = _get_config()
async with limiter.acquire("hackernews"):
comments = await hackernews.search_comments(
query=args["query"],
limit=_clamp_limit(args.get("limit", 25)),
)
if not comments:
return _text_result(f"No HN comments found for: {args['query']}")
coordination = detect_coordination(comments, text_key="comment_text")
filtered, stats = filter_and_annotate(
comments, score_hackernews_comment,
min_score=config.min_credibility_score,
flag_threshold=config.flag_bot_threshold,
)
return _text_result(
_format_with_stats(filtered, stats, coordination, "HN Comments")
)
except BudgetExhaustedError as e:
return _error_result(str(e))
except Exception as e:
return _error_result(f"HN comment search failed: {e}\n{traceback.format_exc()}")
# --- Budget status tool ---
@tool(
"get_api_budget_status",
"Check remaining API call budget, rate limit status, and per-platform stats. "
"Use this before making more API calls to avoid hitting limits.",
{},
)
async def get_api_budget_status(args: dict) -> dict:
limiter = _get_limiter()
stats = limiter.get_stats()
return _text_result(json.dumps(stats, indent=2, default=str))
# --- Server factory ---
def create_social_tools_server(config: SafetyConfig | None = None):
"""Create an MCP server with all social media/forum tools.
Initializes rate limiting and credibility thresholds from config.
"""
global _limiter, _config
_config = config or SafetyConfig.from_env()
_limiter = RateLimiter(max_total_calls=_config.max_total_api_calls)
_limiter.register_platform("bluesky", _config.bluesky_rate)
_limiter.register_platform("reddit", _config.reddit_rate)
_limiter.register_platform("hackernews", _config.hackernews_rate)
return create_sdk_mcp_server(
"social-tools",
tools=[
search_bluesky,
get_bluesky_thread,
search_reddit_tool,
get_reddit_comments,
search_hackernews_tool,
search_hackernews_comments,
get_api_budget_status,
],
)

23
aur-api/.SRCINFO Normal file
View File

@@ -0,0 +1,23 @@
pkgbase = stegasoo-api-git
pkgdesc = Stegasoo REST API with TLS and API key authentication
pkgver = 4.2.1
pkgrel = 1
url = https://github.com/adlee-was-taken/stegasoo
install = stegasoo-api-git.install
arch = x86_64
license = MIT
makedepends = git
makedepends = python
makedepends = python-build
makedepends = python-hatchling
depends = python>=3.11
depends = zbar
optdepends = libjpeg-turbo: jpegtran for lossless JPEG rotation (DCT-safe)
provides = stegasoo-api
conflicts = stegasoo-api
conflicts = stegasoo
conflicts = stegasoo-git
source = stegasoo-api-git::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main
sha256sums = SKIP
pkgname = stegasoo-api-git

109
aur-api/PKGBUILD Normal file
View File

@@ -0,0 +1,109 @@
# Maintainer: Aaron D. Lee <your-email@example.com>
pkgname=stegasoo-api-git
pkgver=4.3.0
pkgrel=1
pkgdesc="Stegasoo REST API with TLS and API key authentication"
arch=('x86_64')
url="https://github.com/adlee-was-taken/stegasoo"
license=('MIT')
# Python 3.11-3.14 supported
depends=(
'python>=3.11'
'zbar' # QR code reading for RSA key extraction
)
makedepends=(
'git'
'python'
'python-build'
'python-hatchling'
)
optdepends=(
'libjpeg-turbo: jpegtran for lossless JPEG rotation (DCT-safe)'
)
provides=('stegasoo-api')
conflicts=('stegasoo-api' 'stegasoo' 'stegasoo-git')
install=stegasoo-api-git.install
source=("${pkgname}::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main")
sha256sums=('SKIP')
pkgver() {
cd "$pkgname"
git describe --long --tags 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || \
printf "%s.r%s.g%s" "4.3.0" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$pkgname"
python -m build --wheel --no-isolation
}
package() {
cd "$pkgname"
# Detect Python version for site-packages path
local pyver=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
# Install to /opt/stegasoo-api with dedicated venv
install -dm755 "$pkgdir/opt/stegasoo-api"
# Create fresh venv in package
python -m venv "$pkgdir/opt/stegasoo-api/venv"
# Install the wheel with API + CLI + compression extras
local wheel=$(ls dist/*.whl | head -1)
"$pkgdir/opt/stegasoo-api/venv/bin/pip" install --no-cache-dir "${wheel}[api,cli,compression]"
# Install API frontend (not included in wheel by default)
local site_packages="$pkgdir/opt/stegasoo-api/venv/lib/python${pyver}/site-packages"
install -dm755 "$site_packages/frontends/api"
cp -r frontends/api/*.py "$site_packages/frontends/api/"
cp -r frontends/__init__.py "$site_packages/frontends/" 2>/dev/null || true
# Create temp directory for API
install -dm755 "$site_packages/frontends/api/temp_files"
# Create config directories
install -dm755 "$pkgdir/opt/stegasoo-api/config"
install -dm700 "$pkgdir/opt/stegasoo-api/certs"
# Fix shebangs - replace build-time paths with installed paths
find "$pkgdir/opt/stegasoo-api/venv/bin" -type f -exec \
sed -i "s|$pkgdir/opt/stegasoo-api/venv|/opt/stegasoo-api/venv|g" {} \;
# Fix pyvenv.cfg
sed -i "s|$pkgdir||g" "$pkgdir/opt/stegasoo-api/venv/pyvenv.cfg"
# Create symlink to /usr/bin
install -dm755 "$pkgdir/usr/bin"
ln -s /opt/stegasoo-api/venv/bin/stegasoo "$pkgdir/usr/bin/stegasoo"
# Install license
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
# Install docs
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
# Install systemd service
install -Dm644 /dev/stdin "$pkgdir/usr/lib/systemd/system/stegasoo-api.service" <<EOF
[Unit]
Description=Stegasoo REST API (HTTPS)
After=network.target
[Service]
Type=simple
User=stegasoo
WorkingDirectory=/opt/stegasoo-api/venv/lib/python${pyver}/site-packages/frontends/api
Environment="PATH=/opt/stegasoo-api/venv/bin"
Environment="HOME=/opt/stegasoo-api"
# TLS enabled by default - certs auto-generated on first run
# Use: stegasoo api tls generate (to pre-generate certs)
# Use: stegasoo api keys create <name> (to create API keys)
ExecStart=/opt/stegasoo-api/venv/bin/stegasoo api serve --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
}

102
aur-api/README.md Normal file
View File

@@ -0,0 +1,102 @@
# Stegasoo API AUR Package
REST API server package for programmatic steganography operations. Includes HTTPS support and API key authentication.
## Installation
### From AUR (once published)
```bash
yay -S stegasoo-api-git
# or
paru -S stegasoo-api-git
```
### Manual build
```bash
git clone https://aur.archlinux.org/stegasoo-api-git.git
cd stegasoo-api-git
makepkg -si
```
## What Gets Installed
- `/opt/stegasoo-api/venv/` - Self-contained Python venv with API dependencies
- `/opt/stegasoo-api/config/` - API key storage
- `/opt/stegasoo-api/certs/` - TLS certificates
- `/usr/bin/stegasoo` - CLI executable
- `/usr/lib/systemd/system/stegasoo-api.service` - Systemd service
## Quick Start
```bash
# 1. Create an API key
sudo -u stegasoo stegasoo api keys create mykey
# 2. Start the service
sudo systemctl enable --now stegasoo-api
# 3. Test the API
curl -k -H "X-API-Key: YOUR_KEY" https://localhost:8000/
```
## Service Details
| Setting | Value |
|---------|-------|
| Port | 8000 |
| Protocol | HTTPS (self-signed cert auto-generated) |
| API Docs | https://localhost:8000/docs |
| OpenAPI | https://localhost:8000/openapi.json |
## API Key Management
```bash
# List all keys
stegasoo api keys list
# Create a new key
sudo -u stegasoo stegasoo api keys create <name>
# Revoke a key
sudo -u stegasoo stegasoo api keys revoke <name>
```
## TLS Configuration
```bash
# View current certificate info
stegasoo api tls info
# Generate new self-signed certificate
sudo -u stegasoo stegasoo api tls generate
# Use custom certificates (edit service)
sudo systemctl edit stegasoo-api
# Add:
# [Service]
# ExecStart=
# ExecStart=/opt/stegasoo-api/venv/bin/stegasoo api serve \
# --host 0.0.0.0 --port 8000 \
# --cert /path/to/cert.pem --key /path/to/key.pem
```
## Manual Run (without systemd)
```bash
# Development mode (auto-reload)
/opt/stegasoo-api/venv/bin/stegasoo api serve --reload
# Production mode
/opt/stegasoo-api/venv/bin/stegasoo api serve --host 0.0.0.0 --port 8000
```
## For Web UI
Install the full package instead:
```bash
yay -S stegasoo-git
```
## Maintainer
Aaron D. Lee

View File

@@ -0,0 +1,63 @@
post_install() {
# Create stegasoo system user if it doesn't exist
if ! getent passwd stegasoo >/dev/null; then
useradd -r -s /usr/bin/nologin -d /opt/stegasoo-api stegasoo
echo "Created system user 'stegasoo'"
fi
# Set ownership of directories
chown -R stegasoo:stegasoo /opt/stegasoo-api/config 2>/dev/null || true
chown -R stegasoo:stegasoo /opt/stegasoo-api/certs 2>/dev/null || true
echo ""
echo "==============================================="
echo " Stegasoo API installed successfully!"
echo "==============================================="
echo ""
echo "-----------------------------------------------"
echo " Quick Start"
echo "-----------------------------------------------"
echo " 1. Create an API key:"
echo " sudo -u stegasoo stegasoo api keys create mykey"
echo ""
echo " 2. Start the API server:"
echo " sudo systemctl start stegasoo-api"
echo " sudo systemctl enable stegasoo-api # auto-start"
echo ""
echo " 3. Access the API:"
echo " curl -k -H 'X-API-Key: YOUR_KEY' https://localhost:8000/"
echo ""
echo "-----------------------------------------------"
echo " Service Details"
echo "-----------------------------------------------"
echo " Port: 8000 (HTTPS by default)"
echo " Docs: https://localhost:8000/docs"
echo " Status: sudo systemctl status stegasoo-api"
echo ""
echo "-----------------------------------------------"
echo " Management Commands"
echo "-----------------------------------------------"
echo " stegasoo api keys list # List API keys"
echo " stegasoo api keys create X # Create new key"
echo " stegasoo api tls generate # Generate TLS certs"
echo " stegasoo api tls info # Show certificate info"
echo " stegasoo api serve --help # Server options"
echo ""
echo "==============================================="
echo ""
}
post_upgrade() {
post_install
}
pre_remove() {
# Stop service if running
systemctl stop stegasoo-api 2>/dev/null || true
}
post_remove() {
echo "Stegasoo API removed."
echo "User 'stegasoo' and config in /opt/stegasoo-api were not removed."
echo "To remove: userdel stegasoo && rm -rf /opt/stegasoo-api"
}

22
aur-api/test-build.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Test build the AUR API package locally
set -e
cd "$(dirname "$0")"
echo "=== Cleaning previous builds ==="
rm -rf stegasoo-api-git pkg src *.pkg.tar.zst *.whl 2>/dev/null || true
echo "=== Generating .SRCINFO ==="
makepkg --printsrcinfo > .SRCINFO
echo "=== Building package ==="
makepkg -sf
echo "=== Package built ==="
ls -la *.pkg.tar.zst
echo ""
echo "To install: sudo pacman -U stegasoo-api-git-*.pkg.tar.zst"
echo "To test: makepkg -si"

22
aur-cli/.SRCINFO Normal file
View File

@@ -0,0 +1,22 @@
pkgbase = stegasoo-cli-git
pkgdesc = Secure steganography CLI with hybrid photo + passphrase + PIN authentication
pkgver = 4.2.1
pkgrel = 1
url = https://github.com/adlee-was-taken/stegasoo
install = stegasoo-cli-git.install
arch = x86_64
license = MIT
makedepends = git
makedepends = python
makedepends = python-build
makedepends = python-hatchling
depends = python>=3.11
optdepends = libjpeg-turbo: jpegtran for lossless JPEG rotation (DCT-safe)
provides = stegasoo-cli
conflicts = stegasoo-cli
conflicts = stegasoo
conflicts = stegasoo-git
source = stegasoo-cli-git::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main
sha256sums = SKIP
pkgname = stegasoo-cli-git

69
aur-cli/PKGBUILD Normal file
View File

@@ -0,0 +1,69 @@
# Maintainer: Aaron D. Lee <your-email@example.com>
pkgname=stegasoo-cli-git
pkgver=4.3.0
pkgrel=1
pkgdesc="Secure steganography CLI with hybrid photo + passphrase + PIN authentication"
arch=('x86_64')
url="https://github.com/adlee-was-taken/stegasoo"
license=('MIT')
# Python 3.11-3.14 supported (uses jpeglib for modern Python compatibility)
depends=(
'python>=3.11'
)
makedepends=(
'git'
'python'
'python-build'
'python-hatchling'
)
optdepends=(
'libjpeg-turbo: jpegtran for lossless JPEG rotation (DCT-safe)'
)
provides=('stegasoo-cli')
conflicts=('stegasoo-cli' 'stegasoo' 'stegasoo-git')
install=stegasoo-cli-git.install
source=("${pkgname}::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main")
sha256sums=('SKIP')
pkgver() {
cd "$pkgname"
git describe --long --tags 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || \
printf "%s.r%s.g%s" "4.3.0" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$pkgname"
python -m build --wheel --no-isolation
}
package() {
cd "$pkgname"
# Install to /opt/stegasoo-cli with dedicated venv
install -dm755 "$pkgdir/opt/stegasoo-cli"
# Create fresh venv in package
python -m venv "$pkgdir/opt/stegasoo-cli/venv"
# Install the wheel with CLI + DCT + compression extras (no web/api)
local wheel=$(ls dist/*.whl | head -1)
"$pkgdir/opt/stegasoo-cli/venv/bin/pip" install --no-cache-dir "${wheel}[cli,dct,compression]"
# Fix shebangs - replace build-time paths with installed paths
find "$pkgdir/opt/stegasoo-cli/venv/bin" -type f -exec \
sed -i "s|$pkgdir/opt/stegasoo-cli/venv|/opt/stegasoo-cli/venv|g" {} \;
# Fix pyvenv.cfg
sed -i "s|$pkgdir||g" "$pkgdir/opt/stegasoo-cli/venv/pyvenv.cfg"
# Create symlink to /usr/bin
install -dm755 "$pkgdir/usr/bin"
ln -s /opt/stegasoo-cli/venv/bin/stegasoo "$pkgdir/usr/bin/stegasoo"
# Install license
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
# Install docs
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
}

62
aur-cli/README.md Normal file
View File

@@ -0,0 +1,62 @@
# Stegasoo CLI AUR Package
Lightweight CLI-only package for steganography operations. No web UI or API server.
## Installation
### From AUR (once published)
```bash
yay -S stegasoo-cli-git
# or
paru -S stegasoo-cli-git
```
### Manual build
```bash
git clone https://aur.archlinux.org/stegasoo-cli-git.git
cd stegasoo-cli-git
makepkg -si
```
## What Gets Installed
- `/opt/stegasoo-cli/venv/` - Self-contained Python venv with CLI dependencies only
- `/usr/bin/stegasoo` - CLI executable
## Usage
```bash
# Show all commands
stegasoo --help
# Generate credentials (passphrase + PIN)
stegasoo generate
stegasoo generate --words 5 --pin-length 8
# Generate with RSA keys and QR codes
stegasoo generate --rsa --qr-ascii
# Encode a message
stegasoo encode -i carrier.jpg -r reference.jpg -m "secret message" \
-P "word1 word2 word3 word4" -p 123456
# Decode a message
stegasoo decode -i encoded.png -r reference.jpg \
-P "word1 word2 word3 word4" -p 123456
# Image tools
stegasoo tools --help
stegasoo tools compress image.png
stegasoo tools rotate image.jpg 90
```
## For Web UI or REST API
Install the full package instead:
```bash
yay -S stegasoo-git
```
## Maintainer
Aaron D. Lee

View File

@@ -0,0 +1,20 @@
post_install() {
echo ""
echo "==============================================="
echo " Stegasoo CLI installed successfully!"
echo "==============================================="
echo ""
echo "Usage:"
echo " stegasoo --help # Show all commands"
echo " stegasoo generate # Generate passphrase + PIN"
echo " stegasoo encode ... # Hide data in an image"
echo " stegasoo decode ... # Extract hidden data"
echo " stegasoo tools --help # Image tools (compress, etc.)"
echo ""
echo "For web UI or REST API, install stegasoo-git instead."
echo ""
}
post_upgrade() {
post_install
}

22
aur-cli/test-build.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Test build the AUR CLI package locally
set -e
cd "$(dirname "$0")"
echo "=== Cleaning previous builds ==="
rm -rf stegasoo-cli-git pkg src *.pkg.tar.zst *.whl 2>/dev/null || true
echo "=== Generating .SRCINFO ==="
makepkg --printsrcinfo > .SRCINFO
echo "=== Building package ==="
makepkg -sf
echo "=== Package built ==="
ls -la *.pkg.tar.zst
echo ""
echo "To install: sudo pacman -U stegasoo-cli-git-*.pkg.tar.zst"
echo "To test: makepkg -si"

120
aur/PKGBUILD Normal file
View File

@@ -0,0 +1,120 @@
# Maintainer: Aaron D. Lee <your-email@example.com>
pkgname=stegasoo-git
pkgver=4.3.0
pkgrel=1
pkgdesc="Secure steganography with hybrid photo + passphrase + PIN authentication"
arch=('x86_64')
url="https://github.com/adlee-was-taken/stegasoo"
license=('MIT')
# Python 3.11-3.14 supported (uses jpeglib for modern Python compatibility)
depends=(
'python>=3.11'
'zbar' # QR code reading for Web UI
)
makedepends=(
'git'
'python'
'python-build'
'python-hatchling'
)
provides=('stegasoo')
conflicts=('stegasoo')
install=stegasoo-git.install
source=("${pkgname}::git+https://github.com/adlee-was-taken/stegasoo.git#branch=main")
sha256sums=('SKIP')
pkgver() {
cd "$pkgname"
git describe --long --tags 2>/dev/null | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' || \
printf "%s.r%s.g%s" "4.3.0" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$pkgname"
python -m build --wheel --no-isolation
}
package() {
cd "$pkgname"
# Detect Python version for site-packages path
local pyver=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
# Install to /opt/stegasoo with dedicated venv
install -dm755 "$pkgdir/opt/stegasoo"
# Create fresh venv in package
python -m venv "$pkgdir/opt/stegasoo/venv"
# Install the wheel with all extras
local wheel=$(ls dist/*.whl | head -1)
"$pkgdir/opt/stegasoo/venv/bin/pip" install --no-cache-dir "${wheel}[all]"
# Install frontends (not included in wheel)
local site_packages="$pkgdir/opt/stegasoo/venv/lib/python${pyver}/site-packages"
cp -r frontends "$site_packages/"
# Create writable directories for stegasoo user
install -dm755 "$pkgdir/opt/stegasoo/venv/var/app-instance"
install -dm755 "$site_packages/frontends/web/temp_files"
install -dm755 "$site_packages/frontends/api/temp_files"
# Fix shebangs - replace build-time paths with installed paths
find "$pkgdir/opt/stegasoo/venv/bin" -type f -exec \
sed -i "s|$pkgdir/opt/stegasoo/venv|/opt/stegasoo/venv|g" {} \;
# Fix pyvenv.cfg
sed -i "s|$pkgdir||g" "$pkgdir/opt/stegasoo/venv/pyvenv.cfg"
# Create symlinks to /usr/bin
install -dm755 "$pkgdir/usr/bin"
ln -s /opt/stegasoo/venv/bin/stegasoo "$pkgdir/usr/bin/stegasoo"
# Install license
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
# Install docs
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
# Install systemd service files
install -Dm644 /dev/stdin "$pkgdir/usr/lib/systemd/system/stegasoo-web.service" <<EOF
[Unit]
Description=Stegasoo Web UI
After=network.target
[Service]
Type=simple
User=stegasoo
WorkingDirectory=/opt/stegasoo/venv/lib/python${pyver}/site-packages/frontends/web
Environment="PATH=/opt/stegasoo/venv/bin"
ExecStart=/opt/stegasoo/venv/bin/gunicorn -b 127.0.0.1:5000 app:app
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
install -Dm644 /dev/stdin "$pkgdir/usr/lib/systemd/system/stegasoo-api.service" <<EOF
[Unit]
Description=Stegasoo REST API (HTTPS)
After=network.target
[Service]
Type=simple
User=stegasoo
WorkingDirectory=/opt/stegasoo/venv/lib/python${pyver}/site-packages/frontends/api
Environment="PATH=/opt/stegasoo/venv/bin"
Environment="HOME=/opt/stegasoo"
# TLS enabled by default - certs auto-generated on first run
# Use stegasoo api tls generate to pre-generate certs
# Use stegasoo api keys create <name> to create API keys
ExecStart=/opt/stegasoo/venv/bin/stegasoo api serve --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
}

90
aur/README.md Normal file
View File

@@ -0,0 +1,90 @@
# Stegasoo AUR Package
Full package with CLI, Web UI, and REST API. Supports Python 3.11-3.14.
## Installation
### From AUR (once published)
```bash
yay -S stegasoo-git
# or
paru -S stegasoo-git
```
### Manual build
```bash
git clone https://aur.archlinux.org/stegasoo-git.git
cd stegasoo-git
makepkg -si
```
## What Gets Installed
- `/opt/stegasoo/venv/` - Self-contained Python venv with all dependencies
- `/usr/bin/stegasoo` - CLI symlink
- `/usr/lib/systemd/system/stegasoo-web.service` - Web UI service (port 5000)
- `/usr/lib/systemd/system/stegasoo-api.service` - REST API service (port 8000, HTTPS)
## Optional Dependencies
```bash
# QR code reading from webcam/images (recommended)
sudo pacman -S zbar
```
All other dependencies are bundled in the venv.
## Usage
### CLI
```bash
stegasoo --help
stegasoo generate # Generate passphrase + PIN
stegasoo generate --rsa --qr-ascii # With RSA keys and QR codes
stegasoo encode -i carrier.jpg -r reference.jpg -m "secret" -P "word1 word2 word3 word4" -p 123456
stegasoo decode -i encoded.png -r reference.jpg -P "word1 word2 word3 word4" -p 123456
```
### Web UI
```bash
# Start service (user created automatically on install)
sudo systemctl enable --now stegasoo-web
# Access at http://localhost:5000
```
### REST API
```bash
# Create an API key first
sudo -u stegasoo stegasoo api keys create mykey
# Start service (HTTPS with auto-generated self-signed cert)
sudo systemctl enable --now stegasoo-api
# Access docs at https://localhost:8000/docs
curl -k -H "X-API-Key: YOUR_KEY" https://localhost:8000/
```
### HTTPS Configuration
The API uses HTTPS by default with auto-generated self-signed certificates.
```bash
# View certificate info
stegasoo api tls info
# Generate new self-signed cert
sudo -u stegasoo stegasoo api tls generate
# Use custom certs (edit service file)
sudo systemctl edit stegasoo-api
```
## Alternative Packages
- `stegasoo-cli-git` - CLI only, minimal dependencies
- `stegasoo-api-git` - CLI + REST API, no web UI
## Maintainer
Aaron D. Lee

75
aur/stegasoo-git.install Normal file
View File

@@ -0,0 +1,75 @@
post_install() {
# Create stegasoo system user if it doesn't exist
if ! getent passwd stegasoo >/dev/null; then
useradd -r -s /usr/bin/nologin -d /opt/stegasoo stegasoo
echo "Created system user 'stegasoo'"
fi
# Set ownership of instance directory for Flask
chown -R stegasoo:stegasoo /opt/stegasoo/venv/var/app-instance 2>/dev/null || true
echo ""
echo "==============================================="
echo " Stegasoo installed successfully!"
echo "==============================================="
echo ""
echo "CLI usage:"
echo " stegasoo --help"
echo " stegasoo generate # Generate credentials"
echo " stegasoo encode # Encode a message"
echo " stegasoo decode # Decode a message"
echo ""
echo "-----------------------------------------------"
echo " Web UI Service"
echo "-----------------------------------------------"
echo " Port: 5000 (HTTP)"
echo " Start: sudo systemctl start stegasoo-web"
echo " Enable: sudo systemctl enable stegasoo-web"
echo " Status: sudo systemctl status stegasoo-web"
echo " Access: http://localhost:5000"
echo ""
echo "-----------------------------------------------"
echo " REST API Service"
echo "-----------------------------------------------"
echo " Port: 8000 (HTTPS by default)"
echo " Start: sudo systemctl start stegasoo-api"
echo " Enable: sudo systemctl enable stegasoo-api"
echo " Status: sudo systemctl status stegasoo-api"
echo " Access: https://localhost:8000"
echo ""
echo "-----------------------------------------------"
echo " HTTPS Configuration"
echo "-----------------------------------------------"
echo " The API generates self-signed certs on first run."
echo " To pre-generate or use custom certificates:"
echo ""
echo " # Generate self-signed certs"
echo " sudo -u stegasoo stegasoo api tls generate"
echo ""
echo " # Use custom certs (edit the service file)"
echo " sudo systemctl edit stegasoo-api"
echo " # Add: ExecStart= with --cert and --key flags"
echo ""
echo " # Create API keys for authentication"
echo " sudo -u stegasoo stegasoo api keys create <name>"
echo ""
echo "==============================================="
echo ""
}
post_upgrade() {
post_install
}
pre_remove() {
# Stop services if running
systemctl stop stegasoo-web 2>/dev/null || true
systemctl stop stegasoo-api 2>/dev/null || true
}
post_remove() {
# Optionally remove the stegasoo user
# userdel stegasoo 2>/dev/null || true
echo "Stegasoo removed. User 'stegasoo' was not removed."
echo "To remove: userdel stegasoo"
}

22
aur/test-build.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Test build the AUR package locally
set -e
cd "$(dirname "$0")"
echo "=== Cleaning previous builds ==="
rm -rf stegasoo-git pkg src *.pkg.tar.zst *.whl 2>/dev/null || true
echo "=== Generating .SRCINFO ==="
makepkg --printsrcinfo > .SRCINFO
echo "=== Building package ==="
makepkg -sf
echo "=== Package built ==="
ls -la *.pkg.tar.zst
echo ""
echo "To install: sudo pacman -U stegasoo-git-*.pkg.tar.zst"
echo "To test: makepkg -si"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 17 KiB

BIN
data/WebUI_About.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
data/WebUI_Account.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

BIN
data/WebUI_Login.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
data/WebUI_Recover.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
data/WebUI_Setup.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
data/WebUI_Tools.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,62 +0,0 @@
version: '3.8'
services:
# ============================================================================
# Web UI (Flask)
# ============================================================================
web:
build:
context: .
target: web
container_name: stegasoo-web
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M # Argon2 needs 256MB per operation
reservations:
memory: 256M
# ============================================================================
# REST API (FastAPI)
# ============================================================================
api:
build:
context: .
target: api
container_name: stegasoo-api
ports:
- "8000:8000"
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
# ============================================================================
# Nginx Reverse Proxy (optional, for production)
# ============================================================================
# nginx:
# image: nginx:alpine
# container_name: stegasoo-nginx
# ports:
# - "80:80"
# - "443:443"
# volumes:
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
# - ./certs:/etc/nginx/certs:ro
# depends_on:
# - web
# - api
# restart: unless-stopped
# ============================================================================
# Development overrides
# ============================================================================
# Use: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up

152
docker/Dockerfile Normal file
View File

@@ -0,0 +1,152 @@
# Stegasoo Docker Image
# Uses pre-built base image for fast rebuilds
#
# First time setup:
# docker build -f Dockerfile.base -t stegasoo-base:latest .
#
# Then build normally (fast!):
# docker-compose build
#
# Or if you don't have the base image, this falls back to building deps
# (slow, but works)
# ============================================================================
# ARG to switch between base image and full build
# ============================================================================
ARG USE_BASE_IMAGE=true
# ============================================================================
# Base stage - use pre-built image if available
# ============================================================================
FROM stegasoo-base:latest AS base-prebuilt
FROM python:3.12-slim AS base-full
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_ROOT_USER_ACTION=ignore
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
libc-dev \
libffi-dev \
libzbar0 \
libjpeg-dev \
zlib1g-dev \
curl \
openssl \
&& rm -rf /var/lib/apt/lists/*
# Install ALL dependencies (slow path)
RUN pip install --no-cache-dir \
cython numpy scipy>=1.10.0 jpegio>=0.2.0 \
argon2-cffi>=23.0.0 pillow>=10.0.0 cryptography>=41.0.0 \
reedsolo>=1.7.0 \
flask>=3.0.0 gunicorn>=21.0.0 \
fastapi>=0.100.0 "uvicorn[standard]>=0.20.0" python-multipart>=0.0.6 \
qrcode>=7.3.0 pyzbar>=0.1.9 click>=8.0.0 lz4>=4.0.0
# ============================================================================
# Select which base to use (default: prebuilt)
# ============================================================================
FROM base-prebuilt AS base
# ============================================================================
# Production stage - Web UI
# ============================================================================
FROM base AS web
WORKDIR /app
# Install runtime dependencies (curl for healthcheck, openssl for cert generation)
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
curl openssl \
&& rm -rf /var/lib/apt/lists/*
# Copy application files (this is all that rebuilds normally!)
COPY src/ src/
COPY data/ data/
COPY frontends/web/ frontends/web/
# Create upload directory and instance directories (for volumes)
# temp_files is for multi-worker temp file sharing
RUN mkdir -p /tmp/stego_uploads /app/frontends/web/instance /app/frontends/web/certs /app/frontends/web/temp_files
# Copy and set up entrypoint (before switching to non-root user)
COPY frontends/web/docker-entrypoint.sh /app/frontends/web/
RUN chmod +x /app/frontends/web/docker-entrypoint.sh
# Create non-root user
RUN useradd -m -u 1000 stego && chown -R stego:stego /app /tmp/stego_uploads
USER stego
# Set Python path
ENV PYTHONPATH=/app/src
# Expose port
EXPOSE 5000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD curl -fsk https://localhost:5000/ || curl -fs http://localhost:5000/ || exit 1
# Run with entrypoint (handles HTTPS/HTTP mode)
WORKDIR /app/frontends/web
ENTRYPOINT ["/app/frontends/web/docker-entrypoint.sh"]
# ============================================================================
# API stage - REST API
# ============================================================================
FROM base AS api
WORKDIR /app
# Copy application files
COPY src/ src/
COPY data/ data/
COPY frontends/api/ frontends/api/
# Create non-root user
RUN useradd -m -u 1000 stego && chown -R stego:stego /app
USER stego
# Set Python path
ENV PYTHONPATH=/app/src
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/')" || exit 1
# Run with uvicorn
WORKDIR /app/frontends/api
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# ============================================================================
# CLI stage - Command line tool
# ============================================================================
FROM base AS cli
WORKDIR /app
# Copy application files
COPY src/ src/
COPY data/ data/
COPY frontends/cli/ frontends/cli/
# Create non-root user
RUN useradd -m -u 1000 stego && chown -R stego:stego /app
USER stego
# Set Python path
ENV PYTHONPATH=/app/src
# Default to help
WORKDIR /app/frontends/cli
ENTRYPOINT ["python", "main.py"]
CMD ["--help"]

57
docker/Dockerfile.base Normal file
View File

@@ -0,0 +1,57 @@
# Stegasoo Base Image
# Contains all slow-to-compile dependencies (jpegio, scipy, argon2)
# Build once: docker build -f Dockerfile.base -t stegasoo-base:latest .
# Push to registry for team use: docker push yourregistry/stegasoo-base:latest
FROM python:3.12-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_ROOT_USER_ACTION=ignore
# Install system dependencies
# NOTE: g++ is required for jpegio C++ compilation
# NOTE: libjpeg-dev is required for jpegio
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
libc-dev \
libffi-dev \
libzbar0 \
libjpeg-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# Install the slow-to-compile packages
# These rarely change, so they get cached in this base image
RUN pip install --no-cache-dir \
cython \
numpy \
scipy>=1.10.0 \
jpegio>=0.2.0 \
argon2-cffi>=23.0.0 \
pillow>=10.0.0 \
cryptography>=41.0.0 \
reedsolo>=1.7.0 \
zstandard>=0.22.0
# Install web/api framework packages (also stable)
RUN pip install --no-cache-dir \
flask>=3.0.0 \
gunicorn>=21.0.0 \
fastapi>=0.100.0 \
"uvicorn[standard]>=0.20.0" \
python-multipart>=0.0.6 \
qrcode>=7.3.0 \
pyzbar>=0.1.9 \
click>=8.0.0 \
lz4>=4.0.0
# Verify key packages work
RUN python -c "import jpegio; import scipy; import numpy; import zstandard; print('jpegio + scipy + numpy + zstd OK')"
# Label for tracking
LABEL org.opencontainers.image.title="Stegasoo Base"
LABEL org.opencontainers.image.description="Pre-compiled dependencies for Stegasoo"
LABEL org.opencontainers.image.version="4.2.1"

64
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,64 @@
# Shared environment variables
x-common-env: &common-env
STEGASOO_CHANNEL_KEY: ${STEGASOO_CHANNEL_KEY:-}
services:
# ============================================================================
# Web UI (Flask)
# ============================================================================
web:
build:
context: ..
dockerfile: docker/Dockerfile
target: web
container_name: stegasoo-web
ports:
- "5000:5000"
environment:
<<: *common-env
FLASK_ENV: production
# Authentication (v4.0.2)
STEGASOO_AUTH_ENABLED: ${STEGASOO_AUTH_ENABLED:-true}
# HTTPS enabled by default - generates self-signed cert if none provided
# To disable: STEGASOO_HTTPS_ENABLED=false docker-compose up
STEGASOO_HTTPS_ENABLED: ${STEGASOO_HTTPS_ENABLED:-true}
STEGASOO_HOSTNAME: ${STEGASOO_HOSTNAME:-localhost}
volumes:
# Persist auth database and SSL certs (v4.0.2)
- stegasoo-web-data:/app/frontends/web/instance
- stegasoo-web-certs:/app/frontends/web/certs
restart: unless-stopped
deploy:
resources:
limits:
memory: 2048M
reservations:
memory: 1024M
# ============================================================================
# REST API (FastAPI)
# ============================================================================
api:
build:
context: ..
dockerfile: docker/Dockerfile
target: api
container_name: stegasoo-api
ports:
- "8000:8000"
environment:
<<: *common-env
restart: unless-stopped
deploy:
resources:
limits:
memory: 2048M
reservations:
memory: 1024M
# Named volumes for persistent data
volumes:
stegasoo-web-data:
driver: local
stegasoo-web-certs:
driver: local

224
docs/CLAUDE_WORKTREES.md Normal file
View File

@@ -0,0 +1,224 @@
# Using Claude Code with Git Worktrees — A Stegasoo Guide
## What is a worktree?
A git worktree is a second (or third, or fourth...) copy of your repo that shares the same `.git` history but lives in its own folder with its own branch. Think of it like opening the same project in a parallel universe — you can hack on a feature in one worktree while keeping `main` pristine in another.
Claude Code has built-in worktree support, so you don't need to memorize any git commands.
## Why bother?
- **Safety net**: Your `main` branch stays untouched. If Claude's changes go sideways, just delete the worktree — zero damage.
- **Easy A/B comparison**: Keep the original code open in one editor tab, Claude's changes in another.
- **Parallel work**: You can keep working in `main` while Claude tinkers in a worktree.
- **Clean PRs**: The worktree branch becomes your PR branch with no stray changes mixed in.
## The 30-second version
1. Ask Claude to work in a worktree
2. Claude creates an isolated copy and works there
3. When done, you either merge or throw it away
That's it. Everything below is details.
---
## How to start a worktree session
### Option A: Ask Claude directly
Just tell Claude you want to work in a worktree:
```
> Let's work in a worktree for this
> Start a worktree called "dct-refactor"
> Can you make these changes in an isolated worktree?
```
Claude will use `EnterWorktree` behind the scenes and switch into it automatically.
### Option B: Use the slash command
```
> /worktree
```
This drops you into a fresh worktree immediately.
### Option C: Tell Claude to launch an agent in a worktree
If you want Claude to do something in the background without touching your working directory:
```
> Run the tests in a worktree so we don't mess up my local state
```
Claude can spin up a sub-agent with `isolation: "worktree"` — it gets its own copy and reports back.
---
## Where do worktrees live?
Claude puts them in:
```
.claude/worktrees/<name>/
```
This directory is inside your repo but ignored by git, so it won't pollute your commits.
## What happens inside a worktree?
The worktree is a full checkout of your repo on a new branch. Claude's working directory switches to it, so all file reads, edits, and commands happen there — not in your main checkout.
**Important for Stegasoo**: The first thing you (or Claude) should do in a fresh worktree is:
```bash
pip install -e ".[dev]"
```
This points your editable install at the worktree's source code instead of your main checkout. Without this, `pytest` will test the wrong copy of the code.
---
## Real-world examples
### Example 1: Feature work
```
You: I want to add lz4 as a default compression option. Let's use a worktree.
Claude: *creates worktree, switches to it*
Claude: *installs dev deps, makes changes, runs tests*
Claude: All tests pass. Ready to merge or open a PR.
You: Looks good, make a PR.
Claude: *pushes branch, creates PR*
```
### Example 2: Risky refactor
```
You: Refactor the crypto module to split KDF logic into its own file.
Do it in a worktree so I can review before touching main.
Claude: *creates worktree "refactor/split-kdf"*
Claude: *does the refactor, runs tests*
You: Hmm, I don't love the approach. Throw it away.
Claude: *removes worktree — main is untouched*
```
### Example 3: Investigate a bug without side effects
```
You: Something's wrong with DCT encoding on large images.
Can you investigate in a worktree? I've got uncommitted work here.
Claude: *creates worktree, adds debug logging, runs tests*
Claude: Found it — the block size calculation overflows at >16MP.
Here's the fix. Want me to apply it to main?
```
---
## When to use a worktree vs. just editing in place
| Situation | Worktree? | Why |
|-----------|-----------|-----|
| Quick one-file fix | No | Overkill — just edit directly |
| Multi-file refactor | Yes | Easy to discard if it goes wrong |
| Touching security-critical code (`crypto.py`, `steganography.py`, etc.) | Yes | Extra safety for sensitive changes |
| Experimental / "let's try this" work | Yes | Zero-cost throwaway |
| You have uncommitted changes you don't want to stash | Yes | Worktree won't touch your working tree |
| Adding a single test | No | Low risk, just do it |
---
## Cleaning up
### If you merged or created a PR
The worktree served its purpose. Clean up:
```bash
git worktree remove .claude/worktrees/<name>
```
Or ask Claude:
```
> Clean up the worktree
```
### If you want to throw everything away
Same command — removing the worktree deletes the directory and its branch reference. Your `main` branch is completely unaffected.
### If Claude's session ends
When a Claude Code session ends while in a worktree, you'll be prompted to keep or remove it. If you keep it, you can resume later:
```bash
cd .claude/worktrees/<name>
# pick up where you left off
```
---
## Branch naming in worktrees
Follow the same conventions as the rest of the project:
| Type | Branch name | Example |
|------|-------------|---------|
| Feature | `feature/description` | `feature/batch-progress-bars` |
| Bug fix | `fix/description` | `fix/dct-overflow-large-images` |
| Docs | `docs/description` | `docs/api-examples` |
| Refactor | `refactor/description` | `refactor/split-crypto-module` |
When Claude creates a worktree automatically, it generates a random branch name. You can rename it before pushing:
```bash
git branch -m <old-name> feature/my-better-name
```
---
## Troubleshooting
### "I ran pytest but it's testing the old code"
You forgot to install in the worktree:
```bash
pip install -e ".[dev]"
```
### "I can't find my worktree"
```bash
git worktree list
```
This shows all worktrees and their paths.
### "I accidentally deleted the worktree folder without removing it from git"
```bash
git worktree prune
```
This cleans up stale worktree references.
### "I want to switch back to my main checkout"
If you're in a Claude Code session that entered a worktree, the session stays in the worktree until it ends. Start a new session to go back to your main checkout, or:
```bash
cd /home/alee/Sources/stegasoo
```
---
## TL;DR
1. Say "use a worktree" when asking Claude to make changes
2. Claude works in an isolated copy — your `main` is safe
3. Merge the good stuff, trash the bad stuff
4. Never think about it again until next time

162
docs/DOCKER_QUICKSTART.md Normal file
View File

@@ -0,0 +1,162 @@
# Docker Quickstart
Get Stegasoo running in Docker in under 5 minutes.
## Build
```bash
# From project root:
# Build web UI image
sudo docker build -t stegasoo-web --target web -f docker/Dockerfile .
# Or build all targets
sudo docker build -t stegasoo-api --target api -f docker/Dockerfile .
sudo docker build -t stegasoo-cli --target cli -f docker/Dockerfile .
# Or use docker-compose
sudo docker-compose -f docker/docker-compose.yml build
```
## Run (Basic)
```bash
# HTTP only, no auth
sudo docker run -d \
-p 5000:5000 \
-e STEGASOO_AUTH_ENABLED=false \
--name stegasoo \
stegasoo-web
```
Visit http://localhost:5000
## Run (Production)
```bash
# HTTPS + Auth + Channel Key
sudo docker run -d \
-p 5000:5000 \
-e STEGASOO_AUTH_ENABLED=true \
-e STEGASOO_HTTPS_ENABLED=true \
-e STEGASOO_HOSTNAME=stegasoo.local \
-e STEGASOO_CHANNEL_KEY=ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456 \
-v stegasoo-data:/opt/stegasoo/frontends/web/instance \
-v stegasoo-certs:/opt/stegasoo/frontends/web/certs \
--name stegasoo \
stegasoo-web
```
Visit https://localhost:5000 (accept self-signed cert warning)
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `STEGASOO_AUTH_ENABLED` | `true` | Require login |
| `STEGASOO_HTTPS_ENABLED` | `false` | Enable HTTPS |
| `STEGASOO_HOSTNAME` | `localhost` | Hostname for SSL cert |
| `STEGASOO_CHANNEL_KEY` | *(none)* | Shared channel key (32 alphanumeric chars with dashes) |
## Docker Compose
Create `.env` file in project root:
```bash
STEGASOO_AUTH_ENABLED=true
STEGASOO_HTTPS_ENABLED=true
STEGASOO_HOSTNAME=stegasoo.local
STEGASOO_CHANNEL_KEY=
```
Run:
```bash
sudo docker-compose -f docker/docker-compose.yml up -d web
```
## Custom SSL Certificates
### Use Your Own Certs
```bash
# Stop container
sudo docker stop stegasoo
# Copy certs to volume
sudo docker run --rm -v stegasoo-certs:/certs -v $(pwd):/src alpine \
sh -c "cp /src/your-cert.crt /certs/server.crt && cp /src/your-key.key /certs/server.key && chmod 600 /certs/server.key"
# Start container
sudo docker start stegasoo
```
### Use mkcert (Local Development)
```bash
# Install mkcert
brew install mkcert # macOS
# or: sudo apt install mkcert # Debian/Ubuntu
# Create local CA and certs
mkcert -install
mkcert -cert-file server.crt -key-file server.key localhost 127.0.0.1 stegasoo.local
# Copy to Docker volume (see above)
```
### Use Let's Encrypt (Public Server)
```bash
# Get cert
sudo certbot certonly --standalone -d yourdomain.com
# Copy to Docker volume
sudo docker run --rm -v stegasoo-certs:/certs alpine \
sh -c "cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem /certs/server.crt && \
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem /certs/server.key && \
chmod 600 /certs/server.key"
```
## Volumes
| Volume | Purpose |
|--------|---------|
| `stegasoo-data` | User database, settings |
| `stegasoo-certs` | SSL certificates |
## Smoke Test
```bash
# Check container logs
sudo docker logs stegasoo
# Test HTTP endpoint
curl -k https://localhost:5000/health
# Expected: {"status":"ok","version":"4.1.7",...}
```
## Troubleshooting
**Container won't start:**
```bash
sudo docker logs stegasoo
```
**Out of memory:**
```bash
# Argon2 needs 256MB+ per operation
sudo docker run --memory=768m ...
```
**Certificate errors:**
```bash
# Regenerate self-signed cert
sudo docker exec stegasoo rm -rf /opt/stegasoo/frontends/web/certs/*
sudo docker restart stegasoo
```
**Reset everything:**
```bash
sudo docker stop stegasoo && sudo docker rm stegasoo
sudo docker volume rm stegasoo-data stegasoo-certs
```

361
docs/TEMPLATES.md Normal file
View File

@@ -0,0 +1,361 @@
# Stegasoo Web Templates Specification
Quick reference for all Jinja2 templates in `frontends/web/templates/`.
## Table of Contents
- [Layout](#layout)
- [Auth & Setup](#auth--setup)
- [Core Features](#core-features)
- [Tools & Account](#tools--account)
- [Admin](#admin)
---
## Layout
### `base.html`
**Purpose:** Master layout template - all pages extend this.
| Block | Description |
|-------|-------------|
| `{% block title %}` | Page title |
| `{% block content %}` | Main page content |
| `{% block scripts %}` | Page-specific JS |
**Key Elements:**
- `nav.navbar` - Bootstrap 5 navbar with logo, links, auth buttons
- `div.toast-container` - Flash message toasts (10s auto-dismiss)
- `main.container` - Content wrapper
- `footer` - Copyright + version
**Variables:** `is_authenticated`, `username`, `is_admin`
---
## Auth & Setup
### `login.html`
**Route:** `/login`
**Form:** `POST /login`
- `username` - text input
- `password` - password input
- "Forgot password?" link to `/recover`
**JS:** `static/js/auth.js` - password toggle
---
### `setup.html`
**Route:** `/setup` (first-run only)
**Form:** `POST /setup`
- `username` - admin username
- `password` - password (min 8 chars)
- `password_confirm` - confirmation
**JS:** Password confirmation validation
---
### `setup_recovery.html`
**Route:** `/setup/recovery`
**Form:** `POST /setup/recovery`
- `recovery_key` - hidden, pre-generated
- `action` - "save" or "skip"
- Checkbox confirmation required for save
**Features:**
- Recovery key display (readonly input)
- Copy to clipboard button
- QR code image (if available)
- Download options: text file, QR image
- Stego backup upload form
---
### `recover.html`
**Route:** `/recover`
**Form:** `POST /recover`
- `recovery_key` - textarea for key input
- `new_password` - new password
- `new_password_confirm` - confirmation
**Accordion:** "Extract from stego backup"
- `POST /recover/stego` with `stego_image` + `reference_image`
- Pre-fills recovery key on success
---
### `regenerate_recovery.html`
**Route:** `/account/recovery/regenerate` (admin only)
**Form:** `POST /account/recovery/regenerate`
- `recovery_key` - hidden field
- `action` - "save" or "cancel"
- Confirmation checkbox
**Features:**
- New key display
- QR code (obfuscated)
- Download: text, QR, stego backup
- Warning if replacing existing key
---
## Core Features
### `index.html`
**Route:** `/`
**Structure:**
- Hero section with tagline
- 3 action cards: Encode, Decode, Generate
- "How It Works" explainer section
---
### `generate.html`
**Route:** `/generate`
**Form:** `POST /generate`
- `words` - passphrase word count (3-12)
- `use_pin` - checkbox
- `pin_length` - PIN digits (6-9)
- `use_rsa` - checkbox
- `rsa_bits` - key size (2048/3072)
**Output panels:**
- Passphrase display
- PIN display (if enabled)
- RSA key + QR (if enabled)
- Entropy calculator
**JS:** `static/js/generate.js`
---
### `encode.html`
**Route:** `/encode`
**Form:** `POST /encode` (multipart)
- `reference_photo` - file upload (drag-drop zone)
- `carrier_image` - file upload (drag-drop zone)
- `mode` - radio: DCT (default) / LSB
- `dct_format` - PNG / JPEG
- `dct_color` - Color / Grayscale
- `payload_type` - radio: Text / File
- `message` - textarea (if text)
- `embed_file` - file input (if file)
- `passphrase` - text input
- `pin` - text input
- `rsa_key` / `rsa_key_qr` - file inputs
- `rsa_key_password` - password
- `channel_key` - select (saved keys) or manual input
**Panels:**
- Reference preview with "Hash Acquired" status
- Carrier preview with capacity info
- Character counter for message
**JS:** `static/js/encode.js`, `static/js/stegasoo.js`
---
### `encode_result.html`
**Route:** `/encode/result/<file_id>`
**Elements:**
- Success message
- Stego image preview
- Download button
- Share button (Web Share API)
- Mode/capacity info
- "Encode Another" link
**Variables:** `file_id`, `filename`, `mode`, `capacity_used`
---
### `decode.html`
**Route:** `/decode`
**Form:** `POST /decode` (multipart)
- `reference_photo` - file upload
- `stego_image` - file upload
- `passphrase` - text input
- `pin` - text input
- `rsa_key` / `rsa_key_qr` - file inputs
- `rsa_key_password` - password
- `channel_key` - select or manual
**Output:**
- Decoded message display
- File download (if file payload)
**JS:** `static/js/decode.js`, `static/js/stegasoo.js`
---
## Tools & Account
### `tools.html`
**Route:** `/tools`
**Tabbed interface:**
| Tab | Endpoint | Description |
|-----|----------|-------------|
| Capacity | `POST /api/tools/capacity` | Image capacity analysis |
| Peek | `POST /api/tools/peek` | Check for Stegasoo header |
| Strip | `POST /api/tools/strip` | Remove hidden data |
| EXIF | `POST /api/tools/exif/*` | Metadata viewer/editor |
**EXIF Editor features:**
- Upload image → view all EXIF fields
- Inline editing (click field to edit)
- "Clear All" button
- "Save" / "Download" buttons
**JS:** `static/js/tools.js`
---
### `account.html`
**Route:** `/account`
**Sections:**
1. **User Info** - Username, role badge, logout link
2. **Recovery Key** (admin only)
- Status: Configured / Not Set
- Generate/Regenerate button
- Disable button
3. **Password Change**
- `current_password`
- `new_password`
- `new_password_confirm`
4. **Saved Channel Keys**
- List of saved keys with edit/delete
- "Add Key" form (name + key)
- Max 10 keys per user
**Variables:** `username`, `is_admin`, `has_recovery`, `channel_keys`
---
### `about.html`
**Route:** `/about`
**Sections:**
- Version info + feature badges
- Security model explanation
- Channel key QR (if configured)
- Dependency status table
- Credits + links
**Variables:** `version`, `has_dct`, `has_qr_write`, `has_qr_read`, `channel_key`, `channel_qr`
---
## Admin
### `admin/users.html`
**Route:** `/admin/users`
**Table columns:** Username | Role | Created | Actions
**Actions per user:**
- Reset Password button
- Delete button (disabled for self)
**Header:**
- User count: "X of 16 users"
- "Add User" button (modal trigger)
**Modal:** Add User form
- `username` input
- `role` select (admin/user)
- Auto-generated temp password display
---
### `admin/user_new.html`
**Route:** `/admin/users/new`
**Form:** `POST /admin/users/new`
- `username` - text input
- `role` - select (user/admin)
Redirects to `user_created.html` on success.
---
### `admin/user_created.html`
**Route:** `/admin/users/created`
**Display:**
- Success message
- Username
- Temporary password (copy button)
- "User must change password on first login" notice
- Back to users link
---
### `admin/password_reset.html`
**Route:** `/admin/users/<id>/password-reset`
**Display:**
- Success message
- New temporary password
- Copy button
- Back link
---
## Common Patterns
### Drag-Drop Upload Zones
```html
<div class="upload-zone" id="referenceZone">
<input type="file" name="reference_photo" accept="image/*">
<div class="preview"></div>
<div class="status"></div>
</div>
```
### Password Toggle
```html
<div class="input-group">
<input type="password" id="passwordInput">
<button onclick="togglePassword('passwordInput', this)">
<i class="bi bi-eye"></i>
</button>
</div>
```
### Toast Flash Messages
Rendered in `base.html`, auto-dismiss after 10 seconds:
- `success` → green
- `warning` → yellow
- `error` → red
---
## External JS Files
| File | Used By |
|------|---------|
| `static/js/stegasoo.js` | encode, decode, about |
| `static/js/auth.js` | login, setup, recover, account |
| `static/js/generate.js` | generate |
| `static/js/encode.js` | encode |
| `static/js/decode.js` | decode |
| `static/js/tools.js` | tools |

418
docs/stegasoo.1 Normal file
View File

@@ -0,0 +1,418 @@
.\" Stegasoo man page
.\" Generate with: groff -man -Tascii stegasoo.1
.TH STEGASOO 1 "February 2026" "Stegasoo 4.3.0" "User Commands"
.SH NAME
stegasoo \- steganography with hybrid authentication
.SH SYNOPSIS
.B stegasoo
[\fB\-v\fR|\fB\-\-version\fR]
[\fB\-\-json\fR]
[\fB\-h\fR|\fB\-\-help\fR]
.I command
[\fIargs\fR]
.SH DESCRIPTION
.B stegasoo
hides messages and files in images and audio using PIN + passphrase security.
It uses LSB (Least Significant Bit) steganography with optional DCT
(Discrete Cosine Transform) encoding for JPEG resilience, and supports
audio steganography with LSB and Spread Spectrum modes.
.PP
Messages are encrypted using a hybrid authentication scheme that combines
a reference photo (shared secret), passphrase, and PIN code.
.SH GLOBAL OPTIONS
.TP
.BR \-v ", " \-\-version
Show version and exit.
.TP
.B \-\-json
Output results as JSON (where supported).
.TP
.BR \-h ", " \-\-help
Show help message and exit.
.SH COMMANDS
.SS encode
Encode a message or file into an image.
.PP
.B stegasoo encode
.I carrier
.B \-r
.I reference
[\fB\-m\fR \fImessage\fR | \fB\-f\fR \fIfile\fR]
[\fIoptions\fR]
.TP
.BR \-r ", " \-\-reference " " \fIPATH\fR
Reference photo (shared secret). Required.
.TP
.BR \-m ", " \-\-message " " \fITEXT\fR
Message to encode.
.TP
.BR \-f ", " \-\-file " " \fIPATH\fR
File to embed instead of message.
.TP
.BR \-o ", " \-\-output " " \fIPATH\fR
Output image path.
.TP
.B \-\-passphrase " " \fITEXT\fR
Passphrase (recommend 4+ words). Prompts if not provided.
.TP
.B \-\-pin " " \fITEXT\fR
PIN code. Prompts if not provided.
.TP
.B \-\-dry\-run
Show capacity usage without encoding.
.PP
.B Examples:
.nf
stegasoo encode photo.png -r ref.jpg -m "Secret" --passphrase --pin
stegasoo encode photo.png -r ref.jpg -f doc.pdf -o encoded.png
.fi
.SS decode
Decode a message or file from an image.
.PP
.B stegasoo decode
.I image
.B \-r
.I reference
[\fIoptions\fR]
.TP
.BR \-r ", " \-\-reference " " \fIPATH\fR
Reference photo (shared secret). Required.
.TP
.B \-\-passphrase " " \fITEXT\fR
Passphrase. Prompts if not provided.
.TP
.B \-\-pin " " \fITEXT\fR
PIN code. Prompts if not provided.
.TP
.BR \-o ", " \-\-output " " \fIPATH\fR
Output path for file payloads.
.PP
.B Examples:
.nf
stegasoo decode encoded.png -r ref.jpg --passphrase --pin
stegasoo decode encoded.png -r ref.jpg -o ./extracted/
.fi
.SS generate
Generate random credentials (passphrase + PIN + optional channel key).
.PP
.B stegasoo generate
[\fIoptions\fR]
.TP
.B \-\-words " " \fIINTEGER\fR
Number of words in passphrase (default: 4).
.TP
.B \-\-pin\-length " " \fIINTEGER\fR
PIN length (default: 6).
.TP
.B \-\-channel\-key
Also generate a 256-bit channel key.
.PP
.B Examples:
.nf
stegasoo generate
stegasoo generate --words 6 --pin-length 8
stegasoo generate --channel-key
.fi
.SS info
Show version, features, and system information.
.PP
.B stegasoo info
[\fB\-\-full\fR]
.TP
.B \-\-full
Show full system information (CPU, temperature, disk on Pi).
.SS batch
Batch operations on multiple images.
.PP
.B stegasoo batch
.I subcommand
[\fIargs\fR]
.TP
.B batch encode
Encode message into multiple images.
.RS
.PP
.B stegasoo batch encode
.I images...
[\fB\-m\fR \fImessage\fR | \fB\-f\fR \fIfile\fR]
[\fIoptions\fR]
.PP
Options: \fB\-m\fR, \fB\-f\fR, \fB\-o\fR/\fB\-\-output\-dir\fR, \fB\-\-suffix\fR, \fB\-\-passphrase\fR, \fB\-\-pin\fR,
\fB\-r\fR/\fB\-\-recursive\fR, \fB\-j\fR/\fB\-\-jobs\fR, \fB\-v\fR/\fB\-\-verbose\fR.
.RE
.TP
.B batch decode
Decode messages from multiple images.
.RS
.PP
.B stegasoo batch decode
.I images...
[\fIoptions\fR]
.PP
Options: \fB\-o\fR/\fB\-\-output\-dir\fR, \fB\-\-passphrase\fR, \fB\-\-pin\fR, \fB\-r\fR/\fB\-\-recursive\fR,
\fB\-j\fR/\fB\-\-jobs\fR, \fB\-v\fR/\fB\-\-verbose\fR.
.RE
.TP
.B batch check
Check capacity of multiple images.
.RS
.PP
.B stegasoo batch check
.I images...
[\fB\-r\fR/\fB\-\-recursive\fR]
.RE
.SS channel
Manage channel keys for deployment isolation.
.PP
Channel keys bind encode/decode operations to a specific group or deployment.
Messages encoded with one channel key can only be decoded by systems with
the same channel key.
.PP
.B stegasoo channel
.I subcommand
[\fIargs\fR]
.TP
.B channel generate
Generate a new random channel key.
.RS
.PP
Options: \fB\-\-save\fR (project config), \fB\-\-save\-user\fR (user config).
.RE
.TP
.B channel show
Show the current channel key.
.RS
.PP
Options: \fB\-\-key\fR \fITEXT\fR (show specific key instead).
.RE
.TP
.B channel qr
Display channel key as QR code.
.RS
.PP
Options: \fB\-\-key\fR \fITEXT\fR, \fB\-\-format\fR [\fIascii\fR|\fIpng\fR], \fB\-o\fR/\fB\-\-output\fR \fIPATH\fR.
.RE
.TP
.B channel status
Show channel key status and configuration.
.TP
.B channel clear
Remove channel key configuration.
.RS
.PP
Options: \fB\-\-project\fR, \fB\-\-user\fR.
.RE
.SS admin
Web UI administration commands.
.PP
.B stegasoo admin
.I subcommand
[\fIargs\fR]
.TP
.B admin generate\-key
Generate a new recovery key (for reference only).
.RS
.PP
Options: \fB\-\-qr\fR (show QR code in terminal).
.RE
.TP
.B admin recover
Reset admin password using recovery key.
.RS
.PP
Options: \fB\-\-db\fR \fIPATH\fR (path to stegasoo.db), \fB\-\-password\fR \fITEXT\fR.
.RE
.SS audio\-encode
Encode a message or file into an audio file.
.PP
.B stegasoo audio\-encode
.I audio
.B \-r
.I reference
[\fB\-m\fR \fImessage\fR | \fB\-f\fR \fIfile\fR]
[\fIoptions\fR]
.TP
.BR \-r ", " \-\-reference " " \fIPATH\fR
Reference photo (shared secret). Required.
.TP
.BR \-m ", " \-\-message " " \fITEXT\fR
Message to encode.
.TP
.BR \-f ", " \-\-file " " \fIPATH\fR
File to embed instead of message.
.TP
.BR \-o ", " \-\-output " " \fIPATH\fR
Output audio path.
.TP
.B \-\-passphrase " " \fITEXT\fR
Passphrase (recommend 4+ words). Prompts if not provided.
.TP
.B \-\-pin " " \fITEXT\fR
PIN code. Prompts if not provided.
.TP
.B \-\-mode " " [\fIlsb\fR|\fIspread\fR]
Embedding mode: lsb (default) or spread (spread spectrum).
.PP
.B Examples:
.nf
stegasoo audio-encode song.wav -r ref.jpg -m "Secret" --passphrase --pin
stegasoo audio-encode podcast.mp3 -r ref.jpg -f doc.pdf --mode spread
.fi
.SS audio\-decode
Decode a message or file from a stego audio file.
.PP
.B stegasoo audio\-decode
.I audio
.B \-r
.I reference
[\fIoptions\fR]
.TP
.BR \-r ", " \-\-reference " " \fIPATH\fR
Reference photo (shared secret). Required.
.TP
.B \-\-passphrase " " \fITEXT\fR
Passphrase. Prompts if not provided.
.TP
.B \-\-pin " " \fITEXT\fR
PIN code. Prompts if not provided.
.TP
.BR \-o ", " \-\-output " " \fIPATH\fR
Output path for file payloads.
.PP
.B Examples:
.nf
stegasoo audio-decode stego.wav -r ref.jpg --passphrase --pin
stegasoo audio-decode stego.wav -r ref.jpg -o ./extracted/
.fi
.SS audio\-info
Display audio file information and steganographic capacity.
.PP
.B stegasoo audio\-info
.I audio
[\fB\-\-json\fR]
.PP
Shows format, sample rate, channels, bit depth, duration, and embedding
capacity for both LSB and Spread Spectrum modes.
.PP
.B Examples:
.nf
stegasoo audio-info song.wav
stegasoo audio-info podcast.mp3 --json
.fi
.SS tools
Image security tools.
.PP
.B stegasoo tools
.I subcommand
[\fIargs\fR]
.TP
.B tools capacity
Show steganography capacity for an image.
.RS
.PP
.B stegasoo tools capacity
.I image
[\fB\-\-json\fR]
.RE
.TP
.B tools exif
View or edit EXIF metadata.
.RS
.PP
.B stegasoo tools exif
.I image
[\fB\-\-clear\fR] [\fB\-\-set\fR \fIFIELD=VALUE\fR] [\fB\-o\fR \fIPATH\fR] [\fB\-\-json\fR]
.RE
.TP
.B tools peek
Check if image contains Stegasoo hidden data.
.RS
.PP
.B stegasoo tools peek
.I image
[\fB\-\-json\fR]
.RE
.TP
.B tools strip
Strip EXIF/metadata from an image.
.RS
.PP
.B stegasoo tools strip
.I image
[\fB\-o\fR \fIPATH\fR] [\fB\-\-format\fR [\fIpng\fR|\fIbmp\fR]]
.RE
.SH ENVIRONMENT
.TP
.B STEGASOO_CHANNEL_KEY
Channel key for encode/decode operations. Overrides config file settings.
.TP
.B STEGASOO_HTTPS_ENABLED
Enable HTTPS for web UI (Docker/service mode).
.TP
.B STEGASOO_HOSTNAME
Hostname for SSL certificate generation.
.SH FILES
.TP
.I ~/.stegasoo/channel.key
User's channel key configuration (encrypted).
.TP
.I .stegasoo.toml
Project-level configuration file.
.TP
.I frontends/web/instance/stegasoo.db
Web UI SQLite database (accounts, settings).
.SH EXAMPLES
.SS Basic encode/decode workflow
.nf
# Generate credentials
stegasoo generate
# Encode a secret message
stegasoo encode vacation.png -r selfie.jpg -m "Meet at noon"
# Decode the message (on another system with same reference photo)
stegasoo decode vacation_steg.png -r selfie.jpg
.fi
.SS Using channel keys for team isolation
.nf
# Generate and save a channel key
stegasoo channel generate --save-user
# Share the key with your team
stegasoo channel qr -o team-key.png
# Now all encode/decode operations use this channel
stegasoo encode photo.png -r ref.jpg -m "Team secret"
.fi
.SS Batch processing
.nf
# Check capacity of all PNGs in a directory
stegasoo batch check ./photos/*.png
# Encode same message into multiple images
stegasoo batch encode ./photos/ -r ref.jpg -m "Secret" -o ./encoded/
.fi
.SH SECURITY
Stegasoo uses multiple layers of security:
.IP \(bu 2
Reference photo provides a visual shared secret
.IP \(bu 2
Passphrase (recommend 4+ words) for strong encryption
.IP \(bu 2
PIN code adds additional entropy
.IP \(bu 2
Channel keys isolate different deployments
.IP \(bu 2
AES-256 encryption for payload data
.PP
For maximum security, share the reference photo out-of-band (in person,
secure messenger) and use a strong passphrase.
.SH SEE ALSO
.BR openssl (1),
.BR qrencode (1)
.SH BUGS
Report bugs at: https://github.com/adlee-was-taken/stegasoo/issues
.SH AUTHOR
Written by the Stegasoo contributors.
.SH COPYRIGHT
Copyright \(co 2024-2026. MIT License.

48
examples/README.md Normal file
View File

@@ -0,0 +1,48 @@
# Stegasoo Examples
This directory contains example scripts demonstrating how to use Stegasoo.
## Prerequisites
Install Stegasoo first:
```bash
pip install stegasoo
# Or for development:
pip install -e ".[all]"
```
## Examples
### basic_usage.py
Basic encode/decode workflow with a text message.
```bash
python basic_usage.py
```
### embed_file.py
Embed and extract files (documents, images, etc.) inside carrier images.
```bash
python embed_file.py
```
### channel_keys.py
Use channel keys to create private communication channels for groups.
```bash
python channel_keys.py
```
## Test Images
You'll need to provide your own images:
- `my_secret_photo.png` - Your reference photo (keep this secret!)
- `carrier.png` - The image that will carry your hidden message
For testing, you can use any PNG or BMP image. JPEG carriers are supported with DCT mode.

59
examples/basic_usage.py Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Basic Stegasoo Usage Example
This example demonstrates how to encode and decode a secret message
using the Stegasoo library.
"""
from pathlib import Path
import stegasoo
def main():
# Load your images
# The reference photo is your "key" - keep it secret!
reference_photo = Path("my_secret_photo.png").read_bytes()
carrier_image = Path("carrier.png").read_bytes()
# Your secret message
message = "This is my secret message!"
# Your credentials
passphrase = "correct horse battery staple" # Use 4+ words
pin = "123456" # 6-9 digits
# === ENCODE ===
print("Encoding message...")
result = stegasoo.encode(
message=message,
reference_photo=reference_photo,
carrier_image=carrier_image,
passphrase=passphrase,
pin=pin,
)
# Save the stego image
output_path = Path(f"secret_{result.suggested_filename}")
output_path.write_bytes(result.stego_image)
print(f"Saved to: {output_path}")
print(f"Capacity used: {result.capacity_used_percent:.1f}%")
# === DECODE ===
print("\nDecoding message...")
stego_image = output_path.read_bytes()
decoded = stegasoo.decode(
stego_image=stego_image,
reference_photo=reference_photo,
passphrase=passphrase,
pin=pin,
)
print(f"Decoded message: {decoded.message}")
print(f"Message type: {decoded.payload_type}")
if __name__ == "__main__":
main()

72
examples/channel_keys.py Normal file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
Channel Keys Example
Channel keys allow you to create private communication channels.
Only people with the same channel key can decode messages.
"""
from pathlib import Path
import stegasoo
from stegasoo.channel import generate_channel_key, get_channel_fingerprint
def main():
# Generate a channel key for your group
channel_key = generate_channel_key()
fingerprint = get_channel_fingerprint(channel_key)
print("=== Channel Key Generated ===")
print(f"Key: {channel_key}")
print(f"Fingerprint: {fingerprint}")
print("\nShare this key securely with your group members!")
print("-" * 40)
# Load images
reference_photo = Path("my_secret_photo.png").read_bytes()
carrier_image = Path("carrier.png").read_bytes()
# Encode with channel key
print("\nEncoding message with channel key...")
result = stegasoo.encode(
message="Secret group message!",
reference_photo=reference_photo,
carrier_image=carrier_image,
passphrase="correct horse battery staple",
pin="123456",
channel_key=channel_key, # Add the channel key
)
stego_data = result.stego_image
print(f"Encoded successfully!")
# Decode with correct channel key
print("\nDecoding with correct channel key...")
decoded = stegasoo.decode(
stego_image=stego_data,
reference_photo=reference_photo,
passphrase="correct horse battery staple",
pin="123456",
channel_key=channel_key, # Same channel key
)
print(f"Message: {decoded.message}")
# Try to decode with wrong channel key
print("\nTrying to decode with wrong channel key...")
wrong_key = generate_channel_key()
try:
stegasoo.decode(
stego_image=stego_data,
reference_photo=reference_photo,
passphrase="correct horse battery staple",
pin="123456",
channel_key=wrong_key, # Different channel key
)
print("ERROR: Should have failed!")
except (stegasoo.DecryptionError, stegasoo.ExtractionError):
print("Correctly rejected - wrong channel key!")
if __name__ == "__main__":
main()

78
examples/embed_file.py Normal file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
File Embedding Example
This example demonstrates how to embed a file (like a document or image)
inside a carrier image using Stegasoo.
"""
from pathlib import Path
import stegasoo
from stegasoo.models import FilePayload
def main():
# Load images
reference_photo = Path("my_secret_photo.png").read_bytes()
carrier_image = Path("carrier.png").read_bytes()
# Load the file to embed
secret_file = Path("secret_document.pdf")
file_data = secret_file.read_bytes()
# Create a FilePayload
payload = FilePayload(
filename=secret_file.name,
data=file_data,
mime_type="application/pdf",
)
# Credentials
passphrase = "correct horse battery staple"
pin = "123456"
# Check capacity first
capacity = stegasoo.calculate_capacity(carrier_image)
print(f"Carrier capacity: {capacity['capacity_bytes']:,} bytes")
print(f"File size: {len(file_data):,} bytes")
if len(file_data) > capacity["capacity_bytes"]:
print("Error: File too large for this carrier!")
return
# Encode the file
print("\nEmbedding file...")
result = stegasoo.encode(
file_payload=payload,
reference_photo=reference_photo,
carrier_image=carrier_image,
passphrase=passphrase,
pin=pin,
)
output_path = Path(f"contains_file_{result.suggested_filename}")
output_path.write_bytes(result.stego_image)
print(f"Saved to: {output_path}")
# Decode and extract the file
print("\nExtracting file...")
decoded = stegasoo.decode(
stego_image=output_path.read_bytes(),
reference_photo=reference_photo,
passphrase=passphrase,
pin=pin,
)
if decoded.payload_type == "file":
extracted_path = Path(f"extracted_{decoded.filename}")
extracted_path.write_bytes(decoded.file_data)
print(f"Extracted: {extracted_path}")
print(f"Original filename: {decoded.filename}")
print(f"MIME type: {decoded.mime_type}")
else:
print(f"Unexpected payload type: {decoded.payload_type}")
if __name__ == "__main__":
main()

View File

@@ -1,952 +0,0 @@
# Stegasoo REST API Documentation
Complete REST API reference for Stegasoo steganography operations.
## Table of Contents
- [Overview](#overview)
- [Installation](#installation)
- [Authentication](#authentication)
- [Base URL](#base-url)
- [Endpoints](#endpoints)
- [GET /](#get--status)
- [POST /generate](#post-generate)
- [POST /encode](#post-encode-json)
- [POST /encode/multipart](#post-encodemultipart)
- [POST /decode](#post-decode-json)
- [POST /decode/multipart](#post-decodemultipart)
- [POST /image/info](#post-imageinfo)
- [Data Models](#data-models)
- [Error Handling](#error-handling)
- [Code Examples](#code-examples)
- [Rate Limiting](#rate-limiting)
- [Security Considerations](#security-considerations)
---
## Overview
The Stegasoo REST API provides programmatic access to all steganography operations:
- **Generate** credentials (phrases, PINs, RSA keys)
- **Encode** messages into images
- **Decode** messages from images
- **Analyze** image capacity
The API supports both JSON (base64-encoded images) and multipart form data (direct file uploads).
---
## Installation
### From PyPI
```bash
pip install stegasoo[api]
```
### From Source
```bash
git clone https://github.com/example/stegasoo.git
cd stegasoo
pip install -e ".[api]"
```
### Running the Server
**Development:**
```bash
cd frontends/api
python main.py
```
**Production:**
```bash
cd frontends/api
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
```
**Docker:**
```bash
docker-compose up api
```
---
## Authentication
The API currently operates without authentication. For production deployments, implement authentication at the reverse proxy level (nginx, Caddy) or add API key middleware.
---
## Base URL
| Environment | URL |
|-------------|-----|
| Local Development | `http://localhost:8000` |
| Docker | `http://localhost:8000` |
| Production | Configure as needed |
---
## Endpoints
### GET / (Status)
Check API status and configuration.
#### Request
```http
GET / HTTP/1.1
Host: localhost:8000
```
#### Response
```json
{
"version": "2.0.1",
"has_argon2": true,
"day_names": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
}
```
#### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `version` | string | Stegasoo library version |
| `has_argon2` | boolean | Whether Argon2id is available |
| `day_names` | array | Day names for phrase mapping |
#### cURL Example
```bash
curl http://localhost:8000/
```
---
### POST /generate
Generate credentials for encoding/decoding.
#### Request
```http
POST /generate HTTP/1.1
Host: localhost:8000
Content-Type: application/json
```
#### Request Body
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `use_pin` | boolean | `true` | Generate a PIN |
| `use_rsa` | boolean | `false` | Generate an RSA key |
| `pin_length` | integer | `6` | PIN length (6-9) |
| `rsa_bits` | integer | `2048` | RSA key size (2048, 3072, 4096) |
| `words_per_phrase` | integer | `3` | Words per phrase (3-12) |
#### Response
```json
{
"phrases": {
"Monday": "abandon ability able",
"Tuesday": "actor actress actual",
"Wednesday": "advice aerobic affair",
"Thursday": "afraid again age",
"Friday": "agree ahead aim",
"Saturday": "airport aisle alarm",
"Sunday": "album alcohol alert"
},
"pin": "847293",
"rsa_key_pem": null,
"entropy": {
"phrase": 33,
"pin": 19,
"rsa": 0,
"total": 52
}
}
```
#### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `phrases` | object | Day-to-phrase mapping |
| `pin` | string\|null | Generated PIN (if requested) |
| `rsa_key_pem` | string\|null | PEM-encoded RSA key (if requested) |
| `entropy.phrase` | integer | Entropy from phrases (bits) |
| `entropy.pin` | integer | Entropy from PIN (bits) |
| `entropy.rsa` | integer | Entropy from RSA key (bits) |
| `entropy.total` | integer | Combined entropy (bits) |
#### cURL Examples
**PIN only:**
```bash
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": true, "use_rsa": false}'
```
**RSA only:**
```bash
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": false, "use_rsa": true, "rsa_bits": 4096}'
```
**Both with custom settings:**
```bash
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{
"use_pin": true,
"use_rsa": true,
"pin_length": 9,
"rsa_bits": 4096,
"words_per_phrase": 6
}'
```
---
### POST /encode (JSON)
Encode a message using base64-encoded images.
#### Request
```http
POST /encode HTTP/1.1
Host: localhost:8000
Content-Type: application/json
```
#### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `message` | string | ✓ | Message to encode |
| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo |
| `carrier_image_base64` | string | ✓ | Base64-encoded carrier image |
| `day_phrase` | string | ✓ | Today's passphrase |
| `pin` | string | * | Static PIN (6-9 digits) |
| `rsa_key_base64` | string | * | Base64-encoded RSA key PEM |
| `rsa_password` | string | | Password for RSA key |
| `date_str` | string | | Date override (YYYY-MM-DD) |
\* At least one of `pin` or `rsa_key_base64` required.
#### Response
```json
{
"stego_image_base64": "iVBORw0KGgo...",
"filename": "a1b2c3d4_20251227.png",
"capacity_used_percent": 12.4,
"date_used": "2025-12-27",
"day_of_week": "Saturday"
}
```
#### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `stego_image_base64` | string | Base64-encoded stego PNG |
| `filename` | string | Suggested filename |
| `capacity_used_percent` | float | Percentage of capacity used |
| `date_used` | string | Date embedded in image (YYYY-MM-DD) |
| `day_of_week` | string | Day name for passphrase rotation |
#### cURL Example
```bash
# Prepare base64-encoded images
REF_B64=$(base64 -w0 reference.jpg)
CARRIER_B64=$(base64 -w0 carrier.png)
curl -X POST http://localhost:8000/encode \
-H "Content-Type: application/json" \
-d "{
\"message\": \"Secret message\",
\"reference_photo_base64\": \"$REF_B64\",
\"carrier_image_base64\": \"$CARRIER_B64\",
\"day_phrase\": \"apple forest thunder\",
\"pin\": \"123456\"
}" | jq -r '.stego_image_base64' | base64 -d > stego.png
```
---
### POST /encode/multipart
Encode a message using direct file uploads. Returns the stego image directly.
#### Request
```http
POST /encode/multipart HTTP/1.1
Host: localhost:8000
Content-Type: multipart/form-data; boundary=----FormBoundary
------FormBoundary
Content-Disposition: form-data; name="message"
Secret message here
------FormBoundary
Content-Disposition: form-data; name="day_phrase"
apple forest thunder
------FormBoundary
Content-Disposition: form-data; name="pin"
123456
------FormBoundary
Content-Disposition: form-data; name="reference_photo"; filename="ref.jpg"
Content-Type: image/jpeg
<binary image data>
------FormBoundary
Content-Disposition: form-data; name="carrier"; filename="carrier.png"
Content-Type: image/png
<binary image data>
------FormBoundary--
```
#### Form Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `message` | string | ✓ | Message to encode |
| `reference_photo` | file | ✓ | Reference photo file |
| `carrier` | file | ✓ | Carrier image file |
| `day_phrase` | string | ✓ | Today's passphrase |
| `pin` | string | * | Static PIN |
| `rsa_key` | file | * | RSA key file (.pem) |
| `rsa_password` | string | | Password for RSA key |
| `date_str` | string | | Date override (YYYY-MM-DD) |
\* At least one of `pin` or `rsa_key` required.
#### Response
Returns the PNG image directly with headers:
- `Content-Type: image/png`
- `Content-Disposition: attachment; filename=<generated_filename>.png`
- `X-Stegasoo-Date: 2025-12-27` (date used for encoding)
- `X-Stegasoo-Day: Saturday` (day of week for passphrase rotation)
- `X-Stegasoo-Capacity-Percent: 12.4` (capacity used)
#### cURL Examples
**With PIN:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
-F "message=Secret message" \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
```
**With RSA key:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
-F "message=Secret message" \
-F "day_phrase=apple forest thunder" \
-F "rsa_key=@mykey.pem" \
-F "rsa_password=keypassword" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
```
**With both PIN and RSA:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
-F "message=Maximum security message" \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "rsa_key=@mykey.pem" \
-F "rsa_password=keypassword" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
```
**With custom date:**
```bash
curl -X POST http://localhost:8000/encode/multipart \
-F "message=Backdated message" \
-F "day_phrase=monday phrase here" \
-F "pin=123456" \
-F "date_str=2025-12-29" \
-F "reference_photo=@reference.jpg" \
-F "carrier=@carrier.png" \
--output stego.png
```
---
### POST /decode (JSON)
Decode a message using base64-encoded images.
#### Request
```http
POST /decode HTTP/1.1
Host: localhost:8000
Content-Type: application/json
```
#### Request Body
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stego_image_base64` | string | ✓ | Base64-encoded stego image |
| `reference_photo_base64` | string | ✓ | Base64-encoded reference photo |
| `day_phrase` | string | ✓ | Passphrase for encoding day |
| `pin` | string | * | Static PIN |
| `rsa_key_base64` | string | * | Base64-encoded RSA key |
| `rsa_password` | string | | Password for RSA key |
\* Must match the security factors used during encoding.
#### Response
```json
{
"message": "Secret message here"
}
```
#### cURL Example
```bash
# Prepare base64-encoded images
STEGO_B64=$(base64 -w0 stego.png)
REF_B64=$(base64 -w0 reference.jpg)
curl -X POST http://localhost:8000/decode \
-H "Content-Type: application/json" \
-d "{
\"stego_image_base64\": \"$STEGO_B64\",
\"reference_photo_base64\": \"$REF_B64\",
\"day_phrase\": \"apple forest thunder\",
\"pin\": \"123456\"
}"
```
---
### POST /decode/multipart
Decode a message using direct file uploads.
#### Request
```http
POST /decode/multipart HTTP/1.1
Host: localhost:8000
Content-Type: multipart/form-data
```
#### Form Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stego_image` | file | ✓ | Stego image file |
| `reference_photo` | file | ✓ | Reference photo file |
| `day_phrase` | string | ✓ | Passphrase for encoding day |
| `pin` | string | * | Static PIN |
| `rsa_key` | file | * | RSA key file |
| `rsa_password` | string | | Password for RSA key |
#### Response
```json
{
"message": "Secret message here"
}
```
#### cURL Examples
**With PIN:**
```bash
curl -X POST http://localhost:8000/decode/multipart \
-F "day_phrase=apple forest thunder" \
-F "pin=123456" \
-F "reference_photo=@reference.jpg" \
-F "stego_image=@stego.png"
```
**With RSA key:**
```bash
curl -X POST http://localhost:8000/decode/multipart \
-F "day_phrase=apple forest thunder" \
-F "rsa_key=@mykey.pem" \
-F "rsa_password=keypassword" \
-F "reference_photo=@reference.jpg" \
-F "stego_image=@stego.png"
```
---
### POST /image/info
Get information about an image's capacity.
#### Request
```http
POST /image/info HTTP/1.1
Host: localhost:8000
Content-Type: multipart/form-data
```
#### Form Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `image` | file | ✓ | Image file to analyze |
#### Response
```json
{
"width": 1920,
"height": 1080,
"pixels": 2073600,
"capacity_bytes": 776970,
"capacity_kb": 758
}
```
#### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `width` | integer | Image width in pixels |
| `height` | integer | Image height in pixels |
| `pixels` | integer | Total pixel count |
| `capacity_bytes` | integer | Maximum message capacity (bytes) |
| `capacity_kb` | integer | Maximum message capacity (KB) |
#### cURL Example
```bash
curl -X POST http://localhost:8000/image/info \
-F "image=@myimage.png"
```
---
## Data Models
### GenerateRequest
```json
{
"use_pin": true,
"use_rsa": false,
"pin_length": 6,
"rsa_bits": 2048,
"words_per_phrase": 3
}
```
### GenerateResponse
```json
{
"phrases": {"Monday": "...", "Tuesday": "...", ...},
"pin": "123456",
"rsa_key_pem": "-----BEGIN PRIVATE KEY-----...",
"entropy": {"phrase": 33, "pin": 19, "rsa": 0, "total": 52}
}
```
### EncodeRequest
```json
{
"message": "string",
"reference_photo_base64": "string",
"carrier_image_base64": "string",
"day_phrase": "string",
"pin": "string",
"rsa_key_base64": "string",
"rsa_password": "string",
"date_str": "YYYY-MM-DD"
}
```
### EncodeResponse
```json
{
"stego_image_base64": "string",
"filename": "string",
"capacity_used_percent": 12.4,
"date_used": "YYYY-MM-DD",
"day_of_week": "Saturday"
}
```
### DecodeRequest
```json
{
"stego_image_base64": "string",
"reference_photo_base64": "string",
"day_phrase": "string",
"pin": "string",
"rsa_key_base64": "string",
"rsa_password": "string"
}
```
### DecodeResponse
```json
{
"message": "string"
}
```
### ImageInfoResponse
```json
{
"width": 1920,
"height": 1080,
"pixels": 2073600,
"capacity_bytes": 776970,
"capacity_kb": 758
}
```
### ErrorResponse
```json
{
"error": "ErrorType",
"detail": "Error description"
}
```
---
## Error Handling
### HTTP Status Codes
| Code | Meaning | Use Case |
|------|---------|----------|
| 200 | OK | Successful operation |
| 400 | Bad Request | Invalid input, capacity error |
| 401 | Unauthorized | Decryption failed (wrong credentials) |
| 500 | Internal Error | Unexpected server error |
### Error Response Format
```json
{
"detail": "Error message describing the problem"
}
```
### Common Errors
| Status | Error | Solution |
|--------|-------|----------|
| 400 | "Must enable at least one of use_pin or use_rsa" | Set `use_pin` or `use_rsa` to true |
| 400 | "rsa_bits must be one of [2048, 3072, 4096]" | Use valid RSA key size |
| 400 | "Carrier image too small" | Use larger carrier image |
| 400 | "PIN must be 6-9 digits" | Fix PIN format |
| 401 | "Decryption failed. Check credentials." | Verify phrase, PIN, ref photo |
| 400 | "Message too long" | Reduce message size or use larger carrier |
---
## Code Examples
### Python with requests
```python
import base64
import requests
BASE_URL = "http://localhost:8000"
# Generate credentials
response = requests.post(f"{BASE_URL}/generate", json={
"use_pin": True,
"use_rsa": False,
"words_per_phrase": 3
})
creds = response.json()
print(f"PIN: {creds['pin']}")
print(f"Monday phrase: {creds['phrases']['Monday']}")
# Encode using multipart
with open("reference.jpg", "rb") as ref, open("carrier.png", "rb") as carrier:
response = requests.post(f"{BASE_URL}/encode/multipart", files={
"reference_photo": ref,
"carrier": carrier,
}, data={
"message": "Secret message",
"day_phrase": "apple forest thunder",
"pin": "123456"
})
with open("stego.png", "wb") as f:
f.write(response.content)
# Decode using multipart
with open("reference.jpg", "rb") as ref, open("stego.png", "rb") as stego:
response = requests.post(f"{BASE_URL}/decode/multipart", files={
"reference_photo": ref,
"stego_image": stego,
}, data={
"day_phrase": "apple forest thunder",
"pin": "123456"
})
print(f"Decoded: {response.json()['message']}")
```
### JavaScript/Node.js
```javascript
const FormData = require('form-data');
const fs = require('fs');
const axios = require('axios');
const BASE_URL = 'http://localhost:8000';
async function encode() {
const form = new FormData();
form.append('message', 'Secret message');
form.append('day_phrase', 'apple forest thunder');
form.append('pin', '123456');
form.append('reference_photo', fs.createReadStream('reference.jpg'));
form.append('carrier', fs.createReadStream('carrier.png'));
const response = await axios.post(`${BASE_URL}/encode/multipart`, form, {
headers: form.getHeaders(),
responseType: 'arraybuffer'
});
fs.writeFileSync('stego.png', response.data);
console.log('Encoded successfully');
}
async function decode() {
const form = new FormData();
form.append('day_phrase', 'apple forest thunder');
form.append('pin', '123456');
form.append('reference_photo', fs.createReadStream('reference.jpg'));
form.append('stego_image', fs.createReadStream('stego.png'));
const response = await axios.post(`${BASE_URL}/decode/multipart`, form, {
headers: form.getHeaders()
});
console.log('Decoded:', response.data.message);
}
encode().then(decode);
```
### Go
```go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
// Encode
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
writer.WriteField("message", "Secret message")
writer.WriteField("day_phrase", "apple forest thunder")
writer.WriteField("pin", "123456")
ref, _ := os.Open("reference.jpg")
refPart, _ := writer.CreateFormFile("reference_photo", "reference.jpg")
io.Copy(refPart, ref)
ref.Close()
carrier, _ := os.Open("carrier.png")
carrierPart, _ := writer.CreateFormFile("carrier", "carrier.png")
io.Copy(carrierPart, carrier)
carrier.Close()
writer.Close()
resp, _ := http.Post(
"http://localhost:8000/encode/multipart",
writer.FormDataContentType(),
body,
)
stego, _ := os.Create("stego.png")
io.Copy(stego, resp.Body)
stego.Close()
resp.Body.Close()
fmt.Println("Encoded successfully")
}
```
### Shell Script (Bash)
```bash
#!/bin/bash
BASE_URL="http://localhost:8000"
REF_PHOTO="reference.jpg"
CARRIER="carrier.png"
PHRASE="apple forest thunder"
PIN="123456"
MESSAGE="Secret message"
# Encode
echo "Encoding..."
curl -s -X POST "$BASE_URL/encode/multipart" \
-F "message=$MESSAGE" \
-F "day_phrase=$PHRASE" \
-F "pin=$PIN" \
-F "reference_photo=@$REF_PHOTO" \
-F "carrier=@$CARRIER" \
--output stego.png
echo "Encoded to stego.png"
# Decode
echo "Decoding..."
DECODED=$(curl -s -X POST "$BASE_URL/decode/multipart" \
-F "day_phrase=$PHRASE" \
-F "pin=$PIN" \
-F "reference_photo=@$REF_PHOTO" \
-F "stego_image=@stego.png" | jq -r '.message')
echo "Decoded message: $DECODED"
```
---
## Rate Limiting
The API does not implement rate limiting by default. For production:
1. **Reverse Proxy**: Use nginx or Caddy rate limiting
2. **Application Level**: Add FastAPI middleware
Example nginx rate limiting:
```nginx
limit_req_zone $binary_remote_addr zone=stegasoo:10m rate=10r/s;
location /api/ {
limit_req zone=stegasoo burst=20 nodelay;
proxy_pass http://localhost:8000/;
}
```
---
## Security Considerations
### In Transit
- Use HTTPS in production
- Configure TLS at reverse proxy level
### Memory Usage
- Argon2id requires 256MB RAM per operation
- Concurrent requests can exhaust memory
- Limit workers based on available RAM
### Input Validation
The API validates:
- PIN format (6-9 digits, no leading zero)
- Message size (max 50KB)
- Image size (max 5MB file, ~4MP dimensions)
- RSA key validity
### Credential Handling
- Credentials are never logged
- No persistent storage of secrets
- Memory cleared after operations
---
## Interactive Documentation
When the API is running, visit:
- **Swagger UI**: http://localhost:8000/docs
- **ReDoc**: http://localhost:8000/redoc
---
## See Also
- [CLI Documentation](CLI.md) - Command-line interface
- [Web UI Documentation](WEB_UI.md) - Browser interface
- [README](README.md) - Project overview
- Image size (max 5MB file, ~4MP dimensions)
- RSA key validity
### Credential Handling
- Credentials are never logged
- No persistent storage of secrets
- Memory cleared after operations
---
## Interactive Documentation
When the API is running, visit:
- **Swagger UI**: http://localhost:8000/docs
- **ReDoc**: http://localhost:8000/redoc
---
## See Also
- [CLI Documentation](CLI.md) - Command-line interface
- [Web UI Documentation](WEB_UI.md) - Browser interface
- [README](README.md) - Project overview

View File

@@ -1,634 +0,0 @@
# Stegasoo CLI Documentation
Complete command-line interface reference for Stegasoo steganography operations.
## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Commands](#commands)
- [generate](#generate-command)
- [encode](#encode-command)
- [decode](#decode-command)
- [info](#info-command)
- [Security Factors](#security-factors)
- [Workflow Examples](#workflow-examples)
- [Piping & Scripting](#piping--scripting)
- [Error Handling](#error-handling)
- [Exit Codes](#exit-codes)
---
## Installation
### From PyPI
```bash
# CLI only
pip install stegasoo[cli]
# With all extras
pip install stegasoo[all]
```
### From Source
```bash
git clone https://github.com/example/stegasoo.git
cd stegasoo
pip install -e ".[cli]"
```
### Verify Installation
```bash
stegasoo --version
stegasoo --help
```
---
## Quick Start
```bash
# 1. Generate credentials (do this once, memorize results)
stegasoo generate --pin --words 3
# 2. Encode a message
stegasoo encode \
--ref secret_photo.jpg \
--carrier meme.png \
--phrase "apple forest thunder" \
--pin 123456 \
--message "Meet at midnight"
# 3. Decode a message
stegasoo decode \
--ref secret_photo.jpg \
--stego stego_abc123_20251227.png \
--phrase "apple forest thunder" \
--pin 123456
```
---
## Commands
### Generate Command
Generate credentials for encoding/decoding operations. Creates daily passphrases and optionally a PIN and/or RSA key.
#### Synopsis
```bash
stegasoo generate [OPTIONS]
```
#### Options
| Option | Short | Type | Default | Description |
|--------|-------|------|---------|-------------|
| `--pin/--no-pin` | | flag | `--pin` | Generate a PIN |
| `--rsa/--no-rsa` | | flag | `--no-rsa` | Generate an RSA key |
| `--pin-length` | | 6-9 | 6 | PIN length in digits |
| `--rsa-bits` | | choice | 2048 | RSA key size (2048, 3072, 4096) |
| `--words` | | 3-12 | 3 | Words per daily phrase |
| `--output` | `-o` | path | | Save RSA key to file |
| `--password` | `-p` | string | | Password for RSA key file |
| `--json` | | flag | | Output as JSON |
#### Examples
**Basic generation with PIN (default):**
```bash
stegasoo generate
```
Output:
```
════════════════════════════════════════════════════════════
STEGASOO CREDENTIALS
════════════════════════════════════════════════════════════
⚠️ MEMORIZE THESE AND CLOSE THIS WINDOW
Do not screenshot or save to file!
─── STATIC PIN ───
847293
─── DAILY PHRASES ───
Monday │ abandon ability able
Tuesday │ actor actress actual
Wednesday │ advice aerobic affair
Thursday │ afraid again age
Friday │ agree ahead aim
Saturday │ airport aisle alarm
Sunday │ album alcohol alert
─── SECURITY ───
Phrase entropy: 33 bits
PIN entropy: 19 bits
Combined: 52 bits
+ photo entropy: 80-256 bits
```
**Generate with RSA key:**
```bash
stegasoo generate --rsa --rsa-bits 4096
```
**Save RSA key to encrypted file:**
```bash
stegasoo generate --rsa -o mykey.pem -p "mysecretpassword"
```
**Maximum security (longer phrases + both factors):**
```bash
stegasoo generate --pin --rsa --words 6 --pin-length 9
```
**JSON output for scripting:**
```bash
stegasoo generate --json | jq '.phrases.Monday'
```
**RSA only (no PIN):**
```bash
stegasoo generate --no-pin --rsa -o key.pem -p "password123"
```
---
### Encode Command
Encode a secret message into an image using steganography.
#### Synopsis
```bash
stegasoo encode [OPTIONS]
```
#### Options
| Option | Short | Type | Required | Description |
|--------|-------|------|----------|-------------|
| `--ref` | `-r` | path | ✓ | Reference photo (shared secret) |
| `--carrier` | `-c` | path | ✓ | Carrier image to hide message in |
| `--phrase` | `-p` | string | ✓ | Today's passphrase |
| `--message` | `-m` | string | | Message to encode |
| `--message-file` | `-f` | path | | Read message from file |
| `--pin` | | string | * | Static PIN (6-9 digits) |
| `--key` | `-k` | path | * | RSA key file |
| `--key-password` | | string | | Password for RSA key |
| `--output` | `-o` | path | | Output filename |
| `--date` | | YYYY-MM-DD | | Date override |
| `--quiet` | `-q` | flag | | Suppress output |
\* At least one of `--pin` or `--key` is required.
#### Message Input Methods
1. **Command line argument:**
```bash
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "Secret message"
```
2. **From file:**
```bash
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -f message.txt
```
3. **From stdin (pipe):**
```bash
echo "Secret message" | stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456
```
#### Examples
**Basic encoding with PIN:**
```bash
stegasoo encode \
--ref photos/vacation.jpg \
--carrier memes/funny_cat.png \
--phrase "correct horse battery" \
--pin 847293 \
--message "The package arrives Tuesday"
```
Output:
```
✓ Encoded successfully!
Output: a1b2c3d4_20251227.png
Size: 245,832 bytes
Capacity used: 12.4%
Date: 2025-12-27
```
**With RSA key:**
```bash
stegasoo encode \
-r reference.jpg \
-c carrier.png \
-p "apple forest thunder" \
-k mykey.pem \
--key-password "secretpassword" \
-m "Encrypted with RSA"
```
**Both PIN and RSA (maximum security):**
```bash
stegasoo encode \
-r ref.jpg \
-c carrier.png \
-p "word1 word2 word3" \
--pin 123456 \
-k mykey.pem \
--key-password "pass" \
-m "Double-locked message"
```
**Custom output filename:**
```bash
stegasoo encode \
-r ref.jpg \
-c carrier.png \
-p "phrase words here" \
--pin 123456 \
-m "Message" \
-o holiday_photo.png
```
**Encoding with specific date (for testing):**
```bash
stegasoo encode \
-r ref.jpg \
-c carrier.png \
-p "monday phrase here" \
--pin 123456 \
-m "Message" \
--date 2025-12-29
```
**Long message from file:**
```bash
stegasoo encode \
-r ref.jpg \
-c large_image.png \
-p "phrase" \
--pin 123456 \
-f secret_document.txt \
-o output.png
```
**Quiet mode for scripting:**
```bash
stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -m "msg" -q -o out.png
# No output, just creates the file
```
---
### Decode Command
Decode a secret message from a stego image.
#### Synopsis
```bash
stegasoo decode [OPTIONS]
```
#### Options
| Option | Short | Type | Required | Description |
|--------|-------|------|----------|-------------|
| `--ref` | `-r` | path | ✓ | Reference photo (same as encoding) |
| `--stego` | `-s` | path | ✓ | Stego image to decode |
| `--phrase` | `-p` | string | ✓ | Passphrase for the encoding day |
| `--pin` | | string | * | Static PIN |
| `--key` | `-k` | path | * | RSA key file |
| `--key-password` | | string | | Password for RSA key |
| `--output` | `-o` | path | | Save message to file |
| `--quiet` | `-q` | flag | | Output only the message |
\* Must provide the same security factors used during encoding.
#### Examples
**Basic decoding with PIN:**
```bash
stegasoo decode \
--ref photos/vacation.jpg \
--stego received_image.png \
--phrase "correct horse battery" \
--pin 847293
```
Output:
```
✓ Decoded successfully!
The package arrives Tuesday
```
**With RSA key:**
```bash
stegasoo decode \
-r reference.jpg \
-s stego_image.png \
-p "apple forest thunder" \
-k mykey.pem \
--key-password "secretpassword"
```
**Save decoded message to file:**
```bash
stegasoo decode \
-r ref.jpg \
-s stego.png \
-p "phrase" \
--pin 123456 \
-o decoded_message.txt
```
Output:
```
✓ Decoded successfully!
Saved to: decoded_message.txt
```
**Quiet mode (message only):**
```bash
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q
```
Output:
```
The package arrives Tuesday
```
**Pipe to another command:**
```bash
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | gpg --decrypt
```
---
### Info Command
Display information about an image's capacity and embedded date.
#### Synopsis
```bash
stegasoo info IMAGE
```
#### Arguments
| Argument | Type | Description |
|----------|------|-------------|
| `IMAGE` | path | Path to image file |
#### Examples
**Check carrier image capacity:**
```bash
stegasoo info vacation_photo.png
```
Output:
```
Image: vacation_photo.png
Dimensions: 1920 × 1080
Pixels: 2,073,600
Mode: RGB
Format: PNG
Capacity: ~776,970 bytes (758 KB)
```
**Check stego image (shows encoding date):**
```bash
stegasoo info stego_a1b2c3d4_20251227.png
```
Output:
```
Image: stego_a1b2c3d4_20251227.png
Dimensions: 1920 × 1080
Pixels: 2,073,600
Mode: RGB
Format: PNG
Capacity: ~776,970 bytes (758 KB)
Embed date: 2025-12-27 (Saturday)
```
---
## Security Factors
Stegasoo uses multiple authentication factors:
| Factor | Description | Entropy |
|--------|-------------|---------|
| Reference Photo | A photo both parties have | ~80-256 bits |
| Day Phrase | Changes daily (e.g., 3 BIP-39 words) | ~33 bits (3 words) |
| Static PIN | Same every day (6-9 digits) | ~20 bits (6 digits) |
| RSA Key | Shared key file | ~128 bits effective |
### Minimum Requirements
- At least one of PIN or RSA key must be provided
- Reference photo is always required
- Day phrase is always required
### Security Configurations
| Configuration | Entropy (excl. photo) | Use Case |
|--------------|----------------------|----------|
| 3-word phrase + 6-digit PIN | ~53 bits | Casual use |
| 6-word phrase + 9-digit PIN | ~96 bits | Standard security |
| 3-word phrase + RSA 2048 | ~161 bits | File-based auth |
| 6-word phrase + PIN + RSA | ~224 bits | Maximum security |
---
## Workflow Examples
### Daily Secure Communication
**Setup (once):**
```bash
# Both parties generate same credentials
stegasoo generate --pin --words 3
# Or share RSA key securely
stegasoo generate --rsa -o shared_key.pem -p "agreedpassword"
# Securely transfer shared_key.pem to recipient
```
**Sender (daily):**
```bash
# Get today's phrase from your memorized list
TODAY_PHRASE="monday phrase words"
# Encode message
stegasoo encode \
-r our_shared_photo.jpg \
-c random_meme.png \
-p "$TODAY_PHRASE" \
--pin 847293 \
-m "Meeting moved to 3pm"
# Share output image via normal channels (email, chat, etc.)
```
**Recipient (daily):**
```bash
# Use the phrase for the day the message was SENT
stegasoo decode \
-r our_shared_photo.jpg \
-s received_image.png \
-p "monday phrase words" \
--pin 847293
```
### Batch Processing
**Encode multiple messages:**
```bash
#!/bin/bash
PHRASE="apple forest thunder"
PIN="123456"
REF="reference.jpg"
for file in messages/*.txt; do
name=$(basename "$file" .txt)
stegasoo encode \
-r "$REF" \
-c "carriers/${name}.png" \
-p "$PHRASE" \
--pin "$PIN" \
-f "$file" \
-o "output/${name}_stego.png" \
-q
echo "Encoded: $name"
done
```
### Archive with Date Preservation
```bash
# Encode with specific date for archival
stegasoo encode \
-r ref.jpg \
-c carrier.png \
-p "archive phrase words" \
--pin 123456 \
-m "Historical record" \
--date 2025-01-15 \
-o archive_2025-01-15.png
```
---
## Piping & Scripting
### Stdin/Stdout Support
**Encode from pipe:**
```bash
cat secret.txt | stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456 -o out.png
```
**Decode to pipe:**
```bash
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | less
```
**Chain with encryption:**
```bash
# Encode GPG-encrypted content
gpg -e -r recipient@email.com secret.txt
cat secret.txt.gpg | base64 | stegasoo encode -r ref.jpg -c carrier.png -p "phrase" --pin 123456
# Decode and decrypt
stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q | base64 -d | gpg -d
```
### JSON Output for Scripts
```bash
# Get credentials as JSON
creds=$(stegasoo generate --json)
# Extract specific fields
pin=$(echo "$creds" | jq -r '.pin')
monday=$(echo "$creds" | jq -r '.phrases.Monday')
entropy=$(echo "$creds" | jq -r '.entropy.total')
echo "PIN: $pin"
echo "Monday phrase: $monday"
echo "Total entropy: $entropy bits"
```
### Error Handling in Scripts
```bash
#!/bin/bash
set -e
if ! stegasoo decode -r ref.jpg -s stego.png -p "phrase" --pin 123456 -q 2>/dev/null; then
echo "Decryption failed - check credentials"
exit 1
fi
```
---
## Error Handling
### Common Errors
| Error | Cause | Solution |
|-------|-------|----------|
| "Must provide --pin or --key" | No security factor given | Add `--pin` or `--key` option |
| "PIN must be 6-9 digits" | Invalid PIN format | Use numeric PIN, 6-9 chars |
| "PIN cannot start with zero" | Leading zero in PIN | Use PIN starting with 1-9 |
| "Carrier image too small" | Message exceeds capacity | Use larger carrier image |
| "Decryption failed" | Wrong credentials | Verify phrase, PIN, ref photo |
| "RSA key is password-protected" | Missing key password | Add `--key-password` option |
### Troubleshooting Decryption Failures
1. **Check the encoding date:** The filename often contains the date (e.g., `_20251227`)
2. **Use correct phrase:** The phrase must match the day the message was encoded, not today
3. **Verify reference photo:** Must be the exact same file, not a resized copy
4. **Check stego image:** Ensure it wasn't resized, recompressed, or converted
---
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error |
| 2 | Invalid arguments/options |
---
## Environment Variables
| Variable | Description |
|----------|-------------|
| `PYTHONPATH` | Include `src/` for development |
---
## See Also
- [API Documentation](API.md) - REST API reference
- [Web UI Documentation](WEB_UI.md) - Browser interface guide
- [README](README.md) - Project overview and security model

View File

@@ -1,739 +0,0 @@
# Stegasoo Web UI Documentation
Complete guide for the Stegasoo web-based steganography interface.
## Table of Contents
- [Overview](#overview)
- [Installation & Setup](#installation--setup)
- [Pages & Features](#pages--features)
- [Home Page](#home-page)
- [Generate Credentials](#generate-credentials)
- [Encode Message](#encode-message)
- [Decode Message](#decode-message)
- [About Page](#about-page)
- [User Interface Guide](#user-interface-guide)
- [Workflow Examples](#workflow-examples)
- [Security Features](#security-features)
- [Configuration](#configuration)
- [Troubleshooting](#troubleshooting)
- [Mobile Support](#mobile-support)
---
## Overview
The Stegasoo Web UI provides a user-friendly browser-based interface for:
- **Generating** secure credentials (phrases, PINs, RSA keys)
- **Encoding** secret messages into images
- **Decoding** hidden messages from images
- **Learning** about the security model
Built with Flask, Bootstrap 5, and a modern dark theme.
### Features
- ✅ Drag-and-drop file uploads
- ✅ Image previews
- ✅ Client-side date detection
- ✅ Native sharing (Web Share API)
- ✅ Responsive design (mobile-friendly)
- ✅ Password-protected RSA key downloads
- ✅ Real-time entropy calculations
- ✅ Automatic file cleanup
---
## Installation & Setup
### From PyPI
```bash
pip install stegasoo[web]
```
### From Source
```bash
git clone https://github.com/example/stegasoo.git
cd stegasoo
pip install -e ".[web]"
```
### Running the Server
**Development:**
```bash
cd frontends/web
python app.py
```
Server starts at http://localhost:5000
**Production with Gunicorn:**
```bash
cd frontends/web
gunicorn --bind 0.0.0.0:5000 --workers 2 --threads 4 --timeout 60 app:app
```
**Docker:**
```bash
docker-compose up web
```
### First-Time Setup
1. Navigate to http://localhost:5000
2. Click "Generate" to create your credentials
3. **Memorize** your phrases and PIN
4. Share credentials securely with your communication partner
---
## Pages & Features
### Home Page
**URL:** `/`
The landing page introduces Stegasoo and provides quick access to all features.
#### Main Actions
| Card | Description | Link |
|------|-------------|------|
| **Encode Message** | Hide a secret in an image | `/encode` |
| **Decode Message** | Extract a hidden message | `/decode` |
| **Generate Keys** | Create new credentials | `/generate` |
#### "How It Works" Section
Explains the three key components:
1. **Reference Photo** - Shared secret image
2. **Day Phrase** - Changes daily
3. **Static PIN** - Same every day
---
### Generate Credentials
**URL:** `/generate`
Create a new set of credentials for steganography operations.
#### Configuration Options
| Option | Range | Default | Description |
|--------|-------|---------|-------------|
| Words per phrase | 3-12 | 3 | BIP-39 words per daily phrase |
| Use PIN | on/off | on | Generate a numeric PIN |
| PIN length | 6-9 | 6 | Digits in the PIN |
| Use RSA Key | on/off | off | Generate an RSA key pair |
| RSA key size | 2048/3072/4096 | 2048 | Key size in bits |
#### Entropy Calculator
The UI displays real-time entropy calculations:
```
Estimated entropy: ~53 bits
[==========> ] Good for most use cases
• Reference photo adds ~80-256 bits more
```
#### Generated Output
After clicking "Generate Credentials":
**Static PIN** (if enabled):
```
┌─────────────────────┐
│ 8 4 7 2 9 3 │
└─────────────────────┘
Use this 6-digit PIN every day
```
**Daily Phrases:**
```
Day │ Phrase
─────────────────────────────────────────
Monday │ abandon ability able
Tuesday │ actor actress actual
Wednesday │ advice aerobic affair
Thursday │ afraid again age
Friday │ agree ahead aim
Saturday │ airport aisle alarm
Sunday │ album alcohol alert
```
**RSA Key** (if enabled):
- Copy to clipboard button
- Download as password-protected .pem file
**Security Summary:**
```
Phrase entropy: 33 bits/phrase
PIN entropy: 19 bits/PIN
RSA entropy: 128 bits/RSA
─────────────────────────────
Total: 180 bits
+ reference photo (~80-256 bits) = 260+ bits combined
```
#### RSA Key Download
1. Click "Download as .pem"
2. Enter a password (minimum 8 characters)
3. Click "Download Protected Key"
4. Save the file securely
5. Share with your communication partner through a secure channel
---
### Encode Message
**URL:** `/encode`
Hide a secret message inside an image.
#### Input Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| Reference Photo | Image file | ✓ | Your shared secret photo |
| Carrier Image | Image file | ✓ | Image to hide message in |
| Secret Message | Text | ✓ | Message to hide (max 50KB) |
| Day Phrase | Text | ✓ | Today's passphrase |
| PIN | Number | * | Your static PIN |
| RSA Key | .pem file | * | Your shared RSA key |
| RSA Key Password | Password | | Password for encrypted key |
\* At least one security factor (PIN or RSA Key) required.
#### Drag-and-Drop Upload
Both image upload zones support:
- Click to browse
- Drag and drop files
- Instant image preview
- File name display
#### Character Counter
```
Message: [ ]
1,234 / 50,000 characters 2%
```
Shows warning at 80% capacity.
#### Day Detection
The page automatically detects your local day of week and updates the label:
```
Saturday's Phrase: [ ]
```
#### Encoding Process
1. Fill in all required fields
2. Click "Encode Message"
3. Wait for processing (shows spinner)
4. Redirected to result page
#### Result Page
**URL:** `/encode/result/<file_id>`
After successful encoding:
```
┌────────────────────────────────────────┐
│ ✓ Message Encoded Successfully! │
│ │
│ 📄 a1b2c3d4_20251227.png │
│ Your secret message is hidden │
│ in this image │
│ │
│ [ Download Image ] │
│ [ Share Image ] │
│ │
│ ⚠️ File expires in 5 minutes. │
│ Download or share now. │
│ │
│ [ Encode Another Message ] │
└────────────────────────────────────────┘
```
**Share Options:**
1. **Native Share** (mobile/supported browsers):
- Uses Web Share API
- Opens system share sheet
- Can share directly to apps
2. **Fallback Share** (desktop):
- Email link
- Telegram link
- WhatsApp link
- Copy link to clipboard
---
### Decode Message
**URL:** `/decode`
Extract a hidden message from a stego image.
#### Input Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| Reference Photo | Image file | ✓ | Same photo used for encoding |
| Stego Image | Image file | ✓ | Image containing hidden message |
| Day Phrase | Text | ✓ | Phrase for the **encoding** day |
| PIN | Number | * | Same PIN used for encoding |
| RSA Key | .pem file | * | Same RSA key used for encoding |
| RSA Key Password | Password | | Password for encrypted key |
\* Must match security factors used during encoding.
#### Date Detection from Filename
When you upload a stego image with a date in the filename (e.g., `stego_20251227.png`), the UI:
1. Extracts the date
2. Determines the day of week
3. Updates the phrase label: "Saturday's Phrase"
This helps you use the correct daily phrase.
#### Decoding Process
1. Fill in all required fields
2. Click "Decode Message"
3. Wait for processing
4. View decoded message on same page
#### Successful Decode
```
┌────────────────────────────────────────┐
│ ✓ Message Decrypted Successfully! │
│ │
│ Decoded Message: │
│ ┌──────────────────────────────────┐ │
│ │ Meet at midnight. The package │ │
│ │ will be under the bridge. │ │
│ └──────────────────────────────────┘ │
│ │
│ [ Decode Another Message ] │
└────────────────────────────────────────┘
```
#### Troubleshooting Tips
The page includes built-in troubleshooting guidance:
- ✓ Use the **exact same reference photo** file
- ✓ Use the phrase for the **encoding day**, not today
- ✓ Provide the **same security factors** used during encoding
- ✓ Ensure the stego image hasn't been **resized or recompressed**
- ✓ If using RSA key, verify the **password is correct**
---
### About Page
**URL:** `/about`
Learn about Stegasoo's security model and best practices.
#### Sections
**System Status:**
- Argon2id availability (vs PBKDF2 fallback)
- AES-256-GCM encryption status
**Security Model Table:**
| Component | Entropy | Purpose |
|-----------|---------|---------|
| Reference Photo | ~80-256 bits | Something you have |
| 3-Word Phrase | ~33 bits | Something you know (daily) |
| 6-Digit PIN | ~20 bits | Something you know (static) |
| Date | N/A | Automatic key rotation |
| **Combined** | **133+ bits** | **Beyond brute force** |
**Attack Resistance:**
What attackers can't do:
- Brute force (2^133 combinations)
- Use rainbow tables (random salt)
- Detect hidden data (random pixels)
- Use GPU farms (256MB RAM per attempt)
Real threats:
- Social engineering
- Physical device access
- Malware/keyloggers
- Shoulder surfing
**Best Practices:**
Do:
- Memorize phrases and PIN
- Use reference photo both parties have
- Use different carrier images each time
- Share stego images through normal channels
Don't:
- Transmit the reference photo
- Reuse carrier images
- Store credentials digitally
- Resize/recompress stego images
---
## User Interface Guide
### Navigation
The navbar provides quick access to all pages:
```
[Logo] Stegasoo Home | Encode | Decode | Generate | About
```
### Color Scheme
| Element | Color | Purpose |
|---------|-------|---------|
| Background | Dark gradient | Reduce eye strain |
| Cards | Semi-transparent | Visual hierarchy |
| Headers | Purple gradient | Brand identity |
| Success | Green | Positive actions |
| Warning | Yellow | Caution messages |
| Error | Red | Error states |
### Form Validation
- Real-time validation feedback
- Clear error messages in alerts
- Required field indicators
- Input constraints (max length, format)
### Loading States
During long operations:
- Button shows spinner
- Button text changes (e.g., "Encoding...")
- Button is disabled to prevent double-submit
### Flash Messages
```
┌──────────────────────────────────────────────┐
│ ✓ Credentials Generated! [×] │
└──────────────────────────────────────────────┘
```
Types:
- Success (green) - Operation completed
- Error (red) - Operation failed
- Warning (yellow) - Caution needed
---
## Workflow Examples
### First-Time Setup (Both Parties)
**Party A:**
1. Go to `/generate`
2. Configure: PIN ✓, 3 words, 6 digits
3. Click "Generate Credentials"
4. **Write down** phrases and PIN on paper
5. **Memorize** over the next few days
6. Destroy the paper
**Share with Party B (in person or secure channel):**
- The 7 daily phrases
- The PIN
- The reference photo file (if not already shared)
### Sending a Secret Message
1. Go to `/encode`
2. Upload your shared reference photo
3. Upload any carrier image (meme, vacation photo, etc.)
4. Type your secret message
5. Enter today's phrase (check your memory!)
6. Enter your PIN
7. Click "Encode Message"
8. Download or share the resulting image
9. Send via any channel (email, social media, chat)
### Receiving a Secret Message
1. Receive the stego image through any channel
2. Go to `/decode`
3. Upload the same reference photo
4. Upload the received stego image
5. Note the date in the filename (e.g., `_20251227`)
6. Enter the phrase for **that day** (not today!)
7. Enter the PIN
8. Click "Decode Message"
9. Read the secret message
### Changing Credentials
To rotate to new credentials:
1. Both parties generate new credentials together
2. Agree on a cutover date
3. Messages encoded before cutover use old credentials
4. Messages encoded after cutover use new credentials
---
## Security Features
### Client-Side Security
| Feature | Implementation |
|---------|----------------|
| Local date detection | JavaScript `Date()` object |
| No credential storage | Nothing saved in browser |
| Automatic cleanup | Files deleted after 5 minutes |
| HTTPS support | Configure at server level |
### Server-Side Security
| Feature | Implementation |
|---------|----------------|
| Memory-hard KDF | Argon2id (256MB RAM) |
| Authenticated encryption | AES-256-GCM |
| Random salt | Per-message salt |
| Temporary storage | In-memory, auto-expiring |
| Input validation | All inputs validated |
| File size limits | 5MB max upload |
### File Security
| Aspect | Protection |
|--------|------------|
| Upload location | `/tmp/stego_uploads` (Docker) |
| Storage duration | 5 minutes maximum |
| Access control | Random 16-byte file ID |
| Cleanup | Automatic + manual |
---
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FLASK_ENV` | production | Flask environment |
| `PYTHONPATH` | - | Include `src/` for development |
### Application Limits
| Limit | Value | Config Location |
|-------|-------|-----------------|
| Max file upload | 5 MB | `app.config['MAX_CONTENT_LENGTH']` |
| File expiry | 5 minutes | `TEMP_FILE_EXPIRY` |
| Max image pixels | 4 MP | `stegasoo.constants` |
| Max message size | 50 KB | `stegasoo.constants` |
| PIN length | 6-9 digits | `stegasoo.constants` |
### Production Deployment
**With Gunicorn:**
```bash
gunicorn \
--bind 0.0.0.0:5000 \
--workers 2 \
--threads 4 \
--timeout 60 \
app:app
```
**Worker Calculation:**
- Each encode/decode uses ~256MB RAM (Argon2)
- Formula: `workers = (available_RAM - 512MB) / 256MB`
**With Nginx (reverse proxy):**
```nginx
server {
listen 80;
server_name stegasoo.example.com;
client_max_body_size 10M;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 120s;
}
}
```
**With Docker Compose:**
```yaml
services:
web:
build:
context: .
target: web
ports:
- "5000:5000"
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
```
---
## Troubleshooting
### Common Issues
#### "Decryption failed"
**Causes:**
- Wrong day phrase
- Wrong PIN
- Different reference photo
- Stego image was modified
**Solutions:**
1. Check the date in the stego filename
2. Use the phrase for that specific day
3. Verify you're using the original reference photo
4. Ensure the stego image wasn't resized/recompressed
#### "Carrier image too small"
**Cause:** Message too large for carrier capacity
**Solutions:**
1. Use a larger carrier image (more pixels)
2. Shorten the message
3. Check capacity with `/info` command (CLI)
#### "You must provide at least a PIN or RSA Key"
**Cause:** No security factor selected
**Solution:** Enter a PIN and/or upload an RSA key
#### Upload fails silently
**Causes:**
- File too large (>5MB)
- Invalid file type
- Browser issue
**Solutions:**
1. Reduce file size
2. Use PNG, JPG, or BMP formats
3. Try a different browser
#### RSA key password error
**Causes:**
- Wrong password
- Unencrypted key with password provided
- Corrupted key file
**Solutions:**
1. Verify the correct password
2. If key is unencrypted, leave password blank
3. Re-download or regenerate the key
### Browser Compatibility
| Browser | Status | Notes |
|---------|--------|-------|
| Chrome 80+ | ✓ Full | Web Share API supported |
| Firefox 80+ | ✓ Full | Limited Web Share |
| Safari 14+ | ✓ Full | Web Share on iOS |
| Edge 80+ | ✓ Full | Web Share API supported |
| IE 11 | ✗ None | Not supported |
### Performance Issues
**Slow encoding/decoding:**
- Normal: Argon2 is intentionally slow (security feature)
- Expected time: 2-5 seconds per operation
**High memory usage:**
- Normal: Argon2 requires 256MB RAM
- Configure worker count based on available RAM
---
## Mobile Support
### Responsive Design
The UI adapts to mobile screens:
- Single-column layout on small screens
- Touch-friendly buttons (48px minimum)
- Readable text without zooming
- Scrollable tables
### Mobile-Specific Features
**Native Sharing:**
On supported mobile browsers, the "Share Image" button opens the native share sheet, allowing you to share directly to:
- Messaging apps (iMessage, WhatsApp, Telegram)
- Social media (Instagram, Twitter)
- Email
- Other installed apps
**Camera Upload:**
File input accepts camera capture:
- Take a new photo as reference
- Capture carrier image directly
### PWA Support (Future)
The web app can be added to home screen on mobile devices for quick access.
---
## Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| `Tab` | Navigate between fields |
| `Enter` | Submit form (when focused) |
| `Esc` | Close modal/alert |
---
## Accessibility
| Feature | Implementation |
|---------|----------------|
| Screen readers | ARIA labels on interactive elements |
| Keyboard navigation | Full tab support |
| Color contrast | WCAG AA compliant |
| Focus indicators | Visible focus rings |
| Form labels | All inputs labeled |
---
## See Also
- [CLI Documentation](CLI.md) - Command-line interface
- [API Documentation](API.md) - REST API reference
- [README](README.md) - Project overview

View File

257
frontends/api/auth.py Normal file
View File

@@ -0,0 +1,257 @@
"""
API Key Authentication for Stegasoo REST API.
Provides simple API key authentication with hashed key storage.
Keys can be stored in user config (~/.stegasoo/) or project config (./config/).
Usage:
from .auth import require_api_key, get_api_key_status
@app.get("/protected")
async def protected_endpoint(api_key: str = Depends(require_api_key)):
return {"status": "authenticated"}
"""
import hashlib
import json
import os
import secrets
from pathlib import Path
from fastapi import HTTPException, Security
from fastapi.security import APIKeyHeader
# API key header name
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
# Config locations
USER_CONFIG_DIR = Path.home() / ".stegasoo"
PROJECT_CONFIG_DIR = Path("./config")
# Key file name
API_KEYS_FILE = "api_keys.json"
# Environment variable for API key (alternative to file)
API_KEY_ENV_VAR = "STEGASOO_API_KEY"
def _hash_key(key: str) -> str:
"""Hash an API key for storage."""
return hashlib.sha256(key.encode()).hexdigest()
def _get_keys_file(location: str = "user") -> Path:
"""Get path to API keys file."""
if location == "project":
return PROJECT_CONFIG_DIR / API_KEYS_FILE
return USER_CONFIG_DIR / API_KEYS_FILE
def _load_keys(location: str = "user") -> dict:
"""Load API keys from config file."""
keys_file = _get_keys_file(location)
if keys_file.exists():
try:
with open(keys_file) as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
return {"keys": [], "enabled": True}
return {"keys": [], "enabled": True}
def _save_keys(data: dict, location: str = "user") -> None:
"""Save API keys to config file."""
keys_file = _get_keys_file(location)
keys_file.parent.mkdir(parents=True, exist_ok=True)
with open(keys_file, "w") as f:
json.dump(data, f, indent=2)
# Secure permissions (owner read/write only)
os.chmod(keys_file, 0o600)
def generate_api_key() -> str:
"""Generate a new API key."""
# Format: stegasoo_XXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXX
# 32 bytes = 256 bits of entropy
random_part = secrets.token_hex(16)
return f"stegasoo_{random_part[:4]}_{random_part[4:]}"
def add_api_key(name: str, location: str = "user") -> str:
"""
Generate and store a new API key.
Args:
name: Descriptive name for the key (e.g., "laptop", "automation")
location: "user" or "project"
Returns:
The generated API key (only shown once!)
"""
key = generate_api_key()
key_hash = _hash_key(key)
data = _load_keys(location)
# Check for duplicate name
for existing in data["keys"]:
if existing["name"] == name:
raise ValueError(f"Key with name '{name}' already exists")
data["keys"].append(
{
"name": name,
"hash": key_hash,
"created": __import__("datetime").datetime.now().isoformat(),
}
)
_save_keys(data, location)
return key
def remove_api_key(name: str, location: str = "user") -> bool:
"""
Remove an API key by name.
Returns:
True if key was found and removed, False otherwise
"""
data = _load_keys(location)
original_count = len(data["keys"])
data["keys"] = [k for k in data["keys"] if k["name"] != name]
if len(data["keys"]) < original_count:
_save_keys(data, location)
return True
return False
def list_api_keys(location: str = "user") -> list[dict]:
"""
List all API keys (names and creation dates, not actual keys).
"""
data = _load_keys(location)
return [{"name": k["name"], "created": k.get("created", "unknown")} for k in data["keys"]]
def set_auth_enabled(enabled: bool, location: str = "user") -> None:
"""Enable or disable API key authentication."""
data = _load_keys(location)
data["enabled"] = enabled
_save_keys(data, location)
def is_auth_enabled() -> bool:
"""Check if API key authentication is enabled."""
# Check project config first, then user config
for location in ["project", "user"]:
data = _load_keys(location)
if "enabled" in data:
return data["enabled"]
# Default: enabled if any keys exist
return bool(get_all_key_hashes())
def get_all_key_hashes() -> set[str]:
"""Get all valid API key hashes from all sources."""
hashes = set()
# Check environment variable first
env_key = os.environ.get(API_KEY_ENV_VAR)
if env_key:
hashes.add(_hash_key(env_key))
# Check project and user configs
for location in ["project", "user"]:
data = _load_keys(location)
for key_entry in data.get("keys", []):
if "hash" in key_entry:
hashes.add(key_entry["hash"])
return hashes
def validate_api_key(key: str) -> bool:
"""Validate an API key against stored hashes."""
if not key:
return False
key_hash = _hash_key(key)
valid_hashes = get_all_key_hashes()
return key_hash in valid_hashes
def get_api_key_status() -> dict:
"""Get current API key authentication status."""
user_keys = list_api_keys("user")
project_keys = list_api_keys("project")
env_configured = bool(os.environ.get(API_KEY_ENV_VAR))
total_keys = len(user_keys) + len(project_keys) + (1 if env_configured else 0)
return {
"enabled": is_auth_enabled(),
"total_keys": total_keys,
"user_keys": len(user_keys),
"project_keys": len(project_keys),
"env_configured": env_configured,
"keys": {
"user": user_keys,
"project": project_keys,
},
}
# FastAPI dependency for API key authentication
async def require_api_key(api_key: str | None = Security(API_KEY_HEADER)) -> str:
"""
FastAPI dependency that requires a valid API key.
Usage:
@app.get("/protected")
async def endpoint(key: str = Depends(require_api_key)):
...
"""
# Check if auth is enabled
if not is_auth_enabled():
return "auth_disabled"
# No keys configured = auth disabled
if not get_all_key_hashes():
return "no_keys_configured"
# Validate the provided key
if not api_key:
raise HTTPException(
status_code=401,
detail="API key required. Provide X-API-Key header.",
headers={"WWW-Authenticate": "ApiKey"},
)
if not validate_api_key(api_key):
raise HTTPException(
status_code=403,
detail="Invalid API key.",
)
return api_key
async def optional_api_key(api_key: str | None = Security(API_KEY_HEADER)) -> str | None:
"""
FastAPI dependency that optionally validates API key.
Returns the key if valid, None if not provided or invalid.
Doesn't raise exceptions - useful for endpoints that work
with or without auth.
"""
if api_key and validate_api_key(api_key):
return api_key
return None

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,16 @@
# Stegasoo Web UI Configuration
# Copy this file to .env and customize
# Authentication (v4.0.2+)
STEGASOO_AUTH_ENABLED=true
STEGASOO_HTTPS_ENABLED=false
STEGASOO_HOSTNAME=localhost
STEGASOO_PORT=5000
# Channel Key (format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX)
# Generate with: stegasoo generate --channel-key
# Leave empty for public mode
STEGASOO_CHANNEL_KEY=
# Flask settings
FLASK_ENV=production

View File

@@ -0,0 +1,62 @@
# Subprocess Isolation for Stegasoo WebUI
This update runs encode/decode/compare operations in isolated subprocesses
to prevent jpegio/scipy crashes from taking down the Flask server.
## Files
- **app.py** - Updated Flask app using subprocess isolation
- **subprocess_stego.py** - Flask-side wrapper with clean API
- **stego_worker.py** - Subprocess script that does actual stegasoo operations
## Setup
1. Place all three files in your `webui/` directory (same level as templates/)
2. Make sure stego_worker.py is executable (optional):
```bash
chmod +x stego_worker.py
```
3. Run the Flask app:
```bash
python app.py
```
## How It Works
Instead of calling stegasoo functions directly in the Flask process:
```python
# OLD (crashes could kill Flask)
result = encode(...)
```
We now run them in subprocesses:
```python
# NEW (crashes only kill the subprocess)
result = subprocess_stego.encode(...)
```
If jpegio or scipy crashes due to memory corruption, only the subprocess
dies. Flask logs the error and continues running. The next request spawns
a fresh subprocess.
## Configuration
In `app.py`, you can adjust the timeout:
```python
subprocess_stego = SubprocessStego(timeout=180) # 3 minutes
```
Larger images may need longer timeouts.
## Troubleshooting
If you see "Worker script not found" errors, make sure `stego_worker.py`
is in the same directory as `app.py`.
If subprocess operations fail, check the Flask logs for error details.
The subprocess wrapper captures both stdout and stderr from the worker.

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,766 +0,0 @@
#!/usr/bin/env python3
"""
Stegasoo Web Frontend
Flask-based web UI for steganography operations.
Supports both text messages and file embedding.
"""
import io
import sys
import time
import secrets
import mimetypes
from pathlib import Path
from datetime import datetime
from PIL import Image
from flask import (
Flask, render_template, request, send_file,
jsonify, flash, redirect, url_for
)
# Add parent to path for development
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
import stegasoo
from stegasoo import (
encode, decode, generate_credentials,
export_rsa_key_pem, load_rsa_key,
validate_pin, validate_message, validate_image,
validate_rsa_key, validate_security_factors,
validate_file_payload,
get_today_day, generate_filename,
DAY_NAMES, __version__,
StegasooError, DecryptionError, CapacityError,
has_argon2,
FilePayload,
MAX_FILE_PAYLOAD_SIZE,
)
from stegasoo.constants import (
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
VALID_RSA_SIZES, MAX_FILE_SIZE,
)
# QR Code support
try:
import qrcode
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
# QR Code reading
try:
from pyzbar.pyzbar import decode as pyzbar_decode
HAS_QRCODE_READ = True
except ImportError:
HAS_QRCODE_READ = False
import zlib
import base64
# Import QR utilities
from stegasoo.qr_utils import (
compress_data, decompress_data, auto_decompress,
is_compressed, can_fit_in_qr, needs_compression,
generate_qr_code, read_qr_code, extract_key_from_qr,
has_qr_write, has_qr_read,
QR_MAX_BINARY, COMPRESSION_PREFIX
)
# ============================================================================
# FLASK APP CONFIGURATION
# ============================================================================
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 20MB max upload
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
TEMP_FILES: dict[str, dict] = {}
THUMBNAIL_FILES: dict[str, bytes] = {}
TEMP_FILE_EXPIRY = 300 # 5 minutes
THUMBNAIL_SIZE = (250, 250) # Maximum dimensions for thumbnail
# ============================================================================
# CONFIGURATION
# ============================================================================
# Override stegasoo limits for larger files
# Note: You might need to modify the stegasoo library itself
# to actually increase these limits in its internal calculations
# Flask upload limit (30MB)
MAX_UPLOAD_SIZE = 30 * 1024 * 1024
# Try to import and override stegasoo constants if possible
try:
# Check current limits
print(f"Current MAX_FILE_SIZE from constants: {MAX_FILE_SIZE}")
print(f"Current MAX_FILE_PAYLOAD_SIZE: {MAX_FILE_PAYLOAD_SIZE}")
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
# Note: You might need to patch the stegasoo module
# if MAX_FILE_PAYLOAD_SIZE is used internally
import stegasoo
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
print(f"Overriding MAX_FILE_PAYLOAD_SIZE to {DESIRED_PAYLOAD_SIZE}")
stegasoo.MAX_FILE_PAYLOAD_SIZE = DESIRED_PAYLOAD_SIZE
except Exception as e:
print(f"Could not override stegasoo limits: {e}")
def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
"""Generate thumbnail from image data."""
try:
with Image.open(io.BytesIO(image_data)) as img:
# Convert to RGB if necessary
if img.mode in ('RGBA', 'LA', 'P'):
# Create white background for transparent images
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'P':
img = img.convert('RGBA')
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
img = background
elif img.mode != 'RGB':
img = img.convert('RGB')
# Create thumbnail
img.thumbnail(size, Image.Resampling.LANCZOS)
# Save to bytes
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=85, optimize=True)
return buffer.getvalue()
except Exception as e:
# Log error but don't crash
print(f"Thumbnail generation error: {e}")
return None
def cleanup_temp_files():
"""Remove expired temporary files."""
now = time.time()
expired = [fid for fid, info in TEMP_FILES.items() if now - info['timestamp'] > TEMP_FILE_EXPIRY]
for fid in expired:
TEMP_FILES.pop(fid, None)
# Also clean up corresponding thumbnail
thumb_id = f"{fid}_thumb"
THUMBNAIL_FILES.pop(thumb_id, None)
def allowed_image(filename: str) -> bool:
"""Check if file has allowed image extension."""
if not filename or '.' not in filename:
return False
ext = filename.rsplit('.', 1)[1].lower()
return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
def format_size(size_bytes: int) -> str:
"""Format file size for display."""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes / (1024 * 1024):.1f} MB"
# ============================================================================
# ROUTES
# ============================================================================
@app.route('/')
def index():
return render_template('index.html')
@app.route('/generate', methods=['GET', 'POST'])
def generate():
if request.method == 'POST':
words_per_phrase = int(request.form.get('words_per_phrase', 3))
use_pin = request.form.get('use_pin') == 'on'
use_rsa = request.form.get('use_rsa') == 'on'
if not use_pin and not use_rsa:
flash('You must select at least one security factor (PIN or RSA Key)', 'error')
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
pin_length = int(request.form.get('pin_length', 6))
rsa_bits = int(request.form.get('rsa_bits', 2048))
# Clamp values
words_per_phrase = max(3, min(12, words_per_phrase))
pin_length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, pin_length))
if rsa_bits not in VALID_RSA_SIZES:
rsa_bits = 2048
try:
creds = generate_credentials(
use_pin=use_pin,
use_rsa=use_rsa,
pin_length=pin_length,
rsa_bits=rsa_bits,
words_per_phrase=words_per_phrase
)
# Store RSA key temporarily for QR generation
qr_token = None
qr_needs_compression = False
qr_too_large = False
if creds.rsa_key_pem and HAS_QRCODE:
# Check if key fits in QR code
if can_fit_in_qr(creds.rsa_key_pem, compress=True):
qr_needs_compression = True
else:
qr_too_large = True
if not qr_too_large:
qr_token = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[qr_token] = {
'data': creds.rsa_key_pem.encode(),
'filename': 'rsa_key.pem',
'timestamp': time.time(),
'type': 'rsa_key',
'compress': qr_needs_compression
}
return render_template('generate.html',
phrases=creds.phrases,
pin=creds.pin,
days=DAY_NAMES,
generated=True,
words_per_phrase=words_per_phrase,
pin_length=pin_length if use_pin else None,
use_pin=use_pin,
use_rsa=use_rsa,
rsa_bits=rsa_bits,
rsa_key_pem=creds.rsa_key_pem,
phrase_entropy=creds.phrase_entropy,
pin_entropy=creds.pin_entropy,
rsa_entropy=creds.rsa_entropy,
total_entropy=creds.total_entropy,
has_qrcode=HAS_QRCODE,
qr_token=qr_token,
qr_needs_compression=qr_needs_compression,
qr_too_large=qr_too_large
)
except Exception as e:
flash(f'Error generating credentials: {e}', 'error')
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
@app.route('/generate/qr/<token>')
def generate_qr(token):
"""Generate QR code for RSA key."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=False
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/qr-download/<token>')
def generate_qr_download(token):
"""Download QR code as PNG file."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=True,
download_name='stegasoo_rsa_key_qr.png'
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/download-key', methods=['POST'])
def download_key():
"""Download RSA key as password-protected PEM file."""
key_pem = request.form.get('key_pem', '')
password = request.form.get('key_password', '')
if not key_pem:
flash('No key to download', 'error')
return redirect(url_for('generate'))
if not password or len(password) < 8:
flash('Password must be at least 8 characters', 'error')
return redirect(url_for('generate'))
try:
private_key = load_rsa_key(key_pem.encode('utf-8'))
encrypted_pem = export_rsa_key_pem(private_key, password=password)
key_id = secrets.token_hex(4)
filename = f'stegasoo_key_{private_key.key_size}_{key_id}.pem'
return send_file(
io.BytesIO(encrypted_pem),
mimetype='application/x-pem-file',
as_attachment=True,
download_name=filename
)
except Exception as e:
flash(f'Error creating key file: {e}', 'error')
return redirect(url_for('generate'))
@app.route('/extract-key-from-qr', methods=['POST'])
def extract_key_from_qr_route():
"""
Extract RSA key from uploaded QR code image.
Returns JSON with the extracted key or error.
"""
if not HAS_QRCODE_READ:
return jsonify({
'success': False,
'error': 'QR code reading not available. Install pyzbar and libzbar.'
}), 501
qr_image = request.files.get('qr_image')
if not qr_image:
return jsonify({
'success': False,
'error': 'No QR image provided'
}), 400
try:
image_data = qr_image.read()
key_pem = extract_key_from_qr(image_data)
if key_pem:
return jsonify({
'success': True,
'key_pem': key_pem
})
else:
return jsonify({
'success': False,
'error': 'No valid RSA key found in QR code'
}), 400
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/encode', methods=['GET', 'POST'])
def encode_page():
day_of_week = get_today_day()
max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024
if request.method == 'POST':
try:
# Get files
ref_photo = request.files.get('reference_photo')
carrier = request.files.get('carrier')
rsa_key_file = request.files.get('rsa_key')
payload_file = request.files.get('payload_file')
if not ref_photo or not carrier:
flash('Both reference photo and carrier image are required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get form data
message = request.form.get('message', '')
day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
payload_type = request.form.get('payload_type', 'text')
# Determine payload
if payload_type == 'file' and payload_file and payload_file.filename:
# File payload
file_data = payload_file.read()
result = validate_file_payload(file_data, payload_file.filename)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
mime_type, _ = mimetypes.guess_type(payload_file.filename)
payload = FilePayload(
data=file_data,
filename=payload_file.filename,
mime_type=mime_type
)
else:
# Text message
result = validate_message(message)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
payload = message
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
carrier_data = carrier.read()
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate carrier image
result = validate_image(carrier_data, "Carrier image")
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get date
client_date = request.form.get('client_date', '').strip()
if client_date and len(client_date) == 10 and client_date[4] == '-' and client_date[7] == '-':
date_str = client_date
else:
date_str = datetime.now().strftime('%Y-%m-%d')
# Encode
encode_result = encode(
message=payload,
reference_photo=ref_data,
carrier_image=carrier_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=date_str
)
# Store temporarily
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[file_id] = {
'data': encode_result.stego_image,
'filename': encode_result.filename,
'timestamp': time.time()
}
return redirect(url_for('encode_result', file_id=file_id))
except CapacityError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
@app.route('/encode/result/<file_id>')
def encode_result(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found. Please encode again.', 'error')
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
# Generate thumbnail
thumbnail_data = generate_thumbnail(file_info['data'])
thumbnail_id = None
if thumbnail_data:
thumbnail_id = f"{file_id}_thumb"
THUMBNAIL_FILES[thumbnail_id] = thumbnail_data
return render_template('encode_result.html',
file_id=file_id,
filename=file_info['filename'],
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None
)
@app.route('/encode/thumbnail/<thumb_id>')
def encode_thumbnail(thumb_id):
"""Serve thumbnail image."""
if thumb_id not in THUMBNAIL_FILES:
return "Thumbnail not found", 404
return send_file(
io.BytesIO(THUMBNAIL_FILES[thumb_id]),
mimetype='image/jpeg',
as_attachment=False
)
@app.route('/encode/download/<file_id>')
def encode_download(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/encode/file/<file_id>')
def encode_file_route(file_id):
"""Serve file for Web Share API."""
if file_id not in TEMP_FILES:
return "Not found", 404
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=False,
download_name=file_info['filename']
)
@app.route('/encode/cleanup/<file_id>', methods=['POST'])
def encode_cleanup(file_id):
"""Manually cleanup a file after sharing."""
TEMP_FILES.pop(file_id, None)
# Also cleanup thumbnail if exists
thumb_id = f"{file_id}_thumb"
THUMBNAIL_FILES.pop(thumb_id, None)
return jsonify({'status': 'ok'})
@app.route('/decode', methods=['GET', 'POST'])
def decode_page():
if request.method == 'POST':
try:
# Get files
ref_photo = request.files.get('reference_photo')
stego_image = request.files.get('stego_image')
rsa_key_file = request.files.get('rsa_key')
if not ref_photo or not stego_image:
flash('Both reference photo and stego image are required', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Get form data
day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
stego_data = stego_image.read()
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Decode
decode_result = decode(
stego_image=stego_data,
reference_photo=ref_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password
)
if decode_result.is_file:
# File content - store temporarily for download
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
filename = decode_result.filename or 'decoded_file'
TEMP_FILES[file_id] = {
'data': decode_result.file_data,
'filename': filename,
'mime_type': decode_result.mime_type,
'timestamp': time.time()
}
return render_template('decode.html',
decoded_file=True,
file_id=file_id,
filename=filename,
file_size=format_size(len(decode_result.file_data)),
mime_type=decode_result.mime_type
)
else:
# Text content
return render_template('decode.html', decoded_message=decode_result.message)
except DecryptionError:
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
@app.route('/decode/download/<file_id>')
def decode_download(file_id):
"""Download decoded file."""
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('decode_page'))
file_info = TEMP_FILES[file_id]
mime_type = file_info.get('mime_type', 'application/octet-stream')
return send_file(
io.BytesIO(file_info['data']),
mimetype=mime_type,
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/about')
def about():
return render_template('about.html',
has_argon2=has_argon2(),
has_qrcode_read=HAS_QRCODE_READ,
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
)
# ============================================================================
# MAIN
# ============================================================================
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)

View File

@@ -1,781 +0,0 @@
#!/usr/bin/env python3
"""
Stegasoo Web Frontend
Flask-based web UI for steganography operations.
Supports both text messages and file embedding.
"""
import io
import sys
import time
import secrets
import mimetypes
from pathlib import Path
from datetime import datetime
from PIL import Image
from flask import (
Flask, render_template, request, send_file,
jsonify, flash, redirect, url_for
)
# Add parent to path for development
sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
import stegasoo
from stegasoo import (
encode, decode, generate_credentials,
export_rsa_key_pem, load_rsa_key,
validate_pin, validate_message, validate_image,
validate_rsa_key, validate_security_factors,
validate_file_payload,
get_today_day, generate_filename,
DAY_NAMES, __version__,
StegasooError, DecryptionError, CapacityError,
has_argon2,
FilePayload,
MAX_FILE_PAYLOAD_SIZE,
)
from stegasoo.constants import (
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
VALID_RSA_SIZES, MAX_FILE_SIZE,
)
# QR Code support
try:
import qrcode
from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_M
HAS_QRCODE = True
except ImportError:
HAS_QRCODE = False
# QR Code reading
try:
from pyzbar.pyzbar import decode as pyzbar_decode
HAS_QRCODE_READ = True
except ImportError:
HAS_QRCODE_READ = False
import zlib
import base64
# Import QR utilities
from stegasoo.qr_utils import (
compress_data, decompress_data, auto_decompress,
is_compressed, can_fit_in_qr, needs_compression,
generate_qr_code, read_qr_code, extract_key_from_qr,
has_qr_write, has_qr_read,
QR_MAX_BINARY, COMPRESSION_PREFIX
)
# ============================================================================
# FLASK APP CONFIGURATION
# ============================================================================
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 20MB max upload
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
TEMP_FILES: dict[str, dict] = {}
THUMBNAIL_FILES: dict[str, bytes] = {}
TEMP_FILE_EXPIRY = 300 # 5 minutes
THUMBNAIL_SIZE = (250, 250) # Maximum dimensions for thumbnail
# ============================================================================
# CONFIGURATION
# ============================================================================
# Override stegasoo limits for larger files
# Note: You might need to modify the stegasoo library itself
# to actually increase these limits in its internal calculations
# Flask upload limit (30MB)
MAX_UPLOAD_SIZE = 30 * 1024 * 1024
# Try to import and override stegasoo constants if possible
try:
# Check current limits
print(f"Current MAX_FILE_SIZE from constants: {MAX_FILE_SIZE}")
print(f"Current MAX_FILE_PAYLOAD_SIZE: {MAX_FILE_PAYLOAD_SIZE}")
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
# Note: You might need to patch the stegasoo module
# if MAX_FILE_PAYLOAD_SIZE is used internally
import stegasoo
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
print(f"Overriding MAX_FILE_PAYLOAD_SIZE to {DESIRED_PAYLOAD_SIZE}")
stegasoo.MAX_FILE_PAYLOAD_SIZE = DESIRED_PAYLOAD_SIZE
except Exception as e:
print(f"Could not override stegasoo limits: {e}")
def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
"""Generate thumbnail from image data."""
try:
with Image.open(io.BytesIO(image_data)) as img:
# Convert to RGB if necessary
if img.mode in ('RGBA', 'LA', 'P'):
# Create white background for transparent images
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'P':
img = img.convert('RGBA')
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
img = background
elif img.mode != 'RGB':
img = img.convert('RGB')
# Create thumbnail
img.thumbnail(size, Image.Resampling.LANCZOS)
# Save to bytes
buffer = io.BytesIO()
img.save(buffer, format='JPEG', quality=85, optimize=True)
return buffer.getvalue()
except Exception as e:
# Log error but don't crash
print(f"Thumbnail generation error: {e}")
return None
def cleanup_temp_files():
"""Remove expired temporary files."""
now = time.time()
expired = [fid for fid, info in TEMP_FILES.items() if now - info['timestamp'] > TEMP_FILE_EXPIRY]
for fid in expired:
TEMP_FILES.pop(fid, None)
# Also clean up corresponding thumbnail
thumb_id = f"{fid}_thumb"
THUMBNAIL_FILES.pop(thumb_id, None)
def allowed_image(filename: str) -> bool:
"""Check if file has allowed image extension."""
if not filename or '.' not in filename:
return False
ext = filename.rsplit('.', 1)[1].lower()
return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
def format_size(size_bytes: int) -> str:
"""Format file size for display."""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
else:
return f"{size_bytes / (1024 * 1024):.1f} MB"
# ============================================================================
# ROUTES
# ============================================================================
@app.route('/')
def index():
return render_template('index.html')
@app.route('/generate', methods=['GET', 'POST'])
def generate():
if request.method == 'POST':
words_per_phrase = int(request.form.get('words_per_phrase', 3))
use_pin = request.form.get('use_pin') == 'on'
use_rsa = request.form.get('use_rsa') == 'on'
if not use_pin and not use_rsa:
flash('You must select at least one security factor (PIN or RSA Key)', 'error')
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
pin_length = int(request.form.get('pin_length', 6))
rsa_bits = int(request.form.get('rsa_bits', 2048))
# Clamp values
words_per_phrase = max(3, min(12, words_per_phrase))
pin_length = max(MIN_PIN_LENGTH, min(MAX_PIN_LENGTH, pin_length))
if rsa_bits not in VALID_RSA_SIZES:
rsa_bits = 2048
try:
creds = generate_credentials(
use_pin=use_pin,
use_rsa=use_rsa,
pin_length=pin_length,
rsa_bits=rsa_bits,
words_per_phrase=words_per_phrase
)
# Store RSA key temporarily for QR generation
qr_token = None
qr_needs_compression = False
qr_too_large = False
if creds.rsa_key_pem and HAS_QRCODE:
# Check if key fits in QR code
if can_fit_in_qr(creds.rsa_key_pem, compress=True):
qr_needs_compression = True
else:
qr_too_large = True
if not qr_too_large:
qr_token = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[qr_token] = {
'data': creds.rsa_key_pem.encode(),
'filename': 'rsa_key.pem',
'timestamp': time.time(),
'type': 'rsa_key',
'compress': qr_needs_compression
}
return render_template('generate.html',
phrases=creds.phrases,
pin=creds.pin,
days=DAY_NAMES,
generated=True,
words_per_phrase=words_per_phrase,
pin_length=pin_length if use_pin else None,
use_pin=use_pin,
use_rsa=use_rsa,
rsa_bits=rsa_bits,
rsa_key_pem=creds.rsa_key_pem,
phrase_entropy=creds.phrase_entropy,
pin_entropy=creds.pin_entropy,
rsa_entropy=creds.rsa_entropy,
total_entropy=creds.total_entropy,
has_qrcode=HAS_QRCODE,
qr_token=qr_token,
qr_needs_compression=qr_needs_compression,
qr_too_large=qr_too_large
)
except Exception as e:
flash(f'Error generating credentials: {e}', 'error')
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
return render_template('generate.html', generated=False, has_qrcode=HAS_QRCODE)
@app.route('/generate/qr/<token>')
def generate_qr(token):
"""Generate QR code for RSA key."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=False
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/qr-download/<token>')
def generate_qr_download(token):
"""Download QR code as PNG file."""
if not HAS_QRCODE:
return "QR code support not available", 501
if token not in TEMP_FILES:
return "Token expired or invalid", 404
file_info = TEMP_FILES[token]
if file_info.get('type') != 'rsa_key':
return "Invalid token type", 400
try:
key_pem = file_info['data'].decode('utf-8')
compress = file_info.get('compress', False)
qr_png = generate_qr_code(key_pem, compress=compress)
return send_file(
io.BytesIO(qr_png),
mimetype='image/png',
as_attachment=True,
download_name='stegasoo_rsa_key_qr.png'
)
except Exception as e:
return f"Error generating QR code: {e}", 500
@app.route('/generate/download-key', methods=['POST'])
def download_key():
"""Download RSA key as password-protected PEM file."""
key_pem = request.form.get('key_pem', '')
password = request.form.get('key_password', '')
if not key_pem:
flash('No key to download', 'error')
return redirect(url_for('generate'))
if not password or len(password) < 8:
flash('Password must be at least 8 characters', 'error')
return redirect(url_for('generate'))
try:
private_key = load_rsa_key(key_pem.encode('utf-8'))
encrypted_pem = export_rsa_key_pem(private_key, password=password)
key_id = secrets.token_hex(4)
filename = f'stegasoo_key_{private_key.key_size}_{key_id}.pem'
return send_file(
io.BytesIO(encrypted_pem),
mimetype='application/x-pem-file',
as_attachment=True,
download_name=filename
)
except Exception as e:
flash(f'Error creating key file: {e}', 'error')
return redirect(url_for('generate'))
@app.route('/extract-key-from-qr', methods=['POST'])
def extract_key_from_qr_route():
"""
Extract RSA key from uploaded QR code image.
Returns JSON with the extracted key or error.
"""
if not HAS_QRCODE_READ:
return jsonify({
'success': False,
'error': 'QR code reading not available. Install pyzbar and libzbar.'
}), 501
qr_image = request.files.get('qr_image')
if not qr_image:
return jsonify({
'success': False,
'error': 'No QR image provided'
}), 400
try:
image_data = qr_image.read()
key_pem = extract_key_from_qr(image_data)
if key_pem:
return jsonify({
'success': True,
'key_pem': key_pem
})
else:
return jsonify({
'success': False,
'error': 'No valid RSA key found in QR code'
}), 400
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/encode', methods=['GET', 'POST'])
def encode_page():
day_of_week = get_today_day()
max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024
if request.method == 'POST':
try:
# Get files
ref_photo = request.files.get('reference_photo')
carrier = request.files.get('carrier')
rsa_key_file = request.files.get('rsa_key')
payload_file = request.files.get('payload_file')
if not ref_photo or not carrier:
flash('Both reference photo and carrier image are required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get form data
message = request.form.get('message', '')
day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
payload_type = request.form.get('payload_type', 'text')
# Determine payload
if payload_type == 'file' and payload_file and payload_file.filename:
# File payload
file_data = payload_file.read()
result = validate_file_payload(file_data, payload_file.filename)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
mime_type, _ = mimetypes.guess_type(payload_file.filename)
payload = FilePayload(
data=file_data,
filename=payload_file.filename,
mime_type=mime_type
)
else:
# Text message
result = validate_message(message)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
payload = message
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
carrier_data = carrier.read()
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Validate carrier image
result = validate_image(carrier_data, "Carrier image")
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
# Get date
client_date = request.form.get('client_date', '').strip()
if client_date and len(client_date) == 10 and client_date[4] == '-' and client_date[7] == '-':
date_str = client_date
else:
date_str = datetime.now().strftime('%Y-%m-%d')
# Encode
encode_result = encode(
message=payload,
reference_photo=ref_data,
carrier_image=carrier_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=date_str
)
# Store temporarily
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
TEMP_FILES[file_id] = {
'data': encode_result.stego_image,
'filename': encode_result.filename,
'timestamp': time.time()
}
return redirect(url_for('encode_result', file_id=file_id))
except CapacityError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb, has_qrcode_read=HAS_QRCODE_READ)
@app.route('/encode/result/<file_id>')
def encode_result(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found. Please encode again.', 'error')
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
# Generate thumbnail
thumbnail_data = generate_thumbnail(file_info['data'])
thumbnail_id = None
if thumbnail_data:
thumbnail_id = f"{file_id}_thumb"
THUMBNAIL_FILES[thumbnail_id] = thumbnail_data
return render_template('encode_result.html',
file_id=file_id,
filename=file_info['filename'],
thumbnail_url=url_for('encode_thumbnail', thumb_id=thumbnail_id) if thumbnail_id else None
)
@app.route('/encode/thumbnail/<thumb_id>')
def encode_thumbnail(thumb_id):
"""Serve thumbnail image."""
if thumb_id not in THUMBNAIL_FILES:
return "Thumbnail not found", 404
return send_file(
io.BytesIO(THUMBNAIL_FILES[thumb_id]),
mimetype='image/jpeg',
as_attachment=False
)
@app.route('/encode/download/<file_id>')
def encode_download(file_id):
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('encode_page'))
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/encode/file/<file_id>')
def encode_file_route(file_id):
"""Serve file for Web Share API."""
if file_id not in TEMP_FILES:
return "Not found", 404
file_info = TEMP_FILES[file_id]
return send_file(
io.BytesIO(file_info['data']),
mimetype='image/png',
as_attachment=False,
download_name=file_info['filename']
)
@app.route('/encode/cleanup/<file_id>', methods=['POST'])
def encode_cleanup(file_id):
"""Manually cleanup a file after sharing."""
TEMP_FILES.pop(file_id, None)
# Also cleanup thumbnail if exists
thumb_id = f"{file_id}_thumb"
THUMBNAIL_FILES.pop(thumb_id, None)
return jsonify({'status': 'ok'})
@app.route('/decode', methods=['GET', 'POST'])
def decode_page():
if request.method == 'POST':
try:
# Get files
ref_photo = request.files.get('reference_photo')
stego_image = request.files.get('stego_image')
rsa_key_file = request.files.get('rsa_key')
if not ref_photo or not stego_image:
flash('Both reference photo and stego image are required', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Get form data
day_phrase = request.form.get('day_phrase', '')
pin = request.form.get('pin', '').strip()
rsa_password = request.form.get('rsa_password', '')
# Get encoding date from form (detected from filename in JS)
stego_date = request.form.get('stego_date', '').strip()
if not day_phrase:
flash('Day phrase is required', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Read files
ref_data = ref_photo.read()
stego_data = stego_image.read()
# Handle RSA key - can come from .pem file or QR code image
rsa_key_data = None
rsa_key_qr = request.files.get('rsa_key_qr')
rsa_key_from_qr = False # Track source for password handling
if rsa_key_file and rsa_key_file.filename:
# RSA key from .pem file
rsa_key_data = rsa_key_file.read()
elif rsa_key_qr and rsa_key_qr.filename and HAS_QRCODE_READ:
# RSA key from QR code image
qr_image_data = rsa_key_qr.read()
key_pem = extract_key_from_qr(qr_image_data)
if key_pem:
rsa_key_data = key_pem.encode('utf-8')
rsa_key_from_qr = True # QR keys are never password-protected
else:
flash('Could not extract RSA key from QR code image. Make sure the image contains a valid QR code.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate security factors
result = validate_security_factors(pin, rsa_key_data)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Validate PIN if provided
if pin:
result = validate_pin(pin)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
# Determine key password - QR code keys are never password-protected
key_password = None if rsa_key_from_qr else (rsa_password if rsa_password else None)
# Validate RSA key if provided
if rsa_key_data:
result = validate_rsa_key(rsa_key_data, key_password)
if not result.is_valid:
flash(result.error_message, 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
with open('/tmp/debug_stego.png', 'wb') as f:
f.write(stego_data)
with open('/tmp/debug_ref.png', 'wb') as f:
f.write(ref_data)
with open('/tmp/debug_params.txt', 'w') as f:
f.write(f"day_phrase: {day_phrase}\n")
f.write(f"pin: {pin}\n")
f.write(f"date_str: {stego_date}\n")
f.write(f"rsa_key: {len(rsa_key_data) if rsa_key_data else None}\n")
print(f"DEBUG: Saved inputs to /tmp/debug_*")
# Decode
decode_result = decode(
stego_image=stego_data,
reference_photo=ref_data,
day_phrase=day_phrase,
pin=pin,
rsa_key_data=rsa_key_data,
rsa_password=key_password,
date_str=stego_date if stego_date else None
)
if decode_result.is_file:
# File content - store temporarily for download
file_id = secrets.token_urlsafe(16)
cleanup_temp_files()
filename = decode_result.filename or 'decoded_file'
TEMP_FILES[file_id] = {
'data': decode_result.file_data,
'filename': filename,
'mime_type': decode_result.mime_type,
'timestamp': time.time()
}
return render_template('decode.html',
decoded_file=True,
file_id=file_id,
filename=filename,
file_size=format_size(len(decode_result.file_data)),
mime_type=decode_result.mime_type
)
else:
# Text content
return render_template('decode.html', decoded_message=decode_result.message)
except DecryptionError:
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except StegasooError as e:
flash(str(e), 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
except Exception as e:
flash(f'Error: {e}', 'error')
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
@app.route('/decode/download/<file_id>')
def decode_download(file_id):
"""Download decoded file."""
if file_id not in TEMP_FILES:
flash('File expired or not found.', 'error')
return redirect(url_for('decode_page'))
file_info = TEMP_FILES[file_id]
mime_type = file_info.get('mime_type', 'application/octet-stream')
return send_file(
io.BytesIO(file_info['data']),
mimetype=mime_type,
as_attachment=True,
download_name=file_info['filename']
)
@app.route('/about')
def about():
return render_template('about.html',
has_argon2=has_argon2(),
has_qrcode_read=HAS_QRCODE_READ,
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
)
# ============================================================================
# MAIN
# ============================================================================
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)

963
frontends/web/auth.py Normal file
View File

@@ -0,0 +1,963 @@
"""
Stegasoo Authentication Module (v4.1.0)
Multi-user authentication with role-based access control.
- Admin user created at first-run setup
- Admin can create up to 16 additional users
- Uses Argon2id password hashing
- Flask sessions for authentication state
- SQLite3 for user storage
"""
import functools
import secrets
import sqlite3
import string
from dataclasses import dataclass
from pathlib import Path
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
from flask import current_app, flash, g, redirect, session, url_for
# Argon2 password hasher (lighter than stegasoo's 256MB for faster login)
ph = PasswordHasher(
time_cost=3,
memory_cost=65536, # 64MB
parallelism=4,
hash_len=32,
salt_len=16,
)
# Constants
MAX_USERS = 16 # Plus 1 admin = 17 total
MAX_CHANNEL_KEYS = 10 # Per user
ROLE_ADMIN = "admin"
ROLE_USER = "user"
@dataclass
class User:
"""User data class."""
id: int
username: str
role: str
created_at: str
@property
def is_admin(self) -> bool:
return self.role == ROLE_ADMIN
def get_db_path() -> Path:
"""Get database path in Flask instance folder."""
instance_path = Path(current_app.instance_path)
instance_path.mkdir(parents=True, exist_ok=True)
return instance_path / "stegasoo.db"
def get_db() -> sqlite3.Connection:
"""Get database connection, cached on Flask g object."""
if "db" not in g:
g.db = sqlite3.connect(get_db_path())
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
"""Close database connection at end of request."""
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
"""Initialize database schema with migration support."""
db = get_db()
# Check if we need to migrate from old single-user schema
cursor = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='admin_user'")
has_old_table = cursor.fetchone() is not None
cursor = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
has_new_table = cursor.fetchone() is not None
if has_old_table and not has_new_table:
# Migrate from old schema
_migrate_from_single_user(db)
elif not has_new_table:
# Fresh install - create new schema
_create_schema(db)
else:
# Existing install - check for new tables (migrations)
_ensure_channel_keys_table(db)
_ensure_app_settings_table(db)
def _create_schema(db: sqlite3.Connection):
"""Create the multi-user schema."""
db.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE TABLE IF NOT EXISTS user_channel_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
channel_key TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_used_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, channel_key)
);
CREATE INDEX IF NOT EXISTS idx_channel_keys_user ON user_channel_keys(user_id);
-- App-level settings (v4.1.0)
-- Stores recovery key hash and other instance-wide settings
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
""")
db.commit()
def _migrate_from_single_user(db: sqlite3.Connection):
"""Migrate from old single-user admin_user table to multi-user users table."""
# Create new table
_create_schema(db)
# Copy admin user from old table
old_user = db.execute(
"SELECT username, password_hash, created_at FROM admin_user WHERE id = 1"
).fetchone()
if old_user:
db.execute(
"""
INSERT INTO users (username, password_hash, role, created_at)
VALUES (?, ?, 'admin', ?)
""",
(old_user["username"], old_user["password_hash"], old_user["created_at"]),
)
db.commit()
# Drop old table
db.execute("DROP TABLE admin_user")
db.commit()
def _ensure_channel_keys_table(db: sqlite3.Connection):
"""Ensure user_channel_keys table exists (migration for existing installs)."""
cursor = db.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='user_channel_keys'"
)
if cursor.fetchone() is None:
db.executescript("""
CREATE TABLE IF NOT EXISTS user_channel_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
channel_key TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_used_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, channel_key)
);
CREATE INDEX IF NOT EXISTS idx_channel_keys_user ON user_channel_keys(user_id);
""")
db.commit()
def _ensure_app_settings_table(db: sqlite3.Connection):
"""Ensure app_settings table exists (v4.1.0 migration)."""
cursor = db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='app_settings'")
if cursor.fetchone() is None:
db.executescript("""
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
""")
db.commit()
# =============================================================================
# App Settings (v4.1.0)
# =============================================================================
def get_app_setting(key: str) -> str | None:
"""Get an app-level setting value."""
db = get_db()
row = db.execute("SELECT value FROM app_settings WHERE key = ?", (key,)).fetchone()
return row["value"] if row else None
def set_app_setting(key: str, value: str) -> None:
"""Set an app-level setting value."""
db = get_db()
db.execute(
"""
INSERT INTO app_settings (key, value)
VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP
""",
(key, value, value),
)
db.commit()
def delete_app_setting(key: str) -> bool:
"""Delete an app-level setting. Returns True if deleted."""
db = get_db()
cursor = db.execute("DELETE FROM app_settings WHERE key = ?", (key,))
db.commit()
return cursor.rowcount > 0
# =============================================================================
# Recovery Key Management (v4.1.0)
# =============================================================================
# Setting key for recovery hash
RECOVERY_KEY_SETTING = "recovery_key_hash"
def has_recovery_key() -> bool:
"""Check if a recovery key has been configured."""
return get_app_setting(RECOVERY_KEY_SETTING) is not None
def get_recovery_key_hash() -> str | None:
"""Get the stored recovery key hash."""
return get_app_setting(RECOVERY_KEY_SETTING)
def set_recovery_key_hash(key_hash: str) -> None:
"""Store a recovery key hash."""
set_app_setting(RECOVERY_KEY_SETTING, key_hash)
def clear_recovery_key() -> bool:
"""Remove the recovery key. Returns True if removed."""
return delete_app_setting(RECOVERY_KEY_SETTING)
def verify_and_reset_admin_password(recovery_key: str, new_password: str) -> tuple[bool, str]:
"""
Verify recovery key and reset the first admin's password.
Args:
recovery_key: User-provided recovery key
new_password: New password to set
Returns:
(success, message) tuple
"""
from stegasoo.recovery import verify_recovery_key
stored_hash = get_recovery_key_hash()
if not stored_hash:
return False, "No recovery key configured for this instance"
if not verify_recovery_key(recovery_key, stored_hash):
return False, "Invalid recovery key"
# Find first admin user
db = get_db()
admin = db.execute(
"SELECT id, username FROM users WHERE role = 'admin' ORDER BY id LIMIT 1"
).fetchone()
if not admin:
return False, "No admin user found"
# Reset password
new_hash = ph.hash(new_password)
db.execute(
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
(new_hash, admin["id"]),
)
db.commit()
# Invalidate all sessions for this user
invalidate_user_sessions(admin["id"])
return True, f"Password reset for '{admin['username']}'"
# =============================================================================
# User Queries
# =============================================================================
def any_users_exist() -> bool:
"""Check if any users have been created (for first-run detection)."""
db = get_db()
result = db.execute("SELECT 1 FROM users LIMIT 1").fetchone()
return result is not None
def user_exists() -> bool:
"""Alias for any_users_exist() for backwards compatibility."""
return any_users_exist()
def get_user_count() -> int:
"""Get total number of users."""
db = get_db()
result = db.execute("SELECT COUNT(*) FROM users").fetchone()
return result[0] if result else 0
def get_non_admin_count() -> int:
"""Get number of non-admin users."""
db = get_db()
result = db.execute("SELECT COUNT(*) FROM users WHERE role != 'admin'").fetchone()
return result[0] if result else 0
def can_create_user() -> bool:
"""Check if we can create more users (within limit)."""
return get_non_admin_count() < MAX_USERS
def get_user_by_id(user_id: int) -> User | None:
"""Get user by ID."""
db = get_db()
row = db.execute(
"SELECT id, username, role, created_at FROM users WHERE id = ?", (user_id,)
).fetchone()
if row:
return User(
id=row["id"],
username=row["username"],
role=row["role"],
created_at=row["created_at"],
)
return None
def get_user_by_username(username: str) -> User | None:
"""Get user by username."""
db = get_db()
row = db.execute(
"SELECT id, username, role, created_at FROM users WHERE username = ?",
(username,),
).fetchone()
if row:
return User(
id=row["id"],
username=row["username"],
role=row["role"],
created_at=row["created_at"],
)
return None
def get_all_users() -> list[User]:
"""Get all users, admins first, then by creation date."""
db = get_db()
rows = db.execute("""
SELECT id, username, role, created_at FROM users
ORDER BY role = 'admin' DESC, created_at ASC
""").fetchall()
return [
User(
id=row["id"],
username=row["username"],
role=row["role"],
created_at=row["created_at"],
)
for row in rows
]
def get_current_user() -> User | None:
"""Get the currently logged-in user from session."""
user_id = session.get("user_id")
if user_id:
return get_user_by_id(user_id)
return None
def get_username() -> str:
"""Get current user's username (backwards compatibility)."""
user = get_current_user()
return user.username if user else "unknown"
# =============================================================================
# Authentication
# =============================================================================
def verify_user_password(username: str, password: str) -> User | None:
"""
Verify password for a user.
Returns User if valid, None if invalid.
Also rehashes password if needed.
"""
db = get_db()
row = db.execute(
"SELECT id, username, role, created_at, password_hash FROM users WHERE username = ?",
(username,),
).fetchone()
if not row:
return None
try:
ph.verify(row["password_hash"], password)
# Rehash if parameters changed
if ph.check_needs_rehash(row["password_hash"]):
new_hash = ph.hash(password)
db.execute(
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
(new_hash, row["id"]),
)
db.commit()
return User(
id=row["id"],
username=row["username"],
role=row["role"],
created_at=row["created_at"],
)
except VerifyMismatchError:
return None
def verify_password(password: str) -> bool:
"""Verify password for current user (backwards compatibility)."""
user = get_current_user()
if not user:
return False
result = verify_user_password(user.username, password)
return result is not None
def is_authenticated() -> bool:
"""Check if current session is authenticated."""
return session.get("user_id") is not None
def is_admin() -> bool:
"""Check if current user is an admin."""
user = get_current_user()
return user.is_admin if user else False
def login_user(user: User):
"""Set up session for logged-in user."""
session["user_id"] = user.id
session["username"] = user.username
session["role"] = user.role
# Legacy compatibility
session["authenticated"] = True
def logout_user():
"""Clear session for logout."""
session.clear()
# =============================================================================
# User Management
# =============================================================================
def generate_temp_password(length: int = 8) -> str:
"""Generate a random temporary password."""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length))
def validate_username(username: str) -> tuple[bool, str]:
"""
Validate username format.
Rules: 3-80 chars, alphanumeric + underscore/hyphen + @/. for email-style
"""
if not username:
return False, "Username is required"
if len(username) < 3:
return False, "Username must be at least 3 characters"
if len(username) > 80:
return False, "Username must be at most 80 characters"
# Allow: alphanumeric, underscore, hyphen, @, . (for email-style)
allowed = set(string.ascii_letters + string.digits + "_-@.")
if not all(c in allowed for c in username):
return False, "Username can only contain letters, numbers, underscore, hyphen, @ and ."
# Must start with letter or number
if username[0] not in string.ascii_letters + string.digits:
return False, "Username must start with a letter or number"
return True, ""
def validate_password(password: str) -> tuple[bool, str]:
"""Validate password requirements."""
if not password:
return False, "Password is required"
if len(password) < 8:
return False, "Password must be at least 8 characters"
return True, ""
def create_user(
username: str, password: str, role: str = ROLE_USER
) -> tuple[bool, str, User | None]:
"""
Create a new user.
Returns (success, message, user).
"""
# Validate username
valid, msg = validate_username(username)
if not valid:
return False, msg, None
# Validate password
valid, msg = validate_password(password)
if not valid:
return False, msg, None
# Check if username already exists
if get_user_by_username(username):
return False, "Username already exists", None
# Check user limit (only for non-admin users)
if role != ROLE_ADMIN and not can_create_user():
return False, f"Maximum of {MAX_USERS} users reached", None
# Create user
password_hash = ph.hash(password)
db = get_db()
try:
cursor = db.execute(
"""
INSERT INTO users (username, password_hash, role)
VALUES (?, ?, ?)
""",
(username, password_hash, role),
)
db.commit()
user = get_user_by_id(cursor.lastrowid)
return True, "User created successfully", user
except sqlite3.IntegrityError:
return False, "Username already exists", None
def create_admin_user(username: str, password: str) -> tuple[bool, str]:
"""Create the initial admin user (first-run setup)."""
if any_users_exist():
return False, "Admin user already exists"
success, msg, _ = create_user(username, password, ROLE_ADMIN)
return success, msg
def change_password(user_id: int, current_password: str, new_password: str) -> tuple[bool, str]:
"""Change a user's password (requires current password)."""
user = get_user_by_id(user_id)
if not user:
return False, "User not found"
# Verify current password
if not verify_user_password(user.username, current_password):
return False, "Current password is incorrect"
# Validate new password
valid, msg = validate_password(new_password)
if not valid:
return False, msg
# Update password
new_hash = ph.hash(new_password)
db = get_db()
db.execute(
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
(new_hash, user_id),
)
db.commit()
return True, "Password changed successfully"
def reset_user_password(user_id: int, new_password: str) -> tuple[bool, str]:
"""Reset a user's password (admin function, no current password required)."""
user = get_user_by_id(user_id)
if not user:
return False, "User not found"
# Validate new password
valid, msg = validate_password(new_password)
if not valid:
return False, msg
# Update password
new_hash = ph.hash(new_password)
db = get_db()
db.execute(
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
(new_hash, user_id),
)
db.commit()
# Invalidate user's sessions
invalidate_user_sessions(user_id)
return True, "Password reset successfully"
def delete_user(user_id: int, current_user_id: int) -> tuple[bool, str]:
"""
Delete a user.
Cannot delete yourself or the last admin.
"""
if user_id == current_user_id:
return False, "Cannot delete yourself"
user = get_user_by_id(user_id)
if not user:
return False, "User not found"
# Check if this is the last admin
if user.role == ROLE_ADMIN:
db = get_db()
admin_count = db.execute("SELECT COUNT(*) FROM users WHERE role = 'admin'").fetchone()[0]
if admin_count <= 1:
return False, "Cannot delete the last admin"
# Invalidate user's sessions before deletion
invalidate_user_sessions(user_id)
# Delete user
db = get_db()
db.execute("DELETE FROM users WHERE id = ?", (user_id,))
db.commit()
return True, f"User '{user.username}' deleted"
def invalidate_user_sessions(user_id: int):
"""
Invalidate all sessions for a user.
This is called when a user is deleted or their password is reset.
Since we use server-side sessions, we increment a "session version"
that's checked on each request.
"""
# For Flask's default session (client-side), we can't truly invalidate.
# But we can add a check - store a "valid_from" timestamp in the DB
# and compare against session creation time.
#
# For now, we'll use a simpler approach: store invalidated user IDs
# in app config (memory) which gets checked by login_required.
#
# This works for single-process deployments (like RPi).
# For multi-process, would need Redis or DB-backed sessions.
if "invalidated_users" not in current_app.config:
current_app.config["invalidated_users"] = set()
current_app.config["invalidated_users"].add(user_id)
def is_session_valid() -> bool:
"""Check if current session is still valid (user not deleted/invalidated)."""
user_id = session.get("user_id")
if not user_id:
return False
# Check if user was invalidated
invalidated = current_app.config.get("invalidated_users", set())
if user_id in invalidated:
return False
# Check if user still exists
if not get_user_by_id(user_id):
return False
return True
# =============================================================================
# Channel Keys
# =============================================================================
@dataclass
class ChannelKey:
"""Saved channel key data class."""
id: int
user_id: int
name: str
channel_key: str
created_at: str
last_used_at: str | None
def get_user_channel_keys(user_id: int) -> list[ChannelKey]:
"""Get all saved channel keys for a user, most recently used first."""
db = get_db()
rows = db.execute(
"""
SELECT id, user_id, name, channel_key, created_at, last_used_at
FROM user_channel_keys
WHERE user_id = ?
ORDER BY last_used_at DESC NULLS LAST, created_at DESC
""",
(user_id,),
).fetchall()
return [
ChannelKey(
id=row["id"],
user_id=row["user_id"],
name=row["name"],
channel_key=row["channel_key"],
created_at=row["created_at"],
last_used_at=row["last_used_at"],
)
for row in rows
]
def get_channel_key_by_id(key_id: int, user_id: int) -> ChannelKey | None:
"""Get a specific channel key (ensures user owns it)."""
db = get_db()
row = db.execute(
"""
SELECT id, user_id, name, channel_key, created_at, last_used_at
FROM user_channel_keys
WHERE id = ? AND user_id = ?
""",
(key_id, user_id),
).fetchone()
if row:
return ChannelKey(
id=row["id"],
user_id=row["user_id"],
name=row["name"],
channel_key=row["channel_key"],
created_at=row["created_at"],
last_used_at=row["last_used_at"],
)
return None
def get_channel_key_count(user_id: int) -> int:
"""Get count of saved channel keys for a user."""
db = get_db()
result = db.execute(
"SELECT COUNT(*) FROM user_channel_keys WHERE user_id = ?", (user_id,)
).fetchone()
return result[0] if result else 0
def can_save_channel_key(user_id: int) -> bool:
"""Check if user can save more channel keys (within limit)."""
return get_channel_key_count(user_id) < MAX_CHANNEL_KEYS
def save_channel_key(
user_id: int, name: str, channel_key: str
) -> tuple[bool, str, ChannelKey | None]:
"""
Save a channel key for a user.
Returns (success, message, key).
"""
# Validate name
name = name.strip()
if not name:
return False, "Key name is required", None
if len(name) > 50:
return False, "Key name must be at most 50 characters", None
# Validate channel key format (hex string)
channel_key = channel_key.strip().lower()
if not channel_key:
return False, "Channel key is required", None
if not all(c in "0123456789abcdef" for c in channel_key):
return False, "Invalid channel key format", None
# Check limit
if not can_save_channel_key(user_id):
return False, f"Maximum of {MAX_CHANNEL_KEYS} saved keys reached", None
db = get_db()
try:
cursor = db.execute(
"""
INSERT INTO user_channel_keys (user_id, name, channel_key)
VALUES (?, ?, ?)
""",
(user_id, name, channel_key),
)
db.commit()
key = get_channel_key_by_id(cursor.lastrowid, user_id)
return True, "Channel key saved", key
except sqlite3.IntegrityError:
return False, "This channel key is already saved", None
def update_channel_key_name(key_id: int, user_id: int, new_name: str) -> tuple[bool, str]:
"""Update the name of a saved channel key."""
new_name = new_name.strip()
if not new_name:
return False, "Key name is required"
if len(new_name) > 50:
return False, "Key name must be at most 50 characters"
key = get_channel_key_by_id(key_id, user_id)
if not key:
return False, "Channel key not found"
db = get_db()
db.execute(
"UPDATE user_channel_keys SET name = ? WHERE id = ? AND user_id = ?",
(new_name, key_id, user_id),
)
db.commit()
return True, "Key name updated"
def update_channel_key_last_used(key_id: int, user_id: int):
"""Update the last_used_at timestamp for a channel key."""
db = get_db()
db.execute(
"""
UPDATE user_channel_keys
SET last_used_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?
""",
(key_id, user_id),
)
db.commit()
def delete_channel_key(key_id: int, user_id: int) -> tuple[bool, str]:
"""Delete a saved channel key."""
key = get_channel_key_by_id(key_id, user_id)
if not key:
return False, "Channel key not found"
db = get_db()
db.execute(
"DELETE FROM user_channel_keys WHERE id = ? AND user_id = ?",
(key_id, user_id),
)
db.commit()
return True, f"Key '{key.name}' deleted"
# =============================================================================
# Decorators
# =============================================================================
def login_required(f):
"""Decorator to require login for a route."""
@functools.wraps(f)
def decorated_function(*args, **kwargs):
# Check if auth is enabled
if not current_app.config.get("AUTH_ENABLED", True):
return f(*args, **kwargs)
# Check for first-run setup
if not any_users_exist():
return redirect(url_for("setup"))
# Check authentication
if not is_authenticated():
return redirect(url_for("login"))
# Check if session is still valid (user not deleted)
if not is_session_valid():
logout_user()
flash("Your session has expired. Please log in again.", "warning")
return redirect(url_for("login"))
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
"""Decorator to require admin role for a route."""
@functools.wraps(f)
def decorated_function(*args, **kwargs):
# Check if auth is enabled
if not current_app.config.get("AUTH_ENABLED", True):
return f(*args, **kwargs)
# Check for first-run setup
if not any_users_exist():
return redirect(url_for("setup"))
# Check authentication
if not is_authenticated():
return redirect(url_for("login"))
# Check if session is still valid
if not is_session_valid():
logout_user()
flash("Your session has expired. Please log in again.", "warning")
return redirect(url_for("login"))
# Check admin role
if not is_admin():
flash("Admin access required", "error")
return redirect(url_for("index"))
return f(*args, **kwargs)
return decorated_function
# =============================================================================
# App Initialization
# =============================================================================
def init_app(app):
"""Initialize auth module with Flask app."""
app.teardown_appcontext(close_db)
with app.app_context():
init_db()

52
frontends/web/dev_run.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/bash
# Stegasoo Web Frontend - Development Runner
# Press 'r' to restart, 'q' to quit (single keypress, no Enter needed)
cd "$(dirname "$0")"
PID=""
cleanup() {
echo -e "\n\033[33mShutting down...\033[0m"
[[ -n "$PID" ]] && kill "$PID" 2>/dev/null
stty sane 2>/dev/null
exit 0
}
trap cleanup SIGINT SIGTERM EXIT
start_server() {
clear
echo -e "\033[36m┌──────────────────────────────────────┐\033[0m"
echo -e "\033[36m│ Stegasoo Dev Server │\033[0m"
echo -e "\033[36m│ \033[0m[r] restart [q] quit\033[36m │\033[0m"
echo -e "\033[36m└──────────────────────────────────────┘\033[0m"
pkill -f "python app.py" 2>/dev/null
sleep 0.3
python app.py 2>&1 &
PID=$!
echo -e "\033[32m✓ Running on http://localhost:5000 (PID: $PID)\033[0m\n"
}
start_server
# Single keypress mode
stty -echo -icanon time 0 min 0
while true; do
key=$(dd bs=1 count=1 2>/dev/null)
case "$key" in
r|R) start_server ;;
q|Q) cleanup ;;
esac
# Check if crashed
if [[ -n "$PID" ]] && ! kill -0 "$PID" 2>/dev/null; then
echo -e "\033[31m✗ Crashed! Press 'r' to restart\033[0m"
PID=""
fi
sleep 0.1
done

View File

@@ -0,0 +1,75 @@
#!/bin/bash
#
# Docker entrypoint for Stegasoo Web UI
# Handles SSL certificate generation and gunicorn startup
#
# Supports mkcert for browser-trusted certificates (no warning screen)
#
set -e
CERT_DIR="/app/frontends/web/certs"
CERT_FILE="$CERT_DIR/cert.pem"
KEY_FILE="$CERT_DIR/key.pem"
HOSTNAME="${STEGASOO_HOSTNAME:-localhost}"
# Generate SSL certificates
# Priority: 1) Existing certs, 2) mkcert (trusted), 3) openssl (self-signed)
generate_certs() {
if [ -f "$CERT_FILE" ] && [ -f "$KEY_FILE" ]; then
echo "Using existing SSL certificates."
return
fi
mkdir -p "$CERT_DIR"
# Try mkcert first (creates browser-trusted certs)
if command -v mkcert &> /dev/null; then
echo "Generating trusted certificate with mkcert for $HOSTNAME..."
cd "$CERT_DIR"
mkcert -key-file key.pem -cert-file cert.pem "$HOSTNAME" localhost 127.0.0.1 ::1
echo "Trusted certificate generated."
echo ""
echo " To trust on other devices, install the CA cert from:"
echo " $(mkcert -CAROOT)/rootCA.pem"
echo ""
return
fi
# Fallback to self-signed (shows browser warning)
echo "Generating self-signed SSL certificate for $HOSTNAME..."
echo "(Install mkcert for browser-trusted certs without warnings)"
openssl req -x509 -newkey rsa:2048 \
-keyout "$KEY_FILE" \
-out "$CERT_FILE" \
-sha256 -days 365 -nodes \
-subj "/CN=$HOSTNAME" \
-addext "subjectAltName=DNS:$HOSTNAME,DNS:localhost,IP:127.0.0.1" \
2>/dev/null
echo "Self-signed certificate generated."
}
# Start gunicorn with appropriate settings
if [ "${STEGASOO_HTTPS_ENABLED:-false}" = "true" ]; then
echo "HTTPS mode enabled"
generate_certs
exec gunicorn \
--bind 0.0.0.0:5000 \
--workers 2 \
--threads 4 \
--timeout 120 \
--certfile "$CERT_FILE" \
--keyfile "$KEY_FILE" \
app:app
else
echo "HTTP mode (HTTPS disabled)"
exec gunicorn \
--bind 0.0.0.0:5000 \
--workers 2 \
--threads 4 \
--timeout 120 \
app:app
fi

155
frontends/web/ssl_utils.py Normal file
View File

@@ -0,0 +1,155 @@
"""
SSL Certificate Utilities
Auto-generates self-signed certificates for HTTPS.
Uses cryptography library (already a dependency).
"""
import datetime
import ipaddress
import socket
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
def _get_local_ips() -> list[str]:
"""Get local IP addresses for this machine."""
ips = []
try:
# Get hostname and resolve to IP
hostname = socket.gethostname()
for addr_info in socket.getaddrinfo(hostname, None, socket.AF_INET):
ip = addr_info[4][0]
if ip not in ips and not ip.startswith("127."):
ips.append(ip)
except Exception:
pass
# Also try connecting to external to get primary interface IP
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
if ip not in ips:
ips.append(ip)
s.close()
except Exception:
pass
return ips
def get_cert_paths(base_dir: Path) -> tuple[Path, Path]:
"""Get paths for cert and key files."""
cert_dir = base_dir / "certs"
cert_dir.mkdir(parents=True, exist_ok=True)
return cert_dir / "server.crt", cert_dir / "server.key"
def certs_exist(base_dir: Path) -> bool:
"""Check if both cert files exist."""
cert_path, key_path = get_cert_paths(base_dir)
return cert_path.exists() and key_path.exists()
def generate_self_signed_cert(
base_dir: Path,
hostname: str = "localhost",
days_valid: int = 365,
) -> tuple[Path, Path]:
"""
Generate self-signed SSL certificate.
Args:
base_dir: Base directory for certs folder
hostname: Server hostname for certificate
days_valid: Certificate validity in days
Returns:
Tuple of (cert_path, key_path)
"""
cert_path, key_path = get_cert_paths(base_dir)
# Generate RSA key
key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# Create certificate
subject = issuer = x509.Name(
[
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Stegasoo"),
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
]
)
# Subject Alternative Names
san_list = [
x509.DNSName(hostname),
x509.DNSName("localhost"),
x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")),
]
# Add hostname.local for mDNS access
if not hostname.endswith(".local"):
san_list.append(x509.DNSName(f"{hostname}.local"))
# Add the hostname as IP if it looks like one
try:
san_list.append(x509.IPAddress(ipaddress.IPv4Address(hostname)))
except ipaddress.AddressValueError:
pass
# Add local network IPs
for local_ip in _get_local_ips():
try:
ip_addr = ipaddress.IPv4Address(local_ip)
if x509.IPAddress(ip_addr) not in san_list:
san_list.append(x509.IPAddress(ip_addr))
except (ipaddress.AddressValueError, ValueError):
pass
now = datetime.datetime.now(datetime.UTC)
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=days_valid))
.add_extension(
x509.SubjectAlternativeName(san_list),
critical=False,
)
.sign(key, hashes.SHA256())
)
# Write key file (chmod 600)
key_path.write_bytes(
key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)
key_path.chmod(0o600)
# Write cert file
cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
return cert_path, key_path
def ensure_certs(base_dir: Path, hostname: str = "localhost") -> tuple[Path, Path]:
"""Ensure certificates exist, generating if needed."""
if certs_exist(base_dir):
return get_cert_paths(base_dir)
print(f"Generating self-signed SSL certificate for {hostname}...")
return generate_self_signed_cert(base_dir, hostname)

View File

@@ -0,0 +1,142 @@
/**
* Stegasoo Authentication Pages JavaScript
* Handles login, setup, account, and admin user management pages
*/
const StegasooAuth = {
// ========================================================================
// PASSWORD VISIBILITY TOGGLE
// ========================================================================
/**
* Toggle password field visibility
* @param {string} inputId - ID of the password input
* @param {HTMLElement} btn - The toggle button element
*/
togglePassword(inputId, btn) {
const input = document.getElementById(inputId);
const icon = btn.querySelector('i');
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
icon?.classList.replace('bi-eye', 'bi-eye-slash');
} else {
input.type = 'password';
icon?.classList.replace('bi-eye-slash', 'bi-eye');
}
},
// ========================================================================
// PASSWORD CONFIRMATION VALIDATION
// ========================================================================
/**
* Initialize password confirmation validation on a form
* @param {string} formId - ID of the form
* @param {string} passwordId - ID of the password field
* @param {string} confirmId - ID of the confirmation field
*/
initPasswordConfirmation(formId, passwordId, confirmId) {
const form = document.getElementById(formId);
if (!form) return;
form.addEventListener('submit', function(e) {
const password = document.getElementById(passwordId)?.value;
const confirm = document.getElementById(confirmId)?.value;
if (password !== confirm) {
e.preventDefault();
alert('Passwords do not match');
}
});
},
// ========================================================================
// COPY TO CLIPBOARD
// ========================================================================
/**
* Copy field value to clipboard with visual feedback
* @param {string} fieldId - ID of the input field to copy
*/
copyField(fieldId) {
const field = document.getElementById(fieldId);
if (!field) return;
field.select();
navigator.clipboard.writeText(field.value).then(() => {
const btn = field.nextElementSibling;
if (!btn) return;
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check"></i>';
setTimeout(() => btn.innerHTML = originalHTML, 1000);
});
},
// ========================================================================
// PASSWORD GENERATION
// ========================================================================
/**
* Generate a random password
* @param {number} length - Password length (default 8)
* @returns {string} Generated password
*/
generatePassword(length = 8) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let password = '';
for (let i = 0; i < length; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
return password;
},
/**
* Regenerate password and update input field
* @param {string} inputId - ID of the password input
* @param {number} length - Password length
*/
regeneratePassword(inputId = 'passwordInput', length = 8) {
const input = document.getElementById(inputId);
if (input) {
input.value = this.generatePassword(length);
}
},
// ========================================================================
// DELETE CONFIRMATION
// ========================================================================
/**
* Confirm deletion with a prompt
* @param {string} itemName - Name of item being deleted
* @param {string} formId - ID of the form to submit if confirmed
* @returns {boolean} True if confirmed
*/
confirmDelete(itemName, formId = null) {
const confirmed = confirm(`Are you sure you want to delete "${itemName}"? This cannot be undone.`);
if (confirmed && formId) {
const form = document.getElementById(formId);
form?.submit();
}
return confirmed;
}
};
// Make togglePassword available globally for onclick handlers
function togglePassword(inputId, btn) {
StegasooAuth.togglePassword(inputId, btn);
}
// Make copyField available globally for onclick handlers
function copyField(fieldId) {
StegasooAuth.copyField(fieldId);
}
// Make regeneratePassword available globally for onclick handlers
function regeneratePassword() {
StegasooAuth.regeneratePassword();
}

View File

@@ -0,0 +1,279 @@
/**
* Stegasoo Generate Page JavaScript
* Handles credential generation form and display
*/
const StegasooGenerate = {
// ========================================================================
// FORM CONTROLS
// ========================================================================
/**
* Initialize the words range slider
*/
initWordsSlider() {
const wordsRange = document.getElementById('wordsRange');
const wordsValue = document.getElementById('wordsValue');
wordsRange?.addEventListener('input', function() {
const bits = this.value * 11;
wordsValue.textContent = `${this.value} words (~${bits} bits)`;
});
},
/**
* Initialize PIN/RSA option toggles
*/
initOptionToggles() {
const usePinCheck = document.getElementById('usePinCheck');
const useRsaCheck = document.getElementById('useRsaCheck');
const pinOptions = document.getElementById('pinOptions');
const rsaOptions = document.getElementById('rsaOptions');
const rsaQrWarning = document.getElementById('rsaQrWarning');
const rsaBitsSelect = document.getElementById('rsaBitsSelect');
usePinCheck?.addEventListener('change', function() {
pinOptions?.classList.toggle('d-none', !this.checked);
});
useRsaCheck?.addEventListener('change', function() {
rsaOptions?.classList.toggle('d-none', !this.checked);
});
// RSA key size QR warning (>3072 bits)
rsaBitsSelect?.addEventListener('change', function() {
rsaQrWarning?.classList.toggle('d-none', parseInt(this.value) <= 3072);
});
},
// ========================================================================
// CREDENTIAL VISIBILITY
// ========================================================================
pinHidden: false,
passphraseHidden: false,
/**
* Toggle PIN visibility
*/
togglePinVisibility() {
const pinDigits = document.getElementById('pinDigits');
const icon = document.getElementById('pinToggleIcon');
const text = document.getElementById('pinToggleText');
this.pinHidden = !this.pinHidden;
pinDigits?.classList.toggle('blurred', this.pinHidden);
if (icon) icon.className = this.pinHidden ? 'bi bi-eye' : 'bi bi-eye-slash';
if (text) text.textContent = this.pinHidden ? 'Show' : 'Hide';
},
/**
* Toggle passphrase visibility
*/
togglePassphraseVisibility() {
const display = document.getElementById('passphraseDisplay');
const icon = document.getElementById('passphraseToggleIcon');
const text = document.getElementById('passphraseToggleText');
this.passphraseHidden = !this.passphraseHidden;
display?.classList.toggle('blurred', this.passphraseHidden);
if (icon) icon.className = this.passphraseHidden ? 'bi bi-eye' : 'bi bi-eye-slash';
if (text) text.textContent = this.passphraseHidden ? 'Show' : 'Hide';
},
// ========================================================================
// MEMORY AID STORY GENERATION
// ========================================================================
currentStoryTemplate: 0,
/**
* Story templates organized by word count (3-12 words supported)
*/
storyTemplates: {
3: [
w => `The ${w[0]} ${w[1]} ${w[2]}.`,
w => `${w[0]} loves ${w[1]} and ${w[2]}.`,
w => `A ${w[0]} found a ${w[1]} near the ${w[2]}.`,
w => `${w[0]}, ${w[1]}, ${w[2]} — never forget.`,
w => `The ${w[0]} hid the ${w[1]} under the ${w[2]}.`,
],
4: [
w => `${w[0]} and ${w[1]} discovered a ${w[2]} made of ${w[3]}.`,
w => `The ${w[0]} ${w[1]} ate ${w[2]} for ${w[3]}.`,
w => `In the ${w[0]}, a ${w[1]} met a ${w[2]} carrying ${w[3]}.`,
w => `${w[0]} said "${w[1]}" while holding a ${w[2]} ${w[3]}.`,
w => `The secret: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}.`,
],
5: [
w => `${w[0]} traveled to ${w[1]} seeking the ${w[2]} of ${w[3]} and ${w[4]}.`,
w => `The ${w[0]} ${w[1]} lived in a ${w[2]} house with ${w[3]} ${w[4]}.`,
w => `"${w[0]}!" shouted ${w[1]} as the ${w[2]} ${w[3]} flew toward ${w[4]}.`,
w => `Captain ${w[0]} sailed the ${w[1]} ${w[2]} searching for ${w[3]} ${w[4]}.`,
w => `In ${w[0]} kingdom, ${w[1]} guards protected the ${w[2]} ${w[3]} ${w[4]}.`,
],
6: [
w => `${w[0]} met ${w[1]} at the ${w[2]}. Together they found ${w[3]}, ${w[4]}, and ${w[5]}.`,
w => `The ${w[0]} ${w[1]} wore a ${w[2]} hat while eating ${w[3]} ${w[4]} ${w[5]}.`,
w => `Detective ${w[0]} found ${w[1]} ${w[2]} near the ${w[3]} ${w[4]} ${w[5]}.`,
w => `In the ${w[0]} ${w[1]}, a ${w[2]} ${w[3]} sang about ${w[4]} ${w[5]}.`,
w => `Chef ${w[0]} combined ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, and ${w[5]}.`,
],
7: [
w => `${w[0]} and ${w[1]} walked through the ${w[2]} ${w[3]} to find the ${w[4]} ${w[5]} ${w[6]}.`,
w => `The ${w[0]} professor studied ${w[1]} ${w[2]} while drinking ${w[3]} ${w[4]} with ${w[5]} ${w[6]}.`,
w => `"${w[0]} ${w[1]}!" yelled ${w[2]} as ${w[3]} ${w[4]} attacked the ${w[5]} ${w[6]}.`,
w => `In ${w[0]}, King ${w[1]} decreed that ${w[2]} ${w[3]} must honor ${w[4]} ${w[5]} ${w[6]}.`,
],
8: [
w => `${w[0]} ${w[1]} and ${w[2]} ${w[3]} met at the ${w[4]} ${w[5]} to discuss ${w[6]} ${w[7]}.`,
w => `The ${w[0]} ${w[1]} ${w[2]} traveled from ${w[3]} to ${w[4]} carrying ${w[5]} ${w[6]} ${w[7]}.`,
w => `${w[0]} discovered that ${w[1]} ${w[2]} plus ${w[3]} ${w[4]} equals ${w[5]} ${w[6]} ${w[7]}.`,
],
9: [
w => `${w[0]} ${w[1]} ${w[2]} watched as ${w[3]} ${w[4]} ${w[5]} danced with ${w[6]} ${w[7]} ${w[8]}.`,
w => `In the ${w[0]} ${w[1]} ${w[2]}, three friends — ${w[3]}, ${w[4]}, ${w[5]} — found ${w[6]} ${w[7]} ${w[8]}.`,
w => `The recipe: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, ${w[5]}, ${w[6]}, ${w[7]}, ${w[8]}.`,
],
10: [
w => `${w[0]} ${w[1]} told ${w[2]} ${w[3]} about the ${w[4]} ${w[5]} ${w[6]} hidden in ${w[7]} ${w[8]} ${w[9]}.`,
w => `The ${w[0]} ${w[1]} ${w[2]} ${w[3]} ${w[4]} lived beside ${w[5]} ${w[6]} ${w[7]} ${w[8]} ${w[9]}.`,
],
11: [
w => `${w[0]} ${w[1]} ${w[2]} and ${w[3]} ${w[4]} ${w[5]} discovered ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]}.`,
w => `In ${w[0]} ${w[1]}, the ${w[2]} ${w[3]} ${w[4]} sang of ${w[5]} ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]}.`,
],
12: [
w => `${w[0]} ${w[1]} ${w[2]} met ${w[3]} ${w[4]} ${w[5]} at the ${w[6]} ${w[7]} ${w[8]} ${w[9]} ${w[10]} ${w[11]}.`,
w => `The twelve treasures: ${w[0]}, ${w[1]}, ${w[2]}, ${w[3]}, ${w[4]}, ${w[5]}, ${w[6]}, ${w[7]}, ${w[8]}, ${w[9]}, ${w[10]}, ${w[11]}.`,
],
},
/**
* Wrap word in highlight span
*/
hl(word) {
return `<span class="passphrase-word">${word}</span>`;
},
/**
* Generate a memory story for given words
* @param {string[]} words - Array of passphrase words
* @param {number|null} idx - Template index (null for current)
* @returns {string} HTML story
*/
generateStory(words, idx = null) {
const count = words.length;
if (count === 0) return '';
// Clamp to supported range (3-12)
const templateKey = Math.max(3, Math.min(12, count));
const templates = this.storyTemplates[templateKey];
if (!templates || templates.length === 0) {
// Fallback: just list the words
return words.map(w => this.hl(w)).join(' &mdash; ');
}
const templateIdx = (idx ?? this.currentStoryTemplate) % templates.length;
// Apply highlighting to words
const highlighted = words.map(w => this.hl(w));
return templates[templateIdx](highlighted);
},
/**
* Toggle memory aid visibility
* @param {string[]} words - Passphrase words array
*/
toggleMemoryAid(words) {
const container = document.getElementById('memoryAidContainer');
const icon = document.getElementById('memoryAidIcon');
const text = document.getElementById('memoryAidText');
const isHidden = container?.classList.contains('d-none');
container?.classList.toggle('d-none', !isHidden);
if (icon) icon.className = isHidden ? 'bi bi-lightbulb-fill' : 'bi bi-lightbulb';
if (text) text.textContent = isHidden ? 'Hide Aid' : 'Memory Aid';
if (isHidden) {
document.getElementById('memoryStory').innerHTML = this.generateStory(words);
}
},
/**
* Regenerate story with next template
* @param {string[]} words - Passphrase words array
*/
regenerateStory(words) {
const count = words.length;
const templateKey = Math.max(3, Math.min(12, count));
const templates = this.storyTemplates[templateKey] || [];
this.currentStoryTemplate = (this.currentStoryTemplate + 1) % Math.max(1, templates.length);
document.getElementById('memoryStory').innerHTML = this.generateStory(words, this.currentStoryTemplate);
},
// ========================================================================
// QR CODE PRINTING
// ========================================================================
/**
* Print QR code in new window
*/
printQrCode() {
const qrImg = document.getElementById('qrCodeImage');
if (!qrImg) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`<!DOCTYPE html>
<html>
<head>
<title>QR Code</title>
<style>
body { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
img { max-width: 400px; }
</style>
</head>
<body>
<img src="${qrImg.src}" alt="QR Code">
<script>window.onload = function() { window.print(); }<\/script>
</body>
</html>`);
printWindow.document.close();
},
// ========================================================================
// INITIALIZATION
// ========================================================================
/**
* Initialize generate form page
*/
initForm() {
this.initWordsSlider();
this.initOptionToggles();
}
};
// Global function wrappers for onclick handlers
function togglePinVisibility() {
StegasooGenerate.togglePinVisibility();
}
function togglePassphraseVisibility() {
StegasooGenerate.togglePassphraseVisibility();
}
function printQrCode() {
StegasooGenerate.printQrCode();
}
// Auto-init form controls
document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('[data-page="generate"]')) {
StegasooGenerate.initForm();
}
});

Some files were not shown because too many files have changed in this diff Show More