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