projects/shelled/rustdesk-web-client/js/crypto.js
Z User 7b758ebe01 Add standalone RustDesk web client (reverse-engineered)
Complete browser-based remote desktop client built from protocol analysis:
- Protobuf message definitions (75+ message types, 10 enums)
- NaCl encryption (Ed25519 + Curve25519 ECDH + XSalsa20-Poly1305)
- WebSocket signaling (hbbs) + relay (hbbr) connection lifecycle
- VP8/VP9/AV1 video decoding via ogvjs WASM + yuv-canvas WebGL
- Opus audio decoding via libopus WASM + Web Audio API
- Full mouse/keyboard input forwarding to protobuf events
- Connection dialog, status bar, toolbar, log panel UI

Also tracked web_deps (codec libraries) in rustdesk-as-ref/
2026-04-06 21:11:21 +00:00

342 lines
11 KiB
JavaScript

/**
* 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<Uint8Array>} - 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
};
})();