Improved configuration file encryption in the database, added testing.
Ylian Saint-Hilaire committed
Aug 4, 2024 at 22:00 UTC
fc29e609391e204208dc84709f3d1444f46eb234
3 files changed
+64
-25
db.js
+36
-21
@@ -422,26 +422,21 @@ module.exports.CreateDB = function (parent, func) {
422
let key;
423
try {
424
key = parent.crypto.pbkdf2Sync(password, salt, iterations, 32, 'sha384');
425
- } catch (e) {
425
+ } catch (ex) {
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
-
432
// Encrypt data
433
obj.encryptData = function (password, plaintext) {
439
- let encryptionVersion = 0x1;
434
+ let encryptionVersion = 0x01;
435
let iterations = 100000
436
const iv = parent.crypto.randomBytes(16);
437
var key = obj.getEncryptDataKey(password, iv, iterations);
438
if (key == null) return null;
444
- const aes = parent.crypto.createCipheriv("aes-256-gcm", key, iv);
439
+ const aes = parent.crypto.createCipheriv('aes-256-gcm', key, iv);
440
var ciphertext = aes.update(plaintext);
441
let versionbuf = Buffer.allocUnsafe(2);
442
versionbuf.writeUInt16BE(encryptionVersion);
@@ -454,35 +449,55 @@ module.exports.CreateDB = function (parent, func) {
449
450
// Decrypt data
451
obj.decryptData = function (password, ciphertext) {
457
- let ciphertextBytes = Buffer.from(ciphertext, 'base64');
458
- try {
459
- const iv = ciphertextBytes.slice(0, 16);
460
- const data = ciphertextBytes.slice(16);
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) {}
452
// Adding an encryption version lets us avoid try catching in the future
453
+ let ciphertextBytes = Buffer.from(ciphertext, 'base64');
454
let encryptionVersion = ciphertextBytes.readUInt16BE(0);
455
try {
456
switch (encryptionVersion) {
471
- case 0x1:
457
+ case 0x01:
458
let iterations = ciphertextBytes.readUInt32BE(2);
459
let authTag = ciphertextBytes.slice(6, 22);
460
const iv = ciphertextBytes.slice(22, 38);
461
const data = ciphertextBytes.slice(38);
462
let key = obj.getEncryptDataKey(password, iv, iterations);
463
if (key == null) return null;
478
- const aes = parent.crypto.createDecipheriv("aes-256-gcm", key, iv);
464
+ const aes = parent.crypto.createDecipheriv('aes-256-gcm', key, iv);
465
aes.setAuthTag(authTag);
466
let plaintextBytes = Buffer.from(aes.update(data));
467
plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
468
return plaintextBytes;
469
default:
484
- return null;
470
+ return obj.oldDecryptData(password, ciphertextBytes);
471
}
472
+ } catch (ex) { return obj.oldDecryptData(password, ciphertextBytes); }
473
+ }
474
+
475
+ // Encrypt data
476
+ // The older encryption system uses CBC without integraty checking.
477
+ // This method is kept only for testing
478
+ obj.oldEncryptData = function (password, plaintext) {
479
+ let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
480
+ if (key == null) return null;
481
+ const iv = parent.crypto.randomBytes(16);
482
+ const aes = parent.crypto.createCipheriv('aes-256-cbc', key, iv);
483
+ var ciphertext = aes.update(plaintext);
484
+ ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
485
+ return ciphertext.toString('base64');
486
+ }
487
+
488
+ // Decrypt data
489
+ // The older encryption system uses CBC without integraty checking.
490
+ // This method is kept only to convert the old encryption to the new one.
491
+ obj.oldDecryptData = function (password, ciphertextBytes) {
492
+ if (typeof password != 'string') return null;
493
+ try {
494
+ const iv = ciphertextBytes.slice(0, 16);
495
+ const data = ciphertextBytes.slice(16);
496
+ let key = parent.crypto.createHash('sha384').update(password).digest('raw').slice(0, 32);
497
+ const aes = parent.crypto.createDecipheriv('aes-256-cbc', key, iv);
498
+ let plaintextBytes = Buffer.from(aes.update(data));
499
+ plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
500
+ return plaintextBytes;
501
} catch (ex) { return null; }
502
}
503
meshcentral.js
+27
-3
@@ -139,7 +139,7 @@ function CreateMeshCentralServer(config, args) {
139
try { require('./pass').hash('test', function () { }, 0); } catch (ex) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
140
141
// Check for invalid arguments
142
- const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy'];
142
+ const validArguments = ['_', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showitem', 'listuserids', 'showusergroups', 'shownodes', 'showallmeshes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbfix', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'usenodedefaulttlsciphers', 'tlsciphers', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'oldencrypt', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats', 'translate', 'createaccount', 'setuptelegram', 'resetaccount', 'pass', 'removesubdomain', 'adminaccount', 'domain', 'email', 'configfile', 'maintenancemode', 'nedbtodb', 'removetestagents', 'agentupdatetest', 'hashpassword', 'hashpass', 'indexmcrec', 'mpsdebug', 'dumpcores', 'dev', 'mysql', 'mariadb', 'trustedproxy'];
143
for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } }
144
const ENVVAR_PREFIX = "meshcentral_"
145
let envArgs = []
@@ -1029,7 +1029,27 @@ function CreateMeshCentralServer(config, args) {
1029
1030
// Show a list of all configuration files in the database
1031
if (obj.args.dblistconfigfiles) {
1032
- obj.db.GetAllType('cfile', function (err, docs) { if (err == null) { if (docs.length == 0) { console.log("No files found."); } else { for (var i in docs) { console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' bytes.'); } } } else { console.log('Unable to read from database.'); } process.exit(); }); return;
1032
+ obj.db.GetAllType('cfile', function (err, docs) {
1033
+ if (err == null) {
1034
+ if (docs.length == 0) {
1035
+ console.log("No files found.");
1036
+ } else {
1037
+ for (var i in docs) {
1038
+ if (typeof obj.args.dblistconfigfiles == 'string') {
1039
+ const data = obj.db.decryptData(obj.args.dblistconfigfiles, docs[i].data);
1040
+ if (data == null) {
1041
+ console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes - Unable to decrypt.');
1042
+ } else {
1043
+ console.log(docs[i]._id.split('/')[1] + ', ' + data.length + ' bytes, decoded correctly.');
1044
+ }
1045
+ } else {
1046
+ console.log(docs[i]._id.split('/')[1] + ', ' + Buffer.from(docs[i].data, 'base64').length + ' encrypted bytes.');
1047
+ }
1048
+ }
1049
+ }
1050
+ } else { console.log('Unable to read from database.'); } process.exit();
1051
+ });
1052
+ return;
1053
}
1054
1055
// Display the content of a configuration file in the database
@@ -1074,7 +1094,11 @@ function CreateMeshCentralServer(config, args) {
1094
const path = obj.path.join(obj.args.dbpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
1095
console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
1096
lockCount++;
1077
- obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
1097
+ if (obj.args.oldencrypt) {
1098
+ obj.db.setConfigFile(file, obj.db.oldEncryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
1099
+ } else {
1100
+ obj.db.setConfigFile(file, obj.db.encryptData(obj.args.configkey, binary), function () { if ((--lockCount) == 0) { console.log('Done.'); process.exit(); } });
1101
+ }
1102
}
1103
}
1104
if (--lockCount == 0) { process.exit(); }
webserver.js
+1
-1
@@ -6884,7 +6884,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6884
if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
6885
if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
6886
//console.log('Agent connect: ' + req.clientIp);
6887
- try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
6887
+ try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (ex) { console.log(e); }
6888
});
6889
6890
// Setup MQTT broker over websocket