Skip to content

Commit 1af5b7b

Browse files
add aes256-gcm encryption
1 parent 1470658 commit 1af5b7b

File tree

2 files changed

+115
-10
lines changed

2 files changed

+115
-10
lines changed

server/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ function testRoute(fastify, opts) {
3737
// we use the encrypted session api with get/set like the normal session api
3838
const previousValue = request.encryptedSession.get("testFromClient");
3939

40+
console.log("value stored before request is processed:", request.encryptedSession.data());
41+
4042
if (query.test) {
4143
request.encryptedSession.set("testFromClient", query.test);
4244
}
4345

44-
console.log("value stored before in request:", request.encryptedSession.data());
45-
4646
request.encryptedSession.set("testKey", "testValue");
4747

4848
return { message: "Test route works!", previousValue: previousValue || "not set", currentValue: request.encryptedSession.get("testFromClient") || "not set" };

server/encrypted-session.js

Lines changed: 113 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import secureSession from "@fastify/secure-session";
22
import fp from "fastify-plugin";
33
import fastifyCookie from "@fastify/cookie";
44
import fastifySession from '@fastify/session';
5+
import crypto from "node:crypto"
56

67

78

@@ -50,19 +51,25 @@ async function encryptedSession(fastify) {
5051

5152
fastify.addHook('onRequest', (request, _reply, next) => {
5253
//we use secure-session cookie to get the encryption key and decrypt the store
53-
if (request[SECURE_SESSION_NAME].get(SECURE_COOKIE_KEY_ENCRYPTION_KEY) === undefined) {
54+
if (!request[SECURE_SESSION_NAME].get(SECURE_COOKIE_KEY_ENCRYPTION_KEY)) {
5455
console.log("encryption key not found, creating new one");
5556

56-
//TODO: create a new encrpytion key and set it in the secure session cookie
57-
request[SECURE_SESSION_NAME].set(SECURE_COOKIE_KEY_ENCRYPTION_KEY, "TODO_SHOULD_BE_RANDOM");
57+
let newEncryptionKey = generateSecureEncryptionKey();
58+
request[SECURE_SESSION_NAME].set(SECURE_COOKIE_KEY_ENCRYPTION_KEY, newEncryptionKey.toString('base64'));
5859
request[REQUEST_DECORATOR] = new Session()
60+
newEncryptionKey = undefined
5961
} else {
6062
console.log("encryption key found, using existing one");
63+
64+
const loadedEncryptionKey = Buffer.from(request[SECURE_SESSION_NAME].get(SECURE_COOKIE_KEY_ENCRYPTION_KEY), "base64");
65+
6166
const encryptedStore = request.session.get("encryptedStore");
6267
if (encryptedStore) {
6368
try {
64-
//TODO: add decrypted step
65-
const decryptedStore = JSON.parse(encryptedStore);
69+
const { cipherText, iv, tag } = encryptedStore;
70+
71+
const decryptedCypherText = decryptSymetric(cipherText, iv, tag, loadedEncryptionKey);
72+
const decryptedStore = JSON.parse(decryptedCypherText);
6673
request[REQUEST_DECORATOR] = new Session(decryptedStore);
6774
} catch (error) {
6875
console.error("Failed to parse encrypted store:", error);
@@ -87,15 +94,23 @@ async function encryptedSession(fastify) {
8794
//on send we will encrypt the store and set it in the backend-side session store
8895
console.log("Encrypted store that will be set in session:", JSON.stringify(request[REQUEST_DECORATOR].data()));
8996

90-
//TODO: encrypt the data here.
97+
const encyrptionKey = Buffer.from(request[SECURE_SESSION_NAME].get(SECURE_COOKIE_KEY_ENCRYPTION_KEY), "base64");
98+
99+
91100
//we store everything in one value in the session, that might be problematic for future redis with expiration times per key. we might want to split this
92-
const encryptedData = JSON.stringify(request[REQUEST_DECORATOR].data())
101+
const stringifiedData = JSON.stringify(request[REQUEST_DECORATOR].data())
102+
const { cipherText, iv, tag } = encryptSymetric(stringifiedData, encyrptionKey);
93103

94104
//remove unencrypted data from memory
95105
delete request[REQUEST_DECORATOR];
96106
request[REQUEST_DECORATOR] = null;
97107

98-
request.session.encryptedStore = encryptedData;
108+
request.session.encryptedStore = {
109+
cipherText,
110+
iv,
111+
tag,
112+
};
113+
console.log("Encrypted store set in session:", request.session.encryptedStore);
99114
next()
100115
})
101116

@@ -153,3 +168,93 @@ class Session {
153168
return copy
154169
}
155170
}
171+
172+
// generates a secure encryption key for aes-256-gcm.
173+
// Returns a buffer of 32 bytes (256 bits).
174+
function generateSecureEncryptionKey() {
175+
// Generates a secure random encryption key of 32 bytes (256 bits)
176+
return crypto.randomBytes(32);
177+
}
178+
179+
// uses authenticated symetric encryption (aes-256-gcm) to encrypt the plaintext with the key.
180+
// If no adequate key is given, it throws an error
181+
// The key needs to be 32bytes (256bits) as type buffer. Needs to be cryptographically secure random generated e.g. with `crypto.randomBytes(32)`
182+
// it outputs cipherText (bas64 encoded string), the initialisation vector (iv) (hex string) and the authentication tag (hex string).
183+
function encryptSymetric(plaintext, key) {
184+
if (key == undefined) {
185+
throw new Error("Key must be provided");
186+
}
187+
if (key.length < 32) {
188+
throw new Error("Key must be at least 32bye = 256 bits long");
189+
}
190+
191+
if (!(key instanceof Buffer)) {
192+
throw new Error("Key must be a Buffer");
193+
}
194+
195+
if (plaintext == undefined) {
196+
throw new Error("Plaintext must be provided");
197+
}
198+
199+
if (typeof plaintext !== "string") {
200+
throw new Error("Plaintext must be a string utf8 encoded");
201+
}
202+
203+
if (!crypto.getCiphers().includes("aes-256-gcm")) {
204+
throw new Error("Cipher suite aes-256-gcm is not available");
205+
}
206+
207+
// initialisation vector. Needs to be stored along the cipherText.
208+
// MUST NOT be reused and MUST be randomly generated for EVERY encryption operation. Otherwise using the same key would be insecure.
209+
const iv = crypto.randomBytes(12);
210+
211+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
212+
let cipherText = cipher.update(plaintext, 'utf8', 'base64');
213+
cipherText += cipher.final('base64');
214+
215+
// the authentication tag is used to verify the integrity of the ciphertext (that it has not been tampered with).
216+
// stored alongside the ciphertext and iv as it can only be changed with the secret key
217+
const tag = cipher.getAuthTag();
218+
219+
return {
220+
cipherText,
221+
iv: iv.toString('base64'),
222+
tag: tag.toString('base64'),
223+
}
224+
}
225+
226+
// uses authenticated symetric encryption (aes-256-gcm) to decrypt the ciphertext with the key.
227+
// requires the ciphertext, the initialisation vector (iv)(hex string), the authentication tag (tag) (hex string) and the key (buffer) to be provided.
228+
//it thows an error if the decryption or tag verification fails
229+
function decryptSymetric(cipherText, iv, tag, key) {
230+
if (key == undefined) {
231+
throw new Error("Key must be provided");
232+
}
233+
if (key.length < 32) {
234+
throw new Error("Key must be at least 32bye = 256 bits long");
235+
}
236+
237+
if (!(key instanceof Buffer)) {
238+
throw new Error("Key must be a Buffer");
239+
}
240+
241+
if (cipherText == undefined) {
242+
throw new Error("Ciphertext must be provided");
243+
}
244+
245+
if (typeof cipherText !== "string") {
246+
throw new Error("Ciphertext must be a string utf8 encoded");
247+
}
248+
249+
if (!crypto.getCiphers().includes("aes-256-gcm")) {
250+
throw new Error("Cipher suite aes-256-gcm is not available");
251+
}
252+
253+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, 'base64'));
254+
decipher.setAuthTag(Buffer.from(tag, 'base64'));
255+
256+
let decrypted = decipher.update(cipherText, 'base64', 'utf8');
257+
decrypted += decipher.final('utf8');
258+
259+
return decrypted;
260+
}

0 commit comments

Comments
 (0)