diff --git a/frontend/assets/lang/en.json b/frontend/assets/lang/en.json index 38a4a27..1197cd6 100644 --- a/frontend/assets/lang/en.json +++ b/frontend/assets/lang/en.json @@ -2,8 +2,10 @@ "page_title": "Hauk", "expired_head": "Location expired", "expired_body": "The shared location you tried to access was not found on the server. If this link worked before, the share might have expired.", - "e2e_password_prompt": "This share is protected by end-to-end encryption. Please enter the encryption password to access the share:", - "e2e_incorrect": "The encryption password you entered was wrong. Please try again:", + "e2e_title": "End-to-end encryption", + "e2e_placeholder": "Encryption password", + "e2e_password_prompt": "This share is protected by end-to-end encryption. Please enter the encryption password to access the share.", + "e2e_incorrect": "The encryption password you entered was wrong. Please try again.", "e2e_unavailable_secure": "This share is protected by end-to-end encryption. Decryption is currently unavailable because you are not using HTTPS. Please ensure you are using HTTPS, then try again.", "e2e_unsupported": "This share is protected by end-to-end encryption. Your browser does not appear to support the cryptographic functions required to decrypt such shares. Please try again with another web browser.", "gnss_signal_head": "Please wait", @@ -16,6 +18,8 @@ "status_expired": "Expired", "status_offline": "Offline", "btn_dismiss": "Dismiss", + "btn_cancel": "Cancel", + "btn_decrypt": "Decrypt", "f_droid_badge_url": "https://fdroid.gitlab.io/artwork/badge/get-it-on.png", "google_play_badge_url": "https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" } diff --git a/frontend/index.html b/frontend/index.html index 899fd97..3a5000b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -81,11 +81,11 @@

