diff --git a/local-tests/setup/networkContext.json b/local-tests/setup/networkContext.json index 47b471331..82da5e588 100644 --- a/local-tests/setup/networkContext.json +++ b/local-tests/setup/networkContext.json @@ -15308,4 +15308,4 @@ ], "name": "Forwarder" } -} \ No newline at end of file +} diff --git a/packages/core/src/lib/lit-core.ts b/packages/core/src/lib/lit-core.ts index 2d7b969fe..9d993d83b 100644 --- a/packages/core/src/lib/lit-core.ts +++ b/packages/core/src/lib/lit-core.ts @@ -50,12 +50,14 @@ import { RejectedNodePromises, SessionSigsMap, SuccessNodePromises, + WalletEncryptedPayload, } from '@lit-protocol/types'; import { composeLitUrl } from './endpoint-version'; import { CoreNodeConfig, EpochCache, + GenericResponse, HandshakeWithNode, Listener, NodeCommandServerKeysResponse, @@ -545,13 +547,30 @@ export class LitCore { requestId ); + if (!handshakeResult.ok) { + const error = handshakeResult.error ?? ""; + const errorObject = handshakeResult.errorObject ?? ""; + + logErrorWithRequestId( + requestId, + `Error handshaking with the node ${url}: ${error} - ${errorObject}` + ); + throw new Error(`${error} - ${errorObject}`); + } + + if (!handshakeResult.data) { + throw new Error('Handshake response data is empty'); + } + const handshakeResponse = handshakeResult.data; + const keys: JsonHandshakeResponse = { - serverPubKey: handshakeResult.serverPublicKey, - subnetPubKey: handshakeResult.subnetPublicKey, - networkPubKey: handshakeResult.networkPublicKey, - networkPubKeySet: handshakeResult.networkPublicKeySet, - hdRootPubkeys: handshakeResult.hdRootPubkeys, - latestBlockhash: handshakeResult.latestBlockhash, + serverPubKey: handshakeResponse.serverPublicKey, + subnetPubKey: handshakeResponse.subnetPublicKey, + networkPubKey: handshakeResponse.networkPublicKey, + networkPubKeySet: handshakeResponse.networkPublicKeySet, + hdRootPubkeys: handshakeResponse.hdRootPubkeys, + latestBlockhash: handshakeResponse.latestBlockhash, + nodeIdentityKey: handshakeResponse.nodeIdentityKey, }; // Nodes that have just bootstrapped will not have negotiated their keys, yet @@ -587,7 +606,7 @@ export class LitCore { this.config.checkNodeAttestation || NETWORKS_REQUIRING_SEV.includes(this.config.litNetwork) ) { - const attestation = handshakeResult.attestation; + const attestation = handshakeResponse.attestation; if (!attestation) { throw new InvalidNodeAttestation( @@ -887,7 +906,7 @@ export class LitCore { protected _handshakeWithNode = async ( params: HandshakeWithNode, requestId: string - ): Promise => { + ): Promise> => { // -- get properties from params const { url } = params; @@ -1029,9 +1048,9 @@ export class LitCore { protected _getNodePromises = ( nodeUrls: string[], // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback: (url: string) => Promise + callback: (url: string) => { url: string, promise: Promise}, // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise[] => { + ): { url: string, promise: Promise }[] => { // FIXME: Replace usage with explicit, strongly typed handlers const nodePromises = []; @@ -1090,25 +1109,25 @@ export class LitCore { * @returns { Promise | RejectedNodePromises> } */ protected _handleNodePromises = async ( - nodePromises: Promise[], + nodePromises: { url: string, promise: Promise }[], requestId: string, minNodeCount: number - ): Promise | RejectedNodePromises> => { + ): Promise | RejectedNodePromises> => { async function waitForNSuccessesWithErrors( - promises: Promise[], + promises: { url: string, promise: Promise }[], n: number // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise<{ successes: T[]; errors: any[] }> { + ): Promise<{ successes: { url: string, result: T }[]; errors: any[] }> { let responses = 0; - const successes: T[] = []; + const successes: { url: string, result: T }[] = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any const errors: any[] = []; return new Promise((resolve) => { - promises.forEach((promise) => { + promises.forEach(({ url, promise }) => { promise .then((result) => { - successes.push(result); + successes.push({ url, result }); if (successes.length >= n) { // If we've got enough successful responses to continue, resolve immediately even if some are pending resolve({ successes, errors }); diff --git a/packages/core/src/lib/types.ts b/packages/core/src/lib/types.ts index b93544add..e821cd3e9 100644 --- a/packages/core/src/lib/types.ts +++ b/packages/core/src/lib/types.ts @@ -8,6 +8,13 @@ export interface SendNodeCommand { requestId: string; } +export interface GenericResponse { + ok: boolean; + error?: string; + errorObject?: string; + data?: T; +} + export interface NodeCommandServerKeysResponse { serverPublicKey: string; subnetPublicKey: string; @@ -16,6 +23,7 @@ export interface NodeCommandServerKeysResponse { hdRootPubkeys: string[]; attestation?: NodeAttestation; latestBlockhash?: string; + nodeIdentityKey: string; } export interface HandshakeWithNode { diff --git a/packages/crypto/src/lib/crypto.spec.ts b/packages/crypto/src/lib/crypto.spec.ts index 8f9f8a02d..1e7c1a6c2 100644 --- a/packages/crypto/src/lib/crypto.spec.ts +++ b/packages/crypto/src/lib/crypto.spec.ts @@ -523,3 +523,112 @@ describe('walletEncrypt and walletDecrypt', () => { expect(decryptedTamperedMessage).toBeNull(); }); }); + +describe('walletEncrypt and walletDecrypt', () => { + it('should encrypt and decrypt a message successfully', async () => { + // Generate key pairs using the box functionality + const aliceKeyPair = nacl.box.keyPair(); + const bobKeyPair = nacl.box.keyPair(); + + console.log('aliceKeyPair', aliceKeyPair); + console.log('bobKeyPair', bobKeyPair); + + // Message to encrypt + const message = new TextEncoder().encode('This is a secret message'); + + // Alice encrypts a message for Bob + const encryptedPayload = await walletEncrypt( + aliceKeyPair.secretKey, + bobKeyPair.publicKey, + MOCK_SESSION_SIGS['http://127.0.0.1:7470'], + message + ); + + console.log('encryptedPayload', encryptedPayload); + + // Verify payload structure + expect(encryptedPayload).toHaveProperty('V1'); + expect(encryptedPayload.V1).toHaveProperty('verification_key'); + expect(encryptedPayload.V1).toHaveProperty('ciphertext_and_tag'); + expect(encryptedPayload.V1).toHaveProperty('session_signature'); + expect(encryptedPayload.V1).toHaveProperty('random'); + expect(encryptedPayload.V1).toHaveProperty('created_at'); + + // Bob decrypts the message from Alice + const decryptedMessage = await walletDecrypt( + bobKeyPair.secretKey, + encryptedPayload + ); + + // Verify decryption was successful + expect(decryptedMessage).not.toBeNull(); + expect(new TextDecoder().decode(decryptedMessage as Uint8Array)).toBe( + 'This is a secret message' + ); + }); + + it('should return null when decryption fails', async () => { + // Generate key pairs + const aliceKeyPair = nacl.box.keyPair(); + const bobKeyPair = nacl.box.keyPair(); + const eveKeyPair = nacl.box.keyPair(); // Eve is an eavesdropper + + // Message to encrypt + const message = new TextEncoder().encode('This is a secret message'); + + // Alice encrypts a message for Bob + const encryptedPayload = await walletEncrypt( + aliceKeyPair.secretKey, + bobKeyPair.publicKey, + MOCK_SESSION_SIGS['http://127.0.0.1:7470'], + message + ); + + // Eve tries to decrypt the message with her key (should fail) + const decryptedByEve = await walletDecrypt( + eveKeyPair.secretKey, + encryptedPayload + ); + + // Verify decryption failed + expect(decryptedByEve).toBeNull(); + }); + + it('should handle tampering with the encrypted payload', async () => { + // Generate key pairs + const aliceKeyPair = nacl.box.keyPair(); + const bobKeyPair = nacl.box.keyPair(); + + // Message to encrypt + const message = new TextEncoder().encode('This is a secret message'); + + // Alice encrypts a message for Bob + const encryptedPayload = await walletEncrypt( + aliceKeyPair.secretKey, + bobKeyPair.publicKey, + MOCK_SESSION_SIGS['http://127.0.0.1:7470'], + message + ); + + // Tamper with the ciphertext + const tamperedPayload = { + ...encryptedPayload, + V1: { + ...encryptedPayload.V1, + ciphertext_and_tag: + encryptedPayload.V1.ciphertext_and_tag.substring(0, 10) + + 'ff' + + encryptedPayload.V1.ciphertext_and_tag.substring(12), + }, + }; + + // Bob tries to decrypt the tampered message + const decryptedTamperedMessage = await walletDecrypt( + bobKeyPair.secretKey, + tamperedPayload + ); + + // Verify decryption failed due to tampering + expect(decryptedTamperedMessage).toBeNull(); + }); +}); diff --git a/packages/crypto/src/lib/crypto.ts b/packages/crypto/src/lib/crypto.ts index 1659f1af8..9f2159fb7 100644 --- a/packages/crypto/src/lib/crypto.ts +++ b/packages/crypto/src/lib/crypto.ts @@ -353,14 +353,11 @@ async function getAmdCert(url: string): Promise { } } -export const walletEncrypt = async ( +export const walletEncrypt = ( myWalletSecretKey: Uint8Array, theirWalletPublicKey: Uint8Array, - sessionSig: AuthSig, message: Uint8Array -): Promise => { - const uint8SessionSig = Buffer.from(JSON.stringify(sessionSig)); - +): WalletEncryptedPayload => { const random = new Uint8Array(16); crypto.getRandomValues(random); const dateNow = Date.now(); @@ -372,12 +369,10 @@ export const walletEncrypt = async ( nacl.lowlevel.crypto_scalarmult_base(myWalletPublicKey, myWalletSecretKey); // Construct AAD (Additional Authenticated Data) - data that is authenticated but not encrypted - const sessionSignature = uint8SessionSig; // Replace with actual session signature const theirPublicKey = Buffer.from(theirWalletPublicKey); // Replace with their public key const myPublicKey = Buffer.from(myWalletPublicKey); // Replace with your wallet public key const aad = Buffer.concat([ - sessionSignature, random, timestamp, theirPublicKey, @@ -398,17 +393,16 @@ export const walletEncrypt = async ( V1: { verification_key: uint8ArrayToHex(myWalletPublicKey), ciphertext_and_tag: uint8ArrayToHex(ciphertext), - session_signature: uint8ArrayToHex(sessionSignature), random: uint8ArrayToHex(random), created_at: new Date(dateNow).toISOString(), }, }; }; -export const walletDecrypt = async ( +export const walletDecrypt = ( myWalletSecretKey: Uint8Array, payload: WalletEncryptedPayload -): Promise => { +): Uint8Array => { const dateSent = new Date(payload.V1.created_at); const createdAt = Math.floor(dateSent.getTime() / 1000); const timestamp = Buffer.alloc(8); @@ -419,15 +413,11 @@ export const walletDecrypt = async ( // Construct AAD const random = Buffer.from(hexToUint8Array(payload.V1.random)); - const sessionSignature = Buffer.from( - hexToUint8Array(payload.V1.session_signature) - ); // Replace with actual session signature const theirPublicKey = hexToUint8Array(payload.V1.verification_key); const theirPublicKeyBuffer = Buffer.from(theirPublicKey); // Replace with their public key const myPublicKey = Buffer.from(myWalletPublicKey); // Replace with your wallet public key const aad = Buffer.concat([ - sessionSignature, random, timestamp, theirPublicKeyBuffer, diff --git a/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts b/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts index 9a13b3934..a08680961 100644 --- a/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts +++ b/packages/lit-node-client-nodejs/src/lib/lit-node-client-nodejs.ts @@ -51,6 +51,8 @@ import { hashLitMessage, verifyAndDecryptWithSignatureShares, verifySignature, + walletDecrypt, + walletEncrypt, } from '@lit-protocol/crypto'; import { defaultMintClaimCallback, @@ -127,10 +129,12 @@ import type { JsonPkpSignSdkParams, JsonSignSessionKeyRequestV1, JsonSignSessionKeyRequestV2, + JsonSignSessionKeyResponse, LitNodeClientConfig, NodeBlsSigningShare, NodeCommandResponse, NodeSet, + PKPSignEndpointResponse, RejectedNodePromises, SessionKeyPair, SessionSigningTemplate, @@ -139,7 +143,10 @@ import type { SignSessionKeyProp, SignSessionKeyResponse, SuccessNodePromises, + WalletEncryptedPayload, } from '@lit-protocol/types'; +import { json } from 'node:stream/consumers'; +import { GenericResponse } from 'packages/core/src/lib/types'; export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { /** Tracks the total max price a user is willing to pay for each supported product type @@ -604,7 +611,7 @@ export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { url: string, formattedParams: JsonExecutionSdkParams & { sessionSigs: SessionSigsMap }, requestId: string, - nodeSet: NodeSet[] + nodeSet: { node: NodeSet, nodeIdentityKey: string }[] ) { // -- choose the right signature const sessionSig = this._getSessionSigByUrl({ @@ -615,7 +622,7 @@ export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { const reqBody: JsonExecutionRequest = { ...formattedParams, authSig: sessionSig, - nodeSet, + nodeSet: nodeSet, }; const urlWithPath = composeLitUrl({ @@ -889,6 +896,17 @@ export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { // Handle promises for commands sent to Lit nodes const targetNodeUrls = targetNodePrices.map(({ url }) => url); + let keySets: Record = {}; + targetNodeUrls.forEach((url) => { + const theirPublicKey = uint8arrayFromString(this.serverKeys[url].nodeIdentityKey); + const keyPair = nacl.box.keyPair.generate(); + const secretKey = keyPair.secretKey; + + keySets[url] = { + theirPublicKey, + secretKey, + }; + }); const nodePromises = this._getNodePromises( targetNodeUrls, (url: string) => { @@ -906,6 +924,8 @@ export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { params.messageToSign! ); + const { theirPublicKey, secretKey } = keySets[url]; + const reqBody: JsonPkpSignRequest = { toSign: normalizeArray(toSign), signingScheme: params.signingScheme, @@ -929,7 +949,13 @@ export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { endpoint: LIT_ENDPOINT.PKP_SIGN, }); - return this.generatePromise(urlWithPath, reqBody, requestId); + const encrypted = walletEncrypt( + theirPublicKey, + secretKey, + uint8arrayFromString(JSON.stringify(reqBody)) + ); + + return { url, promise: this.generatePromise(urlWithPath, encrypted, requestId) }; } ); @@ -944,7 +970,29 @@ export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { return this._throwNodeError(res, requestId); } - const responseData = res.values; + const responseData = res.values.map((values) => { + const secretKey = keySets[values.url].secretKey; + + const decrypted = walletDecrypt( + secretKey, + values.result, + ); + + const response: GenericResponse = JSON.parse(uint8arrayToString(decrypted, 'utf8')); + + if (!response.ok) { + const error = response.error ?? ""; + const errorObject = response.errorObject ?? {}; + + throw new Error(`${error} - ${errorObject}`); + } + + if (!response.data) { + throw new Error('No data returned from pkp sign'); + } + + return response.data; + }); logWithRequestId( requestId, @@ -1521,6 +1569,7 @@ export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { }; getSignSessionKeyShares = async ( + nodeIdentityKey: string, url: string, params: { body: { @@ -1538,11 +1587,34 @@ export class LitNodeClientNodeJs extends LitCore implements ILitNodeClient { url, endpoint: LIT_ENDPOINT.SIGN_SESSION_KEY, }); - return await this._sendCommandToNode({ + + const theirPublicKey = uint8arrayFromString(nodeIdentityKey, 'base16'); + const keyPair = nacl.box.keyPair(); + const secretKey = keyPair.secretKey; + + const encrypted = await walletEncrypt( + secretKey, + theirPublicKey, + uint8arrayFromString(JSON.stringify(params.body), 'utf8') + ); + + const response: WalletEncryptedPayload = await this._sendCommandToNode({ url: urlWithPath, - data: params.body, + data: encrypted, requestId, }); + + const decrypted = await walletDecrypt( + secretKey, + response, + ); + + const outerResponse: GenericResponse = JSON.parse(uint8arrayToString(decrypted, 'utf8')); + if (!outerResponse.ok) { + throw new Error(`${outerResponse.error} - ${outerResponse.errorObject}`); + } + + return outerResponse.data; }; getMaxPricesForNodeProduct = async ({ diff --git a/packages/types/src/lib/interfaces.ts b/packages/types/src/lib/interfaces.ts index c5eba3d22..8eb0ad8aa 100644 --- a/packages/types/src/lib/interfaces.ts +++ b/packages/types/src/lib/interfaces.ts @@ -264,6 +264,16 @@ export interface JsonSignSessionKeyRequestV2 signingScheme: T; } +export interface JsonSignSessionKeyResponse { + result: string; + signatureShare: any; + shareId: string; + curveType: string; + siweMessage: string; + dataSigned: string; + blsRootPubkey: string; +} + // [ // { // "result": "success", @@ -572,7 +582,7 @@ export interface BlsSignatureShare { export interface SuccessNodePromises { success: true; - values: T[]; + values: { url: string, result: T }[]; } export interface RejectedNodePromises { @@ -671,7 +681,6 @@ export interface WalletEncryptedPayload { V1: { verification_key: string; ciphertext_and_tag: string; - session_signature: string; random: string; created_at: string; }; @@ -701,6 +710,7 @@ export interface JsonHandshakeResponse { networkPubKeySet: string; hdRootPubkeys: string[]; latestBlockhash?: string; + nodeIdentityKey: string; } export interface EncryptToJsonProps extends MultipleAccessControlConditions {