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/
342 lines
11 KiB
JavaScript
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
|
|
};
|
|
})();
|