Skip to main content

futu_net/
encrypt.rs

1// AES-128 ECB 加解密
2//
3// FutuOpenD 协议使用 AES-128 ECB 模式加密 body:
4// - InitConnect 成功后,服务端返回 16 字节 AES key(hex 字符串)
5// - 后续所有请求/响应的 body 使用此 key 加解密
6// - SHA1 校验基于明文(加密前计算、解密后验证)
7//
8// 注意:AES-ECB 模式不使用 IV,相同明文始终产生相同密文。
9// 这是 FutuOpenD 协议的设计,Rust 侧需保持兼容。
10
11use futu_core::error::FutuError;
12
13/// AES-128 ECB 加密
14///
15/// key 必须是 16 字节。data 会被 PKCS7 填充到 16 字节的倍数。
16pub fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8]) -> Vec<u8> {
17    use aes::cipher::{BlockCipherEncrypt, KeyInit};
18    use std::iter;
19
20    // PKCS7 padding
21    let block_size = 16;
22    let padding_len = block_size - (data.len() % block_size);
23    let mut padded: Vec<u8> = data.to_vec();
24    padded.extend(iter::repeat_n(padding_len as u8, padding_len));
25
26    // AES-128 ECB: 逐块加密
27    let encryptor = aes::Aes128::new(&aes::cipher::Key::<aes::Aes128>::from(*key));
28    for chunk in padded.chunks_mut(block_size) {
29        let mut block = aes::cipher::Block::<aes::Aes128>::default();
30        block.copy_from_slice(chunk);
31        encryptor.encrypt_block(&mut block);
32        chunk.copy_from_slice(&block);
33    }
34
35    padded
36}
37
38/// AES-128 ECB 解密(PKCS7 padding)
39///
40/// key 必须是 16 字节。返回去除 PKCS7 padding 后的明文。
41pub fn aes_ecb_decrypt(
42    key: &[u8; 16],
43    data: &[u8],
44) -> Result<Vec<u8>, futu_core::error::FutuError> {
45    use aes::cipher::{BlockCipherDecrypt, KeyInit};
46
47    if data.is_empty() || !data.len().is_multiple_of(16) {
48        return Err(futu_core::error::FutuError::Encryption(
49            "ciphertext length must be a multiple of 16".into(),
50        ));
51    }
52
53    let block_size = 16;
54    let mut output = data.to_vec();
55    let decryptor = aes::Aes128::new(&aes::cipher::Key::<aes::Aes128>::from(*key));
56    for chunk in output.chunks_mut(block_size) {
57        let mut block = aes::cipher::Block::<aes::Aes128>::default();
58        block.copy_from_slice(chunk);
59        decryptor.decrypt_block(&mut block);
60        chunk.copy_from_slice(&block);
61    }
62
63    // Remove PKCS7 padding
64    let padding_len = output
65        .last()
66        .copied()
67        .ok_or_else(|| futu_core::error::FutuError::Encryption("empty decrypted body".into()))?
68        as usize;
69    if padding_len == 0 || padding_len > block_size {
70        return Err(futu_core::error::FutuError::Encryption(
71            "invalid PKCS7 padding".into(),
72        ));
73    }
74    if output[output.len() - padding_len..]
75        .iter()
76        .any(|&b| b as usize != padding_len)
77    {
78        return Err(futu_core::error::FutuError::Encryption(
79            "invalid PKCS7 padding bytes".into(),
80        ));
81    }
82    output.truncate(output.len() - padding_len);
83
84    Ok(output)
85}
86
87/// AES 加密单块 —— 支持 16 / 24 / 32 字节 key(对应 AES-128/192/256)。
88/// 对齐 C++ `OMCrypt_FTAES_MD5_Encrypt` 的可变 key 长度支持(auth_cryptor.cpp 里
89/// S3 是 32 字节,走 AES-256)。
90fn aes_encrypt_block_var(key: &[u8], block: &[u8]) -> Result<[u8; 16], FutuError> {
91    use aes::cipher::{BlockCipherEncrypt, KeyInit};
92    let mut out = [0u8; 16];
93    out.copy_from_slice(block);
94    let mut cipher_block = aes::cipher::Block::<aes::Aes128>::from(out);
95    match key.len() {
96        16 => {
97            aes::Aes128::new_from_slice(key)
98                .map_err(|_| unsupported_aes_var_key_len("aes_encrypt_block_var", key.len()))?
99                .encrypt_block(&mut cipher_block);
100        }
101        24 => {
102            aes::Aes192::new_from_slice(key)
103                .map_err(|_| unsupported_aes_var_key_len("aes_encrypt_block_var", key.len()))?
104                .encrypt_block(&mut cipher_block);
105        }
106        32 => {
107            aes::Aes256::new_from_slice(key)
108                .map_err(|_| unsupported_aes_var_key_len("aes_encrypt_block_var", key.len()))?
109                .encrypt_block(&mut cipher_block);
110        }
111        _ => {
112            return Err(unsupported_aes_var_key_len(
113                "aes_encrypt_block_var",
114                key.len(),
115            ));
116        }
117    }
118    Ok(cipher_block.into())
119}
120
121/// AES 解密单块 —— 可变 key 长度(16/24/32 字节)
122fn aes_decrypt_block_var(key: &[u8], block: &[u8]) -> Result<[u8; 16], FutuError> {
123    use aes::cipher::{BlockCipherDecrypt, KeyInit};
124    let mut out = [0u8; 16];
125    out.copy_from_slice(block);
126    let mut cipher_block = aes::cipher::Block::<aes::Aes128>::from(out);
127    match key.len() {
128        16 => {
129            aes::Aes128::new_from_slice(key)
130                .map_err(|_| unsupported_aes_var_key_len("aes_decrypt_block_var", key.len()))?
131                .decrypt_block(&mut cipher_block);
132        }
133        24 => {
134            aes::Aes192::new_from_slice(key)
135                .map_err(|_| unsupported_aes_var_key_len("aes_decrypt_block_var", key.len()))?
136                .decrypt_block(&mut cipher_block);
137        }
138        32 => {
139            aes::Aes256::new_from_slice(key)
140                .map_err(|_| unsupported_aes_var_key_len("aes_decrypt_block_var", key.len()))?
141                .decrypt_block(&mut cipher_block);
142        }
143        _ => {
144            return Err(unsupported_aes_var_key_len(
145                "aes_decrypt_block_var",
146                key.len(),
147            ));
148        }
149    }
150    Ok(cipher_block.into())
151}
152
153fn unsupported_aes_var_key_len(context: &str, len: usize) -> FutuError {
154    FutuError::Encryption(format!("{context}: unsupported key length {len}"))
155}
156
157fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
158    if left.len() != right.len() {
159        return false;
160    }
161
162    let mut diff = 0u8;
163    for (&l, &r) in left.iter().zip(right.iter()) {
164        diff |= l ^ r;
165    }
166    diff == 0
167}
168
169// ===== AES-128 CBC + MD5 校验 (自定义格式) =====
170// 用于 tgtgt 加密和内部通信的 OMCrypt_AES_Encrypt/Decrypt
171// 格式: [CBC加密的数据块] + [padding块(标记最后块大小)] + [MD5(明文所有16字节块)]
172
173/// AES-128 CBC + MD5 加密 (对应 C++ om_aes_encrypt_cbc_md5)
174pub fn aes_cbc_md5_encrypt(key: &[u8; 16], data: &[u8]) -> Result<Vec<u8>, FutuError> {
175    aes_cbc_md5_encrypt_var(key, data)
176}
177
178/// AES-128/192/256 CBC + MD5 加密(可变 key 长度版,对齐 C++
179/// `OMCrypt_FTAES_MD5_Encrypt` 接受任意 16/24/32 字节 key)
180///
181/// 用途:登录流程的 `CreateNewTgtgt` 在 salt32 存在时用 S3(32 字节)作 key
182/// 做 AES-256 加密 tgtgt。
183pub fn aes_cbc_md5_encrypt_var(key: &[u8], data: &[u8]) -> Result<Vec<u8>, FutuError> {
184    let input_len = data.len();
185    let modv = input_len % 16;
186    let last_block_size = if modv == 0 { 0u8 } else { modv as u8 };
187
188    let data_blocks = if modv == 0 {
189        input_len
190    } else {
191        input_len - modv + 16
192    };
193    let total_size = data_blocks + 16 + 16;
194    let mut output = vec![0u8; total_size];
195
196    let mut md5_ctx = md5::Context::new();
197    let mut encrypt_pos = 0usize;
198    let aligned_end = input_len - modv;
199
200    while encrypt_pos < aligned_end {
201        let block = &data[encrypt_pos..encrypt_pos + 16];
202        md5_ctx.consume(block);
203        if encrypt_pos == 0 {
204            let enc = aes_encrypt_block_var(key, block)?;
205            output[..16].copy_from_slice(&enc);
206        } else {
207            let mut xor_block = [0u8; 16];
208            for i in 0..16 {
209                xor_block[i] = output[encrypt_pos - 16 + i] ^ block[i];
210            }
211            let enc = aes_encrypt_block_var(key, &xor_block)?;
212            output[encrypt_pos..encrypt_pos + 16].copy_from_slice(&enc);
213        }
214        encrypt_pos += 16;
215    }
216
217    if modv != 0 {
218        let mut tmp = [0u8; 16];
219        tmp[..last_block_size as usize].copy_from_slice(&data[encrypt_pos..encrypt_pos + modv]);
220        md5_ctx.consume(tmp);
221        if encrypt_pos > 0 {
222            for i in 0..16 {
223                tmp[i] ^= output[encrypt_pos - 16 + i];
224            }
225        }
226        let enc = aes_encrypt_block_var(key, &tmp)?;
227        output[encrypt_pos..encrypt_pos + 16].copy_from_slice(&enc);
228        encrypt_pos += 16;
229    }
230
231    let mut padding = [0u8; 16];
232    padding[15] = last_block_size;
233    let enc_pad = aes_encrypt_block_var(key, &padding)?;
234    output[encrypt_pos..encrypt_pos + 16].copy_from_slice(&enc_pad);
235    encrypt_pos += 16;
236
237    let md5_hash = md5_ctx.finalize();
238    output[encrypt_pos..encrypt_pos + 16].copy_from_slice(&md5_hash.0);
239
240    Ok(output)
241}
242
243/// AES-128/192/256 CBC + MD5 解密(可变 key 长度版)
244pub fn aes_cbc_md5_decrypt_var(
245    key: &[u8],
246    data: &[u8],
247) -> std::result::Result<Vec<u8>, futu_core::error::FutuError> {
248    let input_len = data.len();
249    if input_len < 32 || !input_len.is_multiple_of(16) {
250        return Err(futu_core::error::FutuError::Encryption(format!(
251            "cbc_md5_var: invalid ciphertext length {input_len}"
252        )));
253    }
254
255    let padding_block = aes_decrypt_block_var(key, &data[input_len - 32..input_len - 16])?;
256    let last_block_size = padding_block[15] as usize;
257    if last_block_size > 15 {
258        return Err(futu_core::error::FutuError::Encryption(format!(
259            "cbc_md5_var: last_block_size {last_block_size} > 15"
260        )));
261    }
262
263    let data_blocks_end = input_len - 32;
264    let plaintext_len = if last_block_size == 0 {
265        data_blocks_end
266    } else {
267        data_blocks_end - 16 + last_block_size
268    };
269
270    let mut output = vec![0u8; plaintext_len];
271    let mut md5_ctx = md5::Context::new();
272    let mut pos = 0usize;
273    while pos < data_blocks_end {
274        let block = &data[pos..pos + 16];
275        let decrypted = aes_decrypt_block_var(key, block)?;
276        let mut plain_block = [0u8; 16];
277        if pos == 0 {
278            plain_block.copy_from_slice(&decrypted);
279        } else {
280            for i in 0..16 {
281                plain_block[i] = decrypted[i] ^ data[pos - 16 + i];
282            }
283        }
284        md5_ctx.consume(plain_block);
285
286        let is_last_block = pos + 16 == data_blocks_end;
287        let effective_len = if is_last_block && last_block_size != 0 {
288            last_block_size
289        } else {
290            16
291        };
292        output[pos..pos + effective_len].copy_from_slice(&plain_block[..effective_len]);
293        pos += 16;
294    }
295
296    let computed_md5 = md5_ctx.finalize();
297    if !constant_time_eq(&computed_md5.0, &data[input_len - 16..]) {
298        return Err(futu_core::error::FutuError::Encryption(
299            "cbc_md5_var: MD5 checksum mismatch".into(),
300        ));
301    }
302
303    Ok(output)
304}
305
306/// AES-128 CBC + MD5 解密 (对应 C++ om_aes_decrypt_cbc_md5)
307pub fn aes_cbc_md5_decrypt(
308    key: &[u8; 16],
309    data: &[u8],
310) -> std::result::Result<Vec<u8>, futu_core::error::FutuError> {
311    aes_cbc_md5_decrypt_var(key, data)
312}
313
314// ===== RSA 加解密 =====
315// FutuOpenD 使用 RSA PKCS#1 v1.5 加解密。
316// 客户端和服务端共享同一个 RSA 私钥文件(PEM 格式)。
317// 从私钥中提取公钥用于加密,私钥用于解密。
318
319/// 从 PEM 格式加载 RSA 私钥(支持 PKCS#8 和 PKCS#1 格式)
320fn load_rsa_private_key(
321    pem_private_key: &str,
322) -> Result<rsa::RsaPrivateKey, futu_core::error::FutuError> {
323    use rsa::pkcs8::DecodePrivateKey;
324
325    rsa::RsaPrivateKey::from_pkcs8_pem(pem_private_key)
326        .or_else(|_| {
327            use rsa::pkcs1::DecodeRsaPrivateKey;
328            rsa::RsaPrivateKey::from_pkcs1_pem(pem_private_key)
329        })
330        .map_err(|e| {
331            futu_core::error::FutuError::Encryption(format!("invalid RSA private key: {e}"))
332        })
333}
334
335/// Validate RSA private-key material with the same parser used by every
336/// encrypt/decrypt operation.
337///
338/// Both PKCS#8 and PKCS#1 PEM are supported. Parsing constructs an
339/// [`rsa::RsaPrivateKey`], which validates its key components before returning.
340pub fn validate_rsa_private_key(pem_private_key: &str) -> Result<(), futu_core::error::FutuError> {
341    load_rsa_private_key(pem_private_key).map(drop)
342}
343
344/// 使用 RSA 公钥加密(从私钥中提取公钥)
345///
346/// `pem_private_key`: PEM 格式的 RSA 私钥
347pub fn rsa_public_encrypt(
348    pem_private_key: &str,
349    data: &[u8],
350) -> Result<Vec<u8>, futu_core::error::FutuError> {
351    use rsa::Pkcs1v15Encrypt;
352
353    let private_key = load_rsa_private_key(pem_private_key)?;
354    let public_key = rsa::RsaPublicKey::from(&private_key);
355    let mut rng = rsa::rand_core::OsRng;
356
357    public_key
358        .encrypt(&mut rng, Pkcs1v15Encrypt, data)
359        .map_err(|e| futu_core::error::FutuError::Encryption(format!("RSA encrypt failed: {e}")))
360}
361
362/// 使用 RSA 私钥解密
363pub fn rsa_private_decrypt(
364    pem_private_key: &str,
365    data: &[u8],
366) -> Result<Vec<u8>, futu_core::error::FutuError> {
367    use rsa::Pkcs1v15Encrypt;
368
369    let private_key = load_rsa_private_key(pem_private_key)?;
370
371    private_key
372        .decrypt(Pkcs1v15Encrypt, data)
373        .map_err(|e| futu_core::error::FutuError::Encryption(format!("RSA decrypt failed: {e}")))
374}
375
376/// 使用 RSA 公钥分块加密(支持任意长度数据)
377///
378/// RSA 1024-bit: 每块最多 117 字节明文 → 128 字节密文
379/// RSA 2048-bit: 每块最多 245 字节明文 → 256 字节密文
380/// 对应 C++ FutuOpenD 的 RSA 分块加密逻辑
381pub fn rsa_public_encrypt_blocks(
382    pem_private_key: &str,
383    data: &[u8],
384) -> Result<Vec<u8>, futu_core::error::FutuError> {
385    use rsa::Pkcs1v15Encrypt;
386    use rsa::traits::PublicKeyParts;
387
388    let private_key = load_rsa_private_key(pem_private_key)?;
389    let public_key = rsa::RsaPublicKey::from(&private_key);
390
391    // PKCS1v15 padding 需要 11 字节开销
392    let key_len = public_key.size();
393    let max_block = key_len - 11;
394
395    let mut result = Vec::with_capacity((data.len() / max_block + 1) * key_len);
396    let mut rng = rsa::rand_core::OsRng;
397
398    for chunk in data.chunks(max_block) {
399        let encrypted = public_key
400            .encrypt(&mut rng, Pkcs1v15Encrypt, chunk)
401            .map_err(|e| {
402                futu_core::error::FutuError::Encryption(format!("RSA block encrypt failed: {e}"))
403            })?;
404        result.extend_from_slice(&encrypted);
405    }
406
407    Ok(result)
408}
409
410/// 使用 RSA 私钥分块解密(支持任意长度数据)
411///
412/// 密文按 key_size(128 或 256 字节)分块解密
413pub fn rsa_private_decrypt_blocks(
414    pem_private_key: &str,
415    data: &[u8],
416) -> Result<Vec<u8>, futu_core::error::FutuError> {
417    use rsa::Pkcs1v15Encrypt;
418    use rsa::traits::PublicKeyParts;
419
420    let private_key = load_rsa_private_key(pem_private_key)?;
421    let key_len = private_key.size();
422
423    if !data.len().is_multiple_of(key_len) {
424        return Err(futu_core::error::FutuError::Encryption(format!(
425            "RSA ciphertext length {} is not a multiple of key size {}",
426            data.len(),
427            key_len
428        )));
429    }
430
431    let mut result = Vec::with_capacity(data.len());
432
433    for chunk in data.chunks(key_len) {
434        let decrypted = private_key.decrypt(Pkcs1v15Encrypt, chunk).map_err(|e| {
435            futu_core::error::FutuError::Encryption(format!("RSA block decrypt failed: {e}"))
436        })?;
437        result.extend_from_slice(&decrypted);
438    }
439
440    Ok(result)
441}
442
443#[cfg(test)]
444mod tests;