/** * RustDesk Standalone Web Client - Cryptography Module * * Handles NaCl encryption/decryption, key exchange, and password hashing. * Uses TweetNaCl.js for all cryptographic operations. */ const RDCrypto = (() => { let _sessionKey = null; // 32-byte symmetric key for session encryption let _sendSeq = 0; // Send sequence counter let _recvSeq = 0; // Receive sequence counter let _peerPk = null; // Peer's Ed25519 public key (from PunchHoleResponse) let _mySignKeyPair = null; // Our Ed25519 signing keypair let _myBoxKeyPair = null; // Our Curve25519 box keypair (per-session) /** * Generate a random Ed25519 signing keypair */ function generateSignKeyPair() { _mySignKeyPair = nacl.sign.keyPair(); return _mySignKeyPair; } /** * Generate a random Curve25519 keypair for ECDH */ function generateBoxKeyPair() { _myBoxKeyPair = nacl.box.keyPair(); return _myBoxKeyPair; } /** * Get our Ed25519 public key (32 bytes) */ function getSignPublicKey() { return _mySignKeyPair ? _mySignKeyPair.publicKey : null; } /** * Get our Curve25519 public key (32 bytes) */ function getBoxPublicKey() { return _myBoxKeyPair ? _myBoxKeyPair.publicKey : null; } /** * Set the peer's Ed25519 public key (from PunchHoleResponse.pk) */ function setPeerPublicKey(pk) { _peerPk = new Uint8Array(pk); } /** * Verify a SignedId message * SignedId.id = Ed25519 signature (64 bytes) || serialized IdPk protobuf * Verification uses the peer's Ed25519 public key from PunchHoleResponse * Returns the deserialized IdPk { id, pk } on success */ function verifySignedId(signedIdBytes) { if (!_peerPk || _peerPk.length < 32) { throw new Error('Peer public key not set'); } const signed = new Uint8Array(signedIdBytes); if (signed.length < 64) { throw new Error('SignedId too short'); } // NaCl sign format: signature (64 bytes) || message const signature = signed.slice(0, 64); const message = signed.slice(64); // Verify Ed25519 signature // Note: nacl.sign.detached.verify expects (message, signature, publicKey) const verified = nacl.sign.detached.verify(message, signature, _peerPk); if (!verified) { throw new Error('SignedId verification failed'); } // Parse the message as IdPk protobuf const IdPk = RDProto.lookup('IdPk'); const idPk = IdPk.decode(message); console.log('[RDCrypto] SignedId verified. Peer ID:', idPk.id, 'Curve25519 pk length:', idPk.pk.length); return idPk; } /** * Create a PublicKey message for key exchange response * Generates a random 32-byte symmetric key and encrypts it with nacl.box * * @param {Uint8Array} peerCurvePk - Peer's ephemeral Curve25519 public key (from IdPk.pk) * @returns {Object} { asymmetric_value, symmetric_value } */ function createKeyExchangeResponse(peerCurvePk) { if (!_myBoxKeyPair) { generateBoxKeyPair(); } // Generate random 32-byte symmetric session key _sessionKey = nacl.randomBytes(32); _sendSeq = 0; _recvSeq = 0; // Compute shared key via ECDH: box.before(peer_pk, our_sk) const sharedKey = nacl.box.before(new Uint8Array(peerCurvePk), _myBoxKeyPair.secretKey); // Encrypt the session key with the shared key // nacl.box.after(message, nonce, sharedKey) returns MAC(16) + ciphertext const nonce = new Uint8Array(24); // All zeros for key exchange const encrypted = nacl.box.after(_sessionKey, nonce, sharedKey); const result = { asymmetric_value: _myBoxKeyPair.publicKey, // Our Curve25519 pk (32 bytes) symmetric_value: encrypted // MAC(16) + encrypted key (32) = 48 bytes }; console.log('[RDCrypto] Key exchange response created. Session key established.'); return result; } /** * Encrypt session data using NaCl secretbox * Format: MAC (16 bytes) + ciphertext * Nonce: 24-byte LE encoding of sequence number */ function encrypt(data) { if (!_sessionKey) return data; const nonce = new Uint8Array(24); const view = new DataView(nonce.buffer); _sendSeq++; view.setBigUint64(0, BigInt(_sendSeq), true); // LE const encrypted = nacl.secretbox(new Uint8Array(data), nonce, _sessionKey); return encrypted; } /** * Decrypt session data using NaCl secretbox */ function decrypt(data) { if (!_sessionKey) return new Uint8Array(data); const nonce = new Uint8Array(24); const view = new DataView(nonce.buffer); _recvSeq++; view.setBigUint64(0, BigInt(_recvSeq), true); // LE const decrypted = nacl.secretbox.open(new Uint8Array(data), nonce, _sessionKey); if (!decrypted) { console.error('[RDCrypto] Decryption failed! Seq:', _recvSeq); return null; } return decrypted; } /** * Check if encryption is active */ function isEncrypted() { return _sessionKey !== null; } /** * Reset encryption state */ function reset() { _sessionKey = null; _sendSeq = 0; _recvSeq = 0; _peerPk = null; } /** * Hash password for RustDesk authentication * RustDesk expects: SHA256(salt + password) * * @param {string} salt - The salt from the Hash challenge * @param {string} password - The user's password * @returns {Promise} - SHA256 hash as bytes */ async hashPassword(salt, password) { const data = new TextEncoder().encode(salt + password); const hash = await crypto.subtle.digest('SHA-256', data); return new Uint8Array(hash); } /** * Verify hbbs server key exchange signature * The server sends KeyExchange { keys: [signed_ephemeral_pk] } * where signed_ephemeral_pk is Ed25519-sign(signature || ephemeral_pk) * * @param {Uint8Array} signedKey - The signed key from server's KeyExchange * @param {Uint8Array} serverPk - The server's long-term Ed25519 public key * @returns {Uint8Array|null} - The server's ephemeral Curve25519 public key */ function verifyServerKey(signedKey, serverPk) { const signed = new Uint8Array(signedKey); if (signed.length < 64) return null; const signature = signed.slice(0, 64); const message = signed.slice(64); const verified = nacl.sign.detached.verify(message, signature, new Uint8Array(serverPk)); if (!verified) { console.error('[RDCrypto] Server key verification failed'); return null; } console.log('[RDCrypto] Server key verified, ephemeral pk length:', message.length); return message; // This is the server's ephemeral Curve25519 pk } /** * Create client response for hbbs server key exchange * @param {Uint8Array} serverEphemeralPk - Server's ephemeral Curve25519 pk * @returns {Uint8Array[]} - [our_ephemeral_pk, encrypted_symmetric_key] */ function createServerKeyResponse(serverEphemeralPk) { if (!_myBoxKeyPair) { generateBoxKeyPair(); } // Generate symmetric key for hbbs communication const symmetricKey = nacl.randomBytes(32); // Compute shared key via ECDH const sharedKey = nacl.box.before(new Uint8Array(serverEphemeralPk), _myBoxKeyPair.secretKey); // Encrypt symmetric key const nonce = new Uint8Array(24); const encrypted = nacl.box.after(symmetricKey, nonce, sharedKey); // Store the symmetric key for hbbs communication // We'll handle this separately from the session key // For now, just return the response return [ _myBoxKeyPair.publicKey, // Our ephemeral Curve25519 pk encrypted // Encrypted symmetric key ]; } /** * Get our Curve25519 secret key (32 bytes) */ function getBoxSecretKey() { return _myBoxKeyPair ? _myBoxKeyPair.secretKey : null; } /** * Generate a fresh Curve25519 keypair for hbbs encryption * Separate from the session keypair used for peer-to-peer */ let _hbbsBoxKeyPair = null; let _hbbsSharedKey = null; let _hbbsSendSeq = 0; let _hbbsRecvSeq = 0; function generateHbbsKeyPair() { _hbbsBoxKeyPair = nacl.box.keyPair(); return _hbbsBoxKeyPair; } function setupHbbsEncryption(serverEphemeralPk) { if (!_hbbsBoxKeyPair) generateHbbsKeyPair(); _hbbsSharedKey = nacl.box.before( new Uint8Array(serverEphemeralPk), _hbbsBoxKeyPair.secretKey ); _hbbsSendSeq = 0; _hbbsRecvSeq = 0; } function hbbsEncrypt(data) { if (!_hbbsSharedKey) return data; _hbbsSendSeq++; const nonce = new Uint8Array(24); new DataView(nonce.buffer).setBigUint64(0, BigInt(_hbbsSendSeq), true); return nacl.secretbox(new Uint8Array(data), nonce, _hbbsSharedKey); } function hbbsDecrypt(data) { if (!_hbbsSharedKey) return new Uint8Array(data); _hbbsRecvSeq++; const nonce = new Uint8Array(24); new DataView(nonce.buffer).setBigUint64(0, BigInt(_hbbsRecvSeq), true); const decrypted = nacl.secretbox.open(new Uint8Array(data), nonce, _hbbsSharedKey); if (!decrypted) { console.error('[RDCrypto] hbbs decryption failed!'); return null; } return decrypted; } function isHbbsEncrypted() { return _hbbsSharedKey !== null; } function createHbbsKeyResponse(serverEphemeralPk) { if (!_hbbsBoxKeyPair) generateHbbsKeyPair(); setupHbbsEncryption(serverEphemeralPk); return [ _hbbsBoxKeyPair.publicKey, // Our ephemeral Curve25519 pk null // The shared key is derived via ECDH, no separate encrypted key needed ]; } function resetHbbs() { _hbbsBoxKeyPair = null; _hbbsSharedKey = null; _hbbsSendSeq = 0; _hbbsRecvSeq = 0; } return { generateSignKeyPair, generateBoxKeyPair, getSignPublicKey, getBoxPublicKey, getBoxSecretKey, setPeerPublicKey, verifySignedId, createKeyExchangeResponse, encrypt, decrypt, isEncrypted, reset, hashPassword, verifyServerKey, createServerKeyResponse: createHbbsKeyResponse, setupHbbsEncryption, hbbsEncrypt, hbbsDecrypt, isHbbsEncrypted, generateHbbsKeyPair, get hbbsBoxPublicKey() { return _hbbsBoxKeyPair ? _hbbsBoxKeyPair.publicKey : null; }, resetHbbs }; })();