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

Josiah Baldwin committed Aug 4, 2024 at 16:02 UTC 5b76a31644419b562ee2768d2976c53dcdb3d545
1 file changed +48 -10
db.js
+48 -10
@@ -417,34 +417,72 @@ module.exports.CreateDB = function (parent, func) {
417 };
418
419 // Get encryption key
420 - obj.getEncryptDataKey = function (password) {
420 + obj.getEncryptDataKey = function (password, salt, iterations) {
421 + if (typeof password != 'string') return null;
422 + let key;
423 + try {
424 + key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384');
425 + } catch (e) {
426 + // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default.
427 + key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32);
428 + }
429 + return key
430 + }
431 +
432 + obj.oldGetEncryptDataKey = function (password) {
433 if (typeof password != 'string') return null;
434 return parent.crypto.createHash('sha384').update(password).digest("raw").slice(0, 32);
435 }
436
437 // Encrypt data
438 obj.encryptData = function (password, plaintext) {
427 - var key = obj.getEncryptDataKey(password);
428 - if (key == null) return null;
439 + let encryptionVersion = 0x1;
440 + let iterations = 100000
441 const iv = parent.crypto.randomBytes(16);
430 - const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
442 + var key = obj.getEncryptDataKey(password, iv, iterations);
443 + if (key == null) return null;
444 + const aes = parent.crypto.createCipheriv("aes-256-gcm", key, iv);
445 var ciphertext = aes.update(plaintext);
432 - ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
446 + let versionbuf = Buffer.allocUnsafe(2);
447 + versionbuf.writeUInt16BE(encryptionVersion);
448 + let iterbuf = Buffer.allocUnsafe(4);
449 + iterbuf.writeUInt32BE(iterations);
450 + let encryptedBuf = aes.final();
451 + ciphertext = Buffer.concat([versionbuf, iterbuf, aes.getAuthTag(), iv, ciphertext, encryptedBuf]);
452 return ciphertext.toString('base64');
453 }
454
455 // Decrypt data
456 obj.decryptData = function (password, ciphertext) {
457 + let ciphertextBytes = Buffer.from(ciphertext, 'base64');
458 try {
439 - var key = obj.getEncryptDataKey(password);
440 - if (key == null) return null;
441 - const ciphertextBytes = Buffer.from(ciphertext, 'base64');
459 const iv = ciphertextBytes.slice(0, 16);
460 const data = ciphertextBytes.slice(16);
444 - const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
445 - var plaintextBytes = Buffer.from(aes.update(data));
461 + let key = obj.oldGetEncryptDataKey(password);
462 + const aes = parent.crypto.createDecipheriv("aes-256-cbc", key, iv);
463 + let plaintextBytes = Buffer.from(aes.update(data));
464 plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
465 return plaintextBytes;
466 + } catch (e) {}
467 + // Adding an encryption version lets us avoid try catching in the future
468 + let encryptionVersion = ciphertextBytes.readUInt16BE(0);
469 + try {
470 + switch (encryptionVersion) {
471 + case 0x1:
472 + let iterations = ciphertextBytes.readUInt32BE(2);
473 + let authTag = ciphertextBytes.slice(6, 22);
474 + const iv = ciphertextBytes.slice(22, 38);
475 + const data = ciphertextBytes.slice(38);
476 + let key = obj.getEncryptDataKey(password, iv, iterations);
477 + if (key == null) return null;
478 + const aes = parent.crypto.createDecipheriv("aes-256-gcm", key, iv);
479 + aes.setAuthTag(authTag);
480 + let plaintextBytes = Buffer.from(aes.update(data));
481 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
482 + return plaintextBytes;
483 + default:
484 + return null;
485 + }
486 } catch (ex) { return null; }
487 }
488