From 5b76a31644419b562ee2768d2976c53dcdb3d545 Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Sun, 4 Aug 2024 16:02:22 -0700 Subject: [PATCH] Changed database file encryption to pbkdf2 for key derivation and aes-256-gcm for encryption (#6296) * Added pbkdf2 and aes-256-gcm options for database file encryption * Added dbCipherAlgorithm option * Changed pbkdf2 to default Maintains backward compatibility, but will require a manual repush to update to the new version * Removed dbkeyderivationiterations option, as this branch is to be more opinionated --- db.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/db.js b/db.js index 10bcf59285..524e89bb0b 100644 --- a/db.js +++ b/db.js @@ -417,34 +417,72 @@ module.exports.CreateDB = function (parent, func) { }; // Get encryption key - obj.getEncryptDataKey = function (password) { + obj.getEncryptDataKey = function (password, salt, iterations) { + if (typeof password != 'string') return null; + let key; + try { + key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384'); + } catch (e) { + // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default. + key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32); + } + return key + } + + obj.oldGetEncryptDataKey = function (password) { if (typeof password != 'string') return null; return parent.crypto.createHash('sha384').update(password).digest("raw").slice(0, 32); } // Encrypt data obj.encryptData = function (password, plaintext) { - var key = obj.getEncryptDataKey(password); - if (key == null) return null; + let encryptionVersion = 0x1; + let iterations = 100000 const iv = parent.crypto.randomBytes(16); - const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv); + var key = obj.getEncryptDataKey(password, iv, iterations); + if (key == null) return null; + const aes = parent.crypto.createCipheriv("aes-256-gcm", key, iv); var ciphertext = aes.update(plaintext); - ciphertext = Buffer.concat([iv, ciphertext, aes.final()]); + let versionbuf = Buffer.allocUnsafe(2); + versionbuf.writeUInt16BE(encryptionVersion); + let iterbuf = Buffer.allocUnsafe(4); + iterbuf.writeUInt32BE(iterations); + let encryptedBuf = aes.final(); + ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]); return ciphertext.toString('base64'); } // Decrypt data obj.decryptData = function (password, ciphertext) { + let ciphertextBytes = Buffer.from(ciphertext, 'base64'); try { - var key = obj.getEncryptDataKey(password); - if (key == null) return null; - const ciphertextBytes = Buffer.from(ciphertext, 'base64'); const iv = ciphertextBytes.slice(0, 16); const data = ciphertextBytes.slice(16); - const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv); - var plaintextBytes = Buffer.from(aes.update(data)); + let key = obj.oldGetEncryptDataKey(password); + const aes = parent.crypto.createDecipheriv("aes-256-cbc", key, iv); + let plaintextBytes = Buffer.from(aes.update(data)); plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]); return plaintextBytes; + } catch (e) {} + // Adding an encryption version lets us avoid try catching in the future + let encryptionVersion = ciphertextBytes.readUInt16BE(0); + try { + switch (encryptionVersion) { + case 0x1: + let iterations = ciphertextBytes.readUInt32BE(2); + let authTag = ciphertextBytes.slice(6, 22); + const iv = ciphertextBytes.slice(22, 38); + const data = ciphertextBytes.slice(38); + let key = obj.getEncryptDataKey(password, iv, iterations); + if (key == null) return null; + const aes = parent.crypto.createDecipheriv("aes-256-gcm", key, iv); + aes.setAuthTag(authTag); + let plaintextBytes = Buffer.from(aes.update(data)); + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]); + return plaintextBytes; + default: + return null; + } } catch (ex) { return null; } }