239 lines
6 KiB
JavaScript
239 lines
6 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();
|
|
}
|
|
|
|
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();
|
|
});
|