Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Describe encryption #1810

Merged
merged 19 commits into from
Apr 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ title: reSolve Documentation
- [Adapters](advanced-techniques.md#adapters)
- [Custom Read Models](advanced-techniques.md#custom-read-models)
- [Modules](advanced-techniques.md#modules)
- [Encryption](advanced-techniques.md#encryption)

- [Authentication and Authorization](authentication-and-authorization.md)

Expand Down
118 changes: 116 additions & 2 deletions docs/advanced-techniques.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ When an application runs locally, the `@resolve-js/scripts` utility loads all ne

## Adapters

ReSolve uses the **adapter** mechanism to provide an abstraction layer above APIs used by its subsystems. For instance, adapters are used to define how a reSolve application stores its data. They abstract away all direct interactions with the underlying storage, allowing reSolve to provide a unified data management API.
ReSolve uses the **adapter** mechanism to provide an abstraction layer above APIs used by its subsystems. For instance, adapters are used to define how a reSolve application stores its data. This allows reSolve to abstract away all direct interactions with the underlying storage and expose a unified data management API.

ReSolve uses different types of adapters depending on which kind of data needs to be stored.

- **Event store adapters**
- **Read model store adapters**

Resolve comes with a set of adapters covering popular DBMS choices. You can also implement new adapters to store data in any required way.
Resolve includes a set of adapters compatible with popular Database Management Systems (DBMS). You can also implement new adapters to store data in any required way.

Note that reSolve does not force you to use adapters. For example, you may need to implement a Read Model on top of some arbitrary system, such as a full-text-search engine, OLAP or a particular SQL database. In such case, you can just work with that system in the code of the projection function and query resolver, without writing a new Read Model adapter.

Expand Down Expand Up @@ -227,6 +227,120 @@ A merged module's code is included in the resulting application's bundles.

EugeniyBurmistrov marked this conversation as resolved.
Show resolved Hide resolved
For an example on how to use modules, see the [Hacker News](https://github.com/reimagined/resolve/tree/master/examples/hacker-news) sample application. This application uses the authentication and comments modules from reSolve.

## Encryption

The reSolve framework includes a mechanism that allows you to use an arbitrary encryption algorithm to encrypt the stored events and Read Model state data. You can use this functionality to store user data in compliance with General Data Protection Regulation (GDPR).

Encryption is defined in a file that exports a factory function of the following format:

##### Aggregate Encryption:

```js
// common/aggregates/encryption.js
const createEncryption = (aggregateId, context) => {
...
// Returns an object that contains 'encrypt' and 'decrypt' functions
return {
encrypt: (data) => ..., // A function that takes data and returns its encrypted version
decrypt: (blob) => ..., // A function that takes an encrypted blob and returns unencrypted data
}
}
export default createEncryption
```

##### Read Model Encryption:

```js
// common/read-models/encryption.js
const createEncryption = (event, context) => {
...
// Returns an object that contains 'encrypt' and 'decrypt' functions
return {
encrypt: (data) => ..., // A function that takes data and returns its encrypted version
decrypt: (blob) => ..., // A function that takes an encrypted blob and returns unencrypted data
}
}
export default createEncryption
```

You can assign encryption to aggregates and read models in the application's configuration file as shown below:

```js
const appConfig = {
aggregates: [
{
name: 'user-profile',
commands: 'common/aggregates/user-profile.commands.js',
projection: 'common/aggregates/user-profile.projection.js',
encryption: 'common/aggregates/encryption.js', // The path to a file that defines aggregate encryption
},
...
]
readModels: [
{
name: 'user-profiles',
connectorName: 'default',
projection: 'common/read-models/user-profiles.projection.js',
resolvers: 'common/read-models/user-profiles.resolvers.js',
encryption: 'common/read-models/encryption.js', // The path to a file that defines Read Model encryption
},
...
],
...
}
```

### Storing Secrets

The reSolve framework implements a **secrets manager** that you can use to get, set or delete secrets based on their unique IDs. In an encryption factory function, you can access the secrets manager through the reSolve context object:

```js
// common/aggregates/encryption.js
import { generate } from 'generate-password'

const createEncryption = (aggregateId, context) => {
const { secretsManager } = context
let aggregateKey = await secretsManager.getSecret(aggregateId)
if (!aggregateKey) {
aggregateKey = generate({
length: 20,
numbers: true,
})
await secretsManager.setSecret(aggregateId, aggregateKey)
}
...
}
```

The `secretsManager` object contains the following functions:

| Function Name | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `getSecret` | Takes a unique ID as an argument and returns a promise that resolves to a string if a secret was found or null if a secret was not found. |
| `setSecret` | Takes a unique ID and a secret string as arguments and returns a promise that resolves if the secret was successfully saved. |
| `deleteSecret` | Takes a unique ID as an argument and returns a promise that resolves if the secret was successfully deleted. |

> **NOTE:** The unique ID of an existing or deleted secret cannot be reused. If you pass a previously used ID to the `setSecret` function, an exception is raised.

The secrets manager stores secrets in the 'secrets' table within the event store. To change the table name, use the event store adapter's `secretsTableName` option:

```js
// config.prod.js
const prodConfig = {
eventstoreAdapter: {
module: '@resolve-js/eventstore-lite',
options: {
databaseFile: 'data/event-store.db',
secretsTableName: 'usersecrets',
},
},
}
```

#### Example

The **personal-data** example demonstrates how to store encrypted user data. In this example, the encryption logic is implemented in a separate `common/encryption-factory.js` file and reused on both the read and write sides.

## Upload Files

The **@resolve-js/module-uploader** module implements the file upload functionality. You can enable this module as shown below:
Expand Down