diff --git a/packages/middleware-encryption/README.md b/packages/middleware-encryption/README.md index 643c0376e..11f30e420 100644 --- a/packages/middleware-encryption/README.md +++ b/packages/middleware-encryption/README.md @@ -3,9 +3,9 @@ This package provides an encryption middleware for Inngest, enabling secure handling of sensitive data. It encrypts data being sent to and from Inngest, ensuring plaintext data never leaves your server. ## Features + - **Data Encryption:** Encrypts step and event data, with support for multiple encryption keys. - **Customizable Encryption Service:** Allows use of a custom encryption service or the default AES-based service. -- **Event Data Encryption Option:** Option to encrypt events sent to Inngest, though this may impact certain Inngest dashboard features. ## Installation @@ -20,6 +20,11 @@ npm install @inngest/middleware-encryption To use the encryption middleware, import and initialize it with your encryption key(s). You can optionally provide a custom encryption service. +By default, the following will be encrypted: + +- All step data +- Event data placed inside `data.encrypted` + ```ts import { encryptionMiddleware } from "@inngest/middleware-encryption"; @@ -35,6 +40,25 @@ const inngest = new Inngest({ }); ``` + + +## Customizing event encryption + +Only select pieces of event data are encrypted. By default, only the `data.encrypted` field. + +This can be customized using the `eventEncryptionField` setting + +- `string` - Encrypt fields matching this name +- `string[]` - Encrypt fields matching these names +- `(field: string) => boolean` - Provide a function to decide whether to encrypt a field +- `false` - Disable all event encryption + +## Rotating encryption keys + +Provide an `Array` when providing your `key` to support rotating encryption keys. + +The first key is always used to encrypt, but decryption will be attempted with all keys. + ## Implementing your own encryption To create a custom encryption service, you need to implement the abstract `EncryptionService` class provided by the package. Your custom service must implement two core methods: `encrypt` and `decrypt`. diff --git a/packages/middleware-encryption/src/middleware.ts b/packages/middleware-encryption/src/middleware.ts index de0aec446..1cff41e26 100644 --- a/packages/middleware-encryption/src/middleware.ts +++ b/packages/middleware-encryption/src/middleware.ts @@ -6,6 +6,13 @@ import { InngestMiddleware, type MiddlewareRegisterReturn } from "inngest"; * A marker used to identify encrypted values without having to guess. */ const ENCRYPTION_MARKER = "__ENCRYPTED__"; +export const DEFAULT_ENCRYPTION_FIELD = "encrypted"; + +export type EventEncryptionFieldInput = + | string + | string[] + | ((field: string) => boolean) + | false; /** * Options used to configure the encryption middleware. @@ -25,14 +32,13 @@ export interface EncryptionMiddlewareOptions { encryptionService?: EncryptionService; /** - * Whether to encrypt events sent to Inngest. Defaults to `false`. + * The top-level fields of the event that will be encrypted. Can be a single + * field name, an array of field names, a function that returns `true` if + * a field should be encrypted, or `false` to disable all event encryption. * - * Encrypting event data can impact the features available to you in terms - * of querying and filtering events in the Inngest dashboard, or using - * composability tooling such as `step.waitForEvent()`. Only enable this - * feature if you are absolutely sure that you need it. + * By default, the top-level field named `"encrypted"` will be encrypted (exported as `DEFAULT_ENCRYPTION_FIELD`). */ - encryptEvents?: boolean; + eventEncryptionField?: EventEncryptionFieldInput; } /** @@ -47,15 +53,18 @@ export const encryptionMiddleware = ( ) => { const service = opts.encryptionService || new DefaultEncryptionService(opts.key); + const shouldEncryptEvents = Boolean( + opts.eventEncryptionField ?? DEFAULT_ENCRYPTION_FIELD + ); - const encrypt = (value: unknown): EncryptedValue => { + const encryptValue = (value: unknown): EncryptedValue => { return { [ENCRYPTION_MARKER]: true, data: service.encrypt(value), }; }; - const decrypt = (value: unknown): unknown => { + const decryptValue = (value: unknown): unknown => { if (isEncryptedValue(value)) { return service.decrypt(value.data); } @@ -63,6 +72,46 @@ export const encryptionMiddleware = ( return value; }; + const fieldShouldBeEncrypted = (field: string): boolean => { + if (typeof opts.eventEncryptionField === "undefined") { + return field === DEFAULT_ENCRYPTION_FIELD; + } + + if (typeof opts.eventEncryptionField === "function") { + return opts.eventEncryptionField(field); + } + + if (Array.isArray(opts.eventEncryptionField)) { + return opts.eventEncryptionField.includes(field); + } + + return opts.eventEncryptionField === field; + }; + + const encryptEventData = (data: Record): unknown => { + const encryptedData = Object.keys(data).reduce((acc, key) => { + if (fieldShouldBeEncrypted(key)) { + return { ...acc, [key]: encryptValue(data[key]) }; + } + + return { ...acc, [key]: data[key] }; + }, {}); + + return encryptedData; + }; + + const decryptEventData = (data: Record): unknown => { + const decryptedData = Object.keys(data).reduce((acc, key) => { + if (isEncryptedValue(data[key])) { + return { ...acc, [key]: decryptValue(data[key]) }; + } + + return { ...acc, [key]: data[key] }; + }, {}); + + return decryptedData; + }; + return new InngestMiddleware({ name: "@inngest/middleware-encryption", init: () => { @@ -73,21 +122,21 @@ export const encryptionMiddleware = ( const inputTransformer: InputTransformer = { steps: steps.map((step) => ({ ...step, - data: step.data && decrypt(step.data), + data: step.data && decryptValue(step.data), })), }; - if (opts.encryptEvents) { + if (shouldEncryptEvents) { inputTransformer.ctx = { event: ctx.event && { ...ctx.event, - data: ctx.event.data && decrypt(ctx.event.data), + data: ctx.event.data && decryptEventData(ctx.event.data), }, events: ctx.events && ctx.events?.map((event) => ({ ...event, - data: event.data && decrypt(event.data), + data: event.data && decryptEventData(event.data), })), } as {}; } @@ -101,7 +150,7 @@ export const encryptionMiddleware = ( return { result: { - data: ctx.result.data && encrypt(ctx.result.data), + data: ctx.result.data && encryptValue(ctx.result.data), }, }; }, @@ -109,14 +158,14 @@ export const encryptionMiddleware = ( }, }; - if (opts.encryptEvents) { + if (shouldEncryptEvents) { registration.onSendEvent = () => { return { transformInput: ({ payloads }) => { return { payloads: payloads.map((payload) => ({ ...payload, - data: payload.data && encrypt(payload.data), + data: payload.data && encryptEventData(payload.data), })), }; },