Reputation: 3543
I have two models User
and Email
. Email
has a foreign key from User
.
The value of email in database is encrypted just before it is saved in database. And it is decrypted when the email is retrieved. So, emails are never in plain text in the database, but they can be in plain text when they are being used in the API. I am using hooks to achieve that functionality.
The hooks I am specifying are as follows:
hooks: {
/**
* The query will have plain text email,
* The database has encrypted email.
* Thus, encrypt the query email (if any) BEFORE the query is fired
**/
beforeFind: query => {
if (query && query.where && query.where.email) {
const email = query.where.email;
const AESHash = AES.encrypt(email, KEY, { iv: IV });
const encrypted = AESHash.toString();
query.where.email = encrypted;
console.log(`[hook beforeFind] email "${email}" was changed to "${encrypted}"`);
} else {
console.log(`[hook beforeFind] skipped "${query ? JSON.stringify(query) : query}"`);
}
},
/**
* Once the result is retrieved, the emails (if any) would be encrypted.
* But, the API expects plain text emails.
* Thus, decrypt them BEFORE the query response is returned.
*/
afterFind: query => {
if (query && (query.dataValues || query.email)) {
const email = query.dataValues || query.email;
const decrypt = AES.decrypt(email, KEY, { iv: IV });
const decrypted = decrypt.toString(enc.Utf8);
if (query.dataValues) {
query.dataValues.email = decrypted;
} else {
query.email = decrypted;
}
console.log(`[hook afterFind] email "${email}" was changed to "${decrypted}"`);
} else {
console.log(`[hook afterFind] skipped "${query ? JSON.stringify(query) : query}"`);
}
},
/**
* The API provides plain text email when creating an instance.
* But emails in database have to be encrypted.
* Thus, we need to encrypt the email BEFORE it gets saved in database
*/
beforeCreate: model => {
const email = model.dataValues.email;
if (email.includes("@")) {
const AESHash = AES.encrypt(email, KEY, { iv: IV });
const encrypted = AESHash.toString();
model.dataValues.email = encrypted;
console.log(`[hook beforeCreate] email "${email}" was changed to "${encrypted}"`);
} else {
console.log(`[hook beforeCreate] skipped "${email}"`);
}
},
/**
* Once they are created, the create() response will have the encrypted email
* As API uses plain text email, we will need to decrypt them.
* Thus, Decrypt the email BEFORE the create() response is returned.
*/
afterCreate: model => {
const email = model.dataValues.email;
if (!email.includes("@")) {
const decrypt = AES.decrypt(email, KEY, { iv: IV });
const decrypted = decrypt.toString(enc.Utf8);
model.dataValues.email = decrypted;
console.log(`[hook afterCreate] email "${email}" was changed to "${decrypted}"`);
} else {
console.log(`[hook afterCreate] skipped "${email}"`);
}
}
}
They work perfectly when I need to create / query the Email
model. For example:
async function findEmail() {
console.log("[function findEmail] Executing");
const existingEmail = await Email.findOne({
raw: true
});
console.log("[function findEmail] Result:", existingEmail);
}
And output:
[function findEmail] Executing
[hook beforeFind] skipped "{"raw":true,"limit":1,"plain":true,"rejectOnEmpty":false,"hooks":true}"
[hook afterFind] email "ZxJlbVDJ9MNdCTreKUHPDW6SiNCTslSPCZygnfxE9n0=" was changed to "[email protected]"
[function findEmail] Result: { id: 1, email: '[email protected]', user_id: 1 }
But, they do not work when I query the User
model and include the Email
model.
For example:
async function findUser() {
console.log("[function findUser] Executing");
const existingUser = await User.findOne({
include: [{ model: Email }],
raw: true
});
console.log("[function findUser] Result:", existingUser);
}
And output is:
[function findUser] Executing
[hook afterFind] skipped "null"
[hook beforeCreate] email "[email protected]" was changed to "QuLr/hi7QaJ4vKmxneW0jqwyqQdwhQDQbp+qW1vGpPE="
[hook afterCreate] email "QuLr/hi7QaJ4vKmxneW0jqwyqQdwhQDQbp+qW1vGpPE=" was changed to "[email protected]"
[function findUser] Result: { id: 1,
name: 'John Doe',
'Email.id': 1,
'Email.email': 'QuLr/hi7QaJ4vKmxneW0jqwyqQdwhQDQbp+qW1vGpPE=',
'Email.user_id': 1 }
My question is: Why are the hooks not being executed when the model -- on which the hooks are specified -- is included in while querying some other model?
Here is the complete code I am using: -- on codesandbox
const Sequelize = require("sequelize");
const cryptoJS = require("crypto-js");
const crypto = require("crypto");
const AES = cryptoJS.AES;
const enc = cryptoJS.enc;
const KEY = enc.Utf8.parse(crypto.randomBytes(64).toString("base64"));
const IV = enc.Utf8.parse(crypto.randomBytes(64).toString("base64"));
const DataTypes = Sequelize.DataTypes;
const connectionOptions = {
dialect: "sqlite",
operatorsAliases: false,
storage: "./database.sqlite",
logging: null,
define: {
timestamps: false,
underscored: true
}
};
const sequelize = new Sequelize(connectionOptions);
const User = sequelize.define("User", {
name: {
type: DataTypes.STRING
}
});
const Email = sequelize.define(
"Email",
{
email: {
type: DataTypes.STRING
}
},
{
hooks: {
/**
* The query will have plain text email,
* The database has encrypted email.
* Thus, encrypt the query email (if any) BEFORE the query is fired
**/
beforeFind: query => {
if (query && query.where && query.where.email) {
const email = query.where.email;
const AESHash = AES.encrypt(email, KEY, { iv: IV });
const encrypted = AESHash.toString();
query.where.email = encrypted;
console.log(`[hook beforeFind] email "${email}" was changed to "${encrypted}"`);
} else {
console.log(`[hook beforeFind] skipped "${query ? JSON.stringify(query) : query}"`);
}
},
/**
* Once the result is retrieved, the emails (if any) would be encrypted.
* But, the API expects plain text emails.
* Thus, decrypt them BEFORE the query response is returned.
*/
afterFind: query => {
if (query && (query.dataValues || query.email)) {
const email = query.dataValues || query.email;
const decrypt = AES.decrypt(email, KEY, { iv: IV });
const decrypted = decrypt.toString(enc.Utf8);
if (query.dataValues) {
query.dataValues.email = decrypted;
} else {
query.email = decrypted;
}
console.log(`[hook afterFind] email "${email}" was changed to "${decrypted}"`);
} else {
console.log(`[hook afterFind] skipped "${query ? JSON.stringify(query) : query}"`);
}
},
/**
* The API provides plain text email when creating an instance.
* But emails in database have to be encrypted.
* Thus, we need to encrypt the email BEFORE it gets saved in database
*/
beforeCreate: model => {
const email = model.dataValues.email;
if (email.includes("@")) {
const AESHash = AES.encrypt(email, KEY, { iv: IV });
const encrypted = AESHash.toString();
model.dataValues.email = encrypted;
console.log(`[hook beforeCreate] email "${email}" was changed to "${encrypted}"`);
} else {
console.log(`[hook beforeCreate] skipped "${email}"`);
}
},
/**
* Once they are created, the create() response will have the encrypted email
* As API uses plain text email, we will need to decrypt them.
* Thus, Decrypt the email BEFORE the create() response is returned.
*/
afterCreate: model => {
const email = model.dataValues.email;
if (!email.includes("@")) {
const decrypt = AES.decrypt(email, KEY, { iv: IV });
const decrypted = decrypt.toString(enc.Utf8);
model.dataValues.email = decrypted;
console.log(`[hook afterCreate] email "${email}" was changed to "${decrypted}"`);
} else {
console.log(`[hook afterCreate] skipped "${email}"`);
}
}
}
}
);
Email.belongsTo(User, { allowNull: true });
User.hasOne(Email, { allowNull: true });
sequelize
.authenticate()
.then(() => sequelize.sync({ force: true }))
.then(() => main())
.catch(err => {
console.log(err);
});
async function create() {
const aUser = await User.build({ name: "John Doe" });
const anEmail = await Email.build({ email: "[email protected]" });
aUser.setEmail(anEmail);
await aUser.save();
}
async function findUser() {
console.log("[function findUser] Executing");
const existingUser = await User.findOne({
include: [{ model: Email }],
raw: true
});
console.log("[function findUser] Result:", existingUser);
}
async function findEmail() {
console.log("[function findEmail] Executing");
const existingEmail = await Email.findOne({
raw: true
});
console.log("[function findEmail] Result:", existingEmail);
}
async function main() {
await create();
console.log();
await findUser();
console.log();
await findEmail();
}
Upvotes: 5
Views: 7356
Reputation: 2850
This is a known problem in sequelize and there don't seem to be any plans to fix it. See github issue here.
An alternative approach is to use getters and setters on model properties. The problem with hooks however is that they don't support async so no promises or callbacks.
Here's a guide on how to use getters/setters
Upvotes: 3