Files
One-KVM/docs/report/rustdesk/06-encryption.md
mofeng-git a8a3b6c66b feat: 添加 RustDesk 协议支持和项目文档
- 新增 RustDesk 模块,支持与 RustDesk 客户端连接
  - 实现会合服务器协议和 P2P 连接
  - 支持 NaCl 加密和密钥交换
  - 添加视频帧和 HID 事件适配器
- 添加 Protobuf 协议定义 (message.proto, rendezvous.proto)
- 新增完整项目文档
  - 各功能模块文档 (video, hid, msd, otg, webrtc 等)
  - hwcodec 和 RustDesk 协议技术报告
  - 系统架构和技术栈文档
- 更新 Web 前端 RustDesk 配置界面和 API
2025-12-31 18:59:52 +08:00

10 KiB

加密机制

概述

RustDesk 使用 libsodium (sodiumoxide) 库实现端到端加密,主要包含:

  • Ed25519: 用于身份签名和验证
  • X25519: 用于密钥交换
  • ChaCha20-Poly1305: 用于对称加密

密钥类型

1. 身份密钥对 (Ed25519)

用于 Peer 身份认证和签名:

// 生成密钥对
use sodiumoxide::crypto::sign;
let (pk, sk) = sign::gen_keypair();
// pk: sign::PublicKey (32 bytes)
// sk: sign::SecretKey (64 bytes)

2. 服务器签名密钥

Rendezvous Server 可以配置签名密钥,用于签名 Peer 公钥:

// rustdesk-server/src/rendezvous_server.rs:1185-1210
fn get_server_sk(key: &str) -> (String, Option<sign::SecretKey>) {
    let mut out_sk = None;
    let mut key = key.to_owned();

    // 如果是 base64 编码的私钥
    if let Ok(sk) = base64::decode(&key) {
        if sk.len() == sign::SECRETKEYBYTES {
            log::info!("The key is a crypto private key");
            key = base64::encode(&sk[(sign::SECRETKEYBYTES / 2)..]);  // 公钥部分
            let mut tmp = [0u8; sign::SECRETKEYBYTES];
            tmp[..].copy_from_slice(&sk);
            out_sk = Some(sign::SecretKey(tmp));
        }
    }

    // 如果是占位符,生成新密钥对
    if key.is_empty() || key == "-" || key == "_" {
        let (pk, sk) = crate::common::gen_sk(0);
        out_sk = sk;
        if !key.is_empty() {
            key = pk;
        }
    }

    if !key.is_empty() {
        log::info!("Key: {}", key);
    }
    (key, out_sk)
}

3. 会话密钥 (X25519 + ChaCha20)

用于客户端之间的加密通信:

// hbb_common/src/tcp.rs:27-28
#[derive(Clone)]
pub struct Encrypt(pub Key, pub u64, pub u64);
// Key: secretbox::Key (32 bytes)
// u64: 发送计数器
// u64: 接收计数器

密钥交换流程

1. 身份验证

客户端首先交换签名的身份:

message IdPk {
  string id = 1;   // Peer ID
  bytes pk = 2;    // Ed25519 公钥
}

message SignedId {
  bytes id = 1;    // 签名的 IdPk (by server or self)
}

2. X25519 密钥交换

使用 X25519 ECDH 生成共享密钥:

// 生成临时密钥对
use sodiumoxide::crypto::box_;
let (our_pk, our_sk) = box_::gen_keypair();

// 计算共享密钥
let shared_secret = box_::curve25519xsalsa20poly1305::scalarmult(&our_sk, &their_pk);

// 派生对称密钥
let symmetric_key = secretbox::Key::from_slice(&shared_secret[..32]).unwrap();

3. 对称密钥消息

message PublicKey {
  bytes asymmetric_value = 1;  // X25519 公钥
  bytes symmetric_value = 2;   // 加密的对称密钥(用于额外安全)
}

会话加密

加密实现

// hbb_common/src/tcp.rs
impl Encrypt {
    pub fn new(key: Key) -> Self {
        Self(key, 0, 0)  // 初始化计数器为 0
    }

    // 加密
    pub fn enc(&mut self, data: &[u8]) -> Vec<u8> {
        self.1 += 1;  // 递增发送计数器
        let nonce = self.get_nonce(self.1);
        let encrypted = secretbox::seal(data, &nonce, &self.0);

        // 格式: nonce (8 bytes) + encrypted data
        let mut result = Vec::with_capacity(8 + encrypted.len());
        result.extend_from_slice(&self.1.to_le_bytes());
        result.extend_from_slice(&encrypted);
        result
    }

    // 解密
    pub fn dec(&mut self, data: &mut BytesMut) -> io::Result<()> {
        if data.len() < 8 + secretbox::MACBYTES {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "too short"));
        }

        // 提取 nonce
        let counter = u64::from_le_bytes(data[..8].try_into().unwrap());

        // 防重放攻击检查
        if counter <= self.2 {
            return Err(io::Error::new(io::ErrorKind::InvalidData, "replay attack"));
        }
        self.2 = counter;

        let nonce = self.get_nonce(counter);
        let plaintext = secretbox::open(&data[8..], &nonce, &self.0)
            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "decrypt failed"))?;

        data.clear();
        data.extend_from_slice(&plaintext);
        Ok(())
    }

    fn get_nonce(&self, counter: u64) -> Nonce {
        let mut nonce = [0u8; 24];
        nonce[..8].copy_from_slice(&counter.to_le_bytes());
        Nonce(nonce)
    }
}

