More snazzy 4.0 Web UI improvements.

This commit is contained in:
Aaron D. Lee
2026-01-02 15:45:43 -05:00
parent 1bb3589baf
commit 6fa4b447db
26 changed files with 4282 additions and 2282 deletions

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env python3
"""
Stegasoo CLI - Command-line interface for steganography operations (v3.2.0).
Stegasoo CLI - Command-line interface for steganography operations (v4.0.0).
CHANGES in v4.0.0:
- Added channel key support for deployment/group isolation
- Messages encoded with a channel key can only be decoded with the same key
- New `channel` command group for key management
CHANGES in v3.2.0:
- Removed date dependency from all operations
@@ -16,6 +21,7 @@ Usage:
stegasoo info [OPTIONS]
stegasoo compare [OPTIONS]
stegasoo modes [OPTIONS]
stegasoo channel [SUBCOMMAND]
"""
import sys
@@ -64,6 +70,18 @@ from stegasoo import (
# Models
FilePayload,
# Channel key functions (v4.0.0)
generate_channel_key,
get_channel_key,
set_channel_key,
clear_channel_key,
has_channel_key,
get_channel_status,
validate_channel_key,
format_channel_key,
get_active_channel_key,
get_channel_fingerprint,
)
# Import constants - try main module first, then constants submodule
@@ -136,13 +154,13 @@ def cli():
- Reference photo (something you have)
- Passphrase (something you know)
- Static PIN or RSA key (additional security)
- Channel key (deployment/group isolation) [v4.0.0]
\b
Version 3.2.0 Changes:
- No more date parameters - encode/decode anytime!
- Simplified passphrase (no daily rotation)
- Default passphrase increased to 4 words
- True asynchronous communications
Version 4.0.0 Changes:
- Channel key support for group/deployment isolation
- Messages encoded with a channel key require the same key to decode
- New `stegasoo channel` command for key management
\b
Embedding Modes:
@@ -157,6 +175,60 @@ def cli():
pass
# ============================================================================
# CHANNEL KEY HELPERS
# ============================================================================
def resolve_channel_key_option(channel: Optional[str], channel_file: Optional[str],
no_channel: bool) -> Optional[str]:
"""
Resolve channel key from CLI options.
Returns:
None: Use server-configured key (auto mode)
"": Public mode (no channel key)
str: Explicit channel key
"""
if no_channel:
return "" # Public mode
if channel_file:
# Load from file
path = Path(channel_file)
if not path.exists():
raise click.ClickException(f"Channel key file not found: {channel_file}")
key = path.read_text().strip()
if not validate_channel_key(key):
raise click.ClickException(f"Invalid channel key format in file: {channel_file}")
return key
if channel:
if channel.lower() == 'auto':
return None # Use server config
# Explicit key provided
if not validate_channel_key(channel):
raise click.ClickException(
f"Invalid channel key format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n"
f"Generate a new key with: stegasoo channel generate"
)
return channel
# Default: use server-configured key (auto mode)
return None
def format_channel_status_line(quiet: bool = False) -> Optional[str]:
"""Get a one-line status for channel key configuration."""
if quiet:
return None
status = get_channel_status()
if status['mode'] == 'public':
return None
return f"Channel: {status['fingerprint']} ({status['source']})"
# ============================================================================
# GENERATE COMMAND
# ============================================================================
@@ -229,7 +301,7 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
# Pretty output
click.echo()
click.secho("=" * 60, fg='cyan')
click.secho(" STEGASOO CREDENTIALS (v3.2.0)", fg='cyan', bold=True)
click.secho(" STEGASOO CREDENTIALS (v4.0.0)", fg='cyan', bold=True)
click.secho("=" * 60, fg='cyan')
click.echo()
@@ -269,13 +341,278 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
click.secho(f" + photo entropy: 80-256 bits", dim=True)
click.echo()
click.secho("✓ v3.2.0: Use this passphrase anytime - no date needed!", fg='cyan')
# Show channel key status
if has_channel_key():
status = get_channel_status()
click.secho("─── CHANNEL KEY ───", fg='magenta')
click.echo(f" Status: Private mode")
click.echo(f" Fingerprint: {status['fingerprint']}")
click.secho(f" (configured via {status['source']})", dim=True)
click.echo()
click.secho("✓ v4.0.0: Use this passphrase anytime - no date needed!", fg='cyan')
click.echo()
except Exception as e:
raise click.ClickException(str(e))
# ============================================================================
# CHANNEL COMMAND GROUP (v4.0.0)
# ============================================================================
@cli.group()
def channel():
"""
Manage channel keys for deployment/group isolation.
Channel keys allow different deployments or groups to use Stegasoo
without being able to read each other's messages, even with identical
credentials.
\b
Key Storage (checked in order):
1. Environment variable: STEGASOO_CHANNEL_KEY
2. Project config: ./config/channel.key
3. User config: ~/.stegasoo/channel.key
\b
Subcommands:
generate Create a new channel key
show Display current channel key status
set Save a channel key to config file
clear Remove channel key from config
\b
Examples:
stegasoo channel generate
stegasoo channel show
stegasoo channel set XXXX-XXXX-...
stegasoo channel clear
"""
pass
@channel.command('generate')
@click.option('--save', '-s', is_flag=True, help='Save to user config (~/.stegasoo/channel.key)')
@click.option('--save-project', is_flag=True, help='Save to project config (./config/channel.key)')
@click.option('--env', '-e', is_flag=True, help='Output as environment variable export')
@click.option('--quiet', '-q', is_flag=True, help='Output only the key')
def channel_generate(save, save_project, env, quiet):
"""
Generate a new channel key.
Creates a cryptographically secure 256-bit channel key in the format:
XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
\b
Examples:
# Just display a new key
stegasoo channel generate
# Save to user config
stegasoo channel generate --save
# Output for .env file
stegasoo channel generate --env >> .env
# For scripts
KEY=$(stegasoo channel generate -q)
"""
key = generate_channel_key()
if save and save_project:
raise click.UsageError("Cannot use both --save and --save-project")
if save:
set_channel_key(key, location='user')
if not quiet:
click.secho("✓ Channel key saved to ~/.stegasoo/channel.key", fg='green')
click.echo()
if save_project:
set_channel_key(key, location='project')
if not quiet:
click.secho("✓ Channel key saved to ./config/channel.key", fg='green')
click.echo()
if env:
click.echo(f"STEGASOO_CHANNEL_KEY={key}")
elif quiet:
click.echo(key)
else:
click.echo()
click.secho("─── NEW CHANNEL KEY ───", fg='cyan', bold=True)
click.echo()
click.secho(f" {key}", fg='bright_yellow', bold=True)
click.echo()
fingerprint = f"{key[:4]}-••••-••••-••••-••••-••••-••••-{key[-4:]}"
click.echo(f" Fingerprint: {fingerprint}")
click.echo()
click.secho("Usage:", dim=True)
click.echo(" # Environment variable (recommended)")
click.echo(f" export STEGASOO_CHANNEL_KEY={key}")
click.echo()
click.echo(" # Or save to config")
click.echo(" stegasoo channel generate --save")
click.echo()
click.echo(" # Or add to .env file")
click.echo(" stegasoo channel generate --env >> .env")
click.echo()
@channel.command('show')
@click.option('--reveal', '-r', is_flag=True, help='Show full key (not just fingerprint)')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def channel_show(reveal, as_json):
"""
Display current channel key status.
Shows whether a channel key is configured and where it comes from.
By default shows only fingerprint; use --reveal to see full key.
\b
Examples:
stegasoo channel show
stegasoo channel show --reveal
stegasoo channel show --json
"""
status = get_channel_status()
if as_json:
import json
output = {
'mode': status['mode'],
'configured': status['configured'],
'fingerprint': status.get('fingerprint'),
'source': status.get('source'),
}
if reveal and status['configured']:
output['key'] = status.get('key')
click.echo(json.dumps(output, indent=2))
return
click.echo()
click.secho("─── CHANNEL KEY STATUS ───", fg='cyan', bold=True)
click.echo()
if status['mode'] == 'public':
click.secho(" Mode: PUBLIC", fg='yellow', bold=True)
click.echo(" No channel key configured.")
click.echo()
click.secho(" Messages can be read by any Stegasoo installation", dim=True)
click.secho(" with matching credentials.", dim=True)
else:
click.secho(" Mode: PRIVATE", fg='green', bold=True)
click.echo(f" Fingerprint: {status['fingerprint']}")
click.echo(f" Source: {status['source']}")
if reveal:
click.echo()
click.secho(f" Full key: {status['key']}", fg='bright_yellow')
click.echo()
click.secho(" Messages require this channel key to decode.", dim=True)
click.echo()
@channel.command('set')
@click.argument('key', required=False)
@click.option('--file', '-f', 'key_file', type=click.Path(exists=True), help='Read key from file')
@click.option('--project', '-p', is_flag=True, help='Save to project config instead of user config')
def channel_set(key, key_file, project):
"""
Save a channel key to config file.
Saves to user config (~/.stegasoo/channel.key) by default,
or project config (./config/channel.key) with --project.
\b
Examples:
stegasoo channel set XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
stegasoo channel set --file channel.key
stegasoo channel set XXXX-... --project
"""
if not key and not key_file:
raise click.UsageError("Must provide KEY argument or --file option")
if key and key_file:
raise click.UsageError("Cannot use both KEY argument and --file option")
if key_file:
key = Path(key_file).read_text().strip()
if not validate_channel_key(key):
raise click.ClickException(
f"Invalid channel key format.\n"
f"Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n"
f"Generate a new key with: stegasoo channel generate"
)
location = 'project' if project else 'user'
set_channel_key(key, location=location)
status = get_channel_status()
click.secho(f"✓ Channel key saved", fg='green')
click.echo(f" Location: {status['source']}")
click.echo(f" Fingerprint: {status['fingerprint']}")
@channel.command('clear')
@click.option('--project', '-p', is_flag=True, help='Clear project config instead of user config')
@click.option('--all', 'clear_all', is_flag=True, help='Clear both user and project configs')
@click.option('--force', '-f', is_flag=True, help='Skip confirmation')
def channel_clear(project, clear_all, force):
"""
Remove channel key from config.
Clears user config by default. Use --project for project config,
or --all to clear both.
Note: This does not affect environment variables.
\b
Examples:
stegasoo channel clear
stegasoo channel clear --project
stegasoo channel clear --all
"""
if not force:
if clear_all:
msg = "Clear channel key from both user and project configs?"
elif project:
msg = "Clear channel key from project config (./config/channel.key)?"
else:
msg = "Clear channel key from user config (~/.stegasoo/channel.key)?"
if not click.confirm(msg):
click.echo("Cancelled.")
return
if clear_all:
clear_channel_key(location='user')
clear_channel_key(location='project')
click.secho("✓ Cleared channel key from user and project configs", fg='green')
elif project:
clear_channel_key(location='project')
click.secho("✓ Cleared channel key from project config", fg='green')
else:
clear_channel_key(location='user')
click.secho("✓ Cleared channel key from user config", fg='green')
# Show current status
status = get_channel_status()
if status['configured']:
click.echo()
click.secho(f"Note: Channel key still active from {status['source']}", fg='yellow')
click.echo(f" Fingerprint: {status['fingerprint']}")
else:
click.echo(" Mode is now: PUBLIC")
# ============================================================================
# ENCODE COMMAND
# ============================================================================
@@ -291,6 +628,9 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)')
@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file')
@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)')
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
@click.option('--mode', 'embed_mode', type=click.Choice(['lsb', 'dct']), default='lsb',
help='Embedding mode: lsb (default, color) or dct (requires scipy)')
@@ -300,18 +640,23 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
help='DCT color mode: grayscale (default) or color (preserves original colors)')
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin, key, key_qr,
key_password, output, embed_mode, dct_output_format, dct_color_mode, quiet):
key_password, channel_key, channel_file, no_channel, output, embed_mode,
dct_output_format, dct_color_mode, quiet):
"""
Encode a secret message or file into an image.
Requires a reference photo, carrier image, and passphrase.
Must provide either --pin or --key/--key-qr (or both).
v4.0.0: Channel key support for deployment isolation.
v3.2.0: No --date parameter needed! Encode and decode anytime.
For text messages, use -m or -f or pipe via stdin.
For binary files, use -e/--embed-file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
\b
Channel Key Options:
(no option) Use server-configured key (auto mode)
--channel KEY Use explicit channel key
--channel-file F Read channel key from file
--no-channel Force public mode (no isolation)
\b
Embedding Modes:
@@ -324,25 +669,17 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
- Lower capacity (~75 KB/megapixel)
- Better resistance to visual analysis
\b
DCT Options:
--dct-format png Lossless output (default)
--dct-format jpeg Smaller file, more natural appearance
--dct-color grayscale Convert to grayscale (default, traditional)
--dct-color color Preserve original colors (experimental)
\b
Examples:
# Text message with PIN (LSB mode, default)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder mountain" --pin 123456 -m "secret"
# Text message with PIN (auto channel key)
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
# DCT mode - grayscale PNG (traditional)
stegasoo encode -r photo.jpg -c meme.png -p "secure words here now" --pin 123456 -m "secret" --mode dct
# Explicit channel key
stegasoo encode -r photo.jpg -c meme.png -p "words here" --pin 123456 -m "msg" \\
--channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
# DCT mode - color JPEG
stegasoo encode -r photo.jpg -c meme.png -p "my strong passphrase" --pin 123456 -m "secret" \\
--mode dct --dct-color color --dct-format jpeg
# Public mode (no channel key)
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -m "msg" --no-channel
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -356,6 +693,12 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
if not quiet:
click.secho("Note: --dct-format and --dct-color only apply to DCT mode", fg='yellow', dim=True)
# Resolve channel key
try:
resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel)
except click.ClickException:
raise
# Determine what to encode
payload = None
@@ -431,8 +774,18 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
if embed_mode == 'dct':
mode_desc += f" ({dct_color_mode}, {dct_output_format.upper()})"
click.echo(f"Mode: {mode_desc} ({fit_check['usage_percent']:.1f}% capacity)")
# Show channel status
channel_status = format_channel_status_line()
if resolved_channel_key == "":
click.echo("Channel: PUBLIC (no isolation)")
elif resolved_channel_key:
fingerprint = f"{resolved_channel_key[:4]}-••••-...-{resolved_channel_key[-4:]}"
click.echo(f"Channel: {fingerprint} (explicit)")
elif channel_status:
click.echo(channel_status)
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key parameter
result = encode(
message=payload,
reference_photo=ref_photo,
@@ -444,6 +797,7 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
embed_mode=embed_mode,
dct_output_format=dct_output_format,
dct_color_mode=dct_color_mode,
channel_key=resolved_channel_key,
)
# Determine output path
@@ -485,43 +839,43 @@ def encode_cmd(ref, carrier, message, message_file, embed_file, passphrase, pin,
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)')
@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file')
@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)')
@click.option('--output', '-o', type=click.Path(), help='Save decoded content to file')
@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct')
@click.option('--quiet', '-q', is_flag=True, help='Output only the content (for text) or suppress messages (for files)')
@click.option('--force', is_flag=True, help='Overwrite existing output file')
def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, embed_mode, quiet, force):
def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, channel_key, channel_file,
no_channel, output, embed_mode, quiet, force):
"""
Decode a secret message or file from a stego image.
Must use the same credentials that were used for encoding.
Automatically detects whether content is text or a file.
RSA key can be provided as a .pem file (--key) or QR code image (--key-qr).
v4.0.0: Channel key support - must match what was used for encoding.
v3.2.0: No --date parameter needed! Just use your passphrase.
Note: Extraction works the same regardless of whether the image was
created with color mode or grayscale mode - both use the same Y channel.
\b
Extraction Modes:
--mode auto Auto-detect (default) - tries LSB first, then DCT
--mode lsb Only try LSB extraction
--mode dct Only try DCT extraction (requires scipy)
Channel Key Options:
(no option) Use server-configured key (auto mode)
--channel KEY Use explicit channel key
--channel-file F Read channel key from file
--no-channel Force public mode (for images encoded without channel key)
\b
Examples:
# Decode with PIN (auto-detect mode)
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder mountain" --pin 123456
# Decode with auto channel key
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
# Explicitly specify DCT mode
stegasoo decode -r photo.jpg -s stego.png -p "my passphrase here" --pin 123456 --mode dct
# Decode with explicit channel key
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 \\
--channel ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456
# Decode with RSA key file
stegasoo decode -r photo.jpg -s stego.png -p "strong words" -k mykey.pem
# Save output to file
stegasoo decode -r photo.jpg -s stego.png -p "passphrase" --pin 123456 -o output.txt
# Decode public image (no channel key was used)
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 --no-channel
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -529,6 +883,12 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
"DCT mode requires scipy. Install with: pip install scipy"
)
# Resolve channel key
try:
resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel)
except click.ClickException:
raise
# Load key if provided (from .pem file or QR code image)
rsa_key_data = None
rsa_key_from_qr = False
@@ -563,7 +923,7 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
ref_photo = Path(ref).read_bytes()
stego_image = Path(stego).read_bytes()
# v3.2.0: No date_str parameter
# v4.0.0: Include channel_key parameter
result = decode(
stego_image=stego_image,
reference_photo=ref_photo,
@@ -572,6 +932,7 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
embed_mode=embed_mode,
channel_key=resolved_channel_key,
)
if result.is_file:
@@ -612,6 +973,10 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
click.echo(result.message)
except (DecryptionError, ExtractionError) as e:
# Provide helpful hints for channel key mismatches
error_msg = str(e)
if 'channel key' in error_msg.lower():
raise click.ClickException(error_msg)
raise click.ClickException(f"Decryption failed: {e}")
except StegasooError as e:
raise click.ClickException(str(e))
@@ -631,23 +996,29 @@ def decode_cmd(ref, stego, passphrase, pin, key, key_qr, key_password, output, e
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file (.pem)')
@click.option('--key-qr', type=click.Path(exists=True), help='RSA key from QR code image')
@click.option('--key-password', help='RSA key password (for encrypted .pem files)')
@click.option('--channel', 'channel_key', help='Channel key (or "auto" for server config)')
@click.option('--channel-file', type=click.Path(exists=True), help='Read channel key from file')
@click.option('--no-channel', is_flag=True, help='Force public mode (no channel key)')
@click.option('--mode', 'embed_mode', type=click.Choice(['auto', 'lsb', 'dct']), default='auto',
help='Extraction mode: auto (default), lsb, or dct')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, as_json):
def verify(ref, stego, passphrase, pin, key, key_qr, key_password, channel_key, channel_file,
no_channel, embed_mode, as_json):
"""
Verify that a stego image can be decoded without extracting the message.
Quick check to validate credentials are correct and data is intact.
Does NOT output the actual message content.
v4.0.0: Also verifies channel key matches.
\b
Examples:
stegasoo verify -r photo.jpg -s stego.png -p "my passphrase" --pin 123456
stegasoo verify -r photo.jpg -s stego.png -p "words here" -k mykey.pem --json
stegasoo verify -r photo.jpg -s stego.png -p "test phrase" --pin 123456 --mode dct
stegasoo verify -r photo.jpg -s stego.png -p "test phrase" --pin 123456 --no-channel
"""
# Check DCT mode availability
if embed_mode == 'dct' and not has_dct_support():
@@ -655,6 +1026,12 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a
"DCT mode requires scipy. Install with: pip install scipy"
)
# Resolve channel key
try:
resolved_channel_key = resolve_channel_key_option(channel_key, channel_file, no_channel)
except click.ClickException:
raise
# Load key if provided
rsa_key_data = None
rsa_key_from_qr = False
@@ -685,7 +1062,7 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a
ref_photo = Path(ref).read_bytes()
stego_image = Path(stego).read_bytes()
# Attempt to decode
# Attempt to decode (v4.0.0: with channel_key)
result = decode(
stego_image=stego_image,
reference_photo=ref_photo,
@@ -694,51 +1071,44 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a
rsa_key_data=rsa_key_data,
rsa_password=effective_key_password,
embed_mode=embed_mode,
channel_key=resolved_channel_key,
)
# Calculate payload size
if result.is_file:
payload_size = len(result.file_data) if result.file_data else 0
payload_type = "file"
payload_desc = result.filename or "unnamed file"
if result.mime_type:
payload_desc += f" ({result.mime_type})"
payload_size = len(result.file_data)
content_type = result.mime_type or 'file'
else:
payload_size = len(result.message.encode('utf-8')) if result.message else 0
payload_type = "text"
payload_desc = f"{payload_size} bytes"
payload_size = len(result.message.encode('utf-8'))
content_type = 'text'
if as_json:
import json
output_data = {
"valid": True,
"stego_file": stego,
"payload_type": payload_type,
"payload_size": payload_size,
output = {
'valid': True,
'content_type': content_type,
'payload_size': payload_size,
'filename': result.filename if result.is_file else None,
}
if result.is_file:
output_data["filename"] = result.filename
output_data["mime_type"] = result.mime_type
click.echo(json.dumps(output_data, indent=2))
click.echo(json.dumps(output, indent=2))
else:
click.secho("✓ Valid stego image", fg='green', bold=True)
click.echo(f" Payload: {payload_type} ({payload_desc})")
click.echo(f" Size: {payload_size:,} bytes")
click.secho("✓ Verification successful!", fg='green')
click.echo(f" Content type: {content_type}")
click.echo(f" Payload size: {payload_size:,} bytes")
if result.is_file and result.filename:
click.echo(f" Filename: {result.filename}")
except (DecryptionError, ExtractionError) as e:
if as_json:
import json
output_data = {
"valid": False,
"stego_file": stego,
"error": str(e),
output = {
'valid': False,
'error': str(e),
}
click.echo(json.dumps(output_data, indent=2))
click.echo(json.dumps(output, indent=2))
sys.exit(1)
else:
click.secho("Verification failed", fg='red', bold=True)
click.echo(f" Error: {e}")
sys.exit(1)
raise click.ClickException(f"Verification failed: {e}")
except StegasooError as e:
raise click.ClickException(str(e))
except Exception as e:
@@ -754,64 +1124,38 @@ def verify(ref, stego, passphrase, pin, key, key_qr, key_password, embed_mode, a
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def info(image, as_json):
"""
Show information about an image.
Show information about an image file.
Displays dimensions, capacity for both LSB and DCT modes.
Displays dimensions, format, capacity estimates for different modes,
and whether the image appears suitable as a carrier.
\b
Examples:
stegasoo info photo.png
stegasoo info carrier.jpg --json
"""
try:
image_data = Path(image).read_bytes()
result = validate_image(image_data, check_size=False)
if not result.is_valid:
raise click.ClickException(result.error_message)
# Get capacity comparison
comparison = compare_modes(image_data)
img_info = get_image_info(image_data)
if as_json:
import json
output_data = {
"file": image,
"width": result.details['width'],
"height": result.details['height'],
"pixels": result.details['pixels'],
"mode": result.details['mode'],
"format": result.details['format'],
"capacity": {
"lsb": {
"bytes": comparison['lsb']['capacity_bytes'],
"kb": round(comparison['lsb']['capacity_kb'], 1),
},
"dct": {
"bytes": comparison['dct']['capacity_bytes'],
"kb": round(comparison['dct']['capacity_kb'], 1),
"available": comparison['dct']['available'],
"ratio_vs_lsb": round(comparison['dct']['ratio_vs_lsb'], 1),
"output_formats": ["png", "jpeg"],
"color_modes": ["grayscale", "color"],
},
},
}
click.echo(json.dumps(output_data, indent=2))
click.echo(json.dumps(img_info, indent=2))
return
click.echo()
click.secho(f"Image: {image}", bold=True)
click.echo(f" Dimensions: {result.details['width']} × {result.details['height']}")
click.echo(f" Pixels: {result.details['pixels']:,}")
click.echo(f" Mode: {result.details['mode']}")
click.echo(f" Format: {result.details['format']}")
click.echo()
click.secho(f"=== Image Info: {image} ===", fg='cyan', bold=True)
click.echo(f" Format: {img_info.get('format', 'Unknown')}")
click.echo(f" Dimensions: {img_info.get('width', '?')} × {img_info.get('height', '?')}")
click.echo(f" Mode: {img_info.get('mode', '?')}")
click.echo(f" Size: {len(image_data):,} bytes")
click.secho(" Capacity:", bold=True)
click.echo(f" LSB mode: ~{comparison['lsb']['capacity_bytes']:,} bytes ({comparison['lsb']['capacity_kb']:.1f} KB)")
dct_status = "" if comparison['dct']['available'] else "✗ (scipy not installed)"
click.echo(f" DCT mode: ~{comparison['dct']['capacity_bytes']:,} bytes ({comparison['dct']['capacity_kb']:.1f} KB) {dct_status}")
click.echo(f" DCT ratio: {comparison['dct']['ratio_vs_lsb']:.1f}% of LSB")
if comparison['dct']['available']:
click.secho(" DCT options: grayscale/color, png/jpeg", dim=True)
if 'lsb_capacity' in img_info:
click.echo()
click.secho(" Capacity Estimates:", fg='green')
click.echo(f" LSB mode: {img_info['lsb_capacity']:,} bytes")
if 'dct_capacity' in img_info:
click.echo(f" DCT mode: {img_info['dct_capacity']:,} bytes")
click.echo()
@@ -825,24 +1169,32 @@ def info(image, as_json):
@cli.command()
@click.argument('image', type=click.Path(exists=True))
@click.option('--payload-size', '-s', type=int, help='Check if specific payload size fits')
@click.option('--payload', '-p', type=click.Path(exists=True), help='Check if this file would fit')
@click.option('--size', '-s', type=int, help='Check if this many bytes would fit')
@click.option('--json', 'as_json', is_flag=True, help='Output as JSON')
def compare(image, payload_size, as_json):
def compare(image, payload, size, as_json):
"""
Compare LSB and DCT embedding modes for an image.
Compare embedding mode capacities for an image.
Shows capacity for each mode and recommends which to use.
Optionally checks if a specific payload size would fit.
Shows LSB vs DCT capacity and helps choose the right mode.
Optionally checks if a specific payload would fit.
\b
Examples:
stegasoo compare carrier.png
stegasoo compare carrier.png --payload-size 50000
stegasoo compare carrier.png --json
stegasoo compare carrier.png --payload secret.pdf
stegasoo compare carrier.png --size 50000
"""
try:
image_data = Path(image).read_bytes()
# Get payload size if provided
payload_size = None
if payload:
payload_size = len(Path(payload).read_bytes())
elif size:
payload_size = size
comparison = compare_modes(image_data)
if as_json:
@@ -1004,7 +1356,7 @@ def modes():
Displays which modes are available and their characteristics.
"""
click.echo()
click.secho("=== Stegasoo Embedding Modes (v3.2.0) ===", fg='cyan', bold=True)
click.secho("=== Stegasoo Embedding Modes (v4.0.0) ===", fg='cyan', bold=True)
click.echo()
# LSB Mode
@@ -1039,24 +1391,41 @@ def modes():
click.echo(" --dct-color color Preserves original colors")
click.echo()
# v3.2.0 Note
click.secho(" v3.2.0 Changes:", fg='cyan', bold=True)
click.echo(" ✓ No date parameters needed")
click.echo(" ✓ Single passphrase (no daily rotation)")
click.echo(" ✓ Default passphrase increased to 4 words")
click.echo(" ✓ True asynchronous communications")
# Channel Key Status (v4.0.0)
click.secho(" Channel Key (v4.0.0)", fg='cyan', bold=True)
status = get_channel_status()
if status['mode'] == 'public':
click.echo(" Status: PUBLIC (no key configured)")
click.echo(" Effect: Messages readable by any installation")
else:
click.echo(" Status: PRIVATE")
click.echo(f" Fingerprint: {status['fingerprint']}")
click.echo(f" Source: {status['source']}")
click.echo(" Effect: Messages isolated to this channel")
click.echo()
click.echo(" CLI flags:")
click.echo(" --channel KEY Use explicit channel key")
click.echo(" --channel-file F Read key from file")
click.echo(" --no-channel Force public mode")
click.echo()
# v4.0.0 Changes
click.secho(" v4.0.0 Changes:", fg='cyan', bold=True)
click.echo(" ✓ Channel key support for deployment isolation")
click.echo(" ✓ New `stegasoo channel` command group")
click.echo(" ✓ Messages encoded with channel key require same key to decode")
click.echo()
# Examples
click.secho(" Examples:", dim=True)
click.echo(" # Traditional DCT (grayscale PNG)")
click.echo(" stegasoo encode ... --mode dct")
click.echo(" # Generate channel key")
click.echo(" stegasoo channel generate --save")
click.echo()
click.echo(" # Color-preserving DCT with JPEG output")
click.echo(" stegasoo encode ... --mode dct --dct-color color --dct-format jpeg")
click.echo(" # Encode with channel isolation")
click.echo(" stegasoo encode ... --channel XXXX-XXXX-...")
click.echo()
click.echo(" # Compare modes for an image")
click.echo(" stegasoo compare carrier.png")
click.echo(" # Decode public message (no channel key)")
click.echo(" stegasoo decode ... --no-channel")
click.echo()

1073
frontends/cli/main.py_old Normal file

File diff suppressed because it is too large Load Diff