Skip to content

add methods and struct for e2ee #788

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

Open
wants to merge 7 commits into
base: feat/rc-naga-2025-04-07
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion local-tests/setup/networkContext.json
Original file line number Diff line number Diff line change
Expand Up @@ -15308,4 +15308,4 @@
],
"name": "Forwarder"
}
}
}
53 changes: 36 additions & 17 deletions packages/core/src/lib/lit-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -887,7 +906,7 @@ export class LitCore {
protected _handshakeWithNode = async (
params: HandshakeWithNode,
requestId: string
): Promise<NodeCommandServerKeysResponse> => {
): Promise<GenericResponse<NodeCommandServerKeysResponse>> => {
// -- get properties from params
const { url } = params;

Expand Down Expand Up @@ -1029,9 +1048,9 @@ export class LitCore {
protected _getNodePromises = (
nodeUrls: string[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (url: string) => Promise<any>
callback: (url: string) => { url: string, promise: Promise<any>},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any>[] => {
): { url: string, promise: Promise<any> }[] => {
// FIXME: Replace <any> usage with explicit, strongly typed handlers

const nodePromises = [];
Expand Down Expand Up @@ -1090,25 +1109,25 @@ export class LitCore {
* @returns { Promise<SuccessNodePromises<T> | RejectedNodePromises> }
*/
protected _handleNodePromises = async <T>(
nodePromises: Promise<T>[],
nodePromises: { url: string, promise: Promise<T> }[],
requestId: string,
minNodeCount: number
): Promise<SuccessNodePromises<T> | RejectedNodePromises> => {
): Promise<SuccessNodePromises<WalletEncryptedPayload> | RejectedNodePromises> => {
async function waitForNSuccessesWithErrors<T>(
promises: Promise<T>[],
promises: { url: string, promise: Promise<T> }[],
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 });
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ export interface SendNodeCommand {
requestId: string;
}

export interface GenericResponse<T> {
ok: boolean;
error?: string;
errorObject?: string;
data?: T;
}

export interface NodeCommandServerKeysResponse {
serverPublicKey: string;
subnetPublicKey: string;
Expand All @@ -16,6 +23,7 @@ export interface NodeCommandServerKeysResponse {
hdRootPubkeys: string[];
attestation?: NodeAttestation;
latestBlockhash?: string;
nodeIdentityKey: string;
}

export interface HandshakeWithNode {
Expand Down
109 changes: 109 additions & 0 deletions packages/crypto/src/lib/crypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
18 changes: 4 additions & 14 deletions packages/crypto/src/lib/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,14 +353,11 @@ async function getAmdCert(url: string): Promise<Uint8Array> {
}
}

export const walletEncrypt = async (
export const walletEncrypt = (
myWalletSecretKey: Uint8Array,
theirWalletPublicKey: Uint8Array,
sessionSig: AuthSig,
message: Uint8Array
): Promise<WalletEncryptedPayload> => {
const uint8SessionSig = Buffer.from(JSON.stringify(sessionSig));

): WalletEncryptedPayload => {
const random = new Uint8Array(16);
crypto.getRandomValues(random);
const dateNow = Date.now();
Expand All @@ -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,
Expand All @@ -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> => {
): Uint8Array => {
const dateSent = new Date(payload.V1.created_at);
const createdAt = Math.floor(dateSent.getTime() / 1000);
const timestamp = Buffer.alloc(8);
Expand All @@ -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,
Expand Down
Loading
Loading