Skip to content

Commit

Permalink
Update event field encryption for middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
jpwilliams committed Dec 11, 2023
1 parent c05fc0e commit 91b7017
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 16 deletions.
26 changes: 25 additions & 1 deletion packages/middleware-encryption/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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";

Expand All @@ -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<string>` 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`.
Expand Down
79 changes: 64 additions & 15 deletions packages/middleware-encryption/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

/**
Expand All @@ -47,22 +53,65 @@ 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);
}

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<string, unknown>): 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<string, unknown>): 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: () => {
Expand All @@ -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 {};
}
Expand All @@ -101,22 +150,22 @@ export const encryptionMiddleware = (

return {
result: {
data: ctx.result.data && encrypt(ctx.result.data),
data: ctx.result.data && encryptValue(ctx.result.data),
},
};
},
};
},
};

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),
})),
};
},
Expand Down

0 comments on commit 91b7017

Please sign in to comment.