Bump version to 4.0.1 with Web UI improvements
- Update version to 4.0.1 across constants.py, __init__.py, pyproject.toml, README - Refactor channel key UI from radio buttons to select dropdown - Add LED indicator and key capsule CSS styles - Reorganize encode/decode forms: RSA key section moved up, PIN + Channel in row - Streamline channel key JavaScript for dropdown-based selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ A secure steganography system for hiding encrypted messages in images using hybr
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -751,57 +751,41 @@ const Stegasoo = {
|
||||
/**
|
||||
* 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.selectId - ID of channel select dropdown
|
||||
* @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 selectId = config.selectId || 'channelSelect';
|
||||
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 select = document.getElementById(selectId);
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const updateVisibility = () => {
|
||||
const isCustom = select?.value === 'custom';
|
||||
customInput?.classList.toggle('d-none', !isCustom);
|
||||
if (isCustom && keyInput) {
|
||||
keyInput.focus();
|
||||
}
|
||||
};
|
||||
|
||||
select?.addEventListener('change', updateVisibility);
|
||||
|
||||
// Initial state
|
||||
updateActiveState();
|
||||
|
||||
updateVisibility();
|
||||
|
||||
// Format and validate key input
|
||||
keyInput?.addEventListener('input', () => {
|
||||
this.formatChannelKeyInput(keyInput);
|
||||
});
|
||||
|
||||
|
||||
// Generate button (if present)
|
||||
generateBtn?.addEventListener('click', () => {
|
||||
if (keyInput) {
|
||||
@@ -810,26 +794,26 @@ const Stegasoo = {
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Handle form submission with channel key validation
|
||||
* @param {HTMLFormElement} form - Form element
|
||||
* @param {string} customRadioId - ID of custom radio button
|
||||
* @param {string} selectId - ID of channel select dropdown
|
||||
* @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);
|
||||
validateChannelKeyOnSubmit(form, selectId, keyInputId) {
|
||||
const select = document.getElementById(selectId);
|
||||
const keyInput = document.getElementById(keyInputId);
|
||||
|
||||
if (customRadio?.checked && keyInput) {
|
||||
|
||||
if (select?.value === 'custom' && 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;
|
||||
// Set the select value to the actual key for form submission
|
||||
select.value = keyInput.value;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -880,20 +864,19 @@ const Stegasoo = {
|
||||
this.initCollapseChevrons();
|
||||
this.initPassphraseFontResize();
|
||||
|
||||
// Channel key (v4.0.0) - uses mode-btn style
|
||||
// Channel key (v4.0.0) - uses select dropdown
|
||||
this.initChannelKey({
|
||||
selectId: 'channelSelect',
|
||||
customInputId: 'channelCustomInput',
|
||||
keyInputId: 'channelKeyInput',
|
||||
generateBtnId: 'channelKeyGenerate',
|
||||
customRadioId: 'channelCustom',
|
||||
cardIds: ['channelAutoCard', 'channelPublicCard', 'channelCustomCard']
|
||||
generateBtnId: 'channelKeyGenerate'
|
||||
});
|
||||
|
||||
|
||||
// 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')) {
|
||||
if (!this.validateChannelKeyOnSubmit(form, 'channelSelect', 'channelKeyInput')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
@@ -913,19 +896,18 @@ const Stegasoo = {
|
||||
this.initCollapseChevrons();
|
||||
this.initPassphraseFontResize();
|
||||
|
||||
// Channel key (v4.0.0) - uses mode-btn style
|
||||
// Channel key (v4.0.0) - uses select dropdown
|
||||
this.initChannelKey({
|
||||
selectId: 'channelSelectDec',
|
||||
customInputId: 'channelCustomInputDec',
|
||||
keyInputId: 'channelKeyInputDec',
|
||||
customRadioId: 'channelCustomDec',
|
||||
cardIds: ['channelAutoCardDec', 'channelPublicCardDec', 'channelCustomCardDec']
|
||||
keyInputId: 'channelKeyInputDec'
|
||||
});
|
||||
|
||||
|
||||
// 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')) {
|
||||
if (!this.validateChannelKeyOnSubmit(form, 'channelSelectDec', 'channelKeyInputDec')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1323,3 +1323,58 @@ footer {
|
||||
font-weight: 600;
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
LED Indicator
|
||||
---------------------------------------------------------------------------- */
|
||||
.led-indicator {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.led-yellow {
|
||||
background: #fbbf24;
|
||||
box-shadow: 0 0 3px #fbbf24, 0 0 6px rgba(251, 191, 36, 0.4);
|
||||
}
|
||||
|
||||
.led-green {
|
||||
background: #22c55e;
|
||||
box-shadow: 0 0 3px #22c55e, 0 0 6px rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.led-red {
|
||||
background: #ef4444;
|
||||
box-shadow: 0 0 3px #ef4444, 0 0 6px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
/* LED Badge backgrounds */
|
||||
.led-badge-yellow {
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
border: 1px solid rgba(251, 191, 36, 0.4);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.led-badge-green {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
border: 1px solid rgba(34, 197, 94, 0.4);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.led-badge-red {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Key capsule container */
|
||||
.key-capsule {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@@ -264,9 +264,71 @@
|
||||
<span class="text-warning small">(provide same factors used during encoding)</span>
|
||||
</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="security-box">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
||||
</label>
|
||||
|
||||
<!-- RSA Input Method Toggle -->
|
||||
<div class="btn-group w-100 mb-2" role="group">
|
||||
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodFile" value="file" checked>
|
||||
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodFile">
|
||||
<i class="bi bi-file-earmark me-1"></i>.pem File
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodQr" value="qr">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodQr">
|
||||
<i class="bi bi-qr-code me-1"></i>QR Code
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- .pem File Input -->
|
||||
<div id="rsaFileSection">
|
||||
<input type="file" name="rsa_key" class="form-control form-control-sm" id="rsaKeyInput" accept=".pem,.key,application/x-pem-file">
|
||||
</div>
|
||||
|
||||
<!-- QR Code Input -->
|
||||
<div id="rsaQrSection" class="d-none">
|
||||
<div class="drop-zone p-3" id="qrDropZone">
|
||||
<input type="file" name="rsa_key_qr" accept="image/*" id="rsaKeyQrInput">
|
||||
<div class="drop-zone-label text-center">
|
||||
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
|
||||
<span class="text-muted small">Drop QR image or click to browse</span>
|
||||
</div>
|
||||
<!-- Crop animation container -->
|
||||
<div class="qr-scan-container qr-crop-container d-none" id="qrCropContainer">
|
||||
<img class="qr-original" id="qrOriginal" alt="Original">
|
||||
<img class="qr-cropped" id="qrCropped" alt="Cropped QR">
|
||||
<!-- Data panel -->
|
||||
<div class="qr-data-panel">
|
||||
<div class="qr-data-filename">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<span>RSA Key loaded</span>
|
||||
</div>
|
||||
<div class="qr-data-row">
|
||||
<span class="qr-status-badge">RSA Key</span>
|
||||
<span class="qr-data-value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Password (always visible) -->
|
||||
<div class="input-group input-group-sm mt-2">
|
||||
<input type="password" name="rsa_password" class="form-control" id="rsaPasswordInput" placeholder="Key password (if encrypted)">
|
||||
<button class="btn btn-outline-secondary" type="button" data-toggle-password="rsaPasswordInput">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PIN + Channel Row -->
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="security-box">
|
||||
<div class="security-box h-100">
|
||||
<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;">
|
||||
@@ -277,121 +339,43 @@
|
||||
<div class="form-text">If PIN was used during encoding</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-8 mb-3">
|
||||
<div class="security-box">
|
||||
<div class="security-box h-100">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
||||
<i class="bi bi-broadcast me-1"></i> Channel
|
||||
<span class="badge bg-info ms-1">v4.0</span>
|
||||
<a href="/about#channel-keys" class="text-muted ms-1" title="Learn about channels"><i class="bi bi-info-circle"></i></a>
|
||||
</label>
|
||||
|
||||
<!-- RSA Input Method Toggle -->
|
||||
<div class="btn-group w-100 mb-2" role="group">
|
||||
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodFile" value="file" checked>
|
||||
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodFile">
|
||||
<i class="bi bi-file-earmark me-1"></i>.pem File
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodQr" value="qr">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodQr">
|
||||
<i class="bi bi-qr-code me-1"></i>QR Code
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- .pem File Input -->
|
||||
<div id="rsaFileSection">
|
||||
<input type="file" name="rsa_key" class="form-control form-control-sm" id="rsaKeyInput" accept=".pem,.key,application/x-pem-file">
|
||||
</div>
|
||||
|
||||
<!-- QR Code Input -->
|
||||
<div id="rsaQrSection" class="d-none">
|
||||
<div class="drop-zone p-3" id="qrDropZone">
|
||||
<input type="file" name="rsa_key_qr" accept="image/*" id="rsaKeyQrInput">
|
||||
<div class="drop-zone-label text-center">
|
||||
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
|
||||
<span class="text-muted small">Drop QR image or click to browse</span>
|
||||
</div>
|
||||
<!-- Crop animation container -->
|
||||
<div class="qr-scan-container qr-crop-container d-none" id="qrCropContainer">
|
||||
<img class="qr-original" id="qrOriginal" alt="Original">
|
||||
<img class="qr-cropped" id="qrCropped" alt="Cropped QR">
|
||||
<!-- Data panel -->
|
||||
<div class="qr-data-panel">
|
||||
<div class="qr-data-filename">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<span>RSA Key loaded</span>
|
||||
</div>
|
||||
<div class="qr-data-row">
|
||||
<span class="qr-status-badge">RSA Key</span>
|
||||
<span class="qr-data-value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Password (always visible) -->
|
||||
<div class="input-group input-group-sm mt-2">
|
||||
<input type="password" name="rsa_password" class="form-control" id="rsaPasswordInput" placeholder="Key password (if encrypted)">
|
||||
<button class="btn btn-outline-secondary" type="button" data-toggle-password="rsaPasswordInput">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
|
||||
<select class="form-select" name="channel_key" id="channelSelectDec">
|
||||
<option value="auto" selected>Auto{% if channel_configured %} (Server Key){% endif %}</option>
|
||||
<option value="none">Public</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
|
||||
<!-- 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[:4] }}-••••-···-••••-{{ channel_fingerprint[-4:] }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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"
|
||||
|
||||
<!-- Custom Channel Key Input (shown when Custom selected) -->
|
||||
<div class="mb-4 d-none" id="channelCustomInputDec">
|
||||
<div class="security-box">
|
||||
<label class="form-label"><i class="bi bi-key me-1"></i> Custom Channel Key</label>
|
||||
<div class="input-group">
|
||||
<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>
|
||||
|
||||
<!-- ================================================================
|
||||
|
||||
@@ -331,9 +331,71 @@
|
||||
<span class="text-warning small">(provide at least one: PIN or RSA Key)</span>
|
||||
</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="security-box">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
||||
</label>
|
||||
|
||||
<!-- RSA Input Method Toggle -->
|
||||
<div class="btn-group w-100 mb-2" role="group">
|
||||
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodFile" value="file" checked>
|
||||
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodFile">
|
||||
<i class="bi bi-file-earmark me-1"></i>.pem File
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodQr" value="qr">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodQr">
|
||||
<i class="bi bi-qr-code me-1"></i>QR Code
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- .pem File Input -->
|
||||
<div id="rsaFileSection">
|
||||
<input type="file" name="rsa_key" class="form-control form-control-sm" accept=".pem">
|
||||
</div>
|
||||
|
||||
<!-- QR Code Input -->
|
||||
<div id="rsaQrSection" class="d-none">
|
||||
<div class="drop-zone p-3" id="qrDropZone">
|
||||
<input type="file" name="rsa_key_qr" accept="image/*" id="rsaQrInput">
|
||||
<div class="drop-zone-label text-center">
|
||||
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
|
||||
<span class="text-muted small">Drop QR image or click to browse</span>
|
||||
</div>
|
||||
<!-- Crop animation container -->
|
||||
<div class="qr-scan-container qr-crop-container d-none" id="qrCropContainer">
|
||||
<img class="qr-original" id="qrOriginal" alt="Original">
|
||||
<img class="qr-cropped" id="qrCropped" alt="Cropped QR">
|
||||
<!-- Data panel -->
|
||||
<div class="qr-data-panel">
|
||||
<div class="qr-data-filename">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<span>RSA Key loaded</span>
|
||||
</div>
|
||||
<div class="qr-data-row">
|
||||
<span class="qr-status-badge">RSA Key</span>
|
||||
<span class="qr-data-value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Password (always visible) -->
|
||||
<div class="input-group input-group-sm mt-2">
|
||||
<input type="password" name="rsa_password" class="form-control" id="rsaPasswordInput" placeholder="Key password (if encrypted)">
|
||||
<button class="btn btn-outline-secondary" type="button" data-toggle-password="rsaPasswordInput">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PIN + Channel Row -->
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="security-box">
|
||||
<div class="security-box h-100">
|
||||
<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;">
|
||||
@@ -344,116 +406,39 @@
|
||||
<div class="form-text">Static 6-9 digit PIN</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-8 mb-3">
|
||||
<div class="security-box">
|
||||
<div class="security-box h-100">
|
||||
<label class="form-label">
|
||||
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
||||
<i class="bi bi-broadcast me-1"></i> Channel
|
||||
<span class="badge bg-info ms-1">v4.0</span>
|
||||
<a href="/about#channel-keys" class="text-muted ms-1" title="Learn about channels"><i class="bi bi-info-circle"></i></a>
|
||||
</label>
|
||||
|
||||
<!-- RSA Input Method Toggle -->
|
||||
<div class="btn-group w-100 mb-2" role="group">
|
||||
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodFile" value="file" checked>
|
||||
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodFile">
|
||||
<i class="bi bi-file-earmark me-1"></i>.pem File
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodQr" value="qr">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodQr">
|
||||
<i class="bi bi-qr-code me-1"></i>QR Code
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- .pem File Input -->
|
||||
<div id="rsaFileSection">
|
||||
<input type="file" name="rsa_key" class="form-control form-control-sm" accept=".pem">
|
||||
</div>
|
||||
|
||||
<!-- QR Code Input -->
|
||||
<div id="rsaQrSection" class="d-none">
|
||||
<div class="drop-zone p-3" id="qrDropZone">
|
||||
<input type="file" name="rsa_key_qr" accept="image/*" id="rsaQrInput">
|
||||
<div class="drop-zone-label text-center">
|
||||
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
|
||||
<span class="text-muted small">Drop QR image or click to browse</span>
|
||||
</div>
|
||||
<!-- Crop animation container -->
|
||||
<div class="qr-scan-container qr-crop-container d-none" id="qrCropContainer">
|
||||
<img class="qr-original" id="qrOriginal" alt="Original">
|
||||
<img class="qr-cropped" id="qrCropped" alt="Cropped QR">
|
||||
<!-- Data panel -->
|
||||
<div class="qr-data-panel">
|
||||
<div class="qr-data-filename">
|
||||
<i class="bi bi-check-circle-fill"></i>
|
||||
<span>RSA Key loaded</span>
|
||||
</div>
|
||||
<div class="qr-data-row">
|
||||
<span class="qr-status-badge">RSA Key</span>
|
||||
<span class="qr-data-value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Password (always visible) -->
|
||||
<div class="input-group input-group-sm mt-2">
|
||||
<input type="password" name="rsa_password" class="form-control" id="rsaPasswordInput" placeholder="Key password (if encrypted)">
|
||||
<button class="btn btn-outline-secondary" type="button" data-toggle-password="rsaPasswordInput">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
|
||||
<select class="form-select" name="channel_key" id="channelSelect">
|
||||
<option value="auto" selected>Auto{% if channel_configured %} (Server Key){% endif %}</option>
|
||||
<option value="none">Public</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
|
||||
<!-- 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[:4] }}-••••-···-••••-{{ channel_fingerprint[-4:] }}</code>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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"
|
||||
|
||||
<!-- Custom Channel Key Input (shown when Custom selected) -->
|
||||
<div class="mb-4 d-none" id="channelCustomInput">
|
||||
<div class="security-box">
|
||||
<label class="form-label"><i class="bi bi-key me-1"></i> Custom Channel Key</label>
|
||||
<div class="input-group">
|
||||
<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">
|
||||
@@ -464,7 +449,6 @@
|
||||
Invalid format. Use: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Options (DCT sub-options only) -->
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
<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>
|
||||
<strong>Private Channel Mode</strong>
|
||||
</div>
|
||||
<div class="key-capsule">
|
||||
<span class="badge led-badge-yellow"><span class="led-indicator led-yellow me-1"></span>Key Loaded</span>
|
||||
<code class="small ms-2">{{ channel_fingerprint }}</code>
|
||||
</div>
|
||||
<code class="small">{{ channel_fingerprint }}</code>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "stegasoo"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1"
|
||||
description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Stegasoo - Secure Steganography with Multi-Factor Authentication (v4.0.0)
|
||||
Stegasoo - Secure Steganography with Multi-Factor Authentication (v4.0.1)
|
||||
|
||||
Changes in v4.0.0:
|
||||
- Added channel key support for deployment/group isolation
|
||||
@@ -7,11 +7,11 @@ Changes in v4.0.0:
|
||||
- encode() and decode() now accept channel_key parameter
|
||||
"""
|
||||
|
||||
__version__ = "4.0.0"
|
||||
__version__ = "4.0.1"
|
||||
|
||||
# Core functionality
|
||||
from .encode import encode
|
||||
from .decode import decode, decode_file
|
||||
from .decode import decode, decode_file, decode_text
|
||||
|
||||
# Credential generation
|
||||
from .generate import (
|
||||
@@ -153,11 +153,12 @@ DCT_BYTES_PER_PIXEL = 0.125
|
||||
__all__ = [
|
||||
# Version
|
||||
"__version__",
|
||||
|
||||
|
||||
# Core
|
||||
"encode",
|
||||
"decode",
|
||||
"decode_file",
|
||||
"decode_text",
|
||||
|
||||
# Generation
|
||||
"generate_pin",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Stegasoo Constants and Configuration (v4.0.0 - Channel Key Support)
|
||||
Stegasoo Constants and Configuration (v4.0.1 - Channel Key Support)
|
||||
|
||||
Central location for all magic numbers, limits, and crypto parameters.
|
||||
All version numbers, limits, and configuration values should be defined here.
|
||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
||||
# VERSION
|
||||
# ============================================================================
|
||||
|
||||
__version__ = "4.0.0"
|
||||
__version__ = "4.0.1"
|
||||
|
||||
# ============================================================================
|
||||
# FILE FORMAT
|
||||
|
||||
@@ -624,12 +624,15 @@ def get_active_channel_key() -> Optional[str]:
|
||||
return get_channel_key()
|
||||
|
||||
|
||||
def get_channel_fingerprint() -> Optional[str]:
|
||||
def get_channel_fingerprint(key: Optional[str] = None) -> Optional[str]:
|
||||
"""
|
||||
Get a display-safe fingerprint of the configured channel key.
|
||||
|
||||
Get a display-safe fingerprint of a channel key.
|
||||
|
||||
Args:
|
||||
key: Channel key (if None, uses configured key)
|
||||
|
||||
Returns:
|
||||
Masked key like "ABCD-••••-••••-••••-••••-••••-••••-3456" or None
|
||||
"""
|
||||
from .channel import get_channel_fingerprint as _get_fingerprint
|
||||
return _get_fingerprint()
|
||||
return _get_fingerprint(key)
|
||||
|
||||
@@ -56,23 +56,24 @@ EXT_TO_FORMAT = {
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# OVERHEAD CONSTANTS (v3.2.0 - Updated for date-independent format)
|
||||
# OVERHEAD CONSTANTS (v4.0.0 - Updated for channel key support)
|
||||
# =============================================================================
|
||||
# v3.2.0 Header format (no date field):
|
||||
# v4.0.0 Header format (with flags byte for channel key indicator):
|
||||
# Magic: 4 bytes (\x89ST3)
|
||||
# Version: 1 byte (4 for v3.2.0)
|
||||
# Version: 1 byte (5 for v4.0.0)
|
||||
# Flags: 1 byte (bit 0 = has channel key)
|
||||
# Salt: 32 bytes
|
||||
# IV: 12 bytes
|
||||
# Tag: 16 bytes
|
||||
# -----------------
|
||||
# Total: 65 bytes
|
||||
# Total: 66 bytes
|
||||
#
|
||||
# Previous v3.1.0 had date field (10 bytes + 1 byte length) = 76 bytes header
|
||||
# The old value of 104 was incorrect even for v3.1.0
|
||||
# v3.2.0 had 65 bytes (no flags byte)
|
||||
# v3.1.0 had date field (10 bytes + 1 byte length) = 76 bytes header
|
||||
|
||||
HEADER_OVERHEAD = 65 # v3.2.0: Magic + version + salt + iv + tag
|
||||
HEADER_OVERHEAD = 66 # v4.0.0: Magic + version + flags + salt + iv + tag
|
||||
LENGTH_PREFIX = 4 # 4 bytes for payload length in LSB embedding
|
||||
ENCRYPTION_OVERHEAD = HEADER_OVERHEAD + LENGTH_PREFIX # 69 bytes total
|
||||
ENCRYPTION_OVERHEAD = HEADER_OVERHEAD + LENGTH_PREFIX # 70 bytes total
|
||||
|
||||
# DCT output format options (v3.0.1)
|
||||
DCT_OUTPUT_PNG = 'png'
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""
|
||||
Stegasoo Tests (v4.0.0)
|
||||
|
||||
Tests for key generation, validation, encoding/decoding, and output formats.
|
||||
Tests for key generation, validation, encoding/decoding, output formats,
|
||||
and channel key functionality.
|
||||
|
||||
Updated for v4.0.0:
|
||||
- Same API as v3.2.0 (passphrase, no date_str)
|
||||
- JPEG normalization for jpegio compatibility
|
||||
- Channel key support for deployment/group isolation
|
||||
- HEADER_OVERHEAD increased to 66 bytes (flags byte added)
|
||||
- Python 3.12 recommended (3.13 not supported)
|
||||
"""
|
||||
|
||||
@@ -16,17 +18,20 @@ import io
|
||||
import stegasoo
|
||||
from stegasoo import (
|
||||
generate_pin,
|
||||
generate_phrase,
|
||||
generate_passphrase,
|
||||
generate_credentials,
|
||||
validate_pin,
|
||||
validate_message,
|
||||
validate_passphrase,
|
||||
validate_channel_key,
|
||||
encode,
|
||||
decode,
|
||||
decode_text,
|
||||
generate_channel_key,
|
||||
get_channel_fingerprint,
|
||||
__version__,
|
||||
)
|
||||
from stegasoo.steganography import get_output_format, HEADER_OVERHEAD
|
||||
from stegasoo.steganography import get_output_format
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -104,16 +109,16 @@ class TestKeygen:
|
||||
assert len(pin) == length
|
||||
assert pin.isdigit()
|
||||
|
||||
def test_generate_phrase_default(self):
|
||||
"""Default phrase should have 4 words (v3.2.0 change)."""
|
||||
phrase = generate_phrase()
|
||||
def test_generate_passphrase_default(self):
|
||||
"""Default passphrase should have 4 words (v3.2.0 change)."""
|
||||
phrase = generate_passphrase()
|
||||
words = phrase.split()
|
||||
assert len(words) == 4 # Changed from 3 in v3.1.x
|
||||
|
||||
def test_generate_phrase_custom_length(self):
|
||||
"""Phrase generation should work for custom lengths."""
|
||||
def test_generate_passphrase_custom_length(self):
|
||||
"""Passphrase generation should work for custom lengths."""
|
||||
for length in [3, 4, 5, 6, 8, 12]:
|
||||
phrase = generate_phrase(length)
|
||||
phrase = generate_passphrase(length)
|
||||
words = phrase.split()
|
||||
assert len(words) == length
|
||||
|
||||
@@ -287,19 +292,20 @@ class TestOutputFormat:
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Header Overhead Test (v3.2.0)
|
||||
# Header Overhead Test (v4.0.0)
|
||||
# =============================================================================
|
||||
|
||||
class TestConstants:
|
||||
"""Tests for constants and configuration."""
|
||||
|
||||
|
||||
def test_header_overhead_value(self):
|
||||
"""Header overhead should be 65 bytes (v3.2.0 fix)."""
|
||||
assert HEADER_OVERHEAD == 65
|
||||
"""Header overhead should be 66 bytes (v4.0.0: added flags byte)."""
|
||||
from stegasoo.steganography import HEADER_OVERHEAD
|
||||
assert HEADER_OVERHEAD == 66
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Encode/Decode Tests (v3.2.0 Updated)
|
||||
# Encode/Decode Tests (v4.0.0 Updated)
|
||||
# =============================================================================
|
||||
|
||||
class TestEncodeDecode:
|
||||
@@ -474,8 +480,8 @@ class TestEncodeDecode:
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_filename_has_no_date(self, png_image):
|
||||
"""v3.2.0: Output filename should not have date suffix."""
|
||||
def test_filename_format(self, png_image):
|
||||
"""Output filename should have random hex and date suffix."""
|
||||
result = encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
@@ -483,10 +489,10 @@ class TestEncodeDecode:
|
||||
passphrase="test phrase here now",
|
||||
pin="123456"
|
||||
)
|
||||
# Filename should be like "a1b2c3d4.png", not "a1b2c3d4_20251227.png"
|
||||
# Check that there's no underscore followed by 8 digits
|
||||
# Filename format: {random_hex}_{YYYYMMDD}.{ext}
|
||||
# e.g., "a1b2c3d4_20251227.png"
|
||||
import re
|
||||
assert not re.search(r'_\d{8}\.', result.filename)
|
||||
assert re.search(r'^[a-f0-9]{8}_\d{8}\.png$', result.filename)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -605,3 +611,155 @@ class TestBackwardCompatibility:
|
||||
pin="123456",
|
||||
date_str="2025-01-01" # Removed parameter
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Channel Key Tests (v4.0.0)
|
||||
# =============================================================================
|
||||
|
||||
class TestChannelKey:
|
||||
"""Tests for channel key functionality (v4.0.0)."""
|
||||
|
||||
def test_generate_channel_key_format(self):
|
||||
"""Generated channel key should have correct format."""
|
||||
key = generate_channel_key()
|
||||
# Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX (8 groups of 4)
|
||||
assert len(key) == 39
|
||||
parts = key.split('-')
|
||||
assert len(parts) == 8
|
||||
for part in parts:
|
||||
assert len(part) == 4
|
||||
assert part.isalnum()
|
||||
|
||||
def test_validate_channel_key_valid(self):
|
||||
"""Valid channel key should pass validation."""
|
||||
key = generate_channel_key()
|
||||
assert validate_channel_key(key)
|
||||
|
||||
def test_validate_channel_key_invalid(self):
|
||||
"""Invalid channel key should fail validation."""
|
||||
assert not validate_channel_key("")
|
||||
assert not validate_channel_key("invalid")
|
||||
assert not validate_channel_key("ABCD-1234") # Too short
|
||||
|
||||
def test_channel_fingerprint_format(self):
|
||||
"""Channel fingerprint should mask middle sections."""
|
||||
key = "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456"
|
||||
fingerprint = get_channel_fingerprint(key)
|
||||
assert fingerprint is not None
|
||||
# First and last groups visible, middle masked
|
||||
assert fingerprint.startswith("ABCD-")
|
||||
assert fingerprint.endswith("-3456")
|
||||
assert "••••" in fingerprint
|
||||
|
||||
def test_encode_decode_with_channel_key(self, png_image):
|
||||
"""Encode/decode should work with explicit channel key."""
|
||||
message = "Secret with channel key!"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
channel_key = generate_channel_key()
|
||||
|
||||
# Encode with channel key
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key
|
||||
)
|
||||
|
||||
assert result.stego_image is not None
|
||||
|
||||
# Decode with same channel key
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_decode_wrong_channel_key_fails(self, png_image):
|
||||
"""Decoding with wrong channel key should fail."""
|
||||
message = "Secret message"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
channel_key1 = generate_channel_key()
|
||||
channel_key2 = generate_channel_key()
|
||||
|
||||
# Encode with one channel key
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key1
|
||||
)
|
||||
|
||||
# Decode with different channel key should fail
|
||||
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
|
||||
decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key2
|
||||
)
|
||||
|
||||
def test_encode_decode_public_mode(self, png_image):
|
||||
"""Encode/decode should work without channel key (public mode)."""
|
||||
message = "Public message!"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
|
||||
# Encode without channel key (explicit public mode)
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key="" # Explicit public mode
|
||||
)
|
||||
|
||||
# Decode without channel key
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key="" # Explicit public mode
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_channel_key_mismatch_public_vs_private(self, png_image):
|
||||
"""Decoding public message with channel key should fail."""
|
||||
message = "Public message"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
|
||||
# Encode without channel key (public)
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key="" # Public mode
|
||||
)
|
||||
|
||||
# Decode with channel key should fail
|
||||
channel_key = generate_channel_key()
|
||||
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
|
||||
decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user