Critical: - FR-01: Chain verification now supports key rotation via signed rotation records (soosef/key-rotation-v1 content type). Old single-signer invariant replaced with authorized-signers set. - FR-02: Carrier images stripped of EXIF metadata by default before steganographic encoding (strip_metadata=True). Prevents source location/device leakage. High priority: - FR-03: Session timeout (default 15min) + secure cookie flags (HttpOnly, SameSite=Strict, Secure when HTTPS) - FR-04: CSRF protection via Flask-WTF on all POST forms. Killswitch now requires password re-authentication. - FR-05: Collaborator trust store — trust_key(), get_trusted_keys(), resolve_attestor_name(), untrust_key() in KeystoreManager. - FR-06: Production WSGI server (Waitress) by default, Flask dev server only with --debug flag. - FR-07: Dead man's switch sends warning during grace period via local file + optional webhook before auto-purge. Medium: - FR-08: Geofence get_current_location() via gpsd for --here support. - FR-09: Batch attestation endpoint (/attest/batch) with SHA-256 dedup and per-file status reporting. - FR-10: Key backup tracking with last_backup_info() and is_backup_overdue() + backup_reminder_days config. - FR-11: Verification receipts signed with instance Ed25519 key (schema_version bumped to 2). - FR-12: Login rate limiting with configurable lockout (5 attempts, 15 min default). Nice-to-have: - FR-13: Unified `soosef status` pre-flight command showing identity, channel key, deadman, geofence, chain, and backup status. - FR-14: `soosef chain export` produces ZIP with JSON manifest, public key, and raw chain.bin for legal discovery. Tests: 157 passed, 1 skipped, 1 pre-existing flaky test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
185 lines
7.5 KiB
HTML
185 lines
7.5 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Regenerate Recovery Key - Stegasoo{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8 col-lg-6">
|
|
<div class="card">
|
|
<div class="card-header text-center">
|
|
<i class="bi bi-arrow-repeat fs-1 d-block mb-2"></i>
|
|
<h5 class="mb-0">{{ 'Regenerate' if has_existing else 'Generate' }} Recovery Key</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if has_existing %}
|
|
<!-- Warning for existing key -->
|
|
<div class="alert alert-warning">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
<strong>Warning:</strong> Your existing recovery key will be invalidated.
|
|
Make sure to save this new key before continuing.
|
|
</div>
|
|
{% else %}
|
|
<!-- Info for first-time setup -->
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
<strong>What is a recovery key?</strong><br>
|
|
If you forget your admin password, this key is the ONLY way to reset it.
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Recovery Key Display -->
|
|
<div class="mb-4">
|
|
<label class="form-label">
|
|
<i class="bi bi-key-fill me-1"></i> Your New Recovery Key
|
|
</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control font-monospace text-center"
|
|
id="recoveryKey" value="{{ recovery_key }}" readonly
|
|
style="font-size: 1.1em; letter-spacing: 0.5px;">
|
|
<button class="btn btn-outline-secondary" type="button"
|
|
onclick="copyToClipboard()" title="Copy to clipboard">
|
|
<i class="bi bi-clipboard" id="copyIcon"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- QR Code (if available) -->
|
|
{% if qr_base64 %}
|
|
<div class="mb-4 text-center">
|
|
<label class="form-label d-block">
|
|
<i class="bi bi-qr-code me-1"></i> QR Code
|
|
</label>
|
|
<img src="data:image/png;base64,{{ qr_base64 }}"
|
|
alt="Recovery Key QR Code" class="img-fluid border rounded"
|
|
style="max-width: 200px;" id="qrImage">
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Download Options -->
|
|
<div class="mb-4">
|
|
<label class="form-label">
|
|
<i class="bi bi-download me-1"></i> Download Options
|
|
</label>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<button class="btn btn-outline-primary btn-sm" onclick="downloadTextFile()">
|
|
<i class="bi bi-file-text me-1"></i> Text File
|
|
</button>
|
|
{% if qr_base64 %}
|
|
<button class="btn btn-outline-primary btn-sm" onclick="downloadQRImage()">
|
|
<i class="bi bi-image me-1"></i> QR Image
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stego Backup Option -->
|
|
<div class="mb-4">
|
|
<label class="form-label">
|
|
<i class="bi bi-incognito me-1"></i> Hide in Image
|
|
</label>
|
|
<form method="POST" action="{{ url_for('create_stego_backup') }}"
|
|
enctype="multipart/form-data" class="d-flex gap-2 align-items-end">
|
|
<input type="hidden" name="recovery_key" value="{{ recovery_key }}">
|
|
<div class="flex-grow-1">
|
|
<input type="file" name="carrier_image" class="form-control form-control-sm"
|
|
accept="image/jpeg,image/png" required>
|
|
<div class="form-text">JPG/PNG, 50KB-2MB</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-outline-secondary btn-sm">
|
|
<i class="bi bi-download me-1"></i> Stego
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<!-- Confirmation Form -->
|
|
<form method="POST" id="recoveryForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
<input type="hidden" name="recovery_key" value="{{ recovery_key }}">
|
|
|
|
<!-- Confirm checkbox -->
|
|
<div class="form-check mb-3">
|
|
<input class="form-check-input" type="checkbox" id="confirmSaved"
|
|
onchange="updateButtons()">
|
|
<label class="form-check-label" for="confirmSaved">
|
|
I have saved my recovery key in a secure location
|
|
</label>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2 justify-content-between">
|
|
<!-- Cancel button -->
|
|
<button type="submit" name="action" value="cancel"
|
|
class="btn btn-outline-secondary">
|
|
<i class="bi bi-x-lg me-1"></i> Cancel
|
|
</button>
|
|
|
|
<!-- Save button -->
|
|
<button type="submit" name="action" value="save"
|
|
class="btn btn-primary" id="saveBtn" disabled>
|
|
<i class="bi bi-check-lg me-1"></i> Save New Key
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Copy recovery key to clipboard
|
|
function copyToClipboard() {
|
|
const keyInput = document.getElementById('recoveryKey');
|
|
navigator.clipboard.writeText(keyInput.value).then(() => {
|
|
const icon = document.getElementById('copyIcon');
|
|
icon.className = 'bi bi-clipboard-check';
|
|
setTimeout(() => { icon.className = 'bi bi-clipboard'; }, 2000);
|
|
});
|
|
}
|
|
|
|
// Download as text file
|
|
function downloadTextFile() {
|
|
const key = document.getElementById('recoveryKey').value;
|
|
const content = `Stegasoo Recovery Key
|
|
=====================
|
|
|
|
${key}
|
|
|
|
IMPORTANT:
|
|
- Keep this file in a secure location
|
|
- Anyone with this key can reset admin passwords
|
|
- Do not store with your password
|
|
|
|
Generated: ${new Date().toISOString()}
|
|
`;
|
|
const blob = new Blob([content], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'stegasoo-recovery-key.txt';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Download QR as image
|
|
function downloadQRImage() {
|
|
const img = document.getElementById('qrImage');
|
|
if (!img) return;
|
|
|
|
const a = document.createElement('a');
|
|
a.href = img.src;
|
|
a.download = 'stegasoo-recovery-qr.png';
|
|
a.click();
|
|
}
|
|
|
|
// Enable save button when checkbox is checked
|
|
function updateButtons() {
|
|
const checkbox = document.getElementById('confirmSaved');
|
|
const saveBtn = document.getElementById('saveBtn');
|
|
saveBtn.disabled = !checkbox.checked;
|
|
}
|
|
</script>
|
|
{% endblock %}
|