From 023b02ba94499cbf33bb29873e3a784928013bd8 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 12 May 2025 12:11:43 +0700 Subject: [PATCH 1/6] move secret encryption/storage to Factory --- ABFactory.js | 98 ++++++++++++++++++++++++++++++----- platform/ABObjectApi.js | 110 ++-------------------------------------- 2 files changed, 91 insertions(+), 117 deletions(-) diff --git a/ABFactory.js b/ABFactory.js index db4d939..ff907d7 100644 --- a/ABFactory.js +++ b/ABFactory.js @@ -6,12 +6,18 @@ */ const _ = require("lodash"); +const crypto = require("crypto"); 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"); + +// Encryption settings +const CRYPTO_ALGORITHM = "aes-256-gcm"; +const KEY_LENGTH = 32; +const VI_LENGTH = 16; var ABFactoryCore = require("./core/ABFactoryCore"); @@ -113,15 +119,15 @@ class ABFactory extends ABFactoryCore { // NOTE: .tenantDB() returns the db name enclosed with ` ` // our KNEX connection doesn't want that for the DB Name: var tenantDB = this.req.tenantDB().replaceAll("`", ""); - if (!tenantDB) { +if (!tenantDB) { throw new Error( - `ABFactory.Knex.connection(): Could not find Tenant DB information for id[${this.req.tenantID()}]` + `ABFactory.Knex.connection(): Could not find Tenant DB information for id[${this.req.tenantID()}]`, ); } var config = this.req.connections()["appbuilder"]; if (!config) { throw new Error( - `ABFactory.Knex.connection(): Could not find configuration settings` + `ABFactory.Knex.connection(): Could not find configuration settings`, ); } @@ -186,7 +192,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"); }, @@ -197,7 +203,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"); }, @@ -304,10 +310,10 @@ class ABFactory extends ABFactoryCore { // Convert to UTC by subtracting the timezone offset let startOfDayUTC = new Date( - startOfDay.getTime() + startOfDay.getTimezoneOffset() * 60000 + startOfDay.getTime() + startOfDay.getTimezoneOffset() * 60000, ); let endOfDayUTC = new Date( - endOfDay.getTime() + endOfDay.getTimezoneOffset() * 60000 + endOfDay.getTime() + endOfDay.getTimezoneOffset() * 60000, ); // Format the date in "YYYY-MM-DD HH:MM:SS" format @@ -317,7 +323,7 @@ class ABFactory extends ABFactoryCore { }; return formatDate(startOfDayUTC).concat( "|", - formatDate(endOfDayUTC) + formatDate(endOfDayUTC), ); }, }; @@ -350,7 +356,7 @@ class ABFactory extends ABFactoryCore { let newDef = this.definitionNew(fullDef); this.emit("definition.created", newDef); return newDef; - } + }, ); } @@ -396,7 +402,7 @@ class ABFactory extends ABFactoryCore { this._definitions[id] = newDef; this.emit("definition.updated", id); return newDef; - } + }, ); } @@ -443,7 +449,7 @@ class ABFactory extends ABFactoryCore { */ cacheMatch(key, data) { let matches = Object.keys(this.__Cache).filter( - (k) => k.indexOf(key) > -1 + (k) => k.indexOf(key) > -1, ); if (typeof data != "undefined") { matches.forEach((k) => { @@ -514,6 +520,74 @@ class ABFactory extends ABFactoryCore { return this.req.notify(domain, error, this._notifyInfo(info)); } + // + // Secrets + // + async createPrivateKey(definitionID) { + const key = crypto.randomBytes(KEY_LENGTH); + const hex = key.toString("hex"); + const model = this.objectKey().model(); + await model.create({ + Key: hex, + DefinitionID: definitionID, + }); + return hex; + } + + async getPrivateKey(definitionID) { + const modelKey = this.AB.objectKey().model(); + const list = await modelKey.find({ + where: { DefinitionID: definitionID }, + 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.subarray( + 0, + encryptedBuffer.length - VI_LENGTH * 2, + ); + const vi = encryptedBuffer.subarray( + encryptedBuffer.length - VI_LENGTH * 2, + encryptedBuffer.length - VI_LENGTH, + ); + const authTag = encryptedBuffer.subarray( + 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"); + } + + // // Utilities // diff --git a/platform/ABObjectApi.js b/platform/ABObjectApi.js index e6aa8a3..98314ec 100644 --- a/platform/ABObjectApi.js +++ b/platform/ABObjectApi.js @@ -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 @@ -16,31 +10,12 @@ 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, - }) - ); + this.AB.secretCreate(this.id, secret.name, secret.value); }); return Promise.all(createTasks); @@ -69,7 +44,7 @@ module.exports = class ABObjectApi extends ABObjectApiCore { .modelKnex() .query() .delete() - .where("DefinitionID", "=", this.id) + .where("DefinitionID", "=", this.id), ); // Remove secret values of this API Object @@ -78,88 +53,13 @@ module.exports = class ABObjectApi extends ABObjectApiCore { .modelKnex() .query() .delete() - .where("DefinitionID", "=", this.id) + .where("DefinitionID", "=", this.id), ); return Promise.all(dropTasks); } 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.secretGet(this.id, secretName); } }; From b3394b632aae5e8288b1974a8c72d45377b2b74c Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 12 May 2025 15:43:46 +0700 Subject: [PATCH 2/6] update secret methods in factory --- ABFactory.js | 132 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 94 insertions(+), 38 deletions(-) diff --git a/ABFactory.js b/ABFactory.js index ff907d7..3e40ce6 100644 --- a/ABFactory.js +++ b/ABFactory.js @@ -20,6 +20,7 @@ const KEY_LENGTH = 32; const VI_LENGTH = 16; var ABFactoryCore = require("./core/ABFactoryCore"); +const { definition } = require("./platform/ABDefinition"); function stringifyErrors(param) { if (param instanceof Error) { @@ -119,7 +120,7 @@ class ABFactory extends ABFactoryCore { // NOTE: .tenantDB() returns the db name enclosed with ` ` // our KNEX connection doesn't want that for the DB Name: var tenantDB = this.req.tenantDB().replaceAll("`", ""); -if (!tenantDB) { + if (!tenantDB) { throw new Error( `ABFactory.Knex.connection(): Could not find Tenant DB information for id[${this.req.tenantID()}]`, ); @@ -521,73 +522,128 @@ if (!tenantDB) { } // - // Secrets + // Secret Management // - async createPrivateKey(definitionID) { - const key = crypto.randomBytes(KEY_LENGTH); - const hex = key.toString("hex"); - const model = this.objectKey().model(); - await model.create({ - Key: hex, - DefinitionID: definitionID, - }); - return hex; - } - - async getPrivateKey(definitionID) { - const modelKey = this.AB.objectKey().model(); - const list = await modelKey.find({ - where: { DefinitionID: definitionID }, - limit: 1, - }); - return list[0]?.Key ?? null; + /** + * 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 secretKey(definitionID) { + const cacheKey = `_cachePK_${definitionID}`; + if (!this[cacheKey]) { + const model = this.AB.objectKey().model(); + const [key] = + (await model.find({ + where: { DefinitionID: definitionID }, + limit: 1, + })) ?? []; + if (key) { + this[cacheKey] = key.Key; + } else { + this[cacheKey] = crypto.randomBytes(KEY_LENGTH).toString("hex"); + await model.create({ + Key: this[cacheKey], + DefinitionID: definitionID, + }); + } + + // 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(() => { + delete this[cacheKey]; + delete this[cacheCleanup]; + }, 5 * 60 * 1000); + } + return this[cacheKey]; } - _encryptSecret(key, text) { + /** + * Encyrpts and stores a secret value + * @param {string} defintionID - unique id of the definition this secret + * belongs to + * @param {string} name to refernce this secret by + * @param {string} value the secret to be encrypted + */ + async secretCreate(defintionID, name, value) { + const pk = await this.secretKey(defintionID); + + // Encrypt const iv = crypto.randomBytes(VI_LENGTH); const cipher = crypto.createCipheriv( CRYPTO_ALGORITHM, - Buffer.from(key, "hex"), - iv, + Buffer.from(pk, "hex"), + iv ); - - const encrypted = cipher.update(Buffer.from(text, "utf-8")); + const encrypted = cipher.update(Buffer.from(value, "utf-8")); cipher.final(); + const encryptedValue = Buffer.concat([ + encrypted, + iv, + cipher.getAuthTag(), + ]).toString("hex"); - return Buffer.concat([encrypted, iv, cipher.getAuthTag()]).toString( - "hex", - ); + // Save to DB + const model = this.AB.objectKey().model(); + await model.create({ + Name: name, + Secret: encryptedValue, + DefinitionID: defintionID, + }); } - _decryptSecret(key, encrypted) { - const encryptedBuffer = Buffer.from(encrypted, "hex"); + /** + * 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 secretGet(definitionID, name) { + const pk = await this.secretKey(definitionID); + + // Lookup the secret from the DB + const modelSecret = this.AB.objectSecret().model(); + const list = await modelSecret.find({ + where: { + DefinitionID: definitionID, + Name: name, + }, + limit: 1, + }); + const secret = list?.[0]?.Secret ?? ""; + if (!secret) return null; + + // Decrypt the secret + const encryptedBuffer = Buffer.from(secret, "hex"); const text = encryptedBuffer.subarray( 0, - encryptedBuffer.length - VI_LENGTH * 2, + encryptedBuffer.length - VI_LENGTH * 2 ); const vi = encryptedBuffer.subarray( encryptedBuffer.length - VI_LENGTH * 2, - encryptedBuffer.length - VI_LENGTH, + encryptedBuffer.length - VI_LENGTH ); const authTag = encryptedBuffer.subarray( encryptedBuffer.length - VI_LENGTH, - encryptedBuffer.length, + encryptedBuffer.length ); - const decipher = crypto.createDecipheriv( CRYPTO_ALGORITHM, - Buffer.from(key, "hex"), - vi, + Buffer.from(pk, "hex"), + vi ); decipher.setAuthTag(authTag); - let decrypted = decipher.update(text); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString("utf-8"); } - // // Utilities // From 9d9cea4ac5173cdaa1ae133dd89c13dcb36a8269 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Mon, 12 May 2025 16:36:51 +0700 Subject: [PATCH 3/6] fix referneces to ABFactory --- ABFactory.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ABFactory.js b/ABFactory.js index 3e40ce6..3c832bb 100644 --- a/ABFactory.js +++ b/ABFactory.js @@ -20,7 +20,6 @@ const KEY_LENGTH = 32; const VI_LENGTH = 16; var ABFactoryCore = require("./core/ABFactoryCore"); -const { definition } = require("./platform/ABDefinition"); function stringifyErrors(param) { if (param instanceof Error) { @@ -534,7 +533,7 @@ class ABFactory extends ABFactoryCore { async secretKey(definitionID) { const cacheKey = `_cachePK_${definitionID}`; if (!this[cacheKey]) { - const model = this.AB.objectKey().model(); + const model = this.objectKey().model(); const [key] = (await model.find({ where: { DefinitionID: definitionID }, @@ -590,7 +589,7 @@ class ABFactory extends ABFactoryCore { ]).toString("hex"); // Save to DB - const model = this.AB.objectKey().model(); + const model = this.objectSecret().model(); await model.create({ Name: name, Secret: encryptedValue, @@ -608,7 +607,7 @@ class ABFactory extends ABFactoryCore { const pk = await this.secretKey(definitionID); // Lookup the secret from the DB - const modelSecret = this.AB.objectSecret().model(); + const modelSecret = this.objectSecret().model(); const list = await modelSecret.find({ where: { DefinitionID: definitionID, From de79cc48eebfb37855b179653db76ced3ea96cf3 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 14 May 2025 12:48:35 +0700 Subject: [PATCH 4/6] move secret logic to secretmanager --- ABFactory.js | 149 +++---------------------------- platform/ABObjectApi.js | 12 +-- platform/ABSecretManager.js | 173 ++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 143 deletions(-) create mode 100644 platform/ABSecretManager.js diff --git a/ABFactory.js b/ABFactory.js index 3c832bb..b15e75e 100644 --- a/ABFactory.js +++ b/ABFactory.js @@ -6,7 +6,6 @@ */ const _ = require("lodash"); -const crypto = require("crypto"); const Knex = require("knex"); const moment = require("moment"); const { nanoid } = require("nanoid"); @@ -14,12 +13,8 @@ const Papa = require("papaparse"); const { serializeError, deserializeError } = require("serialize-error"); const uuid = require("uuid"); -// Encryption settings -const CRYPTO_ALGORITHM = "aes-256-gcm"; -const KEY_LENGTH = 32; -const VI_LENGTH = 16; - var ABFactoryCore = require("./core/ABFactoryCore"); +const SecretManager = require("./platform/ABSecretManager"); function stringifyErrors(param) { if (param instanceof Error) { @@ -121,13 +116,13 @@ class ABFactory extends ABFactoryCore { var tenantDB = this.req.tenantDB().replaceAll("`", ""); if (!tenantDB) { throw new Error( - `ABFactory.Knex.connection(): Could not find Tenant DB information for id[${this.req.tenantID()}]`, + `ABFactory.Knex.connection(): Could not find Tenant DB information for id[${this.req.tenantID()}]` ); } var config = this.req.connections()["appbuilder"]; if (!config) { throw new Error( - `ABFactory.Knex.connection(): Could not find configuration settings`, + `ABFactory.Knex.connection(): Could not find configuration settings` ); } @@ -310,10 +305,10 @@ class ABFactory extends ABFactoryCore { // Convert to UTC by subtracting the timezone offset let startOfDayUTC = new Date( - startOfDay.getTime() + startOfDay.getTimezoneOffset() * 60000, + startOfDay.getTime() + startOfDay.getTimezoneOffset() * 60000 ); let endOfDayUTC = new Date( - endOfDay.getTime() + endOfDay.getTimezoneOffset() * 60000, + endOfDay.getTime() + endOfDay.getTimezoneOffset() * 60000 ); // Format the date in "YYYY-MM-DD HH:MM:SS" format @@ -323,13 +318,15 @@ class ABFactory extends ABFactoryCore { }; return formatDate(startOfDayUTC).concat( "|", - formatDate(endOfDayUTC), + formatDate(endOfDayUTC) ); }, }; (Object.keys(platformRules) || []).forEach((k) => { this.rules[k] = platformRules[k]; }); + + this.Secret = new SecretManager(this); } // init() { @@ -356,7 +353,7 @@ class ABFactory extends ABFactoryCore { let newDef = this.definitionNew(fullDef); this.emit("definition.created", newDef); return newDef; - }, + } ); } @@ -402,7 +399,7 @@ class ABFactory extends ABFactoryCore { this._definitions[id] = newDef; this.emit("definition.updated", id); return newDef; - }, + } ); } @@ -449,7 +446,7 @@ class ABFactory extends ABFactoryCore { */ cacheMatch(key, data) { let matches = Object.keys(this.__Cache).filter( - (k) => k.indexOf(key) > -1, + (k) => k.indexOf(key) > -1 ); if (typeof data != "undefined") { matches.forEach((k) => { @@ -520,132 +517,10 @@ class ABFactory extends ABFactoryCore { return this.req.notify(domain, error, this._notifyInfo(info)); } - // - // Secret Management - // - - /** - * 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 secretKey(definitionID) { - const cacheKey = `_cachePK_${definitionID}`; - if (!this[cacheKey]) { - const model = this.objectKey().model(); - const [key] = - (await model.find({ - where: { DefinitionID: definitionID }, - limit: 1, - })) ?? []; - if (key) { - this[cacheKey] = key.Key; - } else { - this[cacheKey] = crypto.randomBytes(KEY_LENGTH).toString("hex"); - await model.create({ - Key: this[cacheKey], - DefinitionID: definitionID, - }); - } - - // 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(() => { - delete this[cacheKey]; - delete this[cacheCleanup]; - }, 5 * 60 * 1000); - } - return this[cacheKey]; - } - - /** - * Encyrpts and stores a secret value - * @param {string} defintionID - unique id of the definition this secret - * belongs to - * @param {string} name to refernce this secret by - * @param {string} value the secret to be encrypted - */ - async secretCreate(defintionID, name, value) { - const pk = await this.secretKey(defintionID); - - // Encrypt - const iv = crypto.randomBytes(VI_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"); - - // Save to DB - const model = this.objectSecret().model(); - await model.create({ - Name: name, - Secret: encryptedValue, - DefinitionID: defintionID, - }); - } - - /** - * 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 secretGet(definitionID, name) { - const pk = await this.secretKey(definitionID); - - // Lookup the secret from the DB - const modelSecret = this.objectSecret().model(); - const list = await modelSecret.find({ - where: { - DefinitionID: definitionID, - Name: name, - }, - limit: 1, - }); - const secret = list?.[0]?.Secret ?? ""; - if (!secret) return null; - - // Decrypt the secret - const encryptedBuffer = Buffer.from(secret, "hex"); - const text = encryptedBuffer.subarray( - 0, - encryptedBuffer.length - VI_LENGTH * 2 - ); - const vi = encryptedBuffer.subarray( - encryptedBuffer.length - VI_LENGTH * 2, - encryptedBuffer.length - VI_LENGTH - ); - const authTag = encryptedBuffer.subarray( - encryptedBuffer.length - VI_LENGTH, - encryptedBuffer.length - ); - const decipher = crypto.createDecipheriv( - CRYPTO_ALGORITHM, - Buffer.from(pk, "hex"), - vi - ); - decipher.setAuthTag(authTag); - let decrypted = decipher.update(text); - decrypted = Buffer.concat([decrypted, decipher.final()]); - return decrypted.toString("utf-8"); - } - // // Utilities // + clone(value) { return _.clone(value); } diff --git a/platform/ABObjectApi.js b/platform/ABObjectApi.js index 98314ec..31dc73c 100644 --- a/platform/ABObjectApi.js +++ b/platform/ABObjectApi.js @@ -14,9 +14,9 @@ module.exports = class ABObjectApi extends ABObjectApiCore { const createTasks = []; // Encrypt/Store secrets of the API Object - (this.secrets ?? []).forEach((secret) => { - this.AB.secretCreate(this.id, secret.name, secret.value); - }); + if (this.secrets) { + this.AB.Secret.create(this.id, ...this.secrets); + } return Promise.all(createTasks); } @@ -44,7 +44,7 @@ module.exports = class ABObjectApi extends ABObjectApiCore { .modelKnex() .query() .delete() - .where("DefinitionID", "=", this.id), + .where("DefinitionID", "=", this.id) ); // Remove secret values of this API Object @@ -53,13 +53,13 @@ module.exports = class ABObjectApi extends ABObjectApiCore { .modelKnex() .query() .delete() - .where("DefinitionID", "=", this.id), + .where("DefinitionID", "=", this.id) ); return Promise.all(dropTasks); } async getSecretValue(secretName) { - return this.AB.secretGet(this.id, secretName); + return this.AB.Secret.getValue(this.id, secretName); } }; diff --git a/platform/ABSecretManager.js b/platform/ABSecretManager.js new file mode 100644 index 0000000..3f333ba --- /dev/null +++ b/platform/ABSecretManager.js @@ -0,0 +1,173 @@ +const crypto = require("crypto"); + +// Encryption settings +const CRYPTO_ALGORITHM = "aes-256-gcm"; +const KEY_LENGTH = 32; +const IV_LENGTH = 16; + +class SecretManager { + constructor(AB) { + this.AB = AB; + this.secret = this.objectSecret().model(); + this.key = this.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; From fa76730ed38dcd851b42c39372c96d6db5f28bd9 Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Wed, 14 May 2025 14:24:49 +0700 Subject: [PATCH 5/6] fix secret manager --- ABFactory.js | 9 ++++----- platform/ABSecretManager.js | 7 +++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ABFactory.js b/ABFactory.js index b15e75e..51e2654 100644 --- a/ABFactory.js +++ b/ABFactory.js @@ -329,11 +329,10 @@ class ABFactory extends ABFactoryCore { 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 diff --git a/platform/ABSecretManager.js b/platform/ABSecretManager.js index 3f333ba..4aed9e3 100644 --- a/platform/ABSecretManager.js +++ b/platform/ABSecretManager.js @@ -6,10 +6,9 @@ const KEY_LENGTH = 32; const IV_LENGTH = 16; class SecretManager { - constructor(AB) { - this.AB = AB; - this.secret = this.objectSecret().model(); - this.key = this.objectKey().model(); + async init(AB) { + this.secret = AB.objectSecret().model(); + this.key = AB.objectKey().model(); } /** From 4348a87a1d2bdabe3d877c643a4aad798e68663d Mon Sep 17 00:00:00 2001 From: nh758 <7259@pm.me> Date: Fri, 23 May 2025 16:16:59 +0700 Subject: [PATCH 6/6] add API process task --- .../process/tasks/ABProcessTaskServiceApi.js | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 platform/process/tasks/ABProcessTaskServiceApi.js diff --git a/platform/process/tasks/ABProcessTaskServiceApi.js b/platform/process/tasks/ABProcessTaskServiceApi.js new file mode 100644 index 0000000..fb9c29b --- /dev/null +++ b/platform/process/tasks/ABProcessTaskServiceApi.js @@ -0,0 +1,97 @@ +const axios = require("axios"); +const ApiTaskCore = require("../../../core/process/tasks/ABProcessTaskServiceApiCore"); + +module.exports = class ApiTask extends ApiTaskCore { + /** + * @method do() + * this method actually performs the action for this task. + * @param {obj} instance + * the instance data of the running process + * @param {Knex.Transaction?} trx + * (optional) Knex Transaction instance. + * @param {ABUtil.reqService} req + * an instance of the current request object for performing tenant + * based operations. + * @return {Promise} + * resolve(true/false) : true if the task is completed. + * false if task is still waiting + */ + async do(instance /*, trx, req */) { + this.instance = instance; + const response = await this.request(instance); + this.stateUpdate(instance, { rawResponse: response.data }); + this.stateCompleted(instance); + } + + static defaults() { + return { key: "Api" }; + } + + async request(instance) { + const [url, headers, data] = await Promise.all([ + this.renderText(this.url, instance), + this.prepareHeaders(instance), + this.renderText(this.body, instance), + ]); + + const opts = { + url, + method: this.method, + headers, + data: data.replace(/\n/g, ""), + }; + + const response = await axios(opts); + + return response; + } + + async prepareHeaders(instance) { + const reqHeaders = {}; + await Promise.all( + this.headers.map(async (header) => { + reqHeaders[header.key] = await this.renderText( + header.value, + instance, + ); + }), + ); + return reqHeaders; + } + + async renderText(text, instance) { + if (!text) return ""; + const secretPattern = /<%= Secret: (.+?) %>/g; + if (secretPattern.test(text)) { + secretPattern.test(""); // Need to reset the "lastIndex" val of our regex + const secretNames = [...text.matchAll(secretPattern)].map((m) => m[1]); + const secrets = {}; + await Promise.all( + secretNames.map(async (s) => { + secrets[s] = await this.AB.Secret.getValue(this.id, s); + }), + ); + text = text.replace(secretPattern, (_, name) => secrets[name]); + } + return text.replace(/<%= (.+?) %>/g, (_, key) => { + const data = this.process.processData(this, [instance, key]); + if (data) return data; + else return ""; + }); + } + + /** + * @method processData() + * return the current value requested for the given data key. + * @param {obj} instance + * @return {mixed} | null + */ + processData(instance, key) { + const [id, param] = (key || "").split("."); + if (id != this.id) return null; + + const data = this.myState(instance); + + return data[param]; + } +};