Added file support and increased file limits.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
Stegasoo REST API
|
||||
|
||||
FastAPI-based REST API for steganography operations.
|
||||
Designed for integration with other services and automation.
|
||||
Supports both text messages and file embedding.
|
||||
"""
|
||||
|
||||
import io
|
||||
@@ -28,6 +28,8 @@ from stegasoo import (
|
||||
DAY_NAMES, __version__,
|
||||
StegasooError, DecryptionError, CapacityError,
|
||||
has_argon2,
|
||||
FilePayload,
|
||||
MAX_FILE_PAYLOAD_SIZE,
|
||||
)
|
||||
from stegasoo.constants import (
|
||||
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
||||
@@ -42,7 +44,7 @@ from stegasoo.constants import (
|
||||
|
||||
app = FastAPI(
|
||||
title="Stegasoo API",
|
||||
description="Secure steganography with hybrid authentication",
|
||||
description="Secure steganography with hybrid authentication. Supports text messages and file embedding.",
|
||||
version=__version__,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
@@ -79,6 +81,20 @@ class EncodeRequest(BaseModel):
|
||||
date_str: Optional[str] = None
|
||||
|
||||
|
||||
class EncodeFileRequest(BaseModel):
|
||||
"""Request for embedding a file (base64-encoded)."""
|
||||
file_data_base64: str
|
||||
filename: str
|
||||
mime_type: Optional[str] = None
|
||||
reference_photo_base64: str
|
||||
carrier_image_base64: str
|
||||
day_phrase: str
|
||||
pin: str = ""
|
||||
rsa_key_base64: Optional[str] = None
|
||||
rsa_password: Optional[str] = None
|
||||
date_str: Optional[str] = None
|
||||
|
||||
|
||||
class EncodeResponse(BaseModel):
|
||||
stego_image_base64: str
|
||||
filename: str
|
||||
@@ -97,7 +113,12 @@ class DecodeRequest(BaseModel):
|
||||
|
||||
|
||||
class DecodeResponse(BaseModel):
|
||||
message: str
|
||||
"""Response for decode - can be text or file."""
|
||||
payload_type: str # 'text' or 'file'
|
||||
message: Optional[str] = None # For text
|
||||
file_data_base64: Optional[str] = None # For file (base64-encoded)
|
||||
filename: Optional[str] = None # For file
|
||||
mime_type: Optional[str] = None # For file
|
||||
|
||||
|
||||
class ImageInfoResponse(BaseModel):
|
||||
@@ -112,6 +133,7 @@ class StatusResponse(BaseModel):
|
||||
version: str
|
||||
has_argon2: bool
|
||||
day_names: list[str]
|
||||
max_payload_kb: int
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
@@ -129,7 +151,8 @@ async def root():
|
||||
return StatusResponse(
|
||||
version=__version__,
|
||||
has_argon2=has_argon2(),
|
||||
day_names=list(DAY_NAMES)
|
||||
day_names=list(DAY_NAMES),
|
||||
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
|
||||
)
|
||||
|
||||
|
||||
@@ -173,7 +196,7 @@ async def api_generate(request: GenerateRequest):
|
||||
@app.post("/encode", response_model=EncodeResponse)
|
||||
async def api_encode(request: EncodeRequest):
|
||||
"""
|
||||
Encode a secret message into an image.
|
||||
Encode a text message into an image.
|
||||
|
||||
Images must be base64-encoded. Returns base64-encoded stego image.
|
||||
"""
|
||||
@@ -194,8 +217,55 @@ async def api_encode(request: EncodeRequest):
|
||||
)
|
||||
|
||||
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
|
||||
day_of_week = get_day_from_date(result.date_used)
|
||||
|
||||
# Get day of week from the date used
|
||||
return EncodeResponse(
|
||||
stego_image_base64=stego_b64,
|
||||
filename=result.filename,
|
||||
capacity_used_percent=result.capacity_percent,
|
||||
date_used=result.date_used,
|
||||
day_of_week=day_of_week
|
||||
)
|
||||
|
||||
except CapacityError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
except StegasooError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(500, str(e))
|
||||
|
||||
|
||||
@app.post("/encode/file", response_model=EncodeResponse)
|
||||
async def api_encode_file(request: EncodeFileRequest):
|
||||
"""
|
||||
Encode a file into an image (JSON with base64).
|
||||
|
||||
File data must be base64-encoded.
|
||||
"""
|
||||
try:
|
||||
file_data = base64.b64decode(request.file_data_base64)
|
||||
ref_photo = base64.b64decode(request.reference_photo_base64)
|
||||
carrier = base64.b64decode(request.carrier_image_base64)
|
||||
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
|
||||
|
||||
payload = FilePayload(
|
||||
data=file_data,
|
||||
filename=request.filename,
|
||||
mime_type=request.mime_type
|
||||
)
|
||||
|
||||
result = encode(
|
||||
message=payload,
|
||||
reference_photo=ref_photo,
|
||||
carrier_image=carrier,
|
||||
day_phrase=request.day_phrase,
|
||||
pin=request.pin,
|
||||
rsa_key_data=rsa_key,
|
||||
rsa_password=request.rsa_password,
|
||||
date_str=request.date_str
|
||||
)
|
||||
|
||||
stego_b64 = base64.b64encode(result.stego_image).decode('utf-8')
|
||||
day_of_week = get_day_from_date(result.date_used)
|
||||
|
||||
return EncodeResponse(
|
||||
@@ -217,16 +287,16 @@ async def api_encode(request: EncodeRequest):
|
||||
@app.post("/decode", response_model=DecodeResponse)
|
||||
async def api_decode(request: DecodeRequest):
|
||||
"""
|
||||
Decode a secret message from a stego image.
|
||||
Decode a message or file from a stego image.
|
||||
|
||||
Images must be base64-encoded.
|
||||
Returns payload_type to indicate if result is text or file.
|
||||
"""
|
||||
try:
|
||||
stego = base64.b64decode(request.stego_image_base64)
|
||||
ref_photo = base64.b64decode(request.reference_photo_base64)
|
||||
rsa_key = base64.b64decode(request.rsa_key_base64) if request.rsa_key_base64 else None
|
||||
|
||||
message = decode(
|
||||
result = decode(
|
||||
stego_image=stego,
|
||||
reference_photo=ref_photo,
|
||||
day_phrase=request.day_phrase,
|
||||
@@ -235,7 +305,18 @@ async def api_decode(request: DecodeRequest):
|
||||
rsa_password=request.rsa_password
|
||||
)
|
||||
|
||||
return DecodeResponse(message=message)
|
||||
if result.is_file:
|
||||
return DecodeResponse(
|
||||
payload_type='file',
|
||||
file_data_base64=base64.b64encode(result.file_data).decode('utf-8'),
|
||||
filename=result.filename,
|
||||
mime_type=result.mime_type
|
||||
)
|
||||
else:
|
||||
return DecodeResponse(
|
||||
payload_type='text',
|
||||
message=result.message
|
||||
)
|
||||
|
||||
except DecryptionError as e:
|
||||
raise HTTPException(401, "Decryption failed. Check credentials.")
|
||||
@@ -247,10 +328,11 @@ async def api_decode(request: DecodeRequest):
|
||||
|
||||
@app.post("/encode/multipart")
|
||||
async def api_encode_multipart(
|
||||
message: str = Form(...),
|
||||
day_phrase: str = Form(...),
|
||||
reference_photo: UploadFile = File(...),
|
||||
carrier: UploadFile = File(...),
|
||||
message: str = Form(""),
|
||||
payload_file: Optional[UploadFile] = File(None),
|
||||
pin: str = Form(""),
|
||||
rsa_key: Optional[UploadFile] = File(None),
|
||||
rsa_password: str = Form(""),
|
||||
@@ -259,6 +341,7 @@ async def api_encode_multipart(
|
||||
"""
|
||||
Encode using multipart form data (file uploads).
|
||||
|
||||
Provide either 'message' (text) or 'payload_file' (binary file).
|
||||
Returns the stego image directly as PNG with metadata headers.
|
||||
"""
|
||||
try:
|
||||
@@ -266,8 +349,21 @@ async def api_encode_multipart(
|
||||
carrier_data = await carrier.read()
|
||||
rsa_key_data = await rsa_key.read() if rsa_key else None
|
||||
|
||||
# Determine payload
|
||||
if payload_file and payload_file.filename:
|
||||
file_data = await payload_file.read()
|
||||
payload = FilePayload(
|
||||
data=file_data,
|
||||
filename=payload_file.filename,
|
||||
mime_type=payload_file.content_type
|
||||
)
|
||||
elif message:
|
||||
payload = message
|
||||
else:
|
||||
raise HTTPException(400, "Must provide either 'message' or 'payload_file'")
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
message=payload,
|
||||
reference_photo=ref_data,
|
||||
carrier_image=carrier_data,
|
||||
day_phrase=day_phrase,
|
||||
@@ -277,7 +373,6 @@ async def api_encode_multipart(
|
||||
date_str=date_str if date_str else None
|
||||
)
|
||||
|
||||
# Get day of week from the date used
|
||||
day_of_week = get_day_from_date(result.date_used)
|
||||
|
||||
return Response(
|
||||
@@ -310,13 +405,15 @@ async def api_decode_multipart(
|
||||
):
|
||||
"""
|
||||
Decode using multipart form data (file uploads).
|
||||
|
||||
Returns JSON with payload_type indicating text or file.
|
||||
"""
|
||||
try:
|
||||
ref_data = await reference_photo.read()
|
||||
stego_data = await stego_image.read()
|
||||
rsa_key_data = await rsa_key.read() if rsa_key else None
|
||||
|
||||
message = decode(
|
||||
result = decode(
|
||||
stego_image=stego_data,
|
||||
reference_photo=ref_data,
|
||||
day_phrase=day_phrase,
|
||||
@@ -325,7 +422,18 @@ async def api_decode_multipart(
|
||||
rsa_password=rsa_password if rsa_password else None
|
||||
)
|
||||
|
||||
return DecodeResponse(message=message)
|
||||
if result.is_file:
|
||||
return DecodeResponse(
|
||||
payload_type='file',
|
||||
file_data_base64=base64.b64encode(result.file_data).decode('utf-8'),
|
||||
filename=result.filename,
|
||||
mime_type=result.mime_type
|
||||
)
|
||||
else:
|
||||
return DecodeResponse(
|
||||
payload_type='text',
|
||||
message=result.message
|
||||
)
|
||||
|
||||
except DecryptionError:
|
||||
raise HTTPException(401, "Decryption failed. Check credentials.")
|
||||
|
||||
@@ -20,12 +20,14 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
||||
|
||||
import stegasoo
|
||||
from stegasoo import (
|
||||
encode, decode, generate_credentials,
|
||||
encode, encode_file, decode,
|
||||
generate_credentials,
|
||||
export_rsa_key_pem, load_rsa_key,
|
||||
validate_image, calculate_capacity,
|
||||
get_day_from_date, parse_date_from_filename,
|
||||
DAY_NAMES, __version__,
|
||||
StegasooError, DecryptionError, ExtractionError,
|
||||
FilePayload,
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +44,7 @@ def cli():
|
||||
"""
|
||||
Stegasoo - Secure steganography with hybrid authentication.
|
||||
|
||||
Hide encrypted messages in images using a combination of:
|
||||
Hide encrypted messages or files in images using a combination of:
|
||||
|
||||
\b
|
||||
• Reference photo (something you have)
|
||||
@@ -170,8 +172,9 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
||||
@cli.command()
|
||||
@click.option('--ref', '-r', required=True, type=click.Path(exists=True), help='Reference photo')
|
||||
@click.option('--carrier', '-c', required=True, type=click.Path(exists=True), help='Carrier image')
|
||||
@click.option('--message', '-m', help='Message to encode (or use stdin)')
|
||||
@click.option('--message-file', '-f', type=click.Path(exists=True), help='Read message from file')
|
||||
@click.option('--message', '-m', help='Text message to encode')
|
||||
@click.option('--message-file', '-f', type=click.Path(exists=True), help='Read text message from file')
|
||||
@click.option('--embed-file', '-e', type=click.Path(exists=True), help='Embed a file (binary)')
|
||||
@click.option('--phrase', '-p', required=True, help='Day phrase')
|
||||
@click.option('--pin', help='Static PIN')
|
||||
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file')
|
||||
@@ -179,28 +182,46 @@ def generate(pin, rsa, pin_length, rsa_bits, words, output, password, as_json):
|
||||
@click.option('--output', '-o', type=click.Path(), help='Output file (default: auto-generated)')
|
||||
@click.option('--date', 'date_str', help='Date override (YYYY-MM-DD)')
|
||||
@click.option('--quiet', '-q', is_flag=True, help='Suppress output except errors')
|
||||
def encode_cmd(ref, carrier, message, message_file, phrase, pin, key, key_password, output, date_str, quiet):
|
||||
def encode_cmd(ref, carrier, message, message_file, embed_file, phrase, pin, key, key_password, output, date_str, quiet):
|
||||
"""
|
||||
Encode a secret message into an image.
|
||||
Encode a secret message or file into an image.
|
||||
|
||||
Requires a reference photo, carrier image, and day phrase.
|
||||
Must provide either --pin or --key (or both).
|
||||
|
||||
For text messages, use -m or -f or pipe via stdin.
|
||||
For binary files, use -e/--embed-file.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
# Text message
|
||||
stegasoo encode -r photo.jpg -c meme.png -p "apple forest thunder" --pin 123456 -m "secret"
|
||||
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "word1 word2 word3" --pin 123456
|
||||
stegasoo encode -r photo.jpg -c meme.png -p "words" -k mykey.pem --key-password "pass"
|
||||
|
||||
# Text from file
|
||||
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -f message.txt
|
||||
|
||||
# Embed a binary file (PDF, ZIP, etc.)
|
||||
stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456 -e secret.pdf
|
||||
|
||||
# Pipe text
|
||||
echo "secret" | stegasoo encode -r photo.jpg -c meme.png -p "words" --pin 123456
|
||||
"""
|
||||
# Get message
|
||||
if message:
|
||||
msg = message
|
||||
# Determine what to encode
|
||||
payload = None
|
||||
|
||||
if embed_file:
|
||||
# Binary file embedding
|
||||
payload = FilePayload.from_file(embed_file)
|
||||
if not quiet:
|
||||
click.echo(f"Embedding file: {payload.filename} ({len(payload.data):,} bytes)")
|
||||
elif message:
|
||||
payload = message
|
||||
elif message_file:
|
||||
msg = Path(message_file).read_text()
|
||||
payload = Path(message_file).read_text()
|
||||
elif not sys.stdin.isatty():
|
||||
msg = sys.stdin.read()
|
||||
payload = sys.stdin.read()
|
||||
else:
|
||||
raise click.UsageError("Must provide message via -m, -f, or stdin")
|
||||
raise click.UsageError("Must provide message via -m, -f, -e, or stdin")
|
||||
|
||||
# Load key if provided
|
||||
rsa_key_data = None
|
||||
@@ -216,7 +237,7 @@ def encode_cmd(ref, carrier, message, message_file, phrase, pin, key, key_passwo
|
||||
carrier_image = Path(carrier).read_bytes()
|
||||
|
||||
result = encode(
|
||||
message=msg,
|
||||
message=payload,
|
||||
reference_photo=ref_photo,
|
||||
carrier_image=carrier_image,
|
||||
day_phrase=phrase,
|
||||
@@ -259,19 +280,26 @@ def encode_cmd(ref, carrier, message, message_file, phrase, pin, key, key_passwo
|
||||
@click.option('--pin', help='Static PIN')
|
||||
@click.option('--key', '-k', type=click.Path(exists=True), help='RSA key file')
|
||||
@click.option('--key-password', help='RSA key password')
|
||||
@click.option('--output', '-o', type=click.Path(), help='Save message to file')
|
||||
@click.option('--quiet', '-q', is_flag=True, help='Output only the message')
|
||||
def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet):
|
||||
@click.option('--output', '-o', type=click.Path(), help='Save decoded content to file')
|
||||
@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, phrase, pin, key, key_password, output, quiet, force):
|
||||
"""
|
||||
Decode a secret message from a stego image.
|
||||
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.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
# Decode and print text
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "apple forest thunder" --pin 123456
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "words" -k mykey.pem --key-password "pass"
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o message.txt
|
||||
|
||||
# Decode and save (auto-detect type)
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -o output.txt
|
||||
|
||||
# Quiet mode for piping text
|
||||
stegasoo decode -r photo.jpg -s stego.png -p "words" --pin 123456 -q | less
|
||||
"""
|
||||
# Load key if provided
|
||||
rsa_key_data = None
|
||||
@@ -286,7 +314,7 @@ def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet):
|
||||
ref_photo = Path(ref).read_bytes()
|
||||
stego_image = Path(stego).read_bytes()
|
||||
|
||||
message = decode(
|
||||
result = decode(
|
||||
stego_image=stego_image,
|
||||
reference_photo=ref_photo,
|
||||
day_phrase=phrase,
|
||||
@@ -295,18 +323,42 @@ def decode_cmd(ref, stego, phrase, pin, key, key_password, output, quiet):
|
||||
rsa_password=key_password,
|
||||
)
|
||||
|
||||
if result.is_file:
|
||||
# File content
|
||||
if output:
|
||||
Path(output).write_text(message)
|
||||
out_path = Path(output)
|
||||
elif result.filename:
|
||||
out_path = Path(result.filename)
|
||||
else:
|
||||
out_path = Path("decoded_file")
|
||||
|
||||
if out_path.exists() and not force:
|
||||
raise click.ClickException(
|
||||
f"Output file '{out_path}' exists. Use --force to overwrite."
|
||||
)
|
||||
|
||||
out_path.write_bytes(result.file_data)
|
||||
|
||||
if not quiet:
|
||||
click.secho(f"✓ Decoded successfully!", fg='green')
|
||||
click.secho("✓ Decoded file successfully!", fg='green')
|
||||
click.echo(f" Saved to: {out_path}")
|
||||
click.echo(f" Size: {len(result.file_data):,} bytes")
|
||||
if result.mime_type:
|
||||
click.echo(f" Type: {result.mime_type}")
|
||||
else:
|
||||
# Text content
|
||||
if output:
|
||||
Path(output).write_text(result.message)
|
||||
if not quiet:
|
||||
click.secho("✓ Decoded successfully!", fg='green')
|
||||
click.echo(f" Saved to: {output}")
|
||||
else:
|
||||
if quiet:
|
||||
click.echo(message)
|
||||
click.echo(result.message)
|
||||
else:
|
||||
click.secho("✓ Decoded successfully!", fg='green')
|
||||
click.echo()
|
||||
click.echo(message)
|
||||
click.echo(result.message)
|
||||
|
||||
except (DecryptionError, ExtractionError) as e:
|
||||
raise click.ClickException(f"Decryption failed: {e}")
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
Stegasoo Web Frontend
|
||||
|
||||
Flask-based web UI for steganography operations.
|
||||
This is a thin wrapper around the stegasoo library.
|
||||
Supports both text messages and file embedding.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import secrets
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
@@ -27,14 +28,17 @@ from stegasoo import (
|
||||
export_rsa_key_pem, load_rsa_key,
|
||||
validate_pin, validate_message, validate_image,
|
||||
validate_rsa_key, validate_security_factors,
|
||||
validate_file_payload,
|
||||
get_today_day, generate_filename,
|
||||
DAY_NAMES, __version__,
|
||||
StegasooError, DecryptionError, CapacityError,
|
||||
has_argon2,
|
||||
FilePayload,
|
||||
MAX_FILE_PAYLOAD_SIZE,
|
||||
)
|
||||
from stegasoo.constants import (
|
||||
MAX_MESSAGE_SIZE, MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
||||
VALID_RSA_SIZES,
|
||||
VALID_RSA_SIZES, MAX_FILE_SIZE,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,7 +48,7 @@ from stegasoo.constants import (
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = secrets.token_hex(32)
|
||||
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5MB max upload
|
||||
app.config['MAX_CONTENT_LENGTH'] = MAX_FILE_SIZE # 10MB max upload
|
||||
|
||||
# Temporary file storage for sharing (file_id -> {data, timestamp, filename})
|
||||
TEMP_FILES: dict[str, dict] = {}
|
||||
@@ -67,6 +71,16 @@ def allowed_image(filename: str) -> bool:
|
||||
return ext in {'png', 'jpg', 'jpeg', 'bmp', 'gif'}
|
||||
|
||||
|
||||
def format_size(size_bytes: int) -> str:
|
||||
"""Format file size for display."""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.1f} KB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ROUTES
|
||||
# ============================================================================
|
||||
@@ -164,6 +178,7 @@ def download_key():
|
||||
@app.route('/encode', methods=['GET', 'POST'])
|
||||
def encode_page():
|
||||
day_of_week = get_today_day()
|
||||
max_payload_kb = MAX_FILE_PAYLOAD_SIZE // 1024
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
@@ -171,30 +186,50 @@ def encode_page():
|
||||
ref_photo = request.files.get('reference_photo')
|
||||
carrier = request.files.get('carrier')
|
||||
rsa_key_file = request.files.get('rsa_key')
|
||||
payload_file = request.files.get('payload_file')
|
||||
|
||||
if not ref_photo or not carrier:
|
||||
flash('Both reference photo and carrier image are required', 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
if not allowed_image(ref_photo.filename) or not allowed_image(carrier.filename):
|
||||
flash('Invalid file type. Use PNG, JPG, or BMP', 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Get form data
|
||||
message = request.form.get('message', '')
|
||||
day_phrase = request.form.get('day_phrase', '')
|
||||
pin = request.form.get('pin', '').strip()
|
||||
rsa_password = request.form.get('rsa_password', '')
|
||||
payload_type = request.form.get('payload_type', 'text')
|
||||
|
||||
# Validate message
|
||||
# Determine payload
|
||||
if payload_type == 'file' and payload_file and payload_file.filename:
|
||||
# File payload
|
||||
file_data = payload_file.read()
|
||||
|
||||
result = validate_file_payload(file_data, payload_file.filename)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(payload_file.filename)
|
||||
payload = FilePayload(
|
||||
data=file_data,
|
||||
filename=payload_file.filename,
|
||||
mime_type=mime_type
|
||||
)
|
||||
else:
|
||||
# Text message
|
||||
result = validate_message(message)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
payload = message
|
||||
|
||||
if not day_phrase:
|
||||
flash('Day phrase is required', 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Read files
|
||||
ref_data = ref_photo.read()
|
||||
@@ -205,27 +240,27 @@ def encode_page():
|
||||
result = validate_security_factors(pin, rsa_key_data)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Validate PIN if provided
|
||||
if pin:
|
||||
result = validate_pin(pin)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Validate RSA key if provided
|
||||
if rsa_key_data:
|
||||
result = validate_rsa_key(rsa_key_data, rsa_password if rsa_password else None)
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Validate carrier image
|
||||
result = validate_image(carrier_data, "Carrier image")
|
||||
if not result.is_valid:
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
# Get date
|
||||
client_date = request.form.get('client_date', '').strip()
|
||||
@@ -236,7 +271,7 @@ def encode_page():
|
||||
|
||||
# Encode
|
||||
encode_result = encode(
|
||||
message=message,
|
||||
message=payload,
|
||||
reference_photo=ref_data,
|
||||
carrier_image=carrier_data,
|
||||
day_phrase=day_phrase,
|
||||
@@ -259,15 +294,15 @@ def encode_page():
|
||||
|
||||
except CapacityError as e:
|
||||
flash(str(e), 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
except StegasooError as e:
|
||||
flash(str(e), 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
except Exception as e:
|
||||
flash(f'Error: {e}', 'error')
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
return render_template('encode.html', day_of_week=day_of_week)
|
||||
return render_template('encode.html', day_of_week=day_of_week, max_payload_kb=max_payload_kb)
|
||||
|
||||
|
||||
@app.route('/encode/result/<file_id>')
|
||||
@@ -299,7 +334,7 @@ def encode_download(file_id):
|
||||
|
||||
|
||||
@app.route('/encode/file/<file_id>')
|
||||
def encode_file(file_id):
|
||||
def encode_file_route(file_id):
|
||||
"""Serve file for Web Share API."""
|
||||
if file_id not in TEMP_FILES:
|
||||
return "Not found", 404
|
||||
@@ -368,7 +403,7 @@ def decode_page():
|
||||
return render_template('decode.html')
|
||||
|
||||
# Decode
|
||||
message = decode(
|
||||
decode_result = decode(
|
||||
stego_image=stego_data,
|
||||
reference_photo=ref_data,
|
||||
day_phrase=day_phrase,
|
||||
@@ -377,7 +412,29 @@ def decode_page():
|
||||
rsa_password=rsa_password if rsa_password else None
|
||||
)
|
||||
|
||||
return render_template('decode.html', decoded_message=message)
|
||||
if decode_result.is_file:
|
||||
# File content - store temporarily for download
|
||||
file_id = secrets.token_urlsafe(16)
|
||||
cleanup_temp_files()
|
||||
|
||||
filename = decode_result.filename or 'decoded_file'
|
||||
TEMP_FILES[file_id] = {
|
||||
'data': decode_result.file_data,
|
||||
'filename': filename,
|
||||
'mime_type': decode_result.mime_type,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
return render_template('decode.html',
|
||||
decoded_file=True,
|
||||
file_id=file_id,
|
||||
filename=filename,
|
||||
file_size=format_size(len(decode_result.file_data)),
|
||||
mime_type=decode_result.mime_type
|
||||
)
|
||||
else:
|
||||
# Text content
|
||||
return render_template('decode.html', decoded_message=decode_result.message)
|
||||
|
||||
except DecryptionError:
|
||||
flash('Decryption failed. Check your phrase, PIN, RSA key, and reference photo.', 'error')
|
||||
@@ -392,9 +449,30 @@ def decode_page():
|
||||
return render_template('decode.html')
|
||||
|
||||
|
||||
@app.route('/decode/download/<file_id>')
|
||||
def decode_download(file_id):
|
||||
"""Download decoded file."""
|
||||
if file_id not in TEMP_FILES:
|
||||
flash('File expired or not found.', 'error')
|
||||
return redirect(url_for('decode_page'))
|
||||
|
||||
file_info = TEMP_FILES[file_id]
|
||||
mime_type = file_info.get('mime_type', 'application/octet-stream')
|
||||
|
||||
return send_file(
|
||||
io.BytesIO(file_info['data']),
|
||||
mimetype=mime_type,
|
||||
as_attachment=True,
|
||||
download_name=file_info['filename']
|
||||
)
|
||||
|
||||
|
||||
@app.route('/about')
|
||||
def about():
|
||||
return render_template('about.html', has_argon2=has_argon2())
|
||||
return render_template('about.html',
|
||||
has_argon2=has_argon2(),
|
||||
max_payload_kb=MAX_FILE_PAYLOAD_SIZE // 1024
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-unlock-fill me-2"></i>Decode Secret Message</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-unlock-fill me-2"></i>Decode Secret Message or File</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if decoded_message %}
|
||||
<!-- Text Message Result -->
|
||||
<div class="alert alert-success">
|
||||
<h6><i class="bi bi-check-circle me-2"></i>Message Decrypted Successfully!</h6>
|
||||
</div>
|
||||
@@ -23,17 +24,40 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
i<!--then<div class="mb-4">
|
||||
<label class="form-label text-muted">Decoded Message:</label>
|
||||
<div class="alert-message">{{ decoded_message }}</div>
|
||||
</div>-->
|
||||
<a href="/decode" class="btn btn-outline-light w-100 mt-3">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Decode Another
|
||||
</a>
|
||||
|
||||
{% elif decoded_file %}
|
||||
<!-- File Result -->
|
||||
<div class="alert alert-success">
|
||||
<h6><i class="bi bi-check-circle me-2"></i>File Decrypted Successfully!</h6>
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<i class="bi bi-file-earmark-check text-success" style="font-size: 4rem;"></i>
|
||||
<h5 class="mt-3">{{ filename }}</h5>
|
||||
<p class="text-muted mb-1">{{ file_size }}</p>
|
||||
{% if mime_type %}
|
||||
<small class="text-muted">Type: {{ mime_type }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('decode_download', file_id=file_id) }}" class="btn btn-primary btn-lg w-100 mb-3">
|
||||
<i class="bi bi-download me-2"></i>Download File
|
||||
</a>
|
||||
|
||||
<div class="alert alert-warning small">
|
||||
<i class="bi bi-clock me-1"></i>
|
||||
<strong>File expires in 5 minutes.</strong> Download now.
|
||||
</div>
|
||||
|
||||
<a href="/decode" class="btn btn-outline-light w-100">
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Decode Another Message
|
||||
<i class="bi bi-arrow-repeat me-2"></i>Decode Another
|
||||
</a>
|
||||
|
||||
{% else %}
|
||||
|
||||
<!-- Decode Form -->
|
||||
<form method="POST" enctype="multipart/form-data" id="decodeForm">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
@@ -66,7 +90,7 @@
|
||||
<img class="drop-zone-preview d-none">
|
||||
</div>
|
||||
<div class="form-text">
|
||||
The image containing the hidden message
|
||||
The image containing the hidden message/file
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,7 +154,7 @@
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="decodeBtn">
|
||||
<i class="bi bi-unlock me-2"></i>Decode Message
|
||||
<i class="bi bi-unlock me-2"></i>Decode
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -138,6 +162,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not decoded_message and not decoded_file %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h6 class="text-muted mb-3"><i class="bi bi-question-circle me-2"></i>Troubleshooting</h6>
|
||||
@@ -165,6 +190,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -209,7 +235,7 @@ function updateDayLabel(dayName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1. PIN Toggle
|
||||
// PIN Toggle
|
||||
document.getElementById('togglePin')?.addEventListener('click', function() {
|
||||
const input = document.getElementById('pinInput');
|
||||
const icon = this.querySelector('i');
|
||||
@@ -222,9 +248,8 @@ document.getElementById('togglePin')?.addEventListener('click', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Paste from Clipboard
|
||||
// Paste from Clipboard
|
||||
document.addEventListener('paste', function(e) {
|
||||
// Only run if the form exists (we are not on the success page)
|
||||
if (!document.getElementById('decodeForm')) return;
|
||||
|
||||
const items = e.clipboardData.items;
|
||||
@@ -232,7 +257,6 @@ document.addEventListener('paste', function(e) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
const blob = items[i].getAsFile();
|
||||
|
||||
// Priority for Decode: Fill Stego Image first (most common), then Reference
|
||||
const stegoInput = document.querySelector('input[name="stego_image"]');
|
||||
const refInput = document.querySelector('input[name="reference_photo"]');
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-lock-fill me-2"></i>Encode Secret Message</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-lock-fill me-2"></i>Encode Secret Message or File</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" enctype="multipart/form-data" id="encodeForm">
|
||||
@@ -49,15 +49,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payload Type Selector -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-box me-1"></i> What to Encode
|
||||
</label>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="payload_type" id="payloadText" value="text" checked>
|
||||
<label class="btn btn-outline-primary" for="payloadText">
|
||||
<i class="bi bi-chat-left-text me-1"></i> Text Message
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="payload_type" id="payloadFile" value="file">
|
||||
<label class="btn btn-outline-primary" for="payloadFile">
|
||||
<i class="bi bi-file-earmark me-1"></i> File
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Text Message Input -->
|
||||
<div class="mb-3" id="textPayloadSection">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-chat-left-text me-1"></i> Secret Message
|
||||
</label>
|
||||
<textarea name="message" class="form-control" rows="4" id="messageInput"
|
||||
placeholder="Enter your secret message here..." required></textarea>
|
||||
placeholder="Enter your secret message here..."></textarea>
|
||||
<div class="d-flex justify-content-between form-text">
|
||||
<span>
|
||||
<span id="charCount">0</span> / 50,000 characters
|
||||
<span id="charCount">0</span> / 250,000 characters
|
||||
<span id="charWarning" class="text-warning d-none ms-2">
|
||||
<i class="bi bi-exclamation-triangle"></i> Getting long!
|
||||
</span>
|
||||
@@ -66,6 +85,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Input -->
|
||||
<div class="mb-3 d-none" id="filePayloadSection">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-earmark me-1"></i> File to Embed
|
||||
</label>
|
||||
<div class="drop-zone" id="payloadDropZone">
|
||||
<input type="file" name="payload_file" id="payloadFileInput">
|
||||
<div class="drop-zone-label" id="payloadDropLabel">
|
||||
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
|
||||
<span class="text-muted">Drop any file or click to browse</span>
|
||||
<div class="small text-muted mt-1">Max {{ max_payload_kb }} KB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Supports any file type: PDF, ZIP, documents, etc.
|
||||
</div>
|
||||
<div id="fileInfo" class="d-none mt-2 p-2 bg-dark rounded">
|
||||
<i class="bi bi-file-earmark-check text-success me-2"></i>
|
||||
<span id="fileInfoName"></span>
|
||||
<span class="text-muted">(<span id="fileInfoSize"></span>)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" id="dayPhraseLabel">
|
||||
<i class="bi bi-chat-quote me-1"></i> {{ day_of_week }}'s Phrase
|
||||
@@ -121,7 +163,7 @@
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="encodeBtn">
|
||||
<i class="bi bi-lock me-2"></i>Encode Message
|
||||
<i class="bi bi-lock me-2"></i>Encode
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -146,8 +188,8 @@
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>Limits:</strong>
|
||||
Carrier image max ~4 megapixels (2000×2000).
|
||||
Files max 5MB each.
|
||||
Message max 50KB.
|
||||
Files max 10MB upload.
|
||||
Payload max {{ max_payload_kb }} KB.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -177,6 +219,57 @@ if (dateInput) {
|
||||
dateInput.value = localDate;
|
||||
}
|
||||
|
||||
// Payload type switching
|
||||
const payloadTextRadio = document.getElementById('payloadText');
|
||||
const payloadFileRadio = document.getElementById('payloadFile');
|
||||
const textSection = document.getElementById('textPayloadSection');
|
||||
const fileSection = document.getElementById('filePayloadSection');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const payloadFileInput = document.getElementById('payloadFileInput');
|
||||
|
||||
function updatePayloadSection() {
|
||||
const isText = payloadTextRadio.checked;
|
||||
textSection.classList.toggle('d-none', !isText);
|
||||
fileSection.classList.toggle('d-none', isText);
|
||||
|
||||
// Update required attribute
|
||||
if (isText) {
|
||||
messageInput.required = true;
|
||||
payloadFileInput.required = false;
|
||||
} else {
|
||||
messageInput.required = false;
|
||||
payloadFileInput.required = true;
|
||||
}
|
||||
}
|
||||
|
||||
payloadTextRadio.addEventListener('change', updatePayloadSection);
|
||||
payloadFileRadio.addEventListener('change', updatePayloadSection);
|
||||
|
||||
// File payload info display
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const fileInfoName = document.getElementById('fileInfoName');
|
||||
const fileInfoSize = document.getElementById('fileInfoSize');
|
||||
const payloadDropLabel = document.getElementById('payloadDropLabel');
|
||||
|
||||
payloadFileInput.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const file = this.files[0];
|
||||
fileInfoName.textContent = file.name;
|
||||
fileInfoSize.textContent = formatFileSize(file.size);
|
||||
fileInfo.classList.remove('d-none');
|
||||
payloadDropLabel.innerHTML = `<i class="bi bi-check-circle text-success fs-3 d-block mb-2"></i><span>${file.name}</span>`;
|
||||
} else {
|
||||
fileInfo.classList.add('d-none');
|
||||
payloadDropLabel.innerHTML = `<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i><span class="text-muted">Drop any file or click to browse</span><div class="small text-muted mt-1">Max {{ max_payload_kb }} KB</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// Show RSA password field when key is selected
|
||||
const rsaKeyInput = document.getElementById('rsaKeyInput');
|
||||
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
|
||||
@@ -192,12 +285,11 @@ document.getElementById('encodeForm').addEventListener('submit', function(e) {
|
||||
btn.disabled = true;
|
||||
});
|
||||
|
||||
// Character counter
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
// Character counter for text
|
||||
const charCount = document.getElementById('charCount');
|
||||
const charWarning = document.getElementById('charWarning');
|
||||
const charPercent = document.getElementById('charPercent');
|
||||
const maxChars = 50000;
|
||||
const maxChars = 250000;
|
||||
|
||||
messageInput.addEventListener('input', function() {
|
||||
const len = this.value.length;
|
||||
@@ -210,11 +302,12 @@ messageInput.addEventListener('input', function() {
|
||||
charCount.classList.toggle('text-danger', len > maxChars * 0.95);
|
||||
});
|
||||
|
||||
// Drag & drop with preview
|
||||
// Drag & drop with preview for images
|
||||
document.querySelectorAll('.drop-zone').forEach(zone => {
|
||||
const input = zone.querySelector('input[type="file"]');
|
||||
const label = zone.querySelector('.drop-zone-label');
|
||||
const preview = zone.querySelector('.drop-zone-preview');
|
||||
const isPayloadZone = zone.id === 'payloadDropZone';
|
||||
|
||||
['dragenter', 'dragover'].forEach(evt => {
|
||||
zone.addEventListener(evt, e => {
|
||||
@@ -233,29 +326,38 @@ document.querySelectorAll('.drop-zone').forEach(zone => {
|
||||
zone.addEventListener('drop', e => {
|
||||
if (e.dataTransfer.files.length) {
|
||||
input.files = e.dataTransfer.files;
|
||||
input.dispatchEvent(new Event('change'));
|
||||
|
||||
if (!isPayloadZone) {
|
||||
showPreview(e.dataTransfer.files[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!isPayloadZone) {
|
||||
input.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
showPreview(this.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showPreview(file) {
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
if (preview) {
|
||||
preview.src = e.target.result;
|
||||
preview.classList.remove('d-none');
|
||||
}
|
||||
label.innerHTML = '<i class="bi bi-check-circle text-success me-1"></i>' + file.name;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
// 1. PIN Toggle Logic
|
||||
|
||||
// PIN Toggle Logic
|
||||
document.getElementById('togglePin').addEventListener('click', function() {
|
||||
const input = document.getElementById('pinInput');
|
||||
const icon = this.querySelector('i');
|
||||
@@ -268,17 +370,16 @@ document.getElementById('togglePin').addEventListener('click', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Prevent Same File Selection
|
||||
// Prevent Same File Selection
|
||||
function checkDuplicateFiles() {
|
||||
const refInput = document.querySelector('input[name="reference_photo"]');
|
||||
const carInput = document.querySelector('input[name="carrier"]');
|
||||
|
||||
if (refInput.files[0] && carInput.files[0]) {
|
||||
// Compare name and size as a proxy for identical files
|
||||
if (refInput.files[0].name === carInput.files[0].name &&
|
||||
refInput.files[0].size === carInput.files[0].size) {
|
||||
alert("Security Warning: You cannot use the same image for both Reference and Carrier!");
|
||||
carInput.value = ''; // Clear carrier
|
||||
carInput.value = '';
|
||||
document.getElementById('carrierPreview').classList.add('d-none');
|
||||
document.querySelector('#carrierDropZone .drop-zone-label').innerHTML =
|
||||
'<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>' +
|
||||
@@ -289,14 +390,13 @@ function checkDuplicateFiles() {
|
||||
document.querySelector('input[name="reference_photo"]').addEventListener('change', checkDuplicateFiles);
|
||||
document.querySelector('input[name="carrier"]').addEventListener('change', checkDuplicateFiles);
|
||||
|
||||
// 3. Paste from Clipboard
|
||||
// Paste from Clipboard
|
||||
document.addEventListener('paste', function(e) {
|
||||
const items = e.clipboardData.items;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
const blob = items[i].getAsFile();
|
||||
|
||||
// Priority: Fill Carrier first, if empty. Else fill Reference.
|
||||
const carrierInput = document.querySelector('input[name="carrier"]');
|
||||
const refInput = document.querySelector('input[name="reference_photo"]');
|
||||
|
||||
@@ -307,7 +407,7 @@ document.addEventListener('paste', function(e) {
|
||||
targetInput.files = container.files;
|
||||
|
||||
targetInput.dispatchEvent(new Event('change'));
|
||||
break; // Only paste one image at a time
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
Stegasoo - Secure Steganography Library
|
||||
|
||||
A Python library for hiding encrypted messages in images using
|
||||
A Python library for hiding encrypted messages and files in images using
|
||||
hybrid photo + passphrase + PIN authentication.
|
||||
|
||||
Basic Usage:
|
||||
Basic Usage - Text Message:
|
||||
from stegasoo import encode, decode, generate_credentials
|
||||
|
||||
# Generate credentials
|
||||
@@ -30,16 +30,36 @@ Basic Usage:
|
||||
f.write(result.stego_image)
|
||||
|
||||
# Decode a message
|
||||
message = decode(
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=ref_photo,
|
||||
day_phrase="apple forest thunder",
|
||||
pin="123456"
|
||||
)
|
||||
print(message) # "Meet at midnight"
|
||||
print(decoded.message) # "Meet at midnight"
|
||||
|
||||
File Embedding:
|
||||
from stegasoo import encode_file, decode, FilePayload
|
||||
|
||||
# Encode a file
|
||||
result = encode_file(
|
||||
filepath="secret_document.pdf",
|
||||
reference_photo=ref_photo,
|
||||
carrier_image=carrier,
|
||||
day_phrase="apple forest thunder",
|
||||
pin="123456"
|
||||
)
|
||||
|
||||
# Decode - automatically detects file vs text
|
||||
decoded = decode(...)
|
||||
if decoded.is_file:
|
||||
with open(decoded.filename, 'wb') as f:
|
||||
f.write(decoded.file_data)
|
||||
else:
|
||||
print(decoded.message)
|
||||
"""
|
||||
|
||||
from .constants import __version__, DAY_NAMES
|
||||
from .constants import __version__, DAY_NAMES, MAX_MESSAGE_SIZE, MAX_FILE_PAYLOAD_SIZE
|
||||
from .models import (
|
||||
Credentials,
|
||||
EncodeInput,
|
||||
@@ -49,6 +69,7 @@ from .models import (
|
||||
EmbedStats,
|
||||
KeyInfo,
|
||||
ValidationResult,
|
||||
FilePayload,
|
||||
)
|
||||
from .exceptions import (
|
||||
StegasooError,
|
||||
@@ -83,6 +104,8 @@ from .keygen import (
|
||||
from .validation import (
|
||||
validate_pin,
|
||||
validate_message,
|
||||
validate_payload,
|
||||
validate_file_payload,
|
||||
validate_image,
|
||||
validate_rsa_key,
|
||||
validate_security_factors,
|
||||
@@ -90,6 +113,7 @@ from .validation import (
|
||||
validate_date_string,
|
||||
require_valid_pin,
|
||||
require_valid_message,
|
||||
require_valid_payload,
|
||||
require_valid_image,
|
||||
require_valid_rsa_key,
|
||||
require_security_factors,
|
||||
@@ -97,6 +121,7 @@ from .validation import (
|
||||
from .crypto import (
|
||||
encrypt_message,
|
||||
decrypt_message,
|
||||
decrypt_message_text,
|
||||
derive_hybrid_key,
|
||||
derive_pixel_key,
|
||||
hash_photo,
|
||||
@@ -109,6 +134,9 @@ from .steganography import (
|
||||
extract_from_image,
|
||||
calculate_capacity,
|
||||
get_image_dimensions,
|
||||
get_image_format,
|
||||
is_lossless_format,
|
||||
LOSSLESS_FORMATS,
|
||||
)
|
||||
from .utils import (
|
||||
generate_filename,
|
||||
@@ -122,11 +150,12 @@ from .utils import (
|
||||
)
|
||||
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
def encode(
|
||||
message: str,
|
||||
message: Union[str, bytes, FilePayload],
|
||||
reference_photo: bytes,
|
||||
carrier_image: bytes,
|
||||
day_phrase: str,
|
||||
@@ -134,15 +163,16 @@ def encode(
|
||||
rsa_key_data: Optional[bytes] = None,
|
||||
rsa_password: Optional[str] = None,
|
||||
date_str: Optional[str] = None,
|
||||
output_format: Optional[str] = None,
|
||||
) -> EncodeResult:
|
||||
"""
|
||||
Encode a secret message into an image.
|
||||
Encode a secret message or file into an image.
|
||||
|
||||
High-level convenience function that handles validation,
|
||||
encryption, and embedding in one call.
|
||||
|
||||
Args:
|
||||
message: Secret message to hide
|
||||
message: Secret message (str), raw bytes, or FilePayload to hide
|
||||
reference_photo: Shared reference photo bytes
|
||||
carrier_image: Image to hide message in
|
||||
day_phrase: Today's passphrase
|
||||
@@ -150,6 +180,8 @@ def encode(
|
||||
rsa_key_data: RSA private key PEM bytes (optional if using PIN)
|
||||
rsa_password: Password for RSA key if encrypted
|
||||
date_str: Date string YYYY-MM-DD (defaults to today)
|
||||
output_format: Force output format ('PNG', 'BMP'). If None, preserves
|
||||
carrier format for lossless types, defaults to PNG for lossy.
|
||||
|
||||
Returns:
|
||||
EncodeResult with stego image and metadata
|
||||
@@ -159,9 +191,13 @@ def encode(
|
||||
SecurityFactorError: If no PIN or RSA key provided
|
||||
CapacityError: If carrier is too small
|
||||
EncryptionError: If encryption fails
|
||||
|
||||
Note:
|
||||
Output format is always lossless (PNG or BMP) to preserve hidden data.
|
||||
If carrier is JPEG/GIF, output will be PNG to maintain data integrity.
|
||||
"""
|
||||
# Validate inputs
|
||||
require_valid_message(message)
|
||||
require_valid_payload(message)
|
||||
require_valid_image(carrier_image, "Carrier image")
|
||||
require_security_factors(pin, rsa_key_data)
|
||||
|
||||
@@ -174,7 +210,7 @@ def encode(
|
||||
if date_str is None:
|
||||
date_str = date.today().isoformat()
|
||||
|
||||
# Encrypt message
|
||||
# Encrypt message/file
|
||||
encrypted = encrypt_message(
|
||||
message, reference_photo, day_phrase, date_str, pin, rsa_key_data
|
||||
)
|
||||
@@ -184,11 +220,13 @@ def encode(
|
||||
reference_photo, day_phrase, date_str, pin, rsa_key_data
|
||||
)
|
||||
|
||||
# Embed in image
|
||||
stego_data, stats = embed_in_image(carrier_image, encrypted, pixel_key)
|
||||
# Embed in image (returns extension too)
|
||||
stego_data, stats, extension = embed_in_image(
|
||||
carrier_image, encrypted, pixel_key, output_format=output_format
|
||||
)
|
||||
|
||||
# Generate filename
|
||||
filename = generate_filename(date_str)
|
||||
# Generate filename with correct extension
|
||||
filename = generate_filename(date_str, extension=extension)
|
||||
|
||||
return EncodeResult(
|
||||
stego_image=stego_data,
|
||||
@@ -200,6 +238,102 @@ def encode(
|
||||
)
|
||||
|
||||
|
||||
def encode_file(
|
||||
filepath: Union[str, Path],
|
||||
reference_photo: bytes,
|
||||
carrier_image: bytes,
|
||||
day_phrase: str,
|
||||
pin: str = "",
|
||||
rsa_key_data: Optional[bytes] = None,
|
||||
rsa_password: Optional[str] = None,
|
||||
date_str: Optional[str] = None,
|
||||
output_format: Optional[str] = None,
|
||||
filename_override: Optional[str] = None,
|
||||
) -> EncodeResult:
|
||||
"""
|
||||
Encode a file into an image.
|
||||
|
||||
Convenience function for embedding files. Preserves original filename.
|
||||
|
||||
Args:
|
||||
filepath: Path to file to embed
|
||||
reference_photo: Shared reference photo bytes
|
||||
carrier_image: Image to hide file in
|
||||
day_phrase: Today's passphrase
|
||||
pin: Static PIN (optional if using RSA key)
|
||||
rsa_key_data: RSA private key PEM bytes (optional if using PIN)
|
||||
rsa_password: Password for RSA key if encrypted
|
||||
date_str: Date string YYYY-MM-DD (defaults to today)
|
||||
output_format: Force output format ('PNG', 'BMP')
|
||||
filename_override: Override the stored filename
|
||||
|
||||
Returns:
|
||||
EncodeResult with stego image and metadata
|
||||
"""
|
||||
payload = FilePayload.from_file(str(filepath), filename_override)
|
||||
|
||||
return encode(
|
||||
message=payload,
|
||||
reference_photo=reference_photo,
|
||||
carrier_image=carrier_image,
|
||||
day_phrase=day_phrase,
|
||||
pin=pin,
|
||||
rsa_key_data=rsa_key_data,
|
||||
rsa_password=rsa_password,
|
||||
date_str=date_str,
|
||||
output_format=output_format,
|
||||
)
|
||||
|
||||
|
||||
def encode_bytes(
|
||||
data: bytes,
|
||||
filename: str,
|
||||
reference_photo: bytes,
|
||||
carrier_image: bytes,
|
||||
day_phrase: str,
|
||||
pin: str = "",
|
||||
rsa_key_data: Optional[bytes] = None,
|
||||
rsa_password: Optional[str] = None,
|
||||
date_str: Optional[str] = None,
|
||||
output_format: Optional[str] = None,
|
||||
mime_type: Optional[str] = None,
|
||||
) -> EncodeResult:
|
||||
"""
|
||||
Encode raw bytes with a filename into an image.
|
||||
|
||||
Convenience function for embedding binary data with metadata.
|
||||
|
||||
Args:
|
||||
data: Raw bytes to embed
|
||||
filename: Filename to associate with the data
|
||||
reference_photo: Shared reference photo bytes
|
||||
carrier_image: Image to hide data in
|
||||
day_phrase: Today's passphrase
|
||||
pin: Static PIN (optional if using RSA key)
|
||||
rsa_key_data: RSA private key PEM bytes (optional if using PIN)
|
||||
rsa_password: Password for RSA key if encrypted
|
||||
date_str: Date string YYYY-MM-DD (defaults to today)
|
||||
output_format: Force output format ('PNG', 'BMP')
|
||||
mime_type: MIME type of the data
|
||||
|
||||
Returns:
|
||||
EncodeResult with stego image and metadata
|
||||
"""
|
||||
payload = FilePayload(data=data, filename=filename, mime_type=mime_type)
|
||||
|
||||
return encode(
|
||||
message=payload,
|
||||
reference_photo=reference_photo,
|
||||
carrier_image=carrier_image,
|
||||
day_phrase=day_phrase,
|
||||
pin=pin,
|
||||
rsa_key_data=rsa_key_data,
|
||||
rsa_password=rsa_password,
|
||||
date_str=date_str,
|
||||
output_format=output_format,
|
||||
)
|
||||
|
||||
|
||||
def decode(
|
||||
stego_image: bytes,
|
||||
reference_photo: bytes,
|
||||
@@ -207,15 +341,15 @@ def decode(
|
||||
pin: str = "",
|
||||
rsa_key_data: Optional[bytes] = None,
|
||||
rsa_password: Optional[str] = None,
|
||||
) -> str:
|
||||
) -> DecodeResult:
|
||||
"""
|
||||
Decode a secret message from a stego image.
|
||||
Decode a secret message or file from a stego image.
|
||||
|
||||
High-level convenience function that handles extraction
|
||||
and decryption in one call.
|
||||
|
||||
Args:
|
||||
stego_image: Image containing hidden message
|
||||
stego_image: Image containing hidden message/file
|
||||
reference_photo: Shared reference photo bytes
|
||||
day_phrase: Passphrase for the day message was encoded
|
||||
pin: Static PIN (if used during encoding)
|
||||
@@ -223,7 +357,12 @@ def decode(
|
||||
rsa_password: Password for RSA key if encrypted
|
||||
|
||||
Returns:
|
||||
Decrypted message string
|
||||
DecodeResult with:
|
||||
- .payload_type: 'text' or 'file'
|
||||
- .message: Decoded text (if text)
|
||||
- .file_data: Decoded bytes (if file)
|
||||
- .filename: Original filename (if file)
|
||||
- .is_text / .is_file: Convenience properties
|
||||
|
||||
Raises:
|
||||
ValidationError: If inputs are invalid
|
||||
@@ -260,21 +399,72 @@ def decode(
|
||||
if not encrypted:
|
||||
raise ExtractionError("Could not extract data. Check your inputs.")
|
||||
|
||||
# Decrypt
|
||||
# Decrypt and return full result
|
||||
return decrypt_message(encrypted, reference_photo, day_phrase, pin, rsa_key_data)
|
||||
|
||||
|
||||
def decode_text(
|
||||
stego_image: bytes,
|
||||
reference_photo: bytes,
|
||||
day_phrase: str,
|
||||
pin: str = "",
|
||||
rsa_key_data: Optional[bytes] = None,
|
||||
rsa_password: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Decode a text message from a stego image.
|
||||
|
||||
Convenience function that returns just the text string.
|
||||
Raises an error if the content is a binary file.
|
||||
|
||||
Args:
|
||||
stego_image: Image containing hidden message
|
||||
reference_photo: Shared reference photo bytes
|
||||
day_phrase: Passphrase for the day message was encoded
|
||||
pin: Static PIN (if used during encoding)
|
||||
rsa_key_data: RSA private key PEM bytes (if used during encoding)
|
||||
rsa_password: Password for RSA key if encrypted
|
||||
|
||||
Returns:
|
||||
Decrypted message string
|
||||
|
||||
Raises:
|
||||
DecryptionError: If content is a binary file, not text
|
||||
"""
|
||||
result = decode(stego_image, reference_photo, day_phrase, pin, rsa_key_data, rsa_password)
|
||||
|
||||
if result.is_file:
|
||||
# Try to decode file as text
|
||||
if result.file_data:
|
||||
try:
|
||||
return result.file_data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
raise DecryptionError(
|
||||
f"Content is a binary file ({result.filename or 'unnamed'}), not text. "
|
||||
"Use decode() instead and check result.is_file."
|
||||
)
|
||||
return ""
|
||||
|
||||
return result.message or ""
|
||||
|
||||
|
||||
__all__ = [
|
||||
# Version
|
||||
'__version__',
|
||||
|
||||
# High-level API
|
||||
'encode',
|
||||
'encode_file',
|
||||
'encode_bytes',
|
||||
'decode',
|
||||
'decode_text',
|
||||
'generate_credentials',
|
||||
|
||||
# Constants
|
||||
'DAY_NAMES',
|
||||
'LOSSLESS_FORMATS',
|
||||
'MAX_MESSAGE_SIZE',
|
||||
'MAX_FILE_PAYLOAD_SIZE',
|
||||
|
||||
# Models
|
||||
'Credentials',
|
||||
@@ -285,6 +475,7 @@ __all__ = [
|
||||
'EmbedStats',
|
||||
'KeyInfo',
|
||||
'ValidationResult',
|
||||
'FilePayload',
|
||||
|
||||
# Exceptions
|
||||
'StegasooError',
|
||||
@@ -318,6 +509,8 @@ __all__ = [
|
||||
# Validation
|
||||
'validate_pin',
|
||||
'validate_message',
|
||||
'validate_payload',
|
||||
'validate_file_payload',
|
||||
'validate_image',
|
||||
'validate_rsa_key',
|
||||
'validate_security_factors',
|
||||
@@ -325,6 +518,7 @@ __all__ = [
|
||||
'validate_date_string',
|
||||
'require_valid_pin',
|
||||
'require_valid_message',
|
||||
'require_valid_payload',
|
||||
'require_valid_image',
|
||||
'require_valid_rsa_key',
|
||||
'require_security_factors',
|
||||
@@ -332,6 +526,7 @@ __all__ = [
|
||||
# Crypto
|
||||
'encrypt_message',
|
||||
'decrypt_message',
|
||||
'decrypt_message_text',
|
||||
'derive_hybrid_key',
|
||||
'derive_pixel_key',
|
||||
'hash_photo',
|
||||
@@ -344,6 +539,8 @@ __all__ = [
|
||||
'extract_from_image',
|
||||
'calculate_capacity',
|
||||
'get_image_dimensions',
|
||||
'get_image_format',
|
||||
'is_lossless_format',
|
||||
|
||||
# Utilities
|
||||
'generate_filename',
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
# VERSION
|
||||
# ============================================================================
|
||||
|
||||
__version__ = "2.0.1"
|
||||
__version__ = "2.1.1"
|
||||
|
||||
# ============================================================================
|
||||
# FILE FORMAT
|
||||
@@ -20,6 +20,10 @@ __version__ = "2.0.1"
|
||||
MAGIC_HEADER = b'\x89ST3'
|
||||
FORMAT_VERSION = 3
|
||||
|
||||
# Payload type markers
|
||||
PAYLOAD_TEXT = 0x01
|
||||
PAYLOAD_FILE = 0x02
|
||||
|
||||
# ============================================================================
|
||||
# CRYPTO PARAMETERS
|
||||
# ============================================================================
|
||||
@@ -40,9 +44,11 @@ PBKDF2_ITERATIONS = 600000
|
||||
# INPUT LIMITS
|
||||
# ============================================================================
|
||||
|
||||
MAX_IMAGE_PIXELS = 4_000_000 # ~4 megapixels (2000x2000)
|
||||
MAX_MESSAGE_SIZE = 50_000 # 50 KB
|
||||
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
|
||||
MAX_IMAGE_PIXELS = 16_000_000 # ~16 megapixels (4000x4000)
|
||||
MAX_MESSAGE_SIZE = 250_000 # 250 KB (text messages)
|
||||
MAX_FILE_PAYLOAD_SIZE = 250_000 # 250 KB (file payloads)
|
||||
MAX_FILENAME_LENGTH = 255 # Max filename length to store
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB (upload limit)
|
||||
|
||||
MIN_PIN_LENGTH = 6
|
||||
MAX_PIN_LENGTH = 9
|
||||
@@ -78,11 +84,17 @@ DAY_NAMES = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
|
||||
def get_data_dir() -> Path:
|
||||
"""Get the data directory path."""
|
||||
# Check multiple locations
|
||||
# From src/stegasoo/constants.py:
|
||||
# .parent = src/stegasoo/
|
||||
# .parent.parent = src/
|
||||
# .parent.parent.parent = project root (where data/ lives)
|
||||
candidates = [
|
||||
Path(__file__).parent.parent.parent/ 'data', # Development
|
||||
Path(__file__).parent.parent.parent / 'data', # Development: src/stegasoo -> project root
|
||||
Path(__file__).parent / 'data', # Installed package
|
||||
Path('/app/data'), # Docker
|
||||
Path.cwd() / 'data', # Current directory
|
||||
Path.cwd().parent / 'data', # One level up from cwd
|
||||
Path.cwd().parent.parent / 'data', # Two levels up from cwd
|
||||
]
|
||||
|
||||
for path in candidates:
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
Stegasoo Cryptographic Functions
|
||||
|
||||
Key derivation, encryption, and decryption using AES-256-GCM.
|
||||
Supports both text messages and binary file payloads.
|
||||
"""
|
||||
|
||||
import io
|
||||
import hashlib
|
||||
import secrets
|
||||
import struct
|
||||
from typing import Optional
|
||||
import json
|
||||
from typing import Optional, Union
|
||||
|
||||
from PIL import Image
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
@@ -19,7 +21,10 @@ from .constants import (
|
||||
SALT_SIZE, IV_SIZE, TAG_SIZE,
|
||||
ARGON2_TIME_COST, ARGON2_MEMORY_COST, ARGON2_PARALLELISM,
|
||||
PBKDF2_ITERATIONS,
|
||||
PAYLOAD_TEXT, PAYLOAD_FILE,
|
||||
MAX_FILENAME_LENGTH,
|
||||
)
|
||||
from .models import FilePayload, DecodeResult
|
||||
from .exceptions import (
|
||||
EncryptionError, DecryptionError, KeyDerivationError, InvalidHeaderError
|
||||
)
|
||||
@@ -171,8 +176,112 @@ def derive_pixel_key(
|
||||
return hashlib.sha256(material + b"pixel_selection").digest()
|
||||
|
||||
|
||||
def _pack_payload(
|
||||
content: Union[str, bytes, FilePayload],
|
||||
) -> tuple[bytes, int]:
|
||||
"""
|
||||
Pack payload with type marker and metadata.
|
||||
|
||||
Format for text:
|
||||
[type:1][data]
|
||||
|
||||
Format for file:
|
||||
[type:1][filename_len:2][filename][mime_len:2][mime][data]
|
||||
|
||||
Args:
|
||||
content: Text string, raw bytes, or FilePayload
|
||||
|
||||
Returns:
|
||||
Tuple of (packed bytes, payload type)
|
||||
"""
|
||||
if isinstance(content, str):
|
||||
# Text message
|
||||
data = content.encode('utf-8')
|
||||
return bytes([PAYLOAD_TEXT]) + data, PAYLOAD_TEXT
|
||||
|
||||
elif isinstance(content, FilePayload):
|
||||
# File with metadata
|
||||
filename = content.filename[:MAX_FILENAME_LENGTH].encode('utf-8')
|
||||
mime = (content.mime_type or '')[:100].encode('utf-8')
|
||||
|
||||
packed = (
|
||||
bytes([PAYLOAD_FILE]) +
|
||||
struct.pack('>H', len(filename)) +
|
||||
filename +
|
||||
struct.pack('>H', len(mime)) +
|
||||
mime +
|
||||
content.data
|
||||
)
|
||||
return packed, PAYLOAD_FILE
|
||||
|
||||
else:
|
||||
# Raw bytes - treat as file with no name
|
||||
packed = (
|
||||
bytes([PAYLOAD_FILE]) +
|
||||
struct.pack('>H', 0) + # No filename
|
||||
struct.pack('>H', 0) + # No mime
|
||||
content
|
||||
)
|
||||
return packed, PAYLOAD_FILE
|
||||
|
||||
|
||||
def _unpack_payload(data: bytes) -> DecodeResult:
|
||||
"""
|
||||
Unpack payload and extract content with metadata.
|
||||
|
||||
Args:
|
||||
data: Packed payload bytes
|
||||
|
||||
Returns:
|
||||
DecodeResult with appropriate content
|
||||
"""
|
||||
if len(data) < 1:
|
||||
raise DecryptionError("Empty payload")
|
||||
|
||||
payload_type = data[0]
|
||||
|
||||
if payload_type == PAYLOAD_TEXT:
|
||||
# Text message
|
||||
text = data[1:].decode('utf-8')
|
||||
return DecodeResult(payload_type='text', message=text)
|
||||
|
||||
elif payload_type == PAYLOAD_FILE:
|
||||
# File with metadata
|
||||
offset = 1
|
||||
|
||||
# Read filename
|
||||
filename_len = struct.unpack('>H', data[offset:offset+2])[0]
|
||||
offset += 2
|
||||
filename = data[offset:offset+filename_len].decode('utf-8') if filename_len else None
|
||||
offset += filename_len
|
||||
|
||||
# Read mime type
|
||||
mime_len = struct.unpack('>H', data[offset:offset+2])[0]
|
||||
offset += 2
|
||||
mime_type = data[offset:offset+mime_len].decode('utf-8') if mime_len else None
|
||||
offset += mime_len
|
||||
|
||||
# Rest is file data
|
||||
file_data = data[offset:]
|
||||
|
||||
return DecodeResult(
|
||||
payload_type='file',
|
||||
file_data=file_data,
|
||||
filename=filename,
|
||||
mime_type=mime_type
|
||||
)
|
||||
|
||||
else:
|
||||
# Unknown type - try to decode as text (backward compatibility)
|
||||
try:
|
||||
text = data.decode('utf-8')
|
||||
return DecodeResult(payload_type='text', message=text)
|
||||
except UnicodeDecodeError:
|
||||
return DecodeResult(payload_type='file', file_data=data)
|
||||
|
||||
|
||||
def encrypt_message(
|
||||
message: str | bytes,
|
||||
message: Union[str, bytes, FilePayload],
|
||||
photo_data: bytes,
|
||||
day_phrase: str,
|
||||
date_str: str,
|
||||
@@ -180,7 +289,7 @@ def encrypt_message(
|
||||
rsa_key_data: Optional[bytes] = None
|
||||
) -> bytes:
|
||||
"""
|
||||
Encrypt message using AES-256-GCM with hybrid key derivation.
|
||||
Encrypt message or file using AES-256-GCM with hybrid key derivation.
|
||||
|
||||
Message format:
|
||||
- Magic header (4 bytes)
|
||||
@@ -193,7 +302,7 @@ def encrypt_message(
|
||||
- Ciphertext (variable, padded)
|
||||
|
||||
Args:
|
||||
message: Message to encrypt
|
||||
message: Message string, raw bytes, or FilePayload to encrypt
|
||||
photo_data: Reference photo bytes
|
||||
day_phrase: The day's phrase
|
||||
date_str: Date string (YYYY-MM-DD)
|
||||
@@ -211,15 +320,15 @@ def encrypt_message(
|
||||
key = derive_hybrid_key(photo_data, day_phrase, date_str, salt, pin, rsa_key_data)
|
||||
iv = secrets.token_bytes(IV_SIZE)
|
||||
|
||||
if isinstance(message, str):
|
||||
message = message.encode('utf-8')
|
||||
# Pack payload with type marker
|
||||
packed_payload, _ = _pack_payload(message)
|
||||
|
||||
# Random padding to hide message length
|
||||
padding_len = secrets.randbelow(256) + 64
|
||||
padded_len = ((len(message) + padding_len + 255) // 256) * 256
|
||||
padding_needed = padded_len - len(message)
|
||||
padding = secrets.token_bytes(padding_needed - 4) + struct.pack('>I', len(message))
|
||||
padded_message = message + padding
|
||||
padded_len = ((len(packed_payload) + padding_len + 255) // 256) * 256
|
||||
padding_needed = padded_len - len(packed_payload)
|
||||
padding = secrets.token_bytes(padding_needed - 4) + struct.pack('>I', len(packed_payload))
|
||||
padded_message = packed_payload + padding
|
||||
|
||||
# Encrypt with AES-256-GCM
|
||||
cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
|
||||
@@ -291,7 +400,7 @@ def decrypt_message(
|
||||
day_phrase: str,
|
||||
pin: str = "",
|
||||
rsa_key_data: Optional[bytes] = None
|
||||
) -> str:
|
||||
) -> DecodeResult:
|
||||
"""
|
||||
Decrypt message using the embedded date from the header.
|
||||
|
||||
@@ -303,7 +412,7 @@ def decrypt_message(
|
||||
rsa_key_data: Optional RSA key bytes
|
||||
|
||||
Returns:
|
||||
Decrypted message string
|
||||
DecodeResult with decrypted content
|
||||
|
||||
Raises:
|
||||
InvalidHeaderError: If data doesn't have valid Stegasoo header
|
||||
@@ -329,7 +438,11 @@ def decrypt_message(
|
||||
padded_plaintext = decryptor.update(header['ciphertext']) + decryptor.finalize()
|
||||
original_length = struct.unpack('>I', padded_plaintext[-4:])[0]
|
||||
|
||||
return padded_plaintext[:original_length].decode('utf-8')
|
||||
payload_data = padded_plaintext[:original_length]
|
||||
result = _unpack_payload(payload_data)
|
||||
result.date_encoded = header['date']
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise DecryptionError(
|
||||
@@ -337,6 +450,47 @@ def decrypt_message(
|
||||
) from e
|
||||
|
||||
|
||||
def decrypt_message_text(
|
||||
encrypted_data: bytes,
|
||||
photo_data: bytes,
|
||||
day_phrase: str,
|
||||
pin: str = "",
|
||||
rsa_key_data: Optional[bytes] = None
|
||||
) -> str:
|
||||
"""
|
||||
Decrypt message and return as text string.
|
||||
|
||||
For backward compatibility - returns text content or raises error for files.
|
||||
|
||||
Args:
|
||||
encrypted_data: Encrypted message bytes
|
||||
photo_data: Reference photo bytes
|
||||
day_phrase: The day's phrase
|
||||
pin: Optional static PIN
|
||||
rsa_key_data: Optional RSA key bytes
|
||||
|
||||
Returns:
|
||||
Decrypted message string
|
||||
|
||||
Raises:
|
||||
DecryptionError: If decryption fails or content is a file
|
||||
"""
|
||||
result = decrypt_message(encrypted_data, photo_data, day_phrase, pin, rsa_key_data)
|
||||
|
||||
if result.is_file:
|
||||
if result.file_data:
|
||||
# Try to decode as text
|
||||
try:
|
||||
return result.file_data.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
raise DecryptionError(
|
||||
f"Content is a binary file ({result.filename or 'unnamed'}), not text"
|
||||
)
|
||||
return ""
|
||||
|
||||
return result.message or ""
|
||||
|
||||
|
||||
def get_date_from_encrypted(encrypted_data: bytes) -> Optional[str]:
|
||||
"""
|
||||
Extract the date string from encrypted data without decrypting.
|
||||
|
||||
@@ -6,7 +6,7 @@ Dataclasses for structured data exchange between modules and frontends.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -43,10 +43,35 @@ class Credentials:
|
||||
return self.phrase_entropy + self.pin_entropy + self.rsa_entropy
|
||||
|
||||
|
||||
@dataclass
|
||||
class FilePayload:
|
||||
"""Represents a file to be embedded."""
|
||||
data: bytes
|
||||
filename: str
|
||||
mime_type: Optional[str] = None
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return len(self.data)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filepath: str, filename: Optional[str] = None) -> 'FilePayload':
|
||||
"""Create FilePayload from a file path."""
|
||||
from pathlib import Path
|
||||
import mimetypes
|
||||
|
||||
path = Path(filepath)
|
||||
data = path.read_bytes()
|
||||
name = filename or path.name
|
||||
mime, _ = mimetypes.guess_type(name)
|
||||
|
||||
return cls(data=data, filename=name, mime_type=mime)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EncodeInput:
|
||||
"""Input parameters for encoding a message."""
|
||||
message: str
|
||||
message: Union[str, bytes, FilePayload] # Text, raw bytes, or file
|
||||
reference_photo: bytes
|
||||
carrier_image: bytes
|
||||
day_phrase: str
|
||||
@@ -90,8 +115,26 @@ class DecodeInput:
|
||||
@dataclass
|
||||
class DecodeResult:
|
||||
"""Result of decoding operation."""
|
||||
message: str
|
||||
date_encoded: str
|
||||
payload_type: str # 'text' or 'file'
|
||||
message: Optional[str] = None # For text payloads
|
||||
file_data: Optional[bytes] = None # For file payloads
|
||||
filename: Optional[str] = None # Original filename for file payloads
|
||||
mime_type: Optional[str] = None # MIME type hint
|
||||
date_encoded: Optional[str] = None
|
||||
|
||||
@property
|
||||
def is_file(self) -> bool:
|
||||
return self.payload_type == 'file'
|
||||
|
||||
@property
|
||||
def is_text(self) -> bool:
|
||||
return self.payload_type == 'text'
|
||||
|
||||
def get_content(self) -> Union[str, bytes]:
|
||||
"""Get the decoded content (text or bytes)."""
|
||||
if self.is_text:
|
||||
return self.message or ""
|
||||
return self.file_data or b""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -16,6 +16,43 @@ from .models import EmbedStats
|
||||
from .exceptions import CapacityError, ExtractionError, EmbeddingError
|
||||
|
||||
|
||||
# Lossless formats that preserve LSB data
|
||||
LOSSLESS_FORMATS = {'PNG', 'BMP', 'TIFF'}
|
||||
|
||||
# Format to extension mapping
|
||||
FORMAT_TO_EXT = {
|
||||
'PNG': 'png',
|
||||
'BMP': 'bmp',
|
||||
'TIFF': 'tiff',
|
||||
}
|
||||
|
||||
# Extension to PIL format mapping
|
||||
EXT_TO_FORMAT = {
|
||||
'png': 'PNG',
|
||||
'bmp': 'BMP',
|
||||
'tiff': 'TIFF',
|
||||
'tif': 'TIFF',
|
||||
}
|
||||
|
||||
|
||||
def get_output_format(input_format: Optional[str]) -> tuple[str, str]:
|
||||
"""
|
||||
Determine the output format based on input format.
|
||||
|
||||
Args:
|
||||
input_format: PIL format string of input image (e.g., 'JPEG', 'PNG')
|
||||
|
||||
Returns:
|
||||
Tuple of (PIL format string, file extension) for output
|
||||
Falls back to PNG for lossy or unknown formats.
|
||||
"""
|
||||
if input_format and input_format.upper() in LOSSLESS_FORMATS:
|
||||
fmt = input_format.upper()
|
||||
return fmt, FORMAT_TO_EXT.get(fmt, 'png')
|
||||
# Default to PNG for lossy formats (JPEG, GIF) or unknown
|
||||
return 'PNG', 'png'
|
||||
|
||||
|
||||
def generate_pixel_indices(key: bytes, num_pixels: int, num_needed: int) -> list[int]:
|
||||
"""
|
||||
Generate pseudo-random pixel indices for embedding.
|
||||
@@ -83,8 +120,9 @@ def embed_in_image(
|
||||
carrier_data: bytes,
|
||||
encrypted_data: bytes,
|
||||
pixel_key: bytes,
|
||||
bits_per_channel: int = 1
|
||||
) -> tuple[bytes, EmbedStats]:
|
||||
bits_per_channel: int = 1,
|
||||
output_format: Optional[str] = None
|
||||
) -> tuple[bytes, EmbedStats, str]:
|
||||
"""
|
||||
Embed encrypted data in carrier image using LSB steganography.
|
||||
|
||||
@@ -96,9 +134,11 @@ def embed_in_image(
|
||||
encrypted_data: Data to embed
|
||||
pixel_key: Key for pixel selection
|
||||
bits_per_channel: Bits to use per color channel (1-2)
|
||||
output_format: Force specific output format (PNG, BMP).
|
||||
If None, auto-detect from carrier (lossless) or default to PNG.
|
||||
|
||||
Returns:
|
||||
Tuple of (PNG image bytes, EmbedStats)
|
||||
Tuple of (image bytes, EmbedStats, file extension)
|
||||
|
||||
Raises:
|
||||
CapacityError: If carrier is too small
|
||||
@@ -106,6 +146,8 @@ def embed_in_image(
|
||||
"""
|
||||
try:
|
||||
img = Image.open(io.BytesIO(carrier_data))
|
||||
input_format = img.format
|
||||
|
||||
if img.mode != 'RGB':
|
||||
img = img.convert('RGB')
|
||||
|
||||
@@ -160,8 +202,15 @@ def embed_in_image(
|
||||
stego_img = Image.new('RGB', img.size)
|
||||
stego_img.putdata(new_pixels)
|
||||
|
||||
# Determine output format
|
||||
if output_format:
|
||||
out_fmt = output_format.upper()
|
||||
out_ext = FORMAT_TO_EXT.get(out_fmt, 'png')
|
||||
else:
|
||||
out_fmt, out_ext = get_output_format(input_format)
|
||||
|
||||
output = io.BytesIO()
|
||||
stego_img.save(output, 'PNG')
|
||||
stego_img.save(output, out_fmt)
|
||||
output.seek(0)
|
||||
|
||||
stats = EmbedStats(
|
||||
@@ -171,7 +220,7 @@ def embed_in_image(
|
||||
bytes_embedded=len(data_with_len)
|
||||
)
|
||||
|
||||
return output.getvalue(), stats
|
||||
return output.getvalue(), stats, out_ext
|
||||
|
||||
except CapacityError:
|
||||
raise
|
||||
@@ -284,3 +333,18 @@ def get_image_dimensions(image_data: bytes) -> tuple[int, int]:
|
||||
"""Get image dimensions without loading full image."""
|
||||
img = Image.open(io.BytesIO(image_data))
|
||||
return img.size
|
||||
|
||||
|
||||
def get_image_format(image_data: bytes) -> Optional[str]:
|
||||
"""Get image format (PIL format string like 'PNG', 'JPEG')."""
|
||||
try:
|
||||
img = Image.open(io.BytesIO(image_data))
|
||||
return img.format
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def is_lossless_format(image_data: bytes) -> bool:
|
||||
"""Check if image is in a lossless format suitable for steganography."""
|
||||
fmt = get_image_format(image_data)
|
||||
return fmt is not None and fmt.upper() in LOSSLESS_FORMATS
|
||||
|
||||
@@ -15,15 +15,20 @@ from typing import Optional
|
||||
from .constants import DAY_NAMES
|
||||
|
||||
|
||||
def generate_filename(date_str: Optional[str] = None, prefix: str = "") -> str:
|
||||
def generate_filename(
|
||||
date_str: Optional[str] = None,
|
||||
prefix: str = "",
|
||||
extension: str = "png"
|
||||
) -> str:
|
||||
"""
|
||||
Generate a filename for stego images.
|
||||
|
||||
Format: {prefix}{random}_{YYYYMMDD}.png
|
||||
Format: {prefix}{random}_{YYYYMMDD}.{extension}
|
||||
|
||||
Args:
|
||||
date_str: Date string (YYYY-MM-DD), defaults to today
|
||||
prefix: Optional prefix
|
||||
extension: File extension without dot (default: 'png')
|
||||
|
||||
Returns:
|
||||
Filename string
|
||||
@@ -34,7 +39,10 @@ def generate_filename(date_str: Optional[str] = None, prefix: str = "") -> str:
|
||||
date_compact = date_str.replace('-', '')
|
||||
random_hex = secrets.token_hex(4)
|
||||
|
||||
return f"{prefix}{random_hex}_{date_compact}.png"
|
||||
# Ensure extension doesn't have a leading dot
|
||||
extension = extension.lstrip('.')
|
||||
|
||||
return f"{prefix}{random_hex}_{date_compact}.{extension}"
|
||||
|
||||
|
||||
def parse_date_from_filename(filename: str) -> Optional[str]:
|
||||
|
||||
@@ -5,17 +5,17 @@ Validators for all user inputs with clear error messages.
|
||||
"""
|
||||
|
||||
import io
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .constants import (
|
||||
MIN_PIN_LENGTH, MAX_PIN_LENGTH,
|
||||
MAX_MESSAGE_SIZE, MAX_IMAGE_PIXELS, MAX_FILE_SIZE,
|
||||
MAX_MESSAGE_SIZE, MAX_FILE_PAYLOAD_SIZE, MAX_IMAGE_PIXELS, MAX_FILE_SIZE,
|
||||
MIN_RSA_BITS, MIN_KEY_PASSWORD_LENGTH,
|
||||
ALLOWED_IMAGE_EXTENSIONS, ALLOWED_KEY_EXTENSIONS,
|
||||
)
|
||||
from .models import ValidationResult
|
||||
from .models import ValidationResult, FilePayload
|
||||
from .exceptions import (
|
||||
ValidationError, PinValidationError, MessageValidationError,
|
||||
ImageValidationError, KeyValidationError, SecurityFactorError,
|
||||
@@ -61,7 +61,7 @@ def validate_pin(pin: str, required: bool = False) -> ValidationResult:
|
||||
|
||||
def validate_message(message: str) -> ValidationResult:
|
||||
"""
|
||||
Validate message content and size.
|
||||
Validate text message content and size.
|
||||
|
||||
Args:
|
||||
message: Message text
|
||||
@@ -80,6 +80,81 @@ def validate_message(message: str) -> ValidationResult:
|
||||
return ValidationResult.ok(length=len(message))
|
||||
|
||||
|
||||
def validate_payload(payload: Union[str, bytes, FilePayload]) -> ValidationResult:
|
||||
"""
|
||||
Validate a payload (text message, bytes, or file).
|
||||
|
||||
Args:
|
||||
payload: Text string, raw bytes, or FilePayload
|
||||
|
||||
Returns:
|
||||
ValidationResult
|
||||
"""
|
||||
if isinstance(payload, str):
|
||||
return validate_message(payload)
|
||||
|
||||
elif isinstance(payload, FilePayload):
|
||||
if not payload.data:
|
||||
return ValidationResult.error("File is empty")
|
||||
|
||||
if len(payload.data) > MAX_FILE_PAYLOAD_SIZE:
|
||||
return ValidationResult.error(
|
||||
f"File too large ({len(payload.data):,} bytes). "
|
||||
f"Maximum: {MAX_FILE_PAYLOAD_SIZE:,} bytes ({MAX_FILE_PAYLOAD_SIZE // 1024} KB)"
|
||||
)
|
||||
|
||||
return ValidationResult.ok(
|
||||
size=len(payload.data),
|
||||
filename=payload.filename,
|
||||
mime_type=payload.mime_type
|
||||
)
|
||||
|
||||
elif isinstance(payload, bytes):
|
||||
if not payload:
|
||||
return ValidationResult.error("Payload is empty")
|
||||
|
||||
if len(payload) > MAX_FILE_PAYLOAD_SIZE:
|
||||
return ValidationResult.error(
|
||||
f"Payload too large ({len(payload):,} bytes). "
|
||||
f"Maximum: {MAX_FILE_PAYLOAD_SIZE:,} bytes ({MAX_FILE_PAYLOAD_SIZE // 1024} KB)"
|
||||
)
|
||||
|
||||
return ValidationResult.ok(size=len(payload))
|
||||
|
||||
else:
|
||||
return ValidationResult.error(f"Invalid payload type: {type(payload)}")
|
||||
|
||||
|
||||
def validate_file_payload(
|
||||
file_data: bytes,
|
||||
filename: str = "",
|
||||
max_size: int = MAX_FILE_PAYLOAD_SIZE
|
||||
) -> ValidationResult:
|
||||
"""
|
||||
Validate a file for embedding.
|
||||
|
||||
Args:
|
||||
file_data: Raw file bytes
|
||||
filename: Original filename (for display in errors)
|
||||
max_size: Maximum allowed size in bytes
|
||||
|
||||
Returns:
|
||||
ValidationResult
|
||||
"""
|
||||
if not file_data:
|
||||
return ValidationResult.error("File is empty")
|
||||
|
||||
if len(file_data) > max_size:
|
||||
size_kb = len(file_data) / 1024
|
||||
max_kb = max_size / 1024
|
||||
return ValidationResult.error(
|
||||
f"File '{filename or 'unnamed'}' too large ({size_kb:.1f} KB). "
|
||||
f"Maximum: {max_kb:.0f} KB"
|
||||
)
|
||||
|
||||
return ValidationResult.ok(size=len(file_data), filename=filename)
|
||||
|
||||
|
||||
def validate_image(
|
||||
image_data: bytes,
|
||||
name: str = "Image",
|
||||
@@ -319,6 +394,13 @@ def require_valid_message(message: str) -> None:
|
||||
raise MessageValidationError(result.error_message)
|
||||
|
||||
|
||||
def require_valid_payload(payload: Union[str, bytes, FilePayload]) -> None:
|
||||
"""Validate payload (text, bytes, or file), raising exception on failure."""
|
||||
result = validate_payload(payload)
|
||||
if not result.is_valid:
|
||||
raise MessageValidationError(result.error_message)
|
||||
|
||||
|
||||
def require_valid_image(image_data: bytes, name: str = "Image") -> None:
|
||||
"""Validate image, raising exception on failure."""
|
||||
result = validate_image(image_data, name)
|
||||
|
||||
BIN
test_data/holmes_shit_20251228.png
Normal file
BIN
test_data/holmes_shit_20251228.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
test_data/scandal.txt.gz
Normal file
BIN
test_data/scandal.txt.gz
Normal file
Binary file not shown.
353
test_data/scandal.txt.gz.b64
Normal file
353
test_data/scandal.txt.gz.b64
Normal file
@@ -0,0 +1,353 @@
|
||||
H4sICKvmUGkCA3NjYW5kYWwudHh0AJV927LjRpLke38Fqh727JqRZ6x3bczaWg9lpUtLp1u3VdVO
|
||||
WT+CJEhCBwQ4AHgo6o/2O/bH1t0jIjPBU5qxadsdSVUkCCQy4+Lh4fGnP/3pT9V/+L/31Yev3v/4
|
||||
9fvvq6cfqy9/+u6bH57e/2ffqd6P8/EyVl8Nfd1XXw+3rvmT/+8/+WL1sd50TTXsq+3Qz00/T//p
|
||||
N/S/r471eW7G6s//tY//z//ax//Xn+7+V3zku/c/f/zml+op/fHHofpwbMZu2D5X3w3dqZmq6dhU
|
||||
7VTV3bW+TdWM/7oOp7p/rJ6qY/3SVFPT7YZTdWzqcVcd25Nd6IRlaIcefzxWl36H/1v3t2qY+d99
|
||||
fWrwdfwlrtvc/DeabdeeJ/xH3e/sGuexwZXbvp4b/+HjYOvMq0zNb7jIXF3rqeqHGR+oZ/xFtW+6
|
||||
mT9m12hOg+6jfm77ah6qbsAt74exehqbvqne77pmfKzed118clrxBuxqQ9/4ndTj3G4vXT12t1V1
|
||||
bcamqjfHYcQ1Zl6VD7Idut2K97xtp6baXHATu1M7Ymv4rWzqru63za7CI+0eq+8a3voKyzjXz1ji
|
||||
eaVnPA3TXJ2bcd9s52ps6mno2/6QF2XYTM34wj861dtji2fQrdp7GTu8AqzH1DT9yu4B66mHHit9
|
||||
4tL5dfTuzl3NG8Jbw1vcV1iiutrXHe7/PEwtl0P32Tf8/nQenrX6/K1p2GNzxepMk63cxIte2/mI
|
||||
6xzaTaOlrKupb7jKH4/NzRfPV4Y3j0fx88LXwmvbIzbjet38tm26jovMv9uN9ZUPzs+8NG1X7Uds
|
||||
POy0h6niy3tp/EL61a3e5mP15WVOV57HGiu283XFM+Hd8V7marpsj3j+ebzoUew6+G97t8O1r3ZN
|
||||
126xFXX1PS7T3fDdXy/TjAvOzQnvrOau14ach3SFcdhdtvhWtWsn/D5uC4+wxz9xT9dji589tYcj
|
||||
3+A4XPmx4bKZq8uZmxbbEj+fT1Td4danSzfjub4dcdt6Y1PT82Vh6dseP3HhJ1cVLl9XW/zgMz+F
|
||||
p8Wr81fvT3TE767PA95IhTXG0VvZBtFx2nAr4lXxri/jxta91s/hkfrD8njpPnBQL/iGlhJrgJ/B
|
||||
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
|
||||
@@ -22,6 +22,7 @@ from stegasoo import (
|
||||
decode,
|
||||
DAY_NAMES,
|
||||
)
|
||||
from stegasoo.steganography import get_output_format, get_image_format
|
||||
|
||||
|
||||
class TestKeygen:
|
||||
@@ -127,19 +128,71 @@ class TestValidation:
|
||||
assert not result.is_valid
|
||||
|
||||
|
||||
class TestOutputFormat:
|
||||
"""Test output format detection and preservation."""
|
||||
|
||||
def test_png_stays_png(self):
|
||||
fmt, ext = get_output_format('PNG')
|
||||
assert fmt == 'PNG'
|
||||
assert ext == 'png'
|
||||
|
||||
def test_bmp_stays_bmp(self):
|
||||
fmt, ext = get_output_format('BMP')
|
||||
assert fmt == 'BMP'
|
||||
assert ext == 'bmp'
|
||||
|
||||
def test_jpeg_becomes_png(self):
|
||||
fmt, ext = get_output_format('JPEG')
|
||||
assert fmt == 'PNG'
|
||||
assert ext == 'png'
|
||||
|
||||
def test_gif_becomes_png(self):
|
||||
fmt, ext = get_output_format('GIF')
|
||||
assert fmt == 'PNG'
|
||||
assert ext == 'png'
|
||||
|
||||
def test_none_becomes_png(self):
|
||||
fmt, ext = get_output_format(None)
|
||||
assert fmt == 'PNG'
|
||||
assert ext == 'png'
|
||||
|
||||
def test_unknown_becomes_png(self):
|
||||
fmt, ext = get_output_format('WEBP')
|
||||
assert fmt == 'PNG'
|
||||
assert ext == 'png'
|
||||
|
||||
|
||||
class TestEncodeDecode:
|
||||
"""Test encoding and decoding (requires test images)."""
|
||||
|
||||
@pytest.fixture
|
||||
def test_image(self):
|
||||
"""Create a simple test image."""
|
||||
def png_image(self):
|
||||
"""Create a simple PNG test image."""
|
||||
from PIL import Image
|
||||
img = Image.new('RGB', (100, 100), color='red')
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format='PNG')
|
||||
return buf.getvalue()
|
||||
|
||||
def test_encode_decode_roundtrip(self, test_image):
|
||||
@pytest.fixture
|
||||
def bmp_image(self):
|
||||
"""Create a simple BMP test image."""
|
||||
from PIL import Image
|
||||
img = Image.new('RGB', (100, 100), color='blue')
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format='BMP')
|
||||
return buf.getvalue()
|
||||
|
||||
@pytest.fixture
|
||||
def jpeg_image(self):
|
||||
"""Create a simple JPEG test image."""
|
||||
from PIL import Image
|
||||
img = Image.new('RGB', (100, 100), color='green')
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format='JPEG', quality=95)
|
||||
return buf.getvalue()
|
||||
|
||||
def test_encode_decode_roundtrip(self, png_image):
|
||||
"""Test full encode/decode cycle."""
|
||||
message = "Secret message!"
|
||||
phrase = "apple forest thunder"
|
||||
@@ -147,8 +200,8 @@ class TestEncodeDecode:
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=test_image,
|
||||
carrier_image=test_image,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
day_phrase=phrase,
|
||||
pin=pin
|
||||
)
|
||||
@@ -159,19 +212,89 @@ class TestEncodeDecode:
|
||||
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=test_image,
|
||||
reference_photo=png_image,
|
||||
day_phrase=phrase,
|
||||
pin=pin
|
||||
)
|
||||
|
||||
assert decoded == message
|
||||
|
||||
def test_wrong_pin_fails(self, test_image):
|
||||
def test_png_carrier_produces_png(self, png_image):
|
||||
"""Test that PNG carrier produces PNG output."""
|
||||
result = encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
day_phrase="test phrase here",
|
||||
pin="123456"
|
||||
)
|
||||
|
||||
assert result.filename.endswith('.png')
|
||||
# Verify actual format
|
||||
output_format = get_image_format(result.stego_image)
|
||||
assert output_format == 'PNG'
|
||||
|
||||
def test_bmp_carrier_produces_bmp(self, bmp_image, png_image):
|
||||
"""Test that BMP carrier produces BMP output."""
|
||||
result = encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=bmp_image,
|
||||
day_phrase="test phrase here",
|
||||
pin="123456"
|
||||
)
|
||||
|
||||
assert result.filename.endswith('.bmp')
|
||||
# Verify actual format
|
||||
output_format = get_image_format(result.stego_image)
|
||||
assert output_format == 'BMP'
|
||||
|
||||
def test_jpeg_carrier_produces_png(self, jpeg_image, png_image):
|
||||
"""Test that JPEG carrier produces PNG output (lossy -> lossless)."""
|
||||
result = encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=jpeg_image,
|
||||
day_phrase="test phrase here",
|
||||
pin="123456"
|
||||
)
|
||||
|
||||
assert result.filename.endswith('.png')
|
||||
# Verify actual format
|
||||
output_format = get_image_format(result.stego_image)
|
||||
assert output_format == 'PNG'
|
||||
|
||||
def test_bmp_roundtrip(self, bmp_image, png_image):
|
||||
"""Test full encode/decode cycle with BMP."""
|
||||
message = "BMP test message!"
|
||||
phrase = "test phrase words"
|
||||
pin = "123456"
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=bmp_image,
|
||||
day_phrase=phrase,
|
||||
pin=pin
|
||||
)
|
||||
|
||||
assert result.filename.endswith('.bmp')
|
||||
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
day_phrase=phrase,
|
||||
pin=pin
|
||||
)
|
||||
|
||||
assert decoded == message
|
||||
|
||||
def test_wrong_pin_fails(self, png_image):
|
||||
"""Test that wrong PIN fails to decode."""
|
||||
result = encode(
|
||||
message="Secret",
|
||||
reference_photo=test_image,
|
||||
carrier_image=test_image,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
day_phrase="test phrase here",
|
||||
pin="123456"
|
||||
)
|
||||
@@ -179,17 +302,17 @@ class TestEncodeDecode:
|
||||
with pytest.raises(stegasoo.DecryptionError):
|
||||
decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=test_image,
|
||||
reference_photo=png_image,
|
||||
day_phrase="test phrase here",
|
||||
pin="654321" # Wrong PIN
|
||||
)
|
||||
|
||||
def test_wrong_phrase_fails(self, test_image):
|
||||
def test_wrong_phrase_fails(self, png_image):
|
||||
"""Test that wrong phrase fails to decode."""
|
||||
result = encode(
|
||||
message="Secret",
|
||||
reference_photo=test_image,
|
||||
carrier_image=test_image,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
day_phrase="correct phrase here",
|
||||
pin="123456"
|
||||
)
|
||||
@@ -197,7 +320,7 @@ class TestEncodeDecode:
|
||||
with pytest.raises(stegasoo.DecryptionError):
|
||||
decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=test_image,
|
||||
reference_photo=png_image,
|
||||
day_phrase="wrong phrase here",
|
||||
pin="123456"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user