Added optional database record encryption support.

Ylian Saint-Hilaire committed Oct 2, 2019 at 17:19 UTC 4d38b390a51d1d4cdbc5ebe024a2bf8f474793f4
2 files changed +104 -28
db.js
+100 -24
@@ -34,6 +34,8 @@ module.exports.CreateDB = function (parent, func) {
34 const common = require('./common.js');
35 obj.identifier = null;
36 obj.dbKey = null;
37 + obj.dbRecordsEncryptKey = null;
38 + obj.dbRecordsDecryptKey = null;
39 obj.changeStream = false;
40
41 obj.SetupDatabase = function (func) {
@@ -193,8 +195,69 @@ module.exports.CreateDB = function (parent, func) {
195 obj.getValueOfTheDay = function (id, startValue, func) { obj.Get(id, function (err, docs) { var date = new Date(), t = date.toLocaleDateString(); if (docs.length == 1) { var r = docs[0]; if (r.day == t) { func({ _id: id, value: r.value, day: t }); return; } } func({ _id: id, value: startValue, day: t }); }); };
196 obj.escapeBase64 = function escapeBase64(val) { return (val.replace(/\+/g, '@').replace(/\//g, '$')); }
197
196 - function Clone(v) { return JSON.parse(JSON.stringify(v)); }
198 + // Encrypt an database object
199 + function performTypedRecordDecrypt(data) {
200 + if ((obj.dbRecordsDecryptKey == null) || (typeof data != 'object')) return data;
201 + for (var i in data) {
202 + if (data[i].type == 'user') {
203 + data[i] = performPartialRecordDecrypt(data[i]);
204 + } else if ((data[i].type == 'node') && (data[i].intelamt != null)) {
205 + data[i].intelamt = performPartialRecordDecrypt(data[i].intelamt);
206 + }
207 + }
208 + return data;
209 + }
210
211 + // Encrypt an database object
212 + function performTypedRecordEncrypt(data) {
213 + if (obj.dbRecordsEncryptKey == null) return data;
214 + if (data.type == 'user') { return performPartialRecordEncrypt(Clone(data), ['otpkeys', 'otphkeys', 'otpsecret', 'salt', 'hash']); }
215 + else if ((data.type == 'node') && (data.intelamt != null)) { var xdata = Clone(data); xdata.intelamt = performPartialRecordEncrypt(xdata.intelamt, ['user', 'pass']); return xdata; }
216 + return data;
217 + }
218 +
219 + // Encrypt an object and return a buffer.
220 + function performPartialRecordEncrypt(plainobj, encryptNames) {
221 + if (typeof plainobj != 'object') return plainobj;
222 + var enc = {}, enclen = 0;
223 + for (var i in encryptNames) { if (plainobj[encryptNames[i]] != null) { enclen++; enc[encryptNames[i]] = plainobj[encryptNames[i]]; delete plainobj[encryptNames[i]]; } }
224 + if (enclen > 0) { plainobj._CRYPT = performRecordEncrypt(enc); } else { delete plainobj._CRYPT; }
225 + return plainobj;
226 + }
227 +
228 + // Encrypt an object and return a buffer.
229 + function performPartialRecordDecrypt(plainobj) {
230 + if ((typeof plainobj != 'object') || (plainobj._CRYPT == null)) return plainobj;
231 + var enc = performRecordDecrypt(plainobj._CRYPT);
232 + if (enc != null) { for (var i in enc) { plainobj[i] = enc[i]; } }
233 + delete plainobj._CRYPT;
234 + return plainobj;
235 + }
236 +
237 + // Encrypt an object and return a base64.
238 + function performRecordEncrypt(plainobj) {
239 + if (obj.dbRecordsEncryptKey == null) return null;
240 + const iv = parent.crypto.randomBytes(16);
241 + const aes = parent.crypto.createCipheriv('aes-256-cbc', obj.dbRecordsEncryptKey, iv);
242 + var ciphertext = aes.update(JSON.stringify(plainobj));
243 + ciphertext = Buffer.concat([iv, ciphertext, aes.final()]);
244 + return ciphertext.toString('base64');
245 + }
246 +
247 + // Takes a base64 and return an object.
248 + function performRecordDecrypt(ciphertext) {
249 + if (obj.dbRecordsDecryptKey == null) return null;
250 + const ciphertextBytes = Buffer.from(ciphertext, 'base64');
251 + const iv = ciphertextBytes.slice(0, 16);
252 + const data = ciphertextBytes.slice(16);
253 + const aes = parent.crypto.createDecipheriv('aes-256-cbc', obj.dbRecordsDecryptKey, iv);
254 + var plaintextBytes = Buffer.from(aes.update(data));
255 + plaintextBytes = Buffer.concat([plaintextBytes, aes.final()]);
256 + return JSON.parse(plaintextBytes.toString());
257 + }
258 +
259 + // Clone an object (TODO: Make this more efficient)
260 + function Clone(v) { return JSON.parse(JSON.stringify(v)); }
261
262 // Read expiration time from configuration file
263 if (typeof parent.args.dbexpire == 'object') {
@@ -203,6 +266,18 @@ module.exports.CreateDB = function (parent, func) {
266 if (typeof parent.args.dbexpire.statsevents == 'number') { expireServerStatsSeconds = parent.args.dbexpire.statsevents; }
267 }
268
269 + // If a DB record encryption key is provided, perform database record encryption
270 + if ((typeof parent.args.dbrecordsencryptkey == 'string') && (parent.args.dbrecordsencryptkey.length != 0)) {
271 + // Hash the database password into a AES256 key and setup encryption and decryption.
272 + obj.dbRecordsEncryptKey = obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsencryptkey).digest("raw").slice(0, 32);
273 + }
274 +
275 + // If a DB record decryption key is provided, perform database record decryption
276 + if ((typeof parent.args.dbrecordsdecryptkey == 'string') && (parent.args.dbrecordsdecryptkey.length != 0)) {
277 + // Hash the database password into a AES256 key and setup encryption and decryption.
278 + obj.dbRecordsDecryptKey = parent.crypto.createHash('sha384').update(parent.args.dbrecordsdecryptkey).digest("raw").slice(0, 32);
279 + }
280 +
281 if (parent.args.mongodb) {
282 // Use MongoDB
283 obj.databaseType = 3;
@@ -517,7 +592,9 @@ module.exports.CreateDB = function (parent, func) {
592 function setupFunctions(func) {
593 if (obj.databaseType == 3) {
594 // Database actions on the main collection (MongoDB)
520 - obj.Set = function (data, func) { obj.file.updateOne({ _id: data._id }, { $set: data }, { upsert: true }, func); };
595 + obj.Set = function (data, func) {
596 + obj.file.replaceOne({ _id: data._id }, performTypedRecordEncrypt(data), { upsert: true }, func);
597 + };
598 obj.Get = function (id, func) {
599 if (arguments.length > 2) {
600 var parms = [func];
@@ -529,19 +606,19 @@ module.exports.CreateDB = function (parent, func) {
606 userCallback.apply(obj, _func2.userArgs);
607 };
608 func2.userArgs = parms;
532 - obj.file.find({ _id: id }).toArray(func2);
609 + obj.file.find({ _id: id }).toArray(function (err, docs) { func2(err, performTypedRecordDecrypt(docs)); });
610 } else {
534 - obj.file.find({ _id: id }).toArray(func);
611 + obj.file.find({ _id: id }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
612 }
613 };
537 - obj.GetAll = function (func) { obj.file.find({}).toArray(func); };
538 - obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(func); };
539 - obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(func); };
540 - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { var x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } obj.file.find(x, { type: 0 }).toArray(func); };
541 - obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(func); };
542 - obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(func); };
543 - obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).project({ type: 0 }).toArray(func); };
544 - obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).project({ type: 0 }).toArray(func); };
614 + obj.GetAll = function (func) { obj.file.find({}).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
615 + obj.GetHash = function (id, func) { obj.file.find({ _id: id }).project({ _id: 0, hash: 1 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
616 + obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
617 + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { var x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } obj.file.find(x, { type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
618 + obj.GetAllType = function (type, func) { obj.file.find({ type: type }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
619 + obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
620 + obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
621 + obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }).project({ type: 0 }).toArray(function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
622 obj.Remove = function (id) { obj.file.deleteOne({ _id: id }); };
623 obj.RemoveAll = function (func) { obj.file.deleteMany({}, { multi: true }, func); };
624 obj.RemoveAllOfType = function (type, func) { obj.file.deleteMany({ type: type }, { multi: true }, func); };
@@ -611,7 +688,7 @@ module.exports.CreateDB = function (parent, func) {
688 }
689 } else {
690 // Database actions on the main collection (NeDB and MongoJS)
614 - obj.Set = function (data, func) { obj.file.update({ _id: data._id }, data, { upsert: true }, func); };
691 + obj.Set = function (data, func) { var xdata = performTypedRecordEncrypt(data); obj.file.update({ _id: xdata._id }, xdata, { upsert: true }, func); };
692 obj.Get = function (id, func) {
693 if (arguments.length > 2) {
694 var parms = [func];
@@ -623,20 +700,19 @@ module.exports.CreateDB = function (parent, func) {
700 userCallback.apply(obj, _func2.userArgs);
701 };
702 func2.userArgs = parms;
626 - obj.file.find({ _id: id }, func2);
627 - }
628 - else {
629 - obj.file.find({ _id: id }, func);
703 + obj.file.find({ _id: id }, function (err, docs) { func2(err, performTypedRecordDecrypt(docs)); });
704 + } else {
705 + obj.file.find({ _id: id }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); });
706 }
707 };
632 - obj.GetAll = function (func) { obj.file.find({}, func); };
708 + obj.GetAll = function (func) { obj.file.find({}, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
709 obj.GetHash = function (id, func) { obj.file.find({ _id: id }, { _id: 0, hash: 1 }, func); };
634 - obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, func); };
635 - obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { var x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } obj.file.find(x, { type: 0 }, func); };
636 - obj.GetAllType = function (type, func) { obj.file.find({ type: type }, func); };
637 - obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, func); };
638 - obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, func); };
639 - obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, { type: 0 }, func); };
710 + obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
711 + obj.GetAllTypeNoTypeFieldMeshFiltered = function (meshes, domain, type, id, func) { var x = { type: type, domain: domain, meshid: { $in: meshes } }; if (id) { x._id = id; } obj.file.find(x, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
712 + obj.GetAllType = function (type, func) { obj.file.find({ type: type }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
713 + obj.GetAllIdsOfType = function (ids, domain, type, func) { obj.file.find({ type: type, domain: domain, _id: { $in: ids } }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
714 + obj.GetUserWithEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
715 + obj.GetUserWithVerifiedEmail = function (domain, email, func) { obj.file.find({ type: 'user', domain: domain, email: email, emailVerified: true }, { type: 0 }, function (err, docs) { func(err, performTypedRecordDecrypt(docs)); }); };
716 obj.Remove = function (id) { obj.file.remove({ _id: id }); };
717 obj.RemoveAll = function (func) { obj.file.remove({}, { multi: true }, func); };
718 obj.RemoveAllOfType = function (type, func) { obj.file.remove({ type: type }, { multi: true }, func); };
webserver.js
+4 -4
@@ -3680,7 +3680,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
3680 };
3681
3682 // Clone a safe version of a user object, remove everything that is secret.
3683 - obj.CloneSafeUser = function(user) {
3683 + obj.CloneSafeUser = function (user) {
3684 if (typeof user != 'object') { return user; }
3685 var user2 = obj.common.Clone(user);
3686 delete user2.hash;
@@ -3690,9 +3690,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
3690 delete user2.domain;
3691 delete user2.subscriptions;
3692 delete user2.passtype;
3693 - if (typeof user2.otpsecret == 'string') { user2.otpsecret = 1; } // Indicates a time secret is present.
3694 - if (typeof user2.otpkeys == 'object') { user2.otpkeys = 0; if (user.otpkeys != null) { for (var i = 0; i < user.otpkeys.keys.length; i++) { if (user.otpkeys.keys[i].u == true) { user2.otpkeys = 1; } } } } // Indicates the number of one time backup codes that are active.
3695 - if (typeof user2.otphkeys == 'object') { user2.otphkeys = user2.otphkeys.length; } // Indicates the number of hardware keys setup
3693 + if ((typeof user2.otpsecret == 'string') && (user2.otpsecret != null)) { user2.otpsecret = 1; } // Indicates a time secret is present.
3694 + if ((typeof user2.otpkeys == 'object') && (user2.otpkeys != null)) { user2.otpkeys = 0; if (user.otpkeys != null) { for (var i = 0; i < user.otpkeys.keys.length; i++) { if (user.otpkeys.keys[i].u == true) { user2.otpkeys = 1; } } } } // Indicates the number of one time backup codes that are active.
3695 + if ((typeof user2.otphkeys == 'object') && (user2.otphkeys != null)) { user2.otphkeys = user2.otphkeys.length; } // Indicates the number of hardware keys setup
3696 return user2;
3697 }
3698