消息格式

加密后的消息结构:

┌──────────────────┬─────────────────────────────────────────┐
│   Counter (8B)   │   Encrypted Data + MAC (N+16 bytes)     │
└──────────────────┴─────────────────────────────────────────┘

密码验证

挑战-响应机制

被控端生成随机盐和挑战,控制端计算哈希响应:

message Hash {
  string salt = 1;       // 随机盐
  string challenge = 2;  // 随机挑战
}

密码处理

// 客户端计算密码哈希
fn get_password_hash(password: &str, salt: &str) -> Vec<u8> {
    let mut hasher = Sha256::new();
    hasher.update(password.as_bytes());
    hasher.update(salt.as_bytes());
    hasher.finalize().to_vec()
}

// 发送加密的密码(使用对称密钥加密)
fn encrypt_password(password_hash: &[u8], symmetric_key: &Key) -> Vec<u8> {
    secretbox::seal(password_hash, &nonce, symmetric_key)
}

服务器公钥验证

签名验证

如果 Rendezvous Server 配置了密钥,会签名 Peer 公钥:

// 服务器签名 IdPk
let signed_id_pk = sign::sign(
    &IdPk { id, pk, ..Default::default() }
        .write_to_bytes()?,
    &server_sk,
);

// 客户端验证
fn verify_server_signature(signed_pk: &[u8], server_pk: &sign::PublicKey) -> Option<IdPk> {
    if let Ok(verified) = sign::verify(signed_pk, server_pk) {
        return IdPk::parse_from_bytes(&verified).ok();
    }
    None
}

客户端获取服务器公钥

pub async fn get_rs_pk(id: &str) -> ResultType<(String, sign::PublicKey)> {
    // 从配置或 Rendezvous Server 获取公钥
    let key = Config::get_option("key");
    if !key.is_empty() {
        if let Ok(pk) = base64::decode(&key) {
            if pk.len() == sign::PUBLICKEYBYTES {
                return Ok((key, sign::PublicKey::from_slice(&pk).unwrap()));
            }
        }
    }
    // ... 从服务器获取
}

TCP 连接加密

安全 TCP 握手

// rustdesk/src/common.rs
pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> {
    // 1. 生成临时 X25519 密钥对
    let (our_pk, our_sk) = box_::gen_keypair();

    // 2. 发送我们的公钥
    let mut msg = Message::new();
    msg.set_public_key(PublicKey {
        asymmetric_value: our_pk.0.to_vec().into(),
        ..Default::default()
    });
    conn.send(&msg).await?;

    // 3. 接收对方公钥
    let msg = conn.next_timeout(CONNECT_TIMEOUT).await?
        .ok_or_else(|| anyhow!("timeout"))?;
    let their_pk = msg.get_public_key();

    // 4. 计算共享密钥
    let shared = box_::curve25519xsalsa20poly1305::scalarmult(
        &our_sk,
        &box_::PublicKey::from_slice(&their_pk.asymmetric_value)?,
    );

    // 5. 设置加密
    conn.set_key(secretbox::Key::from_slice(&shared[..32]).unwrap());
    Ok(())
}

安全特性

1. 前向保密

每个会话使用临时密钥对,即使长期密钥泄露,历史会话仍然安全。

2. 重放攻击防护

使用递增计数器作为 nonce 的一部分,拒绝旧的或重复的消息。

3. 中间人攻击防护

  • 服务器签名 Peer 公钥
  • 可配置服务器公钥验证

4. 密码暴力破解防护

  • 使用盐和多次哈希
  • 服务器端限流

加密算法参数

算法 密钥大小 Nonce 大小 MAC 大小
Ed25519 64 bytes (private), 32 bytes (public) N/A 64 bytes
X25519 32 bytes N/A N/A
ChaCha20-Poly1305 32 bytes 24 bytes 16 bytes

密钥生命周期

┌─────────────────────────────────────────────────────────────┐
│                     长期密钥 (Ed25519)                        │
│  ┌─────────────────┐                                        │
│  │ 设备首次启动时生成 │                                        │
│  │ 存储在配置文件中  │                                        │
│  └─────────────────┘                                        │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                     会话密钥 (X25519)                        │
│  ┌─────────────────┐    ┌─────────────────┐                 │
│  │ 每次连接时生成    │───►│ 用于密钥协商     │                 │
│  │ 临时密钥对       │    │ 派生对称密钥     │                 │
│  └─────────────────┘    └─────────────────┘                 │
│                              │                              │
│                              ▼                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              对称密钥 (ChaCha20-Poly1305)            │   │
│  │              用于会话中的所有消息加密                   │   │
│  │              会话结束时销毁                            │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