diff --git a/packages/crypto/src/lib/crypto.spec.ts b/packages/crypto/src/lib/crypto.spec.ts index 8dd8980e7..304a61719 100644 --- a/packages/crypto/src/lib/crypto.spec.ts +++ b/packages/crypto/src/lib/crypto.spec.ts @@ -2,8 +2,32 @@ import * as ethers from 'ethers'; import { joinSignature } from 'ethers/lib/utils'; import { SigShare } from '@lit-protocol/types'; +import { nacl } from '@lit-protocol/nacl'; +import { combineEcdsaShares, walletDecrypt, walletEncrypt } from './crypto'; -import { combineEcdsaShares } from './crypto'; +const MOCK_SESSION_SIGS = { + 'http://127.0.0.1:7470': { + sig: 'fefcd74c2bb2794356a10e62722c2ca4ef47386475ca72865d8dd7cc096fd1715d8b076b29349328e0b13d09f3296768e6b1cbb81e02d2b697b7641984260b01', + derivedVia: 'litSessionSignViaNacl', + signedMessage: `{"sessionKey":"6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d","resourceAbilityRequests":[{"resource":{"resource":"*","resourcePrefix":"lit-pkp"},"ability":"pkp-signing"},{"resource":{"resource":"*","resourcePrefix":"lit-litaction"},"ability":"lit-action-execution"}],"capabilities":[{"sig":"0x64192b2156f6d60f2c9aed887f5a0c6003afb6cd7a25b94683c74311fe895e8849a178d4edbe9f38cb0d8a3d7d0342b67b521ada92b68f73f0a1e5fa4f33ae751c","derivedVia":"web3.eth.personal.sign","signedMessage":"localhost wants you to sign in with your Ethereum account:\\n0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC\\n\\nThis is a test statement. You can put anything you want here. I further authorize the stated URI to perform the following actions on my behalf: (1) 'Threshold': 'Execution' for 'lit-litaction://*'. (2) 'Threshold': 'Signing' for 'lit-pkp://*'.\\n\\nURI: lit:session:6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d\\nVersion: 1\\nChain ID: 1\\nNonce: 0x4ee32274a45ab4622d49ea0f9bc3984f2a251e7b88320e23e231673efa564280\\nIssued At: 2025-04-09T14:37:09.339Z\\nExpiration Time: 2025-04-10T14:37:09.337Z\\nResources:\\n- urn:recap:eyJhdHQiOnsibGl0LWxpdGFjdGlvbjovLyoiOnsiVGhyZXNob2xkL0V4ZWN1dGlvbiI6W3t9XX0sImxpdC1wa3A6Ly8qIjp7IlRocmVzaG9sZC9TaWduaW5nIjpbe31dfX0sInByZiI6W119","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}],"issuedAt":"2025-04-09T14:37:09.373Z","expiration":"2025-04-10T14:37:09.337Z","nodeAddress":"http://127.0.0.1:7470","maxPrice":"113427455640312821154458202477256070485"}`, + address: '6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d', + algo: 'ed25519', + }, + 'http://127.0.0.1:7471': { + sig: 'ec550ae2addd6fbc399ef26158b9cf8b2a1c52240ee60b62c49c8a782c020d0aae5602090029f5a0024f936f0a747d7c007dfecee44bbe99a5cc12dcef7d3309', + derivedVia: 'litSessionSignViaNacl', + signedMessage: `{"sessionKey":"6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d","resourceAbilityRequests":[{"resource":{"resource":"*","resourcePrefix":"lit-pkp"},"ability":"pkp-signing"},{"resource":{"resource":"*","resourcePrefix":"lit-litaction"},"ability":"lit-action-execution"}],"capabilities":[{"sig":"0x64192b2156f6d60f2c9aed887f5a0c6003afb6cd7a25b94683c74311fe895e8849a178d4edbe9f38cb0d8a3d7d0342b67b521ada92b68f73f0a1e5fa4f33ae751c","derivedVia":"web3.eth.personal.sign","signedMessage":"localhost wants you to sign in with your Ethereum account:\\n0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC\\n\\nThis is a test statement. You can put anything you want here. I further authorize the stated URI to perform the following actions on my behalf: (1) 'Threshold': 'Execution' for 'lit-litaction://*'. (2) 'Threshold': 'Signing' for 'lit-pkp://*'.\\n\\nURI: lit:session:6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d\\nVersion: 1\\nChain ID: 1\\nNonce: 0x4ee32274a45ab4622d49ea0f9bc3984f2a251e7b88320e23e231673efa564280\\nIssued At: 2025-04-09T14:37:09.339Z\\nExpiration Time: 2025-04-10T14:37:09.337Z\\nResources:\\n- urn:recap:eyJhdHQiOnsibGl0LWxpdGFjdGlvbjovLyoiOnsiVGhyZXNob2xkL0V4ZWN1dGlvbiI6W3t9XX0sImxpdC1wa3A6Ly8qIjp7IlRocmVzaG9sZC9TaWduaW5nIjpbe31dfX0sInByZiI6W119","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}],"issuedAt":"2025-04-09T14:37:09.373Z","expiration":"2025-04-10T14:37:09.337Z","nodeAddress":"http://127.0.0.1:7471","maxPrice":"113427455640312821154458202477256070485"}`, + address: '6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d', + algo: 'ed25519', + }, + 'http://127.0.0.1:7472': { + sig: '5f4d83f7fc425ebfca85e45fed0799b93ac215c5ae1c7d279217ca70c434eb43db6e282f365d4f4dc7d76494548e33e4cb6e5f8901b5f142e447fc9718589f07', + derivedVia: 'litSessionSignViaNacl', + signedMessage: `{"sessionKey":"6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d","resourceAbilityRequests":[{"resource":{"resource":"*","resourcePrefix":"lit-pkp"},"ability":"pkp-signing"},{"resource":{"resource":"*","resourcePrefix":"lit-litaction"},"ability":"lit-action-execution"}],"capabilities":[{"sig":"0x64192b2156f6d60f2c9aed887f5a0c6003afb6cd7a25b94683c74311fe895e8849a178d4edbe9f38cb0d8a3d7d0342b67b521ada92b68f73f0a1e5fa4f33ae751c","derivedVia":"web3.eth.personal.sign","signedMessage":"localhost wants you to sign in with your Ethereum account:\\n0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC\\n\\nThis is a test statement. You can put anything you want here. I further authorize the stated URI to perform the following actions on my behalf: (1) 'Threshold': 'Execution' for 'lit-litaction://*'. (2) 'Threshold': 'Signing' for 'lit-pkp://*'.\\n\\nURI: lit:session:6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d\\nVersion: 1\\nChain ID: 1\\nNonce: 0x4ee32274a45ab4622d49ea0f9bc3984f2a251e7b88320e23e231673efa564280\\nIssued At: 2025-04-09T14:37:09.339Z\\nExpiration Time: 2025-04-10T14:37:09.337Z\\nResources:\\n- urn:recap:eyJhdHQiOnsibGl0LWxpdGFjdGlvbjovLyoiOnsiVGhyZXNob2xkL0V4ZWN1dGlvbiI6W3t9XX0sImxpdC1wa3A6Ly8qIjp7IlRocmVzaG9sZC9TaWduaW5nIjpbe31dfX0sInByZiI6W119","address":"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"}],"issuedAt":"2025-04-09T14:37:09.373Z","expiration":"2025-04-10T14:37:09.337Z","nodeAddress":"http://127.0.0.1:7472","maxPrice":"113427455640312821154458202477256070485"}`, + address: '6db14e40ab381cf208b39aec31aebb025e7b12f0ceec60519e2b0c191f4fcb3d', + algo: 'ed25519', + }, +}; describe('combine ECDSA Shares', () => { it('Should recombine ECDSA signature shares', async () => { @@ -67,3 +91,112 @@ describe('combine ECDSA Shares', () => { expect(recoveredAddr).toEqual(addr); }); }); + +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 64a02b2d8..4f9592c1d 100644 --- a/packages/crypto/src/lib/crypto.ts +++ b/packages/crypto/src/lib/crypto.ts @@ -9,13 +9,13 @@ import { UnknownError, UnknownSignatureError, } from '@lit-protocol/constants'; -import { checkType, log } from '@lit-protocol/misc'; +import { log } from '@lit-protocol/misc'; import { nacl } from '@lit-protocol/nacl'; import { + AuthSig, CombinedECDSASignature, NodeAttestation, SessionKeyPair, - SigningAccessControlConditionJWTPayload, SigShare, WalletEncryptedPayload, } from '@lit-protocol/types'; @@ -24,14 +24,14 @@ import { uint8arrayToString, } from '@lit-protocol/uint8arrays'; import { - BlsSignatureShareJsonString, - EcdsaVariant, blsCombine, blsDecrypt, blsEncrypt, + BlsSignatureShareJsonString, blsVerify, ecdsaCombine, ecdsaDeriveKey, + EcdsaVariant, ecdsaVerify, sevSnpGetVcekUrl, sevSnpVerify, @@ -373,27 +373,29 @@ async function getAmdCert(url: string): Promise { } } -export const walletEncrypt = async( - myWalletSecretKey: Uint8Array, +export const walletEncrypt = async ( + myWalletSecretKey: Uint8Array, theirWalletPublicKey: Uint8Array, - sessionSig: Uint8Array, + sessionSig: AuthSig, message: Uint8Array ): Promise => { + const uint8SessionSig = Buffer.from(JSON.stringify(sessionSig)); + const random = new Uint8Array(16); - window.crypto.getRandomValues(random); + crypto.getRandomValues(random); const dateNow = Date.now(); const createdAt = Math.floor(dateNow / 1000); const timestamp = Buffer.alloc(8); timestamp.writeBigUInt64BE(BigInt(createdAt), 0); const myWalletPublicKey = new Uint8Array(32); - nacl.crypto_scalarmult_base(myWalletPublicKey, myWalletSecretKey); + nacl.lowlevel.crypto_scalarmult_base(myWalletPublicKey, myWalletSecretKey); - // Construct AAD - const sessionSignature = Buffer.from(sessionSig); // Replace with actual session signature + // 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, @@ -401,42 +403,49 @@ export const walletEncrypt = async( theirPublicKey, myPublicKey, ]); - + const hash = new Uint8Array(64); - nacl.crypto_hash(hash, aad); + nacl.lowlevel.crypto_hash(hash, aad); const nonce = hash.slice(0, 24); - const ciphertext = nacl.box(message, nonce, theirPublicKey, myWalletSecretKey); + const ciphertext = nacl.box( + message, + nonce, + theirPublicKey, + myWalletSecretKey + ); return { V1: { verification_key: uint8ArrayToHex(myWalletPublicKey), ciphertext_and_tag: uint8ArrayToHex(ciphertext), session_signature: uint8ArrayToHex(sessionSignature), random: uint8ArrayToHex(random), - created_at: dateNow.toISOString(), - } + created_at: new Date(dateNow).toISOString(), + }, }; -} +}; -export const walletDecrypt = async( +export const walletDecrypt = async ( myWalletSecretKey: Uint8Array, payload: WalletEncryptedPayload ): Promise => { - const dateSent = new Date(payload.V1.created_at) - const createdAt = Math.floor(dateSent / 1000); + const dateSent = new Date(payload.V1.created_at); + const createdAt = Math.floor(dateSent.getTime() / 1000); const timestamp = Buffer.alloc(8); timestamp.writeBigUInt64BE(BigInt(createdAt), 0); const myWalletPublicKey = new Uint8Array(32); - nacl.crypto_scalarmult_base(myWalletPublicKey, myWalletSecretKey); + nacl.lowlevel.crypto_scalarmult_base(myWalletPublicKey, myWalletSecretKey); // 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 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, @@ -444,28 +453,37 @@ export const walletDecrypt = async( theirPublicKeyBuffer, myPublicKey, ]); - + const hash = new Uint8Array(64); - nacl.crypto_hash(hash, aad); + nacl.lowlevel.crypto_hash(hash, aad); const nonce = hash.slice(0, 24); - const message = nacl.box.open(payload.V1.ciphertext_and_tag, nonce, theirPublicKey, myWalletSecretKey); + + // Convert hex ciphertext back to Uint8Array + const ciphertext = hexToUint8Array(payload.V1.ciphertext_and_tag); + + const message = nacl.box.open( + ciphertext, + nonce, + theirPublicKey, + myWalletSecretKey + ); return message; -} +}; function uint8ArrayToHex(array: Uint8Array) { return Array.from(array) - .map(byte => byte.toString(16).padStart(2, '0')) - .join(''); + .map((byte) => byte.toString(16).padStart(2, '0')) + .join(''); } function hexToUint8Array(hexString: string): Uint8Array { if (hexString.length % 2 !== 0) { - throw new Error("Hex string must have an even length"); + throw new Error('Hex string must have an even length'); } const bytes = new Uint8Array(hexString.length / 2); for (let i = 0; i < bytes.length; i++) { - bytes[i] = parseInt(hexString.slice(i * 2, i * 2 + 2), 16); + bytes[i] = parseInt(hexString.slice(i * 2, i * 2 + 2), 16); } return bytes; }