Skip to content

Commit 22f6c46

Browse files
authored
Refine secret key handling for better flexibility and security (#220)
* Refine secret key handling for better flexibility and security * Improve secret derivation with cryptographic hashing
1 parent ca28d71 commit 22f6c46

File tree

3 files changed

+260
-5
lines changed

3 files changed

+260
-5
lines changed

index.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,12 @@ function fastifySecureSession (fastify, options, next) {
5858
}
5959

6060
let key
61-
if (sessionOptions.secret) {
61+
62+
if (sessionOptions.secret && !sessionOptions.key) {
6263
if (Buffer.byteLength(sessionOptions.secret) < 32) {
6364
return next(new Error('secret must be at least 32 bytes'))
6465
}
6566

66-
if (!defaultSecret) {
67-
defaultSecret = sessionOptions.secret
68-
}
69-
7067
key = Buffer.allocUnsafe(sodium.crypto_secretbox_KEYBYTES)
7168

7269
// static salt to be used for key derivation, not great for security,
@@ -87,6 +84,8 @@ function fastifySecureSession (fastify, options, next) {
8784
sodium.crypto_pwhash_OPSLIMIT_MODERATE,
8885
sodium.crypto_pwhash_MEMLIMIT_MODERATE,
8986
sodium.crypto_pwhash_ALG_DEFAULT)
87+
88+
defaultSecret = sessionOptions.secret
9089
}
9190

9291
if (sessionOptions.key) {
@@ -108,6 +107,16 @@ function fastifySecureSession (fastify, options, next) {
108107
} else if (Array.isArray(key) && key.every(isBufferKeyLengthInvalid)) {
109108
return next(new Error(`key lengths must be ${sodium.crypto_secretbox_KEYBYTES} bytes`))
110109
}
110+
111+
const outputHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
112+
113+
if (Array.isArray(key)) {
114+
sodium.crypto_generichash(outputHash, key[0])
115+
} else {
116+
sodium.crypto_generichash(outputHash, key)
117+
}
118+
119+
defaultSecret = outputHash.toString('hex')
111120
}
112121

113122
if (!key) {

test/key-rotation.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,169 @@ tap.test('does not support an empty key array', async t => {
161161

162162
await t.rejects(() => fastify.after())
163163
})
164+
165+
tap.test('signing works with only a string key array', function (t) {
166+
const fastify = Fastify({ logger: false })
167+
168+
const key1 = Buffer.alloc(sodium.crypto_secretbox_KEYBYTES)
169+
sodium.randombytes_buf(key1)
170+
171+
const key2 = Buffer.alloc(sodium.crypto_secretbox_KEYBYTES)
172+
sodium.randombytes_buf(key2)
173+
174+
fastify.register(fastifySecureSession, {
175+
key: [key1.toString('base64'), key2.toString('base64')]
176+
})
177+
178+
fastify.post('/', (request, reply) => {
179+
request.session.set('data', request.body)
180+
reply.setCookie('my-session', JSON.stringify(request.body), {
181+
httpOnly: true,
182+
secure: true,
183+
maxAge: 3600,
184+
signed: true,
185+
path: '/'
186+
})
187+
reply.send('session set')
188+
})
189+
190+
fastify.get('/secure-session', (request, reply) => {
191+
const data = request.session.get('data')
192+
if (!data) {
193+
reply.code(404).send()
194+
return
195+
}
196+
reply.send(data)
197+
})
198+
199+
fastify.get('/cookie-signed', (request, reply) => {
200+
const data = request.unsignCookie(request.cookies['my-session'])
201+
if (!data.valid) {
202+
reply.code(404).send()
203+
return
204+
}
205+
reply.send(data.value)
206+
})
207+
208+
t.teardown(fastify.close.bind(fastify))
209+
t.plan(7)
210+
211+
fastify.inject({
212+
method: 'POST',
213+
url: '/',
214+
payload: {
215+
some: 'data'
216+
}
217+
}, (error, response) => {
218+
t.error(error)
219+
t.equal(response.statusCode, 200)
220+
t.ok(response.headers['set-cookie'])
221+
222+
const cookieHeader = response.headers['set-cookie'].join(';')
223+
224+
fastify.inject({
225+
method: 'GET',
226+
url: '/secure-session',
227+
headers: {
228+
cookie: cookieHeader
229+
}
230+
}, (error, response) => {
231+
t.error(error)
232+
t.same(JSON.parse(response.payload), { some: 'data' })
233+
234+
fastify.inject({
235+
method: 'GET',
236+
url: '/cookie-signed',
237+
headers: {
238+
cookie: cookieHeader
239+
}
240+
}, (error, response) => {
241+
t.error(error)
242+
t.same(JSON.parse(response.payload), { some: 'data' })
243+
})
244+
})
245+
})
246+
})
247+
248+
tap.test('signing works with only a buffer key array', function (t) {
249+
const fastify = Fastify({ logger: false })
250+
251+
const key1 = Buffer.alloc(sodium.crypto_secretbox_KEYBYTES)
252+
sodium.randombytes_buf(key1)
253+
254+
const key2 = Buffer.alloc(sodium.crypto_secretbox_KEYBYTES)
255+
sodium.randombytes_buf(key2)
256+
257+
fastify.register(fastifySecureSession, {
258+
key: [key1, key2]
259+
})
260+
261+
fastify.post('/', (request, reply) => {
262+
request.session.set('data', request.body)
263+
reply.setCookie('my-session', JSON.stringify(request.body), {
264+
httpOnly: true,
265+
secure: true,
266+
maxAge: 3600,
267+
signed: true,
268+
path: '/'
269+
})
270+
reply.send('session set')
271+
})
272+
273+
fastify.get('/secure-session', (request, reply) => {
274+
const data = request.session.get('data')
275+
if (!data) {
276+
reply.code(404).send()
277+
return
278+
}
279+
reply.send(data)
280+
})
281+
282+
fastify.get('/cookie-signed', (request, reply) => {
283+
const data = request.unsignCookie(request.cookies['my-session'])
284+
if (!data.valid) {
285+
reply.code(404).send()
286+
return
287+
}
288+
reply.send(data.value)
289+
})
290+
291+
t.teardown(fastify.close.bind(fastify))
292+
t.plan(7)
293+
294+
fastify.inject({
295+
method: 'POST',
296+
url: '/',
297+
payload: {
298+
some: 'data'
299+
}
300+
}, (error, response) => {
301+
t.error(error)
302+
t.equal(response.statusCode, 200)
303+
t.ok(response.headers['set-cookie'])
304+
305+
const cookieHeader = response.headers['set-cookie'].join(';')
306+
307+
fastify.inject({
308+
method: 'GET',
309+
url: '/secure-session',
310+
headers: {
311+
cookie: cookieHeader
312+
}
313+
}, (error, response) => {
314+
t.error(error)
315+
t.same(JSON.parse(response.payload), { some: 'data' })
316+
317+
fastify.inject({
318+
method: 'GET',
319+
url: '/cookie-signed',
320+
headers: {
321+
cookie: cookieHeader
322+
}
323+
}, (error, response) => {
324+
t.error(error)
325+
t.same(JSON.parse(response.payload), { some: 'data' })
326+
})
327+
})
328+
})
329+
})

test/key.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,83 @@ tap.test('does not support key length greater than sodium "crypto_secretbox_KEYB
135135

136136
await t.rejects(() => fastify.after())
137137
})
138+
139+
tap.test('signing works with only a key', function (t) {
140+
const fastify = Fastify({ logger: false })
141+
const key = Buffer.alloc(sodium.crypto_secretbox_KEYBYTES)
142+
143+
sodium.randombytes_buf(key)
144+
145+
fastify.register(fastifySecureSession, {
146+
key
147+
})
148+
149+
fastify.post('/', (request, reply) => {
150+
request.session.set('data', request.body)
151+
reply.setCookie('my-session', JSON.stringify(request.body), {
152+
httpOnly: true,
153+
secure: true,
154+
maxAge: 3600,
155+
signed: true,
156+
path: '/'
157+
})
158+
reply.send('session set')
159+
})
160+
161+
fastify.get('/secure-session', (request, reply) => {
162+
const data = request.session.get('data')
163+
if (!data) {
164+
reply.code(404).send()
165+
return
166+
}
167+
reply.send(data)
168+
})
169+
170+
fastify.get('/cookie-signed', (request, reply) => {
171+
const data = request.unsignCookie(request.cookies['my-session'])
172+
if (!data.valid) {
173+
reply.code(404).send()
174+
return
175+
}
176+
reply.send(data.value)
177+
})
178+
179+
t.teardown(fastify.close.bind(fastify))
180+
t.plan(7)
181+
182+
fastify.inject({
183+
method: 'POST',
184+
url: '/',
185+
payload: {
186+
some: 'data'
187+
}
188+
}, (error, response) => {
189+
t.error(error)
190+
t.equal(response.statusCode, 200)
191+
t.ok(response.headers['set-cookie'])
192+
193+
const cookieHeader = response.headers['set-cookie'].join(';')
194+
195+
fastify.inject({
196+
method: 'GET',
197+
url: '/secure-session',
198+
headers: {
199+
cookie: cookieHeader
200+
}
201+
}, (error, response) => {
202+
t.error(error)
203+
t.same(JSON.parse(response.payload), { some: 'data' })
204+
205+
fastify.inject({
206+
method: 'GET',
207+
url: '/cookie-signed',
208+
headers: {
209+
cookie: cookieHeader
210+
}
211+
}, (error, response) => {
212+
t.error(error)
213+
t.same(JSON.parse(response.payload), { some: 'data' })
214+
})
215+
})
216+
})
217+
})

0 commit comments

Comments
 (0)