Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editable encrypted fields config #116

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions lib/plugins/mongoose-encryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ var mongooseEncryption = function(schema, options) {
}
}
if (this.isSelected('_ct')){
this.decryptSync.call(data);
this.decryptSync.call(data, this);
}
} catch (e) {
err = e;
Expand Down Expand Up @@ -298,7 +298,7 @@ var mongooseEncryption = function(schema, options) {
}

if (_.isFunction(doc.decryptSync)) {
doc.decryptSync();
doc.decryptSync(doc);
}

decryptEmbeddedDocs(doc);
Expand All @@ -319,6 +319,13 @@ var mongooseEncryption = function(schema, options) {
return cb(new Error('Encrypt failed: document already contains ciphertext'));
}

var extractedFields = _.difference(this.decryptedFields, encryptedFields);
// Looks like when multiple nested fields with same root is here,
// Mongoose 6 throws error "MongoServerError: Updating the path '...' would create a conflict at '...'"
// Not sure why it happens, but this fixes it:
var extractedFieldsRoots = _.uniq(extractedFields.map(f => f.split('.')[0]));
extractedFieldsRoots.forEach(field => (that).markModified(field));

// generate random iv
crypto.randomBytes(IV_LENGTH, function(err, iv) {
var cipher, jsonToEncrypt, objectToEncrypt;
Expand Down Expand Up @@ -353,14 +360,14 @@ var mongooseEncryption = function(schema, options) {

schema.methods.decrypt = function(cb) { // callback style but actually synchronous to allow for decryptSync without copypasta or complication
try {
schema.methods.decryptSync.call(this);
schema.methods.decryptSync.call(this, this);
} catch(e){
return cb(e);
}
cb();
};

schema.methods.decryptSync = function() {
schema.methods.decryptSync = function(doc) {
var that = this;
var ct, ctWithIV, decipher, iv, idString, decryptedObject, decryptedObjectJSON, decipheredVal;
if (this._ct) {
Expand All @@ -381,7 +388,9 @@ var mongooseEncryption = function(schema, options) {
throw new Error('Error parsing JSON during decrypt of ' + idString + ': ' + err);
}

encryptedFields.forEach(function(field) {
var fieldsToDecrypt = objectUtil.getPaths(decryptedObject);

fieldsToDecrypt.forEach(function(field) {
decipheredVal = mpath.get(field, decryptedObject);

//JSON.parse returns {type: "Buffer", data: Buffer} for Buffers
Expand All @@ -393,6 +402,8 @@ var mongooseEncryption = function(schema, options) {
}
});

(doc || that).decryptedFields = fieldsToDecrypt

this._ct = undefined;
this._ac = undefined;
}
Expand Down
21 changes: 21 additions & 0 deletions lib/util/object-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ var pick = function(obj, fields, options) {
return result;
};

/**
* Gets the deep keys of an object in dot notation, with recursing into arrays or buffers.
*
* @param {Object} obj The object (recently parsed from JSON from Mongo, as in _ct)
* @return {string[]} List of fields in dot notation
*/
var getPaths = function(obj, pathPrefix = '', fieldsSoFar = []) {
for (var key in obj) {
var val = obj[key]
var fullKey = pathPrefix + key
if (_.isObject(val) && !_.isArray(val) && val.type !== 'Buffer') {
getPaths(val, fullKey + '.', fieldsSoFar)
} else {
fieldsSoFar.push(fullKey)
}
}

return fieldsSoFar
};

/**
* Determines if embedded document.
*
Expand All @@ -74,5 +94,6 @@ var isEmbeddedDocument = function (doc) {
module.exports = {
setFieldValue: setFieldValue,
pick: pick,
getPaths: getPaths,
isEmbeddedDocument: isEmbeddedDocument
};
91 changes: 81 additions & 10 deletions test/encrypt.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2214,13 +2214,84 @@ describe 'migrations', ->
done()

describe 'installing on schema alongside standard encrypt plugin', ->
it 'should throw an error if installed after standard encrypt plugin', ->
EncryptedSchema = mongoose.Schema
text: type: String
EncryptedSchema.plugin encrypt, secret: secret
assert.throw -> EncryptedSchema.plugin encrypt.migrations, secret: secret
it 'should cause encrypt plugin to throw an error if installed first', ->
EncryptedSchema = mongoose.Schema
text: type: String
EncryptedSchema.plugin encrypt.migrations, secret: secret
assert.throw -> EncryptedSchema.plugin encrypt, secret: secret
it 'should throw an error if installed after standard encrypt plugin', ->
EncryptedSchema = mongoose.Schema
text: type: String
EncryptedSchema.plugin encrypt, secret: secret
assert.throw -> EncryptedSchema.plugin encrypt.migrations, secret: secret
it 'should cause encrypt plugin to throw an error if installed first', ->
EncryptedSchema = mongoose.Schema
text: type: String
EncryptedSchema.plugin encrypt.migrations, secret: secret
assert.throw -> EncryptedSchema.plugin encrypt, secret: secret

describe 'changing encrypted fields', ->
beforeEach (done) ->
@simpleTestDoc2 = new BasicEncryptedModel
text: 'Unencrypted text'
bool: true
num: 42
date: new Date '2014-05-19T16:39:07.536Z'
id2: '5303e65d34e1e80d7a7ce212'
arr: ['alpha', 'bravo']
mix: { str: 'A string', bool: false }
buf: new Buffer 'abcdefg'

@simpleTestDoc2.save (err) =>
assert.equal err, null
done()

afterEach (done) ->
@simpleTestDoc2.remove (err) ->
assert.equal err, null
done()

describe 'finding docs', ->
it 'should not drop data from unencrypted fields which were previously encrypted', (done) ->
LessEncryptedModelSchema = mongoose.Schema
text: type: String
bool: type: Boolean
num: type: Number
date: type: Date
id2: type: mongoose.Schema.Types.ObjectId
arr: [ type: String ]
mix: type: mongoose.Schema.Types.Mixed
buf: type: Buffer
idx: type: String, index: true
,
collection: "simples"

LessEncryptedModelSchema.plugin encrypt, secret: secret, collectionId: "Simple", excludeFromEncryption: ['text', 'bool', 'mix.str' ]

LessEncryptedModel = mongoose.model 'LessSimple', LessEncryptedModelSchema

LessEncryptedModel.find
_id: @simpleTestDoc2._id
, (err, docs) ->
assert.equal err, null
assert.lengthOf docs, 1
doc = docs[0]
assert.propertyVal doc, 'num', 42
assert.propertyVal doc, 'text', 'Unencrypted text'
assert.propertyVal doc, 'bool', true
assert.isObject doc, 'mix'
assert.propertyVal doc['mix'], 'str', 'A string'
assert.propertyVal doc['mix'], 'bool', false

doc.num = 13
doc.save (err) ->
assert.equal err, null
LessEncryptedModel.find
_id: doc._id
, (err, docs) ->
assert.equal err, null
assert.lengthOf docs, 1
doc = docs[0]
assert.propertyVal doc, 'num', 13
assert.propertyVal doc, 'text', 'Unencrypted text'
assert.propertyVal doc, 'bool', true
assert.isObject doc, 'mix'
assert.propertyVal doc['mix'], 'str', 'A string'
assert.propertyVal doc['mix'], 'bool', false
done()
return
23 changes: 23 additions & 0 deletions test/object-util.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
chai = require 'chai'
assert = chai.assert

objectUtil = require('../lib/util/object-util.js')

describe 'getPaths', ->
it 'should list field names, but not introspect into arrays or buffers', ->
obj =
text: 'Unencrypted text'
bool: true
num: 42
arr: [ 'alpha', 'bravo' ]
mix:
str: 'A string'
bool: false
deeperObj:
foo: 'bar'
buf:
type: 'Buffer'
data: [ 97, 98, 99, 100, 101, 102, 103 ]
nothing: null

assert.deepEqual objectUtil.getPaths(obj).sort(), ['text', 'bool', 'num', 'arr', 'mix.str', 'mix.bool', 'mix.deeperObj.foo', 'buf', 'nothing'].sort()