diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index e3ad6e080..4f5aa4dfa 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -44,6 +44,10 @@ const EnvironmentVariables = ({ environment, collection }) => { variable.enabled = e.target.checked; break; } + case 'secret': { + variable.secret = e.target.checked; + break; + } } reducerDispatch({ type: 'UPDATE_VAR', @@ -63,8 +67,10 @@ const EnvironmentVariables = ({ environment, collection }) => { + + @@ -73,6 +79,14 @@ const EnvironmentVariables = ({ environment, collection }) => { ? variables.map((variable, index) => { return ( + + ); diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js index c72bf7b24..a5aa3e0c1 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/reducer.js @@ -12,6 +12,7 @@ const reducer = (state, action) => { name: '', value: '', type: 'text', + secret: false, enabled: true }); draft.hasChanges = true; @@ -24,6 +25,7 @@ const reducer = (state, action) => { variable.name = action.variable.name; variable.value = action.variable.value; variable.enabled = action.variable.enabled; + variable.secret = action.variable.secret; draft.hasChanges = true; }); } diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index b3c7780ad..64fc0da91 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -54,11 +54,22 @@ const deleteUidsInEnvs = (envs) => { }); }; +const deleteSecretsInEnvs = (envs) => { + each(envs, (env) => { + each(env.variables, (variable) => { + if (variable.secret) { + variable.value = ''; + } + }); + }); +}; + const exportCollection = (collection) => { // delete uids delete collection.uid; deleteUidsInItems(collection.items); deleteUidsInEnvs(collection.environments); + deleteSecretsInEnvs(collection.environments); transformItem(collection.items); const fileName = `${collection.name}.json`; diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 6edd0b617..84c959cba 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -9,6 +9,10 @@ const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBru const { itemSchema } = require('@usebruno/schema'); const { uuid } = require('../utils/common'); const { getRequestUid } = require('../cache/requestUids'); +const { decryptString } = require('../utils/encryption'); +const EnvironmentSecretsStore = require('../store/env-secrets'); + +const environmentSecretsStore = new EnvironmentSecretsStore(); const isJsonEnvironmentConfig = (pathname, collectionPath) => { const dirname = path.dirname(pathname); @@ -47,7 +51,13 @@ const hydrateRequestWithUuid = (request, pathname) => { return request; }; -const addEnvironmentFile = async (win, pathname, collectionUid) => { +const envHasSecrets = (environment = {}) => { + const secrets = _.filter(environment.variables, (v) => v.secret); + + return secrets && secrets.length > 0; +}; + +const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => { try { const basename = path.basename(pathname); const file = { @@ -70,13 +80,25 @@ const addEnvironmentFile = async (win, pathname, collectionUid) => { file.data.uid = getRequestUid(pathname); _.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid())); + + // hydrate environment variables with secrets + if (envHasSecrets(file.data)) { + const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data); + _.each(envSecrets, (secret) => { + const variable = _.find(file.data.variables, (v) => v.name === secret.name); + if (variable) { + variable.value = decryptString(secret.value); + } + }); + } + win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file); } catch (err) { console.error(err); } }; -const changeEnvironmentFile = async (win, pathname, collectionUid) => { +const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => { try { const basename = path.basename(pathname); const file = { @@ -93,6 +115,17 @@ const changeEnvironmentFile = async (win, pathname, collectionUid) => { file.data.uid = getRequestUid(pathname); _.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid())); + // hydrate environment variables with secrets + if (envHasSecrets(file.data)) { + const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data); + _.each(envSecrets, (secret) => { + const variable = _.find(file.data.variables, (v) => v.name === secret.name); + if (variable) { + variable.value = decryptString(secret.value); + } + }); + } + // we are reusing the addEnvironmentFile event itself // this is because the uid of the pathname remains the same // and the collection tree will be able to update the existing environment @@ -152,7 +185,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => { } if (isBruEnvironmentConfig(pathname, collectionPath)) { - return addEnvironmentFile(win, pathname, collectionUid); + return addEnvironmentFile(win, pathname, collectionUid, collectionPath); } // migrate old json files to bru @@ -221,7 +254,7 @@ const addDirectory = (win, pathname, collectionUid, collectionPath) => { const change = async (win, pathname, collectionUid, collectionPath) => { if (isBruEnvironmentConfig(pathname, collectionPath)) { - return changeEnvironmentFile(win, pathname, collectionUid); + return changeEnvironmentFile(win, pathname, collectionUid, collectionPath); } if (hasBruExtension(pathname)) { diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index 6b1d73cdf..2a0d21da5 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -15,7 +15,7 @@ const bruToEnvJson = (bru) => { return json; } catch (error) { - return Promise.reject(e); + return Promise.reject(error); } }; @@ -24,7 +24,7 @@ const envJsonToBru = (json) => { const bru = envJsonToBruV2(json); return bru; } catch (error) { - return Promise.reject(e); + return Promise.reject(error); } }; @@ -32,7 +32,7 @@ const envJsonToBru = (json) => { * The transformer function for converting a BRU file to JSON. * * We map the json response from the bru lang and transform it into the DSL - * format that the app users + * format that the app uses * * @param {string} bru The BRU file content. * @returns {object} The JSON representation of the BRU file. diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index e3aaae712..30a98da39 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -18,7 +18,7 @@ setContentSecurityPolicy(` connect-src * 'unsafe-inline'; base-uri 'none'; form-action 'none'; - img-src 'self' data:image/svg+xml + img-src 'self' data:image/svg+xml; `); const menu = Menu.buildFromTemplate(menuTemplate); diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index fd8975d2e..cf87ab1fd 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -18,6 +18,15 @@ const { openCollectionDialog, openCollection } = require('../app/collections'); const { generateUidBasedOnHash } = require('../utils/common'); const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids'); const { setPreferences } = require('../app/preferences'); +const EnvironmentSecretsStore = require('../store/env-secrets'); + +const environmentSecretsStore = new EnvironmentSecretsStore(); + +const envHasSecrets = (environment = {}) => { + const secrets = _.filter(environment.variables, (v) => v.secret); + + return secrets && secrets.length > 0; +}; const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { // browse directory @@ -153,6 +162,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`environment: ${envFilePath} does not exist`); } + if (envHasSecrets(environment)) { + environmentSecretsStore.storeEnvSecrets(collectionPathname, environment); + } + const content = envJsonToBru(environment); await writeFile(envFilePath, content); } catch (error) { @@ -175,6 +188,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } fs.renameSync(envFilePath, newEnvFilePath); + + environmentSecretsStore.renameEnvironment(collectionPathname, environmentName, newName); } catch (error) { return Promise.reject(error); } @@ -190,6 +205,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } fs.unlinkSync(envFilePath); + + environmentSecretsStore.deleteEnvironment(collectionPathname, environmentName); } catch (error) { return Promise.reject(error); } diff --git a/packages/bruno-electron/src/store/env-secrets.js b/packages/bruno-electron/src/store/env-secrets.js new file mode 100644 index 000000000..b3d26c723 --- /dev/null +++ b/packages/bruno-electron/src/store/env-secrets.js @@ -0,0 +1,126 @@ +const _ = require('lodash'); +const Store = require('electron-store'); +const { encryptString } = require('../utils/encryption'); + +/** + * Sample secrets store file + * + * { + * "collections": [{ + * "path": "/Users/anoop/Code/acme-acpi-collection", + * "environments" : [{ + * "name": "Local", + * "secrets": [{ + * "name": "token", + * "value": "abracadabra" + * }] + * }] + * }] + * } + */ + +class EnvironmentSecretsStore { + constructor() { + this.store = new Store({ + name: 'secrets', + clearInvalidConfig: true + }); + } + + isValidValue(val) { + return val && typeof val === 'string' && val.length > 0; + } + + storeEnvSecrets(collectionPathname, environment) { + const envVars = []; + _.each(environment.variables, (v) => { + if (v.secret) { + envVars.push({ + name: v.name, + value: this.isValidValue(v.value) ? encryptString(v.value) : '' + }); + } + }); + + const collections = this.store.get('collections') || []; + const collection = _.find(collections, (c) => c.path === collectionPathname); + + // if collection doesn't exist, create it, add the environment and save + if (!collection) { + collections.push({ + path: collectionPathname, + environments: [ + { + name: environment.name, + secrets: envVars + } + ] + }); + + this.store.set('collections', collections); + return; + } + + // if collection exists, check if environment exists + // if environment doesn't exist, add the environment and save + collection.environments = collection.environments || []; + const env = _.find(collection.environments, (e) => e.name === environment.name); + if (!env) { + collection.environments.push({ + name: environment.name, + secrets: envVars + }); + + this.store.set('collections', collections); + return; + } + + // if environment exists, update the secrets and save + env.secrets = envVars; + this.store.set('collections', collections); + } + + getEnvSecrets(collectionPathname, environment) { + const collections = this.store.get('collections') || []; + const collection = _.find(collections, (c) => c.path === collectionPathname); + if (!collection) { + return []; + } + + const env = _.find(collection.environments, (e) => e.name === environment.name); + if (!env) { + return []; + } + + return env.secrets || []; + } + + renameEnvironment(collectionPathname, oldName, newName) { + const collections = this.store.get('collections') || []; + const collection = _.find(collections, (c) => c.path === collectionPathname); + if (!collection) { + return; + } + + const env = _.find(collection.environments, (e) => e.name === oldName); + if (!env) { + return; + } + + env.name = newName; + this.store.set('collections', collections); + } + + deleteEnvironment(collectionPathname, environmentName) { + const collections = this.store.get('collections') || []; + const collection = _.find(collections, (c) => c.path === collectionPathname); + if (!collection) { + return; + } + + _.remove(collection.environments, (e) => e.name === environmentName); + this.store.set('collections', collections); + } +} + +module.exports = EnvironmentSecretsStore; diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js index 820b37b74..980311ff9 100644 --- a/packages/bruno-electron/src/utils/encryption.js +++ b/packages/bruno-electron/src/utils/encryption.js @@ -7,14 +7,17 @@ const ELECTRONSAFESTORAGE_ALGO = '00'; const AES256_ALGO = '01'; // AES-256 encryption and decryption functions -function aes256Encrypt(data, key) { +function aes256Encrypt(data) { + const key = machineIdSync(); const cipher = crypto.createCipher('aes-256-cbc', key); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; } -function aes256Decrypt(data, key) { + +function aes256Decrypt(data) { + const key = machineIdSync(); const decipher = crypto.createDecipher('aes-256-cbc', key); let decrypted = decipher.update(data, 'hex', 'utf8'); decrypted += decipher.final('utf8'); @@ -22,20 +25,42 @@ function aes256Decrypt(data, key) { return decrypted; } +// electron safe storage encryption and decryption functions +function safeStorageEncrypt(str) { + let encryptedStringBuffer = safeStorage.encryptString(str); + + // Convert the encrypted buffer to a hexadecimal string + const encryptedString = encryptedStringBuffer.toString('hex'); + + return encryptedString; +} +function safeStorageDecrypt(str) { + // Convert the hexadecimal string to a buffer + const encryptedStringBuffer = Buffer.from(str, 'hex'); + + // Decrypt the buffer + const decryptedStringBuffer = safeStorage.decryptString(encryptedStringBuffer); + + // Convert the decrypted buffer to a string + const decryptedString = decryptedStringBuffer.toString(); + + return decryptedString; +} + function encryptString(str) { if (!str || typeof str !== 'string' || str.length === 0) { throw new Error('Encrypt failed: invalid string'); } - if (safeStorage && safeStorage.isEncryptionAvailable()) { - let encryptedString = safeStorage.encryptString(str); + let encryptedString = ''; + if (safeStorage && safeStorage.isEncryptionAvailable()) { + encryptedString = safeStorageEncrypt(str); return `$${ELECTRONSAFESTORAGE_ALGO}:${encryptedString}`; } // fallback to aes256 - const key = machineIdSync(); - let encryptedString = aes256Encrypt(str, key); + encryptedString = aes256Encrypt(str); return `$${AES256_ALGO}:${encryptedString}`; } @@ -45,25 +70,27 @@ function decryptString(str) { throw new Error('Decrypt failed: unrecognized string format'); } - const match = str.match(/^\$(.*?):(.*)$/); - if (!match) { + // Find the index of the first colon + const colonIndex = str.indexOf(':'); + + if (colonIndex === -1) { throw new Error('Decrypt failed: unrecognized string format'); } - const algo = match[1]; - const encryptedString = match[2]; + // Extract algo and encryptedString based on the colon index + const algo = str.substring(1, colonIndex); + const encryptedString = str.substring(colonIndex + 1); if ([ELECTRONSAFESTORAGE_ALGO, AES256_ALGO].indexOf(algo) === -1) { throw new Error('Decrypt failed: Invalid algo'); } if (algo === ELECTRONSAFESTORAGE_ALGO) { - return safeStorage.decryptString(encryptedString); + return safeStorageDecrypt(encryptedString); } if (algo === AES256_ALGO) { - const key = machineIdSync(); - return aes256Decrypt(encryptedString, key); + return aes256Decrypt(encryptedString); } } diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index fb5195488..ba2256a53 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -6,7 +6,8 @@ const environmentVariablesSchema = Yup.object({ name: Yup.string().nullable(), value: Yup.string().nullable(), type: Yup.string().oneOf(['text']).required('type is required'), - enabled: Yup.boolean().defined() + enabled: Yup.boolean().defined(), + secret: Yup.boolean() }) .noUnknown(true) .strict();
Enabled Name ValueSecret
+ handleVarChange(e, variable, 'enabled')} + /> + { autoCorrect="off" autoCapitalize="off" spellCheck="false" - value={variable.value} + value={variable.value || ''} className="mousetrap" onChange={(e) => handleVarChange(e, variable, 'value')} /> -
- handleVarChange(e, variable, 'enabled')} - /> - -
+ handleVarChange(e, variable, 'secret')} + /> +
+