Skip to content

Commit b90adea

Browse files
authored
feat(sdk): Explicit encryption keys (#3284)
## Summary Make it possible to provide explicit encryption keys to the StreamrClient. These keys will be used in a given stream for all publishers as the encryption key. Setting the explicit keys disables the group key exchange. Therefore, all participants in the stream need to agree on what key is being used for encryption and decryption to work properly. ## Changes - Added new configuration field `keys` to the `encryption` section - The `GroupKeyManager#fetchKey` returns explicitly defined keys if they are defined - `Publisher` initializes the `GroupKeyQueue` with the explicit keys if they are defined
1 parent 1c39b80 commit b90adea

17 files changed

+194
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Changes before Tatum release are not documented in this file.
1515
- Proxy connections now support bidirectionality and it is the default behavior (https://github.com/streamr-dev/network/pull/3260)
1616
- Add `StreamrClient#findProxyNodes()` function for discovering proxy nodes via Operator nodes (https://github.com/streamr-dev/network/pull/3257)
1717
- Add `StreamrClient#publishRaw()` for publishing raw messages (https://github.com/streamr-dev/network/pull/3280)
18+
- Add new `keys` configuration to the `encryption` section (https://github.com/streamr-dev/network/pull/3284)
1819

1920
#### Changed
2021

packages/sdk/src/Config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,11 @@ export interface StreamrClientConfig {
390390
* Note that subscribers will still accept unencrypted (public) data despite this setting.
391391
*/
392392
requireQuantumResistantEncryption?: boolean
393+
394+
/**
395+
* If this is defined only these encryption keys will be used. This will disable the Streamr key-exchange.
396+
*/
397+
keys?: Record<string, { id: string, data: HexString }>
393398
}
394399

395400
contracts?: {

packages/sdk/src/StreamrClientError.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export type StreamrClientErrorCode =
1919
'UNKNOWN_ERROR' |
2020
'ASSERTION_FAILED' |
2121
'SIGNATURE_POLICY_VIOLATION' |
22-
'ENCRYPTION_POLICY_VIOLATION'
22+
'ENCRYPTION_POLICY_VIOLATION' |
23+
'UNEXPECTED_INPUT'
2324

2425
export class StreamrClientError extends Error {
2526

packages/sdk/src/config.schema.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,15 @@
375375
"requireQuantumResistantEncryption": {
376376
"type": "boolean",
377377
"default": false
378+
},
379+
"keys": {
380+
"type": "object",
381+
"propertyNames": {
382+
"$ref": "#/definitions/streamIdOrPath"
383+
},
384+
"additionalProperties": {
385+
"$ref": "#/definitions/encryptionKey"
386+
}
378387
}
379388
},
380389
"default": {}
@@ -563,6 +572,25 @@
563572
"type": "number"
564573
}
565574
}
575+
},
576+
"streamIdOrPath": {
577+
"type": "string"
578+
},
579+
"encryptionKey": {
580+
"type": "object",
581+
"properties": {
582+
"id": {
583+
"type": "string"
584+
},
585+
"data": {
586+
"type": "string",
587+
"format": "hex-string"
588+
}
589+
},
590+
"required": [
591+
"id",
592+
"data"
593+
]
566594
}
567595

568596
}

packages/sdk/src/encryption/GroupKeyManager.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { StreamID, StreamPartID, UserID, waitForEvent } from '@streamr/utils'
1+
import { hexToBinary, StreamID, StreamPartID, StreamPartIDUtils, UserID, waitForEvent } from '@streamr/utils'
22
import crypto from 'crypto'
33
import { Lifecycle, inject, scoped } from 'tsyringe'
44
import { Identity, IdentityInjectionToken } from '../identity/Identity'
@@ -9,12 +9,35 @@ import { uuid } from '../utils/uuid'
99
import { GroupKey } from './GroupKey'
1010
import { LocalGroupKeyStore } from './LocalGroupKeyStore'
1111
import { SubscriberKeyExchange } from './SubscriberKeyExchange'
12+
import { StreamIDBuilder } from '../StreamIDBuilder'
13+
import { createLazyMap, Mapping } from '../utils/Mapping'
14+
import { StreamrClientError } from '../StreamrClientError'
15+
16+
/**
17+
* Gets an explicit encryption key from config for a given stream.
18+
* Returns undefined if no key is configured for the stream.
19+
*/
20+
export const getExplicitKey = async (
21+
streamId: StreamID,
22+
streamIdBuilder: StreamIDBuilder,
23+
config: StrictStreamrClientConfig['encryption']
24+
): Promise<GroupKey | undefined> => {
25+
if (config.keys !== undefined) {
26+
for (const entry of Object.entries(config.keys)) {
27+
if (await streamIdBuilder.toStreamID(entry[0]) === streamId) {
28+
return new GroupKey(entry[1].id, Buffer.from(hexToBinary(entry[1].data)))
29+
}
30+
}
31+
}
32+
return undefined
33+
}
1234

1335
@scoped(Lifecycle.ContainerScoped)
1436
export class GroupKeyManager {
1537

1638
private readonly subscriberKeyExchange: SubscriberKeyExchange
1739
private readonly localGroupKeyStore: LocalGroupKeyStore
40+
private readonly explicitKeys?: Mapping<StreamID, GroupKey | undefined>
1841
private readonly config: Pick<StrictStreamrClientConfig, 'encryption'>
1942
private readonly identity: Identity
2043
private readonly eventEmitter: StreamrClientEventEmitter
@@ -23,6 +46,7 @@ export class GroupKeyManager {
2346
constructor(
2447
subscriberKeyExchange: SubscriberKeyExchange,
2548
localGroupKeyStore: LocalGroupKeyStore,
49+
streamIdBuilder: StreamIDBuilder,
2650
@inject(ConfigInjectionToken) config: Pick<StrictStreamrClientConfig, 'encryption'>,
2751
@inject(IdentityInjectionToken) identity: Identity,
2852
eventEmitter: StreamrClientEventEmitter,
@@ -34,16 +58,35 @@ export class GroupKeyManager {
3458
this.identity = identity
3559
this.eventEmitter = eventEmitter
3660
this.destroySignal = destroySignal
61+
if (config.encryption.keys !== undefined) {
62+
this.explicitKeys = createLazyMap({
63+
valueFactory: async (streamId: StreamID) => {
64+
return getExplicitKey(streamId, streamIdBuilder, config.encryption)
65+
}
66+
})
67+
}
3768
}
3869

3970
async fetchKey(streamPartId: StreamPartID, groupKeyId: string, publisherId: UserID): Promise<GroupKey> {
40-
// 1st try: local storage
71+
// If explicit keys are defined only those keys are used.
72+
if (this.explicitKeys !== undefined) {
73+
const explicitKey = await this.explicitKeys.get(StreamPartIDUtils.getStreamID(streamPartId))
74+
if (explicitKey !== undefined) {
75+
return explicitKey
76+
}
77+
throw new StreamrClientError(
78+
`No encryption key available for stream part ID: groupKeyId=${groupKeyId}, streamPartId=${streamPartId}`,
79+
'UNEXPECTED_INPUT'
80+
)
81+
}
82+
83+
// 2nd try: local storage
4184
let groupKey = await this.localGroupKeyStore.get(groupKeyId, publisherId)
4285
if (groupKey !== undefined) {
4386
return groupKey
4487
}
4588

46-
// 2nd try: Streamr key-exchange
89+
// 3rd try: Streamr key-exchange
4790
await this.subscriberKeyExchange.requestGroupKey(groupKeyId, publisherId, streamPartId)
4891
const groupKeyIds = await waitForEvent(
4992
// TODO remove "as any" type casing in NET-889

packages/sdk/src/encryption/PublisherKeyExchange.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,22 @@ export class PublisherKeyExchange {
7272
this.identity = identity
7373
this.logger = loggerFactory.createLogger(module)
7474
this.config = config
75-
networkNodeFacade.once('start', async () => {
76-
networkNodeFacade.addMessageListener((msg: StreamMessage) => this.onMessage(msg))
77-
this.logger.debug('Started')
78-
})
79-
eventEmitter.on('messagePublished', (msg) => {
80-
if (msg.signatureType === SignatureType.ERC_1271) {
81-
const publisherId = msg.getPublisherId()
82-
if (!this.erc1271Publishers.has(publisherId)) {
83-
logger.debug('Add ERC-1271 publisher', { publisherId })
84-
this.erc1271Publishers.add(publisherId)
75+
// Setting explicit keys disables the key-exchange
76+
if (config.encryption.keys === undefined) {
77+
networkNodeFacade.once('start', async () => {
78+
networkNodeFacade.addMessageListener((msg: StreamMessage) => this.onMessage(msg))
79+
this.logger.debug('Started')
80+
})
81+
eventEmitter.on('messagePublished', (msg) => {
82+
if (msg.signatureType === SignatureType.ERC_1271) {
83+
const publisherId = msg.getPublisherId()
84+
if (!this.erc1271Publishers.has(publisherId)) {
85+
logger.debug('Add ERC-1271 publisher', { publisherId })
86+
this.erc1271Publishers.add(publisherId)
87+
}
8588
}
86-
}
87-
})
89+
})
90+
}
8891
}
8992

9093
private async onMessage(request: StreamMessage): Promise<void> {

packages/sdk/src/encryption/SubscriberKeyExchange.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { EncryptionUtil } from './EncryptionUtil'
2020
import { AsymmetricEncryptionType, ContentType, EncryptionType, GroupKeyRequest, GroupKeyResponse, SignatureType } from '@streamr/trackerless-network'
2121
import { KeyExchangeKeyPair } from './KeyExchangeKeyPair'
2222
import { createCompliantExchangeKeys } from '../utils/encryptionCompliance'
23+
import { StreamrClientError } from '../StreamrClientError'
2324

2425
const MAX_PENDING_REQUEST_COUNT = 50000 // just some limit, we can tweak the number if needed
2526

@@ -62,14 +63,24 @@ export class SubscriberKeyExchange {
6263
this.subscriber = subscriber
6364
this.identity = identity
6465
this.logger = loggerFactory.createLogger(module)
65-
this.ensureStarted = pOnce(async () => {
66-
this.keyPair = await createCompliantExchangeKeys(identity, config)
67-
networkNodeFacade.addMessageListener((msg: StreamMessage) => this.onMessage(msg))
68-
this.logger.debug('Started')
69-
})
70-
this.requestGroupKey = withThrottling((groupKeyId: string, publisherId: UserID, streamPartId: StreamPartID) => {
71-
return this.doRequestGroupKey(groupKeyId, publisherId, streamPartId)
72-
}, config.encryption.maxKeyRequestsPerSecond)
66+
// Setting explicit keys disables the key-exchange
67+
if (config.encryption.keys === undefined) {
68+
this.ensureStarted = pOnce(async () => {
69+
this.keyPair = await createCompliantExchangeKeys(identity, config)
70+
networkNodeFacade.addMessageListener((msg: StreamMessage) => this.onMessage(msg))
71+
this.logger.debug('Started')
72+
})
73+
this.requestGroupKey = withThrottling((groupKeyId: string, publisherId: UserID, streamPartId: StreamPartID) => {
74+
return this.doRequestGroupKey(groupKeyId, publisherId, streamPartId)
75+
}, config.encryption.maxKeyRequestsPerSecond)
76+
} else {
77+
this.ensureStarted = async () => {
78+
throw new StreamrClientError('Assertion failed', 'ASSERTION_FAILED')
79+
}
80+
this.requestGroupKey = async () => {
81+
throw new StreamrClientError('Assertion failed', 'ASSERTION_FAILED')
82+
}
83+
}
7384
}
7485

7586
private async doRequestGroupKey(groupKeyId: string, publisherId: UserID, streamPartId: StreamPartID): Promise<void> {

packages/sdk/src/generated/validateConfig.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sdk/src/publish/GroupKeyQueue.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ export class GroupKeyQueue {
1919
static async createInstance(
2020
streamId: StreamID,
2121
identity: Identity,
22-
groupKeyManager: GroupKeyManager
22+
groupKeyManager: GroupKeyManager,
23+
currentGroupKey?: GroupKey
2324
): Promise<GroupKeyQueue> {
2425
const instance = new GroupKeyQueue(streamId, identity, groupKeyManager)
25-
instance.currentGroupKey = await instance.groupKeyManager.fetchLatestEncryptionKey(
26+
instance.currentGroupKey = currentGroupKey ?? await instance.groupKeyManager.fetchLatestEncryptionKey(
2627
await identity.getUserId(),
2728
streamId,
28-
)
29+
) ?? undefined
2930
return instance
3031
}
3132

packages/sdk/src/publish/Publisher.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { NetworkNodeFacade } from '../NetworkNodeFacade'
77
import { StreamIDBuilder } from '../StreamIDBuilder'
88
import { StreamrClientError } from '../StreamrClientError'
99
import { StreamRegistry } from '../contracts/StreamRegistry'
10-
import { GroupKeyManager } from '../encryption/GroupKeyManager'
10+
import { getExplicitKey, GroupKeyManager } from '../encryption/GroupKeyManager'
1111
import { StreamMessage } from '../protocol/StreamMessage'
1212
import { MessageSigner } from '../signature/MessageSigner'
1313
import { SignatureValidator } from '../signature/SignatureValidator'
@@ -80,7 +80,8 @@ export class Publisher {
8080
})
8181
this.groupKeyQueues = createLazyMap({
8282
valueFactory: async (streamId) => {
83-
return GroupKeyQueue.createInstance(streamId, this.identity, groupKeyManager)
83+
const explicitKey = await getExplicitKey(streamId, this.streamIdBuilder, this.config.encryption)
84+
return await GroupKeyQueue.createInstance(streamId, this.identity, groupKeyManager, explicitKey)
8485
}
8586
})
8687
}

0 commit comments

Comments
 (0)