@godspeedsystems/prisma-deterministic-search-field-encryption
Version:
Transparent and customizable field-level encryption at rest for Prisma based on prisma-field-encryption package
176 lines (175 loc) • 7.47 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.decryptOnRead = exports.encryptOnWrite = exports.configureKeys = exports.configureFunctions = exports.isCustomConfiguration = exports.isDefaultConfiguration = exports.getMethod = exports.configureKeysAndFunctions = void 0;
const cloak_1 = require("@47ng/cloak");
const immer_1 = __importDefault(require("immer"));
const object_path_1 = __importDefault(require("object-path"));
const errors_1 = require("./errors");
const visitor_1 = require("./visitor");
const ENCRYPTION_KEY_PROP = 'encryptionKey';
const DECRYPTION_KEYS_PROP = 'decryptionKeys';
const ENCRYPTION_FN_PROP = 'encryptFn';
const DECRYPTION_FN_PROP = 'decryptFn';
function configureKeysAndFunctions(config) {
const method = getMethod(config);
const keys = method === 'DEFAULT' ? configureKeys(config) : null;
const cipherFunctions = method === 'CUSTOM' ? configureFunctions(config) : null;
return { cipherFunctions, keys, method };
}
exports.configureKeysAndFunctions = configureKeysAndFunctions;
function getMethod(config) {
if (isDefaultConfiguration(config)) {
return 'DEFAULT';
}
if (isCustomConfiguration(config)) {
return 'CUSTOM';
}
throw new Error(errors_1.errors.invalidConfig);
}
exports.getMethod = getMethod;
function isDefaultConfiguration(config) {
return !(ENCRYPTION_FN_PROP in config) && !(DECRYPTION_FN_PROP in config);
}
exports.isDefaultConfiguration = isDefaultConfiguration;
function isCustomConfiguration(config) {
return (ENCRYPTION_FN_PROP in config &&
DECRYPTION_FN_PROP in config &&
!(ENCRYPTION_KEY_PROP in config) &&
!(DECRYPTION_KEYS_PROP in config));
}
exports.isCustomConfiguration = isCustomConfiguration;
function configureFunctions(config) {
const encryptFn = config[ENCRYPTION_FN_PROP];
const decryptFn = config[DECRYPTION_FN_PROP];
if (typeof encryptFn !== 'function' || typeof decryptFn !== 'function') {
throw new Error(errors_1.errors.invalidFunctionsConfiguration);
}
const cipherFunctions = {
encryptFn,
decryptFn
};
return cipherFunctions;
}
exports.configureFunctions = configureFunctions;
function configureKeys(config) {
var _a, _b;
const configureKeysParams = {
encryptionKey: config[ENCRYPTION_KEY_PROP],
decryptionKeys: config[DECRYPTION_KEYS_PROP]
};
const encryptionKey = configureKeysParams.encryptionKey || process.env.PRISMA_FIELD_ENCRYPTION_KEY;
if (!encryptionKey) {
throw new Error(errors_1.errors.noEncryptionKey);
}
const decryptionKeysFromEnv = ((_a = process.env.PRISMA_FIELD_DECRYPTION_KEYS) !== null && _a !== void 0 ? _a : '')
.split(',')
.filter(Boolean);
const decryptionKeys = Array.from(new Set([
encryptionKey,
...((_b = configureKeysParams.decryptionKeys) !== null && _b !== void 0 ? _b : decryptionKeysFromEnv)
]));
const keychain = (0, cloak_1.makeKeychainSync)(decryptionKeys);
return {
encryptionKey: (0, cloak_1.parseKeySync)(encryptionKey),
keychain
};
}
exports.configureKeys = configureKeys;
// --
const writeOperations = [
'create',
'createMany',
'update',
'updateMany',
'upsert'
];
const whereClauseRegExp = /\.where\./;
function encryptOnWrite(params, models, operation, method, keys, encryptFn) {
// Commenting this code so as to apply encryption in all the methods so that in case of deterministic algorithm, search operation can be performed on the encrypted field.
// if (!writeOperations.includes(params.action)) {
// return params // No input data to encrypt
// }
const encryptionErrors = [];
const mutatedParams = (0, immer_1.default)(params, (draft) => {
(0, visitor_1.visitInputTargetFields)(draft, models, function encryptFieldValue({ fieldConfig, value: clearText, path, model, field }) {
if (!fieldConfig.encrypt) {
return;
}
if (whereClauseRegExp.test(path)) {
console.warn(errors_1.warnings.whereClause(operation, path));
}
try {
let cipherText;
if (method === 'CUSTOM' && !!encryptFn) {
cipherText = encryptFn(clearText);
}
if (method === 'DEFAULT' && !!keys) {
cipherText = (0, cloak_1.encryptStringSync)(clearText, keys.encryptionKey);
}
// if (!cipherText) {
// throw new Error(errors.invalidConfig)
// }
object_path_1.default.set(draft.args, path, cipherText);
}
catch (error) {
encryptionErrors.push(errors_1.errors.fieldEncryptionError(model, field, path, error));
}
});
});
if (encryptionErrors.length > 0) {
throw new Error(errors_1.errors.encryptionErrorReport(operation, encryptionErrors));
}
return mutatedParams;
}
exports.encryptOnWrite = encryptOnWrite;
function decryptOnRead(params, result, models, operation, method, keys, decryptFn) {
var _a, _b;
// Analyse the query to see if there's anything to decrypt.
const model = models[params.model];
if (Object.keys(model.fields).length === 0 && Object.keys(model.connections).length === 0 && !((_a = params.args) === null || _a === void 0 ? void 0 : _a.include) && !((_b = params.args) === null || _b === void 0 ? void 0 : _b.select)) {
// The queried model doesn't have any encrypted field,
// and there are no included connections.
// We can safely skip decryption for the returned data.
// todo: Walk the include/select tree for a better decision.
return;
}
const decryptionErrors = [];
const fatalDecryptionErrors = [];
(0, visitor_1.visitOutputTargetFields)(params, result, models, function decryptFieldValue({ fieldConfig, value: cipherText, path, model, field }) {
try {
if (!decryptFn && !cloak_1.cloakedStringRegex.test(cipherText)) {
return;
}
let clearText;
if (method === 'CUSTOM' && !!decryptFn) {
clearText = decryptFn(cipherText);
}
if (method === 'DEFAULT' && !!keys) {
clearText = (0, cloak_1.decryptStringSync)(cipherText, (0, cloak_1.findKeyForMessage)(cipherText, keys.keychain));
}
// if (!clearText) {
// throw new Error(errors.invalidConfig)
// }
object_path_1.default.set(result, path, clearText);
}
catch (error) {
const message = errors_1.errors.fieldDecryptionError(model, field, path, error);
if (fieldConfig.strictDecryption) {
fatalDecryptionErrors.push(message);
}
else {
decryptionErrors.push(message);
}
}
});
if (decryptionErrors.length > 0) {
console.error(errors_1.errors.encryptionErrorReport(operation, decryptionErrors));
}
if (fatalDecryptionErrors.length > 0) {
throw new Error(errors_1.errors.decryptionErrorReport(operation, fatalDecryptionErrors));
}
}
exports.decryptOnRead = decryptOnRead;
;