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

[patch] Use Node version check to determine whether to process encryption options for each model #1532

Closed
wants to merge 3 commits into from
Closed
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
317 changes: 160 additions & 157 deletions lib/waterline.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,191 +176,194 @@ function Waterline() {
});//∞
});//∞

var RX_NODE_MAJOR_DOT_MINOR = /^v([^.]+\.?[^.]+)\./;
var parsedNodeMajorAndMinorVersion = process.version.match(RX_NODE_MAJOR_DOT_MINOR) && (+(process.version.match(RX_NODE_MAJOR_DOT_MINOR)[1]));
var MIN_NODE_VERSION = 6;
var isNativeCryptoFullyCapable = parsedNodeMajorAndMinorVersion >= MIN_NODE_VERSION;

// Only allow using at-rest encryption for compatible Node versions
if (areAnyModelsUsingAtRestEncryption) {
var RX_NODE_MAJOR_DOT_MINOR = /^v([^.]+\.?[^.]+)\./;
var parsedNodeMajorAndMinorVersion = process.version.match(RX_NODE_MAJOR_DOT_MINOR) && (+(process.version.match(RX_NODE_MAJOR_DOT_MINOR)[1]));
var MIN_NODE_VERSION = 6;
var isNativeCryptoFullyCapable = parsedNodeMajorAndMinorVersion >= MIN_NODE_VERSION;
if (!isNativeCryptoFullyCapable) {
throw new Error('Current installed node version\'s native `crypto` module is not fully capable of the necessary functionality for encrypting/decrypting data at rest with Waterline. To use this feature, please upgrade to Node v' + MIN_NODE_VERSION + ' or above, flush your node_modules, run npm install, and then try again. Otherwise, if you cannot upgrade Node.js, please remove the `encrypt` property from your models\' attributes.');
}
if (areAnyModelsUsingAtRestEncryption && !isNativeCryptoFullyCapable) {
throw new Error('Current installed node version\'s native `crypto` module is not fully capable of the necessary functionality for encrypting/decrypting data at rest with Waterline. To use this feature, please upgrade to Node v' + MIN_NODE_VERSION + ' or above, flush your node_modules, run npm install, and then try again. Otherwise, if you cannot upgrade Node.js, please remove the `encrypt` property from your models\' attributes.');
}//fi

_.each(wmds, function(wmd){

var modelDef = wmd.prototype;

// Verify that `encrypt` attr prop is valid, if in use.
var isThisModelUsingAtRestEncryption;
try {
_.each(modelDef.attributes, function(attrDef, attrName){
if (attrDef.encrypt !== undefined) {
if (!_.isBoolean(attrDef.encrypt)){
throw flaverr({
code: 'E_INVALID_ENCRYPT',
attrName: attrName,
message: 'If set, `encrypt` must be either `true` or `false`.'
});
}//•
if (isNativeCryptoFullyCapable) {

if (attrDef.encrypt === true){
_.each(wmds, function(wmd){

isThisModelUsingAtRestEncryption = true;
var modelDef = wmd.prototype;

if (attrDef.type === 'ref') {
// Verify that `encrypt` attr prop is valid, if in use.
var isThisModelUsingAtRestEncryption;
try {
_.each(modelDef.attributes, function(attrDef, attrName){
if (attrDef.encrypt !== undefined) {
if (!_.isBoolean(attrDef.encrypt)){
throw flaverr({
code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION',
code: 'E_INVALID_ENCRYPT',
attrName: attrName,
whyNotCompatible: 'with `type: \'ref\'` attributes.'
message: 'If set, `encrypt` must be either `true` or `false`.'
});
}//•

if (attrDef.autoCreatedAt || attrDef.autoUpdatedAt) {
throw flaverr({
code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION',
attrName: attrName,
whyNotCompatible: 'with `'+(attrDef.autoCreatedAt?'autoCreatedAt':'autoUpdatedAt')+'` attributes.'
});
}//•
if (attrDef.encrypt === true){

isThisModelUsingAtRestEncryption = true;

if (attrDef.type === 'ref') {
throw flaverr({
code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION',
attrName: attrName,
whyNotCompatible: 'with `type: \'ref\'` attributes.'
});
}//•

if (attrDef.autoCreatedAt || attrDef.autoUpdatedAt) {
throw flaverr({
code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION',
attrName: attrName,
whyNotCompatible: 'with `'+(attrDef.autoCreatedAt?'autoCreatedAt':'autoUpdatedAt')+'` attributes.'
});
}//•

if (attrDef.model || attrDef.collection) {
throw flaverr({
code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION',
attrName: attrName,
whyNotCompatible: 'with associations.'
});
}//•

if (attrDef.defaultsTo !== undefined) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: Consider adding support for this. Will require some refactoring
// in order to do it right (i.e. otherwise we'll just be copying and pasting
// the encryption logic.) We'll want to pull it out from normalize-value-to-set
// into a new utility, then call that from the appropriate spot in
// normalize-new-record in order to encrypt the initial default value.
//
// (See also the other note in normalize-new-record re defaultsTo + cloneDeep.)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
throw flaverr({
code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION',
attrName: attrName,
whyNotCompatible: 'with an attribute that also specifies a `defaultsTo`. '+
'Please remove the `defaultsTo` from this attribute definition.'
});
}//•

}//fi

if (attrDef.model || attrDef.collection) {
}//fi
});//∞
} catch (err) {
switch (err.code) {
case 'E_INVALID_ENCRYPT':
throw flaverr({
message:
'Invalid usage of `encrypt` in the definition for `'+modelDef.identity+'` model\'s '+
'`'+err.attrName+'` attribute. '+err.message
}, err);
case 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION':
throw flaverr({
message:
'Invalid usage of `encrypt` in the definition for `'+modelDef.identity+'` model\'s '+
'`'+err.attrName+'` attribute. At-rest encryption (`encrypt: true`) cannot be used '+
err.whyNotCompatible
}, err);
default: throw err;
}
}


// Verify `dataEncryptionKeys`.
// (Remember, if there is a secondary key system in use, these DEKs should have
// already been "unwrapped" before they were passed in to Waterline as model settings.)
if (modelDef.dataEncryptionKeys !== undefined) {

if (!_.isObject(modelDef.dataEncryptionKeys) || _.isArray(modelDef.dataEncryptionKeys) || _.isFunction(modelDef.dataEncryptionKeys)) {
throw flaverr({
message: 'In the definition for the `'+modelDef.identity+'` model, the `dataEncryptionKeys` model setting '+
'is invalid. If specified, `dataEncryptionKeys` must be a dictionary (plain JavaScript object).'
});
}//•

// Check all DEKs for validity.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// (FUTURE: maybe extend EA to support a `validateKeys()` method instead of this--
// or at least to have error code)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
try {
_.each(modelDef.dataEncryptionKeys, function(dek, dekId){

if (!dek || !_.isString(dek)) {
throw flaverr({
code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION',
attrName: attrName,
whyNotCompatible: 'with associations.'
code: 'E_INVALID_DATA_ENCRYPTION_KEYS',
dekId: dekId,
message: 'Must be a cryptographically random, 32 byte string.'
});
}//•

if (attrDef.defaultsTo !== undefined) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: Consider adding support for this. Will require some refactoring
// in order to do it right (i.e. otherwise we'll just be copying and pasting
// the encryption logic.) We'll want to pull it out from normalize-value-to-set
// into a new utility, then call that from the appropriate spot in
// normalize-new-record in order to encrypt the initial default value.
//
// (See also the other note in normalize-new-record re defaultsTo + cloneDeep.)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if (!dekId.match(/^[a-z\$]([a-z0-9])*$/i)){
throw flaverr({
code: 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION',
attrName: attrName,
whyNotCompatible: 'with an attribute that also specifies a `defaultsTo`. '+
'Please remove the `defaultsTo` from this attribute definition.'
code: 'E_INVALID_DATA_ENCRYPTION_KEYS',
dekId: dekId,
message: 'Please make sure the ids of all of your data encryption keys begin with a letter and do not contain any special characters.'
});
}//•

}//fi

}//fi
});//∞
} catch (err) {
switch (err.code) {
case 'E_INVALID_ENCRYPT':
throw flaverr({
message:
'Invalid usage of `encrypt` in the definition for `'+modelDef.identity+'` model\'s '+
'`'+err.attrName+'` attribute. '+err.message
}, err);
case 'E_ATTR_NOT_COMPATIBLE_WITH_AT_REST_ENCRYPTION':
throw flaverr({
message:
'Invalid usage of `encrypt` in the definition for `'+modelDef.identity+'` model\'s '+
'`'+err.attrName+'` attribute. At-rest encryption (`encrypt: true`) cannot be used '+
err.whyNotCompatible
}, err);
default: throw err;
}
}


// Verify `dataEncryptionKeys`.
// (Remember, if there is a secondary key system in use, these DEKs should have
// already been "unwrapped" before they were passed in to Waterline as model settings.)
if (modelDef.dataEncryptionKeys !== undefined) {

if (!_.isObject(modelDef.dataEncryptionKeys) || _.isArray(modelDef.dataEncryptionKeys) || _.isFunction(modelDef.dataEncryptionKeys)) {
throw flaverr({
message: 'In the definition for the `'+modelDef.identity+'` model, the `dataEncryptionKeys` model setting '+
'is invalid. If specified, `dataEncryptionKeys` must be a dictionary (plain JavaScript object).'
});
}//•

// Check all DEKs for validity.
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// (FUTURE: maybe extend EA to support a `validateKeys()` method instead of this--
// or at least to have error code)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
try {
_.each(modelDef.dataEncryptionKeys, function(dek, dekId){

if (!dek || !_.isString(dek)) {
throw flaverr({
code: 'E_INVALID_DATA_ENCRYPTION_KEYS',
dekId: dekId,
message: 'Must be a cryptographically random, 32 byte string.'
});
}//•

if (!dekId.match(/^[a-z\$]([a-z0-9])*$/i)){
throw flaverr({
code: 'E_INVALID_DATA_ENCRYPTION_KEYS',
dekId: dekId,
message: 'Please make sure the ids of all of your data encryption keys begin with a letter and do not contain any special characters.'
});
}//•

try {
var EA = require('encrypted-attr');
EA(undefined, { keys: modelDef.dataEncryptionKeys, keyId: dekId }).encryptAttribute(undefined, 'test-value-purely-for-validation');
} catch (err) {
throw flaverr({
code: 'E_INVALID_DATA_ENCRYPTION_KEYS',
dekId: dekId
}, err);
try {
var EA = require('encrypted-attr');
EA(undefined, { keys: modelDef.dataEncryptionKeys, keyId: dekId }).encryptAttribute(undefined, 'test-value-purely-for-validation');
} catch (err) {
throw flaverr({
code: 'E_INVALID_DATA_ENCRYPTION_KEYS',
dekId: dekId
}, err);
}

});//∞
} catch (err) {
switch (err.code) {
case 'E_INVALID_DATA_ENCRYPTION_KEYS':
throw flaverr({
message: 'In the definition for the `'+modelDef.identity+'` model, one of the data encryption keys (`dataEncryptionKeys.'+err.dekId+'`) is invalid.\n'+
'Details:\n'+
' '+err.message
}, err);
default:
throw err;
}

});//∞
} catch (err) {
switch (err.code) {
case 'E_INVALID_DATA_ENCRYPTION_KEYS':
throw flaverr({
message: 'In the definition for the `'+modelDef.identity+'` model, one of the data encryption keys (`dataEncryptionKeys.'+err.dekId+'`) is invalid.\n'+
'Details:\n'+
' '+err.message
}, err);
default:
throw err;
}
}

}//fi
}//fi


// If any attrs have `encrypt: true`, verify that there is both a valid
// `dataEncryptionKeys` dictionary and a valid `dataEncryptionKeys.default` DEK set.
if (isThisModelUsingAtRestEncryption) {
// If any attrs have `encrypt: true`, verify that there is both a valid
// `dataEncryptionKeys` dictionary and a valid `dataEncryptionKeys.default` DEK set.
if (isThisModelUsingAtRestEncryption) {

if (!modelDef.dataEncryptionKeys || !modelDef.dataEncryptionKeys.default) {
throw flaverr({
message:
'DEKs should be 32 bytes long, and cryptographically random. A random, default DEK is included '+
'in new Sails apps, so one easy way to generate a new DEK is to generate a new Sails app. '+
'Alternatively, you could run:\n'+
' require(\'crypto\').randomBytes(32).toString(\'base64\')\n'+
'\n'+
'Remember: once in production, you should manage your DEKs like you would any other sensitive credential. '+
'For example, one common best practice is to configure them using environment variables.\n'+
'In a Sails app:\n'+
' sails_models__dataEncryptionKeys__default=vpB2EhXaTi+wYKUE0ojI5cVQX/VRGP++Fa0bBW/NFSs=\n'+
'\n'+
' [?] If you\'re unsure or want advice, head over to https://sailsjs.com/support'
});
}//•
}//fi
if (!modelDef.dataEncryptionKeys || !modelDef.dataEncryptionKeys.default) {
throw flaverr({
message:
'DEKs should be 32 bytes long, and cryptographically random. A random, default DEK is included '+
'in new Sails apps, so one easy way to generate a new DEK is to generate a new Sails app. '+
'Alternatively, you could run:\n'+
' require(\'crypto\').randomBytes(32).toString(\'base64\')\n'+
'\n'+
'Remember: once in production, you should manage your DEKs like you would any other sensitive credential. '+
'For example, one common best practice is to configure them using environment variables.\n'+
'In a Sails app:\n'+
' sails_models__dataEncryptionKeys__default=vpB2EhXaTi+wYKUE0ojI5cVQX/VRGP++Fa0bBW/NFSs=\n'+
'\n'+
' [?] If you\'re unsure or want advice, head over to https://sailsjs.com/support'
});
}//•
}//fi


});//∞
});//∞

} // </ if (isNativeCryptoFullyCapable) >


// Next, set up support for the default archive, and validate related settings:
Expand Down