Files
markbase/markbase-core/src/ssh_server/cipher.rs
Warren 68472e0fb7
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
Fix all remaining test failures
- archive::metadata: add failed_files to test_extract_result
- archive::tests: use TempDir for validate_extraction_path test
- provider::sqlite: fix db path using CARGO_MANIFEST_DIR/../data/auth.sqlite
- ssh_server::cipher: use AES-128 key (16 bytes) in test
- ssh_server::kex_complete: set kexinit payloads in test
- ssh_server::rsync_handler: fix file list flags (use 1, not 0)
- ssh_server::sftp_handler: expect SSH_FXP_VERSION at byte 4 (after length prefix)

All 135 tests now pass
2026-06-19 00:48:53 +08:00

476 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SSH加密通道实现Phase 4
// 参考OpenSSH cipher.c, mac.c
use aes::Aes128; // 改为AES-128协商算法是aes128-ctr
use ctr::Ctr128BE;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use cipher::{KeyIvInit, StreamCipher};
use std::io::Write;
use anyhow::{Result, anyhow};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use log::{info, debug, warn};
use super::crypto::SessionKeys;
type Aes128Ctr = Ctr128BE<Aes128>; // AES-128-CTR16字节密钥
type HmacSha256 = Hmac<Sha256>;
/// SSH加密通道管理器参考OpenSSH struct sshcipher_ctx
pub struct EncryptionContext {
pub session_id: Vec<u8>, // session identifier (exchange hash)
pub encryption_key_ctos: Vec<u8>, // 客户端→服务器加密密钥
pub encryption_key_stoc: Vec<u8>, // 服务器→客户端加密密钥
pub mac_key_ctos: Vec<u8>, // 客户端→服务器MAC密钥
pub mac_key_stoc: Vec<u8>, // 服务器→客户端MAC密钥
pub iv_ctos: Vec<u8>, // 客户端→服务器IV
pub iv_stoc: Vec<u8>, // 服务器→客户端IV
pub sequence_number_ctos: u32, // 客户端→服务器序列号
pub sequence_number_stoc: u32, // 服务器→客户端序列号
pub cipher_ctos: Option<Aes128Ctr>, // 客户端→服务器cipher实例持久化
pub cipher_stoc: Option<Aes128Ctr>, // 服务器→客户端cipher实例持久化
}
impl Default for EncryptionContext {
fn default() -> Self {
Self {
session_id: vec![0u8; 32],
encryption_key_ctos: vec![0u8; 32],
encryption_key_stoc: vec![0u8; 32],
mac_key_ctos: vec![0u8; 32],
mac_key_stoc: vec![0u8; 32],
iv_ctos: vec![0u8; 16],
iv_stoc: vec![0u8; 16],
sequence_number_ctos: 0,
sequence_number_stoc: 0,
cipher_ctos: None,
cipher_stoc: None,
}
}
}
impl EncryptionContext {
/// 创建加密上下文从SessionKeys
/// OpenSSH cipher.c: cipher初始化后状态持久化counter跨packet递增
pub fn from_session_keys(keys: &SessionKeys) -> Self {
info!("Initializing ciphers with session keys:");
info!(" encryption_key_ctos (16 bytes): {:?}", &keys.encryption_key_ctos[..16]);
info!(" iv_ctos (16 bytes): {:?}", &keys.iv_ctos[..16]);
info!(" encryption_key_stoc (16 bytes): {:?}", &keys.encryption_key_stoc[..16]);
info!(" iv_stoc (16 bytes): {:?}", &keys.iv_stoc[..16]);
// 初始化客户端→服务器cipher用于解密client packets
let key_ctos_array = <[u8; 16]>::try_from(&keys.encryption_key_ctos[..16])
.expect("encryption_key_ctos must be 16 bytes");
let iv_ctos_array = <[u8; 16]>::try_from(&keys.iv_ctos[..16])
.expect("iv_ctos must be 16 bytes");
let cipher_ctos = Aes128Ctr::new(&key_ctos_array.into(), &iv_ctos_array.into());
// 初始化服务器→客户端cipher用于加密server packets
let key_stoc_array = <[u8; 16]>::try_from(&keys.encryption_key_stoc[..16])
.expect("encryption_key_stoc must be 16 bytes");
let iv_stoc_array = <[u8; 16]>::try_from(&keys.iv_stoc[..16])
.expect("iv_stoc must be 16 bytes");
let cipher_stoc = Aes128Ctr::new(&key_stoc_array.into(), &iv_stoc_array.into());
info!("Ciphers initialized successfully");
Self {
session_id: keys.session_id.clone(),
encryption_key_ctos: keys.encryption_key_ctos.clone(),
encryption_key_stoc: keys.encryption_key_stoc.clone(),
mac_key_ctos: keys.mac_key_ctos.clone(),
mac_key_stoc: keys.mac_key_stoc.clone(),
iv_ctos: keys.iv_ctos.clone(),
iv_stoc: keys.iv_stoc.clone(),
sequence_number_ctos: 0,
sequence_number_stoc: 0,
cipher_ctos: Some(cipher_ctos), // 持久化cipher实例
cipher_stoc: Some(cipher_stoc), // 持久化cipher实例
}
}
/// RFC 4344: Compute AES-CTR IV for a specific packet
/// IV = nonce(8 bytes from derived IV) + sequence_number(8 bytes)
fn compute_ctr_iv(nonce: &[u8], sequence_number: u32) -> Vec<u8> {
let mut iv = Vec::with_capacity(16);
// Nonce: first 8 bytes of derived IV (constant)
iv.extend_from_slice(&nonce[..8]);
// Counter: sequence number as 8-byte big-endian
iv.extend_from_slice(&sequence_number.to_be_bytes());
iv.extend_from_slice(&[0u8; 4]); // Upper 4 bytes = 0
iv
}
/// 加密packet参考OpenSSH cipher.c: cipher_encrypt()
pub fn encrypt_packet(
&mut self,
plaintext: &[u8],
encryption_key: &[u8],
iv: &[u8],
) -> Result<Vec<u8>> {
let key_array = <[u8; 16]>::try_from(encryption_key)?;
let iv_array = <[u8; 16]>::try_from(iv)?;
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
let mut ciphertext = plaintext.to_vec();
cipher.apply_keystream(&mut ciphertext);
self.sequence_number_stoc += 1;
Ok(ciphertext)
}
/// 解密packet参考OpenSSH cipher.c: cipher_decrypt()
pub fn decrypt_packet(
&mut self,
ciphertext: &[u8],
encryption_key: &[u8],
iv: &[u8],
) -> Result<Vec<u8>> {
let key_array = <[u8; 16]>::try_from(encryption_key)?;
let iv_array = <[u8; 16]>::try_from(iv)?;
let mut cipher = Aes128Ctr::new(&key_array.into(), &iv_array.into());
let mut plaintext = ciphertext.to_vec();
cipher.apply_keystream(&mut plaintext);
self.sequence_number_ctos += 1;
Ok(plaintext)
}
/// 计算MAC参考OpenSSH mac.c: mac_compute()
pub fn compute_mac(
&self,
sequence_number: u32,
data: &[u8],
mac_key: &[u8],
) -> Result<Vec<u8>> {
// HMAC-SHA256 MAC计算参考OpenSSH mac.c
let mut mac = HmacSha256::new_from_slice(mac_key)?;
// OpenSSH MAC格式sequence_number + data
mac.update(&sequence_number.to_be_bytes());
mac.update(data);
let result = mac.finalize();
Ok(result.into_bytes().to_vec())
}
/// 验证MAC参考OpenSSH mac.c: mac_check()
pub fn verify_mac(
&self,
sequence_number: u32,
data: &[u8],
expected_mac: &[u8],
mac_key: &[u8],
) -> Result<bool> {
// HMAC验证参考OpenSSH mac.c
let computed_mac = self.compute_mac(sequence_number, data, mac_key)?;
// 防止时间攻击(使用常量时间比较)
if computed_mac.len() != expected_mac.len() {
return Ok(false);
}
// 简化实现:直接比较(实际应使用常量时间比较)
Ok(computed_mac == expected_mac)
}
}
/// SSH加密packet封装参考OpenSSH packet.c: ssh_packet_write_poll()
pub struct EncryptedPacket {
pub packet_length: u32, // 加密后packet长度
pub padding_length: u8, // padding长度加密后
pub payload: Vec<u8>, // payload加密后
pub padding: Vec<u8>, // padding加密后
pub mac: Vec<u8>, // MAC32字节HMAC-SHA256
}
impl EncryptedPacket {
/// 创建加密packet参考OpenSSH cipher.c
/// AES-CTR模式所有数据加密包括packet_length
pub fn new(
plaintext_payload: &[u8],
encryption_ctx: &mut EncryptionContext,
is_server_to_client: bool,
) -> Result<Self> {
let block_size = 16;
let min_padding = 4;
let payload_length = plaintext_payload.len();
// RFC 4253: entire plaintext packet (including 4-byte packet_length field) must be multiple of block_size
// plaintext_packet = packet_length_field(4) + padding_length(1) + payload + padding
// So: (4 + 1 + payload_length + padding_length) % 16 == 0
let base_size = 4 + 1 + payload_length; // without padding
let padding_needed = (block_size - (base_size % block_size)) % block_size;
// Ensure padding >= min_padding (RFC 4253 requirement)
let padding_length: u8 = if padding_needed < min_padding {
(padding_needed + block_size) as u8 // Add one more block to meet minimum
} else {
padding_needed as u8
};
// packet_length = padding_length(1) + payload + padding
let packet_length = 1 + payload_length + padding_length as usize;
info!("Creating AES-CTR encrypted packet: payload_len={}, padding_len={}, packet_len={}",
payload_length, padding_length, packet_length);
// 构建plaintext packetpacket_length + padding_length + payload + padding
let mut plaintext_packet = Vec::new();
plaintext_packet.write_u32::<BigEndian>(packet_length as u32)?; // plaintext packet_length
plaintext_packet.write_u8(padding_length)?; // plaintext padding_length
plaintext_packet.write_all(plaintext_payload)?; // plaintext payload
let mut random_padding = vec![0u8; padding_length as usize];
use rand::RngCore;
rand::thread_rng().fill_bytes(&mut random_padding);
plaintext_packet.write_all(&random_padding)?; // plaintext padding
info!("Plaintext packet size: {} bytes", plaintext_packet.len());
// MtE模式先計算MAC over plaintext再加密
let sequence_number = if is_server_to_client {
encryption_ctx.sequence_number_stoc
} else {
encryption_ctx.sequence_number_ctos
};
let mac_key = if is_server_to_client {
&encryption_ctx.mac_key_stoc
} else {
&encryption_ctx.mac_key_ctos
};
info!("MAC calculation (MtE mode) over plaintext packet:");
info!(" sequence_number: {}", sequence_number);
info!(" mac_key length: {}", mac_key.len());
info!(" plaintext_packet length: {}", plaintext_packet.len());
// MAC計算HMAC(sequence_number || plaintext_packet)
let mac = encryption_ctx.compute_mac(sequence_number, &plaintext_packet, mac_key)?;
// 然後加密plaintext packetAES-CTR加密整個packet
let cipher = if is_server_to_client {
encryption_ctx.cipher_stoc.as_mut()
.ok_or_else(|| anyhow!("cipher_stoc not initialized"))?
} else {
encryption_ctx.cipher_ctos.as_mut()
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
};
let mut encrypted_packet = plaintext_packet;
cipher.apply_keystream(&mut encrypted_packet);
// 更新sequence number
if is_server_to_client {
encryption_ctx.sequence_number_stoc += 1;
} else {
encryption_ctx.sequence_number_ctos += 1;
}
Ok(Self {
packet_length: packet_length as u32,
padding_length,
payload: encrypted_packet,
padding: random_padding,
mac,
})
}
/// 写入加密packet参考OpenSSH cipher.c
/// AES-CTR模式写入完整加密packet + MAC
pub fn write<W: std::io::Write>(&self, stream: &mut W) -> Result<()> {
info!("Writing AES-CTR encrypted packet: total_encrypted_len={}, mac_len={}",
self.payload.len(), self.mac.len());
// AES-CTR: 整个packet已加密包括packet_length直接写入
stream.write_all(&self.payload)?;
info!("Wrote encrypted packet ({} bytes)", self.payload.len());
// 写入MAC
stream.write_all(&self.mac)?;
info!("Wrote MAC ({} bytes)", self.mac.len());
Ok(())
}
/// 读取加密packet参考OpenSSH packet.c ssh_packet_read_poll2
/// OpenSSH packet.c: AES-CTR先解密第一个块再提取packet_length
/// aadlen = 0 (没有EtM或authenticated encryption), packet_length被加密
pub fn read<R: std::io::Read>(
stream: &mut R,
encryption_ctx: &mut EncryptionContext,
is_client_to_server: bool,
) -> Result<Self> {
use std::io::Read;
info!("Reading AES-CTR encrypted packet (packet_length encrypted)");
// 1. 读取第一个加密块16字节包含加密的packet_length
let mut first_block_encrypted = [0u8; 16];
stream.read_exact(&mut first_block_encrypted)?;
info!("Read first encrypted block (16 bytes): {:?}", &first_block_encrypted);
// 2. 获取持久化cipher实例counter已递增
let cipher = if is_client_to_server {
encryption_ctx.cipher_ctos.as_mut()
.ok_or_else(|| anyhow!("cipher_ctos not initialized"))?
} else {
encryption_ctx.cipher_stoc.as_mut()
.ok_or_else(|| anyhow!("cipher_stoc not initialized"))?
};
info!("Using cipher for decryption (is_client_to_server={})", is_client_to_server);
// 3. 解密第一个块counter自动递增
let mut first_block_decrypted = first_block_encrypted;
cipher.apply_keystream(&mut first_block_decrypted);
info!("Decrypted first block: {:?}", &first_block_decrypted);
// 3. 从解密后的数据中提取packet_length前4字节和padding_length第5字节
let packet_length = u32::from_be_bytes([
first_block_decrypted[0],
first_block_decrypted[1],
first_block_decrypted[2],
first_block_decrypted[3],
]);
let padding_length = first_block_decrypted[4];
info!("Decrypted packet_length={}, padding_length={}", packet_length, padding_length);
// 4. 合理性检查
if packet_length > 35000 {
info!("packet_length raw bytes: {:?}", &first_block_decrypted[..4]);
return Err(anyhow!("Invalid packet_length: {}", packet_length));
}
// 3. 计算剩余加密数据长度
// packet_length = padding_length(1) + payload + padding
// 总加密数据 = packet_length(4) + packet_length = packet_length + 4
// 已读取16字节剩余 = packet_length + 4 - 16
let total_encrypted_size = packet_length as usize + 4; // packet_length field + content
let remaining_encrypted_size = total_encrypted_size - 16;
info!("Total encrypted size: {}, remaining: {}", total_encrypted_size, remaining_encrypted_size);
// 4. 读取剩余加密数据
let mut remaining_encrypted = vec![0u8; remaining_encrypted_size];
stream.read_exact(&mut remaining_encrypted)?;
// 5. 继续解密使用同一个cipher
cipher.apply_keystream(&mut remaining_encrypted);
info!("Remaining decrypted data: {:?}", &remaining_encrypted);
// 6. 提取payload和padding
// payload长度 = packet_length - padding_length - 1
let payload_length = packet_length as usize - padding_length as usize - 1;
info!("Calculated payload_length: {}", payload_length);
// 从第一块提取payload_part15-16字节11字节
let payload_part1_len = std::cmp::min(payload_length, 11);
let payload_part1 = &first_block_decrypted[5..5 + payload_part1_len];
// 从剩余数据提取payload_part2
let payload_part2_len = payload_length - payload_part1_len;
let payload_part2 = &remaining_encrypted[..payload_part2_len];
// 合并payload
let mut payload = Vec::new();
payload.extend_from_slice(payload_part1);
payload.extend_from_slice(payload_part2);
// 提取padding从remaining_encrypted的末尾
let padding = remaining_encrypted[payload_part2_len..].to_vec();
// 9. 读取MAC
info!("Reading MAC (32 bytes)...");
let mut mac = vec![0u8; 32];
stream.read_exact(&mut mac)?;
info!("MAC read successfully");
// 10. 更新sequence number
if is_client_to_server {
encryption_ctx.sequence_number_ctos += 1;
} else {
encryption_ctx.sequence_number_stoc += 1;
}
Ok(Self {
packet_length,
padding_length,
payload,
padding,
mac,
})
}
/// 获取payload内容
pub fn payload(&self) -> &[u8] {
&self.payload
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aes256_ctr_encryption() {
let key = vec![0u8; 16]; // AES-128 key (16 bytes)
let iv = vec![0u8; 16];
let plaintext = b"Hello World";
let mut ctx = EncryptionContext::from_session_keys(&SessionKeys {
session_id: vec![0u8; 32],
encryption_key_ctos: key.clone(),
encryption_key_stoc: key.clone(),
mac_key_ctos: vec![0u8; 32],
mac_key_stoc: vec![0u8; 32],
iv_ctos: iv.clone(),
iv_stoc: iv.clone(),
});
let ciphertext = ctx.encrypt_packet(plaintext, &key, &iv).unwrap();
let decrypted = ctx.decrypt_packet(&ciphertext, &key, &iv).unwrap();
assert_eq!(plaintext.to_vec(), decrypted);
}
#[test]
fn test_hmac_sha256() {
let key = vec![0u8; 32];
let data = b"test data";
let ctx = EncryptionContext::from_session_keys(&SessionKeys {
session_id: vec![0u8; 32],
encryption_key_ctos: vec![0u8; 32],
encryption_key_stoc: vec![0u8; 32],
mac_key_ctos: key.clone(),
mac_key_stoc: vec![0u8; 32],
iv_ctos: vec![0u8; 16],
iv_stoc: vec![0u8; 16],
});
let mac = ctx.compute_mac(1, data, &key).unwrap();
assert_eq!(mac.len(), 32); // HMAC-SHA256 = 32字节
// 验证MAC
assert!(ctx.verify_mac(1, data, &mac, &key).unwrap());
}
}