@thant-dev/ciphersuite
Version:
A suite of cryptographic utilities, including signal protocol, AES, and public key encryption for secure messaging.
602 lines • 30.4 kB
JavaScript
import * as secp256k1 from "@noble/secp256k1";
import { webcrypto as nodeCrypto } from "crypto"; // Node.js crypto
function uint8ArrayToHexString(uint8Array) {
return Array.from(uint8Array)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
export class Ratchet {
constructor(options) {
this.MAX_SKIP = 1000; // Maximum number of skipped messages
this.MAX_CACHE_SIZE = 2000; // Maximum size of message key cache
this.MESSAGE_KEY_TTL = 14 * 24 * 60 * 60 * 1000; // 14 days in milliseconds
// Use window.crypto for browser or Node.js's crypto for server
const globalCrypto = typeof window !== "undefined" ? window.crypto : nodeCrypto;
if (!globalCrypto || !globalCrypto.subtle) {
throw new Error("Web Crypto API is not available.");
}
// TypeScript type assertion
this.crypto = globalCrypto;
this.subtle = this.crypto.subtle;
this.verboseLog = options.verboseLog || true;
this.debugLog = false;
this.revertStateOnFailure = options.revertStateOnFailure || true;
// Validate keypair
if (!options.keyPair ||
!options.keyPair.publicKey ||
!options.keyPair.privateKey) {
throw new Error("Invalid keypair provided");
}
if (options.keyPair.privateKey.length !== 32) {
throw new Error("Invalid private key length. Expected 32 bytes");
}
if (options.keyPair.publicKey.length !== 65) {
throw new Error("Invalid public key length. Expected 65 bytes (uncompressed format)");
}
// Verify the keypair is valid
const derivedPubKey = secp256k1.getPublicKey(options.keyPair.privateKey, false);
if (!this.areBuffersEqual(derivedPubKey, options.keyPair.publicKey)) {
throw new Error("Public key does not match private key");
}
// Store initial keypair
this.initialPublicKey = options.keyPair.publicKey;
this.initialPrivateKey = options.keyPair.privateKey;
// Initialize DH keys as null
this.currentDHPublicKey = options.keyPair.publicKey;
this.currentDHPrivateKey = options.keyPair.privateKey;
this.rootKey = new Uint8Array(32);
this.sendChainKey = new Uint8Array(32);
this.recvChainKey = new Uint8Array(32);
this.remotePublicKey = new Uint8Array(32);
this.isInitiator = options.isInitiator;
this.needToRatchet = this.isInitiator ? true : false;
this.isFirstMessage = true;
this.sendMessageNumber = 0;
this.recvMessageNumber = 0;
this.previousChainLength = 0;
this.messageKeyCache = new Map();
this.sentMessageKeyCache = new Map(); // Add sent message key cache
this.sentPublicKeys = new Set();
}
async hkdf(salt, ikm, info, length) {
const encoder = new TextEncoder();
const baseKey = await this.subtle.importKey("raw", ikm, { name: "HKDF" }, false, ["deriveBits"]);
const derivedBits = await this.subtle.deriveBits({
name: "HKDF",
hash: "SHA-256",
salt: salt,
info: encoder.encode(info),
}, baseKey, length * 8);
return new Uint8Array(derivedBits);
}
async kdfRoot(rootKey, dhOutput) {
const salt = rootKey || new Uint8Array(32);
const okm = await this.hkdf(salt, dhOutput, "DoubleRatchetRoot", 64);
const newRootKey = okm.slice(0, 32);
const newChainKey = okm.slice(32, 64);
return { newRootKey, newChainKey };
}
async kdfChainKey(chainKey) {
const encoder = new TextEncoder();
const key = await this.subtle.importKey("raw", chainKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const signature = await this.subtle.sign("HMAC", key, encoder.encode("ChainKey"));
return new Uint8Array(signature);
}
async kdfMessageKey(chainKey) {
const encoder = new TextEncoder();
const key = await this.subtle.importKey("raw", chainKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const signature = await this.subtle.sign("HMAC", key, encoder.encode("MessageKey"));
return new Uint8Array(signature);
}
async initialize(remotePublicKey) {
if (remotePublicKey.length !== 65) {
throw new Error("Invalid remote public key length. Expected 65 bytes (compressed format)");
}
this.remotePublicKey = remotePublicKey;
// Compute shared secret using ECDH with secp256k1
const sharedPoint = secp256k1.getSharedSecret(this.initialPrivateKey, this.remotePublicKey);
// Use only the x-coordinate (first 32 bytes) as the shared secret
const sharedSecret = sharedPoint.slice(1, 33);
// Derive initial root key and chain key
const { newRootKey, newChainKey } = await this.kdfRoot(new Uint8Array(32), sharedSecret);
this.rootKey = newRootKey;
if (this.debugLog)
console.log("Initializing: Root key", uint8ArrayToHexString(this.rootKey));
if (this.debugLog)
console.log("Initializing: Chain key", uint8ArrayToHexString(newChainKey));
if (this.isInitiator) {
this.sendChainKey = newChainKey;
this.needToRatchet = true;
if (this.debugLog)
console.log("Initiator: send chain key", uint8ArrayToHexString(this.sendChainKey));
}
else {
this.recvChainKey = newChainKey;
this.needToRatchet = false;
if (this.debugLog)
console.log("Non-initiator: receive chain key", uint8ArrayToHexString(this.recvChainKey));
}
}
getCacheKey(chainKey) {
return uint8ArrayToHexString(chainKey);
}
async storeReceivedMessageKey(dhPublicKey, messageNumber, messageKey) {
const cacheKey = this.getCacheKey(dhPublicKey);
if (!this.messageKeyCache.has(cacheKey)) {
this.messageKeyCache.set(cacheKey, new Map());
}
const chainCache = this.messageKeyCache.get(cacheKey);
chainCache.set(messageNumber, messageKey);
// Add timestamp to track TTL
const timestamp = Date.now();
chainCache.set(messageNumber, messageKey);
// Enforce cache size limit
if (chainCache.size > this.MAX_CACHE_SIZE) {
const sortedKeys = Array.from(chainCache.keys()).sort((a, b) => a - b);
while (chainCache.size > this.MAX_CACHE_SIZE) {
chainCache.delete(sortedKeys.shift());
}
}
}
async encrypt(plaintext) {
if (!this.remotePublicKey) {
throw new Error("Ratchet not initialized. Call initialize() first");
}
if (this.debugLog) {
console.log(`======== ENCRYPT ========`);
console.log(`isInitiator: ${this.isInitiator}`);
console.log(`needToRatchet: ${this.needToRatchet}`);
}
if (this.needToRatchet) {
if (this.verboseLog)
console.log("Generating new DH key pair due to ratchet step");
// Generate new DH key pair
this.currentDHPrivateKey = secp256k1.utils.randomPrivateKey();
this.currentDHPublicKey = secp256k1.getPublicKey(this.currentDHPrivateKey, false);
// Track this public key as one we used for sending
if (this.verboseLog)
console.log(`Generated new DH key pair for encrypting message.`, Buffer.from(this.currentDHPublicKey).toString("hex"));
// Perform a Diffie-Hellman ratchet step
if (this.verboseLog)
console.log(`Mixing with remote public key: ${Buffer.from(this.remotePublicKey).toString("hex")}`);
const sharedPoint = secp256k1.getSharedSecret(this.currentDHPrivateKey, this.remotePublicKey);
if (this.debugLog)
console.log(`Shared point: ${Buffer.from(sharedPoint).toString("hex")}`);
const sharedSecret = sharedPoint.slice(1, 33);
if (!this.rootKey) {
throw new Error("Root key not initialized");
}
// Update root key and derive the new send chain key
const { newRootKey, newChainKey } = await this.kdfRoot(this.rootKey, sharedSecret);
this.rootKey = newRootKey;
this.sendChainKey = newChainKey;
// Reset message numbers for new chain
this.previousChainLength = this.sendMessageNumber;
this.sendMessageNumber = 0;
this.needToRatchet = false;
if (this.debugLog)
console.log(`New Root Key: ${Buffer.from(this.rootKey).toString("hex")}`);
if (this.debugLog)
console.log(`New Send Chain Key: ${Buffer.from(this.sendChainKey).toString("hex")}`);
}
else {
if (this.verboseLog)
console.log("Not ratcheting. Using existing send chain key");
}
// Include message number in the packet
const messageNumber = this.sendMessageNumber;
this.sentPublicKeys.add(uint8ArrayToHexString(this.currentDHPublicKey));
this.sendMessageNumber++;
if (this.verboseLog)
console.log(`send DH public key stored`);
// Derive message key
const messageKey = await this.kdfMessageKey(this.sendChainKey);
if (messageKey.length !== 32) {
throw new Error("Derived message key length is invalid");
}
// Store the message key with the public key as cache key for later decryption
await this.storeSentMessageKey(this.currentDHPublicKey, messageNumber, messageKey);
// Advance the send chain key for the next message
this.sendChainKey = await this.kdfChainKey(this.sendChainKey);
if (this.debugLog)
console.log(`Advancing send chain key for next message`, Buffer.from(this.sendChainKey).toString("hex"));
// Generate a 12-byte IV for AES-GCM encryption
const iv = this.crypto.getRandomValues(new Uint8Array(12));
if (iv.length !== 12) {
throw new Error("IV length must be 12 bytes");
}
if (this.debugLog)
console.log(`Encrypting with message key: ${Buffer.from(messageKey).toString("hex")} and messageNumber: ${messageNumber}`);
// Import the message key for encryption
const encKey = await this.subtle.importKey("raw", messageKey, { name: "AES-GCM" }, false, ["encrypt"]);
// Encrypt the plaintext
const encoder = new TextEncoder();
const plaintextBytes = encoder.encode(plaintext || " "); // Fallback to a single space for empty strings
const encryptedData = await this.subtle.encrypt({ name: "AES-GCM", iv }, encKey, plaintextBytes);
return {
dhPublicKey: this.currentDHPublicKey,
iv,
ciphertext: new Uint8Array(encryptedData),
messageNumber,
};
}
async decrypt(packet) {
if (!packet.dhPublicKey || !packet.iv || !packet.ciphertext) {
throw new Error("Invalid encrypted message format");
}
if (packet.dhPublicKey.length !== 65) {
throw new Error("Invalid public key length. Expected 65 bytes (uncompressed format)");
}
if (packet.iv.length !== 12) {
throw new Error("Invalid IV length. Expected 12 bytes");
}
if (packet.ciphertext.length === 0) {
throw new Error("Empty ciphertext");
}
try {
let messageKey;
if (this.isSentMessage(packet)) {
if (this.verboseLog)
console.log(`Decrypting own message #${packet.messageNumber}`);
// Use the message's DH public key as cache key
const cacheKey = uint8ArrayToHexString(packet.dhPublicKey);
const chainCache = this.sentMessageKeyCache.get(cacheKey);
if (chainCache == null) {
throw new Error("Sent message key cache not found");
}
messageKey = chainCache.get(packet.messageNumber);
if (!messageKey) {
throw new Error("Sent message key not found in cache");
}
// Decrypt the message using the found message key
const decKey = await this.subtle.importKey("raw", messageKey, { name: "AES-GCM" }, false, ["decrypt"]);
const decryptedData = await this.subtle.decrypt({ name: "AES-GCM", iv: packet.iv }, decKey, packet.ciphertext);
const decoder = new TextDecoder();
if (this.verboseLog)
console.log("Decrypted completed for sent message");
return decoder.decode(decryptedData).trim();
}
else {
if (this.verboseLog)
console.log(`Decrypting received message #${packet.messageNumber}`);
const decrypted = await this.decryptReceivedMessage(packet);
if (this.verboseLog)
console.log("Decrypted completed for received message");
return decrypted;
}
}
catch (error) {
console.error("Decryption failed:", error);
throw new Error(error instanceof Error
? error.message
: "Decryption failed due to incorrect key or corrupted message data");
}
}
async decryptReceivedMessage(packet) {
// Backup current state to revert in case of failure
const oldRootKey = this.rootKey ? new Uint8Array(this.rootKey) : null;
const oldRecvChainKey = this.recvChainKey
? new Uint8Array(this.recvChainKey)
: null;
const oldRemotePublicKey = this.remotePublicKey
? new Uint8Array(this.remotePublicKey)
: null;
try {
// Save current state before any changes
const savedState = {
rootKey: this.rootKey ? new Uint8Array(this.rootKey) : null,
recvChainKey: this.recvChainKey
? new Uint8Array(this.recvChainKey)
: null,
remotePublicKey: this.remotePublicKey
? new Uint8Array(this.remotePublicKey)
: null,
sendMessageNumber: this.sendMessageNumber,
recvMessageNumber: this.recvMessageNumber,
previousChainLength: this.previousChainLength,
};
const keysAreDifferent = !this.remotePublicKey ||
!this.areBuffersEqual(packet.dhPublicKey, this.remotePublicKey);
// Check if this is an out-of-order message, but skip check for first message
const isFirstMessage = packet.messageNumber === 0;
if (this.verboseLog)
console.log(`isFirstMessage: ${isFirstMessage}`);
if (!isFirstMessage && packet.messageNumber > this.recvMessageNumber) {
if (this.verboseLog) {
console.log(`Out-of-order message received. Skipping ${packet.messageNumber - this.recvMessageNumber} messages.`);
}
// Store current state and throw error
if (this.recvChainKey) {
await this.storeReceivedMessageKey(packet.dhPublicKey, // Use DH public key instead of chain key
packet.messageNumber, await this.kdfMessageKey(this.recvChainKey));
}
throw new Error("Message received out of order");
}
// Handle DH ratchet step if needed
if (keysAreDifferent) {
if (this.verboseLog) {
console.log("Sender's public keys are different. Generating new shared secret and ratcheting.");
console.log("Sender's newest public key:", uint8ArrayToHexString(packet.dhPublicKey));
if (this.debugLog) {
if (this.currentDHPublicKey)
console.log("Our current public key:", uint8ArrayToHexString(this.currentDHPublicKey));
if (this.currentDHPrivateKey)
console.log("Our current private key:", uint8ArrayToHexString(this.currentDHPrivateKey));
}
}
if (this.currentDHPrivateKey === null) {
throw new Error("Current DH private key is not initialized");
}
this.remotePublicKey = packet.dhPublicKey;
// Compute new shared secret using received public key and current private key
const sharedPoint = secp256k1.getSharedSecret(this.currentDHPrivateKey, packet.dhPublicKey);
if (this.debugLog)
console.log(`Shared point: ${Buffer.from(sharedPoint).toString("hex")}`);
const sharedSecret = sharedPoint.slice(1, 33);
if (!this.rootKey) {
throw new Error("Root key not initialized");
}
// Derive new root key and receive chain key
const derivedKeys = await this.kdfRoot(this.rootKey, sharedSecret);
this.rootKey = derivedKeys.newRootKey;
this.recvChainKey = derivedKeys.newChainKey;
if (this.debugLog)
console.log(`New ratchet keys: rootKey: ${uint8ArrayToHexString(this.rootKey)}, recvChainKey: ${uint8ArrayToHexString(this.recvChainKey)}`);
// Store the length of the previous sending chain
this.previousChainLength = this.sendMessageNumber;
this.sendMessageNumber = 0;
this.recvMessageNumber = 0;
// Ensure non-initiator initializes the send chain key after receiving the first message
if (!this.sendChainKey) {
this.sendChainKey = await this.kdfChainKey(this.recvChainKey);
}
// Set flag to generate new DH key pair before sending next message
this.needToRatchet = true;
}
let messageKey;
// Try to find message key in cache first
const cacheKey = this.getCacheKey(packet.dhPublicKey);
const chainCache = this.messageKeyCache.get(cacheKey);
if (chainCache == null) {
if (this.verboseLog)
console.log(`Message key cache not found for public key: ${uint8ArrayToHexString(packet.dhPublicKey)}`);
}
else {
if (chainCache) {
if (this.debugLog) {
console.log(`Message key found in cache for message #${packet.messageNumber} and publicKey: ${uint8ArrayToHexString(packet.dhPublicKey)}`);
}
}
messageKey = chainCache.get(packet.messageNumber);
}
// If message key is not found in cache, derive it
if (!messageKey) {
if (!isFirstMessage && packet.messageNumber < this.recvMessageNumber) {
// Restore previous state and try again
Object.assign(this, savedState);
if (this.verboseLog)
console.log(`Restored state after decryption failure. packet.messageNumber: ${packet.messageNumber}, this.recvMessageNumber: ${this.recvMessageNumber}`);
throw new Error("Message from old chain that we don't have cached");
}
// Generate new message key and advance chain
messageKey = await this.kdfMessageKey(this.recvChainKey);
// todo: why are we updating here?
this.recvChainKey = await this.kdfChainKey(this.recvChainKey);
this.recvMessageNumber = packet.messageNumber + 1;
if (this.debugLog)
console.log(`New receive chain key: ${uint8ArrayToHexString(this.recvChainKey)} and received message number: ${packet.messageNumber} messageKey: ${uint8ArrayToHexString(messageKey)}`);
// Store the message key with DH public key as cache key
await this.storeReceivedMessageKey(packet.dhPublicKey, packet.messageNumber, messageKey);
}
if (messageKey.length !== 32) {
throw new Error("Derived message key length is invalid");
}
if (this.debugLog) {
console.log(`Decrypting with received chain key: ${Buffer.from(this.recvChainKey).toString("hex")}`);
console.log(`Decrypt with message key: ${Buffer.from(messageKey).toString("hex")}`);
}
// Import message key for decryption
const decKey = await this.subtle.importKey("raw", messageKey, { name: "AES-GCM" }, false, ["decrypt"]);
// Attempt to decrypt the ciphertext
const decryptedData = await this.subtle.decrypt({ name: "AES-GCM", iv: packet.iv }, decKey, packet.ciphertext);
// Return the decrypted plaintext
const decoder = new TextDecoder();
return decoder.decode(decryptedData).trim();
}
catch (error) {
if (this.verboseLog) {
console.error(`Decryption failed due to incorrect key or corrupted message data. Reverting the receive chain key to: ${oldRecvChainKey
? Buffer.from(oldRecvChainKey).toString("hex")
: "not initialized"}`);
}
if (this.revertStateOnFailure) {
// Revert to the previous state if decryption fails
this.rootKey = oldRootKey
? new Uint8Array(oldRootKey)
: new Uint8Array(32);
this.recvChainKey = oldRecvChainKey
? new Uint8Array(oldRecvChainKey)
: new Uint8Array(32);
this.remotePublicKey = oldRemotePublicKey
? new Uint8Array(oldRemotePublicKey)
: new Uint8Array(32);
}
throw error;
}
}
async storeSentMessageKey(dhPublicKey, messageNumber, messageKey) {
const cacheKey = uint8ArrayToHexString(dhPublicKey);
if (!this.sentMessageKeyCache.has(cacheKey)) {
this.sentMessageKeyCache.set(cacheKey, new Map());
}
const chainCache = this.sentMessageKeyCache.get(cacheKey);
chainCache.set(messageNumber, messageKey);
// Enforce cache size limit
if (chainCache.size > this.MAX_CACHE_SIZE) {
const sortedKeys = Array.from(chainCache.keys()).sort((a, b) => a - b);
while (chainCache.size > this.MAX_CACHE_SIZE) {
chainCache.delete(sortedKeys.shift());
}
}
if (this.verboseLog)
console.log(`Stored sent message key for message #${messageNumber} with public key: ${uint8ArrayToHexString(dhPublicKey)}`);
}
// Add method to check if a message was sent by us
isSentMessage(packet) {
return this.sentPublicKeys.has(uint8ArrayToHexString(packet.dhPublicKey));
}
restoreState(state) {
if (this.verboseLog)
console.log("======== RESTORE STATE ========");
if (!state) {
throw new Error("No state provided for restoration");
}
// Helper function to convert serialized object to Uint8Array
const toUint8Array = (data) => {
if (!data)
return new Uint8Array(32);
// If it's already a Uint8Array, return a copy
if (data instanceof Uint8Array) {
return new Uint8Array(data);
}
// If it's a string (hex), convert from hex
if (typeof data === "string") {
return new Uint8Array(Buffer.from(data, "hex"));
}
// If it's an object with numeric keys (serialized Uint8Array)
if (typeof data === "object" && !Array.isArray(data)) {
// Convert object with numeric keys to array
const arr = new Uint8Array(Object.keys(data).length);
for (let key in data) {
if (data.hasOwnProperty(key)) {
arr[parseInt(key)] = data[key];
}
}
return arr;
}
// If it's an array-like object
if (Array.isArray(data)) {
return new Uint8Array(data);
}
console.warn("Unknown data type for conversion:", data);
return new Uint8Array(32);
};
// Restore basic properties with proper Uint8Array conversion
this.rootKey = toUint8Array(state.rootKey);
this.sendChainKey = toUint8Array(state.sendChainKey);
this.recvChainKey = toUint8Array(state.recvChainKey);
this.remotePublicKey = toUint8Array(state.remotePublicKey);
// Restore message counters and flags
this.sendMessageNumber = state.sendMessageNumber;
this.recvMessageNumber = state.recvMessageNumber;
this.previousChainLength = state.previousChainLength;
this.needToRatchet = state.needToRatchet;
this.isFirstMessage = state.isFirstMessage;
this.isInitiator = state.isInitiator;
this.sentPublicKeys = new Set(state.sentPublicKeys);
// Restore DH keys with proper type conversion
if (state.currentDHPublicKey) {
this.currentDHPublicKey = toUint8Array(state.currentDHPublicKey);
if (this.debugLog) {
console.log(`Restoring DH public key from state: ${this.currentDHPublicKey
? uint8ArrayToHexString(this.currentDHPublicKey)
: "null"}`);
}
}
if (state.currentDHPrivateKey) {
this.currentDHPrivateKey = toUint8Array(state.currentDHPrivateKey);
if (this.debugLog) {
console.log(`Restoring DH private key from state: ${this.currentDHPrivateKey
? uint8ArrayToHexString(this.currentDHPrivateKey)
: "null"}`);
}
}
// Restore message key caches
this.messageKeyCache = new Map(Object.entries(state.messageKeyCache).map(([chainKey, messages]) => [
chainKey,
new Map(Object.entries(messages).map(([msgNum, keyHex]) => [
parseInt(msgNum),
new Uint8Array(Buffer.from(keyHex, "hex")),
])),
]));
// Restore sent message key cache
this.sentMessageKeyCache = new Map(Object.entries(state.sentMessageKeyCache).map(([chainKey, messages]) => [
chainKey,
new Map(Object.entries(messages).map(([msgNum, keyHex]) => [
parseInt(msgNum),
new Uint8Array(Buffer.from(keyHex, "hex")),
])),
]));
if (this.debugLog) {
console.log("Restored ratchet state with caches:", {
rootKey: this.rootKey
? uint8ArrayToHexString(this.rootKey)
: new Uint8Array(32),
sendChainKey: this.sendChainKey
? uint8ArrayToHexString(this.sendChainKey)
: new Uint8Array(32),
recvChainKey: this.recvChainKey
? uint8ArrayToHexString(this.recvChainKey)
: new Uint8Array(32),
remotePublicKey: this.remotePublicKey
? uint8ArrayToHexString(this.remotePublicKey)
: new Uint8Array(32),
sendMessageNumber: this.sendMessageNumber,
recvMessageNumber: this.recvMessageNumber,
previousChainLength: this.previousChainLength,
needToRatchet: this.needToRatchet,
isFirstMessage: this.isFirstMessage,
sentPublicKeysCount: this.sentPublicKeys.size,
messageKeyCacheSize: this.messageKeyCache.size,
sentMessageKeyCacheSize: this.sentMessageKeyCache.size,
currentDHPublicKey: this.currentDHPublicKey,
});
console.log(`State after restoration:`, this.getState());
}
}
getState() {
// Convert message key caches to serializable format using Object.fromEntries
const messageKeyCache = Object.fromEntries(Array.from(this.messageKeyCache.entries()).map(([chainKey, messages]) => [
chainKey,
Object.fromEntries(Array.from(messages.entries()).map(([msgNum, keyBytes]) => [
msgNum,
uint8ArrayToHexString(keyBytes),
])),
]));
const sentMessageKeyCache = Object.fromEntries(Array.from(this.sentMessageKeyCache.entries()).map(([chainKey, messages]) => [
chainKey,
Object.fromEntries(Array.from(messages.entries()).map(([msgNum, keyBytes]) => [
msgNum,
uint8ArrayToHexString(keyBytes),
])),
]));
return {
rootKey: this.rootKey,
sendChainKey: this.sendChainKey,
recvChainKey: this.recvChainKey,
remotePublicKey: this.remotePublicKey,
needToRatchet: this.needToRatchet,
isFirstMessage: this.isFirstMessage,
sendMessageNumber: this.sendMessageNumber,
recvMessageNumber: this.recvMessageNumber,
previousChainLength: this.previousChainLength,
sentPublicKeys: Array.from(this.sentPublicKeys),
messageKeyCache,
sentMessageKeyCache,
// Include DH keys in state
currentDHPublicKey: this.currentDHPublicKey,
currentDHPrivateKey: this.currentDHPrivateKey,
isInitiator: this.isInitiator,
};
}
// Add helper method to convert public key to/from string
publicKeyToString(pubKey) {
return uint8ArrayToHexString(pubKey);
}
areBuffersEqual(buf1, buf2) {
if (buf1.length !== buf2.length)
return false;
return buf1.every((byte, i) => byte === buf2[i]);
}
}
//# sourceMappingURL=signal.js.map