- + + diff --git a/frontend/main.js b/frontend/main.js index 8b74e79..c1100f2 100644 --- a/frontend/main.js +++ b/frontend/main.js @@ -246,14 +246,25 @@ function getJSON(url, callback, invalid) { xhr.send(); } -var dismissExpiredE = document.getElementById("dismiss-expired"); -if (dismissExpiredE !== null) { - dismissExpiredE.addEventListener("click", function() { - var expiredE = document.getElementById("expired"); - if (expiredE !== null) expiredE.style.display = "none"; +// General message popup box. Reused for several popups. +var dismissMessageE = document.getElementById("dismiss-message"); +if (dismissMessageE !== null) { + dismissMessageE.addEventListener("click", function() { + var messageE = document.getElementById("message-popup"); + if (messageE !== null) messageE.style.display = "none"; }); } +// Shows a dialog box with a title and message. +function showMessage(title, message) { + var messageE = document.getElementById("message-popup"); + var titleE = document.getElementById("message-title"); + var bodyE = document.getElementById("message-body"); + if (titleE !== null) titleE.textContent = title; + if (bodyE !== null) bodyE.textContent = message; + if (messageE !== null) messageE.style.display = "block"; +} + var dismissOfflineE = document.getElementById("dismiss-offline"); if (dismissOfflineE !== null) { dismissOfflineE.addEventListener("click", function() { @@ -262,12 +273,33 @@ if (dismissOfflineE !== null) { }); } +// End-to-end encryption password prompt handlers. +var passwordInputE = document.getElementById("e2e-password"); +var passwordDecryptE = document.getElementById("decrypt-e2e-password"); +if (passwordInputE !== null) { + passwordInputE.addEventListener("keyup", function(e) { + if (e.keyCode == 13) { + if (passwordDecryptE !== null) { + passwordDecryptE.click(); + } + } + }); +} + +var passwordCancelE = document.getElementById("cancel-e2e-password"); +if (passwordCancelE !== null) { + passwordCancelE.addEventListener("click", function() { + var promptE = document.getElementById("e2e-prompt"); + if (promptE !== null) promptE.style.display = "none"; + if (passwordDecryptE !== null && acceptKeyFunc !== null) passwordDecryptE.removeEventListener("click", acceptKeyFunc); + }); +} + var fetchIntv; var countIntv; function setNewInterval(expire, interval) { var countdownE = document.getElementById("countdown"); - var expiredE = document.getElementById("expired"); // The data contains an expiration time. Create a countdown at the top of // the map screen that ends when the share is over. @@ -300,7 +332,7 @@ function setNewInterval(expire, interval) { clearInterval(fetchIntv); clearInterval(countIntv); if (countdownE !== null) countdownE.textContent = LANG["status_expired"]; - if (expiredE !== null) expiredE.style.display = "block"; + showMessage(LANG["dialog_expired_head"], LANG["dialog_expired_body"]); } getJSON("./api/fetch.php?id=" + id, function(data) { @@ -317,7 +349,7 @@ function setNewInterval(expire, interval) { clearInterval(fetchIntv); clearInterval(countIntv); if (countdownE !== null) countdownE.textContent = LANG["status_expired"]; - if (expiredE !== null) expiredE.style.display = "block"; + showMessage(LANG["dialog_expired_head"], LANG["dialog_expired_body"]); }); }, interval * 1000); } @@ -336,9 +368,8 @@ var following = null; // The decryption key for end-to-end encrypted shares. var aesKey = null; -// Whether or not the user has already entered an incorrect encryption password -// at least once. -var hasEnteredPass = false; +// Button handler for the "Decrypt" button on the E2E password prompt. +var acceptKeyFunc = null; // Converts a base64-encoded string to a Uint8Array ArrayBuffer for use with // WebCrypto. @@ -366,13 +397,13 @@ function processUpdate(data, init) { // Check for crypto support if necessary. if (data.encrypted && !("crypto" in window)) { - alert(LANG["e2e_unsupported"]); + showMessage(LANG["e2e_title"], LANG["e2e_unsupported"]); return; } else if (data.encrypted && !("subtle" in window.crypto)) { if (!window.isSecureContext) { - alert(LANG["e2e_unavailable_secure"]); + showMessage(LANG["e2e_title"], LANG["e2e_unavailable_secure"]); } else { - alert(LANG["e2e_unsupported"]); + showMessage(LANG["e2e_title"], LANG["e2e_unsupported"]); } return; } @@ -380,29 +411,44 @@ function processUpdate(data, init) { if (data.encrypted && aesKey == null) { // If using end-to-end encryption, we need to decrypt the data. We have // not obtained an AES key yet, so prompt the user for it. - var password = prompt(hasEnteredPass ? LANG["e2e_incorrect"] : LANG["e2e_password_prompt"]); - if (password == null) return; - hasEnteredPass = true; - - // Get the salt in binary format. - var salt = byteArray(data.salt); - - // Derive the encryption key using PBKDF2 with SHA-1. SHA-1 was chosen - // because of availability in Android. - crypto.subtle - .importKey("raw", new TextEncoder("utf-8").encode(password), "PBKDF2", false, ["deriveKey"]) - .then(key => crypto.subtle.deriveKey( - {name: "PBKDF2", salt: salt, iterations: 65536, hash: "SHA-1"}, - key, - {name: "AES-CBC", length: 256}, - false, - ["decrypt"] - )) - .then(key => { - // Store the crypto key and re-process the update. - aesKey = key; - processUpdate(data, init); - }); + var promptE = document.getElementById("e2e-prompt"); + var labelE = document.getElementById("e2e-password-label"); + if (promptE !== null && passwordInputE !== null && passwordDecryptE !== null && labelE !== null) { + acceptKeyFunc = function() { + // Remove the event listener, hide the dialog and fetch the + // password. + passwordDecryptE.removeEventListener("click", acceptKeyFunc); + promptE.style.display = "none"; + labelE.textContent = LANG["e2e_incorrect"]; + var password = passwordInputE.value; + + // Get the salt in binary format. + var salt = byteArray(data.salt); + + // Derive the encryption key using PBKDF2 with SHA-1. SHA-1 was chosen + // because of availability in Android. + crypto.subtle + .importKey("raw", new TextEncoder("utf-8").encode(password), "PBKDF2", false, ["deriveKey"]) + .then(key => crypto.subtle.deriveKey( + {name: "PBKDF2", salt: salt, iterations: 65536, hash: "SHA-1"}, + key, + {name: "AES-CBC", length: 256}, + false, + ["decrypt"] + )) + .then(key => { + // Store the crypto key and re-process the update. + aesKey = key; + processUpdate(data, init); + }); + }; + + // Attach the listener to the dialog box and show it. + passwordDecryptE.addEventListener("click", acceptKeyFunc); + passwordInputE.value = ""; + promptE.style.display = "block"; + passwordInputE.focus(); + } return; diff --git a/frontend/style.css b/frontend/style.css index e4cc5cd..630e8e9 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -101,6 +101,11 @@ body { font-size: 1em; } +/* Ensure the password prompt fills the dialog box. */ +.dialog input[type=password] { + width: calc(100% - 20px); +} + /* Visible on the root page of Hauk. */ #url { color: #d80037;