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)
|
* `metadata` (key-value store)
|
||||||
* Different frontends may store simple key-value metadata pairs on pastes to enable specific functionality (for example clientside encryption)
|
* 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
|
## Endpoints
|
||||||
|
|
||||||
### [UNSECURED] Retrieve application information
|
### [UNSECURED] Retrieve application information
|
||||||
|
@ -130,6 +130,10 @@ html, body {
|
|||||||
transition: all 250ms;
|
transition: all 250ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation .button.active svg {
|
||||||
|
stroke: #2daa57;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation .button:hover {
|
.navigation .button:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
@ -109,6 +109,9 @@ html, body {
|
|||||||
& svg {
|
& svg {
|
||||||
transition: all 250ms;
|
transition: all 250ms;
|
||||||
}
|
}
|
||||||
|
&.active svg {
|
||||||
|
stroke: #2daa57;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
& svg {
|
& 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 Notifications from "./notifications.js";
|
||||||
import * as Spinner from "./spinner.js";
|
import * as Spinner from "./spinner.js";
|
||||||
import * as Animation from "./animation.js";
|
import * as Animation from "./animation.js";
|
||||||
|
import * as Encryption from "./encryption.js";
|
||||||
|
|
||||||
const CODE_ELEMENT = document.getElementById("code");
|
const CODE_ELEMENT = document.getElementById("code");
|
||||||
const LINE_NUMBERS_ELEMENT = document.getElementById("linenos");
|
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_CANCEL_ELEMENT = document.getElementById("btn_edit_cancel");
|
||||||
const BUTTON_EDIT_APPLY_ELEMENT = document.getElementById("btn_edit_apply");
|
const BUTTON_EDIT_APPLY_ELEMENT = document.getElementById("btn_edit_apply");
|
||||||
|
|
||||||
|
const BUTTON_TOGGLE_ENCRYPTION_ELEMENT = document.getElementById("btn_toggle_encryption");
|
||||||
|
|
||||||
let PASTE_ID;
|
let PASTE_ID;
|
||||||
let LANGUAGE;
|
let LANGUAGE;
|
||||||
let CODE;
|
let CODE;
|
||||||
|
|
||||||
|
let ENCRYPTION_KEY;
|
||||||
|
let ENCRYPTION_IV;
|
||||||
|
|
||||||
let EDIT_MODE = false;
|
let EDIT_MODE = false;
|
||||||
|
|
||||||
let API_INFORMATION = {
|
let API_INFORMATION = {
|
||||||
@ -39,6 +45,11 @@ export async function initialize() {
|
|||||||
setupButtonFunctionality();
|
setupButtonFunctionality();
|
||||||
setupKeybinds();
|
setupKeybinds();
|
||||||
|
|
||||||
|
// Enable encryption if enabled from last session
|
||||||
|
if (localStorage.getItem("encryption") === "true") {
|
||||||
|
BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
if (location.pathname !== "/") {
|
if (location.pathname !== "/") {
|
||||||
// Extract the paste data (ID and language)
|
// Extract the paste data (ID and language)
|
||||||
const split = location.pathname.replace("/", "").split(".");
|
const split = location.pathname.replace("/", "").split(".");
|
||||||
@ -56,7 +67,26 @@ export async function initialize() {
|
|||||||
// Set the persistent paste data
|
// Set the persistent paste data
|
||||||
PASTE_ID = pasteID;
|
PASTE_ID = pasteID;
|
||||||
LANGUAGE = language;
|
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
|
// Fill the code block with the just received data
|
||||||
updateCode();
|
updateCode();
|
||||||
@ -231,8 +261,24 @@ function setupButtonFunctionality() {
|
|||||||
return;
|
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
|
// Try to create the paste
|
||||||
const response = await API.createPaste(INPUT_ELEMENT.value);
|
const response = await API.createPaste(value, metadata);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
Notifications.error("Error while creating paste: <b>" + await response.text() + "</b>");
|
Notifications.error("Error while creating paste: <b>" + await response.text() + "</b>");
|
||||||
return;
|
return;
|
||||||
@ -245,7 +291,7 @@ function setupButtonFunctionality() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirect the user to his newly created paste
|
// 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;
|
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
|
// 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) {
|
if (!response.ok) {
|
||||||
Notifications.error("Error while editing paste: <b>" + await response.text() + "</b>");
|
Notifications.error("Error while editing paste: <b>" + await response.text() + "</b>");
|
||||||
return;
|
return;
|
||||||
@ -311,6 +364,11 @@ function setupButtonFunctionality() {
|
|||||||
Notifications.success("Successfully edited paste.");
|
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 () => {
|
BUTTON_REPORT_ELEMENT.addEventListener("click", async () => {
|
||||||
// Ask the user for a reason
|
// Ask the user for a reason
|
||||||
const reason = prompt("Reason:");
|
const reason = prompt("Reason:");
|
||||||
|
@ -95,6 +95,18 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="notifications"></div>
|
<div id="notifications"></div>
|
||||||
@ -118,6 +130,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.1.2/build/highlight.min.js"></script>
|
<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>
|
<script src="assets/js/app.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user