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",