/
index.js
159 lines (128 loc) · 4.15 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
'use strict'
const fp = require('fastify-plugin')
const sodium = require('sodium-native')
const kObj = Symbol('object')
module.exports = fp(function (fastify, options, next) {
var key
var salt
if (options.secret) {
if (Buffer.byteLength(options.secret) < 32) {
return next(new Error('secret must be at least 32 bytes'))
}
key = Buffer.allocUnsafe(sodium.crypto_secretbox_KEYBYTES)
salt = Buffer.alloc(sodium.crypto_pwhash_SALTBYTES)
if (options.salt) {
salt = Buffer.from(options.salt, 'ascii')
} else {
sodium.randombytes_buf(salt)
}
if (Buffer.byteLength(salt) !== sodium.crypto_pwhash_SALTBYTES) {
return next(new Error('salt must be length ' + sodium.crypto_pwhash_SALTBYTES))
}
sodium.crypto_pwhash(key,
Buffer.from(options.secret),
salt,
sodium.crypto_pwhash_OPSLIMIT_MODERATE,
sodium.crypto_pwhash_MEMLIMIT_MODERATE,
sodium.crypto_pwhash_ALG_DEFAULT)
}
if (options.key) {
key = options.key
if (typeof key === 'string') {
key = Buffer.from(key, 'base64')
} else if (!(key instanceof Buffer)) {
return next(new Error('key must be a string or a Buffer'))
}
if (key.length < sodium.crypto_secretbox_KEYBYTES) {
return next(new Error(`key must be at least ${sodium.crypto_secretbox_KEYBYTES} bytes`))
}
}
if (!key) {
return next(new Error('key or secret must specified'))
}
const cookieName = options.cookieName || 'session'
const cookieOptions = options.cookieOptions || options.cookie || {}
// just to add something to the shape
// TODO verify if it helps the perf
fastify.decorateRequest('session', null)
fastify
.register(require('fastify-cookie'))
.register(fp(addHooks))
next()
function addHooks (fastify, options, next) {
// the hooks must be registered after fastify-cookie hooks
fastify.addHook('onRequest', function decodeSession (request, reply, next) {
const cookie = request.cookies[cookieName]
if (cookie === undefined) {
// there is no cookie
request.session = new Session({})
next()
return
}
// do not use destructuring or it will deopt
const split = cookie.split(';')
const cyphertextB64 = split[0]
const nonceB64 = split[1]
const cipher = Buffer.from(cyphertextB64, 'base64')
const nonce = Buffer.from(nonceB64, 'base64')
if (cipher.length < sodium.crypto_secretbox_MACBYTES) {
// not long enough
request.session = new Session({})
next()
return
}
const msg = Buffer.allocUnsafe(cipher.length - sodium.crypto_secretbox_MACBYTES)
if (!sodium.crypto_secretbox_open_easy(msg, cipher, nonce, key)) {
// unable to decrypt
request.session = new Session({})
next()
return
}
request.session = new Session(JSON.parse(msg))
next()
})
fastify.addHook('onSend', function encodeSession (request, reply, payload, next) {
const session = request.session
if (!session || !session.changed) {
// nothing to do
next()
return
} else if (session.deleted) {
const tmpCookieOptions = Object.assign({}, cookieOptions, { expires: new Date(0), maxAge: 0 })
reply.setCookie(cookieName, '', tmpCookieOptions)
next()
return
}
const nonce = genNonce()
const msg = Buffer.from(JSON.stringify(session[kObj]))
const cipher = Buffer.allocUnsafe(msg.length + sodium.crypto_secretbox_MACBYTES)
sodium.crypto_secretbox_easy(cipher, msg, nonce, key)
reply.setCookie(cookieName, cipher.toString('base64') + ';' + nonce.toString('base64'), cookieOptions)
next()
})
next()
}
}, '>= 0.35.0')
class Session {
constructor (obj) {
this[kObj] = obj
this.changed = false
this.deleted = false
}
get (key) {
return this[kObj][key]
}
set (key, value) {
this.changed = true
this[kObj][key] = value
}
delete () {
this.changed = true
this.deleted = true
}
}
function genNonce () {
var buf = Buffer.allocUnsafe(sodium.crypto_secretbox_NONCEBYTES)
sodium.randombytes_buf(buf)
return buf
}