Skip to content

Commit 1470658

Browse files
add first rough poc of user-side session encryption
1 parent da60c07 commit 1470658

File tree

2 files changed

+190
-8
lines changed

2 files changed

+190
-8
lines changed

server/app.js

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,49 @@ import path, { join, dirname } from "node:path";
22
import { fileURLToPath } from "node:url";
33
import AutoLoad from "@fastify/autoload";
44
import envPlugin from "./config/env.js";
5+
import encryptedSession from "./encrypted-session.js";
56

67
export const options = {};
78

89
const __filename = fileURLToPath(import.meta.url);
910
const __dirname = dirname(__filename);
1011

11-
export default async function(fastify, opts) {
12+
export default async function (fastify, opts) {
1213
await fastify.register(envPlugin);
14+
await fastify.register(testRoute)
1315

14-
await fastify.register(AutoLoad, {
15-
dir: join(__dirname, "plugins"),
16-
options: { ...opts },
16+
// await fastify.register(AutoLoad, {
17+
// dir: join(__dirname, "plugins"),
18+
// options: { ...opts },
19+
// });
20+
21+
// await fastify.register(AutoLoad, {
22+
// dir: join(__dirname, "routes"),
23+
// options: { ...opts },
24+
// });
25+
}
26+
27+
function testRoute(fastify, opts) {
28+
fastify.register(encryptedSession, {
29+
...opts,
1730
});
1831

19-
await fastify.register(AutoLoad, {
20-
dir: join(__dirname, "routes"),
21-
options: { ...opts },
32+
// this route basically stores the query parameter test in the encrypted session store and reads it on subequent requests
33+
fastify.get("/test", async (request, reply) => {
34+
// read query param
35+
const { query } = request;
36+
37+
// we use the encrypted session api with get/set like the normal session api
38+
const previousValue = request.encryptedSession.get("testFromClient");
39+
40+
if (query.test) {
41+
request.encryptedSession.set("testFromClient", query.test);
42+
}
43+
44+
console.log("value stored before in request:", request.encryptedSession.data());
45+
46+
request.encryptedSession.set("testKey", "testValue");
47+
48+
return { message: "Test route works!", previousValue: previousValue || "not set", currentValue: request.encryptedSession.get("testFromClient") || "not set" };
2249
});
23-
}
50+
}

server/encrypted-session.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import secureSession from "@fastify/secure-session";
2+
import fp from "fastify-plugin";
3+
import fastifyCookie from "@fastify/cookie";
4+
import fastifySession from '@fastify/session';
5+
6+
7+
8+
export const COOKIE_NAME_ENCRYPTION_KEY = "session_encrpytion_key";
9+
export const COOKIE_NAME_SESSION = "session-cookie";
10+
11+
export const SECURE_SESSION_NAME = "encryptedSessionInternal";
12+
export const UNDERLYING_SESSION_NAME = "underlyingSessionNotPerUserEncrypted";
13+
14+
// This is the key used to store the encryption key in the secure session cookie
15+
export const SECURE_COOKIE_KEY_ENCRYPTION_KEY = "encryptionKey";
16+
17+
export const REQUEST_DECORATOR = "encryptedSession";
18+
19+
async function encryptedSession(fastify) {
20+
const { COOKIE_SECRET, NODE_ENV } = fastify.config;
21+
22+
await fastify.register(fastifyCookie);
23+
24+
fastify.register(secureSession, {
25+
secret: Buffer.from(COOKIE_SECRET, "hex"),
26+
cookieName: COOKIE_NAME_ENCRYPTION_KEY,
27+
sessionName: SECURE_SESSION_NAME,
28+
cookie: {
29+
path: "/",
30+
httpOnly: true,
31+
sameSite: "lax",
32+
secure: NODE_ENV === "production",
33+
maxAge: 60 * 60 * 24 * 7, // 7 days
34+
},
35+
});
36+
37+
38+
fastify.register(fastifySession, {
39+
secret: "test-secret-32-char-or-longerasdasdasdasdasdasdasdasdasdasd",
40+
cookieName: COOKIE_NAME_SESSION,
41+
// sessionName: UNDERLYING_SESSION_NAME, //NOT POSSIBLE to change the name it is decorated on the request object
42+
cookie: {
43+
path: "/",
44+
httpOnly: true,
45+
sameSite: "lax",
46+
secure: NODE_ENV === "production",
47+
maxAge: 60 * 60 * 24 * 7, // 7 days
48+
},
49+
});
50+
51+
fastify.addHook('onRequest', (request, _reply, next) => {
52+
//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+
console.log("encryption key not found, creating new one");
55+
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");
58+
request[REQUEST_DECORATOR] = new Session()
59+
} else {
60+
console.log("encryption key found, using existing one");
61+
const encryptedStore = request.session.get("encryptedStore");
62+
if (encryptedStore) {
63+
try {
64+
//TODO: add decrypted step
65+
const decryptedStore = JSON.parse(encryptedStore);
66+
request[REQUEST_DECORATOR] = new Session(decryptedStore);
67+
} catch (error) {
68+
console.error("Failed to parse encrypted store:", error);
69+
request[REQUEST_DECORATOR] = new Session();
70+
}
71+
} else {
72+
// we could not parse the encrypted store, so we create a new one and it would overwrite the previously stored store.
73+
console.log("No encrypted store found, creating new session");
74+
request[REQUEST_DECORATOR] = new Session();
75+
}
76+
}
77+
78+
next()
79+
})
80+
81+
//TODO maybe move to onResponse after res is send. Lifecycle Doc https://fastify.dev/docs/latest/Reference/Lifecycle/
82+
// onSend is called before the response is send. Here we take encrypt the Session object and store it in the fastify-session.
83+
// Then we also want to make sure the unencrypted object is removed from memory
84+
fastify.addHook('onSend', (request, reply, _payload, next) => {
85+
console.log("onSend hook called", request[REQUEST_DECORATOR].data());
86+
87+
//on send we will encrypt the store and set it in the backend-side session store
88+
console.log("Encrypted store that will be set in session:", JSON.stringify(request[REQUEST_DECORATOR].data()));
89+
90+
//TODO: encrypt the data here.
91+
//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())
93+
94+
//remove unencrypted data from memory
95+
delete request[REQUEST_DECORATOR];
96+
request[REQUEST_DECORATOR] = null;
97+
98+
request.session.encryptedStore = encryptedData;
99+
next()
100+
})
101+
102+
103+
}
104+
105+
export default fp(encryptedSession);
106+
107+
// maybe use a closure to encapsulate the session data so noone can reference it and we are the only ones keeping a reference
108+
function createEncryptedSession(previousValue) {
109+
let encryptedStore = {}; // Private variable
110+
if (previousValue) {
111+
encryptedStore = previousValue;
112+
}
113+
return {
114+
set(key, value) {
115+
encryptedStore[key] = value;
116+
},
117+
get(key) {
118+
return encryptedStore[key];
119+
},
120+
delete(key) {
121+
delete encryptedStore[key];
122+
},
123+
clear() {
124+
encryptedStore = {}; // Clear all data
125+
},
126+
};
127+
}
128+
129+
class Session {
130+
#data;
131+
132+
constructor(obj) {
133+
this.#data = obj || {}
134+
}
135+
136+
get(key) {
137+
return this.#data[key]
138+
}
139+
140+
set(key, value) {
141+
this.#data[key] = value
142+
}
143+
144+
delete(key) {
145+
this.#data[key] = undefined
146+
}
147+
148+
data() {
149+
const copy = {
150+
...this.#data
151+
}
152+
153+
return copy
154+
}
155+
}

0 commit comments

Comments
 (0)