feat(#199): electron safeStorage util for storing secrets with aes256 fallback

This commit is contained in:
Anoop M D 2023-09-24 17:49:28 +05:30
parent 7a1b44858d
commit f78c1640e9
3 changed files with 107 additions and 1 deletions

View File

@ -10,7 +10,8 @@
"clean": "rimraf dist", "clean": "rimraf dist",
"dev": "electron .", "dev": "electron .",
"dist": "electron-builder --mac --config electron-builder-config.js", "dist": "electron-builder --mac --config electron-builder-config.js",
"pack": "electron-builder --dir" "pack": "electron-builder --dir",
"test": "jest"
}, },
"dependencies": { "dependencies": {
"@usebruno/js": "0.4.0", "@usebruno/js": "0.4.0",
@ -31,6 +32,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"node-machine-id": "^1.1.12",
"qs": "^6.11.0", "qs": "^6.11.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vm2": "^3.9.13", "vm2": "^3.9.13",

View File

@ -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
};

View File

@ -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');
});
});