622 lines
17 KiB
Python
622 lines
17 KiB
Python
"""
|
|
Stegasoo - Secure Steganography Library
|
|
|
|
A Python library for hiding encrypted messages and files in images using
|
|
hybrid photo + passphrase + PIN authentication.
|
|
|
|
Basic Usage - Text Message:
|
|
from stegasoo import encode, decode, generate_credentials
|
|
|
|
# Generate credentials
|
|
creds = generate_credentials(use_pin=True, use_rsa=False)
|
|
print(creds.phrases['Monday'])
|
|
print(creds.pin)
|
|
|
|
# Encode a message
|
|
with open('secret.jpg', 'rb') as f:
|
|
ref_photo = f.read()
|
|
with open('meme.png', 'rb') as f:
|
|
carrier = f.read()
|
|
|
|
result = encode(
|
|
message="Meet at midnight",
|
|
reference_photo=ref_photo,
|
|
carrier_image=carrier,
|
|
day_phrase="apple forest thunder",
|
|
pin="123456"
|
|
)
|
|
|
|
with open('stego.png', 'wb') as f:
|
|
f.write(result.stego_image)
|
|
|
|
# Decode a message
|
|
decoded = decode(
|
|
stego_image=result.stego_image,
|
|
reference_photo=ref_photo,
|
|
day_phrase="apple forest thunder",
|
|
pin="123456"
|
|
)
|
|
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)
|
|
|
|
Debugging:
|
|
from stegasoo.debug import debug
|
|
debug.enable(True) # Enable debug output
|
|
debug.enable_performance(True) # Enable timing
|
|
"""
|
|
|
|
from .constants import __version__, DAY_NAMES, MAX_MESSAGE_SIZE, MAX_FILE_PAYLOAD_SIZE
|
|
from .models import (
|
|
Credentials,
|
|
EncodeInput,
|
|
EncodeResult,
|
|
DecodeInput,
|
|
DecodeResult,
|
|
EmbedStats,
|
|
KeyInfo,
|
|
ValidationResult,
|
|
FilePayload,
|
|
)
|
|
from .exceptions import (
|
|
StegasooError,
|
|
ValidationError,
|
|
PinValidationError,
|
|
MessageValidationError,
|
|
ImageValidationError,
|
|
KeyValidationError,
|
|
SecurityFactorError,
|
|
CryptoError,
|
|
EncryptionError,
|
|
DecryptionError,
|
|
KeyDerivationError,
|
|
KeyGenerationError,
|
|
KeyPasswordError,
|
|
SteganographyError,
|
|
CapacityError,
|
|
ExtractionError,
|
|
EmbeddingError,
|
|
InvalidHeaderError,
|
|
)
|
|
from .keygen import (
|
|
generate_credentials,
|
|
generate_pin,
|
|
generate_phrase,
|
|
generate_day_phrases,
|
|
generate_rsa_key,
|
|
export_rsa_key_pem,
|
|
load_rsa_key,
|
|
get_key_info,
|
|
)
|
|
from .validation import (
|
|
validate_pin,
|
|
validate_message,
|
|
validate_payload,
|
|
validate_file_payload,
|
|
validate_image,
|
|
validate_rsa_key,
|
|
validate_security_factors,
|
|
validate_phrase,
|
|
validate_date_string,
|
|
require_valid_pin,
|
|
require_valid_message,
|
|
require_valid_payload,
|
|
require_valid_image,
|
|
require_valid_rsa_key,
|
|
require_security_factors,
|
|
)
|
|
from .crypto import (
|
|
encrypt_message,
|
|
decrypt_message,
|
|
decrypt_message_text,
|
|
derive_hybrid_key,
|
|
derive_pixel_key,
|
|
hash_photo,
|
|
parse_header,
|
|
get_date_from_encrypted,
|
|
has_argon2,
|
|
)
|
|
from .steganography import (
|
|
embed_in_image,
|
|
extract_from_image,
|
|
calculate_capacity,
|
|
get_image_dimensions,
|
|
get_image_format,
|
|
is_lossless_format,
|
|
LOSSLESS_FORMATS,
|
|
)
|
|
from .utils import (
|
|
generate_filename,
|
|
parse_date_from_filename,
|
|
get_day_from_date,
|
|
get_today_date,
|
|
get_today_day,
|
|
secure_delete,
|
|
SecureDeleter,
|
|
format_file_size,
|
|
)
|
|
from .debug import debug # Import debug utilities
|
|
|
|
# QR Code utilities (optional, depends on qrcode and pyzbar)
|
|
try:
|
|
from .qr_utils import (
|
|
generate_qr_code,
|
|
read_qr_code,
|
|
read_qr_code_from_file,
|
|
extract_key_from_qr,
|
|
extract_key_from_qr_file,
|
|
compress_data,
|
|
decompress_data,
|
|
auto_decompress,
|
|
normalize_pem,
|
|
is_compressed,
|
|
can_fit_in_qr,
|
|
needs_compression,
|
|
has_qr_read,
|
|
has_qr_write,
|
|
has_qr_support,
|
|
)
|
|
HAS_QR_UTILS = True
|
|
except ImportError:
|
|
HAS_QR_UTILS = False
|
|
|
|
from datetime import date
|
|
from pathlib import Path
|
|
from typing import Optional, Union, Dict, Any
|
|
|
|
|
|
def encode(
|
|
message: Union[str, bytes, FilePayload],
|
|
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,
|
|
) -> EncodeResult:
|
|
"""
|
|
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 (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
|
|
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'). If None, preserves
|
|
carrier format for lossless types, defaults to PNG for lossy.
|
|
|
|
Returns:
|
|
EncodeResult with stego image and metadata
|
|
|
|
Raises:
|
|
ValidationError: If inputs are invalid
|
|
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.
|
|
"""
|
|
# Debug logging
|
|
debug.print(f"encode called: message type={type(message).__name__}, "
|
|
f"day_phrase='{day_phrase[:20]}...', pin_length={len(pin)}")
|
|
|
|
# Validate inputs
|
|
require_valid_payload(message)
|
|
require_valid_image(carrier_image, "Carrier image")
|
|
require_security_factors(pin, rsa_key_data)
|
|
|
|
if pin:
|
|
require_valid_pin(pin)
|
|
if rsa_key_data:
|
|
require_valid_rsa_key(rsa_key_data, rsa_password)
|
|
|
|
# Default date to today
|
|
if date_str is None:
|
|
date_str = date.today().isoformat()
|
|
|
|
debug.print(f"Encoding for date: {date_str}")
|
|
|
|
# Encrypt message/file
|
|
encrypted = encrypt_message(
|
|
message, reference_photo, day_phrase, date_str, pin, rsa_key_data
|
|
)
|
|
|
|
# Debug: show encrypted data size
|
|
debug.print(f"Encrypted payload: {len(encrypted)} bytes")
|
|
|
|
# Get pixel key
|
|
pixel_key = derive_pixel_key(
|
|
reference_photo, day_phrase, date_str, pin, rsa_key_data
|
|
)
|
|
|
|
debug.data(pixel_key, "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 with correct extension
|
|
filename = generate_filename(date_str, extension=extension)
|
|
|
|
debug.print(f"Encoding complete: {filename}, "
|
|
f"modified {stats.pixels_modified}/{stats.total_pixels} pixels "
|
|
f"({stats.modification_percent:.2f}%)")
|
|
|
|
return EncodeResult(
|
|
stego_image=stego_data,
|
|
filename=filename,
|
|
pixels_modified=stats.pixels_modified,
|
|
total_pixels=stats.total_pixels,
|
|
capacity_used=stats.capacity_used,
|
|
date_used=date_str
|
|
)
|
|
|
|
|
|
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
|
|
"""
|
|
debug.print(f"encode_file called: filepath={filepath}")
|
|
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
|
|
"""
|
|
debug.print(f"encode_bytes called: filename={filename}, data_size={len(data)}")
|
|
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,
|
|
)
|
|
|
|
|
|
@debug.time
|
|
def decode(
|
|
stego_image: bytes,
|
|
reference_photo: bytes,
|
|
day_phrase: str,
|
|
pin: str = "",
|
|
rsa_key_data: Optional[bytes] = None,
|
|
rsa_password: Optional[str] = None,
|
|
date_str: Optional[str] = None,
|
|
) -> DecodeResult:
|
|
"""
|
|
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/file
|
|
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:
|
|
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
|
|
SecurityFactorError: If no PIN or RSA key provided
|
|
ExtractionError: If data cannot be extracted
|
|
DecryptionError: If decryption fails
|
|
"""
|
|
debug.print(f"decode called: stego_image_size={len(stego_image)}, "
|
|
f"day_phrase='{day_phrase[:20]}...'")
|
|
|
|
# Validate inputs
|
|
require_security_factors(pin, rsa_key_data)
|
|
|
|
if pin:
|
|
require_valid_pin(pin)
|
|
if rsa_key_data:
|
|
require_valid_rsa_key(rsa_key_data, rsa_password)
|
|
|
|
# Try to extract with today's date first
|
|
# Use provided date or fall back to today
|
|
if date_str is None:
|
|
date_str = date.today().isoformat()
|
|
pixel_key = derive_pixel_key(
|
|
reference_photo, day_phrase, date_str, pin, rsa_key_data
|
|
)
|
|
|
|
debug.data(pixel_key, "Pixel key for extraction")
|
|
|
|
encrypted = extract_from_image(stego_image, pixel_key)
|
|
|
|
# If we got data, check if it's from a different date
|
|
if encrypted:
|
|
header = parse_header(encrypted)
|
|
if header and header['date'] != date_str:
|
|
debug.print(f"Found different date in header: {header['date']} (expected {date_str})")
|
|
# Re-extract with correct date
|
|
pixel_key = derive_pixel_key(
|
|
reference_photo, day_phrase, header['date'], pin, rsa_key_data
|
|
)
|
|
encrypted = extract_from_image(stego_image, pixel_key)
|
|
|
|
if not encrypted:
|
|
debug.print("No data extracted from image")
|
|
raise ExtractionError("Could not extract data. Check your inputs.")
|
|
|
|
debug.print(f"Extracted {len(encrypted)} bytes from image")
|
|
debug.data(encrypted[:64], "First 64 bytes of extracted data")
|
|
|
|
# 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,
|
|
date_str: 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
|
|
"""
|
|
debug.print("decode_text called")
|
|
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:
|
|
debug.print(f"File is binary: {result.filename or 'unnamed'}")
|
|
raise DecryptionError(
|
|
f"Content is a binary file ({result.filename or 'unnamed'}), not text. "
|
|
"Use decode() instead and check result.is_file."
|
|
)
|
|
return ""
|
|
|
|
debug.print(f"Decoded text: {result.message[:100] if result.message else 'empty'}...")
|
|
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',
|
|
'EncodeInput',
|
|
'EncodeResult',
|
|
'DecodeInput',
|
|
'DecodeResult',
|
|
'EmbedStats',
|
|
'KeyInfo',
|
|
'ValidationResult',
|
|
'FilePayload',
|
|
|
|
# Exceptions
|
|
'StegasooError',
|
|
'ValidationError',
|
|
'PinValidationError',
|
|
'MessageValidationError',
|
|
'ImageValidationError',
|
|
'KeyValidationError',
|
|
'SecurityFactorError',
|
|
'CryptoError',
|
|
'EncryptionError',
|
|
'DecryptionError',
|
|
'KeyDerivationError',
|
|
'KeyGenerationError',
|
|
'KeyPasswordError',
|
|
'SteganographyError',
|
|
'CapacityError',
|
|
'ExtractionError',
|
|
'EmbeddingError',
|
|
'InvalidHeaderError',
|
|
|
|
# Key generation
|
|
'generate_pin',
|
|
'generate_phrase',
|
|
'generate_day_phrases',
|
|
'generate_rsa_key',
|
|
'export_rsa_key_pem',
|
|
'load_rsa_key',
|
|
'get_key_info',
|
|
|
|
# Validation
|
|
'validate_pin',
|
|
'validate_message',
|
|
'validate_payload',
|
|
'validate_file_payload',
|
|
'validate_image',
|
|
'validate_rsa_key',
|
|
'validate_security_factors',
|
|
'validate_phrase',
|
|
'validate_date_string',
|
|
'require_valid_pin',
|
|
'require_valid_message',
|
|
'require_valid_payload',
|
|
'require_valid_image',
|
|
'require_valid_rsa_key',
|
|
'require_security_factors',
|
|
|
|
# Crypto
|
|
'encrypt_message',
|
|
'decrypt_message',
|
|
'decrypt_message_text',
|
|
'derive_hybrid_key',
|
|
'derive_pixel_key',
|
|
'hash_photo',
|
|
'parse_header',
|
|
'get_date_from_encrypted',
|
|
'has_argon2',
|
|
|
|
# Steganography
|
|
'embed_in_image',
|
|
'extract_from_image',
|
|
'calculate_capacity',
|
|
'get_image_dimensions',
|
|
'get_image_format',
|
|
'is_lossless_format',
|
|
|
|
# Utilities
|
|
'generate_filename',
|
|
'parse_date_from_filename',
|
|
'get_day_from_date',
|
|
'get_today_date',
|
|
'get_today_day',
|
|
'secure_delete',
|
|
'SecureDeleter',
|
|
'format_file_size',
|
|
|
|
# Debugging
|
|
'debug',
|
|
]
|