Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion lib/migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@ function mixinMigration(PostgreSQL) {
});
// default extension
if (!createExtensions) {
createExtensions = 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";';
createExtensions = `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";`;
}

// Please note IF NOT EXISTS is introduced in postgresql v9.3
Expand Down
26 changes: 25 additions & 1 deletion lib/postgresql.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ PostgreSQL.prototype.connect = function(callback) {
self.client = client;
process.nextTick(releaseCb);
callback && callback(err, client);
if (!err) self.execute('CREATE EXTENSION IF NOT EXISTS pgcrypto', function(createExtensionError) {});
});
};

Expand Down Expand Up @@ -588,6 +589,17 @@ PostgreSQL.prototype.buildWhere = function(model, where) {
return whereClause;
};

PostgreSQL.prototype.getEncryptionFields = function(modelDefinition) {
if (modelDefinition
&& modelDefinition.settings
&& modelDefinition.settings.mixins
&& modelDefinition.settings.mixins.Encryption
&& modelDefinition.settings.mixins.Encryption.fields) {
return modelDefinition.settings.mixins.Encryption.fields;
}
return [];
};

/**
* @private
* @param model
Expand All @@ -606,6 +618,7 @@ PostgreSQL.prototype._buildWhere = function(model, where) {
const self = this;
const props = self.getModelDefinition(model).properties;

const encryptedFields = this.getEncryptionFields(this.getModelDefinition(model));
const whereStmts = [];
for (const key in where) {
const stmt = new ParameterizedSQL('', []);
Expand Down Expand Up @@ -646,7 +659,18 @@ PostgreSQL.prototype._buildWhere = function(model, where) {
}
// eslint-disable one-var
let expression = where[key];
const columnName = self.columnEscaped(model, key);
let columnName = self.columnEscaped(model, key);
if (encryptedFields.includes(key)) {
columnName = `convert_from(
decrypt_iv(
DECODE(${key},'hex')::bytea,
decode('${process.env.ENCRYPTION_HEX_KEY}','hex')::bytea,
decode('${process.env.ENCRYPTION_HEX_IV}','hex')::bytea,
'aes'
),
'utf8'
)`;
}
// eslint-enable one-var
if (expression === null || expression === undefined) {
stmt.merge(columnName + ' IS NULL');
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
},
"devDependencies": {
"eslint": "^7.7.0",
"chai": "^4.3.4",
"chai-subset": "^1.6.0",
"eslint-config-loopback": "^13.1.0",
"juggler-v3": "file:./deps/juggler-v3",
"juggler-v4": "file:./deps/juggler-v4",
Expand Down
3 changes: 3 additions & 0 deletions test/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ process.env.PGUSER = process.env.POSTGRESQL_USER ||
process.env.PGPASSWORD = process.env.POSTGRESQL_PASSWORD ||
process.env.PGPASSWORD ||
'';
process.env.ENCRYPTION_HEX_KEY = process.env.ENCRYPTION_HEX_KEY || 'abcdef0123456789abcdef0123456789';
process.env.ENCRYPTION_HEX_IV = process.env.ENCRYPTION_HEX_IV || '0123456789abcdef0123456789abcdef';

config = {
host: process.env.PGHOST,
port: process.env.PGPORT,
Expand Down
79 changes: 79 additions & 0 deletions test/postgresql.encrypted.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
// Node module: loopback-connector-postgresql
// This file is licensed under the Artistic License 2.0.
// License text available at https://opensource.org/licenses/Artistic-2.0

'use strict';
process.env.NODE_ENV = 'test';
require('should');
const expect = require('chai').expect;
const async = require('async');
const chai = require('chai');
const chaiSubset = require('chai-subset');
chai.use(chaiSubset);

let db;

before(function() {
db = global.getSchema();
});

describe('Mapping models', function() {
it('should return encrypted data by filter', function(done) {
const schema =
{
'name': 'EncryptedData',
'options': {
'idInjection': false,
'postgresql': {
'schema': 'public', 'table': 'encrypted_data',
},
},
'properties': {
'id': {
'type': 'String',
'id': true,
},
'data': {
'type': 'String',
},
},
'mixins': {
'Encryption': {
'fields': [
'data',
],
},
},
};

const EncryptedData = db.createModel(schema.name, schema.properties, schema.options);
EncryptedData.settings.mixins = schema.mixins;

db.automigrate('EncryptedData', function(err) {
if (err) console.error({err});
EncryptedData.create({
id: '2',
data: '1c93722e6cf53f93dd4eb15a18444dc3e910fded18239db612794059af1fa5e8',
}, function(err, encryptedData) {
if (err) console.log({err2: err});
async.series([
function(callback) {
EncryptedData.findOne({where: {data: {ilike: '%test%'}}}, function(err, retreivedData) {
if (err) console.error({err111: err});
expect(retreivedData).to.containSubset(encryptedData);
callback(null, retreivedData);
});
},
function(callback) {
EncryptedData.find({where: {data: {ilike: '%not found%'}}}, function(err, retreivedData) {
if (err) console.error({err111: err});
expect(retreivedData.length).to.equal(0);
callback(null, retreivedData);
});
},
], done);
});
});
});
});
12 changes: 12 additions & 0 deletions test/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,16 @@ CREATE TABLE reservation (
);


--
-- Name: encrypted_data; Type: TABLE; Schema: strongloop; Owner: strongloop
--

CREATE TABLE encrypted_data (
id character varying(64),
data text
);


--
-- Name: session; Type: TABLE; Schema: strongloop; Owner: strongloop
--
Expand Down Expand Up @@ -1207,6 +1217,8 @@ INSERT INTO product VALUES ('87', 'NV Goggles', NULL, NULL, NULL, NULL, NULL);
INSERT INTO product VALUES ('2', 'G17', 53, 75, 15, 'Flashlight', 'Single');
INSERT INTO product VALUES ('5', 'M9 SD', 0, 75, 15, 'Silenced', 'Single');

INSERT INTO encrypted_data VALUES('1', '1c93722e6cf53f93dd4eb15a18444dc3e910fded18239db612794059af1fa5e8');


--
-- Data for Name: reservation; Type: TABLE DATA; Schema: strongloop; Owner: strongloop
Expand Down