From f78c1640e91a607e7ce751007c77bc9242a81a86 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Sun, 24 Sep 2023 17:49:28 +0530 Subject: [PATCH] feat(#199): electron safeStorage util for storing secrets with aes256 fallback --- packages/bruno-electron/package.json | 4 +- .../bruno-electron/src/utils/encryption.js | 73 +++++++++++++++++++ .../tests/utils/encryption.spec.js | 31 ++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/bruno-electron/src/utils/encryption.js create mode 100644 packages/bruno-electron/tests/utils/encryption.spec.js diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index d9f89baa..2cacfda5 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -10,7 +10,8 @@ "clean": "rimraf dist", "dev": "electron .", "dist": "electron-builder --mac --config electron-builder-config.js", - "pack": "electron-builder --dir" + "pack": "electron-builder --dir", + "test": "jest" }, "dependencies": { "@usebruno/js": "0.4.0", @@ -31,6 +32,7 @@ "lodash": "^4.17.21", "mustache": "^4.2.0", "nanoid": "3.3.4", + "node-machine-id": "^1.1.12", "qs": "^6.11.0", "uuid": "^9.0.0", "vm2": "^3.9.13", diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js new file mode 100644 index 00000000..820b37b7 --- /dev/null +++ b/packages/bruno-electron/src/utils/encryption.js @@ -0,0 +1,73 @@ +const crypto = require('crypto'); +const { machineIdSync } = require('node-machine-id'); +const { safeStorage } = require('electron'); + +// Constants for algorithm identification +const ELECTRONSAFESTORAGE_ALGO = '00'; +const AES256_ALGO = '01'; + +// AES-256 encryption and decryption functions +function aes256Encrypt(data, key) { + 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) { + const decipher = crypto.createDecipher('aes-256-cbc', key); + let decrypted = decipher.update(data, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; +} + +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); + + return `$${ELECTRONSAFESTORAGE_ALGO}:${encryptedString}`; + } + + // fallback to aes256 + const key = machineIdSync(); + let encryptedString = aes256Encrypt(str, key); + + return `$${AES256_ALGO}:${encryptedString}`; +} + +function decryptString(str) { + if (!str) { + throw new Error('Decrypt failed: unrecognized string format'); + } + + const match = str.match(/^\$(.*?):(.*)$/); + if (!match) { + throw new Error('Decrypt failed: unrecognized string format'); + } + + const algo = match[1]; + const encryptedString = match[2]; + + if ([ELECTRONSAFESTORAGE_ALGO, AES256_ALGO].indexOf(algo) === -1) { + throw new Error('Decrypt failed: Invalid algo'); + } + + if (algo === ELECTRONSAFESTORAGE_ALGO) { + return safeStorage.decryptString(encryptedString); + } + + if (algo === AES256_ALGO) { + const key = machineIdSync(); + return aes256Decrypt(encryptedString, key); + } +} + +module.exports = { + encryptString, + decryptString +}; diff --git a/packages/bruno-electron/tests/utils/encryption.spec.js b/packages/bruno-electron/tests/utils/encryption.spec.js new file mode 100644 index 00000000..b0077228 --- /dev/null +++ b/packages/bruno-electron/tests/utils/encryption.spec.js @@ -0,0 +1,31 @@ +const { encryptString, decryptString } = require('../../src/utils/encryption'); + +// We can only unit test aes 256 fallback as safeStorage is only available +// in the main process + +describe('Encryption and Decryption Tests', () => { + it('should encrypt and decrypt using AES-256', () => { + const plaintext = 'bruno is awesome'; + const encrypted = encryptString(plaintext); + const decrypted = decryptString(encrypted); + + expect(decrypted).toBe(plaintext); + }); + + it('encrypt should throw an error for invalid string', () => { + expect(() => encryptString(null)).toThrow('Encrypt failed: invalid string'); + expect(() => encryptString('')).toThrow('Encrypt failed: invalid string'); + }); + + it('decrypt should throw an error for invalid string', () => { + expect(() => decryptString(null)).toThrow('Decrypt failed: unrecognized string format'); + expect(() => decryptString('')).toThrow('Decrypt failed: unrecognized string format'); + expect(() => decryptString('garbage')).toThrow('Decrypt failed: unrecognized string format'); + }); + + it('decrypt should throw an error for invalid algorithm', () => { + const invalidAlgo = '$99:abcdefg'; + + expect(() => decryptString(invalidAlgo)).toThrow('Decrypt failed: Invalid algo'); + }); +});