Security: Password-protect channel key export, add audit plan
Channel Key Protection: - Hide channel key by default in admin settings - Require password re-authentication to view/export key - Add /admin/settings/unlock API endpoint for verification - Key re-locks on page navigation (per-page-load only) QR Print Sheet Refinements: - Key split above/below QR image - 10pt bold font, 1.6in QR size - Zero gap between tiles, minimal margins - No page header/footer for clean printing Security Audit Plan: - Comprehensive checklist covering auth, crypto, input validation - Steganography-specific security considerations - Air-gap deployment focus with known limitations documented - Penetration testing checklist and automated tool recommendations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
284
SECURITY_AUDIT_PLAN.md
Normal file
284
SECURITY_AUDIT_PLAN.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# Stegasoo Security Audit Plan
|
||||
|
||||
> **Target Audience**: Developers, security reviewers, and deployment administrators
|
||||
> **Scope**: Web UI, REST API, CLI, and cryptographic core
|
||||
> **Deployment Model**: Air-gapped / private LAN (primary), Internet-facing (secondary)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Stegasoo is a steganography tool designed for **air-gapped deployments** on private networks. While the primary threat model assumes a trusted local network, this audit plan covers security best practices for both isolated and potentially exposed deployments.
|
||||
|
||||
### Known Limitations (By Design)
|
||||
|
||||
- **Self-signed certificates**: HTTPS uses self-signed certs; users must add exceptions or deploy their own CA
|
||||
- **No rate limiting**: Assumes trusted users on private network
|
||||
- **Single-node**: No distributed session store; sessions are per-instance
|
||||
- **Air-gap focus**: External security (firewalls, network isolation) is user's responsibility
|
||||
|
||||
---
|
||||
|
||||
## 1. Authentication & Authorization
|
||||
|
||||
### 1.1 Password Security
|
||||
- [ ] Passwords hashed with Argon2id (preferred) or PBKDF2 fallback
|
||||
- [ ] Minimum password length enforced (8+ characters)
|
||||
- [ ] Password not logged or exposed in error messages
|
||||
- [ ] Password change requires current password verification
|
||||
- [ ] Admin re-authentication required for sensitive operations (channel key export)
|
||||
|
||||
### 1.2 Session Management
|
||||
- [ ] Session tokens are cryptographically random
|
||||
- [ ] Session cookies have `HttpOnly` flag
|
||||
- [ ] Session cookies have `Secure` flag (when HTTPS enabled)
|
||||
- [ ] Session cookies have `SameSite` attribute
|
||||
- [ ] Sessions invalidated on logout
|
||||
- [ ] Sessions invalidated on password change
|
||||
- [ ] Session timeout configured appropriately
|
||||
|
||||
### 1.3 Authorization
|
||||
- [ ] Admin-only routes protected by `@admin_required` decorator
|
||||
- [ ] User-only routes protected by `@login_required` decorator
|
||||
- [ ] Users cannot access other users' saved channel keys
|
||||
- [ ] Users cannot modify other users' accounts
|
||||
- [ ] Role escalation not possible through API manipulation
|
||||
|
||||
---
|
||||
|
||||
## 2. Cryptographic Implementation
|
||||
|
||||
### 2.1 Key Derivation
|
||||
- [ ] KDF uses Argon2id with appropriate parameters (memory, iterations, parallelism)
|
||||
- [ ] PBKDF2 fallback uses sufficient iterations (600,000+)
|
||||
- [ ] Salt is cryptographically random and unique per operation
|
||||
- [ ] PIN/passphrase combined securely before KDF
|
||||
|
||||
### 2.2 Encryption
|
||||
- [ ] AES-256-GCM used for payload encryption
|
||||
- [ ] Nonce/IV is unique per encryption operation
|
||||
- [ ] Authentication tag verified before decryption
|
||||
- [ ] No padding oracle vulnerabilities
|
||||
|
||||
### 2.3 Channel Keys
|
||||
- [ ] Channel keys are 128-bit (32 hex chars)
|
||||
- [ ] Channel key derivation uses HKDF or similar
|
||||
- [ ] Channel isolation prevents cross-channel decryption
|
||||
- [ ] Fingerprint reveals no information about full key
|
||||
|
||||
### 2.4 Random Number Generation
|
||||
- [ ] All random values use `secrets` module or OS CSPRNG
|
||||
- [ ] No use of `random` module for security-sensitive operations
|
||||
|
||||
---
|
||||
|
||||
## 3. Input Validation & Injection Prevention
|
||||
|
||||
### 3.1 Web UI
|
||||
- [ ] All user input sanitized before rendering (XSS prevention)
|
||||
- [ ] Jinja2 auto-escaping enabled
|
||||
- [ ] No `| safe` filter on user-controlled content
|
||||
- [ ] Content-Security-Policy header configured
|
||||
- [ ] X-Content-Type-Options: nosniff
|
||||
|
||||
### 3.2 File Uploads
|
||||
- [ ] File size limits enforced server-side
|
||||
- [ ] File type validation (magic bytes, not just extension)
|
||||
- [ ] Uploaded files not executed
|
||||
- [ ] Filenames sanitized (path traversal prevention)
|
||||
- [ ] Temporary files cleaned up after processing
|
||||
|
||||
### 3.3 API Inputs
|
||||
- [ ] JSON schema validation on API endpoints
|
||||
- [ ] Integer overflow checks on size parameters
|
||||
- [ ] No SQL injection (parameterized queries only)
|
||||
- [ ] No command injection (no shell=True with user input)
|
||||
|
||||
---
|
||||
|
||||
## 4. Steganography-Specific Security
|
||||
|
||||
### 4.1 Carrier Image Handling
|
||||
- [ ] Malformed images don't crash the server (PIL/jpegio hardening)
|
||||
- [ ] DCT mode subprocess isolation for crash protection
|
||||
- [ ] Memory limits on image processing
|
||||
- [ ] No arbitrary code execution from image metadata
|
||||
|
||||
### 4.2 Payload Security
|
||||
- [ ] Payload size limits enforced
|
||||
- [ ] Encrypted payload indistinguishable from random noise
|
||||
- [ ] No metadata leakage in output images
|
||||
- [ ] Reference photo required (prevents dictionary attacks)
|
||||
|
||||
### 4.3 Capacity Reporting
|
||||
- [ ] Capacity calculation doesn't leak information about encoding method
|
||||
- [ ] Failed decodes don't reveal why (wrong key vs no data vs corrupted)
|
||||
|
||||
---
|
||||
|
||||
## 5. Network & Transport Security
|
||||
|
||||
### 5.1 HTTPS Configuration
|
||||
- [ ] TLS 1.2+ only (no SSLv3, TLS 1.0/1.1)
|
||||
- [ ] Strong cipher suites configured
|
||||
- [ ] Certificate generation uses 2048+ bit RSA or P-256 EC
|
||||
- [ ] Private key file permissions restricted (600)
|
||||
|
||||
### 5.2 Headers
|
||||
- [ ] X-Frame-Options: DENY (clickjacking prevention)
|
||||
- [ ] X-Content-Type-Options: nosniff
|
||||
- [ ] Referrer-Policy: same-origin
|
||||
- [ ] Permissions-Policy configured
|
||||
|
||||
### 5.3 CORS (if applicable)
|
||||
- [ ] CORS not enabled (or restricted to specific origins)
|
||||
- [ ] Credentials not allowed cross-origin
|
||||
|
||||
---
|
||||
|
||||
## 6. Error Handling & Logging
|
||||
|
||||
### 6.1 Error Messages
|
||||
- [ ] Stack traces not exposed to users in production
|
||||
- [ ] Error messages don't reveal sensitive paths or config
|
||||
- [ ] Failed login doesn't reveal if username exists
|
||||
|
||||
### 6.2 Logging
|
||||
- [ ] Passwords never logged
|
||||
- [ ] Channel keys never logged
|
||||
- [ ] Passphrases never logged
|
||||
- [ ] Log files have appropriate permissions
|
||||
- [ ] Sensitive operations logged for audit trail (optional)
|
||||
|
||||
---
|
||||
|
||||
## 7. Dependency Security
|
||||
|
||||
### 7.1 Python Dependencies
|
||||
- [ ] All dependencies pinned to specific versions
|
||||
- [ ] No known vulnerabilities in dependencies (run `pip-audit` or `safety`)
|
||||
- [ ] Dependencies from trusted sources only (PyPI)
|
||||
|
||||
### 7.2 Frontend Dependencies
|
||||
- [ ] All JS/CSS served locally (air-gap ready)
|
||||
- [ ] No CDN dependencies
|
||||
- [ ] Bootstrap and libraries are official releases
|
||||
- [ ] Subresource integrity considered for any external loads
|
||||
|
||||
---
|
||||
|
||||
## 8. Deployment Security
|
||||
|
||||
### 8.1 File Permissions
|
||||
- [ ] Database file not world-readable (600 or 640)
|
||||
- [ ] SSL certificates/keys not world-readable
|
||||
- [ ] Config files with secrets protected
|
||||
- [ ] Instance directory not in web root
|
||||
|
||||
### 8.2 Docker Deployment
|
||||
- [ ] Container runs as non-root user
|
||||
- [ ] No unnecessary capabilities
|
||||
- [ ] Resource limits configured
|
||||
- [ ] Health checks don't expose sensitive info
|
||||
|
||||
### 8.3 Raspberry Pi Deployment
|
||||
- [ ] Default passwords changed
|
||||
- [ ] SSH key-only authentication (recommended)
|
||||
- [ ] Unnecessary services disabled
|
||||
- [ ] Firewall configured (UFW/iptables)
|
||||
|
||||
---
|
||||
|
||||
## 9. Air-Gap Specific Considerations
|
||||
|
||||
### 9.1 Network Isolation
|
||||
- [ ] Document expected network topology
|
||||
- [ ] No phone-home or telemetry
|
||||
- [ ] No external API calls
|
||||
- [ ] Works fully offline after deployment
|
||||
|
||||
### 9.2 Key Distribution
|
||||
- [ ] QR code export for channel keys (offline transfer)
|
||||
- [ ] Print sheet for physical key backup
|
||||
- [ ] No cloud sync or external key servers
|
||||
|
||||
### 9.3 Updates
|
||||
- [ ] Document offline update procedure
|
||||
- [ ] Signed releases (future consideration)
|
||||
- [ ] Checksum verification for downloads
|
||||
|
||||
---
|
||||
|
||||
## 10. Penetration Testing Checklist
|
||||
|
||||
### 10.1 Authentication Attacks
|
||||
- [ ] Brute force login (note: no rate limiting by design)
|
||||
- [ ] Session fixation
|
||||
- [ ] Session hijacking
|
||||
- [ ] Password reset flow abuse
|
||||
|
||||
### 10.2 Injection Attacks
|
||||
- [ ] SQL injection on all inputs
|
||||
- [ ] XSS (stored, reflected, DOM-based)
|
||||
- [ ] Command injection
|
||||
- [ ] Path traversal
|
||||
- [ ] SSTI (Server-Side Template Injection)
|
||||
|
||||
### 10.3 Business Logic
|
||||
- [ ] Access control bypass
|
||||
- [ ] IDOR (Insecure Direct Object Reference)
|
||||
- [ ] Race conditions
|
||||
- [ ] Integer overflow in capacity calculations
|
||||
|
||||
### 10.4 Cryptographic Attacks
|
||||
- [ ] Known-plaintext attacks on stego output
|
||||
- [ ] Timing attacks on password verification
|
||||
- [ ] Padding oracle attacks
|
||||
- [ ] Key reuse vulnerabilities
|
||||
|
||||
---
|
||||
|
||||
## Tools for Automated Testing
|
||||
|
||||
```bash
|
||||
# Dependency vulnerability scan
|
||||
pip-audit
|
||||
safety check
|
||||
|
||||
# Static analysis
|
||||
bandit -r stegasoo/ frontends/
|
||||
|
||||
# Web security scan (if exposed)
|
||||
nikto -h https://localhost:5000
|
||||
OWASP ZAP (manual)
|
||||
|
||||
# SSL/TLS configuration
|
||||
testssl.sh https://localhost:5000
|
||||
|
||||
# Python code quality
|
||||
ruff check .
|
||||
mypy stegasoo/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Audit Schedule
|
||||
|
||||
| Phase | Focus Area | Priority |
|
||||
|-------|-----------|----------|
|
||||
| Pre-release | Crypto implementation, auth flow | Critical |
|
||||
| Post-release | Dependency scan, static analysis | High |
|
||||
| Quarterly | Full penetration test | Medium |
|
||||
| Ongoing | CVE monitoring for dependencies | High |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- This plan assumes **trusted users on a private network** as the primary deployment model
|
||||
- Internet-facing deployments should add rate limiting, fail2ban, and reverse proxy hardening
|
||||
- For high-security deployments, consider external security audit by professionals
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-01-07*
|
||||
@@ -2707,11 +2707,10 @@ def admin_settings():
|
||||
|
||||
return render_template(
|
||||
"admin/settings.html",
|
||||
# Channel info
|
||||
# Channel info (key hidden until password verified)
|
||||
channel_configured=channel_status["configured"],
|
||||
channel_fingerprint=channel_status.get("fingerprint"),
|
||||
channel_source=channel_status.get("source"),
|
||||
channel_key_full=channel_status.get("key") if channel_status["configured"] else "",
|
||||
# Server config
|
||||
hostname=os.environ.get("STEGASOO_HOSTNAME") or socket.gethostname(),
|
||||
port=os.environ.get("STEGASOO_PORT", "5000"),
|
||||
@@ -2729,6 +2728,35 @@ def admin_settings():
|
||||
)
|
||||
|
||||
|
||||
@app.route("/admin/settings/unlock", methods=["POST"])
|
||||
@admin_required
|
||||
def admin_settings_unlock():
|
||||
"""Verify password and return channel key (AJAX)."""
|
||||
from stegasoo.channel import get_channel_status
|
||||
|
||||
data = request.get_json() or {}
|
||||
password = data.get("password", "")
|
||||
|
||||
if not password:
|
||||
return jsonify({"success": False, "error": "Password required"})
|
||||
|
||||
# Get current user and verify password
|
||||
username = get_username()
|
||||
user = verify_user_password(username, password)
|
||||
|
||||
if not user:
|
||||
return jsonify({"success": False, "error": "Incorrect password"})
|
||||
|
||||
# Password verified - return channel key
|
||||
channel_status = get_channel_status()
|
||||
channel_key = channel_status.get("key") if channel_status["configured"] else ""
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"channel_key": channel_key
|
||||
})
|
||||
|
||||
|
||||
@app.route("/admin/users")
|
||||
@admin_required
|
||||
def admin_users():
|
||||
|
||||
@@ -360,12 +360,18 @@ function printQrSheet(canvas, keyText, title) {
|
||||
const cols = 4;
|
||||
const rows = 5;
|
||||
|
||||
// Split key into two lines (4 groups each)
|
||||
const keyParts = keyText.split('-');
|
||||
const keyLine1 = keyParts.slice(0, 4).join('-');
|
||||
const keyLine2 = keyParts.slice(4).join('-');
|
||||
|
||||
let qrGrid = '';
|
||||
for (let i = 0; i < rows * cols; i++) {
|
||||
qrGrid += `
|
||||
<div class="qr-tile">
|
||||
<div class="key-text">${keyLine1}</div>
|
||||
<img src="${qrDataUrl}" alt="QR">
|
||||
<div class="key-text">${keyText}</div>
|
||||
<div class="key-text">${keyLine2}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -374,11 +380,17 @@ function printQrSheet(canvas, keyText, title) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Stegasoo ${title} - Print Sheet</title>
|
||||
<title></title>
|
||||
<style>
|
||||
@page {
|
||||
size: letter;
|
||||
margin: 0.2in;
|
||||
margin-top: 0.1in;
|
||||
margin-bottom: 0.1in;
|
||||
}
|
||||
@media print {
|
||||
@page { margin: 0.15in; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@@ -388,30 +400,27 @@ function printQrSheet(canvas, keyText, title) {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(${cols}, 1fr);
|
||||
gap: 0.08in;
|
||||
margin-top: 0.20in;
|
||||
gap: 0;
|
||||
margin-top: 0.09in;
|
||||
}
|
||||
.qr-tile {
|
||||
border: 1px dashed #ccc;
|
||||
padding: 0.08in;
|
||||
padding: 0.04in;
|
||||
text-align: center;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.qr-tile img {
|
||||
width: 1.4in;
|
||||
height: 1.4in;
|
||||
width: 1.6in;
|
||||
height: 1.6in;
|
||||
}
|
||||
.key-text {
|
||||
font-size: 6pt;
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 0.03in;
|
||||
word-break: break-all;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding-top: 0.1in;
|
||||
font-size: 7pt;
|
||||
color: #999;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -33,24 +33,52 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted mb-3">Generate a QR code to share a channel key with others.</p>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label small">Channel Key</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control font-monospace" id="channelKeyQrInput"
|
||||
placeholder="Enter or generate a key"
|
||||
{% if channel_configured %}value="{{ channel_key_full }}"{% endif %}>
|
||||
<button class="btn btn-outline-secondary" type="button" id="channelKeyQrGenerate"
|
||||
title="Generate random key">
|
||||
<i class="bi bi-shuffle"></i>
|
||||
|
||||
<!-- Locked state - requires password -->
|
||||
<div id="channelKeyLocked">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label small">Channel Key</label>
|
||||
<div class="input-group">
|
||||
<input type="password" class="form-control font-monospace"
|
||||
value="********************************" disabled>
|
||||
<span class="input-group-text"><i class="bi bi-lock"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-warning w-100" type="button" id="channelKeyUnlock">
|
||||
<i class="bi bi-unlock me-1"></i>Unlock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-primary w-100" type="button" id="channelKeyQrShow">
|
||||
<i class="bi bi-qr-code me-1"></i>Show QR
|
||||
</button>
|
||||
<small class="text-muted mt-2 d-block">
|
||||
<i class="bi bi-shield-lock me-1"></i>Re-enter your password to view or export the channel key.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Unlocked state - shows key and QR options -->
|
||||
<div id="channelKeyUnlocked" style="display: none;">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label small">Channel Key</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control font-monospace" id="channelKeyQrInput"
|
||||
placeholder="Enter or generate a key">
|
||||
<button class="btn btn-outline-secondary" type="button" id="channelKeyQrGenerate"
|
||||
title="Generate random key">
|
||||
<i class="bi bi-shuffle"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-primary w-100" type="button" id="channelKeyQrShow">
|
||||
<i class="bi bi-qr-code me-1"></i>Show QR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-success mt-2 d-block">
|
||||
<i class="bi bi-unlock me-1"></i>Unlocked for this session.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,6 +211,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Verification Modal -->
|
||||
<div class="modal fade" id="passwordModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title"><i class="bi bi-shield-lock me-2"></i>Verify Password</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small text-muted mb-3">Re-enter your password to access sensitive data.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Password</label>
|
||||
<input type="password" class="form-control" id="verifyPassword" autocomplete="current-password">
|
||||
<div class="invalid-feedback" id="passwordError">Incorrect password</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-warning btn-sm" id="verifyPasswordBtn">
|
||||
<i class="bi bi-unlock me-1"></i>Unlock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Modal -->
|
||||
<div class="modal fade" id="channelKeyQrModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered">
|
||||
@@ -224,6 +278,77 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const modalEl = document.getElementById('channelKeyQrModal');
|
||||
const modal = modalEl ? new bootstrap.Modal(modalEl) : null;
|
||||
|
||||
// Password verification elements
|
||||
const lockedDiv = document.getElementById('channelKeyLocked');
|
||||
const unlockedDiv = document.getElementById('channelKeyUnlocked');
|
||||
const unlockBtn = document.getElementById('channelKeyUnlock');
|
||||
const passwordModalEl = document.getElementById('passwordModal');
|
||||
const passwordModal = passwordModalEl ? new bootstrap.Modal(passwordModalEl) : null;
|
||||
const verifyPasswordInput = document.getElementById('verifyPassword');
|
||||
const verifyPasswordBtn = document.getElementById('verifyPasswordBtn');
|
||||
const passwordError = document.getElementById('passwordError');
|
||||
|
||||
// Unlock button shows password modal
|
||||
unlockBtn?.addEventListener('click', function() {
|
||||
verifyPasswordInput.value = '';
|
||||
verifyPasswordInput.classList.remove('is-invalid');
|
||||
passwordModal?.show();
|
||||
setTimeout(() => verifyPasswordInput.focus(), 300);
|
||||
});
|
||||
|
||||
// Handle Enter key in password field
|
||||
verifyPasswordInput?.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
verifyPasswordBtn?.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Verify password and unlock
|
||||
verifyPasswordBtn?.addEventListener('click', async function() {
|
||||
const password = verifyPasswordInput.value;
|
||||
if (!password) {
|
||||
verifyPasswordInput.classList.add('is-invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
verifyPasswordBtn.disabled = true;
|
||||
verifyPasswordBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Verifying...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/settings/unlock', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Unlock successful
|
||||
passwordModal?.hide();
|
||||
lockedDiv.style.display = 'none';
|
||||
unlockedDiv.style.display = 'block';
|
||||
if (data.channel_key && input) {
|
||||
input.value = data.channel_key;
|
||||
}
|
||||
} else {
|
||||
// Password incorrect
|
||||
verifyPasswordInput.classList.add('is-invalid');
|
||||
passwordError.textContent = data.error || 'Incorrect password';
|
||||
}
|
||||
} catch (error) {
|
||||
verifyPasswordInput.classList.add('is-invalid');
|
||||
passwordError.textContent = 'Verification failed';
|
||||
} finally {
|
||||
verifyPasswordBtn.disabled = false;
|
||||
verifyPasswordBtn.innerHTML = '<i class="bi bi-unlock me-1"></i>Unlock';
|
||||
}
|
||||
});
|
||||
|
||||
// Generate random key
|
||||
generateBtn?.addEventListener('click', function() {
|
||||
if (!input) return;
|
||||
@@ -302,12 +427,18 @@ function printQrSheet(canvas, keyText, title) {
|
||||
const cols = 4;
|
||||
const rows = 5;
|
||||
|
||||
// Split key into two lines (4 groups each)
|
||||
const keyParts = keyText.split('-');
|
||||
const keyLine1 = keyParts.slice(0, 4).join('-');
|
||||
const keyLine2 = keyParts.slice(4).join('-');
|
||||
|
||||
let qrGrid = '';
|
||||
for (let i = 0; i < rows * cols; i++) {
|
||||
qrGrid += `
|
||||
<div class="qr-tile">
|
||||
<div class="key-text">${keyLine1}</div>
|
||||
<img src="${qrDataUrl}" alt="QR">
|
||||
<div class="key-text">${keyText}</div>
|
||||
<div class="key-text">${keyLine2}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -316,11 +447,17 @@ function printQrSheet(canvas, keyText, title) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Stegasoo ${title} - Print Sheet</title>
|
||||
<title></title>
|
||||
<style>
|
||||
@page {
|
||||
size: letter;
|
||||
margin: 0.2in;
|
||||
margin-top: 0.1in;
|
||||
margin-bottom: 0.1in;
|
||||
}
|
||||
@media print {
|
||||
@page { margin: 0.15in; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@@ -330,30 +467,27 @@ function printQrSheet(canvas, keyText, title) {
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(${cols}, 1fr);
|
||||
gap: 0.08in;
|
||||
margin-top: 0.20in;
|
||||
gap: 0;
|
||||
margin-top: 0.09in;
|
||||
}
|
||||
.qr-tile {
|
||||
border: 1px dashed #ccc;
|
||||
padding: 0.08in;
|
||||
padding: 0.04in;
|
||||
text-align: center;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.qr-tile img {
|
||||
width: 1.4in;
|
||||
height: 1.4in;
|
||||
width: 1.6in;
|
||||
height: 1.6in;
|
||||
}
|
||||
.key-text {
|
||||
font-size: 6pt;
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 0.03in;
|
||||
word-break: break-all;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding-top: 0.1in;
|
||||
font-size: 7pt;
|
||||
color: #999;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user