forked from bcgov/common-hosted-form-service
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add encryption headers and proxy endpoint for external API
Signed-off-by: Jason Sherman <[email protected]>
- Loading branch information
1 parent
7bcb9cd
commit e6c0461
Showing
12 changed files
with
249 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
const crypto = require('crypto'); | ||
|
||
const ENCRYPTION_TYPES = { | ||
AES_256_GCM: 'aes-256-gcm', | ||
}; | ||
|
||
class Encryption { | ||
// eslint-disable-next-line no-unused-vars | ||
encrypt(payload, masterkey) { | ||
throw new Error('encrypt must be overridden.'); | ||
} | ||
// eslint-disable-next-line no-unused-vars | ||
decrypt(encdata, masterkey) { | ||
throw new Error('decrypt must be overridden.'); | ||
} | ||
} | ||
|
||
class Aes256Gcm extends Encryption { | ||
// | ||
// For a masterkey: | ||
// we want a sha256 hash: 256 bits/32 bytes/64 characters | ||
// to generate: | ||
// crypto.createHash('sha256').update("sometext").digest('hex'); | ||
// | ||
encrypt(payload, masterkey) { | ||
// random initialization vector | ||
const iv = crypto.randomBytes(16); | ||
|
||
// random salt | ||
const salt = crypto.randomBytes(64); | ||
|
||
// derive encryption key: 32 byte key length | ||
// in assumption the masterkey is a cryptographic and NOT a password there is no need for | ||
// a large number of iterations. It may can replaced by HKDF | ||
// the value of 2145 is randomly chosen! | ||
const key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512'); | ||
|
||
// AES 256 GCM Mode | ||
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); | ||
|
||
// encrypt the given text | ||
const encrypted = Buffer.concat([cipher.update(JSON.stringify(payload), 'utf8'), cipher.final()]); | ||
|
||
// extract the auth tag | ||
const tag = cipher.getAuthTag(); | ||
|
||
// generate output | ||
return Buffer.concat([salt, iv, tag, encrypted]).toString('base64'); | ||
} | ||
decrypt(encdata, masterkey) { | ||
// base64 decoding | ||
const bData = Buffer.from(encdata, 'base64'); | ||
|
||
// convert data to buffers | ||
const salt = bData.subarray(0, 64); | ||
const iv = bData.subarray(64, 80); | ||
const tag = bData.subarray(80, 96); | ||
const payload = bData.subarray(96); | ||
|
||
// derive key using; 32 byte key length | ||
const key = crypto.pbkdf2Sync(masterkey, salt, 2145, 32, 'sha512'); | ||
|
||
// AES 256 GCM Mode | ||
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); | ||
decipher.setAuthTag(tag); | ||
|
||
// encrypt the given text | ||
const decrypted = decipher.update(payload, 'binary', 'utf8') + decipher.final('utf8'); | ||
|
||
return decrypted; | ||
} | ||
} | ||
|
||
module.exports = { | ||
createEncryption: (type) => { | ||
switch (type) { | ||
case ENCRYPTION_TYPES.AES_256_GCM: | ||
return new Aes256Gcm(); | ||
default: | ||
throw new Error('Invalid encryption type'); | ||
} | ||
}, | ||
ENCRYPTION_TYPES: Object.freeze(ENCRYPTION_TYPES), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
const service = require('./service'); | ||
const jwtService = require('../../components/jwtService'); | ||
|
||
module.exports = { | ||
generateProxyHeaders: async (req, res, next) => { | ||
try { | ||
const response = await service.generateProxyHeaders(req.body, req.currentUser, jwtService.getBearerToken(req)); | ||
res.status(200).json(response); | ||
} catch (error) { | ||
next(error); | ||
} | ||
}, | ||
callExternalApi: async (req, res, next) => { | ||
try { | ||
const headers = await service.readProxyHeaders(req.headers); | ||
// read external api config | ||
// prepare external api call | ||
// | ||
res.status(200).json([ | ||
{ | ||
name: headers['username'], | ||
abbreviation: 'USER', | ||
}, | ||
{ | ||
name: headers['email'], | ||
abbreviation: 'EMAIL', | ||
}, | ||
]); | ||
} catch (error) { | ||
next(error); | ||
} | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const routes = require('./routes'); | ||
const setupMount = require('../common/utils').setupMount; | ||
|
||
module.exports.mount = (app) => { | ||
return setupMount('proxy', app, routes); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
const cors = require('cors'); | ||
|
||
const { currentUser } = require('../auth/middleware/userAccess'); | ||
const controller = require('./controller'); | ||
|
||
const routes = require('express').Router(); | ||
|
||
// need to allow cors for OPTIONS call | ||
// formio component will call OPTIONS pre-flight | ||
routes.options('/external', cors()); | ||
|
||
// called with encrypted headers, no current user!!! | ||
routes.get('/external', cors(), async (_req, res, next) => { | ||
await controller.callExternalApi(_req, res, next); | ||
}); | ||
|
||
routes.post('/headers', currentUser, async (_req, res, next) => { | ||
await controller.generateProxyHeaders(_req, res, next); | ||
}); | ||
|
||
module.exports = routes; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
const config = require('config'); | ||
const { createEncryption, ENCRYPTION_TYPES } = require('../../components/encryption'); | ||
|
||
const encryptionKey = config.get('server.encryption.proxy'); | ||
const encryption = createEncryption(ENCRYPTION_TYPES.AES_256_GCM); | ||
|
||
const service = { | ||
generateProxyHeaders: async (payload, currentUser, token) => { | ||
const headerData = { | ||
formId: payload['formId'], | ||
versionId: payload['versionId'], | ||
submissionId: payload['submissionId'], | ||
userId: currentUser.idpUserId, | ||
username: currentUser.username, | ||
firstName: currentUser.firstName, | ||
lastName: currentUser.lastName, | ||
fullName: currentUser.fullName, | ||
email: currentUser.email, | ||
idp: currentUser.idp, | ||
token: token, | ||
}; | ||
const encryptedHeaderData = encryption.encrypt(headerData, encryptionKey); | ||
return { | ||
'X-CHEFS-PROXY-DATA': encryptedHeaderData, | ||
}; | ||
}, | ||
readProxyHeaders: async (headers) => { | ||
const encryptedHeaderData = headers['X-CHEFS-PROXY-DATA'] || headers['x-chefs-proxy-data']; | ||
if (encryptedHeaderData) { | ||
//error check that we can decrypt it and it contains expected data... | ||
try { | ||
const decryptedHeaderData = encryption.decrypt(encryptedHeaderData, encryptionKey); | ||
const data = JSON.parse(decryptedHeaderData); | ||
return data; | ||
} catch (error) { | ||
throw Error(`Could not decrypt proxy headers: ${error.message}`); | ||
} | ||
} else { | ||
throw Error('Proxy headers not found'); | ||
} | ||
}, | ||
}; | ||
|
||
module.exports = service; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters