1- import secureSession from " @fastify/secure-session" ;
2- import fp from " fastify-plugin" ;
3- import fastifyCookie from " @fastify/cookie" ;
1+ import secureSession from ' @fastify/secure-session' ;
2+ import fp from ' fastify-plugin' ;
3+ import fastifyCookie from ' @fastify/cookie' ;
44import fastifySession from '@fastify/session' ;
5- import crypto from " node:crypto"
5+ import crypto from ' node:crypto' ;
66
7-
8- // name of the request decorator this plugin exposes. Using request.encryptedSession can be used with set, get, clear delete
7+ // name of the request decorator this plugin exposes. Using request.encryptedSession can be used with set, get, clear delete
98// functions and the encryption will then be handled in this plugin.
10- export const REQUEST_DECORATOR = " encryptedSession" ;
9+ export const REQUEST_DECORATOR = ' encryptedSession' ;
1110// name of the request decorator of the secure-session library that stores its session data in an encrypted cookie on user side.
12- export const ENCRYPTED_COOKIE_REQUEST_DECORATOR = " encryptedSessionInternal" ;
11+ export const ENCRYPTED_COOKIE_REQUEST_DECORATOR = ' encryptedSessionInternal' ;
1312// name of the request decorator of the session library that is used as underlying store for this library.
14- export const UNDERLYING_SESSION_NAME_REQUEST_DECORATOR = " underlyingSessionNotPerUserEncrypted" ;
13+ export const UNDERLYING_SESSION_NAME_REQUEST_DECORATOR = ' underlyingSessionNotPerUserEncrypted' ;
1514
1615// name of the secure-session cookie that stores the encryption key on user side.
17- export const ENCRYPTION_KEY_COOKIE_NAME = " session_encryption_key" ;
16+ export const ENCRYPTION_KEY_COOKIE_NAME = ' session_encryption_key' ;
1817// the key used to store the encryption key in the secure-session cookie on user side.
19- export const ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY = " encryptionKey" ;
18+ export const ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY = ' encryptionKey' ;
2019// name of the cookie that stores the session identifier on user side.
21- export const SESSION_COOKIE_NAME = " session-cookie" ;
20+ export const SESSION_COOKIE_NAME = ' session-cookie' ;
2221
2322async function encryptedSession ( fastify ) {
2423 const { COOKIE_SECRET , SESSION_SECRET , NODE_ENV } = fastify . config ;
2524
2625 await fastify . register ( fastifyCookie ) ;
2726
2827 fastify . register ( secureSession , {
29- secret : Buffer . from ( COOKIE_SECRET , " hex" ) ,
28+ secret : Buffer . from ( COOKIE_SECRET , ' hex' ) ,
3029 cookieName : ENCRYPTION_KEY_COOKIE_NAME ,
3130 sessionName : ENCRYPTED_COOKIE_REQUEST_DECORATOR ,
3231 cookie : {
33- path : "/" ,
32+ path : '/' ,
3433 httpOnly : true ,
35- sameSite : " lax" ,
36- secure : NODE_ENV === " production" ,
34+ sameSite : ' lax' ,
35+ secure : NODE_ENV === ' production' ,
3736 maxAge : 60 * 60 * 24 * 7 , // 7 days
3837 } ,
3938 } ) ;
@@ -42,29 +41,29 @@ async function encryptedSession(fastify) {
4241 cookieName : SESSION_COOKIE_NAME ,
4342 // sessionName: UNDERLYING_SESSION_NAME, //NOT POSSIBLE to change the name it is decorated on the request object
4443 cookie : {
45- path : "/" ,
44+ path : '/' ,
4645 httpOnly : true ,
47- sameSite : " lax" ,
48- secure : NODE_ENV === " production" ,
46+ sameSite : ' lax' ,
47+ secure : NODE_ENV === ' production' ,
4948 maxAge : 60 * 60 * 24 * 7 , // 7 days
5049 } ,
5150 } ) ;
5251
5352 fastify . addHook ( 'onRequest' , ( request , _reply , next ) => {
5453 const userEncryptionKey = getUserEncryptionKeyFromUserCookie ( request ) ;
5554 if ( ! userEncryptionKey ) {
56- request . log . info ( { " plugin" : " encrypted-session" } , " user-side encryption key not found, creating new one" ) ;
55+ request . log . info ( { plugin : ' encrypted-session' } , ' user-side encryption key not found, creating new one' ) ;
5756
5857 let newEncryptionKey = generateSecureEncryptionKey ( ) ;
5958 setUserEncryptionKeyIntoUserCookie ( request , newEncryptionKey ) ;
60- request [ REQUEST_DECORATOR ] = createStore ( )
61- newEncryptionKey = undefined
59+ request [ REQUEST_DECORATOR ] = createStore ( ) ;
60+ newEncryptionKey = undefined ;
6261 } else {
63- request . log . info ( { " plugin" : " encrypted-session" } , " user-side encryption key found, using existing one" ) ;
62+ request . log . info ( { plugin : ' encrypted-session' } , ' user-side encryption key found, using existing one' ) ;
6463
65- const loadedEncryptionKey = Buffer . from ( userEncryptionKey , " base64" ) ;
64+ const loadedEncryptionKey = Buffer . from ( userEncryptionKey , ' base64' ) ;
6665
67- const encryptedStore = request . session . get ( " encryptedStore" ) ;
66+ const encryptedStore = request . session . get ( ' encryptedStore' ) ;
6867 if ( encryptedStore ) {
6968 try {
7069 const { cipherText, iv, tag } = encryptedStore ;
@@ -73,28 +72,31 @@ async function encryptedSession(fastify) {
7372 const decryptedStore = JSON . parse ( decryptedCypherText ) ;
7473 request [ REQUEST_DECORATOR ] = createStore ( decryptedStore ) ;
7574 } catch ( error ) {
76- request . log . error ( { " plugin" : " encrypted-session" } , " Failed to parse encrypted session store" , error ) ;
75+ request . log . error ( { plugin : ' encrypted-session' } , ' Failed to parse encrypted session store' , error ) ;
7776 request [ REQUEST_DECORATOR ] = createStore ( ) ;
7877 }
7978 } else {
8079 // we could not parse the encrypted store, so we create a new one and it would overwrite the previously stored store.
81- request . log . info ( { " plugin" : " encrypted-session" } , " No encrypted store found, creating new empty store" ) ;
80+ request . log . info ( { plugin : ' encrypted-session' } , ' No encrypted store found, creating new empty store' ) ;
8281 request [ REQUEST_DECORATOR ] = createStore ( ) ;
8382 }
8483 }
8584
86- next ( )
87- } )
85+ next ( ) ;
86+ } ) ;
8887
8988 //TODO maybe move to onResponse after res is send. Lifecycle Doc https://fastify.dev/docs/latest/Reference/Lifecycle/
9089 // onSend is called before the response is send. Here we take encrypt the Session object and store it in the fastify-session.
9190 // Then we also want to make sure the unencrypted object is removed from memory
92- fastify . addHook ( 'onSend' , async ( request , reply , _payload ) => {
93- const encryptionKey = Buffer . from ( getUserEncryptionKeyFromUserCookie ( request ) , " base64" ) ;
91+ fastify . addHook ( 'onSend' , async ( request , _reply , payload ) => {
92+ const encryptionKey = Buffer . from ( getUserEncryptionKeyFromUserCookie ( request ) , ' base64' ) ;
9493 if ( ! encryptionKey ) {
9594 // 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
96- request . log . error ( { "plugin" : "encrypted-session" } , "No encryption key found in secure session, cannot encrypt store" ) ;
97- throw new Error ( "No encryption key found in secure session, cannot encrypt store" ) ;
95+ request . log . error (
96+ { plugin : 'encrypted-session' } ,
97+ 'No encryption key found in secure session, cannot encrypt store' ,
98+ ) ;
99+ throw new Error ( 'No encryption key found in secure session, cannot encrypt store' ) ;
98100 }
99101
100102 //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
@@ -105,17 +107,19 @@ async function encryptedSession(fastify) {
105107 delete request [ REQUEST_DECORATOR ] ;
106108 request [ REQUEST_DECORATOR ] = null ;
107109
108- request . session . set ( " encryptedStore" , {
110+ request . session . set ( ' encryptedStore' , {
109111 cipherText,
110112 iv,
111113 tag,
112114 } ) ;
113- await request . session . save ( )
114- request . log . info ( "store encrypted and set into request.session.encryptedStore" ) ;
115- } )
115+ await request . session . save ( ) ;
116+ request . log . info ( 'store encrypted and set into request.session.encryptedStore' ) ;
117+
118+ return payload ;
119+ } ) ;
116120
117121 function getUserEncryptionKeyFromUserCookie ( request ) {
118- return request [ ENCRYPTED_COOKIE_REQUEST_DECORATOR ] . get ( ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY )
122+ return request [ ENCRYPTED_COOKIE_REQUEST_DECORATOR ] . get ( ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY ) ;
119123 }
120124
121125 function setUserEncryptionKeyIntoUserCookie ( request , key ) {
@@ -163,33 +167,33 @@ function generateSecureEncryptionKey() {
163167// it outputs cipherText (bas64 encoded string), the initialisation vector (iv) (hex string) and the authentication tag (hex string).
164168function encryptSymetric ( plaintext , key ) {
165169 if ( key == undefined ) {
166- throw new Error ( " Key must be provided" ) ;
170+ throw new Error ( ' Key must be provided' ) ;
167171 }
168172 if ( key . length < 32 ) {
169- throw new Error ( " Key must be at least 32 byte = 256 bits long" ) ;
173+ throw new Error ( ' Key must be at least 32 byte = 256 bits long' ) ;
170174 }
171175
172176 if ( ! ( key instanceof Buffer ) ) {
173- throw new Error ( " Key must be a Buffer" ) ;
177+ throw new Error ( ' Key must be a Buffer' ) ;
174178 }
175179
176180 if ( plaintext == undefined ) {
177- throw new Error ( " Plaintext must be provided" ) ;
181+ throw new Error ( ' Plaintext must be provided' ) ;
178182 }
179183
180- if ( typeof plaintext !== " string" ) {
181- throw new Error ( " Plaintext must be a string utf8 encoded" ) ;
184+ if ( typeof plaintext !== ' string' ) {
185+ throw new Error ( ' Plaintext must be a string utf8 encoded' ) ;
182186 }
183187
184- if ( ! crypto . getCiphers ( ) . includes ( " aes-256-gcm" ) ) {
185- throw new Error ( " Cipher suite aes-256-gcm is not available" ) ;
188+ if ( ! crypto . getCiphers ( ) . includes ( ' aes-256-gcm' ) ) {
189+ throw new Error ( ' Cipher suite aes-256-gcm is not available' ) ;
186190 }
187191
188192 // initialisation vector. Needs to be stored along the cipherText.
189193 // MUST NOT be reused and MUST be randomly generated for EVERY encryption operation. Otherwise using the same key would be insecure.
190194 const iv = crypto . randomBytes ( 12 ) ;
191195
192- const cipher = crypto . createCipheriv ( " aes-256-gcm" , key , iv ) ;
196+ const cipher = crypto . createCipheriv ( ' aes-256-gcm' , key , iv ) ;
193197 let cipherText = cipher . update ( plaintext , 'utf8' , 'base64' ) ;
194198 cipherText += cipher . final ( 'base64' ) ;
195199
@@ -201,41 +205,41 @@ function encryptSymetric(plaintext, key) {
201205 cipherText,
202206 iv : iv . toString ( 'base64' ) ,
203207 tag : tag . toString ( 'base64' ) ,
204- }
208+ } ;
205209}
206210
207211// uses authenticated symetric encryption (aes-256-gcm) to decrypt the ciphertext with the key.
208212// requires the ciphertext, the initialisation vector (iv)(hex string), the authentication tag (tag) (hex string) and the key (buffer) to be provided.
209213//it thows an error if the decryption or tag verification fails
210214function decryptSymetric ( cipherText , iv , tag , key ) {
211215 if ( key == undefined ) {
212- throw new Error ( " Key must be provided" ) ;
216+ throw new Error ( ' Key must be provided' ) ;
213217 }
214218 if ( key . length < 32 ) {
215- throw new Error ( " Key must be at least 32 byte = 256 bits long" ) ;
219+ throw new Error ( ' Key must be at least 32 byte = 256 bits long' ) ;
216220 }
217221
218222 if ( ! ( key instanceof Buffer ) ) {
219- throw new Error ( " Key must be a Buffer" ) ;
223+ throw new Error ( ' Key must be a Buffer' ) ;
220224 }
221225
222226 if ( cipherText == undefined ) {
223- throw new Error ( " Ciphertext must be provided" ) ;
227+ throw new Error ( ' Ciphertext must be provided' ) ;
224228 }
225229
226- if ( typeof cipherText !== " string" ) {
227- throw new Error ( " Ciphertext must be a string utf8 encoded" ) ;
230+ if ( typeof cipherText !== ' string' ) {
231+ throw new Error ( ' Ciphertext must be a string utf8 encoded' ) ;
228232 }
229233
230- if ( ! crypto . getCiphers ( ) . includes ( " aes-256-gcm" ) ) {
231- throw new Error ( " Cipher suite aes-256-gcm is not available" ) ;
234+ if ( ! crypto . getCiphers ( ) . includes ( ' aes-256-gcm' ) ) {
235+ throw new Error ( ' Cipher suite aes-256-gcm is not available' ) ;
232236 }
233237
234- const decipher = crypto . createDecipheriv ( " aes-256-gcm" , key , Buffer . from ( iv , 'base64' ) ) ;
238+ const decipher = crypto . createDecipheriv ( ' aes-256-gcm' , key , Buffer . from ( iv , 'base64' ) ) ;
235239 decipher . setAuthTag ( Buffer . from ( tag , 'base64' ) ) ;
236240
237241 let decrypted = decipher . update ( cipherText , 'base64' , 'utf8' ) ;
238242 decrypted += decipher . final ( 'utf8' ) ;
239243
240244 return decrypted ;
241- }
245+ }
0 commit comments