Implement clientside paste encryption

This commit is contained in:
Lukas Schulte Pelkum 2021-07-30 21:24:08 +02:00
parent d4e3430feb
commit 4f3b5b193b
No known key found for this signature in database
GPG Key ID: 408DA7CA81DB885C
7 changed files with 160 additions and 5 deletions

17
API.md
View File

@ -40,6 +40,23 @@ The central paste entity has the following fields:
* `metadata` (key-value store)
* Different frontends may store simple key-value metadata pairs on pastes to enable specific functionality (for example clientside encryption)
### Encryption
The frontend pasty ships with implements an encryption option. This en- and decrypts pastes clientside and appends the HEX-encoded en-/decryption key to the paste URL (after a `#` because the so called **hash** is not sent to the server).
If a paste is encrypted using this feature, its `metadata` field contains a field like this:
```jsonc
{
// --- omitted other entity field
"metadata": {
"pf_encryption": {
"alg": "AES-CBC", // The algorithm used to encrypt the paste (currently, only AES-CBC is used)
"iv": "54baa80cd8d8328dc4630f9316130f49" // The HEX-encoded initialization vector of the AES-CBC encryption
}
}
}
```
## Endpoints
### [UNSECURED] Retrieve application information

View File

@ -130,6 +130,10 @@ html, body {
transition: all 250ms;
}
.navigation .button.active svg {
stroke: #2daa57;
}
.navigation .button:hover {
cursor: pointer;
}

File diff suppressed because one or more lines are too long

View File

@ -109,6 +109,9 @@ html, body {
& svg {
transition: all 250ms;
}
&.active svg {
stroke: #2daa57;
}
&:hover {
cursor: pointer;
& svg {

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:");

View File

@ -95,6 +95,18 @@
</svg>
</button>
</div>
<div class="buttons">
<button class="button" id="btn_toggle_encryption" title="Toggle encryption">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock" width="40"
height="40" viewBox="0 0 24 24" stroke-width="1.5" stroke="#bebebe" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<rect x="5" y="11" width="14" height="10" rx="2" />
<circle cx="12" cy="16" r="1" />
<path d="M8 11v-4a4 4 0 0 1 8 0v4" />
</svg>
</button>
</div>
</div>
<div class="container">
<div id="notifications"></div>
@ -118,6 +130,7 @@
</div>
</div>
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.2/build/highlight.min.js"></script>
<script type="text/javascript" src="https://cdn.rawgit.com/ricmoo/aes-js/e27b99df/index.js"></script>
<script src="assets/js/app.js" type="module"></script>
</body>