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>
This commit is contained in:
Aaron D. Lee
2026-01-06 13:05:17 -05:00
parent 893a044eaa
commit cc46993d80
3 changed files with 166 additions and 17 deletions

View File

@@ -9,14 +9,14 @@
## Features ## Features
- [ ] QR channel key sharing (needs UI thought - avoid crowding encode/decode pages) - [ ] QR channel key sharing (needs UI thought - avoid crowding encode/decode pages)
- [ ] Role-based permissions: admin / mod / user - [ ] Role-based permissions: admin / mod / user
- [ ] `stegasoo info` fastfetch-style command (version, service status, channel, CPU, temp, etc.) - [x] `stegasoo info` fastfetch-style command (version, service status, channel, CPU, temp, etc.)
- [ ] Better capacity estimates / pre-flight check before encode fails - [ ] Better capacity estimates / pre-flight check before encode fails
## Security ## Security
- [ ] Optional encryption for temp file storage (paranoid mode, config toggle) - [ ] Optional encryption for temp file storage (paranoid mode, config toggle)
## Docs ## Docs
- [ ] Update UNDER_THE_HOOD.md - [x] Update UNDER_THE_HOOD.md (v4.1 changes, channel keys)
- [ ] General docs refresh - [ ] General docs refresh
## Ideas (maybe later) ## Ideas (maybe later)

View File

@@ -2,7 +2,7 @@
A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work under the hood. A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work under the hood.
**Version 4.0** - Updated for simplified authentication (no date dependency) **Version 4.1** - Updated for channel keys and deployment isolation
--- ---
@@ -22,7 +22,7 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde
``` ```
┌─────────────────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────────────────┐
│ STEGASOO ARCHITECTURE (v4.0) │ │ STEGASOO ARCHITECTURE (v4.1) │
├─────────────────────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────────────────────┤
│ │ │ │
│ INPUTS PROCESSING OUTPUT │ │ INPUTS PROCESSING OUTPUT │
@@ -30,8 +30,8 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde
│ │ │ │
│ Reference Photo ─┐ │ │ Reference Photo ─┐ │
│ Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key │ │ Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key │
│ PIN/RSA Key ───── │ │ │ PIN/RSA Key ───── │ │
▼ │ Channel Key ─────┘ (v4.1) ▼ │
│ Message/File ────────────────────────► AES-256-GCM ──► Ciphertext │ │ Message/File ────────────────────────► AES-256-GCM ──► Ciphertext │
│ Encryption │ │ │ Encryption │ │
│ ▼ │ │ ▼ │
@@ -50,11 +50,24 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde
| Header size | 75 bytes | 65 bytes (no date field) | | Header size | 75 bytes | 65 bytes (no date field) |
| Python support | 3.10+ | 3.10-3.12 only | | 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 Responsibilities
| Module | File | Purpose | | Module | File | Purpose |
|--------|------|---------| |--------|------|---------|
| **Crypto** | `crypto.py` | Key derivation (Argon2id), AES-256-GCM encryption/decryption | | **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 | | **Steganography** | `steganography.py` | LSB pixel manipulation, capacity calculation |
| **DCT Steganography** | `dct_steganography.py` | Frequency-domain embedding, jpegio integration | | **DCT Steganography** | `dct_steganography.py` | Frequency-domain embedding, jpegio integration |
| **Compression** | `compression.py` | Optional LZ4 compression of payload | | **Compression** | `compression.py` | Optional LZ4 compression of payload |

View File

