Skip to content

Commit

Permalink
add notifyDecryptFails option
Browse files Browse the repository at this point in the history
  • Loading branch information
wheresvic committed Jul 18, 2023
1 parent b1cc18d commit 0cd8142
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const results = await Message.find({ name: messageToSearchWith.name });
- `secret` (required): a string cipher (or a synchronous factory function which returns a string cipher) which is used to encrypt the data (don't lose this!)
- `useAes256Ctr` (optional, default `false`): a boolean indicating whether the older `aes-256-ctr` algorithm should be used. Note that this is strictly a backwards compatibility feature and for new installations it is recommended to leave this at default.
- `saltGenerator` (optional, default `const defaultSaltGenerator = secret => crypto.randomBytes(16);`): a function that should return either a `utf-8` encoded string that is 16 characters in length or a `Buffer` of length 16. This function is also passed the secret as shown in the default function example.
- `notifyDecryptFails` (optional, default `true`): An option to enable or disable an exception on decryption failures. When disabled, exceptions will be inhibited, and an empty field will be returned.

### Static methods

Expand Down
42 changes: 27 additions & 15 deletions lib/mongoose-field-encryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,30 @@ const defaultSaltGenerator = (secret) => crypto.randomBytes(16);
* @param {*} encryptedHex
* @param {*} secret
*/
const decrypt = function (encryptedHex, secret) {
const encryptedArray = encryptedHex.split(":");
const decrypt = function (encryptedHex, secret, decryptOptions = {}) {
try {
const encryptedArray = encryptedHex.split(":");

// maintain backwards compatibility
if (encryptedArray.length === 1) {
return decryptAes256Ctr(encryptedArray[0], secret);
// maintain backwards compatibility
if (encryptedArray.length === 1) {
return decryptAes256Ctr(encryptedArray[0], secret);
}

// @ts-ignore
const iv = new Buffer.from(encryptedArray[0], "hex");
// @ts-ignore
const encrypted = new Buffer.from(encryptedArray[1], "hex");
const decipher = crypto.createDecipheriv(algorithm, secret, iv);
const decrypted = decipher.update(encrypted);
const clearText = Buffer.concat([decrypted, decipher.final()]).toString();
return clearText;
} catch (err) {
if (decryptOptions.notifyDecryptFails) {
throw err;
}
}

// @ts-ignore
const iv = new Buffer.from(encryptedArray[0], "hex");
// @ts-ignore
const encrypted = new Buffer.from(encryptedArray[1], "hex");
const decipher = crypto.createDecipheriv(algorithm, secret, iv);
const decrypted = decipher.update(encrypted);
const clearText = Buffer.concat([decrypted, decipher.final()]).toString();
return clearText;
return "";
};

const fieldEncryption = function (schema, options) {
Expand All @@ -90,6 +98,10 @@ const fieldEncryption = function (schema, options) {
const encryptionStrategy = useAes256Ctr ? encryptAes256Ctr : encrypt;
const saltGenerator = options.saltGenerator ? options.saltGenerator : defaultSaltGenerator;

// Added option for a user to get an exception if decrypt fails.
// Maintained default behaviour that mongoose-field-encryption notifies decrypt failures.
const notifyDecryptFails = options.notifyDecryptFails !== undefined ? options.notifyDecryptFails : true;

// add marker fields to schema
for (const field of fieldsToEncrypt) {
const encryptedFieldName = encryptedFieldNamePrefix + field;
Expand Down Expand Up @@ -166,7 +178,7 @@ const fieldEncryption = function (schema, options) {
if (obj[field]) {
// handle strings separately to maintain searchability
const encryptedValue = obj[field];
obj[field] = decrypt(encryptedValue, secret);
obj[field] = decrypt(encryptedValue, secret, { notifyDecryptFails: notifyDecryptFails });
obj[encryptedFieldName] = false;
}
}
Expand Down Expand Up @@ -258,7 +270,7 @@ const fieldEncryption = function (schema, options) {
for (let doc of docs) {
encryptFields(doc, fieldsToEncrypt, secret());
}

next();
} catch (err) {
next(err);
Expand Down
91 changes: 91 additions & 0 deletions test/test-encryption-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use strict";
const expect = require("chai").expect;
const mongoose = require("mongoose");
const Promise = require("bluebird");

mongoose.Promise = Promise;
mongoose.set("bufferCommands", false);

const fieldEncryptionPlugin = require("../lib/mongoose-field-encryption").fieldEncryption;

describe("Test fieldEncryption options behaviour", function () {
before(function (done) {

done();
});

it("Demonstrate notifyDecryptFails: false - inhibit error, and return empty value", function (done) {
const FieldEncryptionSchema = new mongoose.Schema({
noEncrypt: { type: String },
toEncrypt1: { type: String },
toEncrypt2: { type: String },
});

FieldEncryptionSchema.plugin(fieldEncryptionPlugin, {
fields: ["toEncrypt1", "toEncrypt2"],
secret: "letsdothis",
notifyDecryptFails: false,
});

const FieldEncryptionOptionsTest1 = mongoose.model("FieldEncryptionOptionsTest1", FieldEncryptionSchema);
// given
const sut = new FieldEncryptionOptionsTest1({
noEncrypt: "clear",
toEncrypt1: "some stuff",
toEncrypt2: "after exception",
});

// when
sut.encryptFieldsSync();
sut.toEncrypt1 = sut.toEncrypt1.substring(0, sut.toEncrypt1.length - 1);

// then
sut.decryptFieldsSync();
expect(sut.noEncrypt).to.equal("clear");
expect(sut.__enc_noEncrypt).to.be.undefined;

expect(sut.__enc_toEncrypt1).to.be.false;
expect(sut.toEncrypt1).to.eql("");

expect(sut.__enc_toEncrypt2).to.be.false;
expect(sut.toEncrypt2).to.eql("after exception");
done();
});

it("Demonstrate notifyDecryptFails: true (default) - throw error", function (done) {
const FieldEncryptionSchema = new mongoose.Schema({
noEncrypt: { type: String },
toEncrypt1: { type: String },
toEncrypt2: { type: String },
});

FieldEncryptionSchema.plugin(fieldEncryptionPlugin, {
fields: ["toEncrypt1", "toEncrypt2"],
secret: "letsdothis",
});

const FieldEncryptionOptionsTest2 = mongoose.model("FieldEncryptionOptionsTest2", FieldEncryptionSchema);
// given
const sut = new FieldEncryptionOptionsTest2({
noEncrypt: "clear",
toEncrypt1: "some stuff",
toEncrypt2: "after exception",
});

// when
sut.encryptFieldsSync();
sut.toEncrypt1 = sut.toEncrypt1.substring(0, sut.toEncrypt1.length - 1);

// then
try {
sut.decryptFieldsSync();
} catch (err) {
console.log(err);
expect(err.reason).to.equal("wrong final block length");
done();
return;
}

done(new Error("should have thrown an exception"));
});
});

0 comments on commit 0cd8142

Please sign in to comment.