Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
9d22437
wip
harbu Dec 21, 2022
38098ae
clean up
harbu Dec 21, 2022
f53395a
encryptedgroupkey change
harbu Dec 21, 2022
dc07a35
litprotocol get key
harbu Dec 22, 2022
cfa0fca
fixes
harbu Dec 27, 2022
acc151c
fix
harbu Dec 27, 2022
46d975e
subscribe permission
harbu Dec 27, 2022
4ab7321
fixes
harbu Dec 27, 2022
57b8c82
config
harbu Dec 27, 2022
b4a92d0
clean up
harbu Dec 28, 2022
11c7fc6
refactor
harbu Dec 29, 2022
b815e49
fix issue
harbu Dec 29, 2022
07c8f85
fix
harbu Dec 29, 2022
510d719
fix encoding
harbu Dec 29, 2022
3c52c96
refactor
harbu Dec 30, 2022
f71f03b
try-catch LitProtocolKeyStore#get
harbu Dec 30, 2022
53baa7a
return EncryptedGroupKey
harbu Dec 30, 2022
24c8daa
add lit-protocl key to groupkeystore
harbu Dec 30, 2022
3633833
refactor: rm unused import
harbu Jan 2, 2023
73d5805
return undefined on error
harbu Jan 2, 2023
5efa7ba
add comments, fix indentatin
harbu Jan 2, 2023
89a5e97
make tests build
harbu Jan 10, 2023
71e3795
re-order package.json
harbu Jan 10, 2023
c10eef0
feat: configuration option to enable/disable
harbu Jan 11, 2023
534510a
refactor: dep siwe to lit-siwe
harbu Jan 16, 2023
1d16442
Merge branch 'main' into lit-protocol
harbu Jan 16, 2023
7ca5e89
deps: update package-lock.json
harbu Jan 16, 2023
384143c
fix: quick fix test?
harbu Jan 16, 2023
4467709
refactor: addressInChecksumCase
harbu Jan 16, 2023
885396a
docs: add class comment to LitProtocolKeyStore
harbu Jan 16, 2023
8cc2904
refactor: style question
harbu Jan 16, 2023
3c6818a
refactor: generateNewKey
harbu Jan 17, 2023
d57d817
refactor: GroupKeyManager
harbu Jan 17, 2023
71b2b52
refactor: slight refactor to style
harbu Jan 17, 2023
d9b364e
refactor: GroupKeyManager add storeKey method
harbu Jan 17, 2023
ce441d3
fix: fix tests
harbu Jan 17, 2023
7e03380
refactor: LitProtocolKeyStore param naming
harbu Jan 17, 2023
ff49651
eslint
harbu Jan 17, 2023
98d6187
refactor: method parameter order
harbu Jan 17, 2023
d73781f
tests: fix
harbu Jan 17, 2023
466f0a6
fix tests
harbu Jan 17, 2023
efcd770
eslint
harbu Jan 17, 2023
8497cc7
LitProtocolFacade refactorings
harbu Jan 17, 2023
d65c119
feat: logging (also refactoring)
harbu Jan 18, 2023
b04c071
fixes
harbu Jan 18, 2023
7094926
fix eslint
harbu Jan 18, 2023
33922ba
fix tests
harbu Jan 18, 2023
de2ecff
fix tests
harbu Jan 18, 2023
fbfc7eb
release(client, cli-tools): v7.3.0-beta.0
harbu Jan 18, 2023
e8aca22
feat(utils): withRateLimit
harbu Jan 18, 2023
cc5edea
work
harbu Jan 18, 2023
4056d45
refactor: address PR comments
harbu Jan 19, 2023
d15c6de
test: GroupKeyManager
harbu Jan 23, 2023
1965f16
feat: add guard on StreamrClient#updateEncryptionKey preventing givin…
harbu Jan 23, 2023
6512cef
Merge remote-tracking branch 'origin/main' into lit-protocol
harbu Jan 23, 2023
fabe6d2
deps: update lit-protocol
harbu Jan 27, 2023
e17972b
release(client, cli-tools): v7.4.0-beta.0
harbu Jan 27, 2023
b0e91b9
docs: typedoc and changelog
harbu Jan 27, 2023
c151f78
Merge branch 'main' into lit-protocol
harbu Jan 27, 2023
b526ed7
make eslint happy
harbu Jan 27, 2023
db63013
fix: missing @inject
harbu Jan 27, 2023
9ae9b9d
fix: inject fix
harbu Jan 27, 2023
700f8da
deps: bump lit-client to 2.1.16
harbu Jan 30, 2023
2e83372
release(client, cli-tools): v7.4.0-beta.1
harbu Jan 30, 2023
958e383
Revert "release(client, cli-tools): v7.4.0-beta.1"
teogeb Feb 16, 2023
e8e2502
deps: bump lit-client to 2.1.38
teogeb Feb 16, 2023
2c1166e
Merge branch 'main' into lit-protocol
teogeb Feb 16, 2023
b6cd163
release: revert client version to match main
teogeb Feb 17, 2023
d2e1dd8
Merge branch 'main' into lit-protocol
teogeb Feb 20, 2023
44f94c0
Merge branch 'main' into lit-protocol
teogeb Feb 20, 2023
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
5,499 changes: 5,343 additions & 156 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"husky": "^6.0.0",
"jest": "^29.4.3",
"jest-extended": "^3.2.3",
"jest-mock-extended": "^3.0.1",
"lerna": "^4.0.0",
"node-gyp-build": "^4.3.0",
"semver": "^7.3.5",
Expand Down
3 changes: 3 additions & 0 deletions packages/client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add support for experimental encryption key exchange via [Lit Protocol](https://litprotocol.com/). Enabled by
setting configuration option `encryption.litProtocolEnabled` to be true.

### Changed

- All contract providers are used to query the tracker registry, storage node registry and stream storage registry
Expand Down
2 changes: 2 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"@ethersproject/transactions": "^5.5.0",
"@ethersproject/wallet": "^5.5.0",
"@ethersproject/web": "^5.5.0",
"@lit-protocol/lit-node-client": "2.1.38",
"@streamr/network-node": "7.3.0",
"@streamr/protocol": "7.3.0",
"@streamr/utils": "7.3.0",
Expand All @@ -107,6 +108,7 @@
"eventemitter3": "^4.0.7",
"heap": "^0.2.6",
"idb-keyval": "^5.1.5",
"lit-siwe": "^1.1.8",
"lodash": "^4.17.21",
"mem": "^8.1.1",
"node-fetch": "^2.6.6",
Expand Down
21 changes: 21 additions & 0 deletions packages/client/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ export interface StreamrClientConfig {
retryResendAfter?: number
gapFillTimeout?: number

encryption?: {
/**
* Enable experimental Lit Protocol key exchange.
*
* When enabled encryption key storing and fetching will be primarily done through the Lit Protocol and
* secondarily through the standard Streamr key-exchange system.
*/
litProtocolEnabled?: boolean

/**
* Enable log messages of the Lit Protocol library to be printed to stdout.
*/
litProtocolLogging?: boolean
}

network?: {
id?: string
acceptProxyConnections?: boolean
Expand Down Expand Up @@ -140,6 +155,7 @@ export interface StreamrClientConfig {
export type StrictStreamrClientConfig = MarkOptional<Required<StreamrClientConfig>, 'auth' | 'metrics'> & {
network: MarkOptional<Exclude<Required<StreamrClientConfig['network']>, undefined>, 'location'>
contracts: Exclude<Required<StreamrClientConfig['contracts']>, undefined>
encryption: Exclude<Required<StreamrClientConfig['encryption']>, undefined>
decryption: Exclude<Required<StreamrClientConfig['decryption']>, undefined>
cache: Exclude<Required<StreamrClientConfig['cache']>, undefined>
_timeouts: Exclude<DeepRequired<StreamrClientConfig['_timeouts']>, undefined>
Expand All @@ -158,6 +174,11 @@ export const STREAM_CLIENT_DEFAULTS:
maxGapRequests: 5,
retryResendAfter: 5000,
gapFillTimeout: 5000,

encryption: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would make sense to combine encryption and decryption blocks at some point. In a separate PR? If we'd keep these as separate blocks, it could kind of indicate that LIT is used only for encryption and not for decryption.

litProtocolEnabled: false,
litProtocolLogging: false
},

network: {
acceptProxyConnections: false,
Expand Down
4 changes: 4 additions & 0 deletions packages/client/src/StreamrClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { LoggerFactory } from './utils/LoggerFactory'
import { convertStreamMessageToMessage, Message } from './Message'
import { ErrorCode } from './HttpUtil'
import { omit } from 'lodash'
import { StreamrClientError } from './StreamrClientError'

/**
* The main API used to interact with Streamr.
Expand Down Expand Up @@ -123,6 +124,9 @@ export class StreamrClient {
if (opts.streamId === undefined) {
throw new Error('streamId required')
}
if (opts.key !== undefined && this.config.encryption.litProtocolEnabled) {
throw new StreamrClientError('cannot pass "key" when Lit Protocol is enabled', 'UNSUPPORTED_OPERATION')
}
const streamId = await this.streamIdBuilder.toStreamID(opts.streamId)
const queue = await this.publisher.getGroupKeyQueue(streamId)
if (opts.distributionMethod === 'rotate') {
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/StreamrClientError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type StreamrClientErrorCode =
'INVALID_ARGUMENT' |
'CLIENT_DESTROYED' |
'PIPELINE_ERROR' |
'UNSUPPORTED_OPERATION' |
'UNKNOWN_ERROR'

export class StreamrClientError extends Error {
Expand Down
15 changes: 15 additions & 0 deletions packages/client/src/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,21 @@
},
"default": {}
},
"encryption": {
"type": "object",
"additionalProperties": false,
"properties": {
"litProtocolEnabled": {
"type": "boolean",
"default": false
},
"litProtocolLogging": {
"type": "boolean",
"default": false
}
},
"default": {}
},
"decryption": {
"type": "object",
"additionalProperties": false,
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/encryption/GroupKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export class GroupKey {
/** @internal */
readonly id: string
/** @internal */
readonly data: Uint8Array
readonly data: Buffer

constructor(groupKeyId: string, data: Uint8Array) {
constructor(groupKeyId: string, data: Buffer) {
this.id = groupKeyId
if (!groupKeyId) {
throw new GroupKeyError(`groupKeyId must not be falsey ${groupKeyId}`)
Expand Down
88 changes: 88 additions & 0 deletions packages/client/src/encryption/GroupKeyManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { LitProtocolFacade } from './LitProtocolFacade'
import { inject, Lifecycle, scoped } from 'tsyringe'
import { GroupKeyStore } from './GroupKeyStore'
import { GroupKey } from './GroupKey'
import { StreamID, StreamPartID, StreamPartIDUtils } from '@streamr/protocol'
import { EthereumAddress, waitForEvent } from '@streamr/utils'
import { SubscriberKeyExchange } from './SubscriberKeyExchange'
import { ConfigInjectionToken, StrictStreamrClientConfig } from '../Config'
import { StreamrClientEventEmitter } from '../events'
import { DestroySignal } from '../DestroySignal'
import crypto from 'crypto'
import { uuid } from '../utils/uuid'

@scoped(Lifecycle.ContainerScoped)
export class GroupKeyManager {
private readonly groupKeyStore: GroupKeyStore
private readonly litProtocolFacade: LitProtocolFacade
private readonly subscriberKeyExchange: SubscriberKeyExchange
private readonly eventEmitter: StreamrClientEventEmitter
private readonly destroySignal: DestroySignal
private readonly config: Pick<StrictStreamrClientConfig, 'decryption' | 'encryption'>

constructor(
groupKeyStore: GroupKeyStore,
litProtocolFacade: LitProtocolFacade,
subscriberKeyExchange: SubscriberKeyExchange,
eventEmitter: StreamrClientEventEmitter,
destroySignal: DestroySignal,
@inject(ConfigInjectionToken) config: Pick<StrictStreamrClientConfig, 'decryption' | 'encryption'>
) {
this.groupKeyStore = groupKeyStore
this.litProtocolFacade = litProtocolFacade
this.subscriberKeyExchange = subscriberKeyExchange
this.eventEmitter = eventEmitter
this.destroySignal = destroySignal
this.config = config
}

async fetchKey(streamPartId: StreamPartID, groupKeyId: string, publisherId: EthereumAddress): Promise<GroupKey> {
const streamId = StreamPartIDUtils.getStreamID(streamPartId)

// 1st try: local storage
let groupKey = await this.groupKeyStore.get(groupKeyId, streamId)
if (groupKey !== undefined) {
return groupKey
}

// 2nd try: lit-protocol
if (this.config.encryption.litProtocolEnabled) {
groupKey = await this.litProtocolFacade.get(streamId, groupKeyId)
if (groupKey !== undefined) {
await this.groupKeyStore.add(groupKey, streamId)
return groupKey
}
}

// 3rd try: Streamr key-exchange
await this.subscriberKeyExchange.requestGroupKey(groupKeyId, publisherId, streamPartId)
const groupKeys = await waitForEvent(
// TODO remove "as any" type casing in NET-889
this.eventEmitter as any,
'addGroupKey',
this.config.decryption.keyRequestTimeout,
(storedGroupKey: GroupKey) => storedGroupKey.id === groupKeyId,
this.destroySignal.abortSignal
)
return groupKeys[0] as GroupKey
}

async storeKey(groupKey: GroupKey | undefined, streamId: StreamID): Promise<GroupKey> { // TODO: name
if (groupKey === undefined) {
const keyData = crypto.randomBytes(32)
// 1st try lit-protocol, if a key cannot be generated and stored, then generate group key locally
if (this.config.encryption.litProtocolEnabled) {
groupKey = await this.litProtocolFacade.store(streamId, keyData)
}
if (groupKey === undefined) {
groupKey = new GroupKey(uuid('GroupKey'), keyData)
}
}
await this.groupKeyStore.add(groupKey, streamId)
return groupKey
}

addKeyToLocalStore(groupKey: GroupKey, streamId: StreamID): Promise<void> {
return this.groupKeyStore.add(groupKey, streamId)
}
}
3 changes: 3 additions & 0 deletions packages/client/src/encryption/GroupKeyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export interface UpdateEncryptionKeyOptions {
key?: GroupKey
}

/**
* TODO: rename to e.g. `LocalGroupKeyStore` for clarity
*/
@scoped(Lifecycle.ContainerScoped)
export class GroupKeyStore {

Expand Down
159 changes: 159 additions & 0 deletions packages/client/src/encryption/LitProtocolFacade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import * as LitJsSdk from '@lit-protocol/lit-node-client'
import { inject, Lifecycle, scoped } from 'tsyringe'
import * as siwe from 'lit-siwe'
import { Authentication, AuthenticationInjectionToken } from '../Authentication'
import { ethers } from 'ethers'
import { StreamID } from '@streamr/protocol'
import { StreamPermission, streamPermissionToSolidityType } from '../permission'
import { ConfigInjectionToken, StrictStreamrClientConfig } from '../Config'
import { GroupKey } from './GroupKey'
import { Logger, withRateLimit } from '@streamr/utils'
import { LoggerFactory } from '../utils/LoggerFactory'

const logger = new Logger(module)

const chain = 'polygon'

const LIT_PROTOCOL_CONNECT_INTERVAL = 60 * 60 * 1000 // 1h

const formEvmContractConditions = (streamRegistryChainAddress: string, streamId: StreamID) => ([
{
contractAddress: streamRegistryChainAddress,
chain,
functionName: 'hasPermission',
functionParams: [streamId, ':userAddress', `${streamPermissionToSolidityType(StreamPermission.SUBSCRIBE)}`],
functionAbi: {
inputs: [
{
name: "streamId",
type: "string"
},
{
name: "user",
type: "address"
},
{
name: "permissionType",
type: "uint8"
}
],
name: "hasPermission",
outputs: [
{
name: "userHasPermission",
type: "bool"
}
],
stateMutability: "view",
type: "function"
},
returnValueTest: {
key: "userHasPermission",
comparator: '=',
value: "true",
},
}
])

const signAuthMessage = async (authentication: Authentication) => {
const domain = "dummy.com"
const uri = "https://dummy.com"
const statement = "dummy"
const addressInChecksumCase = ethers.utils.getAddress(await authentication.getAddress())
const siweMessage = new siwe.SiweMessage({
domain,
uri,
statement,
address: addressInChecksumCase,
version: "1",
chainId: 1
})
const messageToSign = siweMessage.prepareMessage()
const signature = await authentication.createMessageSignature(messageToSign)
return {
sig: signature,
derivedVia: "web3.eth.personal.sign",
signedMessage: messageToSign,
address: addressInChecksumCase
}
}

/**
* This class only operates with Polygon production network and therefore ignores contracts config.
*/
@scoped(Lifecycle.ContainerScoped)
export class LitProtocolFacade {
private readonly authentication: Authentication
private readonly config: Pick<StrictStreamrClientConfig, 'contracts' | 'encryption'>
private readonly logger: Logger
private litNodeClient?: LitJsSdk.LitNodeClient
private connectLitNodeClient?: () => Promise<void>

constructor(
loggerFactory: LoggerFactory,
@inject(ConfigInjectionToken) config: Pick<StrictStreamrClientConfig, 'contracts' | 'encryption'>,
@inject(AuthenticationInjectionToken) authentication: Authentication,
) {
this.authentication = authentication
this.config = config
this.logger = loggerFactory.createLogger(module)
}

async getLitNodeClient(): Promise<LitJsSdk.LitNodeClient> {
if (this.litNodeClient === undefined) {
this.litNodeClient = new LitJsSdk.LitNodeClient({
alertWhenUnauthorized: false,
debug: this.config.encryption.litProtocolLogging
})
// Add a rate limiter to avoid calling `connect` each time we want to use lit protocol as this would cause an unnecessary handshake.
this.connectLitNodeClient = withRateLimit(() => this.litNodeClient!.connect(), LIT_PROTOCOL_CONNECT_INTERVAL)
}
await this.connectLitNodeClient!()
return this.litNodeClient
}

async store(streamId: StreamID, symmetricKey: Uint8Array): Promise<GroupKey | undefined> {
this.logger.debug('storing key: %j', { streamId })
try {
const authSig = await signAuthMessage(this.authentication)
const client = await this.getLitNodeClient()
const encryptedSymmetricKey = await client.saveEncryptionKey({
evmContractConditions: formEvmContractConditions(this.config.contracts.streamRegistryChainAddress, streamId),
symmetricKey,
authSig,
chain
})
if (encryptedSymmetricKey === undefined) {
return undefined
}
const groupKeyId = LitJsSdk.uint8arrayToString(encryptedSymmetricKey, 'base16')
this.logger.debug('stored key: %j', { groupKeyId, streamId })
return new GroupKey(groupKeyId, Buffer.from(symmetricKey))
} catch (e) {
logger.warn('encountered error when trying to store key on lit-protocol: %s', e?.message)
return undefined
}
}

async get(streamId: StreamID, groupKeyId: string): Promise<GroupKey | undefined> {
this.logger.debug('get key: %j', { groupKeyId, streamId })
try {
const authSig = await signAuthMessage(this.authentication)
const client = await this.getLitNodeClient()
const symmetricKey = await client.getEncryptionKey({
evmContractConditions: formEvmContractConditions(this.config.contracts.streamRegistryChainAddress, streamId),
toDecrypt: groupKeyId,
chain,
authSig
})
if (symmetricKey === undefined) {
return undefined
}
this.logger.debug('got key: %j', { groupKeyId, streamId })
return new GroupKey(groupKeyId, Buffer.from(symmetricKey))
} catch (e) {
logger.warn('encountered error when trying to get key from lit-protocol: %s', e?.message)
return undefined
}
}
}
Loading