From ba2e854d461b9dde2ce412e01811e83157dcd5fe Mon Sep 17 00:00:00 2001 From: ashutoshdhande <50861363+ashutoshdhande@users.noreply.github.com> Date: Wed, 23 Aug 2023 13:17:20 +0530 Subject: [PATCH] feat: encrypt pii data (#203) Co-authored-by: jatin --- packages/backend/config.ts | 1 + packages/backend/helpers/gcmUtil.ts | 48 +++++++++++++++++ packages/backend/prisma/client.ts | 66 +++++++++++++++++++++++ packages/backend/services/connection.ts | 3 +- scripts/backend/encrypt_customerEmails.js | 49 +++++++++++++++++ 5 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 packages/backend/helpers/gcmUtil.ts create mode 100644 scripts/backend/encrypt_customerEmails.js diff --git a/packages/backend/config.ts b/packages/backend/config.ts index cd1627aab..27830d4a8 100644 --- a/packages/backend/config.ts +++ b/packages/backend/config.ts @@ -23,6 +23,7 @@ const config = { SHORTLOOP_AUTH_KEY: process.env.SHORTLOOP_AUTH_KEY!, SVIX_ENDPOINT_SECRET: process.env.SVIX_ENDPOINT_SECRET!, svix: new Svix(process.env.SVIX_AUTH_TOKEN!), + AES_ENCRYPTION_SECRET: process.env.AES_ENCRYPTION_SECRET!, }; export default config; diff --git a/packages/backend/helpers/gcmUtil.ts b/packages/backend/helpers/gcmUtil.ts new file mode 100644 index 000000000..e6bb3bad7 --- /dev/null +++ b/packages/backend/helpers/gcmUtil.ts @@ -0,0 +1,48 @@ +import crypto from 'crypto'; + +const CIPHER_ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 16; +const TAG_LENGTH = 16; +const SALT_LENGTH = 64; +const ITERATIONS = 10000; + +const tagPosition = SALT_LENGTH + IV_LENGTH; +const encryptedPosition = tagPosition + TAG_LENGTH; + +const getKey = (salt: Buffer, secret: string) => { + return crypto.pbkdf2Sync(secret, salt, ITERATIONS, 32, 'sha256'); +}; + +const gcm = { + encrypt: (input: string, secret: string) => { + const iv = crypto.randomBytes(IV_LENGTH); + const salt = crypto.randomBytes(SALT_LENGTH); + + const AES_KEY = getKey(salt, secret); + + const cipher = crypto.createCipheriv(CIPHER_ALGORITHM, AES_KEY, iv); + const encrypted = Buffer.concat([cipher.update(String(input), 'utf8'), cipher.final()]); + + const tag = cipher.getAuthTag(); + + return Buffer.concat([salt, iv, tag, encrypted]).toString('hex'); + }, + + decrypt: (input: string, secret: string) => { + const inputValue = Buffer.from(String(input), 'hex'); + const salt = inputValue.subarray(0, SALT_LENGTH); + const iv = inputValue.subarray(SALT_LENGTH, tagPosition); + const tag = inputValue.subarray(tagPosition, encryptedPosition); + const encrypted = inputValue.subarray(encryptedPosition); + + const key = getKey(salt, secret); + + const decipher = crypto.createDecipheriv(CIPHER_ALGORITHM, key, iv); + + decipher.setAuthTag(tag); + + return decipher.update(encrypted) + decipher.final('utf8'); + }, +}; + +export default gcm; diff --git a/packages/backend/prisma/client.ts b/packages/backend/prisma/client.ts index 6610722ae..b47a3c982 100644 --- a/packages/backend/prisma/client.ts +++ b/packages/backend/prisma/client.ts @@ -1,6 +1,72 @@ import { PrismaClient, Prisma } from '@prisma/client'; +import config from '../config'; +import gcm from '../helpers/gcmUtil'; const prisma = new PrismaClient(); +// Prisma middleware to encrypt and decrypt data on fly +prisma.$use(async (params, next) => { + if (params.model !== 'connections') { + return next(params); + } + + const isReadOperation = ['findFirst', 'findMany', 'delete'].includes(params.action); + const isWriteOperation = ['update', 'upsert'].includes(params.action); + + if (isReadOperation) { + return handleReadOperation(params, next); + } else if (isWriteOperation) { + return handleWriteOperation(params, next); + } + return next(params); +}); + +async function handleReadOperation( + params: Prisma.MiddlewareParams, + next: (params: Prisma.MiddlewareParams) => Promise +): Promise { + try { + const connections = await next(params); + if (!connections) return connections; + if (Array.isArray(connections)) { + connections.forEach((connection) => { + connection.tp_customer_id = gcm.decrypt(connection.tp_customer_id, config.AES_ENCRYPTION_SECRET); + }); + } else { + connections.tp_customer_id = gcm.decrypt(connections.tp_customer_id, config.AES_ENCRYPTION_SECRET); + } + return connections; + } catch (error: any) { + throw new Error(error); + } +} + +async function handleWriteOperation( + params: Prisma.MiddlewareParams, + _: (params: Prisma.MiddlewareParams) => Promise +): Promise { + try { + if (Array.isArray(params.args?.data)) { + params.args?.data.forEach((connection: any) => { + connection.tp_customer_id = gcm.encrypt(connection.tp_customer_id, config.AES_ENCRYPTION_SECRET); + }); + } else { + if (params.action === 'upsert') { + params.args.create.tp_customer_id = gcm.encrypt( + params.args.create.tp_customer_id, + config.AES_ENCRYPTION_SECRET + ); + } else { + params.args.data.tp_customer_id = gcm.encrypt( + params.args.data.tp_customer_id, + config.AES_ENCRYPTION_SECRET + ); + } + } + } catch (error: any) { + throw new Error(error); + } +} + export default prisma; export { Prisma }; diff --git a/packages/backend/services/connection.ts b/packages/backend/services/connection.ts index 7b85758e5..bda9db0d6 100644 --- a/packages/backend/services/connection.ts +++ b/packages/backend/services/connection.ts @@ -102,8 +102,7 @@ const connectionService = new ConnectionService({ const deleted: any = await prisma.connections.delete({ // TODO: Add environments to connections. where: { - uniqueCustomerPerTenantPerThirdParty: { - tp_customer_id: connection.tp_customer_id, + uniqueThirdPartyPerTenant: { t_id: connection.t_id, tp_id: connection.tp_id, }, diff --git a/scripts/backend/encrypt_customerEmails.js b/scripts/backend/encrypt_customerEmails.js new file mode 100644 index 000000000..37c6b1463 --- /dev/null +++ b/scripts/backend/encrypt_customerEmails.js @@ -0,0 +1,49 @@ +const cryp = require('crypto'); + +const CIPHER_ALGORITHM = 'aes-256-gcm'; +const IV_LENGTH = 16; +const TAG_LENGTH = 16; +const SALT_LENGTH = 64; +const ITERATIONS = 10000; + +const tagPosition = SALT_LENGTH + IV_LENGTH; +const encryptedPosition = tagPosition + TAG_LENGTH; + +const getKey = (salt, secret) => { + return cryp.pbkdf2Sync(secret, salt, ITERATIONS, 32, 'sha256'); +}; + +const gcm = { + encrypt: (input, secret) => { + const iv = cryp.randomBytes(IV_LENGTH); + const salt = cryp.randomBytes(SALT_LENGTH); + + const AES_KEY = getKey(salt, secret); + + const cipher = cryp.createCipheriv(CIPHER_ALGORITHM, AES_KEY, iv); + const encrypted = Buffer.concat([cipher.update(String(input), 'utf8'), cipher.final()]); + + const tag = cipher.getAuthTag(); + + return Buffer.concat([salt, iv, tag, encrypted]).toString('hex'); + }, + + decrypt: (input, secret) => { + const inputValue = Buffer.from(String(input), 'hex'); + const salt = inputValue.subarray(0, SALT_LENGTH); + const iv = inputValue.subarray(SALT_LENGTH, tagPosition); + const tag = inputValue.subarray(tagPosition, encryptedPosition); + const encrypted = inputValue.subarray(encryptedPosition); + + const key = getKey(salt, secret); + + const decipher = cryp.createDecipheriv(CIPHER_ALGORITHM, key, iv); + + decipher.setAuthTag(tag); + + return decipher.update(encrypted) + decipher.final('utf8'); + }, +}; + +// Usage +// console.log('jatin@revert.dev :', gcm.encrypt('jatin@revert.dev', process.env.AES_ENCRYPTION_SECRET));