@@ -586,11 +586,113 @@ def generate(ctx, words, pin_length, channel_key):
@cli.command() @cli.command()
@click.option("--full", is_flag=True, help="Show full system information (Pi stats)")
@click.pass_context @click.pass_context
def info(ctx): def info(ctx, full):
"""Show version and feature information.""" """Show version, features, and system information."""
import os
import subprocess
# Check for DCT support
try:
from .dct_steganography import HAS_SCIPY, HAS_JPEGIO
HAS_DCT = HAS_SCIPY and HAS_JPEGIO
except ImportError:
HAS_DCT = False
# Check service status
service_status = "unknown"
service_url = None
try:
result = subprocess.run(
["systemctl", "is-active", "stegasoo"],
capture_output=True,
text=True,
timeout=2,
)
service_status = result.stdout.strip()
if service_status == "active":
# Try to get URL from service environment
env_result = subprocess.run(
["systemctl", "show", "stegasoo", "--property=Environment"],
capture_output=True,
text=True,
timeout=2,
)
https_enabled = "HTTPS_ENABLED=true" in env_result.stdout
protocol = "https" if https_enabled else "http"
# Get IP
ip_result = subprocess.run(
["hostname", "-I"],
capture_output=True,
text=True,
timeout=2,
)
ip = ip_result.stdout.strip().split()[0] if ip_result.stdout.strip() else "localhost"
service_url = f"{protocol}://{ip}"
except (subprocess.TimeoutExpired, FileNotFoundError, IndexError):
pass
# Check channel key
channel_fingerprint = None
channel_source = None
try:
from .channel import get_channel_key, get_channel_fingerprint, get_channel_status
key = get_channel_key()
if key:
channel_fingerprint = get_channel_fingerprint(key)
status = get_channel_status()
channel_source = status.get("source")
except ImportError:
pass
# System info (Pi-specific)
cpu_freq = None
cpu_temp = None
disk_free = None
uptime = None
if full:
try:
# CPU frequency
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq") as f:
cpu_freq = int(f.read().strip()) // 1000 # MHz
except (FileNotFoundError, ValueError):
pass
try:
# CPU temp
with open("/sys/class/thermal/thermal_zone0/temp") as f:
cpu_temp = int(f.read().strip()) / 1000 # Celsius
except (FileNotFoundError, ValueError):
pass
try:
# Disk free
st = os.statvfs("/")
disk_free = (st.f_bavail * st.f_frsize) / (1024 ** 3) # GB
except OSError:
pass
try:
# Uptime
with open("/proc/uptime") as f:
uptime_secs = float(f.read().split()[0])
days = int(uptime_secs // 86400)
hours = int((uptime_secs % 86400) // 3600)
uptime = f"{days}d {hours}h" if days else f"{hours}h"
except (FileNotFoundError, ValueError):
pass
info_data = { info_data = {
"version": __version__, "version": __version__,
"service": service_status,
"url": service_url,
"dct_support": HAS_DCT,
"channel": {
"fingerprint": channel_fingerprint,
"source": channel_source,
} if channel_fingerprint else None,
"compression": { "compression": {
"available": [algorithm_name(a) for a in get_available_algorithms()], "available": [algorithm_name(a) for a in get_available_algorithms()],
"lz4_installed": HAS_LZ4, "lz4_installed": HAS_LZ4,
@@ -599,20 +701,54 @@ def info(ctx):
"max_message_bytes": MAX_MESSAGE_SIZE, "max_message_bytes": MAX_MESSAGE_SIZE,
"max_file_payload_bytes": MAX_FILE_PAYLOAD_SIZE, "max_file_payload_bytes": MAX_FILE_PAYLOAD_SIZE,
}, },
"system": {
"cpu_mhz": cpu_freq,
"temp_c": cpu_temp,
"disk_free_gb": round(disk_free, 1) if disk_free else None,
"uptime": uptime,
} if full else None,
} }
if ctx.obj.get("json"): if ctx.obj.get("json"):
click.echo(json.dumps(info_data, indent=2)) click.echo(json.dumps(info_data, indent=2))
else: else:
click.echo(f"Stegasoo v{__version__}") # Fastfetch-style output
click.echo("\nCompression algorithms:") click.echo(f"\033[1mSTEGASOO\033[0m v{__version__}")
for algo in get_available_algorithms(): click.echo("" * 36)
click.echo(f"{algorithm_name(algo)}")
if not HAS_LZ4: # Service status
click.echo(" (install 'lz4' for LZ4 support)") if service_status == "active":
click.echo("\nLimits:") click.echo(f" Service: \033[32m● running\033[0m")
click.echo(f" • Max message: {MAX_MESSAGE_SIZE:,} bytes") if service_url:
click.echo(f" • Max file payload: {MAX_FILE_PAYLOAD_SIZE:,} bytes") click.echo(f" URL: {service_url}")
elif service_status == "inactive":
click.echo(f" Service: \033[31m○ stopped\033[0m")
else:
click.echo(f" Service: \033[33m? {service_status}\033[0m")
# Channel
if channel_fingerprint:
masked = f"{channel_fingerprint[:4]}••••••••{channel_fingerprint[-4:]}"
click.echo(f" Channel: {masked}")
else:
click.echo(f" Channel: \033[33mpublic\033[0m")
# DCT
dct_status = "\033[32m✓ enabled\033[0m" if HAS_DCT else "\033[31m✗ disabled\033[0m"
click.echo(f" DCT: {dct_status}")
# System info (if --full)
if full and any([cpu_freq, cpu_temp, disk_free, uptime]):
click.echo("" * 36)
if cpu_freq:
click.echo(f" CPU: {cpu_freq} MHz")
if cpu_temp:
temp_color = "\033[32m" if cpu_temp < 60 else "\033[33m" if cpu_temp < 75 else "\033[31m"
click.echo(f" Temp: {temp_color}{cpu_temp:.1f}°C\033[0m")
if uptime:
click.echo(f" Uptime: {uptime}")
if disk_free:
click.echo(f" Disk: {disk_free:.1f} GB free")
# ============================================================================= # =============================================================================