Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"semi": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 80,
"printWidth": 120,
"singleQuote": true
}
18 changes: 8 additions & 10 deletions server/app.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import path, { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import AutoLoad from "@fastify/autoload";
import envPlugin from "./config/env.js";
import encryptedSession from "./encrypted-session.js";
import path, { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import AutoLoad from '@fastify/autoload';
import envPlugin from './config/env.js';
import encryptedSession from './encrypted-session.js';

export const options = {};

Expand All @@ -16,14 +16,12 @@ export default async function (fastify, opts) {
});

await fastify.register(AutoLoad, {
dir: join(__dirname, "plugins"),
dir: join(__dirname, 'plugins'),
options: { ...opts },
});

await fastify.register(AutoLoad, {
dir: join(__dirname, "routes"),
dir: join(__dirname, 'routes'),
options: { ...opts },
});


}
}
120 changes: 62 additions & 58 deletions server/encrypted-session.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
import secureSession from "@fastify/secure-session";
import fp from "fastify-plugin";
import fastifyCookie from "@fastify/cookie";
import secureSession from '@fastify/secure-session';
import fp from 'fastify-plugin';
import fastifyCookie from '@fastify/cookie';
import fastifySession from '@fastify/session';
import crypto from "node:crypto"
import crypto from 'node:crypto';


// name of the request decorator this plugin exposes. Using request.encryptedSession can be used with set, get, clear delete
// name of the request decorator this plugin exposes. Using request.encryptedSession can be used with set, get, clear delete
// functions and the encryption will then be handled in this plugin.
export const REQUEST_DECORATOR = "encryptedSession";
export const REQUEST_DECORATOR = 'encryptedSession';
// name of the request decorator of the secure-session library that stores its session data in an encrypted cookie on user side.
export const ENCRYPTED_COOKIE_REQUEST_DECORATOR = "encryptedSessionInternal";
export const ENCRYPTED_COOKIE_REQUEST_DECORATOR = 'encryptedSessionInternal';
// name of the request decorator of the session library that is used as underlying store for this library.
export const UNDERLYING_SESSION_NAME_REQUEST_DECORATOR = "underlyingSessionNotPerUserEncrypted";
export const UNDERLYING_SESSION_NAME_REQUEST_DECORATOR = 'underlyingSessionNotPerUserEncrypted';

// name of the secure-session cookie that stores the encryption key on user side.
export const ENCRYPTION_KEY_COOKIE_NAME = "session_encryption_key";
export const ENCRYPTION_KEY_COOKIE_NAME = 'session_encryption_key';
// the key used to store the encryption key in the secure-session cookie on user side.
export const ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY = "encryptionKey";
export const ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY = 'encryptionKey';
// name of the cookie that stores the session identifier on user side.
export const SESSION_COOKIE_NAME = "session-cookie";
export const SESSION_COOKIE_NAME = 'session-cookie';

async function encryptedSession(fastify) {
const { COOKIE_SECRET, SESSION_SECRET, NODE_ENV } = fastify.config;

await fastify.register(fastifyCookie);

fastify.register(secureSession, {
secret: Buffer.from(COOKIE_SECRET, "hex"),
secret: Buffer.from(COOKIE_SECRET, 'hex'),
cookieName: ENCRYPTION_KEY_COOKIE_NAME,
sessionName: ENCRYPTED_COOKIE_REQUEST_DECORATOR,
cookie: {
path: "/",
path: '/',
httpOnly: true,
sameSite: "lax",
secure: NODE_ENV === "production",
sameSite: 'lax',
secure: NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 7 days
},
});
Expand All @@ -42,29 +41,29 @@ async function encryptedSession(fastify) {
cookieName: SESSION_COOKIE_NAME,
// sessionName: UNDERLYING_SESSION_NAME, //NOT POSSIBLE to change the name it is decorated on the request object
cookie: {
path: "/",
path: '/',
httpOnly: true,
sameSite: "lax",
secure: NODE_ENV === "production",
sameSite: 'lax',
secure: NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 7 days
},
});

fastify.addHook('onRequest', (request, _reply, next) => {
const userEncryptionKey = getUserEncryptionKeyFromUserCookie(request);
if (!userEncryptionKey) {
request.log.info({ "plugin": "encrypted-session" }, "user-side encryption key not found, creating new one");
request.log.info({ plugin: 'encrypted-session' }, 'user-side encryption key not found, creating new one');

let newEncryptionKey = generateSecureEncryptionKey();
setUserEncryptionKeyIntoUserCookie(request, newEncryptionKey);
request[REQUEST_DECORATOR] = createStore()
newEncryptionKey = undefined
request[REQUEST_DECORATOR] = createStore();
newEncryptionKey = undefined;
} else {
request.log.info({ "plugin": "encrypted-session" }, "user-side encryption key found, using existing one");
request.log.info({ plugin: 'encrypted-session' }, 'user-side encryption key found, using existing one');

const loadedEncryptionKey = Buffer.from(userEncryptionKey, "base64");
const loadedEncryptionKey = Buffer.from(userEncryptionKey, 'base64');

const encryptedStore = request.session.get("encryptedStore");
const encryptedStore = request.session.get('encryptedStore');
if (encryptedStore) {
try {
const { cipherText, iv, tag } = encryptedStore;
Expand All @@ -73,28 +72,31 @@ async function encryptedSession(fastify) {
const decryptedStore = JSON.parse(decryptedCypherText);
request[REQUEST_DECORATOR] = createStore(decryptedStore);
} catch (error) {
request.log.error({ "plugin": "encrypted-session" }, "Failed to parse encrypted session store", error);
request.log.error({ plugin: 'encrypted-session' }, 'Failed to parse encrypted session store', error);
request[REQUEST_DECORATOR] = createStore();
}
} else {
// we could not parse the encrypted store, so we create a new one and it would overwrite the previously stored store.
request.log.info({ "plugin": "encrypted-session" }, "No encrypted store found, creating new empty store");
request.log.info({ plugin: 'encrypted-session' }, 'No encrypted store found, creating new empty store');
request[REQUEST_DECORATOR] = createStore();
}
}

next()
})
next();
});

//TODO maybe move to onResponse after res is send. Lifecycle Doc https://fastify.dev/docs/latest/Reference/Lifecycle/
// onSend is called before the response is send. Here we take encrypt the Session object and store it in the fastify-session.
// Then we also want to make sure the unencrypted object is removed from memory
fastify.addHook('onSend', async (request, reply, _payload) => {
const encryptionKey = Buffer.from(getUserEncryptionKeyFromUserCookie(request), "base64");
fastify.addHook('onSend', async (request, _reply, payload) => {
const encryptionKey = Buffer.from(getUserEncryptionKeyFromUserCookie(request), 'base64');
if (!encryptionKey) {
// if no encryption key is found in the secure session, we cannot encrypt the store. This should not happen since an encrption key is generated when the request arrived
request.log.error({ "plugin": "encrypted-session" }, "No encryption key found in secure session, cannot encrypt store");
throw new Error("No encryption key found in secure session, cannot encrypt store");
request.log.error(
{ plugin: 'encrypted-session' },
'No encryption key found in secure session, cannot encrypt store',
);
throw new Error('No encryption key found in secure session, cannot encrypt store');
}

//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
Expand All @@ -105,17 +107,19 @@ async function encryptedSession(fastify) {
delete request[REQUEST_DECORATOR];
request[REQUEST_DECORATOR] = null;

request.session.set("encryptedStore", {
request.session.set('encryptedStore', {
cipherText,
iv,
tag,
});
await request.session.save()
request.log.info("store encrypted and set into request.session.encryptedStore");
})
await request.session.save();
request.log.info('store encrypted and set into request.session.encryptedStore');

return payload;
});

function getUserEncryptionKeyFromUserCookie(request) {
return request[ENCRYPTED_COOKIE_REQUEST_DECORATOR].get(ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY)
return request[ENCRYPTED_COOKIE_REQUEST_DECORATOR].get(ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY);
}

function setUserEncryptionKeyIntoUserCookie(request, key) {
Expand Down Expand Up @@ -163,33 +167,33 @@ function generateSecureEncryptionKey() {
// it outputs cipherText (bas64 encoded string), the initialisation vector (iv) (hex string) and the authentication tag (hex string).
function encryptSymetric(plaintext, key) {
if (key == undefined) {
throw new Error("Key must be provided");
throw new Error('Key must be provided');
}
if (key.length < 32) {
throw new Error("Key must be at least 32 byte = 256 bits long");
throw new Error('Key must be at least 32 byte = 256 bits long');
}

if (!(key instanceof Buffer)) {
throw new Error("Key must be a Buffer");
throw new Error('Key must be a Buffer');
}

if (plaintext == undefined) {
throw new Error("Plaintext must be provided");
throw new Error('Plaintext must be provided');
}

if (typeof plaintext !== "string") {
throw new Error("Plaintext must be a string utf8 encoded");
if (typeof plaintext !== 'string') {
throw new Error('Plaintext must be a string utf8 encoded');
}

if (!crypto.getCiphers().includes("aes-256-gcm")) {
throw new Error("Cipher suite aes-256-gcm is not available");
if (!crypto.getCiphers().includes('aes-256-gcm')) {
throw new Error('Cipher suite aes-256-gcm is not available');
}

// initialisation vector. Needs to be stored along the cipherText.
// MUST NOT be reused and MUST be randomly generated for EVERY encryption operation. Otherwise using the same key would be insecure.
const iv = crypto.randomBytes(12);

const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let cipherText = cipher.update(plaintext, 'utf8', 'base64');
cipherText += cipher.final('base64');

Expand All @@ -201,41 +205,41 @@ function encryptSymetric(plaintext, key) {
cipherText,
iv: iv.toString('base64'),
tag: tag.toString('base64'),
}
};
}

// uses authenticated symetric encryption (aes-256-gcm) to decrypt the ciphertext with the key.
// requires the ciphertext, the initialisation vector (iv)(hex string), the authentication tag (tag) (hex string) and the key (buffer) to be provided.
//it thows an error if the decryption or tag verification fails
function decryptSymetric(cipherText, iv, tag, key) {
if (key == undefined) {
throw new Error("Key must be provided");
throw new Error('Key must be provided');
}
if (key.length < 32) {
throw new Error("Key must be at least 32 byte = 256 bits long");
throw new Error('Key must be at least 32 byte = 256 bits long');
}

if (!(key instanceof Buffer)) {
throw new Error("Key must be a Buffer");
throw new Error('Key must be a Buffer');
}

if (cipherText == undefined) {
throw new Error("Ciphertext must be provided");
throw new Error('Ciphertext must be provided');
}

if (typeof cipherText !== "string") {
throw new Error("Ciphertext must be a string utf8 encoded");
if (typeof cipherText !== 'string') {
throw new Error('Ciphertext must be a string utf8 encoded');
}

if (!crypto.getCiphers().includes("aes-256-gcm")) {
throw new Error("Cipher suite aes-256-gcm is not available");
if (!crypto.getCiphers().includes('aes-256-gcm')) {
throw new Error('Cipher suite aes-256-gcm is not available');
}

const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(iv, 'base64'));
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'));
decipher.setAuthTag(Buffer.from(tag, 'base64'));

let decrypted = decipher.update(cipherText, 'base64', 'utf8');
decrypted += decipher.final('utf8');

return decrypted;
}
}
Loading
Loading