@@ -2,6 +2,7 @@ import secureSession from "@fastify/secure-session";
22import fp from "fastify-plugin" ;
33import fastifyCookie from "@fastify/cookie" ;
44import 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