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>
This commit is contained in:
@@ -35,8 +35,8 @@ else
|
||||
fi
|
||||
|
||||
# Test credentials
|
||||
ADMIN_USER="smokeadmin"
|
||||
ADMIN_PASS="SmokeAdmin123!"
|
||||
ADMIN_USER="admin"
|
||||
ADMIN_PASS="stegasoo"
|
||||
REGULAR_USER="smokeuser"
|
||||
REGULAR_PASS="SmokeUser123!"
|
||||
|
||||
@@ -222,15 +222,35 @@ if [ "$ADMIN_LOGGED_IN" = true ]; then
|
||||
if create_test_image 2>/dev/null; then
|
||||
CSRF_TOKEN=$(echo "$ENCODE_PAGE" | grep -oP 'name="csrf_token"[^>]*value="\K[^"]+' || echo "")
|
||||
|
||||
HTTP_CODE=$(curl $CURL_OPTS -s -o "$ENCODED_IMAGE" -w "%{http_code}" \
|
||||
-b "$COOKIE_JAR" \
|
||||
# For encode: use same image as reference_photo and carrier (for simplicity)
|
||||
# First POST (no redirect follow), get Location header, then GET result page
|
||||
ENCODE_RESULT=$(curl $CURL_OPTS -s -D - -o /dev/null \
|
||||
-b "$COOKIE_JAR" -c "$COOKIE_JAR" \
|
||||
-X POST "$BASE_URL/encode" \
|
||||
-F "image=@$TEST_IMAGE" \
|
||||
-F "reference_photo=@$TEST_IMAGE" \
|
||||
-F "carrier=@$TEST_IMAGE" \
|
||||
-F "message=Admin smoke test" \
|
||||
-F "passphrase=smoke test phrase" \
|
||||
-F "pin=123456" \
|
||||
-F "csrf_token=$CSRF_TOKEN")
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ] && [ -s "$ENCODED_IMAGE" ]; then
|
||||
if file "$ENCODED_IMAGE" | grep -qi "image\|PNG\|JPEG"; then
|
||||
# Extract redirect location
|
||||
RESULT_LOCATION=$(echo "$ENCODE_RESULT" | grep -i "^location:" | tr -d '\r' | awk '{print $2}')
|
||||
|
||||
if [ -n "$RESULT_LOCATION" ]; then
|
||||
# GET the result page
|
||||
RESULT_PAGE=$(curl $CURL_OPTS -s -b "$COOKIE_JAR" "$BASE_URL$RESULT_LOCATION")
|
||||
|
||||
# Look for download link in result page
|
||||
DOWNLOAD_URL=$(echo "$RESULT_PAGE" | grep -oP 'href="(/encode/download/[^"]+)"' | head -1 | grep -oP '/encode/download/[^"]+')
|
||||
fi
|
||||
|
||||
if [ -n "$DOWNLOAD_URL" ]; then
|
||||
# Download the encoded image
|
||||
HTTP_CODE=$(curl $CURL_OPTS -s -o "$ENCODED_IMAGE" -w "%{http_code}" \
|
||||
-b "$COOKIE_JAR" "$BASE_URL$DOWNLOAD_URL")
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ] && file "$ENCODED_IMAGE" | grep -qi "image\|PNG\|JPEG"; then
|
||||
pass "Admin encoding works"
|
||||
|
||||
# Now decode it
|
||||
@@ -240,7 +260,10 @@ if [ "$ADMIN_LOGGED_IN" = true ]; then
|
||||
DECODED=$(curl $CURL_OPTS -s \
|
||||
-b "$COOKIE_JAR" \
|
||||
-X POST "$BASE_URL/decode" \
|
||||
-F "image=@$ENCODED_IMAGE" \
|
||||
-F "reference_photo=@$TEST_IMAGE" \
|
||||
-F "stego_image=@$ENCODED_IMAGE" \
|
||||
-F "passphrase=smoke test phrase" \
|
||||
-F "pin=123456" \
|
||||
-F "csrf_token=$CSRF_TOKEN")
|
||||
|
||||
if echo "$DECODED" | grep -q "Admin smoke test"; then
|
||||
@@ -249,10 +272,16 @@ if [ "$ADMIN_LOGGED_IN" = true ]; then
|
||||
fail "Admin decode failed"
|
||||
fi
|
||||
else
|
||||
fail "Encoding returned non-image response"
|
||||
fail "Failed to download encoded image (HTTP $HTTP_CODE)"
|
||||
fi
|
||||
else
|
||||
fail "Encoding request failed (HTTP $HTTP_CODE)"
|
||||
# Check for error messages in result page
|
||||
ERROR_MSG=$(echo "$RESULT_PAGE" | grep -oP 'toast-body">[^<]*<[^>]*>[^<]*' | head -1)
|
||||
if [ -n "$ERROR_MSG" ]; then
|
||||
fail "Encoding failed: $ERROR_MSG"
|
||||
else
|
||||
fail "No download link found in encode result"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
skip "Encode/Decode (no image tools)"
|
||||
@@ -322,7 +351,7 @@ echo -e "${BOLD}[6/9] Regular User Workflow${NC}"
|
||||
|
||||
if [ "$USER_CREATED" = true ]; then
|
||||
# Logout admin first (get fresh session)
|
||||
curl $CURL_OPTS -s -b "$COOKIE_JAR" "$BASE_URL/logout" > /dev/null
|
||||
curl $CURL_OPTS -s -b "$COOKIE_JAR" "$BASE_URL/logout" >/dev/null
|
||||
|
||||
# Login as regular user
|
||||
LOGIN_PAGE=$(curl $CURL_OPTS -s -c "$COOKIE_JAR_USER" "$BASE_URL/login")
|
||||
@@ -347,11 +376,14 @@ if [ "$USER_CREATED" = true ]; then
|
||||
HTTP_CODE=$(curl $CURL_OPTS -s -o "$ENCODED_IMAGE_USER" -w "%{http_code}" \
|
||||
-b "$COOKIE_JAR_USER" \
|
||||
-X POST "$BASE_URL/encode" \
|
||||
-F "image=@$TEST_IMAGE" \
|
||||
-F "reference_photo=@$TEST_IMAGE" \
|
||||
-F "carrier=@$TEST_IMAGE" \
|
||||
-F "message=User smoke test" \
|
||||
-F "passphrase=user test phrase" \
|
||||
-F "pin=567890" \
|
||||
-F "csrf_token=$CSRF_TOKEN")
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ] && [ -s "$ENCODED_IMAGE_USER" ]; then
|
||||
if [ "$HTTP_CODE" = "200" ] && [ -s "$ENCODED_IMAGE_USER" ] && file "$ENCODED_IMAGE_USER" | grep -qi "image\|PNG"; then
|
||||
pass "Regular user encoding works"
|
||||
else
|
||||
fail "Regular user encoding failed"
|
||||
@@ -384,8 +416,8 @@ curl $CURL_OPTS -s -o /dev/null \
|
||||
-L
|
||||
|
||||
# Check for recovery QR endpoint
|
||||
RECOVERY_PAGE=$(curl $CURL_OPTS -s -b "$COOKIE_JAR" "$BASE_URL/recovery" 2>/dev/null || \
|
||||
curl $CURL_OPTS -s -b "$COOKIE_JAR" "$BASE_URL/settings" 2>/dev/null || \
|
||||
RECOVERY_PAGE=$(curl $CURL_OPTS -s -b "$COOKIE_JAR" "$BASE_URL/recovery" 2>/dev/null ||
|
||||
curl $CURL_OPTS -s -b "$COOKIE_JAR" "$BASE_URL/settings" 2>/dev/null ||
|
||||
curl $CURL_OPTS -s -b "$COOKIE_JAR" "$BASE_URL/account" 2>/dev/null || echo "")
|
||||
|
||||
if echo "$RECOVERY_PAGE" | grep -qi "recovery\|qr\|backup"; then
|
||||
|
||||
@@ -56,7 +56,14 @@ def cli(ctx, json_output):
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("image", type=click.Path(exists=True))
|
||||
@click.argument("carrier", type=click.Path(exists=True))
|
||||
@click.option(
|
||||
"-r",
|
||||
"--reference",
|
||||
required=True,
|
||||
type=click.Path(exists=True),
|
||||
help="Reference photo (shared secret)",
|
||||
)
|
||||
@click.option("-m", "--message", help="Message to encode")
|
||||
@click.option(
|
||||
"-f",
|
||||
@@ -86,18 +93,20 @@ def cli(ctx, json_output):
|
||||
@click.option("--dry-run", is_flag=True, help="Show capacity usage without encoding")
|
||||
@click.pass_context
|
||||
def encode(
|
||||
ctx, image, message, file_payload, output, passphrase, pin, compress, algorithm, dry_run
|
||||
ctx, carrier, reference, message, file_payload, output, passphrase, pin, compress, algorithm, dry_run
|
||||
):
|
||||
"""
|
||||
Encode a message or file into an image.
|
||||
|
||||
Examples:
|
||||
|
||||
stegasoo encode photo.png -m "Secret message" --passphrase --pin
|
||||
stegasoo encode photo.png -r ref.jpg -m "Secret message" --passphrase --pin
|
||||
|
||||
stegasoo encode photo.png -f secret.pdf -o encoded.png
|
||||
stegasoo encode photo.png -r ref.jpg -f secret.pdf -o encoded.png
|
||||
"""
|
||||
from PIL import Image
|
||||
from .encode import encode as stegasoo_encode
|
||||
from .encode import encode_file as stegasoo_encode_file
|
||||
|
||||
if not message and not file_payload:
|
||||
raise click.UsageError("Either --message or --file is required")
|
||||
@@ -123,13 +132,14 @@ def encode(
|
||||
payload_type = "text"
|
||||
|
||||
# Get image capacity
|
||||
with Image.open(image) as img:
|
||||
with Image.open(carrier) as img:
|
||||
width, height = img.size
|
||||
capacity_bytes = (width * height * 3 // 8) - 69 # v3.2.0: corrected overhead
|
||||
|
||||
if dry_run:
|
||||
result = {
|
||||
"image": image,
|
||||
"carrier": carrier,
|
||||
"reference": reference,
|
||||
"dimensions": f"{width}x{height}",
|
||||
"capacity_bytes": capacity_bytes,
|
||||
"payload_type": payload_type,
|
||||
@@ -142,7 +152,8 @@ def encode(
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(json.dumps(result, indent=2))
|
||||
else:
|
||||
click.echo(f"Image: {image} ({width}x{height})")
|
||||
click.echo(f"Carrier: {carrier} ({width}x{height})")
|
||||
click.echo(f"Reference: {reference}")
|
||||
click.echo(f"Capacity: {capacity_bytes:,} bytes ({capacity_bytes//1024} KB)")
|
||||
click.echo(f"Payload: {payload_size:,} bytes ({payload_type})")
|
||||
click.echo(f"Compression: {algorithm_name(compression_algo)}")
|
||||
@@ -150,16 +161,46 @@ def encode(
|
||||
click.echo(f"Status: {'✓ Fits' if result['fits'] else '✗ Too large'}")
|
||||
return
|
||||
|
||||
# Actual encoding would happen here
|
||||
# For now, show what would be done
|
||||
output = output or f"{Path(image).stem}_encoded.png"
|
||||
# Read input files
|
||||
with open(reference, "rb") as f:
|
||||
reference_data = f.read()
|
||||
with open(carrier, "rb") as f:
|
||||
carrier_data = f.read()
|
||||
|
||||
# Determine output path
|
||||
output = output or f"{Path(carrier).stem}_encoded.png"
|
||||
|
||||
try:
|
||||
if file_payload:
|
||||
# Encode file
|
||||
result = stegasoo_encode_file(
|
||||
filepath=file_payload,
|
||||
reference_photo=reference_data,
|
||||
carrier_image=carrier_data,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
else:
|
||||
# Encode message
|
||||
result = stegasoo_encode(
|
||||
message=message,
|
||||
reference_photo=reference_data,
|
||||
carrier_image=carrier_data,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
# Write output
|
||||
with open(output, "wb") as f:
|
||||
f.write(result.stego_image)
|
||||
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"input": image,
|
||||
"carrier": carrier,
|
||||
"reference": reference,
|
||||
"output": output,
|
||||
"payload_type": payload_type,
|
||||
"compression": algorithm_name(compression_algo),
|
||||
@@ -169,38 +210,110 @@ def encode(
|
||||
)
|
||||
else:
|
||||
click.echo(f"✓ Encoded {payload_type} to {output}")
|
||||
click.echo(f" Reference: {reference}")
|
||||
click.echo(f" Compression: {algorithm_name(compression_algo)}")
|
||||
|
||||
except Exception as e:
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(json.dumps({"status": "error", "error": str(e)}, indent=2))
|
||||
else:
|
||||
click.echo(f"✗ Encoding failed: {e}", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("image", type=click.Path(exists=True))
|
||||
@click.option(
|
||||
"-r",
|
||||
"--reference",
|
||||
required=True,
|
||||
type=click.Path(exists=True),
|
||||
help="Reference photo (shared secret)",
|
||||
)
|
||||
@click.option("--passphrase", prompt=True, hide_input=True, help="Passphrase")
|
||||
@click.option("--pin", prompt=True, hide_input=True, help="PIN code")
|
||||
@click.option("-o", "--output", type=click.Path(), help="Output path for file payloads")
|
||||
@click.pass_context
|
||||
def decode(ctx, image, passphrase, pin, output):
|
||||
def decode(ctx, image, reference, passphrase, pin, output):
|
||||
"""
|
||||
Decode a message or file from an image.
|
||||
|
||||
Examples:
|
||||
|
||||
stegasoo decode encoded.png --passphrase --pin
|
||||
stegasoo decode encoded.png -r ref.jpg --passphrase --pin
|
||||
|
||||
stegasoo decode encoded.png -o ./extracted/
|
||||
stegasoo decode encoded.png -r ref.jpg -o ./extracted/
|
||||
"""
|
||||
# Actual decoding would happen here
|
||||
result = {
|
||||
"status": "success",
|
||||
"image": image,
|
||||
"payload_type": "text",
|
||||
"message": "[Decoded message would appear here]",
|
||||
}
|
||||
from .decode import decode as stegasoo_decode
|
||||
|
||||
# Read input files
|
||||
with open(image, "rb") as f:
|
||||
stego_data = f.read()
|
||||
with open(reference, "rb") as f:
|
||||
reference_data = f.read()
|
||||
|
||||
try:
|
||||
result = stegasoo_decode(
|
||||
stego_image=stego_data,
|
||||
reference_photo=reference_data,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
if result.is_file:
|
||||
# File payload
|
||||
filename = result.filename or "decoded_file"
|
||||
output_path = Path(output) / filename if output else Path(filename)
|
||||
|
||||
# Ensure output directory exists
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(result.file_data)
|
||||
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(json.dumps(result, indent=2))
|
||||
click.echo(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"image": image,
|
||||
"reference": reference,
|
||||
"payload_type": "file",
|
||||
"filename": filename,
|
||||
"output": str(output_path),
|
||||
"size": len(result.file_data),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
else:
|
||||
click.echo(f"✓ Extracted file: {output_path}")
|
||||
click.echo(f" Size: {len(result.file_data):,} bytes")
|
||||
else:
|
||||
# Text message
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "success",
|
||||
"image": image,
|
||||
"reference": reference,
|
||||
"payload_type": "text",
|
||||
"message": result.message,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
else:
|
||||
click.echo(f"Decoded from {image}:")
|
||||
click.echo(result["message"])
|
||||
click.echo(result.message)
|
||||
|
||||
except Exception as e:
|
||||
if ctx.obj.get("json"):
|
||||
click.echo(json.dumps({"status": "error", "error": str(e)}, indent=2))
|
||||
else:
|
||||
click.echo(f"✗ Decoding failed: {e}", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user