From 00f115fb32422272758dcb51db2dcfe9e80370b8 Mon Sep 17 00:00:00 2001 From: Jason Sherman Date: Fri, 21 Jun 2024 13:25:51 -0700 Subject: [PATCH] use cryptr library for encryption/decryption. remove db encryption configuration. update tests Signed-off-by: Jason Sherman --- .devcontainer/chefs_local/local.json.sample | 3 + app/config/custom-environment-variables.json | 3 +- app/config/default.json | 3 +- app/package-lock.json | 39 +++++++----- app/package.json | 1 + app/src/components/encryptionService.js | 61 +++---------------- .../forms/common/models/tables/externalAPI.js | 32 ---------- .../unit/components/encryptionService.spec.js | 25 ++------ app/tests/unit/forms/proxy/service.spec.js | 3 +- openshift/README.md | 6 +- 10 files changed, 46 insertions(+), 130 deletions(-) diff --git a/.devcontainer/chefs_local/local.json.sample b/.devcontainer/chefs_local/local.json.sample index ebbeaf9c7..676f2edd9 100644 --- a/.devcontainer/chefs_local/local.json.sample +++ b/.devcontainer/chefs_local/local.json.sample @@ -56,6 +56,9 @@ "windowMs": "900000", "max": "100" } + }, + "encryption": { + "proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7" } }, "serviceClient": { diff --git a/app/config/custom-environment-variables.json b/app/config/custom-environment-variables.json index 2c30cdefa..8b2ef444f 100755 --- a/app/config/custom-environment-variables.json +++ b/app/config/custom-environment-variables.json @@ -55,8 +55,7 @@ "logLevel": "SERVER_LOGLEVEL", "port": "SERVER_PORT", "encryption": { - "proxy": "SERVER_ENCRYPTION_PROXY", - "db": "SERVER_ENCRYPTION_DB" + "proxy": "SERVER_ENCRYPTION_PROXY" } }, "serviceClient": { diff --git a/app/config/default.json b/app/config/default.json index 2a761689b..931c2592b 100644 --- a/app/config/default.json +++ b/app/config/default.json @@ -58,8 +58,7 @@ } }, "encryption": { - "proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7", - "db": "728160b156ad4fd97f8fe6c5c2d23d8b543acc9d04e5002ae652a10285ff9fe4" + "proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7" } }, "serviceClient": { diff --git a/app/package-lock.json b/app/package-lock.json index c84bf15aa..52864794e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -20,6 +20,7 @@ "compression": "^1.7.4", "config": "^3.3.9", "cors": "^2.8.5", + "cryptr": "^6.3.0", "express": "^4.19.2", "express-basic-auth": "^1.2.1", "express-rate-limit": "^7.2.0", @@ -2434,12 +2435,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3017,6 +3018,11 @@ "node": ">= 8" } }, + "node_modules/cryptr": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.3.0.tgz", + "integrity": "sha512-TA4byAuorT8qooU9H8YJhBwnqD151i1rcauHfJ3Divg6HmukHB2AYMp0hmjv2873J2alr4t15QqC7zAnWFrtfQ==" + }, "node_modules/db-errors": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/db-errors/-/db-errors-0.2.3.tgz", @@ -6043,9 +6049,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -12976,12 +12982,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -13410,6 +13416,11 @@ "which": "^2.0.1" } }, + "cryptr": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/cryptr/-/cryptr-6.3.0.tgz", + "integrity": "sha512-TA4byAuorT8qooU9H8YJhBwnqD151i1rcauHfJ3Divg6HmukHB2AYMp0hmjv2873J2alr4t15QqC7zAnWFrtfQ==" + }, "db-errors": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/db-errors/-/db-errors-0.2.3.tgz", @@ -15719,9 +15730,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" diff --git a/app/package.json b/app/package.json index a18da46ab..2b9cbd218 100644 --- a/app/package.json +++ b/app/package.json @@ -58,6 +58,7 @@ "compression": "^1.7.4", "config": "^3.3.9", "cors": "^2.8.5", + "cryptr": "^6.3.0", "express": "^4.19.2", "express-basic-auth": "^1.2.1", "express-rate-limit": "^7.2.0", diff --git a/app/src/components/encryptionService.js b/app/src/components/encryptionService.js index 78b6ddfdc..433f46884 100644 --- a/app/src/components/encryptionService.js +++ b/app/src/components/encryptionService.js @@ -1,5 +1,5 @@ const config = require('config'); -const crypto = require('crypto'); +const Cryptr = require('cryptr'); const SERVICE = 'EncryptionService'; @@ -30,52 +30,14 @@ class Aes256Gcm extends Encryption { // 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); - + const cryptr = new Cryptr(masterkey); // encrypt the given text/json - const strPayload = typeof payload === 'string' || payload instanceof String ? payload : JSON.stringify(payload); - const encrypted = Buffer.concat([cipher.update(strPayload, 'utf8'), cipher.final()]); - - // extract the auth tag - const tag = cipher.getAuthTag(); - - // generate output - return Buffer.concat([salt, iv, tag, encrypted]).toString('base64'); + const strPayload = typeof payload === 'string' || payload instanceof String ? payload : JSON.stringify(payload); // random initialization vector + return cryptr.encrypt(strPayload); } 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; + const cryptr = new Cryptr(masterkey); + return cryptr.decrypt(encdata); } } @@ -138,20 +100,11 @@ class EncryptionService { decryptProxy(payload) { return this.decrypt(ENCRYPTION_ALGORITHMS.AES_256_GCM, ENCRYPTION_KEYS.PROXY, payload); } - - encryptDb(payload) { - return this.encrypt(ENCRYPTION_ALGORITHMS.AES_256_GCM, ENCRYPTION_KEYS.DATABASE, payload); - } - - decryptDb(payload) { - return this.decrypt(ENCRYPTION_ALGORITHMS.AES_256_GCM, ENCRYPTION_KEYS.DATABASE, payload); - } } const proxy = config.get('server.encryption.proxy'); -const db = config.get('server.encryption.db'); -const keys = { proxy: proxy, db: db }; +const keys = { proxy: proxy }; const algorithms = { 'aes-256-gcm': new Aes256Gcm() }; let encryptionService = new EncryptionService({ diff --git a/app/src/forms/common/models/tables/externalAPI.js b/app/src/forms/common/models/tables/externalAPI.js index 020749b9b..83e850996 100644 --- a/app/src/forms/common/models/tables/externalAPI.js +++ b/app/src/forms/common/models/tables/externalAPI.js @@ -3,8 +3,6 @@ const { Timestamps } = require('../mixins'); const { Regex } = require('../../constants'); const stamps = require('../jsonSchema').stamps; -const { encryptionService } = require('../../../../components/encryptionService'); - class ExternalAPI extends Timestamps(Model) { static get tableName() { return 'external_api'; @@ -45,36 +43,6 @@ class ExternalAPI extends Timestamps(Model) { }; } - async $beforeInsert(context) { - await super.$beforeInsert(context); - if (this.apiKey) { - this.apiKey = encryptionService.encryptDb(this.apiKey); - } - if (this.userInfoEncryptionKey) { - this.userInfoEncryptionKey = encryptionService.encryptDb(this.userInfoEncryptionKey); - } - } - - async $beforeUpdate(context) { - await super.$beforeUpdate(context); - if (this.apiKey) { - this.apiKey = encryptionService.encryptDb(this.apiKey); - } - if (this.userInfoEncryptionKey) { - this.userInfoEncryptionKey = encryptionService.encryptDb(this.userInfoEncryptionKey); - } - } - - async $afterFind(context) { - await super.$afterFind(context); - if (this.apiKey) { - this.apiKey = encryptionService.decryptDb(this.apiKey); - } - if (this.userInfoEncryptionKey) { - this.userInfoEncryptionKey = encryptionService.decryptDb(this.userInfoEncryptionKey); - } - } - static get jsonSchema() { return { type: 'object', diff --git a/app/tests/unit/components/encryptionService.spec.js b/app/tests/unit/components/encryptionService.spec.js index 666b17220..78374ca7f 100644 --- a/app/tests/unit/components/encryptionService.spec.js +++ b/app/tests/unit/components/encryptionService.spec.js @@ -2,7 +2,7 @@ const { MockModel } = require('../../common/dbHelper'); const { encryptionService, ENCRYPTION_ALGORITHMS, ENCRYPTION_KEYS } = require('../../../src/components/encryptionService'); // change these as appropriate after adding new default keys/algos... -const KEY_COUNT = 2; +const KEY_COUNT = 1; const ALGO_COUNT = 1; beforeEach(() => { @@ -42,30 +42,13 @@ describe('encryptionService', () => { expect(data).toEqual(dec); }); - it('should encrypt/decrypt db data (object)', () => { - const data = { username: 'unittest', email: 'email@mail.com' }; - const enc = encryptionService.encryptDb(data); - expect(enc).toBeTruthy(); - const dec = encryptionService.decryptDb(enc); - expect(dec).toBeTruthy(); - expect(data).toMatchObject(JSON.parse(dec)); - }); - - it('should encrypt/decrypt db data (string)', () => { - const data = 'this is my string value'; - const enc = encryptionService.encryptDb(data); - expect(enc).toBeTruthy(); - const dec = encryptionService.decryptDb(enc); - expect(dec).toBeTruthy(); - expect(data).toEqual(dec); - }); - - it('should not decrypt a proxy encryption with db key', () => { + it('should not decrypt a proxy encryption with external key', () => { + const externalKey = 'e9eb43121581f1877e2b8135c8d9079b91c04aab6c717799196630a685b2c6c0'; const data = 'this is my string value'; const enc = encryptionService.encryptProxy(data); expect(enc).toBeTruthy(); expect(() => { - encryptionService.decryptDb(enc); + encryptionService.decryptExternal(ENCRYPTION_ALGORITHMS.AES_256_GCM, externalKey, enc); }).toThrowError(); }); diff --git a/app/tests/unit/forms/proxy/service.spec.js b/app/tests/unit/forms/proxy/service.spec.js index 1871963ca..2fa294125 100644 --- a/app/tests/unit/forms/proxy/service.spec.js +++ b/app/tests/unit/forms/proxy/service.spec.js @@ -102,7 +102,8 @@ describe('Proxy Service', () => { }); it('should throw error if payload uses non-proxy encryption', async () => { - const data = encryptionService.encryptDb(goodProxyHeaderInfo); + const externalKey = 'e9eb43121581f1877e2b8135c8d9079b91c04aab6c717799196630a685b2c6c0'; + const data = encryptionService.encryptExternal(ENCRYPTION_ALGORITHMS.AES_256_GCM, externalKey, goodProxyHeaderInfo); await expect(service.readProxyHeaders({ 'X-CHEFS-PROXY-DATA': data })).rejects.toThrow(); }); diff --git a/openshift/README.md b/openshift/README.md index 3d647086a..fb633ae36 100644 --- a/openshift/README.md +++ b/openshift/README.md @@ -144,19 +144,17 @@ oc create -n $NAMESPACE secret generic $APP_NAME-objectstorage-secret \ --from-literal=password=$password ``` -We need to store encryption keys as secrets. These keys are used to handle communication between the frontend and external APIS (proxy) and storing key data in the database (db). In both cases we will be using `aes-256-gcm` for the encryption and keys for `aes-256-gcm` should be sha256 hashes: 256 bits/32 bytes/64 characters. +We need to store encryption keys as secrets. These keys are used to handle communication between the frontend and external APIS (proxy). We will be using `aes-256-gcm` for the encryption and keys for `aes-256-gcm` should be sha256 hashes: 256 bits/32 bytes/64 characters. Using `node.js` you can generate keys: `crypto.createHash('sha256').update("some seed text").digest('hex');` ```sh export proxy_key= -export db_key= oc create -n $NAMESPACE secret generic $APP_NAME-encryption-keys \ --type=Opaque \ - --from-literal=proxy=$proxy_key \ - --from-literal=db=$db_key + --from-literal=proxy=$proxy_key ``` ## Deployment