Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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