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>
This commit is contained in:
@@ -8,7 +8,7 @@ Version 4.1.0 is a feature release focusing on small-group deployment improvemen
|
||||
|
||||
1. ~~**Multi-User Support** - Admin can create up to 16 users for shared deployments~~ ✅ DONE
|
||||
2. **Channel Key QR** - Easy visual sharing of channel keys via QR codes
|
||||
3. **CLI Channel Commands** - Manage channel keys from command line
|
||||
3. ~~**CLI Channel Commands** - Manage channel keys from command line~~ ✅ DONE
|
||||
4. **Advanced Tools** - Image/stego utilities (TBD)
|
||||
|
||||
---
|
||||
@@ -426,6 +426,6 @@ Or simpler: detect on startup, update schema automatically (current pattern).
|
||||
## Progress
|
||||
|
||||
- [x] Multi-User Support (commit 7b33501)
|
||||
- [ ] Channel Key QR
|
||||
- [ ] CLI Channel Commands
|
||||
- [ ] Channel Key QR (Web UI)
|
||||
- [x] CLI Channel Commands
|
||||
- [ ] Advanced Tools
|
||||
|
||||
@@ -501,6 +501,287 @@ def info(ctx):
|
||||
click.echo(f" • Max file payload: {MAX_FILE_PAYLOAD_SIZE:,} bytes")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CHANNEL KEY COMMANDS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@cli.group()
|
||||
@click.pass_context
|
||||
def channel(ctx):
|
||||
"""
|
||||
Manage channel keys for deployment isolation.
|
||||
|
||||
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.
|
||||
|
||||
Examples:
|
||||
|
||||
stegasoo channel generate
|
||||
|
||||
stegasoo channel show
|
||||
|
||||
stegasoo channel qr
|
||||
|
||||
stegasoo channel qr -o channel-key.png
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@channel.command("generate")
|
||||
@click.option("--save", is_flag=True, help="Save to project config file")
|
||||
@click.option("--save-user", is_flag=True, help="Save to user config (~/.stegasoo/)")
|
||||
@click.pass_context
|
||||
def channel_generate(ctx, save, save_user):
|
||||
"""
|
||||
Generate a new random channel key.
|
||||
|
||||
Examples:
|
||||
|
||||
stegasoo channel generate
|
||||
|
||||
stegasoo channel generate --save
|
||||
|
||||
stegasoo channel generate --save-user
|
||||
"""
|
||||
from .channel import generate_channel_key, set_channel_key
|
||||
|
||||
key = generate_channel_key()
|
||||
|
||||
if ctx.obj.get("json"):
|
||||
result = {"channel_key": key}
|
||||
if save or save_user:
|
||||
location = "user" if save_user else "project"
|
||||
path = set_channel_key(key, location)
|
||||
result["saved_to"] = str(path)
|
||||
click.echo(json.dumps(result, indent=2))
|
||||
else:
|
||||
click.echo("Generated channel key:")
|
||||
click.echo(f" {key}")
|
||||
click.echo()
|
||||
|
||||
if save or save_user:
|
||||
location = "user" if save_user else "project"
|
||||
path = set_channel_key(key, location)
|
||||
click.echo(f"Saved to: {path}")
|
||||
else:
|
||||
click.echo("To use this key:")
|
||||
click.echo(f' export STEGASOO_CHANNEL_KEY="{key}"')
|
||||
click.echo()
|
||||
click.echo("Or save to config:")
|
||||
click.echo(" stegasoo channel generate --save")
|
||||
|
||||
|
||||
@channel.command("show")
|
||||
@click.option("--key", "explicit_key", help="Show this key instead of configured one")
|
||||
@click.pass_context
|
||||
def channel_show(ctx, explicit_key):
|
||||
"""
|
||||
Show the current channel key.
|
||||
|
||||
Examples:
|
||||
|
||||
stegasoo channel show
|
||||
|
||||
stegasoo channel show --key "ABCD-1234-..."
|
||||
"""
|
||||
from .channel import format_channel_key, get_channel_status, validate_channel_key
|
||||
|
||||
if explicit_key:
|
||||
if not validate_channel_key(explicit_key):
|
||||
click.echo("Error: Invalid channel key format", err=True)
|
||||
raise SystemExit(1)
|
||||
key = format_channel_key(explicit_key)
|
||||
source = "command line"
|
||||
else:
|
||||
status = get_channel_status()
|
||||
if not status["configured"]:
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(json.dumps({"configured": False, "mode": "public"}))
|
||||
else:
|
||||
click.echo("No channel key configured (public mode)")
|
||||
return
|
||||
key = status["key"]
|
||||
source = status["source"]
|
||||
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(json.dumps({"channel_key": key, "source": source}))
|
||||
else:
|
||||
click.echo(f"Channel key: {key}")
|
||||
click.echo(f"Source: {source}")
|
||||
|
||||
|
||||
@channel.command("status")
|
||||
@click.pass_context
|
||||
def channel_status(ctx):
|
||||
"""
|
||||
Show channel key status and configuration.
|
||||
|
||||
Examples:
|
||||
|
||||
stegasoo channel status
|
||||
|
||||
stegasoo --json channel status
|
||||
"""
|
||||
from .channel import get_channel_status
|
||||
|
||||
status = get_channel_status()
|
||||
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(json.dumps(status, indent=2))
|
||||
else:
|
||||
click.echo(f"Mode: {status['mode'].upper()}")
|
||||
if status["configured"]:
|
||||
click.echo(f"Fingerprint: {status['fingerprint']}")
|
||||
click.echo(f"Source: {status['source']}")
|
||||
else:
|
||||
click.echo("No channel key configured")
|
||||
click.echo()
|
||||
click.echo("To set up a channel key:")
|
||||
click.echo(" stegasoo channel generate --save")
|
||||
|
||||
|
||||
@channel.command("qr")
|
||||
@click.option("--key", "explicit_key", help="Generate QR for this key instead of configured one")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["ascii", "png"]),
|
||||
default="ascii",
|
||||
help="Output format (default: ascii)",
|
||||
)
|
||||
@click.option("-o", "--output", type=click.Path(), help="Output file (PNG format, or - for stdout)")
|
||||
@click.pass_context
|
||||
def channel_qr(ctx, explicit_key, output_format, output):
|
||||
"""
|
||||
Display channel key as QR code.
|
||||
|
||||
Examples:
|
||||
|
||||
stegasoo channel qr
|
||||
|
||||
stegasoo channel qr -o channel-key.png
|
||||
|
||||
stegasoo channel qr --format png -o - > key.png
|
||||
"""
|
||||
import sys
|
||||
|
||||
from .channel import format_channel_key, get_channel_key, validate_channel_key
|
||||
|
||||
# Get the key to display
|
||||
if explicit_key:
|
||||
if not validate_channel_key(explicit_key):
|
||||
click.echo("Error: Invalid channel key format", err=True)
|
||||
raise SystemExit(1)
|
||||
key = format_channel_key(explicit_key)
|
||||
else:
|
||||
key = get_channel_key()
|
||||
if not key:
|
||||
click.echo("Error: No channel key configured", err=True)
|
||||
click.echo("Generate one with: stegasoo channel generate", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
# Import qrcode
|
||||
try:
|
||||
import qrcode
|
||||
except ImportError:
|
||||
click.echo("Error: qrcode library not installed", err=True)
|
||||
click.echo("Install with: pip install qrcode[pil]", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
# Determine output mode
|
||||
if output:
|
||||
output_format = "png" # Force PNG when output file specified
|
||||
|
||||
if output_format == "png":
|
||||
# Generate PNG QR code (requires Pillow)
|
||||
try:
|
||||
import PIL # noqa: F401 - check Pillow is available
|
||||
except ImportError:
|
||||
click.echo("Error: PIL/Pillow not installed for PNG output", err=True)
|
||||
click.echo("Install with: pip install Pillow", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
||||
box_size=10,
|
||||
border=4,
|
||||
)
|
||||
qr.add_data(key)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
if output == "-":
|
||||
# Write to stdout
|
||||
img.save(sys.stdout.buffer, format="PNG")
|
||||
elif output:
|
||||
# Write to file
|
||||
img.save(output)
|
||||
click.echo(f"Saved QR code to: {output}", err=True)
|
||||
else:
|
||||
# No output specified but PNG format requested - error
|
||||
click.echo("Error: PNG format requires -o/--output", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
else:
|
||||
# ASCII output to terminal
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
||||
box_size=1,
|
||||
border=2,
|
||||
)
|
||||
qr.add_data(key)
|
||||
qr.make(fit=True)
|
||||
|
||||
click.echo()
|
||||
click.echo(f"Channel Key: {key}")
|
||||
click.echo()
|
||||
qr.print_ascii(invert=True)
|
||||
click.echo()
|
||||
click.echo("Scan this QR code to share the channel key.")
|
||||
|
||||
|
||||
@channel.command("clear")
|
||||
@click.option("--project", is_flag=True, help="Only clear project config")
|
||||
@click.option("--user", is_flag=True, help="Only clear user config")
|
||||
@click.pass_context
|
||||
def channel_clear(ctx, project, user):
|
||||
"""
|
||||
Remove channel key configuration.
|
||||
|
||||
Examples:
|
||||
|
||||
stegasoo channel clear
|
||||
|
||||
stegasoo channel clear --project
|
||||
|
||||
stegasoo channel clear --user
|
||||
"""
|
||||
from .channel import clear_channel_key
|
||||
|
||||
if project and user:
|
||||
location = "all"
|
||||
elif project:
|
||||
location = "project"
|
||||
elif user:
|
||||
location = "user"
|
||||
else:
|
||||
location = "all"
|
||||
|
||||
deleted = clear_channel_key(location)
|
||||
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(json.dumps({"deleted": [str(p) for p in deleted]}))
|
||||
else:
|
||||
if deleted:
|
||||
click.echo(f"Removed channel key from: {', '.join(str(p) for p in deleted)}")
|
||||
else:
|
||||
click.echo("No channel key files found")
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for CLI."""
|
||||
cli(obj={})
|
||||
|
||||
Reference in New Issue
Block a user