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
|
## Features
|
||||||
|
|
||||||
|
|||||||
@@ -751,51 +751,35 @@ const Stegasoo = {
|
|||||||
/**
|
/**
|
||||||
* Initialize channel key UI for encode/decode pages
|
* Initialize channel key UI for encode/decode pages
|
||||||
* @param {Object} config - Configuration object
|
* @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.customInputId - ID of custom key input container
|
||||||
* @param {string} config.keyInputId - ID of key input field
|
* @param {string} config.keyInputId - ID of key input field
|
||||||
* @param {string} config.generateBtnId - ID of generate button (optional)
|
* @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 = {}) {
|
initChannelKey(config = {}) {
|
||||||
const radioName = config.radioName || 'channel_key';
|
const selectId = config.selectId || 'channelSelect';
|
||||||
const customInputId = config.customInputId || 'channelCustomInput';
|
const customInputId = config.customInputId || 'channelCustomInput';
|
||||||
const keyInputId = config.keyInputId || 'channelKeyInput';
|
const keyInputId = config.keyInputId || 'channelKeyInput';
|
||||||
const generateBtnId = config.generateBtnId;
|
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 customInput = document.getElementById(customInputId);
|
||||||
const keyInput = document.getElementById(keyInputId);
|
const keyInput = document.getElementById(keyInputId);
|
||||||
const generateBtn = generateBtnId ? document.getElementById(generateBtnId) : null;
|
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
|
// Show/hide custom input based on selection
|
||||||
radios.forEach(radio => {
|
const updateVisibility = () => {
|
||||||
radio.addEventListener('change', () => {
|
const isCustom = select?.value === 'custom';
|
||||||
updateActiveState();
|
|
||||||
const isCustom = customRadio?.checked;
|
|
||||||
customInput?.classList.toggle('d-none', !isCustom);
|
customInput?.classList.toggle('d-none', !isCustom);
|
||||||
if (isCustom && keyInput) {
|
if (isCustom && keyInput) {
|
||||||
keyInput.focus();
|
keyInput.focus();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
});
|
|
||||||
|
select?.addEventListener('change', updateVisibility);
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
updateActiveState();
|
updateVisibility();
|
||||||
|
|
||||||
// Format and validate key input
|
// Format and validate key input
|
||||||
keyInput?.addEventListener('input', () => {
|
keyInput?.addEventListener('input', () => {
|
||||||
@@ -814,22 +798,22 @@ const Stegasoo = {
|
|||||||
/**
|
/**
|
||||||
* Handle form submission with channel key validation
|
* Handle form submission with channel key validation
|
||||||
* @param {HTMLFormElement} form - Form element
|
* @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
|
* @param {string} keyInputId - ID of key input field
|
||||||
* @returns {boolean} True if valid, false to prevent submission
|
* @returns {boolean} True if valid, false to prevent submission
|
||||||
*/
|
*/
|
||||||
validateChannelKeyOnSubmit(form, customRadioId, keyInputId) {
|
validateChannelKeyOnSubmit(form, selectId, keyInputId) {
|
||||||
const customRadio = document.getElementById(customRadioId);
|
const select = document.getElementById(selectId);
|
||||||
const keyInput = document.getElementById(keyInputId);
|
const keyInput = document.getElementById(keyInputId);
|
||||||
|
|
||||||
if (customRadio?.checked && keyInput) {
|
if (select?.value === 'custom' && keyInput) {
|
||||||
if (!this.validateChannelKey(keyInput.value)) {
|
if (!this.validateChannelKey(keyInput.value)) {
|
||||||
keyInput.classList.add('is-invalid');
|
keyInput.classList.add('is-invalid');
|
||||||
keyInput.focus();
|
keyInput.focus();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Set the radio value to the actual key for form submission
|
// Set the select value to the actual key for form submission
|
||||||
customRadio.value = keyInput.value;
|
select.value = keyInput.value;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@@ -880,20 +864,19 @@ const Stegasoo = {
|
|||||||
this.initCollapseChevrons();
|
this.initCollapseChevrons();
|
||||||
this.initPassphraseFontResize();
|
this.initPassphraseFontResize();
|
||||||
|
|
||||||
// Channel key (v4.0.0) - uses mode-btn style
|
// Channel key (v4.0.0) - uses select dropdown
|
||||||
this.initChannelKey({
|
this.initChannelKey({
|
||||||
|
selectId: 'channelSelect',
|
||||||
customInputId: 'channelCustomInput',
|
customInputId: 'channelCustomInput',
|
||||||
keyInputId: 'channelKeyInput',
|
keyInputId: 'channelKeyInput',
|
||||||
generateBtnId: 'channelKeyGenerate',
|
generateBtnId: 'channelKeyGenerate'
|
||||||
customRadioId: 'channelCustom',
|
|
||||||
cardIds: ['channelAutoCard', 'channelPublicCard', 'channelCustomCard']
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form submission with channel key validation
|
// Form submission with channel key validation
|
||||||
const form = document.getElementById('encodeForm');
|
const form = document.getElementById('encodeForm');
|
||||||
const btn = document.getElementById('encodeBtn');
|
const btn = document.getElementById('encodeBtn');
|
||||||
form?.addEventListener('submit', (e) => {
|
form?.addEventListener('submit', (e) => {
|
||||||
if (!this.validateChannelKeyOnSubmit(form, 'channelCustom', 'channelKeyInput')) {
|
if (!this.validateChannelKeyOnSubmit(form, 'channelSelect', 'channelKeyInput')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -913,19 +896,18 @@ const Stegasoo = {
|
|||||||
this.initCollapseChevrons();
|
this.initCollapseChevrons();
|
||||||
this.initPassphraseFontResize();
|
this.initPassphraseFontResize();
|
||||||
|
|
||||||
// Channel key (v4.0.0) - uses mode-btn style
|
// Channel key (v4.0.0) - uses select dropdown
|
||||||
this.initChannelKey({
|
this.initChannelKey({
|
||||||
|
selectId: 'channelSelectDec',
|
||||||
customInputId: 'channelCustomInputDec',
|
customInputId: 'channelCustomInputDec',
|
||||||
keyInputId: 'channelKeyInputDec',
|
keyInputId: 'channelKeyInputDec'
|
||||||
customRadioId: 'channelCustomDec',
|
|
||||||
cardIds: ['channelAutoCardDec', 'channelPublicCardDec', 'channelCustomCardDec']
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form submission with channel key validation and mode display
|
// Form submission with channel key validation and mode display
|
||||||
const form = document.getElementById('decodeForm');
|
const form = document.getElementById('decodeForm');
|
||||||
const btn = document.getElementById('decodeBtn');
|
const btn = document.getElementById('decodeBtn');
|
||||||
form?.addEventListener('submit', (e) => {
|
form?.addEventListener('submit', (e) => {
|
||||||
if (!this.validateChannelKeyOnSubmit(form, 'channelCustomDec', 'channelKeyInputDec')) {
|
if (!this.validateChannelKeyOnSubmit(form, 'channelSelectDec', 'channelKeyInputDec')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1323,3 +1323,58 @@ footer {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.5rem;
|
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,21 +264,7 @@
|
|||||||
<span class="text-warning small">(provide same factors used during encoding)</span>
|
<span class="text-warning small">(provide same factors used during encoding)</span>
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
<div class="row">
|
<div class="mb-3">
|
||||||
<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;">
|
|
||||||
<button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput">
|
|
||||||
<i class="bi bi-eye"></i>
|
|
||||||
</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">
|
<div class="security-box">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
||||||
@@ -338,53 +324,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- PIN + Channel Row -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<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;">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" data-toggle-password="pinInput">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">If PIN was used during encoding</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ================================================================
|
<div class="col-md-8 mb-3">
|
||||||
CHANNEL KEY (v4.0.0) - Deployment/Group Isolation
|
<div class="security-box h-100">
|
||||||
================================================================ -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="security-box">
|
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
<i class="bi bi-broadcast me-1"></i> Channel
|
<i class="bi bi-broadcast me-1"></i> Channel
|
||||||
<span class="badge bg-info ms-1">v4.0</span>
|
<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>
|
</label>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<select class="form-select" name="channel_key" id="channelSelectDec">
|
||||||
<!-- Auto Mode -->
|
<option value="auto" selected>Auto{% if channel_configured %} (Server Key){% endif %}</option>
|
||||||
<label class="mode-btn flex-fill {% if channel_configured %}active{% endif %}" id="channelAutoCardDec" for="channelAutoDec">
|
<option value="none">Public</option>
|
||||||
<input class="form-check-input" type="radio" name="channel_key" id="channelAutoDec" value="auto" checked>
|
<option value="custom">Custom</option>
|
||||||
<i class="bi bi-gear-fill {% if channel_configured %}text-success{% else %}text-secondary{% endif %} ms-2"></i>
|
</select>
|
||||||
<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) -->
|
<!-- Server channel indicator (compact) -->
|
||||||
{% if channel_configured %}
|
{% if channel_configured %}
|
||||||
<div class="small text-success mt-2">
|
<div class="small text-success mt-2">
|
||||||
<i class="bi bi-shield-lock me-1"></i>
|
<i class="bi bi-shield-lock me-1"></i>
|
||||||
Server: <code>{{ channel_fingerprint }}</code>
|
Server: <code>{{ channel_fingerprint[:4] }}-••••-···-••••-{{ channel_fingerprint[-4:] }}</code>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Custom key input -->
|
<!-- Custom Channel Key Input (shown when Custom selected) -->
|
||||||
<div class="mt-2 d-none" id="channelCustomInputDec">
|
<div class="mb-4 d-none" id="channelCustomInputDec">
|
||||||
<div class="input-group input-group-sm">
|
<div class="security-box">
|
||||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
<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"
|
<input type="text" name="channel_key_custom" class="form-control font-monospace"
|
||||||
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
||||||
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}"
|
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}"
|
||||||
@@ -392,7 +377,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ================================================================
|
<!-- ================================================================
|
||||||
ADVANCED OPTIONS (v3.0) - Extraction Mode
|
ADVANCED OPTIONS (v3.0) - Extraction Mode
|
||||||
|
|||||||
@@ -331,21 +331,7 @@
|
|||||||
<span class="text-warning small">(provide at least one: PIN or RSA Key)</span>
|
<span class="text-warning small">(provide at least one: PIN or RSA Key)</span>
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
<div class="row">
|
<div class="mb-3">
|
||||||
<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;">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="col-md-8 mb-3">
|
|
||||||
<div class="security-box">
|
<div class="security-box">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
|
||||||
@@ -405,53 +391,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- PIN + Channel Row -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<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;">
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- ================================================================
|
<div class="col-md-8 mb-3">
|
||||||
CHANNEL KEY (v4.0.0) - Deployment/Group Isolation
|
<div class="security-box h-100">
|
||||||
================================================================ -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="security-box">
|
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
<i class="bi bi-broadcast me-1"></i> Channel
|
<i class="bi bi-broadcast me-1"></i> Channel
|
||||||
<span class="badge bg-info ms-1">v4.0</span>
|
<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>
|
</label>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<select class="form-select" name="channel_key" id="channelSelect">
|
||||||
<!-- Auto Mode -->
|
<option value="auto" selected>Auto{% if channel_configured %} (Server Key){% endif %}</option>
|
||||||
<label class="mode-btn flex-fill {% if channel_configured %}active{% endif %}" id="channelAutoCard" for="channelAuto">
|
<option value="none">Public</option>
|
||||||
<input class="form-check-input" type="radio" name="channel_key" id="channelAuto" value="auto" checked>
|
<option value="custom">Custom</option>
|
||||||
<i class="bi bi-gear-fill {% if channel_configured %}text-success{% else %}text-secondary{% endif %} ms-2"></i>
|
</select>
|
||||||
<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) -->
|
<!-- Server channel indicator (compact) -->
|
||||||
{% if channel_configured %}
|
{% if channel_configured %}
|
||||||
<div class="small text-success mt-2" id="channelServerInfo">
|
<div class="small text-success mt-2" id="channelServerInfo">
|
||||||
<i class="bi bi-shield-lock me-1"></i>
|
<i class="bi bi-shield-lock me-1"></i>
|
||||||
Server: <code>{{ channel_fingerprint }}</code>
|
Server: <code>{{ channel_fingerprint[:4] }}-••••-···-••••-{{ channel_fingerprint[-4:] }}</code>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Custom key input -->
|
<!-- Custom Channel Key Input (shown when Custom selected) -->
|
||||||
<div class="mt-2 d-none" id="channelCustomInput">
|
<div class="mb-4 d-none" id="channelCustomInput">
|
||||||
<div class="input-group input-group-sm">
|
<div class="security-box">
|
||||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
<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"
|
<input type="text" name="channel_key_custom" class="form-control font-monospace"
|
||||||
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX"
|
||||||
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}"
|
pattern="[A-Za-z0-9]{4}(-[A-Za-z0-9]{4}){7}"
|
||||||
@@ -465,7 +450,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Advanced Options (DCT sub-options only) -->
|
<!-- Advanced Options (DCT sub-options only) -->
|
||||||
<div class="mb-4 {% if not has_dct %}d-none{% endif %}" id="advancedOptionsContainer">
|
<div class="mb-4 {% if not has_dct %}d-none{% endif %}" id="advancedOptionsContainer">
|
||||||
|
|||||||
@@ -25,10 +25,12 @@
|
|||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<i class="bi bi-shield-lock me-2"></i>
|
<i class="bi bi-shield-lock me-2"></i>
|
||||||
<strong>Private Channel Active</strong>
|
<strong>Private Channel Mode</strong>
|
||||||
<span class="text-muted ms-2">Messages are isolated to this deployment</span>
|
</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>
|
</div>
|
||||||
<code class="small">{{ channel_fingerprint }}</code>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "stegasoo"
|
name = "stegasoo"
|
||||||
version = "4.0.0"
|
version = "4.0.1"
|
||||||
description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
|
description = "Secure steganography with hybrid photo + passphrase + PIN authentication"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT"
|
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:
|
Changes in v4.0.0:
|
||||||
- Added channel key support for deployment/group isolation
|
- 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
|
- encode() and decode() now accept channel_key parameter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "4.0.0"
|
__version__ = "4.0.1"
|
||||||
|
|
||||||
# Core functionality
|
# Core functionality
|
||||||
from .encode import encode
|
from .encode import encode
|
||||||
from .decode import decode, decode_file
|
from .decode import decode, decode_file, decode_text
|
||||||
|
|
||||||
# Credential generation
|
# Credential generation
|
||||||
from .generate import (
|
from .generate import (
|
||||||
@@ -158,6 +158,7 @@ __all__ = [
|
|||||||
"encode",
|
"encode",
|
||||||
"decode",
|
"decode",
|
||||||
"decode_file",
|
"decode_file",
|
||||||
|
"decode_text",
|
||||||
|
|
||||||
# Generation
|
# Generation
|
||||||
"generate_pin",
|
"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.
|
Central location for all magic numbers, limits, and crypto parameters.
|
||||||
All version numbers, limits, and configuration values should be defined here.
|
All version numbers, limits, and configuration values should be defined here.
|
||||||
@@ -21,7 +21,7 @@ from pathlib import Path
|
|||||||
# VERSION
|
# VERSION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
__version__ = "4.0.0"
|
__version__ = "4.0.1"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# FILE FORMAT
|
# FILE FORMAT
|
||||||
|
|||||||
@@ -624,12 +624,15 @@ def get_active_channel_key() -> Optional[str]:
|
|||||||
return get_channel_key()
|
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:
|
Returns:
|
||||||
Masked key like "ABCD-••••-••••-••••-••••-••••-••••-3456" or None
|
Masked key like "ABCD-••••-••••-••••-••••-••••-••••-3456" or None
|
||||||
"""
|
"""
|
||||||
from .channel import get_channel_fingerprint as _get_fingerprint
|
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)
|
# 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
|
# Salt: 32 bytes
|
||||||
# IV: 12 bytes
|
# IV: 12 bytes
|
||||||
# Tag: 16 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
|
# v3.2.0 had 65 bytes (no flags byte)
|
||||||
# The old value of 104 was incorrect even for v3.1.0
|
# 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
|
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 format options (v3.0.1)
|
||||||
DCT_OUTPUT_PNG = 'png'
|
DCT_OUTPUT_PNG = 'png'
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
Stegasoo Tests (v4.0.0)
|
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:
|
Updated for v4.0.0:
|
||||||
- Same API as v3.2.0 (passphrase, no date_str)
|
- 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)
|
- Python 3.12 recommended (3.13 not supported)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -16,17 +18,20 @@ import io
|
|||||||
import stegasoo
|
import stegasoo
|
||||||
from stegasoo import (
|
from stegasoo import (
|
||||||
generate_pin,
|
generate_pin,
|
||||||
generate_phrase,
|
generate_passphrase,
|
||||||
generate_credentials,
|
generate_credentials,
|
||||||
validate_pin,
|
validate_pin,
|
||||||
validate_message,
|
validate_message,
|
||||||
validate_passphrase,
|
validate_passphrase,
|
||||||
|
validate_channel_key,
|
||||||
encode,
|
encode,
|
||||||
decode,
|
decode,
|
||||||
decode_text,
|
decode_text,
|
||||||
|
generate_channel_key,
|
||||||
|
get_channel_fingerprint,
|
||||||
__version__,
|
__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 len(pin) == length
|
||||||
assert pin.isdigit()
|
assert pin.isdigit()
|
||||||
|
|
||||||
def test_generate_phrase_default(self):
|
def test_generate_passphrase_default(self):
|
||||||
"""Default phrase should have 4 words (v3.2.0 change)."""
|
"""Default passphrase should have 4 words (v3.2.0 change)."""
|
||||||
phrase = generate_phrase()
|
phrase = generate_passphrase()
|
||||||
words = phrase.split()
|
words = phrase.split()
|
||||||
assert len(words) == 4 # Changed from 3 in v3.1.x
|
assert len(words) == 4 # Changed from 3 in v3.1.x
|
||||||
|
|
||||||
def test_generate_phrase_custom_length(self):
|
def test_generate_passphrase_custom_length(self):
|
||||||
"""Phrase generation should work for custom lengths."""
|
"""Passphrase generation should work for custom lengths."""
|
||||||
for length in [3, 4, 5, 6, 8, 12]:
|
for length in [3, 4, 5, 6, 8, 12]:
|
||||||
phrase = generate_phrase(length)
|
phrase = generate_passphrase(length)
|
||||||
words = phrase.split()
|
words = phrase.split()
|
||||||
assert len(words) == length
|
assert len(words) == length
|
||||||
|
|
||||||
@@ -287,19 +292,20 @@ class TestOutputFormat:
|
|||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Header Overhead Test (v3.2.0)
|
# Header Overhead Test (v4.0.0)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
class TestConstants:
|
class TestConstants:
|
||||||
"""Tests for constants and configuration."""
|
"""Tests for constants and configuration."""
|
||||||
|
|
||||||
def test_header_overhead_value(self):
|
def test_header_overhead_value(self):
|
||||||
"""Header overhead should be 65 bytes (v3.2.0 fix)."""
|
"""Header overhead should be 66 bytes (v4.0.0: added flags byte)."""
|
||||||
assert HEADER_OVERHEAD == 65
|
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:
|
class TestEncodeDecode:
|
||||||
@@ -474,8 +480,8 @@ class TestEncodeDecode:
|
|||||||
|
|
||||||
assert decoded.message == message
|
assert decoded.message == message
|
||||||
|
|
||||||
def test_filename_has_no_date(self, png_image):
|
def test_filename_format(self, png_image):
|
||||||
"""v3.2.0: Output filename should not have date suffix."""
|
"""Output filename should have random hex and date suffix."""
|
||||||
result = encode(
|
result = encode(
|
||||||
message="Test",
|
message="Test",
|
||||||
reference_photo=png_image,
|
reference_photo=png_image,
|
||||||
@@ -483,10 +489,10 @@ class TestEncodeDecode:
|
|||||||
passphrase="test phrase here now",
|
passphrase="test phrase here now",
|
||||||
pin="123456"
|
pin="123456"
|
||||||
)
|
)
|
||||||
# Filename should be like "a1b2c3d4.png", not "a1b2c3d4_20251227.png"
|
# Filename format: {random_hex}_{YYYYMMDD}.{ext}
|
||||||
# Check that there's no underscore followed by 8 digits
|
# e.g., "a1b2c3d4_20251227.png"
|
||||||
import re
|
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",
|
pin="123456",
|
||||||
date_str="2025-01-01" # Removed parameter
|
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