/* Welcome to the magic behind PassED! This file is responsible for encrypting passwords in the browser and to only send encrypted passwords to the server. - https://git.1e99.eu/1e99/passed - https://discord.gg/NuGxJKtDKS */ const ALGORITHM_NAME = "AES-GCM"; const ALGORITHM_LENGTH = 256; const IV_LENGTH = 16; const TEXT_ENCODER = new TextEncoder(); const TEXT_DECODER = new TextDecoder(); const ACTIVE_PAGE = "page-active"; /** Coverts an ArrayBuffer into a Base64 encoded string * @param {ArrayBuffer} buffer * @returns {string} */ function bufferToBase64(buffer) { let binary = ""; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } /** Converts a Base64 encoded string into an ArrayBuffer * @param {string} base64 * @returns {ArrayBuffer} */ function base64ToBuffer(base64) { const binaryString = atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } /** Generates an key and encrypts "password" using it * @param {string} password */ async function encryptPassword(password) { const iv = new Uint8Array(IV_LENGTH); window.crypto.getRandomValues(iv); const key = await window.crypto.subtle.generateKey( { name: ALGORITHM_NAME, length: ALGORITHM_LENGTH, }, true, ["encrypt", "decrypt"], ); const encryptedPassword = await window.crypto.subtle.encrypt( { name: ALGORITHM_NAME, iv: iv, }, key, TEXT_ENCODER.encode(password), ); const exportedKey = await window.crypto.subtle.exportKey("raw", key); const encodedPassword = bufferToBase64(encryptedPassword); const encodedKey = bufferToBase64(exportedKey); const encodedIv = bufferToBase64(iv.buffer); return { password: encodedPassword, key: encodedKey, iv: encodedIv, }; } /** Decodes to key and iv and decryptes the password * @param {string} password * @param {string} key * @param {string} iv */ async function decryptPassword(password, key, iv) { const decodedPassword = base64ToBuffer(password); const decodedKey = base64ToBuffer(key); const decodedIv = base64ToBuffer(iv); const cryptoKey = await window.crypto.subtle.importKey( "raw", decodedKey, { name: ALGORITHM_NAME, length: ALGORITHM_LENGTH, }, true, ["encrypt", "decrypt"], ); const decryptedPassword = await window.crypto.subtle.decrypt( { name: ALGORITHM_NAME, iv: decodedIv, }, cryptoKey, decodedPassword, ); return TEXT_DECODER.decode(decryptedPassword); } function showError(error) { const errorDialog = document.querySelector("dialog#error"); const errorMessage = document.querySelector("textarea#error-message"); errorMessage.value = error; errorDialog.showModal(); } function init() { const passwordForm = document.querySelector("form#password"); const loadingDialog = document.querySelector("dialog#loading"); const urlDialog = document.querySelector("dialog#url"); const urlCopy = document.querySelector("button#url-copy"); const passwordUrl = document.querySelector("input#password-url"); const shareAnother = document.querySelector("button#share-another"); passwordForm.addEventListener("submit", async (ev) => { try { loadingDialog.showModal(); ev.preventDefault(); const data = new FormData(ev.target); const encrypted = await encryptPassword(data.get("password")); const res = await fetch("/api/password", { method: "POST", body: JSON.stringify({ password: encrypted.password, "expires-in": data.get("expires-in"), }), }); if (!res.ok) { const msg = await res.text(); throw new Error(`Failed to upload password: ${res.status}: ${msg}`); } const json = await res.json(); const params = new URLSearchParams(); params.set("id", json.id); params.set("key", encrypted.key); params.set("iv", encrypted.iv); const url = new URL(window.location); url.search = params.toString(); passwordUrl.value = url.toString(); urlDialog.showModal(); passwordUrl.select(); } catch (error) { showError(error); } finally { loadingDialog.close(); } }); loadingDialog.addEventListener("close", (ev) => ev.preventDefault()); shareAnother.addEventListener("click", (ev) => { passwordForm.reset(); passwordUrl.value = ""; urlDialog.close(); }); urlCopy.addEventListener("click", async (ev) => { passwordUrl.select(); passwordUrl.setSelectionRange(0, 99999); await navigator.clipboard.writeText(passwordUrl.value); }); } function confirmViewPassword() { const confirmDialog = document.querySelector("dialog#confirm-view"); const confirm = document.querySelector("button#confirm-view"); confirmDialog.showModal(); confirm.addEventListener("click", (ev) => { confirmDialog.close(); viewPassword(); }); } async function viewPassword() { const viewDialog = document.querySelector("dialog#view"); const password = document.querySelector("textarea#password"); const loadingDialog = document.querySelector("dialog#loading"); const params = new URLSearchParams(window.location.search); try { loadingDialog.showModal(); const res = await fetch(`/api/password/${params.get("id")}`); if (!res.ok) { const msg = await res.text(); throw new Error(`Failed to load password: ${res.status}: ${msg}`); } const json = await res.json(); const decrypted = await decryptPassword( json.password, params.get("key"), params.get("iv"), ); password.value = decrypted; viewDialog.showModal(); } catch (error) { showError(error); } finally { loadingDialog.close(); } } window.addEventListener("load", () => { const query = window.location.search; if (query.trim() != "") { confirmViewPassword(); } init(); });