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:
@@ -9,14 +9,14 @@
|
||||
## Features
|
||||
- [ ] QR channel key sharing (needs UI thought - avoid crowding encode/decode pages)
|
||||
- [ ] 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
|
||||
|
||||
## Security
|
||||
- [ ] Optional encryption for temp file storage (paranoid mode, config toggle)
|
||||
|
||||
## Docs
|
||||
- [ ] Update UNDER_THE_HOOD.md
|
||||
- [x] Update UNDER_THE_HOOD.md (v4.1 changes, channel keys)
|
||||
- [ ] General docs refresh
|
||||
|
||||
## Ideas (maybe later)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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 │
|
||||
@@ -30,8 +30,8 @@ A detailed breakdown of how Stegasoo's LSB and DCT steganography modes work unde
|
||||
│ │
|
||||
│ Reference Photo ─┐ │
|
||||
│ Passphrase ──────┼──► Argon2id KDF ──► AES-256 Key │
|
||||
│ PIN/RSA Key ─────┘ │ │
|
||||
│ ▼ │
|
||||
│ PIN/RSA Key ─────┤ │ │
|
||||
│ Channel Key ─────┘ (v4.1) ▼ │
|
||||
│ Message/File ────────────────────────► AES-256-GCM ──► Ciphertext │
|
||||
│ 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) |
|
||||
| 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 |
|
||||
|
||||
@@ -586,11 +586,113 @@ def generate(ctx, words, pin_length, channel_key):
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--full", is_flag=True, help="Show full system information (Pi stats)")
|
||||
@click.pass_context
|
||||
def info(ctx):
|
||||
"""Show version and feature information."""
|
||||
def info(ctx, full):
|
||||
"""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 = {
|
||||
"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": {
|
||||
"available": [algorithm_name(a) for a in get_available_algorithms()],
|
||||
"lz4_installed": HAS_LZ4,
|
||||
@@ -599,20 +701,54 @@ def info(ctx):
|
||||
"max_message_bytes": MAX_MESSAGE_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"):
|
||||
click.echo(json.dumps(info_data, indent=2))
|
||||
else:
|
||||
click.echo(f"Stegasoo v{__version__}")
|
||||
click.echo("\nCompression algorithms:")
|
||||
for algo in get_available_algorithms():
|
||||
click.echo(f" • {algorithm_name(algo)}")
|
||||
if not HAS_LZ4:
|
||||
click.echo(" (install 'lz4' for LZ4 support)")
|
||||
click.echo("\nLimits:")
|
||||
click.echo(f" • Max message: {MAX_MESSAGE_SIZE:,} bytes")
|
||||
click.echo(f" • Max file payload: {MAX_FILE_PAYLOAD_SIZE:,} bytes")
|
||||
# Fastfetch-style output
|
||||
click.echo(f"\033[1mSTEGASOO\033[0m v{__version__}")
|
||||
click.echo("─" * 36)
|
||||
|
||||
# Service status
|
||||
if service_status == "active":
|
||||
click.echo(f" Service: \033[32m● running\033[0m")
|
||||
if service_url:
|
||||
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")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user