-
Notifications
You must be signed in to change notification settings - Fork 43
feat: lit protocol key exchange #1052
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
Merged
Merged
Changes from all commits
Commits
Show all changes
70 commits
Select commit
Hold shift + click to select a range
9d22437
wip
harbu 38098ae
clean up
harbu f53395a
encryptedgroupkey change
harbu dc07a35
litprotocol get key
harbu cfa0fca
fixes
harbu acc151c
fix
harbu 46d975e
subscribe permission
harbu 4ab7321
fixes
harbu 57b8c82
config
harbu b4a92d0
clean up
harbu 11c7fc6
refactor
harbu b815e49
fix issue
harbu 07c8f85
fix
harbu 510d719
fix encoding
harbu 3c52c96
refactor
harbu f71f03b
try-catch LitProtocolKeyStore#get
harbu 53baa7a
return EncryptedGroupKey
harbu 24c8daa
add lit-protocl key to groupkeystore
harbu 3633833
refactor: rm unused import
harbu 73d5805
return undefined on error
harbu 5efa7ba
add comments, fix indentatin
harbu 89a5e97
make tests build
harbu 71e3795
re-order package.json
harbu c10eef0
feat: configuration option to enable/disable
harbu 534510a
refactor: dep siwe to lit-siwe
harbu 1d16442
Merge branch 'main' into lit-protocol
harbu 7ca5e89
deps: update package-lock.json
harbu 384143c
fix: quick fix test?
harbu 4467709
refactor: addressInChecksumCase
harbu 885396a
docs: add class comment to LitProtocolKeyStore
harbu 8cc2904
refactor: style question
harbu 3c6818a
refactor: generateNewKey
harbu d57d817
refactor: GroupKeyManager
harbu 71b2b52
refactor: slight refactor to style
harbu d9b364e
refactor: GroupKeyManager add storeKey method
harbu ce441d3
fix: fix tests
harbu 7e03380
refactor: LitProtocolKeyStore param naming
harbu ff49651
eslint
harbu 98d6187
refactor: method parameter order
harbu d73781f
tests: fix
harbu 466f0a6
fix tests
harbu efcd770
eslint
harbu 8497cc7
LitProtocolFacade refactorings
harbu d65c119
feat: logging (also refactoring)
harbu b04c071
fixes
harbu 7094926
fix eslint
harbu 33922ba
fix tests
harbu de2ecff
fix tests
harbu fbfc7eb
release(client, cli-tools): v7.3.0-beta.0
harbu e8aca22
feat(utils): withRateLimit
harbu cc5edea
work
harbu 4056d45
refactor: address PR comments
harbu d15c6de
test: GroupKeyManager
harbu 1965f16
feat: add guard on StreamrClient#updateEncryptionKey preventing givin…
harbu 6512cef
Merge remote-tracking branch 'origin/main' into lit-protocol
harbu fabe6d2
deps: update lit-protocol
harbu e17972b
release(client, cli-tools): v7.4.0-beta.0
harbu b0e91b9
docs: typedoc and changelog
harbu c151f78
Merge branch 'main' into lit-protocol
harbu b526ed7
make eslint happy
harbu db63013
fix: missing @inject
harbu 9ae9b9d
fix: inject fix
harbu 700f8da
deps: bump lit-client to 2.1.16
harbu 2e83372
release(client, cli-tools): v7.4.0-beta.1
harbu 958e383
Revert "release(client, cli-tools): v7.4.0-beta.1"
teogeb e8e2502
deps: bump lit-client to 2.1.38
teogeb 2c1166e
Merge branch 'main' into lit-protocol
teogeb b6cd163
release: revert client version to match main
teogeb d2e1dd8
Merge branch 'main' into lit-protocol
teogeb 44f94c0
Merge branch 'main' into lit-protocol
teogeb File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.