More snazzy 4.0 Web UI improvements.
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Stegasoo Web Frontend (v3.2.0)
|
||||
Stegasoo Web Frontend (v4.0.0)
|
||||
|
||||
Flask-based web UI for steganography operations.
|
||||
Supports both text messages and file embedding.
|
||||
|
||||
CHANGES in v4.0.0:
|
||||
- Added channel key support for deployment/group isolation
|
||||
- New /api/channel/status endpoint
|
||||
- Channel key selector on encode/decode pages
|
||||
- Messages encoded with channel key require same key to decode
|
||||
|
||||
CHANGES in v3.2.0:
|
||||
- Removed date dependency from all operations
|
||||
- Renamed day_phrase → passphrase
|
||||
@@ -52,6 +58,11 @@ from stegasoo import (
|
||||
EMBED_MODE_DCT,
|
||||
EMBED_MODE_AUTO,
|
||||
has_dct_support,
|
||||
# Channel key functions (v4.0.0)
|
||||
has_channel_key,
|
||||
get_channel_status,
|
||||
validate_channel_key,
|
||||
generate_channel_key,
|
||||
# NOTE: encode, decode, compare_modes, will_fit_by_mode now use subprocess isolation
|
||||
)
|
||||
from stegasoo.constants import (
|
||||
@@ -126,6 +137,9 @@ THUMBNAIL_FILES: dict[str, bytes] = {}
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
"""Inject global variables into all templates."""
|
||||
# Get channel status (v4.0.0)
|
||||
channel_status = get_channel_status()
|
||||
|
||||
return {
|
||||
'version': __version__,
|
||||
'max_message_chars': MAX_MESSAGE_CHARS,
|
||||
@@ -140,6 +154,11 @@ def inject_globals():
|
||||
'default_passphrase_words': DEFAULT_PASSPHRASE_WORDS,
|
||||
# NEW in v3.0
|
||||
'has_dct': has_dct_support(),
|
||||
# NEW in v4.0.0 - Channel key status
|
||||
'channel_mode': channel_status['mode'],
|
||||
'channel_configured': channel_status['configured'],
|
||||
'channel_fingerprint': channel_status.get('fingerprint'),
|
||||
'channel_source': channel_status.get('source'),
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +173,13 @@ try:
|
||||
print(f"DCT support: {has_dct_support()}")
|
||||
print(f"QR code support: write={HAS_QRCODE}, read={HAS_QRCODE_READ}")
|
||||
|
||||
# Channel key status (v4.0.0)
|
||||
channel_status = get_channel_status()
|
||||
print(f"Channel key: {channel_status['mode']} mode")
|
||||
if channel_status['configured']:
|
||||
print(f" Fingerprint: {channel_status.get('fingerprint')}")
|
||||
print(f" Source: {channel_status.get('source')}")
|
||||
|
||||
DESIRED_PAYLOAD_SIZE = 2 * 1024 * 1024 # 2MB
|
||||
|
||||
if hasattr(stegasoo, 'MAX_FILE_PAYLOAD_SIZE'):
|
||||
@@ -164,6 +190,33 @@ except Exception as e:
|
||||
print(f"Could not override stegasoo limits: {e}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CHANNEL KEY HELPER (v4.0.0)
|
||||
# ============================================================================
|
||||
|
||||
def resolve_channel_key_form(channel_key_value: str) -> str:
|
||||
"""
|
||||
Resolve channel key from form input.
|
||||
|
||||
Args:
|
||||
channel_key_value: Form value ('auto', 'none', or explicit key)
|
||||
|
||||
Returns:
|
||||
Value to pass to subprocess_stego ('auto', 'none', or explicit key)
|
||||
"""
|
||||
if not channel_key_value or channel_key_value == 'auto':
|
||||
return 'auto'
|
||||
elif channel_key_value == 'none':
|
||||
return 'none'
|
||||
else:
|
||||
# Explicit key - validate format
|
||||
if validate_channel_key(channel_key_value):
|
||||
return channel_key_value
|
||||
else:
|
||||
# Invalid format, fall back to auto
|
||||
return 'auto'
|
||||
|
||||
|
||||
def generate_thumbnail(image_data: bytes, size: tuple = THUMBNAIL_SIZE) -> bytes:
|
||||
"""Generate thumbnail from image data."""
|
||||
try:
|
||||
@@ -233,6 +286,71 @@ def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CHANNEL KEY API (v4.0.0)
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/api/channel/status')
|
||||
def api_channel_status():
|
||||
"""
|
||||
Get current channel key status (v4.0.0).
|
||||
|
||||
Returns JSON with mode, fingerprint, and source.
|
||||
"""
|
||||
# Use subprocess for isolation
|
||||
result = subprocess_stego.get_channel_status(reveal=False)
|
||||
|
||||
if result.success:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'mode': result.mode,
|
||||
'configured': result.configured,
|
||||
'fingerprint': result.fingerprint,
|
||||
'source': result.source,
|
||||
})
|
||||
else:
|
||||
# Fallback to direct call if subprocess fails
|
||||
status = get_channel_status()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'mode': status['mode'],
|
||||
'configured': status['configured'],
|
||||
'fingerprint': status.get('fingerprint'),
|
||||
'source': status.get('source'),
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/channel/validate', methods=['POST'])
|
||||
def api_channel_validate():
|
||||
"""
|
||||
Validate a channel key format (v4.0.0).
|
||||
|
||||
Returns JSON with validation result.
|
||||
"""
|
||||
key = request.form.get('key', '') or request.json.get('key', '') if request.is_json else ''
|
||||
|
||||
if not key:
|
||||
return jsonify({'valid': False, 'error': 'No key provided'})
|
||||
|
||||
is_valid = validate_channel_key(key)
|
||||
|
||||
if is_valid:
|
||||
fingerprint = f"{key[:4]}-••••-••••-••••-••••-••••-••••-{key[-4:]}"
|
||||
return jsonify({
|
||||
'valid': True,
|
||||
'fingerprint': fingerprint,
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'valid': False,
|
||||
'error': 'Invalid format. Expected: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX',
|
||||
})
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# GENERATE
|
||||
# ============================================================================
|
||||
|
||||
@app.route('/generate', methods=['GET', 'POST'])
|
||||
def generate():
|
||||
if request.method == 'POST':
|
||||
@@ -614,6 +732,9 @@ def encode_page():
|
||||
if dct_color_mode not in ('grayscale', 'color'):
|
||||
dct_color_mode = 'color'
|
||||
|
||||
# NEW in v4.0.0 - Channel key
|
||||
channel_key = resolve_channel_key_form(request.form.get('channel_key', 'auto'))
|
||||
|
||||
# Check DCT availability
|
||||
if embed_mode == 'dct' and not has_dct_support():
|
||||
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
|
||||
@@ -708,7 +829,7 @@ def encode_page():
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('encode.html', has_qrcode_read=HAS_QRCODE_READ)
|
||||
|
||||
# v3.2.0: No date parameter needed
|
||||
# v4.0.0: Include channel_key parameter
|
||||
# Use subprocess-isolated encode to prevent crashes
|
||||
if payload_type == 'file' and payload_file and payload_file.filename:
|
||||
encode_result = subprocess_stego.encode(
|
||||
@@ -724,6 +845,7 @@ def encode_page():
|
||||
embed_mode=embed_mode,
|
||||
dct_output_format=dct_output_format if embed_mode == 'dct' else 'png',
|
||||
dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color',
|
||||
channel_key=channel_key, # v4.0.0
|
||||
)
|
||||
else:
|
||||
encode_result = subprocess_stego.encode(
|
||||
@@ -737,6 +859,7 @@ def encode_page():
|
||||
embed_mode=embed_mode,
|
||||
dct_output_format=dct_output_format if embed_mode == 'dct' else 'png',
|
||||
dct_color_mode=dct_color_mode if embed_mode == 'dct' else 'color',
|
||||
channel_key=channel_key, # v4.0.0
|
||||
)
|
||||
|
||||
# Check for subprocess errors
|
||||
@@ -772,6 +895,9 @@ def encode_page():
|
||||
'output_format': dct_output_format if embed_mode == 'dct' else 'png',
|
||||
'color_mode': dct_color_mode if embed_mode == 'dct' else None,
|
||||
'mime_type': output_mime,
|
||||
# Channel info (v4.0.0)
|
||||
'channel_mode': encode_result.channel_mode,
|
||||
'channel_fingerprint': encode_result.channel_fingerprint,
|
||||
}
|
||||
|
||||
return redirect(url_for('encode_result', file_id=file_id))
|
||||
@@ -812,6 +938,9 @@ def encode_result(file_id):
|
||||
embed_mode=file_info.get('embed_mode', 'lsb'),
|
||||
output_format=file_info.get('output_format', 'png'),
|
||||
color_mode=file_info.get('color_mode'),
|
||||
# Channel info (v4.0.0)
|
||||
channel_mode=file_info.get('channel_mode', 'public'),
|
||||
channel_fingerprint=file_info.get('channel_fingerprint'),
|
||||
)
|
||||
|
||||
|
||||
@@ -901,6 +1030,9 @@ def decode_page():
|
||||
if embed_mode not in ('auto', 'lsb', 'dct'):
|
||||
embed_mode = 'auto'
|
||||
|
||||
# NEW in v4.0.0 - Channel key
|
||||
channel_key = resolve_channel_key_form(request.form.get('channel_key', 'auto'))
|
||||
|
||||
# Check DCT availability
|
||||
if embed_mode == 'dct' and not has_dct_support():
|
||||
flash('DCT mode requires scipy. Install with: pip install scipy', 'error')
|
||||
@@ -957,7 +1089,7 @@ def decode_page():
|
||||
flash(result.error_message, 'error')
|
||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
||||
|
||||
# v3.2.0: No date_str parameter needed
|
||||
# v4.0.0: Include channel_key parameter
|
||||
# Use subprocess-isolated decode to prevent crashes
|
||||
decode_result = subprocess_stego.decode(
|
||||
stego_data=stego_data,
|
||||
@@ -967,11 +1099,16 @@ def decode_page():
|
||||
rsa_key_data=rsa_key_data,
|
||||
rsa_password=key_password,
|
||||
embed_mode=embed_mode,
|
||||
channel_key=channel_key, # v4.0.0
|
||||
)
|
||||
|
||||
# Check for subprocess errors
|
||||
if not decode_result.success:
|
||||
error_msg = decode_result.error or 'Decoding failed'
|
||||
# Check for channel key related errors
|
||||
if 'channel key' in error_msg.lower():
|
||||
flash(error_msg, 'error')
|
||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
||||
if 'decrypt' in error_msg.lower() or decode_result.error_type == 'DecryptionError':
|
||||
raise DecryptionError(error_msg)
|
||||
raise StegasooError(error_msg)
|
||||
@@ -1005,7 +1142,7 @@ def decode_page():
|
||||
)
|
||||
|
||||
except DecryptionError:
|
||||
flash('Decryption failed. Check your passphrase, PIN, RSA key, and reference photo.', 'error')
|
||||
flash('Decryption failed. Check your passphrase, PIN, RSA key, reference photo, and channel key.', 'error')
|
||||
return render_template('decode.html', has_qrcode_read=HAS_QRCODE_READ)
|
||||
except StegasooError as e:
|
||||
flash(str(e), 'error')
|
||||
|
||||
@@ -696,6 +696,177 @@ const Stegasoo = {
|
||||
adjust();
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// CHANNEL KEY HANDLING (v4.0.0)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Generate a random channel key in format XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
|
||||
* @returns {string} Generated key
|
||||
*/
|
||||
generateChannelKey() {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let key = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (i > 0) key += '-';
|
||||
for (let j = 0; j < 4; j++) {
|
||||
key += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
}
|
||||
return key;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate channel key format
|
||||
* @param {string} key - Key to validate
|
||||
* @returns {boolean} True if valid
|
||||
*/
|
||||
validateChannelKey(key) {
|
||||
const pattern = /^[A-Z0-9]{4}(-[A-Z0-9]{4}){7}$/;
|
||||
return pattern.test(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Format channel key input (auto-add dashes, uppercase)
|
||||
* @param {HTMLInputElement} input - Input element
|
||||
*/
|
||||
formatChannelKeyInput(input) {
|
||||
let value = input.value.toUpperCase();
|
||||
const clean = value.replace(/-/g, '');
|
||||
|
||||
if (clean.length > 0 && clean.length <= 32) {
|
||||
const formatted = clean.match(/.{1,4}/g)?.join('-') || clean;
|
||||
if (formatted !== value && formatted.length <= 39) {
|
||||
input.value = formatted;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and show/hide error state
|
||||
const isValid = this.validateChannelKey(input.value);
|
||||
input.classList.toggle('is-invalid', input.value.length > 0 && !isValid);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize channel key UI for encode/decode pages
|
||||
* @param {Object} config - Configuration object
|
||||
* @param {string} config.radioName - Name of radio buttons (default: 'channel_key')
|
||||
* @param {string} config.customInputId - ID of custom key input container
|
||||
* @param {string} config.keyInputId - ID of key input field
|
||||
* @param {string} config.generateBtnId - ID of generate button (optional)
|
||||
* @param {string} config.customRadioId - ID of custom radio button
|
||||
* @param {string[]} config.cardIds - Array of card/label IDs for active class toggling
|
||||
*/
|
||||
initChannelKey(config = {}) {
|
||||
const radioName = config.radioName || 'channel_key';
|
||||
const customInputId = config.customInputId || 'channelCustomInput';
|
||||
const keyInputId = config.keyInputId || 'channelKeyInput';
|
||||
const generateBtnId = config.generateBtnId;
|
||||
const customRadioId = config.customRadioId || 'channelCustom';
|
||||
const cardIds = config.cardIds || [];
|
||||
|
||||
const radios = document.querySelectorAll(`input[name="${radioName}"]`);
|
||||
const customInput = document.getElementById(customInputId);
|
||||
const keyInput = document.getElementById(keyInputId);
|
||||
const generateBtn = generateBtnId ? document.getElementById(generateBtnId) : null;
|
||||
const customRadio = document.getElementById(customRadioId);
|
||||
|
||||
// Toggle active class on mode-btn cards
|
||||
const updateActiveState = () => {
|
||||
radios.forEach(radio => {
|
||||
const card = radio.closest('.mode-btn');
|
||||
if (card) {
|
||||
card.classList.toggle('active', radio.checked);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Show/hide custom input based on selection
|
||||
radios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
updateActiveState();
|
||||
const isCustom = customRadio?.checked;
|
||||
customInput?.classList.toggle('d-none', !isCustom);
|
||||
if (isCustom && keyInput) {
|
||||
keyInput.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initial state
|
||||
updateActiveState();
|
||||
|
||||
// Format and validate key input
|
||||
keyInput?.addEventListener('input', () => {
|
||||
this.formatChannelKeyInput(keyInput);
|
||||
});
|
||||
|
||||
// Generate button (if present)
|
||||
generateBtn?.addEventListener('click', () => {
|
||||
if (keyInput) {
|
||||
keyInput.value = this.generateChannelKey();
|
||||
keyInput.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle form submission with channel key validation
|
||||
* @param {HTMLFormElement} form - Form element
|
||||
* @param {string} customRadioId - ID of custom radio button
|
||||
* @param {string} keyInputId - ID of key input field
|
||||
* @returns {boolean} True if valid, false to prevent submission
|
||||
*/
|
||||
validateChannelKeyOnSubmit(form, customRadioId, keyInputId) {
|
||||
const customRadio = document.getElementById(customRadioId);
|
||||
const keyInput = document.getElementById(keyInputId);
|
||||
|
||||
if (customRadio?.checked && keyInput) {
|
||||
if (!this.validateChannelKey(keyInput.value)) {
|
||||
keyInput.classList.add('is-invalid');
|
||||
keyInput.focus();
|
||||
return false;
|
||||
}
|
||||
// Set the radio value to the actual key for form submission
|
||||
customRadio.value = keyInput.value;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize standalone channel key generator (for generate page)
|
||||
* @param {string} inputId - ID of generated key input
|
||||
* @param {string} generateBtnId - ID of generate button
|
||||
* @param {string} copyBtnId - ID of copy button
|
||||
*/
|
||||
initChannelKeyGenerator(inputId, generateBtnId, copyBtnId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const generateBtn = document.getElementById(generateBtnId);
|
||||
const copyBtn = document.getElementById(copyBtnId);
|
||||
|
||||
generateBtn?.addEventListener('click', () => {
|
||||
if (input) {
|
||||
input.value = this.generateChannelKey();
|
||||
}
|
||||
if (copyBtn) {
|
||||
copyBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
copyBtn?.addEventListener('click', () => {
|
||||
if (input?.value) {
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
const icon = copyBtn.querySelector('i');
|
||||
if (icon) {
|
||||
icon.className = 'bi bi-check';
|
||||
setTimeout(() => { icon.className = 'bi bi-clipboard'; }, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// INITIALIZATION HELPERS
|
||||
// ========================================================================
|
||||
@@ -707,8 +878,30 @@ const Stegasoo = {
|
||||
this.initClipboardPaste(['input[name="carrier"]', 'input[name="reference_photo"]']);
|
||||
this.initQrCropAnimation('rsaQrInput');
|
||||
this.initCollapseChevrons();
|
||||
this.initFormLoading('encodeForm', 'encodeBtn', 'Encoding...');
|
||||
this.initPassphraseFontResize();
|
||||
|
||||
// Channel key (v4.0.0) - uses mode-btn style
|
||||
this.initChannelKey({
|
||||
customInputId: 'channelCustomInput',
|
||||
keyInputId: 'channelKeyInput',
|
||||
generateBtnId: 'channelKeyGenerate',
|
||||
customRadioId: 'channelCustom',
|
||||
cardIds: ['channelAutoCard', 'channelPublicCard', 'channelCustomCard']
|
||||
});
|
||||
|
||||
// Form submission with channel key validation
|
||||
const form = document.getElementById('encodeForm');
|
||||
const btn = document.getElementById('encodeBtn');
|
||||
form?.addEventListener('submit', (e) => {
|
||||
if (!this.validateChannelKeyOnSubmit(form, 'channelCustom', 'channelKeyInput')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
initDecodePage() {
|
||||
@@ -718,13 +911,36 @@ const Stegasoo = {
|
||||
this.initClipboardPaste(['input[name="stego_image"]', 'input[name="reference_photo"]']);
|
||||
this.initQrCropAnimation('rsaKeyQrInput');
|
||||
this.initCollapseChevrons();
|
||||
this.initFormLoading('decodeForm', 'decodeBtn', 'Decoding...');
|
||||
this.initPassphraseFontResize();
|
||||
|
||||
// Channel key (v4.0.0) - uses mode-btn style
|
||||
this.initChannelKey({
|
||||
customInputId: 'channelCustomInputDec',
|
||||
keyInputId: 'channelKeyInputDec',
|
||||
customRadioId: 'channelCustomDec',
|
||||
cardIds: ['channelAutoCardDec', 'channelPublicCardDec', 'channelCustomCardDec']
|
||||
});
|
||||
|
||||
// Form submission with channel key validation and mode display
|
||||
const form = document.getElementById('decodeForm');
|
||||
const btn = document.getElementById('decodeBtn');
|
||||
form?.addEventListener('submit', (e) => {
|
||||
if (!this.validateChannelKeyOnSubmit(form, 'channelCustomDec', 'channelKeyInputDec')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
const selectedMode = document.querySelector('input[name="embed_mode"]:checked')?.value || 'auto';
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Decoding (${selectedMode.toUpperCase()})...`;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
initGeneratePage() {
|
||||
this.initPasswordToggles();
|
||||
// Generate page has mostly unique functionality
|
||||
// Channel key generator (v4.0.0)
|
||||
this.initChannelKeyGenerator('channelKeyGenerated', 'generateChannelKeyBtn', 'copyChannelKeyBtn');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,23 @@
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Channel Card Icons (About page) - Contrast fix for gradient backgrounds
|
||||
---------------------------------------------------------------------------- */
|
||||
#channel-keys .card-header i.bi {
|
||||
/* Add outline/shadow for visibility on gradient backgrounds */
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
|
||||
drop-shadow(0 0 4px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
/* Override green Auto icon to white for better contrast */
|
||||
#channel-keys .card-header i.bi-gear-fill.text-success {
|
||||
color: #ffffff !important;
|
||||
filter: drop-shadow(0 0 3px rgba(34, 197, 94, 0.8))
|
||||
drop-shadow(0 0 6px rgba(34, 197, 94, 0.5))
|
||||
drop-shadow(0 0 2px rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Mode Selection Buttons (Compact)
|
||||
---------------------------------------------------------------------------- */
|
||||
@@ -34,11 +51,13 @@
|
||||
border: 2px solid var(--border-light);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
padding-left: 2.75rem; /* Make room for absolutely positioned radio */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative; /* For absolute positioning of radio */
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
@@ -52,10 +71,35 @@
|
||||
}
|
||||
|
||||
.mode-btn .form-check-input {
|
||||
margin-top: 0;
|
||||
position: absolute;
|
||||
left: 15px; /* Fixed distance from left edge of card */
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Remove ms-2 margin from first icon after radio since radio is now absolute */
|
||||
.mode-btn > i.bi:first-of-type {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* Equal-width mode buttons (ignores content length) */
|
||||
.mode-btn.equal-width {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
Security Factor Boxes - Matches drop-zone dashed border style
|
||||
---------------------------------------------------------------------------- */
|
||||
.security-box {
|
||||
border: 2px dashed rgba(255, 255, 255, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mode-info-icon {
|
||||
cursor: help;
|
||||
opacity: 0.6;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Stegasoo Subprocess Worker
|
||||
Stegasoo Subprocess Worker (v4.0.0)
|
||||
|
||||
This script runs in a subprocess and handles encode/decode operations.
|
||||
If it crashes due to jpegio/scipy issues, the parent Flask process survives.
|
||||
|
||||
CHANGES in v4.0.0:
|
||||
- Added channel_key support for encode/decode operations
|
||||
- New channel_status operation
|
||||
|
||||
Communication is via JSON over stdin/stdout:
|
||||
- Input: JSON object with operation parameters
|
||||
- Output: JSON object with results or error
|
||||
@@ -24,6 +28,49 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'src'))
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
|
||||
def _resolve_channel_key(channel_key_param):
|
||||
"""
|
||||
Resolve channel_key parameter to value for stegasoo.
|
||||
|
||||
Args:
|
||||
channel_key_param: 'auto', 'none', explicit key, or None
|
||||
|
||||
Returns:
|
||||
None (auto), "" (public), or explicit key string
|
||||
"""
|
||||
if channel_key_param is None or channel_key_param == "auto":
|
||||
return None # Auto mode - use server config
|
||||
elif channel_key_param == "none":
|
||||
return "" # Public mode
|
||||
else:
|
||||
return channel_key_param # Explicit key
|
||||
|
||||
|
||||
def _get_channel_info(resolved_key):
|
||||
"""
|
||||
Get channel mode and fingerprint for response.
|
||||
|
||||
Returns:
|
||||
(mode, fingerprint) tuple
|
||||
"""
|
||||
from stegasoo import has_channel_key, get_channel_status
|
||||
|
||||
if resolved_key == "":
|
||||
return "public", None
|
||||
|
||||
if resolved_key is not None:
|
||||
# Explicit key
|
||||
fingerprint = f"{resolved_key[:4]}-••••-••••-••••-••••-••••-••••-{resolved_key[-4:]}"
|
||||
return "private", fingerprint
|
||||
|
||||
# Auto mode - check server config
|
||||
if has_channel_key():
|
||||
status = get_channel_status()
|
||||
return "private", status.get('fingerprint')
|
||||
|
||||
return "public", None
|
||||
|
||||
|
||||
def encode_operation(params: dict) -> dict:
|
||||
"""Handle encode operation."""
|
||||
from stegasoo import encode, FilePayload
|
||||
@@ -48,6 +95,9 @@ def encode_operation(params: dict) -> dict:
|
||||
else:
|
||||
payload = params.get('message', '')
|
||||
|
||||
# Resolve channel key (v4.0.0)
|
||||
resolved_channel_key = _resolve_channel_key(params.get('channel_key', 'auto'))
|
||||
|
||||
# Call encode with correct parameter names
|
||||
result = encode(
|
||||
message=payload,
|
||||
@@ -60,6 +110,7 @@ def encode_operation(params: dict) -> dict:
|
||||
embed_mode=params.get('embed_mode', 'lsb'),
|
||||
dct_output_format=params.get('dct_output_format', 'png'),
|
||||
dct_color_mode=params.get('dct_color_mode', 'color'),
|
||||
channel_key=resolved_channel_key, # v4.0.0
|
||||
)
|
||||
|
||||
# Build stats dict if available
|
||||
@@ -71,11 +122,16 @@ def encode_operation(params: dict) -> dict:
|
||||
'bytes_embedded': getattr(result.stats, 'bytes_embedded', 0),
|
||||
}
|
||||
|
||||
# Get channel info for response (v4.0.0)
|
||||
channel_mode, channel_fingerprint = _get_channel_info(resolved_channel_key)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'stego_b64': base64.b64encode(result.stego_image).decode('ascii'),
|
||||
'filename': getattr(result, 'filename', None),
|
||||
'stats': stats,
|
||||
'channel_mode': channel_mode,
|
||||
'channel_fingerprint': channel_fingerprint,
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +148,9 @@ def decode_operation(params: dict) -> dict:
|
||||
if params.get('rsa_key_b64'):
|
||||
rsa_key_data = base64.b64decode(params['rsa_key_b64'])
|
||||
|
||||
# Resolve channel key (v4.0.0)
|
||||
resolved_channel_key = _resolve_channel_key(params.get('channel_key', 'auto'))
|
||||
|
||||
# Call decode with correct parameter names
|
||||
result = decode(
|
||||
stego_image=stego_data,
|
||||
@@ -101,6 +160,7 @@ def decode_operation(params: dict) -> dict:
|
||||
rsa_key_data=rsa_key_data,
|
||||
rsa_password=params.get('rsa_password'),
|
||||
embed_mode=params.get('embed_mode', 'auto'),
|
||||
channel_key=resolved_channel_key, # v4.0.0
|
||||
)
|
||||
|
||||
if result.is_file:
|
||||
@@ -150,6 +210,25 @@ def capacity_check_operation(params: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def channel_status_operation(params: dict) -> dict:
|
||||
"""Handle channel status check (v4.0.0)."""
|
||||
from stegasoo import get_channel_status
|
||||
|
||||
status = get_channel_status()
|
||||
reveal = params.get('reveal', False)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'status': {
|
||||
'mode': status['mode'],
|
||||
'configured': status['configured'],
|
||||
'fingerprint': status.get('fingerprint'),
|
||||
'source': status.get('source'),
|
||||
'key': status.get('key') if reveal and status['configured'] else None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point - read JSON from stdin, write JSON to stdout."""
|
||||
try:
|
||||
@@ -170,6 +249,8 @@ def main():
|
||||
output = compare_operation(params)
|
||||
elif operation == 'capacity':
|
||||
output = capacity_check_operation(params)
|
||||
elif operation == 'channel_status':
|
||||
output = channel_status_operation(params)
|
||||
else:
|
||||
output = {'success': False, 'error': f'Unknown operation: {operation}'}
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
"""
|
||||
Subprocess Steganography Wrapper
|
||||
Subprocess Steganography Wrapper (v4.0.0)
|
||||
|
||||
Runs stegasoo operations in isolated subprocesses to prevent crashes
|
||||
from taking down the Flask server.
|
||||
|
||||
CHANGES in v4.0.0:
|
||||
- Added channel_key parameter to encode() and decode() methods
|
||||
- Channel keys enable deployment/group isolation
|
||||
|
||||
Usage:
|
||||
from subprocess_stego import SubprocessStego
|
||||
|
||||
stego = SubprocessStego()
|
||||
|
||||
# Encode
|
||||
# Encode with channel key
|
||||
result = stego.encode(
|
||||
carrier_data=carrier_bytes,
|
||||
reference_data=ref_bytes,
|
||||
@@ -17,6 +21,7 @@ Usage:
|
||||
passphrase="my passphrase",
|
||||
pin="123456",
|
||||
embed_mode="dct",
|
||||
channel_key="auto", # or "none", or explicit key
|
||||
)
|
||||
|
||||
if result.success:
|
||||
@@ -31,6 +36,7 @@ Usage:
|
||||
reference_data=ref_bytes,
|
||||
passphrase="my passphrase",
|
||||
pin="123456",
|
||||
channel_key="auto",
|
||||
)
|
||||
|
||||
# Compare modes (capacity)
|
||||
@@ -60,6 +66,9 @@ class EncodeResult:
|
||||
stego_data: Optional[bytes] = None
|
||||
filename: Optional[str] = None
|
||||
stats: Optional[Dict[str, Any]] = None
|
||||
# Channel info (v4.0.0)
|
||||
channel_mode: Optional[str] = None
|
||||
channel_fingerprint: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
error_type: Optional[str] = None
|
||||
|
||||
@@ -101,6 +110,18 @@ class CapacityResult:
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChannelStatusResult:
|
||||
"""Result from channel status check (v4.0.0)."""
|
||||
success: bool
|
||||
mode: str = "public"
|
||||
configured: bool = False
|
||||
fingerprint: Optional[str] = None
|
||||
source: Optional[str] = None
|
||||
key: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class SubprocessStego:
|
||||
"""
|
||||
Subprocess-isolated steganography operations.
|
||||
@@ -205,6 +226,8 @@ class SubprocessStego:
|
||||
embed_mode: str = "lsb",
|
||||
dct_output_format: str = "png",
|
||||
dct_color_mode: str = "color",
|
||||
# Channel key (v4.0.0)
|
||||
channel_key: Optional[str] = "auto",
|
||||
timeout: Optional[int] = None,
|
||||
) -> EncodeResult:
|
||||
"""
|
||||
@@ -224,6 +247,7 @@ class SubprocessStego:
|
||||
embed_mode: 'lsb' or 'dct'
|
||||
dct_output_format: 'png' or 'jpeg' (for DCT mode)
|
||||
dct_color_mode: 'grayscale' or 'color' (for DCT mode)
|
||||
channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
|
||||
timeout: Operation timeout in seconds
|
||||
|
||||
Returns:
|
||||
@@ -239,6 +263,7 @@ class SubprocessStego:
|
||||
'embed_mode': embed_mode,
|
||||
'dct_output_format': dct_output_format,
|
||||
'dct_color_mode': dct_color_mode,
|
||||
'channel_key': channel_key, # v4.0.0
|
||||
}
|
||||
|
||||
if file_data:
|
||||
@@ -258,6 +283,8 @@ class SubprocessStego:
|
||||
stego_data=base64.b64decode(result['stego_b64']),
|
||||
filename=result.get('filename'),
|
||||
stats=result.get('stats'),
|
||||
channel_mode=result.get('channel_mode'),
|
||||
channel_fingerprint=result.get('channel_fingerprint'),
|
||||
)
|
||||
else:
|
||||
return EncodeResult(
|
||||
@@ -275,6 +302,8 @@ class SubprocessStego:
|
||||
rsa_key_data: Optional[bytes] = None,
|
||||
rsa_password: Optional[str] = None,
|
||||
embed_mode: str = "auto",
|
||||
# Channel key (v4.0.0)
|
||||
channel_key: Optional[str] = "auto",
|
||||
timeout: Optional[int] = None,
|
||||
) -> DecodeResult:
|
||||
"""
|
||||
@@ -288,6 +317,7 @@ class SubprocessStego:
|
||||
rsa_key_data: Optional RSA key PEM bytes
|
||||
rsa_password: RSA key password if encrypted
|
||||
embed_mode: 'auto', 'lsb', or 'dct'
|
||||
channel_key: 'auto' (server config), 'none' (public), or explicit key (v4.0.0)
|
||||
timeout: Operation timeout in seconds
|
||||
|
||||
Returns:
|
||||
@@ -300,6 +330,7 @@ class SubprocessStego:
|
||||
'passphrase': passphrase,
|
||||
'pin': pin,
|
||||
'embed_mode': embed_mode,
|
||||
'channel_key': channel_key, # v4.0.0
|
||||
}
|
||||
|
||||
if rsa_key_data:
|
||||
@@ -411,6 +442,44 @@ class SubprocessStego:
|
||||
success=False,
|
||||
error=result.get('error', 'Unknown error'),
|
||||
)
|
||||
|
||||
def get_channel_status(
|
||||
self,
|
||||
reveal: bool = False,
|
||||
timeout: Optional[int] = None,
|
||||
) -> ChannelStatusResult:
|
||||
"""
|
||||
Get current channel key status (v4.0.0).
|
||||
|
||||
Args:
|
||||
reveal: Include full key in response
|
||||
timeout: Operation timeout in seconds
|
||||
|
||||
Returns:
|
||||
ChannelStatusResult with channel info
|
||||
"""
|
||||
params = {
|
||||
'operation': 'channel_status',
|
||||
'reveal': reveal,
|
||||
}
|
||||
|
||||
result = self._run_worker(params, timeout)
|
||||
|
||||
if result.get('success'):
|
||||
status = result.get('status', {})
|
||||
return ChannelStatusResult(
|
||||
success=True,
|
||||
mode=status.get('mode', 'public'),
|
||||
configured=status.get('configured', False),
|
||||
fingerprint=status.get('fingerprint'),
|
||||
source=status.get('source'),
|
||||
key=status.get('key') if reveal else None,
|
||||
)
|
||||
else:
|
||||
return ChannelStatusResult(
|
||||
success=False,
|
||||
error=result.get('error', 'Unknown error'),
|
||||
)
|
||||
|
||||
|
||||
# Convenience function for quick usage
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="lead">
|
||||
Stegasoo is a steganography tool that hides encrypted messages and files
|
||||
inside ordinary images using multi-factor authentication.
|
||||
Stegasoo hides encrypted messages and files inside images using multi-factor authentication.
|
||||
</p>
|
||||
|
||||
<h6 class="text-primary mt-4 mb-3">Features</h6>
|
||||
@@ -22,22 +21,22 @@
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
<strong>Text & File Embedding</strong>
|
||||
<br><small class="text-muted">Hide messages or any file type (PDF, ZIP, documents)</small>
|
||||
<br><small class="text-muted">Any file type: PDF, ZIP, documents</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
<strong>Multi-Factor Security</strong>
|
||||
<br><small class="text-muted">Combines photo + passphrase + PIN/RSA key</small>
|
||||
<br><small class="text-muted">Photo + passphrase + PIN/RSA key</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
<strong>AES-256-GCM Encryption</strong>
|
||||
<br><small class="text-muted">Authenticated encryption with integrity verification</small>
|
||||
<br><small class="text-muted">Authenticated encryption with integrity check</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
<strong>LSB & DCT Modes</strong>
|
||||
<br><small class="text-muted">Choose capacity (LSB) or JPEG resilience (DCT)</small>
|
||||
<strong>DCT & LSB Modes</strong>
|
||||
<br><small class="text-muted">JPEG resilience (DCT) or high capacity (LSB)</small>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -46,12 +45,12 @@
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
<strong>Random Pixel Embedding</strong>
|
||||
<br><small class="text-muted">Key-derived selection defeats statistical analysis</small>
|
||||
<br><small class="text-muted">Defeats statistical analysis</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
<strong>Large Image Support</strong>
|
||||
<br><small class="text-muted">Up to {{ max_payload_kb }} KB payload, tested with 14MB+ images</small>
|
||||
<br><small class="text-muted">Up to {{ max_payload_kb }} KB, tested with 14MB+ images</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
@@ -61,7 +60,13 @@
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
<strong>QR Code Keys</strong>
|
||||
<br><small class="text-muted">Import/export RSA keys via QR codes</small>
|
||||
<br><small class="text-muted">Import/export RSA keys via QR</small>
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
<strong>Channel Keys</strong>
|
||||
<span class="badge bg-info ms-1">v4.0</span>
|
||||
<br><small class="text-muted">Group/deployment isolation</small>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -75,67 +80,61 @@
|
||||
<h5 class="mb-0"><i class="bi bi-cpu me-2"></i>Embedding Modes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
Stegasoo supports two embedding modes, each optimized for different use cases.
|
||||
</p>
|
||||
<p>Two modes optimized for different use cases.</p>
|
||||
|
||||
<div class="row mt-4">
|
||||
<!-- LSB Mode -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card bg-dark h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-grid-3x3-gap text-primary me-2"></i>
|
||||
<strong>LSB Mode</strong>
|
||||
<span class="badge bg-success ms-2">Default</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">
|
||||
<strong>LSB (Least Significant Bit)</strong> embeds data in the lowest bit
|
||||
of each color channel. Changing the LSB changes pixel values by at most 1,
|
||||
which is imperceptible to the human eye.
|
||||
</p>
|
||||
<ul class="small mb-0">
|
||||
<li><strong>Capacity:</strong> ~375 KB per megapixel</li>
|
||||
<li><strong>Output:</strong> PNG (lossless)</li>
|
||||
<li><strong>Color:</strong> Full color preserved</li>
|
||||
<li><strong>Speed:</strong> Fast (~0.5s)</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div class="small">
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Email attachments<br>
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Cloud storage (Dropbox, Drive)<br>
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Direct file transfer<br>
|
||||
<i class="bi bi-x-circle text-danger me-1"></i> Social media (recompresses)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DCT Mode -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card bg-dark h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-soundwave text-warning me-2"></i>
|
||||
<strong>DCT Mode</strong>
|
||||
<span class="badge bg-success ms-2">Default</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">
|
||||
<strong>DCT (Discrete Cosine Transform)</strong> embeds data in frequency
|
||||
coefficients rather than raw pixels. This survives JPEG recompression
|
||||
because coefficients are preserved during re-encoding.
|
||||
<strong>DCT (Discrete Cosine Transform)</strong> embeds data in frequency coefficients. Survives JPEG recompression.
|
||||
</p>
|
||||
<ul class="small mb-0">
|
||||
<li><strong>Capacity:</strong> ~75 KB per megapixel</li>
|
||||
<li><strong>Capacity:</strong> ~75 KB/MP</li>
|
||||
<li><strong>Output:</strong> JPEG or PNG</li>
|
||||
<li><strong>Color:</strong> Color or grayscale</li>
|
||||
<li><strong>Speed:</strong> Slower (~2s)</li>
|
||||
<li><strong>Speed:</strong> ~2s</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div class="small">
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Instagram, Facebook<br>
|
||||
<i class="bi bi-check-circle text-success me-1"></i> WhatsApp, Signal, Telegram<br>
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Twitter/X<br>
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Any platform that recompresses
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Any recompressing platform
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LSB Mode -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card bg-dark h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-grid-3x3-gap text-primary me-2"></i>
|
||||
<strong>LSB Mode</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">
|
||||
<strong>LSB (Least Significant Bit)</strong> embeds data in the lowest bit of each color channel. Imperceptible to the eye.
|
||||
</p>
|
||||
<ul class="small mb-0">
|
||||
<li><strong>Capacity:</strong> ~375 KB/MP</li>
|
||||
<li><strong>Output:</strong> PNG (lossless)</li>
|
||||
<li><strong>Color:</strong> Full color</li>
|
||||
<li><strong>Speed:</strong> ~0.5s</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div class="small">
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Email attachments<br>
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Cloud storage<br>
|
||||
<i class="bi bi-check-circle text-success me-1"></i> Direct file transfer<br>
|
||||
<i class="bi bi-x-circle text-danger me-1"></i> Social media
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,35 +148,30 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Aspect</th>
|
||||
<th>DCT Mode <span class="badge bg-success ms-1">Default</span></th>
|
||||
<th>LSB Mode</th>
|
||||
<th>DCT Mode</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Capacity (1080p)</td>
|
||||
<td class="text-success">~770 KB</td>
|
||||
<td class="text-warning">~50 KB</td>
|
||||
<td class="text-success">~770 KB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Survives JPEG</td>
|
||||
<td class="text-danger">❌ No</td>
|
||||
<td class="text-success">✅ Yes</td>
|
||||
<td class="text-danger">❌ No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Social Media</td>
|
||||
<td class="text-danger">❌ Broken</td>
|
||||
<td class="text-success">✅ Works</td>
|
||||
<td class="text-danger">❌ Broken</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Detection Resistance</td>
|
||||
<td>Moderate</td>
|
||||
<td>Better</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dependencies</td>
|
||||
<td>Pillow, NumPy</td>
|
||||
<td>+ scipy, jpegio</td>
|
||||
<td>Moderate</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -185,8 +179,7 @@
|
||||
|
||||
<div class="alert alert-info small mt-3 mb-0">
|
||||
<i class="bi bi-lightbulb me-2"></i>
|
||||
<strong>Auto-Detection:</strong> When decoding, Stegasoo automatically detects whether
|
||||
LSB or DCT mode was used. You don't need to specify the mode during decoding.
|
||||
<strong>Auto-Detection:</strong> Mode is detected automatically when decoding.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,64 +189,149 @@
|
||||
<h5 class="mb-0"><i class="bi bi-shield-lock me-2"></i>How Security Works</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Stegasoo uses <strong>multi-factor authentication</strong> to derive encryption keys:</p>
|
||||
<p>Multi-factor authentication derives encryption keys:</p>
|
||||
|
||||
<div class="row text-center my-4">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="p-3 bg-dark rounded">
|
||||
<div class="col-6 col-lg-3 mb-3">
|
||||
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
|
||||
<i class="bi bi-image text-info fs-2 d-block mb-2"></i>
|
||||
<strong>Reference Photo</strong>
|
||||
<div class="small text-muted mt-1">Something you have</div>
|
||||
<div class="small text-success">~80-256 bits</div>
|
||||
<div class="small text-success mt-auto pt-2">~80-256 bits</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="p-3 bg-dark rounded">
|
||||
<div class="col-6 col-lg-3 mb-3">
|
||||
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
|
||||
<i class="bi bi-chat-quote text-warning fs-2 d-block mb-2"></i>
|
||||
<strong>Passphrase</strong>
|
||||
<div class="small text-muted mt-1">Something you know</div>
|
||||
<div class="small text-success">~44 bits (4 words)</div>
|
||||
<div class="small text-success mt-auto pt-2">~44 bits (4 words)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="p-3 bg-dark rounded">
|
||||
<div class="col-6 col-lg-3 mb-3">
|
||||
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
|
||||
<i class="bi bi-123 text-danger fs-2 d-block mb-2"></i>
|
||||
<strong>Static PIN</strong>
|
||||
<div class="small text-muted mt-1">Something you know</div>
|
||||
<div class="small text-success">~20 bits (6 digits)</div>
|
||||
<div class="small text-success mt-auto pt-2">~20 bits (6 digits)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="p-3 bg-dark rounded">
|
||||
<div class="col-6 col-lg-3 mb-3">
|
||||
<div class="p-3 bg-dark rounded h-100 d-flex flex-column align-items-center">
|
||||
<i class="bi bi-key text-primary fs-2 d-block mb-2"></i>
|
||||
<strong>RSA Key</strong>
|
||||
<div class="small text-muted mt-1">Something you have (optional)</div>
|
||||
<div class="small text-success">~128 bits</div>
|
||||
<div class="small text-muted mt-1">Optional</div>
|
||||
<div class="small text-success mt-auto pt-2">~128 bits</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-secondary">
|
||||
<i class="bi bi-calculator me-2"></i>
|
||||
<strong>Combined entropy:</strong> 144-424+ bits depending on configuration.
|
||||
For reference, 128 bits is considered computationally infeasible to brute force.
|
||||
<strong>Combined entropy:</strong> 144-424+ bits. 128 bits is infeasible to brute force.
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4">Key Derivation</h6>
|
||||
<p>
|
||||
{% if has_argon2 %}
|
||||
<span class="badge bg-success me-1"><i class="bi bi-check"></i> Argon2id</span>
|
||||
Using <strong>Argon2id</strong> with 256MB memory cost — memory-hard KDF that
|
||||
makes GPU/ASIC attacks infeasible.
|
||||
256MB memory cost. Memory-hard KDF defeats GPU/ASIC attacks.
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark me-1"><i class="bi bi-exclamation-triangle"></i> Argon2 Not Available</span>
|
||||
Falling back to <strong>PBKDF2-SHA512</strong> with 600,000 iterations.
|
||||
Using PBKDF2-SHA512 with 600k iterations.
|
||||
Install <code>argon2-cffi</code> for stronger security.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel Keys (v4.0.0) -->
|
||||
<div class="card mb-4" id="channel-keys">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-broadcast me-2"></i>Channel Keys
|
||||
<span class="badge bg-info ms-2">v4.0</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
Channel keys provide <strong>deployment/group isolation</strong>. Messages encoded with one channel key
|
||||
cannot be decoded with a different key, even if all other credentials match.
|
||||
</p>
|
||||
|
||||
<div class="row mt-4">
|
||||
<!-- Auto Mode -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card bg-dark h-100">
|
||||
<div class="card-header text-center">
|
||||
<i class="bi bi-gear-fill text-success fs-2 d-block mb-2"></i>
|
||||
<strong>Auto</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small mb-2">Uses server-configured key if available, otherwise public mode.</p>
|
||||
<ul class="small mb-0">
|
||||
<li>Set via <code>STEGASOO_CHANNEL_KEY</code> env var</li>
|
||||
<li>Or <code>channel_key</code> in config file</li>
|
||||
<li>All users share the same channel</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public Mode -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card bg-dark h-100">
|
||||
<div class="card-header text-center">
|
||||
<i class="bi bi-globe text-info fs-2 d-block mb-2"></i>
|
||||
<strong>Public</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small mb-2">No channel key. Compatible with other public installations.</p>
|
||||
<ul class="small mb-0">
|
||||
<li>Default if no server key configured</li>
|
||||
<li>Anyone can decode (with credentials)</li>
|
||||
<li>Interoperable between deployments</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Mode -->
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card bg-dark h-100">
|
||||
<div class="card-header text-center">
|
||||
<i class="bi bi-key-fill text-warning fs-2 d-block mb-2"></i>
|
||||
<strong>Custom</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small mb-2">Your own group key. Share with recipients.</p>
|
||||
<ul class="small mb-0">
|
||||
<li>Format: <code>XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX</code></li>
|
||||
<li>32 chars (128 bits entropy)</li>
|
||||
<li>Private group communication</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if channel_configured %}
|
||||
<div class="alert alert-success mt-3 mb-0">
|
||||
<i class="bi bi-shield-lock me-2"></i>
|
||||
<strong>This server has a channel key configured:</strong>
|
||||
<code class="ms-2">{{ channel_fingerprint }}</code>
|
||||
<span class="text-muted ms-2">({{ channel_source }})</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info mt-3 mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
This server is running in <strong>public mode</strong>.
|
||||
Set <code>STEGASOO_CHANNEL_KEY</code> to enable server-wide channel isolation.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version History -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
@@ -272,18 +350,18 @@
|
||||
<tr>
|
||||
<td><strong>4.0.0</strong></td>
|
||||
<td>
|
||||
Simplified auth (no date dependency), passphrase replaces day_phrase,
|
||||
4-word default, JPEG normalization fix, large image support (14MB+ tested),
|
||||
subprocess isolation for stability, Python 3.10-3.12 required
|
||||
<strong>Channel keys</strong> for group/deployment isolation,
|
||||
DCT default, simplified auth, passphrase replaces day_phrase,
|
||||
4-word default, JPEG fix, large image support, subprocess isolation, Python 3.10-3.12
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3.2.0</td>
|
||||
<td>Single passphrase (removed day-of-week rotation), increased default words</td>
|
||||
<td>Single passphrase, more default words</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3.0.0</td>
|
||||
<td>DCT steganography mode, JPEG output, color preservation option</td>
|
||||
<td>DCT mode, JPEG output, color preservation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2.2.0</td>
|
||||
@@ -291,11 +369,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2.1.0</td>
|
||||
<td>File embedding, compression support</td>
|
||||
<td>File embedding, compression</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2.0.0</td>
|
||||
<td>Web UI, REST API, RSA key support</td>
|
||||
<td>Web UI, REST API, RSA keys</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1.0.0</td>
|
||||
@@ -304,12 +382,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning small mt-3 mb-0">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Compatibility:</strong> v4.0 cannot decode messages from v3.1 or earlier (different format).
|
||||
Messages encoded with v3.2 should decode correctly.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -329,11 +401,11 @@
|
||||
<div id="setup" class="accordion-collapse collapse show" data-bs-parent="#usageAccordion">
|
||||
<div class="accordion-body">
|
||||
<ol>
|
||||
<li>Both parties agree on a <strong>reference photo</strong> (shared secretly, never transmitted)</li>
|
||||
<li>Go to <a href="/generate">Generate</a> and create credentials</li>
|
||||
<li><strong>Memorize</strong> the passphrase and PIN</li>
|
||||
<li>If using RSA, download and securely store the key file</li>
|
||||
<li>Share credentials with your contact through a secure channel</li>
|
||||
<li>Agree on a <strong>reference photo</strong> (never transmitted)</li>
|
||||
<li>Go to <a href="/generate">Generate</a> to create credentials</li>
|
||||
<li>Memorize passphrase and PIN</li>
|
||||
<li>If using RSA, store the key file securely</li>
|
||||
<li>Share credentials via secure channel</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,24 +415,23 @@
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-light" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#encoding">
|
||||
<i class="bi bi-2-circle me-2"></i>Encoding a Message
|
||||
<i class="bi bi-2-circle me-2"></i>Encoding
|
||||
</button>
|
||||
</h2>
|
||||
<div id="encoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
|
||||
<div class="accordion-body">
|
||||
<ol>
|
||||
<li>Go to <a href="/encode">Encode</a></li>
|
||||
<li>Choose your <strong>embedding mode</strong>:
|
||||
<li>Upload <strong>reference photo</strong> and <strong>carrier image</strong></li>
|
||||
<li>Choose mode:
|
||||
<ul>
|
||||
<li><strong>LSB</strong> – for email, cloud storage, direct transfer</li>
|
||||
<li><strong>DCT</strong> – for social media (Instagram, WhatsApp, etc.)</li>
|
||||
<li><strong>DCT</strong> (default): social media</li>
|
||||
<li><strong>LSB</strong>: email, cloud, direct transfer</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Upload your <strong>reference photo</strong> and <strong>carrier image</strong></li>
|
||||
<li>Enter your message or select a file to embed</li>
|
||||
<li>Enter your <strong>passphrase</strong> and PIN/key</li>
|
||||
<li>Download the resulting stego image</li>
|
||||
<li>Send through any channel!</li>
|
||||
<li>Enter message or select file</li>
|
||||
<li>Enter passphrase and PIN/key</li>
|
||||
<li>Download stego image</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
@@ -370,22 +441,21 @@
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed bg-dark text-light" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#decoding">
|
||||
<i class="bi bi-3-circle me-2"></i>Decoding a Message
|
||||
<i class="bi bi-3-circle me-2"></i>Decoding
|
||||
</button>
|
||||
</h2>
|
||||
<div id="decoding" class="accordion-collapse collapse" data-bs-parent="#usageAccordion">
|
||||
<div class="accordion-body">
|
||||
<ol>
|
||||
<li>Go to <a href="/decode">Decode</a></li>
|
||||
<li>Upload your <strong>reference photo</strong> (same one used for encoding)</li>
|
||||
<li>Upload the <strong>stego image</strong> you received</li>
|
||||
<li>Enter your <strong>passphrase</strong></li>
|
||||
<li>Enter your PIN and/or RSA key</li>
|
||||
<li>View the decoded message or download the extracted file</li>
|
||||
<li>Upload <strong>reference photo</strong></li>
|
||||
<li>Upload <strong>stego image</strong></li>
|
||||
<li>Enter passphrase and PIN/key</li>
|
||||
<li>View message or download file</li>
|
||||
</ol>
|
||||
<div class="alert alert-info small mt-3 mb-0">
|
||||
<i class="bi bi-magic me-2"></i>
|
||||
<strong>Auto-detection:</strong> Stegasoo automatically detects LSB vs DCT mode.
|
||||
Mode is auto-detected.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,67 +466,64 @@
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits & Specifications</h5>
|
||||
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>Limits & Specs</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-dark table-striped small">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><i class="bi bi-file-text me-2"></i>Max text message</td>
|
||||
<td><strong>2 million characters</strong></td>
|
||||
<td><i class="bi bi-file-text me-2"></i>Max text</td>
|
||||
<td><strong>2M characters</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-file-earmark me-2"></i>Max file payload</td>
|
||||
<td><i class="bi bi-file-earmark me-2"></i>Max file</td>
|
||||
<td><strong>{{ max_payload_kb }} KB</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-image me-2"></i>Max carrier image</td>
|
||||
<td><strong>24 megapixels</strong> (~6000×4000)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-grid-3x3 me-2"></i>LSB capacity</td>
|
||||
<td><strong>~375 KB/megapixel</strong></td>
|
||||
<td><i class="bi bi-image me-2"></i>Max carrier</td>
|
||||
<td><strong>24 MP</strong> (~6000x4000)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-soundwave me-2"></i>DCT capacity</td>
|
||||
<td><strong>~75 KB/megapixel</strong></td>
|
||||
<td><strong>~75 KB/MP</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-upload me-2"></i>Max upload size</td>
|
||||
<td><i class="bi bi-grid-3x3 me-2"></i>LSB capacity</td>
|
||||
<td><strong>~375 KB/MP</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-upload me-2"></i>Max upload</td>
|
||||
<td><strong>30 MB</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-clock me-2"></i>Temp file expiry</td>
|
||||
<td><strong>5 minutes</strong></td>
|
||||
<td><i class="bi bi-clock me-2"></i>File expiry</td>
|
||||
<td><strong>5 min</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-key me-2"></i>PIN length</td>
|
||||
<td><i class="bi bi-key me-2"></i>PIN</td>
|
||||
<td><strong>6-9 digits</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-shield me-2"></i>RSA key sizes</td>
|
||||
<td><strong>2048, 3072, 4096 bits</strong></td>
|
||||
<td><i class="bi bi-shield me-2"></i>RSA keys</td>
|
||||
<td><strong>2048, 3072, 4096 bit</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-chat-quote me-2"></i>Passphrase length</td>
|
||||
<td><strong>3-12 words</strong> (BIP-39, recommended: 4+ words)</td>
|
||||
<td><i class="bi bi-chat-quote me-2"></i>Passphrase</td>
|
||||
<td><strong>3-12 words</strong> (BIP-39)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-code me-2"></i>Python version</td>
|
||||
<td><strong>3.10-3.12</strong> (3.13 not supported)</td>
|
||||
<td><i class="bi bi-code me-2"></i>Python Version</td>
|
||||
<td><strong>3.10-3.12</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="bi bi-box me-2"></i>Built with</td>
|
||||
<td>Flask, Pillow, NumPy, SciPy, jpegio, cryptography, argon2-cffi</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4 text-muted small">
|
||||
<p>
|
||||
Stegasoo v{{ version }} •
|
||||
<i class="bi bi-github me-1"></i>Open Source •
|
||||
Built with Python, Flask, and cryptography
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -266,6 +266,7 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="security-box">
|
||||
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
|
||||
<div class="input-group pin-input-container">
|
||||
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
|
||||
@@ -274,9 +275,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">If PIN was used during encoding</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-3">
|
||||
<div class="security-box">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
||||
</label>
|
||||
@@ -333,9 +336,64 @@
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
CHANNEL KEY (v4.0.0) - Deployment/Group Isolation
|
||||
================================================================ -->
|
||||
<div class="mb-4">
|
||||
<div class="security-box">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-broadcast me-1"></i> Channel
|
||||
<span class="badge bg-info ms-1">v4.0</span>
|
||||
</label>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Auto Mode -->
|
||||
<label class="mode-btn flex-fill {% if channel_configured %}active{% endif %}" id="channelAutoCardDec" for="channelAutoDec">
|
||||
<input class="form-check-input" type="radio" name="channel_key" id="channelAutoDec" value="auto" checked>
|
||||
<i class="bi bi-gear-fill {% if channel_configured %}text-success{% else %}text-secondary{% endif %} ms-2"></i>
|
||||
<span class="ms-2"><strong>Auto</strong> <span class="text-muted d-none d-sm-inline">· {% if channel_configured %}Server Key{% else %}Public{% endif %}</span></span>
|
||||
</label>
|
||||
|
||||
<!-- Public Mode -->
|
||||
<label class="mode-btn flex-fill" id="channelPublicCardDec" for="channelPublicDec">
|
||||
<input class="form-check-input" type="radio" name="channel_key" id="channelPublicDec" value="none">
|
||||
<i class="bi bi-globe text-info ms-2"></i>
|
||||
<span class="ms-2"><strong>Public</strong> <span class="text-muted d-none d-sm-inline">· No key</span></span>
|
||||
</label>
|
||||
|
||||
<!-- Custom Key -->
|
||||
<label class="mode-btn flex-fill" id="channelCustomCardDec" for="channelCustomDec">
|
||||
<input class="form-check-input" type="radio" name="channel_key" id="channelCustomDec" value="custom">
|
||||
<i class="bi bi-key-fill text-warning ms-2"></i>
|
||||
<span class="ms-2"><strong>Custom</strong></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Server channel indicator (compact) -->
|
||||
{% if channel_configured %}
|
||||
<div class="small text-success mt-2">
|
||||
<i class="bi bi-shield-lock me-1"></i>
|
||||
Server: <code>{{ channel_fingerprint }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom key input -->
|
||||
<div class="mt-2 d-none" id="channelCustomInputDec">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="text" name="channel_key_custom" class="form-control font-monospace"
|
||||
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
||||
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}"
|
||||
id="channelKeyInputDec">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
ADVANCED OPTIONS (v3.0) - Extraction Mode
|
||||
================================================================ -->
|
||||
@@ -355,51 +413,34 @@
|
||||
<span class="badge bg-info ms-1">v3.0</span>
|
||||
</label>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Auto Mode -->
|
||||
<div class="col-4">
|
||||
<div class="form-check card p-2 text-center h-100" id="autoModeCard">
|
||||
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeAuto" value="auto" checked>
|
||||
<label class="form-check-label w-100" for="modeAuto">
|
||||
<i class="bi bi-magic text-success fs-4 d-block mb-1"></i>
|
||||
<strong>Auto</strong>
|
||||
<div class="small text-muted">Try both</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="mode-btn flex-fill active" id="autoModeCard" for="modeAuto">
|
||||
<input class="form-check-input" type="radio" name="embed_mode" id="modeAuto" value="auto" checked>
|
||||
<i class="bi bi-magic text-success"></i>
|
||||
<span class="ms-2"><strong>Auto</strong> <span class="text-muted d-none d-sm-inline">· Try both</span></span>
|
||||
</label>
|
||||
|
||||
<!-- LSB Mode -->
|
||||
<div class="col-4">
|
||||
<div class="form-check card p-2 text-center h-100" id="lsbModeCardDec">
|
||||
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeLsbDec" value="lsb">
|
||||
<label class="form-check-label w-100" for="modeLsbDec">
|
||||
<i class="bi bi-grid-3x3-gap text-primary fs-4 d-block mb-1"></i>
|
||||
<strong>LSB</strong>
|
||||
<div class="small text-muted">Spatial only</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="mode-btn flex-fill" id="lsbModeCardDec" for="modeLsbDec">
|
||||
<input class="form-check-input" type="radio" name="embed_mode" id="modeLsbDec" value="lsb">
|
||||
<i class="bi bi-grid-3x3-gap text-primary"></i>
|
||||
<span class="ms-2"><strong>LSB</strong> <span class="text-muted d-none d-sm-inline">· Spatial</span></span>
|
||||
</label>
|
||||
|
||||
<!-- DCT Mode -->
|
||||
<div class="col-4">
|
||||
<div class="form-check card p-2 text-center h-100 {% if not has_dct %}opacity-50{% endif %}" id="dctModeCardDec">
|
||||
<input class="form-check-input mx-auto" type="radio" name="embed_mode" id="modeDctDec" value="dct" {% if not has_dct %}disabled{% endif %}>
|
||||
<label class="form-check-label w-100" for="modeDctDec">
|
||||
<i class="bi bi-soundwave text-info fs-4 d-block mb-1"></i>
|
||||
<strong>DCT</strong>
|
||||
<div class="small text-muted">
|
||||
{% if has_dct %}Frequency only{% else %}N/A{% endif %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label class="mode-btn flex-fill {% if not has_dct %}opacity-50{% endif %}" id="dctModeCardDec" for="modeDctDec">
|
||||
<input class="form-check-input" type="radio" name="embed_mode" id="modeDctDec" value="dct" {% if not has_dct %}disabled{% endif %}>
|
||||
<i class="bi bi-soundwave text-warning"></i>
|
||||
<span class="ms-2"><strong>DCT</strong> <span class="text-muted d-none d-sm-inline">· Frequency</span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-text mt-2">
|
||||
<i class="bi bi-lightbulb me-1"></i>
|
||||
<strong>Auto</strong> tries LSB first, then DCT. Use specific mode if you know how it was encoded.
|
||||
<strong>Auto</strong> tries LSB first, then DCT.
|
||||
{% if not has_dct %}
|
||||
<br><span class="text-warning"><i class="bi bi-exclamation-triangle me-1"></i>DCT requires scipy: <code>pip install scipy</code></span>
|
||||
<span class="text-warning ms-2"><i class="bi bi-exclamation-triangle me-1"></i>DCT requires scipy</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,6 +483,10 @@
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning me-1"></i>
|
||||
<strong>Format compatibility:</strong> v4.0 cannot decode messages from v3.1 or earlier (different format)
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-broadcast text-info me-1"></i>
|
||||
<strong>Channel key:</strong> Use the same channel (Auto/Public/Custom) that was used during encoding
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-info-circle-fill text-info me-1"></i>
|
||||
If using an RSA key, verify the <strong>password is correct</strong> (if key is encrypted)
|
||||
@@ -461,34 +506,22 @@
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
|
||||
<script>
|
||||
// ============================================================================
|
||||
// DECODE PAGE - Initialize shared components
|
||||
// ============================================================================
|
||||
// Extraction mode button active state toggle
|
||||
const extractModeRadios = document.querySelectorAll('input[name="embed_mode"]');
|
||||
const extractModeBtns = {
|
||||
'auto': document.getElementById('autoModeCard'),
|
||||
'lsb': document.getElementById('lsbModeCardDec'),
|
||||
'dct': document.getElementById('dctModeCardDec')
|
||||
};
|
||||
|
||||
Stegasoo.initPasswordToggles();
|
||||
Stegasoo.initRsaMethodToggle();
|
||||
Stegasoo.initDropZones();
|
||||
Stegasoo.initClipboardPaste(['input[name="stego_image"]', 'input[name="reference_photo"]']);
|
||||
Stegasoo.initQrCropAnimation('rsaKeyQrInput');
|
||||
Stegasoo.initPassphraseFontResize();
|
||||
|
||||
// ============================================================================
|
||||
// DECODE PAGE - Mode card highlighting
|
||||
// ============================================================================
|
||||
|
||||
Stegasoo.initModeCards({
|
||||
radioName: 'embed_mode',
|
||||
cards: {
|
||||
'auto': { id: 'autoModeCard', borderClass: 'border-success' },
|
||||
'lsb': { id: 'lsbModeCardDec', borderClass: 'border-primary' },
|
||||
'dct': { id: 'dctModeCardDec', borderClass: 'border-info' }
|
||||
}
|
||||
extractModeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
Object.values(extractModeBtns).forEach(btn => btn?.classList.remove('active'));
|
||||
extractModeBtns[radio.value]?.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// DECODE PAGE - Advanced options chevron
|
||||
// ============================================================================
|
||||
|
||||
// Advanced options chevron
|
||||
const advancedOptionsDec = document.getElementById('advancedOptionsDec');
|
||||
advancedOptionsDec?.addEventListener('show.bs.collapse', () => {
|
||||
document.getElementById('advancedChevronDec')?.classList.replace('bi-chevron-down', 'bi-chevron-up');
|
||||
@@ -496,18 +529,5 @@ advancedOptionsDec?.addEventListener('show.bs.collapse', () => {
|
||||
advancedOptionsDec?.addEventListener('hide.bs.collapse', () => {
|
||||
document.getElementById('advancedChevronDec')?.classList.replace('bi-chevron-up', 'bi-chevron-down');
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// DECODE PAGE - Form submission with mode-specific loading text
|
||||
// ============================================================================
|
||||
|
||||
document.getElementById('decodeForm')?.addEventListener('submit', function() {
|
||||
const btn = document.getElementById('decodeBtn');
|
||||
const selectedMode = document.querySelector('input[name="embed_mode"]:checked')?.value || 'auto';
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Decoding (${selectedMode.toUpperCase()})...`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -333,17 +333,20 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
|
||||
<div class="input-group pin-input-container">
|
||||
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
|
||||
<button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
<div class="security-box">
|
||||
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
|
||||
<div class="input-group pin-input-container">
|
||||
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
|
||||
<button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">Static 6-9 digit PIN</div>
|
||||
</div>
|
||||
<div class="form-text">Static 6-9 digit PIN</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-3">
|
||||
<div class="security-box">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
||||
</label>
|
||||
@@ -400,9 +403,70 @@
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================
|
||||
CHANNEL KEY (v4.0.0) - Deployment/Group Isolation
|
||||
================================================================ -->
|
||||
<div class="mb-4">
|
||||
<div class="security-box">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-broadcast me-1"></i> Channel
|
||||
<span class="badge bg-info ms-1">v4.0</span>
|
||||
</label>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Auto Mode -->
|
||||
<label class="mode-btn flex-fill {% if channel_configured %}active{% endif %}" id="channelAutoCard" for="channelAuto">
|
||||
<input class="form-check-input" type="radio" name="channel_key" id="channelAuto" value="auto" checked>
|
||||
<i class="bi bi-gear-fill {% if channel_configured %}text-success{% else %}text-secondary{% endif %} ms-2"></i>
|
||||
<span class="ms-2"><strong>Auto</strong> <span class="text-muted d-none d-sm-inline">· {% if channel_configured %}Server Key{% else %}Public{% endif %}</span></span>
|
||||
</label>
|
||||
|
||||
<!-- Public Mode -->
|
||||
<label class="mode-btn flex-fill" id="channelPublicCard" for="channelPublic">
|
||||
<input class="form-check-input" type="radio" name="channel_key" id="channelPublic" value="none">
|
||||
<i class="bi bi-globe text-info ms-2"></i>
|
||||
<span class="ms-2"><strong>Public</strong> <span class="text-muted d-none d-sm-inline">· No key</span></span>
|
||||
</label>
|
||||
|
||||
<!-- Custom Key -->
|
||||
<label class="mode-btn flex-fill" id="channelCustomCard" for="channelCustom">
|
||||
<input class="form-check-input" type="radio" name="channel_key" id="channelCustom" value="custom">
|
||||
<i class="bi bi-key-fill text-warning ms-2"></i>
|
||||
<span class="ms-2"><strong>Custom</strong></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Server channel indicator (compact) -->
|
||||
{% if channel_configured %}
|
||||
<div class="small text-success mt-2" id="channelServerInfo">
|
||||
<i class="bi bi-shield-lock me-1"></i>
|
||||
Server: <code>{{ channel_fingerprint }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Custom key input -->
|
||||
<div class="mt-2 d-none" id="channelCustomInput">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="text" name="channel_key_custom" class="form-control font-monospace"
|
||||
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
||||
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}"
|
||||
id="channelKeyInput">
|
||||
<button class="btn btn-outline-secondary" type="button" id="channelKeyGenerate" title="Generate random key">
|
||||
<i class="bi bi-shuffle"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="invalid-feedback" id="channelKeyError">
|
||||
Invalid format. Use: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Options (DCT sub-options only) -->
|
||||
<div class="mb-4 {% if not has_dct %}d-none{% endif %}" id="advancedOptionsContainer">
|
||||
<a class="btn btn-sm btn-outline-secondary w-100" data-bs-toggle="collapse" href="#advancedOptions" role="button" aria-expanded="false">
|
||||
@@ -411,70 +475,43 @@
|
||||
</a>
|
||||
|
||||
<div class="collapse" id="advancedOptions">
|
||||
<div class="card card-body mt-2 bg-dark border-secondary">
|
||||
<div class="card card-body mt-2 bg-dark border-secondary py-3">
|
||||
|
||||
<div class="alert alert-info small mb-3">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
<strong>DCT defaults:</strong> Color mode + JPEG output for best social media compatibility.
|
||||
</div>
|
||||
|
||||
<!-- DCT Color Mode -->
|
||||
<!-- DCT Color Mode - Compact -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-palette me-1"></i> Color Mode
|
||||
<label class="form-label small mb-2">
|
||||
<i class="bi bi-palette me-1"></i> Color
|
||||
</label>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="form-check card p-2 text-center border-success border-2" id="dctColorCard">
|
||||
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked>
|
||||
<label class="form-check-label w-100" for="dctColorColor">
|
||||
<i class="bi bi-palette-fill text-success fs-5 d-block"></i>
|
||||
<strong>Color</strong>
|
||||
<div class="small text-muted">Recommended</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-check card p-2 text-center" id="dctGrayscaleCard">
|
||||
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
|
||||
<label class="form-check-label w-100" for="dctColorGrayscale">
|
||||
<i class="bi bi-circle-half text-secondary fs-5 d-block"></i>
|
||||
<strong>Grayscale</strong>
|
||||
<div class="small text-muted">B&W output</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<label class="mode-btn equal-width active" id="dctColorCard" for="dctColorColor">
|
||||
<input class="form-check-input" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked>
|
||||
<i class="bi bi-palette-fill text-success"></i>
|
||||
<span class="ms-2"><strong>Color</strong> <span class="badge bg-success ms-1">Default</span></span>
|
||||
</label>
|
||||
<label class="mode-btn equal-width" id="dctGrayscaleCard" for="dctColorGrayscale">
|
||||
<input class="form-check-input" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
|
||||
<i class="bi bi-circle-half text-secondary"></i>
|
||||
<span class="ms-2"><strong>Grayscale</strong></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DCT Output Format -->
|
||||
<!-- DCT Output Format - Compact -->
|
||||
<div class="mb-0">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-image me-1"></i> Output Format
|
||||
<label class="form-label small mb-2">
|
||||
<i class="bi bi-file-image me-1"></i> Format
|
||||
</label>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<div class="form-check card p-2 text-center" id="dctPngCard">
|
||||
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png">
|
||||
<label class="form-check-label w-100" for="dctFormatPng">
|
||||
<i class="bi bi-file-earmark-image text-primary fs-5 d-block"></i>
|
||||
<strong>PNG</strong>
|
||||
<div class="small text-muted">Lossless, larger</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-check card p-2 text-center border-warning border-2" id="dctJpegCard">
|
||||
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg" checked>
|
||||
<label class="form-check-label w-100" for="dctFormatJpeg">
|
||||
<i class="bi bi-file-earmark-richtext text-warning fs-5 d-block"></i>
|
||||
<strong>JPEG</strong>
|
||||
<div class="small text-muted">Recommended</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<label class="mode-btn equal-width active" id="dctJpegCard" for="dctFormatJpeg">
|
||||
<input class="form-check-input" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg" checked>
|
||||
<i class="bi bi-file-earmark-richtext text-warning"></i>
|
||||
<span class="ms-2"><strong>JPEG</strong> <span class="badge bg-warning text-dark ms-1">Default</span></span>
|
||||
</label>
|
||||
<label class="mode-btn equal-width" id="dctPngCard" for="dctFormatPng">
|
||||
<input class="form-check-input" type="radio" name="dct_output_format" id="dctFormatPng" value="png">
|
||||
<i class="bi bi-file-earmark-image text-primary"></i>
|
||||
<span class="ms-2"><strong>PNG</strong> <span class="text-muted d-none d-sm-inline">· Lossless</span></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -520,17 +557,6 @@
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/stegasoo.js') }}"></script>
|
||||
<script>
|
||||
// ============================================================================
|
||||
// ENCODE PAGE - Initialize shared components
|
||||
// ============================================================================
|
||||
|
||||
Stegasoo.initPasswordToggles();
|
||||
Stegasoo.initRsaMethodToggle();
|
||||
Stegasoo.initDropZones();
|
||||
Stegasoo.initClipboardPaste(['input[name="carrier"]', 'input[name="reference_photo"]']);
|
||||
Stegasoo.initQrCropAnimation('rsaQrInput');
|
||||
Stegasoo.initPassphraseFontResize();
|
||||
|
||||
// ============================================================================
|
||||
// ENCODE PAGE - Payload type switching
|
||||
// ============================================================================
|
||||
@@ -683,22 +709,26 @@ document.querySelectorAll('input[name="embed_mode"]').forEach(radio => {
|
||||
});
|
||||
});
|
||||
|
||||
// DCT format cards
|
||||
Stegasoo.initModeCards({
|
||||
radioName: 'dct_output_format',
|
||||
cards: {
|
||||
'png': { id: 'dctPngCard', borderClass: 'border-primary' },
|
||||
'jpeg': { id: 'dctJpegCard', borderClass: 'border-warning' }
|
||||
}
|
||||
// DCT color mode button active state toggle
|
||||
const colorModeRadios = document.querySelectorAll('input[name="dct_color_mode"]');
|
||||
const colorModeBtns = { 'color': document.getElementById('dctColorCard'), 'grayscale': document.getElementById('dctGrayscaleCard') };
|
||||
|
||||
colorModeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
Object.values(colorModeBtns).forEach(btn => btn?.classList.remove('active'));
|
||||
colorModeBtns[radio.value]?.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// DCT color cards
|
||||
Stegasoo.initModeCards({
|
||||
radioName: 'dct_color_mode',
|
||||
cards: {
|
||||
'color': { id: 'dctColorCard', borderClass: 'border-success' },
|
||||
'grayscale': { id: 'dctGrayscaleCard', borderClass: 'border-secondary' }
|
||||
}
|
||||
// DCT format button active state toggle
|
||||
const formatRadios = document.querySelectorAll('input[name="dct_output_format"]');
|
||||
const formatBtns = { 'png': document.getElementById('dctPngCard'), 'jpeg': document.getElementById('dctJpegCard') };
|
||||
|
||||
formatRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
Object.values(formatBtns).forEach(btn => btn?.classList.remove('active'));
|
||||
formatBtns[radio.value]?.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Advanced options chevron
|
||||
@@ -735,17 +765,5 @@ function checkDuplicateFiles() {
|
||||
|
||||
document.querySelector('input[name="reference_photo"]')?.addEventListener('change', checkDuplicateFiles);
|
||||
document.querySelector('input[name="carrier"]')?.addEventListener('change', checkDuplicateFiles);
|
||||
|
||||
// ============================================================================
|
||||
// ENCODE PAGE - Form submission
|
||||
// ============================================================================
|
||||
|
||||
document.getElementById('encodeForm')?.addEventListener('submit', function() {
|
||||
const btn = document.getElementById('encodeBtn');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -91,6 +91,24 @@
|
||||
</span>
|
||||
<div class="small text-muted mt-2">Full color PNG, spatial LSB embedding</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Channel info (v4.0.0) -->
|
||||
<div class="mt-3">
|
||||
{% if channel_mode == 'private' %}
|
||||
<span class="badge bg-warning text-dark fs-6">
|
||||
<i class="bi bi-shield-lock me-1"></i>Private Channel
|
||||
</span>
|
||||
{% if channel_fingerprint %}
|
||||
<div class="small text-muted mt-1">
|
||||
<code>{{ channel_fingerprint }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-info fs-6">
|
||||
<i class="bi bi-globe me-1"></i>Public Channel
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
@@ -123,6 +141,9 @@
|
||||
<li>Color preserved - extraction works on both color and grayscale</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if channel_mode == 'private' %}
|
||||
<li><i class="bi bi-shield-lock text-warning me-1"></i>Recipient needs the <strong>same channel key</strong> to decode</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}Generate Credentials - Stegasoo{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="row justify-content-center" data-page="generate">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@@ -74,6 +74,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Channel Key Generation (v4.0.0) -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-broadcast me-1"></i> Channel Key
|
||||
<span class="badge bg-info ms-1">v4.0</span>
|
||||
<a href="{{ url_for('about') }}#channel-keys" class="text-muted ms-2" title="Learn about channel keys">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</a>
|
||||
</label>
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="text" class="form-control font-monospace" id="channelKeyGenerated"
|
||||
placeholder="Click Generate" readonly>
|
||||
<button class="btn btn-outline-primary" type="button" id="generateChannelKeyBtn">
|
||||
<i class="bi bi-shuffle me-1"></i>Generate
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" type="button" id="copyChannelKeyBtn" disabled title="Copy to clipboard">
|
||||
<i class="bi bi-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">For private groups: generate, then use <strong>Custom</strong> mode when encoding/decoding.</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100 mt-3">
|
||||
<i class="bi bi-shuffle me-2"></i>Generate Credentials
|
||||
</button>
|
||||
|
||||
@@ -19,6 +19,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel Status Banner (v4.0.0) -->
|
||||
{% if channel_configured %}
|
||||
<div class="alert alert-success mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<i class="bi bi-shield-lock me-2"></i>
|
||||
<strong>Private Channel Active</strong>
|
||||
<span class="text-muted ms-2">Messages are isolated to this deployment</span>
|
||||
</div>
|
||||
<code class="small">{{ channel_fingerprint }}</code>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<!-- Encode Card -->
|
||||
<div class="col-md-4">
|
||||
@@ -81,22 +95,22 @@
|
||||
<div class="row text-center">
|
||||
<div class="col-md-6 mb-3 mb-md-0">
|
||||
<div class="p-3 bg-dark rounded h-100">
|
||||
<i class="bi bi-grid-3x3-gap text-primary fs-2 d-block mb-2"></i>
|
||||
<strong>LSB Mode</strong>
|
||||
<i class="bi bi-soundwave text-warning fs-2 d-block mb-2"></i>
|
||||
<strong>DCT Mode</strong>
|
||||
<span class="badge bg-success ms-1">Default</span>
|
||||
<div class="small text-muted mt-2">
|
||||
Higher capacity (~375 KB/MP)<br>
|
||||
Best for email & file transfer
|
||||
Survives JPEG recompression<br>
|
||||
Best for social media
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 bg-dark rounded h-100">
|
||||
<i class="bi bi-soundwave text-warning fs-2 d-block mb-2"></i>
|
||||
<strong>DCT Mode</strong>
|
||||
<i class="bi bi-grid-3x3-gap text-primary fs-2 d-block mb-2"></i>
|
||||
<strong>LSB Mode</strong>
|
||||
<div class="small text-muted mt-2">
|
||||
Survives JPEG recompression<br>
|
||||
Best for social media
|
||||
Higher capacity (~375 KB/MP)<br>
|
||||
Best for email & file transfer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,15 +130,15 @@
|
||||
<ul class="list-unstyled small">
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-image text-info me-2"></i>
|
||||
<strong>Reference Photo</strong> — shared secret image
|
||||
<strong>Reference Photo</strong>: shared secret
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-chat-quote text-info me-2"></i>
|
||||
<strong>Passphrase</strong> — 4+ words
|
||||
<strong>Passphrase</strong>: 4+ words
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-123 text-info me-2"></i>
|
||||
<strong>PIN</strong> — 6-9 digits (and/or RSA key)
|
||||
<strong>PIN</strong>: 6-9 digits (or RSA key)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -143,6 +157,11 @@
|
||||
<i class="bi bi-shuffle text-success me-2"></i>
|
||||
Pseudo-random embedding
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<i class="bi bi-broadcast text-success me-2"></i>
|
||||
<strong>Channel keys</strong> for group isolation
|
||||
<span class="badge bg-info ms-1">v4.0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user