Skip to content

Commit a2d6f7d

Browse files
author
Eric Andrews
authored
feat: lit protocol key exchange (#1052)
## Summary Add support for retrieving and storing encryption keys via lit-protocol. When enabled we will first try to retrieve / store key using lit protocol, if this doesn't work, we use our own key-exchange system. ## Changes - All lit-protocol library interactions are encapsulated into class `LitProtocolFacade`. - Add new class `GroupKeyManager` that is used to retrieve and store encryption keys. It contains the logic for iterating over different key exchange strategies until a key is found (or successfully generated and stored). - Retrieve key: (1) local, (2) lit-protcol, and (3) Streamr key-exchange - Store key: (1) generate with lit-protocol, and if that fails, (2) generate locally. We _always_ store encryption key into local store. ## Limitations and future improvements - lit-protocol is hard coded to work with main net Polygon and pre-defined streamRegistryChainAddress - no integration tests for lit-protocol - implement in future when it becomes possible to run own nodes and override streamRegistryChainAddress and RPC settings - Since we can't write integration tests for lit-protocol, do we need some automated tests to run against production? - Combine `encryption` and `decryption` client config blocks ## Checklist before requesting a review - [x] Is this a breaking change? If it is, be clear in summary. - [ ] Read through code myself one more time. - [ ] Make sure any and all `TODO` comments left behind are meant to be left in. - [x] Has reasonable passing test coverage? - [x] Updated changelog if applicable. - [x] Updated documentation if applicable.
1 parent d65a6f2 commit a2d6f7d

29 files changed

Lines changed: 5994 additions & 305 deletions

package-lock.json

Lines changed: 5343 additions & 156 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"husky": "^6.0.0",
4343
"jest": "^29.4.3",
4444
"jest-extended": "^3.2.3",
45+
"jest-mock-extended": "^3.0.1",
4546
"lerna": "^4.0.0",
4647
"node-gyp-build": "^4.3.0",
4748
"semver": "^7.3.5",

packages/client/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010

11+
- Add support for experimental encryption key exchange via [Lit Protocol](https://litprotocol.com/). Enabled by
12+
setting configuration option `encryption.litProtocolEnabled` to be true.
13+
1114
### Changed
1215

1316
- All contract providers are used to query the tracker registry, storage node registry and stream storage registry

packages/client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"@ethersproject/transactions": "^5.5.0",
9898
"@ethersproject/wallet": "^5.5.0",
9999
"@ethersproject/web": "^5.5.0",
100+
"@lit-protocol/lit-node-client": "2.1.38",
100101
"@streamr/network-node": "7.3.0",
101102
"@streamr/protocol": "7.3.0",
102103
"@streamr/utils": "7.3.0",
@@ -107,6 +108,7 @@
107108
"eventemitter3": "^4.0.7",
108109
"heap": "^0.2.6",
109110
"idb-keyval": "^5.1.5",
111+
"lit-siwe": "^1.1.8",
110112
"lodash": "^4.17.21",
111113
"mem": "^8.1.1",
112114
"node-fetch": "^2.6.6",

packages/client/src/Config.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ export interface StreamrClientConfig {
6767
retryResendAfter?: number
6868
gapFillTimeout?: number
6969

70+
encryption?: {
71+
/**
72+
* Enable experimental Lit Protocol key exchange.
73+
*
74+
* When enabled encryption key storing and fetching will be primarily done through the Lit Protocol and
75+
* secondarily through the standard Streamr key-exchange system.
76+
*/
77+
litProtocolEnabled?: boolean
78+
79+
/**
80+
* Enable log messages of the Lit Protocol library to be printed to stdout.
81+
*/
82+
litProtocolLogging?: boolean
83+
}
84+
7085
network?: {
7186
id?: string
7287
acceptProxyConnections?: boolean
@@ -140,6 +155,7 @@ export interface StreamrClientConfig {
140155
export type StrictStreamrClientConfig = MarkOptional<Required<StreamrClientConfig>, 'auth' | 'metrics'> & {
141156
network: MarkOptional<Exclude<Required<StreamrClientConfig['network']>, undefined>, 'location'>
142157
contracts: Exclude<Required<StreamrClientConfig['contracts']>, undefined>
158+
encryption: Exclude<Required<StreamrClientConfig['encryption']>, undefined>
143159
decryption: Exclude<Required<StreamrClientConfig['decryption']>, undefined>
144160
cache: Exclude<Required<StreamrClientConfig['cache']>, undefined>
145161
_timeouts: Exclude<DeepRequired<StreamrClientConfig['_timeouts']>, undefined>
@@ -158,6 +174,11 @@ export const STREAM_CLIENT_DEFAULTS:
158174
maxGapRequests: 5,
159175
retryResendAfter: 5000,
160176
gapFillTimeout: 5000,
177+
178+
encryption: {
179+
litProtocolEnabled: false,
180+
litProtocolLogging: false
181+
},
161182

162183
network: {
163184
acceptProxyConnections: false,

packages/client/src/StreamrClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { LoggerFactory } from './utils/LoggerFactory'
3535
import { convertStreamMessageToMessage, Message } from './Message'
3636
import { ErrorCode } from './HttpUtil'
3737
import { omit } from 'lodash'
38+
import { StreamrClientError } from './StreamrClientError'
3839

3940
/**
4041
* The main API used to interact with Streamr.
@@ -123,6 +124,9 @@ export class StreamrClient {
123124
if (opts.streamId === undefined) {
124125
throw new Error('streamId required')
125126
}
127+
if (opts.key !== undefined && this.config.encryption.litProtocolEnabled) {
128+
throw new StreamrClientError('cannot pass "key" when Lit Protocol is enabled', 'UNSUPPORTED_OPERATION')
129+
}
126130
const streamId = await this.streamIdBuilder.toStreamID(opts.streamId)
127131
const queue = await this.publisher.getGroupKeyQueue(streamId)
128132
if (opts.distributionMethod === 'rotate') {

packages/client/src/StreamrClientError.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type StreamrClientErrorCode =
44
'INVALID_ARGUMENT' |
55
'CLIENT_DESTROYED' |
66
'PIPELINE_ERROR' |
7+
'UNSUPPORTED_OPERATION' |
78
'UNKNOWN_ERROR'
89

910
export class StreamrClientError extends Error {

packages/client/src/config.schema.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,21 @@
306306
},
307307
"default": {}
308308
},
309+
"encryption": {
310+
"type": "object",
311+
"additionalProperties": false,
312+
"properties": {
313+
"litProtocolEnabled": {
314+
"type": "boolean",
315+
"default": false
316+
},
317+
"litProtocolLogging": {
318+
"type": "boolean",
319+
"default": false
320+
}
321+
},
322+
"default": {}
323+
},
309324
"decryption": {
310325
"type": "object",
311326
"additionalProperties": false,

packages/client/src/encryption/GroupKey.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ export class GroupKey {
2323
/** @internal */
2424
readonly id: string
2525
/** @internal */
26-
readonly data: Uint8Array
26+
readonly data: Buffer
2727

28-
constructor(groupKeyId: string, data: Uint8Array) {
28+
constructor(groupKeyId: string, data: Buffer) {
2929
this.id = groupKeyId
3030
if (!groupKeyId) {
3131
throw new GroupKeyError(`groupKeyId must not be falsey ${groupKeyId}`)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { LitProtocolFacade } from './LitProtocolFacade'
2+
import { inject, Lifecycle, scoped } from 'tsyringe'
3+
import { GroupKeyStore } from './GroupKeyStore'
4+
import { GroupKey } from './GroupKey'
5+
import { StreamID, StreamPartID, StreamPartIDUtils } from '@streamr/protocol'
6+
import { EthereumAddress, waitForEvent } from '@streamr/utils'
7+
import { SubscriberKeyExchange } from './SubscriberKeyExchange'
8+
import { ConfigInjectionToken, StrictStreamrClientConfig } from '../Config'
9+
import { StreamrClientEventEmitter } from '../events'
10+
import { DestroySignal } from '../DestroySignal'
11+
import crypto from 'crypto'
12+
import { uuid } from '../utils/uuid'
13+
14+
@scoped(Lifecycle.ContainerScoped)
15+
export class GroupKeyManager {
16+
private readonly groupKeyStore: GroupKeyStore
17+
private readonly litProtocolFacade: LitProtocolFacade
18+
private readonly subscriberKeyExchange: SubscriberKeyExchange
19+
private readonly eventEmitter: StreamrClientEventEmitter
20+
private readonly destroySignal: DestroySignal
21+
private readonly config: Pick<StrictStreamrClientConfig, 'decryption' | 'encryption'>
22+
23+
constructor(
24+
groupKeyStore: GroupKeyStore,
25+
litProtocolFacade: LitProtocolFacade,
26+
subscriberKeyExchange: SubscriberKeyExchange,
27+
eventEmitter: StreamrClientEventEmitter,
28+
destroySignal: DestroySignal,
29+
@inject(ConfigInjectionToken) config: Pick<StrictStreamrClientConfig, 'decryption' | 'encryption'>
30+
) {
31+
this.groupKeyStore = groupKeyStore
32+
this.litProtocolFacade = litProtocolFacade
33+
this.subscriberKeyExchange = subscriberKeyExchange
34+
this.eventEmitter = eventEmitter
35+
this.destroySignal = destroySignal
36+
this.config = config
37+
}
38+
39+
async fetchKey(streamPartId: StreamPartID, groupKeyId: string, publisherId: EthereumAddress): Promise<GroupKey> {
40+
const streamId = StreamPartIDUtils.getStreamID(streamPartId)
41+
42+
// 1st try: local storage
43+
let groupKey = await this.groupKeyStore.get(groupKeyId, streamId)
44+
if (groupKey !== undefined) {
45+
return groupKey
46+
}
47+
48+
// 2nd try: lit-protocol
49+
if (this.config.encryption.litProtocolEnabled) {
50+
groupKey = await this.litProtocolFacade.get(streamId, groupKeyId)
51+
if (groupKey !== undefined) {
52+
await this.groupKeyStore.add(groupKey, streamId)
53+
return groupKey
54+
}
55+
}
56+
57+
// 3rd try: Streamr key-exchange
58+
await this.subscriberKeyExchange.requestGroupKey(groupKeyId, publisherId, streamPartId)
59+
const groupKeys = await waitForEvent(
60+
// TODO remove "as any" type casing in NET-889
61+
this.eventEmitter as any,
62+
'addGroupKey',
63+
this.config.decryption.keyRequestTimeout,
64+
(storedGroupKey: GroupKey) => storedGroupKey.id === groupKeyId,
65+
this.destroySignal.abortSignal
66+
)
67+
return groupKeys[0] as GroupKey
68+
}
69+
70+
async storeKey(groupKey: GroupKey | undefined, streamId: StreamID): Promise<GroupKey> { // TODO: name
71+
if (groupKey === undefined) {
72+
const keyData = crypto.randomBytes(32)
73+
// 1st try lit-protocol, if a key cannot be generated and stored, then generate group key locally
74+
if (this.config.encryption.litProtocolEnabled) {
75+
groupKey = await this.litProtocolFacade.store(streamId, keyData)
76+
}
77+
if (groupKey === undefined) {
78+
groupKey = new GroupKey(uuid('GroupKey'), keyData)
79+
}
80+
}
81+
await this.groupKeyStore.add(groupKey, streamId)
82+
return groupKey
83+
}
84+
85+
addKeyToLocalStore(groupKey: GroupKey, streamId: StreamID): Promise<void> {
86+
return this.groupKeyStore.add(groupKey, streamId)
87+
}
88+
}

0 commit comments

Comments
 (0)