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