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

feat(sdk): [NET-1268][!]: lit protocol upgrade #2475

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
7,786 changes: 5,678 additions & 2,108 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@babel/preset-env": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@jest/globals": "^29.5.0",
"@lit-protocol/lit-node-client": "^6.0.3",
"@streamr/test-utils": "101.0.1",
"@types/heap": "^0.2.34",
"@types/lodash": "^4.17.6",
Expand Down Expand Up @@ -84,7 +85,6 @@
"dependencies": {
"@babel/runtime": "^7.24.7",
"@babel/runtime-corejs3": "^7.24.7",
"@lit-protocol/core": "2.2.5",
"@lit-protocol/uint8arrays": "^6.1.0",
"@streamr/config": "^5.3.13",
"@streamr/dht": "101.0.1",
Expand Down
14 changes: 0 additions & 14 deletions packages/sdk/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,20 +339,6 @@ export interface StreamrClientConfig {
* how encryption keys should be exchanged.
*/
encryption?: {
/**
* Enable experimental Lit Protocol key exchange.
*
* When enabled encryption key storing and fetching will primarily be done through the
* [Lit Protocol](https://litprotocol.com/) 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

// TODO keyRequestTimeout and maxKeyRequestsPerSecond config options could be applied
// to lit protocol key requests (both encryption and decryption?)
/**
Expand Down
21 changes: 20 additions & 1 deletion packages/sdk/src/StreamrClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import { StreamDefinition } from './types'
import { LoggerFactory } from './utils/LoggerFactory'
import { pOnce } from './utils/promises'
import { convertPeerDescriptorToNetworkPeerDescriptor, createTheGraphClient } from './utils/utils'
import type { LitNodeClient } from '@lit-protocol/lit-node-client'
import { LitProtocolFacade } from './encryption/LitProtocolFacade'

// TODO: this type only exists to enable tsdoc to generate proper documentation
export type SubscribeOptions = StreamDefinition & ExtraSubscribeOptions
Expand Down Expand Up @@ -91,6 +93,7 @@ export class StreamrClient {
private readonly operatorRegistry: OperatorRegistry
private readonly contractFactory: ContractFactory
private readonly localGroupKeyStore: LocalGroupKeyStore
private readonly litProtocolFacade: LitProtocolFacade
private readonly theGraphClient: TheGraphClient
private readonly streamIdBuilder: StreamIDBuilder
private readonly config: StrictStreamrClientConfig
Expand Down Expand Up @@ -127,6 +130,7 @@ export class StreamrClient {
this.operatorRegistry = container.resolve<OperatorRegistry>(OperatorRegistry)
this.contractFactory = container.resolve<ContractFactory>(ContractFactory)
this.localGroupKeyStore = container.resolve<LocalGroupKeyStore>(LocalGroupKeyStore)
this.litProtocolFacade = container.resolve<LitProtocolFacade>(LitProtocolFacade)
this.streamIdBuilder = container.resolve<StreamIDBuilder>(StreamIDBuilder)
this.eventEmitter = container.resolve<StreamrClientEventEmitter>(StreamrClientEventEmitter)
this.destroySignal = container.resolve<DestroySignal>(DestroySignal)
Expand Down Expand Up @@ -159,14 +163,29 @@ export class StreamrClient {
return convertStreamMessageToMessage(result)
}

// --------------------------------------------------------------------------------------------
// Encryption
// --------------------------------------------------------------------------------------------

/**
* Enable experimental Lit Protocol key exchange.
*
* When enabled encryption key storing and fetching will primarily be done through the
* [Lit Protocol](https://litprotocol.com/) and secondarily through the standard Streamr
* key-exchange system.
*/
enableLitProtocol(litProtocolClient: LitNodeClient): void {
this.litProtocolFacade.setLitNodeClient(litProtocolClient)
}

/**
* Manually updates the encryption key used when publishing messages to a given stream.
*/
async updateEncryptionKey(opts: UpdateEncryptionKeyOptions): Promise<void> {
if (opts.streamId === undefined) {
throw new Error('streamId required')
}
if (opts.key !== undefined && this.config.encryption.litProtocolEnabled) {
if (opts.key !== undefined && this.litProtocolFacade.isLitProtocolEnabled()) {
throw new StreamrClientError('cannot pass "key" when Lit Protocol is enabled', 'UNSUPPORTED_OPERATION')
}
const streamId = await this.streamIdBuilder.toStreamID(opts.streamId)
Expand Down
14 changes: 5 additions & 9 deletions packages/sdk/src/encryption/GroupKeyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@ export class GroupKeyManager {
}

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

// 3rd try: Streamr key-exchange
Expand Down Expand Up @@ -87,9 +85,7 @@ export class GroupKeyManager {
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)
}
groupKey = await this.litProtocolFacade.store(streamId, keyData)
if (groupKey === undefined) {
groupKey = new GroupKey(uuid('GroupKey'), keyData)
}
Expand Down
105 changes: 59 additions & 46 deletions packages/sdk/src/encryption/LitProtocolFacade.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import { LitCore } from '@lit-protocol/core'
import { uint8arrayToString } from '@lit-protocol/uint8arrays'
import { Logger, StreamID, randomString, withRateLimit } from '@streamr/utils'
import { ethers } from 'ethers'
import * as siwe from 'lit-siwe'
import type { LitNodeClient } from '@lit-protocol/lit-node-client'
import { Logger, StreamID, randomString, binaryToHex } from '@streamr/utils'
import { Lifecycle, inject, scoped } from 'tsyringe'
import { Authentication, AuthenticationInjectionToken } from '../Authentication'
import { ConfigInjectionToken, StrictStreamrClientConfig } from '../Config'
import { StreamPermission, streamPermissionToSolidityType } from '../permission'
import { LoggerFactory } from '../utils/LoggerFactory'
import { GroupKey } from './GroupKey'
import { ethers } from 'ethers'
import { SiweMessage } from 'lit-siwe'

const logger = new Logger(module)

const chain = 'polygon'

const LIT_PROTOCOL_CONNECT_INTERVAL = 60 * 60 * 1000 // 1h
const GROUP_KEY_ID_SEPARATOR = '::'

// TODO: can this type be imported directly from '@lit-protocol/lit-node-client'?
type ContractConditions = Parameters<LitNodeClient['encrypt']>[0]['evmContractConditions']

const formEvmContractConditions = (streamRegistryChainAddress: string, streamId: StreamID) => ([
export const formEvmContractConditions = (
streamRegistryChainAddress: string,
streamId: StreamID
): ContractConditions => ([
{
contractAddress: streamRegistryChainAddress,
chain,
Expand Down Expand Up @@ -55,44 +60,55 @@ const formEvmContractConditions = (streamRegistryChainAddress: string, streamId:
}
])

const signAuthMessage = async (authentication: Authentication) => {
const domain = 'dummy.com'
const uri = 'https://dummy.com'
const signAuthMessage = async (litNodeClient: LitNodeClient, authentication: Authentication) => {
const domain = 'localhost'
const uri = 'https://localhost/login'
const statement = 'dummy'
const addressInChecksumCase = ethers.getAddress(await authentication.getAddress())
const siweMessage = new siwe.SiweMessage({
const nonce = await litNodeClient.getLatestBlockhash()
const expirationTime = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString()
const siweMessage = new SiweMessage({
domain,
uri,
statement,
address: addressInChecksumCase,
version: '1',
chainId: 1
chainId: 1,
expirationTime,
nonce
})
const messageToSign = siweMessage.prepareMessage()
const signature = await authentication.createMessageSignature(Buffer.from(messageToSign))
return {
sig: signature,
sig: binaryToHex(signature, true),
derivedVia: 'web3.eth.personal.sign',
signedMessage: messageToSign,
address: addressInChecksumCase
}
}

function splitGroupKeyId(groupKeyId: string): { ciphertext: string, dataToEncryptHash: string } | undefined {
const [ciphertext, dataToEncryptHash] = groupKeyId.split(GROUP_KEY_ID_SEPARATOR)
if (ciphertext !== undefined && dataToEncryptHash !== undefined) {
return { ciphertext, dataToEncryptHash }
}
return undefined
}

/**
* This class only operates with Polygon production network and therefore ignores contracts config.
*/
@scoped(Lifecycle.ContainerScoped)
export class LitProtocolFacade {

private litNodeClient?: LitCore
private readonly config: Pick<StrictStreamrClientConfig, 'contracts' | 'encryption'>
private readonly config: Pick<StrictStreamrClientConfig, 'contracts'>
private readonly authentication: Authentication
private readonly logger: Logger
private connectLitNodeClient?: () => Promise<void>
private litNodeClient?: LitNodeClient

/* eslint-disable indent */
constructor(
@inject(ConfigInjectionToken) config: Pick<StrictStreamrClientConfig, 'contracts' | 'encryption'>,
@inject(ConfigInjectionToken) config: Pick<StrictStreamrClientConfig, 'contracts'>,
@inject(AuthenticationInjectionToken) authentication: Authentication,
loggerFactory: LoggerFactory
) {
Expand All @@ -101,35 +117,27 @@ export class LitProtocolFacade {
this.logger = loggerFactory.createLogger(module)
}

async getLitNodeClient(): Promise<LitCore> {
if (this.litNodeClient === undefined) {
this.litNodeClient = new LitCore({
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
setLitNodeClient(litNodeClient: LitNodeClient): void {
this.litNodeClient = litNodeClient
}

isLitProtocolEnabled(): boolean {
return this.litNodeClient !== undefined
}

async store(streamId: StreamID, symmetricKey: Uint8Array): Promise<GroupKey | undefined> {
if (this.litNodeClient === undefined) {
return undefined
}
const traceId = randomString(5)
this.logger.debug('Storing key', { streamId, traceId })
try {
const authSig = await signAuthMessage(this.authentication)
const client = await this.getLitNodeClient()
const encryptedSymmetricKey = await client.saveEncryptionKey({
await this.litNodeClient.connect()
const { ciphertext, dataToEncryptHash } = await this.litNodeClient.encrypt({
evmContractConditions: formEvmContractConditions(this.config.contracts.streamRegistryChainAddress, streamId),
symmetricKey,
authSig,
chain
dataToEncrypt: symmetricKey
})
if (encryptedSymmetricKey === undefined) {
return undefined
}
const groupKeyId = uint8arrayToString(encryptedSymmetricKey, 'base16')
const groupKeyId = ciphertext + GROUP_KEY_ID_SEPARATOR + dataToEncryptHash
this.logger.debug('Stored key', { traceId, streamId, groupKeyId })
return new GroupKey(groupKeyId, Buffer.from(symmetricKey))
} catch (err) {
Expand All @@ -139,21 +147,26 @@ export class LitProtocolFacade {
}

async get(streamId: StreamID, groupKeyId: string): Promise<GroupKey | undefined> {
if (this.litNodeClient === undefined) {
return undefined
}
this.logger.debug('Getting key', { groupKeyId, streamId })
try {
const authSig = await signAuthMessage(this.authentication)
const client = await this.getLitNodeClient()
const symmetricKey = await client.getEncryptionKey({
const splitResult = splitGroupKeyId(groupKeyId)
if (splitResult === undefined) {
return undefined
}
await this.litNodeClient.connect()
const authSig = await signAuthMessage(this.litNodeClient, this.authentication)
const decryptResponse = await this.litNodeClient.decrypt({
evmContractConditions: formEvmContractConditions(this.config.contracts.streamRegistryChainAddress, streamId),
toDecrypt: groupKeyId,
ciphertext: splitResult.ciphertext,
dataToEncryptHash: splitResult.dataToEncryptHash,
chain,
authSig
})
if (symmetricKey === undefined) {
return undefined
}
this.logger.debug('Got key', { groupKeyId, streamId })
return new GroupKey(groupKeyId, Buffer.from(symmetricKey))
return new GroupKey(groupKeyId, Buffer.from(decryptResponse.decryptedData))
} catch (err) {
logger.warn('Failed to get key', { err, streamId, groupKeyId })
return undefined
Expand Down
2 changes: 0 additions & 2 deletions packages/sdk/test/test-utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,6 @@ export const createGroupKeyManager = (
groupKeyStore,
{
encryption: {
litProtocolEnabled: false,
litProtocolLogging: false,
maxKeyRequestsPerSecond: 10,
keyRequestTimeout: 50,
// eslint-disable-next-line no-underscore-dangle
Expand Down
Loading
Loading