From d766b58f42cfeefab9ea59ab67009d088093355d Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 25 Jan 2024 16:07:57 +1100 Subject: [PATCH] feat: add server crypto (#863) ## Description Currently we are required to ensure PII data is not passed around in search parameters and in the open for GDPR reasons. Allowing us to encrypt and decrypt values with expiry dates will allow us to ensure this doesn't happen. ## Changes Made - Added TPRC router for encryption method ## Testing Performed - Tested encrypting and decrypting data with and without `expiredAt` - Tested via directly accessing API and also via trpc in react components - Tested parsing en email search param in a page and decrypting it successfully ## Checklist - [X] I have tested these changes locally and they work as expected. - [X] I have followed the project's coding style guidelines. --- .env.example | 6 ++-- packages/lib/constants/crypto.ts | 22 ++++++++++++ packages/lib/server-only/crypto/decrypt.ts | 33 +++++++++++++++++ packages/lib/server-only/crypto/encrypt.ts | 42 ++++++++++++++++++++++ packages/trpc/server/crypto/router.ts | 17 +++++++++ packages/trpc/server/crypto/schema.ts | 15 ++++++++ packages/trpc/server/router.ts | 2 ++ packages/tsconfig/process-env.d.ts | 1 + turbo.json | 1 + 9 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 packages/lib/server-only/crypto/decrypt.ts create mode 100644 packages/lib/server-only/crypto/encrypt.ts create mode 100644 packages/trpc/server/crypto/router.ts create mode 100644 packages/trpc/server/crypto/schema.ts diff --git a/.env.example b/.env.example index d188894de..06498f2bc 100644 --- a/.env.example +++ b/.env.example @@ -4,8 +4,10 @@ NEXTAUTH_SECRET="secret" # [[CRYPTO]] # Application Key for symmetric encryption and decryption -# This should be a random string of at least 32 characters -NEXT_PRIVATE_ENCRYPTION_KEY="CAFEBABE" +# REQUIRED: This should be a random string of at least 32 characters +NEXT_PRIVATE_ENCRYPTION_KEY="" +# REQUIRED: This should be a random string of at least 32 characters +NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="" # [[AUTH OPTIONAL]] NEXT_PRIVATE_GOOGLE_CLIENT_ID="" diff --git a/packages/lib/constants/crypto.ts b/packages/lib/constants/crypto.ts index d911cd6cf..40d3ef113 100644 --- a/packages/lib/constants/crypto.ts +++ b/packages/lib/constants/crypto.ts @@ -1 +1,23 @@ export const DOCUMENSO_ENCRYPTION_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_KEY; + +export const DOCUMENSO_ENCRYPTION_SECONDARY_KEY = process.env.NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY; + +if (!DOCUMENSO_ENCRYPTION_KEY || !DOCUMENSO_ENCRYPTION_SECONDARY_KEY) { + throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY or DOCUMENSO_ENCRYPTION_SECONDARY_KEY keys'); +} + +if (DOCUMENSO_ENCRYPTION_KEY === DOCUMENSO_ENCRYPTION_SECONDARY_KEY) { + throw new Error( + 'DOCUMENSO_ENCRYPTION_KEY and DOCUMENSO_ENCRYPTION_SECONDARY_KEY cannot be equal', + ); +} + +if (DOCUMENSO_ENCRYPTION_KEY === 'CAFEBABE') { + console.warn('*********************************************************************'); + console.warn('*'); + console.warn('*'); + console.warn('Please change the encryption key from the default value of "CAFEBABE"'); + console.warn('*'); + console.warn('*'); + console.warn('*********************************************************************'); +} diff --git a/packages/lib/server-only/crypto/decrypt.ts b/packages/lib/server-only/crypto/decrypt.ts new file mode 100644 index 000000000..7b4db9894 --- /dev/null +++ b/packages/lib/server-only/crypto/decrypt.ts @@ -0,0 +1,33 @@ +import { DOCUMENSO_ENCRYPTION_SECONDARY_KEY } from '@documenso/lib/constants/crypto'; +import { ZEncryptedDataSchema } from '@documenso/lib/server-only/crypto/encrypt'; +import { symmetricDecrypt } from '@documenso/lib/universal/crypto'; + +/** + * Decrypt the passed in data. This uses the secondary encrypt key for miscellaneous data. + * + * @param encryptedData The data encrypted with the `encryptSecondaryData` function. + * @returns The decrypted value, or `null` if the data is invalid or expired. + */ +export const decryptSecondaryData = (encryptedData: string): string | null => { + if (!DOCUMENSO_ENCRYPTION_SECONDARY_KEY) { + throw new Error('Missing encryption key'); + } + + const decryptedBufferValue = symmetricDecrypt({ + key: DOCUMENSO_ENCRYPTION_SECONDARY_KEY, + data: encryptedData, + }); + + const decryptedValue = Buffer.from(decryptedBufferValue).toString('utf-8'); + const result = ZEncryptedDataSchema.safeParse(JSON.parse(decryptedValue)); + + if (!result.success) { + return null; + } + + if (result.data.expiresAt !== undefined && result.data.expiresAt < Date.now()) { + return null; + } + + return result.data.data; +}; diff --git a/packages/lib/server-only/crypto/encrypt.ts b/packages/lib/server-only/crypto/encrypt.ts new file mode 100644 index 000000000..83de19cc2 --- /dev/null +++ b/packages/lib/server-only/crypto/encrypt.ts @@ -0,0 +1,42 @@ +import { z } from 'zod'; + +import { DOCUMENSO_ENCRYPTION_SECONDARY_KEY } from '@documenso/lib/constants/crypto'; +import { symmetricEncrypt } from '@documenso/lib/universal/crypto'; +import type { TEncryptSecondaryDataMutationSchema } from '@documenso/trpc/server/crypto/schema'; + +export const ZEncryptedDataSchema = z.object({ + data: z.string(), + expiresAt: z.number().optional(), +}); + +export type EncryptDataOptions = { + data: string; + + /** + * When the data should no longer be allowed to be decrypted. + * + * Leave this empty to never expire the data. + */ + expiresAt?: number; +}; + +/** + * Encrypt the passed in data. This uses the secondary encrypt key for miscellaneous data. + * + * @returns The encrypted data. + */ +export const encryptSecondaryData = ({ data, expiresAt }: TEncryptSecondaryDataMutationSchema) => { + if (!DOCUMENSO_ENCRYPTION_SECONDARY_KEY) { + throw new Error('Missing encryption key'); + } + + const dataToEncrypt: z.infer = { + data, + expiresAt, + }; + + return symmetricEncrypt({ + key: DOCUMENSO_ENCRYPTION_SECONDARY_KEY, + data: JSON.stringify(dataToEncrypt), + }); +}; diff --git a/packages/trpc/server/crypto/router.ts b/packages/trpc/server/crypto/router.ts new file mode 100644 index 000000000..db9616436 --- /dev/null +++ b/packages/trpc/server/crypto/router.ts @@ -0,0 +1,17 @@ +import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt'; + +import { procedure, router } from '../trpc'; +import { ZEncryptSecondaryDataMutationSchema } from './schema'; + +export const cryptoRouter = router({ + encryptSecondaryData: procedure + .input(ZEncryptSecondaryDataMutationSchema) + .mutation(({ input }) => { + try { + return encryptSecondaryData(input); + } catch { + // Never leak errors for crypto. + throw new Error('Failed to encrypt data'); + } + }), +}); diff --git a/packages/trpc/server/crypto/schema.ts b/packages/trpc/server/crypto/schema.ts new file mode 100644 index 000000000..ee4b49d53 --- /dev/null +++ b/packages/trpc/server/crypto/schema.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const ZEncryptSecondaryDataMutationSchema = z.object({ + data: z.string(), + expiresAt: z.number().optional(), +}); + +export const ZDecryptDataMutationSchema = z.object({ + data: z.string(), +}); + +export type TEncryptSecondaryDataMutationSchema = z.infer< + typeof ZEncryptSecondaryDataMutationSchema +>; +export type TDecryptDataMutationSchema = z.infer; diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts index 77d18e06d..3ed2a0d05 100644 --- a/packages/trpc/server/router.ts +++ b/packages/trpc/server/router.ts @@ -1,5 +1,6 @@ import { adminRouter } from './admin-router/router'; import { authRouter } from './auth-router/router'; +import { cryptoRouter } from './crypto/router'; import { documentRouter } from './document-router/router'; import { fieldRouter } from './field-router/router'; import { profileRouter } from './profile-router/router'; @@ -12,6 +13,7 @@ import { twoFactorAuthenticationRouter } from './two-factor-authentication-route export const appRouter = router({ auth: authRouter, + crypto: cryptoRouter, profile: profileRouter, document: documentRouter, field: fieldRouter, diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index badc05931..d7fc44ef7 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -8,6 +8,7 @@ declare namespace NodeJS { NEXT_PRIVATE_DATABASE_URL: string; NEXT_PRIVATE_ENCRYPTION_KEY: string; + NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string; diff --git a/turbo.json b/turbo.json index 3a96c2a07..b78d7c9d0 100644 --- a/turbo.json +++ b/turbo.json @@ -34,6 +34,7 @@ "globalEnv": [ "APP_VERSION", "NEXT_PRIVATE_ENCRYPTION_KEY", + "NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY", "NEXTAUTH_URL", "NEXTAUTH_SECRET", "NEXT_PUBLIC_PROJECT",