- 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
476 lines
18 KiB
Rust
476 lines
18 KiB
Rust
// 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-CTR(16字节密钥)
|
||
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>, // MAC(32字节,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 packet(packet_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 packet(AES-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_part1(5-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());
|
||
}
|
||
}
|