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:
parent
d4e3430feb
commit
4f3b5b193b
17
API.md
17
API.md
@ -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
|
||||
|
@ -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
@ -109,6 +109,9 @@ html, body {
|
||||
& svg {
|
||||
transition: all 250ms;
|
||||
}
|
||||
&.active svg {
|
||||
stroke: #2daa57;
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
& svg {
|
||||
|
60
web/assets/js/modules/encryption.js
Normal file
60
web/assets/js/modules/encryption.js
Normal 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));
|
||||
}
|
@ -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:");
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user