| {% extends "base.html" %} |
| {% block title %}Secure Message{% endblock %} |
| {% block page_title %}Dead Drop Protocol{% endblock %} |
|
|
| {% block content %} |
| <div class="row justify-content-center mt-5"> |
| <div class="col-lg-6 fade-in-up"> |
| <div class="cyber-card p-5 text-center"> |
| |
| <div id="locked-state"> |
| <div class="mb-4 position-relative d-inline-block"> |
| <i class="bi bi-file-earmark-lock2-fill text-warning" style="font-size: 4rem;"></i> |
| <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"> |
| 1 View Left |
| </span> |
| </div> |
| |
| <h3 class="text-white mb-3">Encrypted Transmission</h3> |
| <p class="text-muted mb-4"> |
| This message is stored in a Zero-Knowledge Vault. |
| Once you reveal it, it will be <strong>permanently deleted</strong> from the server and a self-destruct timer will begin. |
| </p> |
| |
| <button id="reveal-btn" class="btn btn-warning w-100 py-3 fw-bold"> |
| <i class="bi bi-eye-fill me-2"></i>Reveal Secret Message |
| </button> |
| </div> |
|
|
| <div id="unlocked-state" class="d-none text-start"> |
| <div class="d-flex align-items-center mb-3"> |
| <i class="bi bi-unlock-fill text-success fs-2 me-3"></i> |
| <h4 class="m-0 text-success">Decryption Successful</h4> |
| </div> |
| |
| <div class="alert alert-warning border-warning bg-warning bg-opacity-10 d-flex justify-content-between align-items-center mb-4"> |
| <div class="text-warning"> |
| <i class="bi bi-stopwatch fs-4 me-2"></i> <strong>Self-Destructing in:</strong> |
| </div> |
| <div id="countdown-timer" class="fs-1 fw-bold text-danger font-monospace">--</div> |
| </div> |
|
|
| <label class="small text-muted text-uppercase fw-bold">Message Contents:</label> |
| <div class="p-4 bg-dark border border-success rounded mt-2 position-relative"> |
| <code id="secret-content" class="text-white fs-5" style="word-break: break-all;"></code> |
| <button class="btn btn-sm btn-outline-success position-absolute top-0 end-0 m-2" onclick="copySecret()"> |
| Copy |
| </button> |
| </div> |
| </div> |
| |
| <div id="error-state" class="d-none"> |
| <i class="bi bi-x-octagon-fill text-danger mb-3" style="font-size: 4rem;"></i> |
| <h3 class="text-danger">Access Denied</h3> |
| <p class="text-muted" id="error-msg"></p> |
| <a href="{{ url_for('share.share_ui') }}" class="btn btn-outline-light mt-3">Create New Drop</a> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script src="{{ url_for('static', filename='js/zk_crypto.js') }}"></script> |
| <script> |
| function copySecret() { |
| const text = document.getElementById("secret-content").innerText; |
| navigator.clipboard.writeText(text); |
| alert("Copied to clipboard!"); |
| } |
| |
| document.getElementById("reveal-btn").addEventListener("click", async () => { |
| const btn = document.getElementById("reveal-btn"); |
| btn.disabled = true; |
| btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Decrypting...'; |
| |
| try { |
| const keyFromHash = window.location.hash.substring(1); |
| if (!keyFromHash) throw new Error("Decryption key missing. The link is incomplete."); |
| |
| const dropId = "{{ drop_id }}"; |
| const res = await fetch(`/api/share/${dropId}`, { method: "POST" }); |
| |
| if (res.status === 410 || res.status === 404) { |
| throw new Error("This message has expired or has already been viewed."); |
| } |
| if (!res.ok) throw new Error("Server communication error."); |
| |
| const data = await res.json(); |
| |
| const key = await ZKCrypto.deriveKey(keyFromHash, Uint8Array.from(atob(data.salt), c => c.charCodeAt(0))); |
| const iv = Uint8Array.from(atob(data.iv), c => c.charCodeAt(0)); |
| const encrypted = Uint8Array.from(atob(data.ciphertext), c => c.charCodeAt(0)); |
| |
| const decryptedBuf = await window.crypto.subtle.decrypt( |
| { name: "AES-GCM", iv: iv }, |
| key, |
| encrypted |
| ); |
| |
| const dec = new TextDecoder(); |
| const plainText = dec.decode(decryptedBuf); |
| |
| document.getElementById("locked-state").classList.add("d-none"); |
| document.getElementById("secret-content").textContent = plainText; |
| document.getElementById("unlocked-state").classList.remove("d-none"); |
| |
| |
| let timeLeft = data.view_time || 30; |
| const timerDisplay = document.getElementById("countdown-timer"); |
| timerDisplay.textContent = timeLeft + "s"; |
| |
| const timerInterval = setInterval(() => { |
| timeLeft--; |
| timerDisplay.textContent = timeLeft + "s"; |
| |
| if (timeLeft <= 0) { |
| clearInterval(timerInterval); |
| |
| |
| window.location.reload(true); |
| } |
| }, 1000); |
| |
| } catch (err) { |
| document.getElementById("locked-state").classList.add("d-none"); |
| document.getElementById("error-msg").textContent = err.message; |
| document.getElementById("error-state").classList.remove("d-none"); |
| } |
| }); |
| </script> |
| {% endblock %} |