passed/static/index.js
2024-10-30 11:25:42 +01:00

269 lines
6.8 KiB
JavaScript

/*
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();
console.error(error);
}
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": parseInt(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);
});
}
async function confirmViewPassword(params) {
const confirmDialog = document.querySelector("dialog#confirm-view");
const confirm = document.querySelector("button#confirm-view");
const loadingDialog = document.querySelector("dialog#loading");
const passwordNEDialog = document.querySelector("dialog#password-ne");
try {
loadingDialog.showModal();
const res = await fetch(`/api/password/${params.get("id")}`, {
method: "HEAD",
});
console.log(res.status);
if (res.status == 404) {
loadingDialog.close();
passwordNEDialog.showModal();
console.log("Return");
return;
}
if (!res.ok) {
const msg = await res.text();
throw new Error(
`Failed to check if password exists: ${res.status}: ${msg}`,
);
}
} catch (error) {
showError(error);
} finally {
}
loadingDialog.close();
confirmDialog.showModal();
confirm.addEventListener("click", (ev) => {
confirmDialog.close();
viewPassword(params);
});
}
async function viewPassword(params) {
const viewDialog = document.querySelector("dialog#view");
const password = document.querySelector("textarea#password");
const loadingDialog = document.querySelector("dialog#loading");
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() != "") {
const params = new URLSearchParams(window.location.search);
confirmViewPassword(params);
}
init();
});