UNPKG

@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
"use strict"; 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;