Skip to content

Commit

Permalink
add unit tests.
Browse files Browse the repository at this point in the history
refactor encryption service.
add database model/migration for the configuration of external apis.

Signed-off-by: Jason Sherman <[email protected]>
  • Loading branch information
usingtechnology committed May 28, 2024
1 parent e6c0461 commit 00efb56
Show file tree
Hide file tree
Showing 18 changed files with 1,175 additions and 109 deletions.
4 changes: 4 additions & 0 deletions .devcontainer/chefs_local/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
"windowMs": "900000",
"max": "100"
}
},
"encryption": {
"proxy": "5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505",
"db": "055cc521987c070c72880d988fd40233fe86efe64e99960fd93bb87145beb6c9"
}
},
"serviceClient": {
Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
//"env": { "NODE_ENV": "test" },
"program": "${workspaceFolder}/app/node_modules/.bin/jest",
"args": [
"${fileBasenameNoExtension}",
"${file}",
"--config",
"${workspaceFolder}/app/jest.config.js",
"--coverage=false"
Expand Down
1 change: 1 addition & 0 deletions app/frontend/tests/unit/utils/constants.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('Constants', () => {
USERS: '/users',
UTILS: '/utils',
FILES_API_ACCESS: '/filesApiAccess',
PROXY: '/proxy',
});
});

Expand Down
84 changes: 0 additions & 84 deletions app/src/components/encryption.js

This file was deleted.

166 changes: 166 additions & 0 deletions app/src/components/encryptionService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
const config = require('config');
const crypto = require('crypto');

const SERVICE = 'EncryptionService';

const ENCRYPTION_KEYS = {
PROXY: 'proxy',
DATABASE: 'db',
};
const ENCRYPTION_ALGORITHMS = {
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/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');
}
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;
}
}

class EncryptionService {
constructor({ keys, algorithms }) {
if (!keys || !algorithms) {
throw new Error(`${SERVICE} is not configured. Check configuration.`);
}

this.keys = keys;
this.algorithms = algorithms;
}

_callAlgorithm(operation, algorithmName, masterkey, data) {
const algo = this.algorithms[algorithmName];
if (algo) {
try {
if (operation == 'encryption') {
return algo.encrypt(data, masterkey);
} else {
return algo.decrypt(data, masterkey);
}
} catch (error) {
throw new Error(`${SERVICE} could not perform ${operation} using algorithm '${algorithmName}'. ${error.message}`);
}
} else {
throw new Error(`${SERVICE} does not support ${algorithmName} algorithm.`);
}
}

_callAlgorithmWithKeyName(operation, algorithmName, keyName, data) {
const masterkey = this.keys[keyName];
if (masterkey) {
return this._callAlgorithm(operation, algorithmName, masterkey, data);
} else {
throw new Error(`${SERVICE} does not have encryption key: '${keyName}'.`);
}
}

encryptExternal(algorithmName, key, payload) {
return this._callAlgorithm('encryption', algorithmName, key, payload);
}

decryptExternal(algorithmName, key, encdata) {
return this._callAlgorithm('decryption', algorithmName, key, encdata);
}

encrypt(algorithmName, keyName, payload) {
return this._callAlgorithmWithKeyName('encryption', algorithmName, keyName, payload);
}

decrypt(algorithmName, keyName, encdata) {
return this._callAlgorithmWithKeyName('decryption', algorithmName, keyName, encdata);
}

encryptProxy(payload) {
return this.encrypt(ENCRYPTION_ALGORITHMS.AES_256_GCM, ENCRYPTION_KEYS.PROXY, payload);
}

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 algorithms = { 'aes-256-gcm': new Aes256Gcm() };

let encryptionService = new EncryptionService({
keys: keys,
algorithms: algorithms,
});

module.exports = {
encryptionService: encryptionService,
ENCRYPTION_ALGORITHMS: Object.freeze(ENCRYPTION_ALGORITHMS),
ENCRYPTION_KEYS: Object.freeze(ENCRYPTION_KEYS),
};
40 changes: 40 additions & 0 deletions app/src/db/migrations/20240521210143_046_external_api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const stamps = require('../stamps');

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/

exports.up = function (knex) {
return Promise.resolve().then(() =>
knex.schema.createTable('external_api', (table) => {
table.uuid('id').primary();
table.uuid('formId').references('id').inTable('form').notNullable().index();
table.string('name', 255).notNullable();
table.string('endpointUrl').notNullable();

table.boolean('sendApiKey').defaultTo(false);
table.string('apiKeyHeader');
table.string('apiKey');

table.boolean('sendUserToken').defaultTo(false);
table.string('userTokenHeader');
table.boolean('userTokenBearer').defaultTo(true);

table.boolean('sendUserInfo').defaultTo(false);
table.string('userInfoHeader');
table.boolean('userInfoEncrypted').defaultTo(false);
table.string('userInfoEncryptionKey');
table.string('userInfoEncryptionAlgo');
stamps(knex, table);
})
);
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return Promise.resolve().then(() => knex.schema.dropTableIfExists('external_api'));
};
1 change: 1 addition & 0 deletions app/src/forms/common/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module.exports = {
Label: require('./tables/label'),
FormComponentsProactiveHelp: require('./tables/formComponentsProactiveHelp'),
FormSubscription: require('./tables/formSubscription'),
ExternalAPI: require('./tables/externalAPI'),

// Views
FormSubmissionUserPermissions: require('./views/formSubmissionUserPermissions'),
Expand Down
Loading

0 comments on commit 00efb56

Please sign in to comment.