1
0
mirror of https://github.com/lus/pasty.git synced 2023-08-10 21:13:09 +03:00

Implement clientside paste encryption

This commit is contained in:
Lukas Schulte Pelkum
2021-07-30 21:24:08 +02:00
parent d4e3430feb
commit 4f3b5b193b
7 changed files with 160 additions and 5 deletions

View File

@@ -0,0 +1,60 @@
// Encrypts a piece of text using AES-CBC and returns the HEX-encoded key, initialization vector and encrypted text
export async function encrypt(encryptionData, text) {
const key = encryptionData.key;
const iv = encryptionData.iv;
const textBytes = aesjs.padding.pkcs7.pad(aesjs.utils.utf8.toBytes(text));
const aes = new aesjs.ModeOfOperation.cbc(key, iv);
const encrypted = aes.encrypt(textBytes);
return {
key: aesjs.utils.hex.fromBytes(key),
iv: aesjs.utils.hex.fromBytes(iv),
result: aesjs.utils.hex.fromBytes(encrypted)
};
}
// Decrypts an encrypted piece of AES-CBC encrypted text
export async function decrypt(keyHex, ivHex, inputHex) {
const key = aesjs.utils.hex.toBytes(keyHex);
const iv = aesjs.utils.hex.toBytes(ivHex);
const input = aesjs.utils.hex.toBytes(inputHex);
const aes = new aesjs.ModeOfOperation.cbc(key, iv);
const decrypted = aesjs.padding.pkcs7.strip(aes.decrypt(input));
return aesjs.utils.utf8.fromBytes(decrypted);
}
// Creates encryption data from hex key and IV
export async function encryptionDataFromHex(keyHex, ivHex) {
return {
key: aesjs.utils.hex.toBytes(keyHex),
iv: aesjs.utils.hex.toBytes(ivHex)
};
}
// Generates encryption data to pass into the encrypt function
export async function generateEncryptionData() {
return {
key: await generateKey(),
iv: generateIV()
};
}
// Generates a new 256-bit AES-CBC key
async function generateKey() {
const key = await crypto.subtle.generateKey({
name: "AES-CBC",
length: 256
}, true, ["encrypt", "decrypt"]);
const extracted = await crypto.subtle.exportKey("raw", key);
return new Uint8Array(extracted);
}
// Generates a new cryptographically secure 16-byte array which is used as the initialization vector (IV) for AES-CBC
function generateIV() {
return crypto.getRandomValues(new Uint8Array(16));
}

View File

@@ -2,6 +2,7 @@ import * as API from "./api.js";
import * as Notifications from "./notifications.js";
import * as Spinner from "./spinner.js";
import * as Animation from "./animation.js";
import * as Encryption from "./encryption.js";
const CODE_ELEMENT = document.getElementById("code");
const LINE_NUMBERS_ELEMENT = document.getElementById("linenos");
@@ -20,10 +21,15 @@ const BUTTONS_EDIT_ELEMENT = document.getElementById("buttons_edit");
const BUTTON_EDIT_CANCEL_ELEMENT = document.getElementById("btn_edit_cancel");
const BUTTON_EDIT_APPLY_ELEMENT = document.getElementById("btn_edit_apply");
const BUTTON_TOGGLE_ENCRYPTION_ELEMENT = document.getElementById("btn_toggle_encryption");
let PASTE_ID;
let LANGUAGE;
let CODE;
let ENCRYPTION_KEY;
let ENCRYPTION_IV;
let EDIT_MODE = false;
let API_INFORMATION = {
@@ -39,6 +45,11 @@ export async function initialize() {
setupButtonFunctionality();
setupKeybinds();
// Enable encryption if enabled from last session
if (localStorage.getItem("encryption") === "true") {
BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.add("active");
}
if (location.pathname !== "/") {
// Extract the paste data (ID and language)
const split = location.pathname.replace("/", "").split(".");
@@ -56,7 +67,26 @@ export async function initialize() {
// Set the persistent paste data
PASTE_ID = pasteID;
LANGUAGE = language;
CODE = (await response.json()).content;
// Decode the response and decrypt the content if needed
const json = await response.json();
CODE = json.content;
if (json.metadata.pf_encryption) {
ENCRYPTION_KEY = location.hash.replace("#", "");
while (ENCRYPTION_KEY.length == 0) {
ENCRYPTION_KEY = prompt("Your decryption key:");
}
try {
CODE = await Encryption.decrypt(ENCRYPTION_KEY, json.metadata.pf_encryption.iv, CODE);
ENCRYPTION_IV = json.metadata.pf_encryption.iv;
} catch (error) {
console.log(error);
Notifications.error("Could not decrrypt paste; make sure the decryption key is correct.");
setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000);
return;
}
}
// Fill the code block with the just received data
updateCode();
@@ -231,8 +261,24 @@ function setupButtonFunctionality() {
return;
}
// Encrypt the paste if needed
let value = INPUT_ELEMENT.value;
let metadata;
let key;
if (BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.contains("active")) {
const encrypted = await Encryption.encrypt(await Encryption.generateEncryptionData(), value);
value = encrypted.result;
metadata = {
pf_encryption: {
alg: "AES-CBC",
iv: encrypted.iv
}
};
key = encrypted.key;
}
// Try to create the paste
const response = await API.createPaste(INPUT_ELEMENT.value);
const response = await API.createPaste(value, metadata);
if (!response.ok) {
Notifications.error("Error while creating paste: <b>" + await response.text() + "</b>");
return;
@@ -245,7 +291,7 @@ function setupButtonFunctionality() {
}
// Redirect the user to his newly created paste
location.replace(location.protocol + "//" + location.host + "/" + data.id);
location.replace(location.protocol + "//" + location.host + "/" + data.id + (key ? "#" + key : ""));
});
});
@@ -297,8 +343,15 @@ function setupButtonFunctionality() {
return;
}
// Re-encrypt the paste data if needed
let value = INPUT_ELEMENT.value;
if (ENCRYPTION_KEY && ENCRYPTION_IV) {
const encrypted = await Encryption.encrypt(await Encryption.encryptionDataFromHex(ENCRYPTION_KEY, ENCRYPTION_IV), value);
value = encrypted.result;
}
// Try to edit the paste
const response = await API.editPaste(PASTE_ID, modificationToken, INPUT_ELEMENT.value);
const response = await API.editPaste(PASTE_ID, modificationToken, value);
if (!response.ok) {
Notifications.error("Error while editing paste: <b>" + await response.text() + "</b>");
return;
@@ -311,6 +364,11 @@ function setupButtonFunctionality() {
Notifications.success("Successfully edited paste.");
});
BUTTON_TOGGLE_ENCRYPTION_ELEMENT.addEventListener("click", () => {
const active = BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.toggle("active");
localStorage.setItem("encryption", active);
});
BUTTON_REPORT_ELEMENT.addEventListener("click", async () => {
// Ask the user for a reason
const reason = prompt("Reason:");