Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions ABFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ const _ = require("lodash");
const Knex = require("knex");
const moment = require("moment");
const { nanoid } = require("nanoid");
const Papa = require("papaparse");
const { serializeError, deserializeError } = require("serialize-error");
const uuid = require("uuid");
const Papa = require("papaparse");

var ABFactoryCore = require("./core/ABFactoryCore");
const SecretManager = require("./platform/ABSecretManager");

function stringifyErrors(param) {
if (param instanceof Error) {
Expand Down Expand Up @@ -186,7 +187,7 @@ class ABFactory extends ABFactoryCore {
* @param {string} date String of a date you want converted
* @return {string}
*/
toSQLDate: function (date) {
toSQLDate: function(date) {
return moment(date).format("YYYY-MM-DD");
// return moment(date).format("YYYY-MM-DD 00:00:00");
},
Expand All @@ -197,7 +198,7 @@ class ABFactory extends ABFactoryCore {
* @param {string} date String of a date you want converted
* @return {string}
*/
toSQLDateTime: function (date) {
toSQLDateTime: function(date) {
return moment(date).utc().format("YYYY-MM-DD HH:mm:ss");
},

Expand Down Expand Up @@ -324,13 +325,14 @@ class ABFactory extends ABFactoryCore {
(Object.keys(platformRules) || []).forEach((k) => {
this.rules[k] = platformRules[k];
});

this.Secret = new SecretManager(this);
}

// init() {
// super.init().then(()=>{
// // perform any local setups here.
// });
// }
async init() {
await super.init();
await this.Secret.init(this);
}

//
// Definitions
Expand Down Expand Up @@ -517,6 +519,7 @@ class ABFactory extends ABFactoryCore {
//
// Utilities
//

clone(value) {
return _.clone(value);
}
Expand Down
110 changes: 5 additions & 105 deletions platform/ABObjectApi.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
const crypto = require("crypto");

const ABObjectApiCore = require("../core/ABObjectApiCore");

const CRYPTO_ALGORITHM = "aes-256-gcm";
const KEY_LENGTH = 32;
const VI_LENGTH = 16;

module.exports = class ABObjectApi extends ABObjectApiCore {
/**
* migrateCreate
Expand All @@ -16,32 +10,13 @@ module.exports = class ABObjectApi extends ABObjectApiCore {
* the Knex connection.
* @return {Promise}
*/
async migrateCreate(req, knex) {
async migrateCreate(/*req, knex*/) {
const createTasks = [];
const modelKey = this.AB.objectKey().model();
const modelSecret = this.AB.objectSecret().model();

// Create/Store the key of the API Object
const privateKey = this._createPrivateKey();
createTasks.push(
modelKey.create({
Key: privateKey,
DefinitionID: this.id,
})
);

// Encrypt/Store secrets of the API Object
(this.secrets ?? []).forEach((secret) => {
const encrypted = this._encryptSecret(privateKey, secret.value);

createTasks.push(
modelSecret.create({
Name: secret.name,
Secret: encrypted,
DefinitionID: this.id,
})
);
});
if (this.secrets) {
this.AB.Secret.create(this.id, ...this.secrets);
}

return Promise.all(createTasks);
}
Expand Down Expand Up @@ -85,81 +60,6 @@ module.exports = class ABObjectApi extends ABObjectApiCore {
}

async getSecretValue(secretName) {
if (!secretName) return null;

const privateKey = await this._getPrivateKey();
if (!privateKey) return null;

const modelSecret = this.AB.objectSecret().model();
const list = await modelSecret.find({
where: {
DefinitionID: this.id,
Name: secretName,
},
limit: 1,
});
const secret = list?.[0]?.Secret ?? "";
if (!secret) return null;

return this._decryptSecret(privateKey, secret);
}

_createPrivateKey() {
const key = crypto.randomBytes(KEY_LENGTH);

return key.toString("hex");
}

async _getPrivateKey() {
const modelKey = this.AB.objectKey().model();
const list = await modelKey.find({
where: { DefinitionID: this.id },
limit: 1,
});

return list[0]?.Key ?? null;
}

_encryptSecret(key, text) {
const iv = crypto.randomBytes(VI_LENGTH);
const cipher = crypto.createCipheriv(
CRYPTO_ALGORITHM,
Buffer.from(key, "hex"),
iv
);

const encrypted = cipher.update(Buffer.from(text, "utf-8"));
cipher.final();

return Buffer.concat([encrypted, iv, cipher.getAuthTag()]).toString(
"hex"
);
}

_decryptSecret(key, encrypted) {
const encryptedBuffer = Buffer.from(encrypted, "hex");
const text = encryptedBuffer.slice(
0,
encryptedBuffer.length - VI_LENGTH * 2
);
const vi = encryptedBuffer.slice(
encryptedBuffer.length - VI_LENGTH * 2,
encryptedBuffer.length - VI_LENGTH
);
const authTag = encryptedBuffer.slice(
encryptedBuffer.length - VI_LENGTH,
encryptedBuffer.length
);

const decipher = crypto.createDecipheriv(
CRYPTO_ALGORITHM,
Buffer.from(key, "hex"),
vi
);
decipher.setAuthTag(authTag);

let decrypted = decipher.update(text);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString("utf-8");
return this.AB.Secret.getValue(this.id, secretName);
}
};
172 changes: 172 additions & 0 deletions platform/ABSecretManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
const crypto = require("crypto");

// Encryption settings
const CRYPTO_ALGORITHM = "aes-256-gcm";
const KEY_LENGTH = 32;
const IV_LENGTH = 16;

class SecretManager {
async init(AB) {
this.secret = AB.objectSecret().model();
this.key = AB.objectKey().model();
}

/**
* Retrieves a saved private key for a given definition. Will create and save
* one if it does not exist.
* @param {string} definitionID unique id of the definition the key is for
* @resolves {string} private key
*/
async _getKey(definitionID) {
const cacheKey = `_cachePK_${definitionID}`;
if (!this[cacheKey]) {
// this[cacheKey] is promise to avoid looking up / creating multiple
// times. it will resolve with the key
this[cacheKey] = (async () => {
// Check if the database has one
const [key] =
(await this.key.find({
where: { DefinitionID: definitionID },
limit: 1,
})) ?? [];
if (key) return key.Key;
// If not we'll create one
const newKey = crypto.randomBytes(KEY_LENGTH).toString("hex");
await this.key.create({
Key: newKey,
DefinitionID: definitionID,
});
return newKey;
})();

// We don't want to cache these keys in memory for a long time, but also
// don't want to read from the database each time. One defintion might
// have multiple secrets that will be read or written within a few
// seconds. So we'll cache it, but also schedule a cleanup in 5 mins.
const cacheCleanup = `${cacheKey}_cleanup`;
clearTimeout(this[cacheCleanup]);
this[cacheCleanup] = setTimeout(async () => {
await this[cacheKey];
delete this[cacheKey];
delete this[cacheCleanup];
}, 5 * 60 * 1000);
}
return await this[cacheKey];
}

/**
* Deletes a secret from the database
*/
delete(definitionID, name) {
return this.secret
.find({
where: {
DefinitionID: definitionID,
Name: name,
},
limit: 1,
})
.then((result) => {
const toDelete = result?.[0]?.uuid;
if (toDelete) this.secret.delete(toDelete);
});
}

/**
* Encyrpts and stores a secret value
* @param {string} defintionID - unique id of the definition this secret
* belongs to
* @param {object} secret any number of secrets to add
* @param {string} secret.name to refernce this secret by
* @param {string} secret.value the secret to be encrypted
*/
async create(defintionID, ...secrets) {
const pk = await this._getKey(defintionID);
const saves = secrets.map(({ name, value }) => {
// Encrypt
const encryptedValue = this._encrypt(pk, value);

// Save to DB
return this.secret.create({
Name: name,
Secret: encryptedValue,
DefinitionID: defintionID,
});
});
await Promise.all(saves);
}

/**
* Retrieve and decrypt a stored secret
* @param {string} defintionID - unique id of the definition the secret
* belongs to
* @param {string} name of the secret
*/
async getValue(definitionID, name) {
const pk = await this._getKey(definitionID);

// Lookup the secret from the DB
const list = await this.secret.find({
where: {
DefinitionID: definitionID,
Name: name,
},
limit: 1,
});
const secret = list?.[0]?.Secret ?? "";
if (!secret) return null;

return this._decrypt(pk, secret);
}

/**
* get a list of sotred secret names for a given definition
*/
async getStoredNames(definitionID) {
// Lookup the secrets from the DB
const list = await this.secret.find({
where: {
DefinitionID: definitionID,
},
});
return list?.map?.((secret) => secret.Name);
}

_encrypt(pk, value) {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(
CRYPTO_ALGORITHM,
Buffer.from(pk, "hex"),
iv
);
const encrypted = cipher.update(Buffer.from(value, "utf-8"));
cipher.final();
const encryptedValue = Buffer.concat([
encrypted,
iv,
cipher.getAuthTag(),
]).toString("hex");

return encryptedValue;
}

_decrypt(pk, encrypted) {
const encryptedBuffer = Buffer.from(encrypted, "hex");
const bufferLength = encryptedBuffer.length;
const diff = bufferLength - IV_LENGTH;
const text = encryptedBuffer.subarray(0, diff * 2);
const iv = encryptedBuffer.subarray(diff * 2, diff);
const authTag = encryptedBuffer.subarray(diff, bufferLength);
const decipher = crypto.createDecipheriv(
CRYPTO_ALGORITHM,
Buffer.from(pk, "hex"),
iv
);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(text);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString("utf-8");
}
}

module.exports = SecretManager;
Loading
Loading