hmac and ad

This commit is contained in:
2026-03-01 19:27:06 +07:00
parent 322dc5b6b0
commit b6381b8b7b
11 changed files with 108 additions and 58 deletions
+4 -4
View File
@@ -76,8 +76,8 @@ impl AeadPacker for ChaChaCipher {
let current_counter = self.encrypt_state.counter; let current_counter = self.encrypt_state.counter;
let nonce = self.encrypt_state.next_nonce(); let nonce = self.encrypt_state.next_nonce();
let data_len = data.len(); let data_len = data.len();
// maybe ad should be stream id
match self.encrypt_cipher.encrypt_in_place(&nonce, &[], data) { match self.encrypt_cipher.encrypt_in_place(&nonce, &nonce, data) {
Ok(_) => { Ok(_) => {
tracing::trace!( tracing::trace!(
counter = current_counter, counter = current_counter,
@@ -104,8 +104,8 @@ impl AeadPacker for ChaChaCipher {
let current_counter = self.decrypt_state.counter; let current_counter = self.decrypt_state.counter;
let nonce = self.decrypt_state.next_nonce(); let nonce = self.decrypt_state.next_nonce();
let data_len = data.len(); let data_len = data.len();
// maybe ad should be stream id
match self.decrypt_cipher.decrypt_in_place(&nonce, &[], data) { match self.decrypt_cipher.decrypt_in_place(&nonce, &nonce, data) {
Ok(_) => { Ok(_) => {
tracing::trace!( tracing::trace!(
counter = current_counter, counter = current_counter,
-25
View File
@@ -1,25 +0,0 @@
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
pub fn generate_auth_tag(secret: &[u8]) -> [u8; 16] {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let minute_step = (now / 60).to_be_bytes();
// Создаем HMAC-Sha256 с твоим секретным ключом
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC can take key of any size");
// Подмешиваем туда текущую минуту
mac.update(&minute_step);
let result = mac.finalize().into_bytes();
// Берем первые 16 байт от 32-байтного хеша SHA256
let mut tag = [0u8; 16];
tag.copy_from_slice(&result[..16]);
tag
}
-1
View File
@@ -2,6 +2,5 @@ pub mod aead;
pub mod chacha; pub mod chacha;
mod ecdh; mod ecdh;
mod hkdf; mod hkdf;
mod hmac;
mod salt_pair; mod salt_pair;
pub mod session; pub mod session;
+53
View File
@@ -5,9 +5,15 @@ use crate::{
tlseng::extension::ExtensionStack, tlseng::extension::ExtensionStack,
}; };
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
pub struct SessionKeys { pub struct SessionKeys {
pub salt: SaltPair, pub salt: SaltPair,
pub ecdh: ECDH, pub ecdh: ECDH,
pub auth_key: [u8; 32],
} }
impl SessionKeys { impl SessionKeys {
@@ -15,6 +21,7 @@ impl SessionKeys {
Self { Self {
salt: SaltPair::new(is_initiator), salt: SaltPair::new(is_initiator),
ecdh: ECDH::new(), ecdh: ECDH::new(),
auth_key: [0u8; 32],
} }
} }
@@ -112,6 +119,8 @@ impl SessionKeys {
let s_key = HKDF::expand_key::<32>(&hkdf, b"server_aead").map_err(|e| e.to_string())?; let s_key = HKDF::expand_key::<32>(&hkdf, b"server_aead").map_err(|e| e.to_string())?;
let s_iv = HKDF::expand_key::<12>(&hkdf, b"server_iv").map_err(|e| e.to_string())?; let s_iv = HKDF::expand_key::<12>(&hkdf, b"server_iv").map_err(|e| e.to_string())?;
let auth_secret = HKDF::expand_key::<32>(&hkdf, b"auth_key").map_err(|e| e.to_string())?;
self.auth_key = auth_secret;
tracing::info!( tracing::info!(
client_key_short = %hex::encode(&c_key[..4]), client_key_short = %hex::encode(&c_key[..4]),
server_key_short = %hex::encode(&s_key[..4]), server_key_short = %hex::encode(&s_key[..4]),
@@ -125,4 +134,48 @@ impl SessionKeys {
Ok((c_key, c_iv, s_key, s_iv)) // Клиент пишет своим, читает серверным Ok((c_key, c_iv, s_key, s_iv)) // Клиент пишет своим, читает серверным
} }
} }
fn compute_tag(secret: &[u8], step: u64) -> [u8; 16] {
let mut mac = HmacSha256::new_from_slice(secret).expect("HMAC error");
mac.update(&step.to_be_bytes());
let result = mac.finalize().into_bytes();
let mut tag = [0u8; 16];
tag.copy_from_slice(&result[..16]);
tag
}
pub fn generate_auth_tag(&self) -> [u8; 16] {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
// Генерируем на основе текущей минуты
Self::compute_tag(&self.auth_key, now / 60)
}
pub fn verify_auth_tag(&self, received_tag: &[u8; 16]) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
let current_step = now / 60;
// 1. Проверяем текущую минуту (самый вероятный случай)
if &Self::compute_tag(&self.auth_key, current_step) == received_tag {
return true;
}
// 2. Проверяем предыдущую минуту (на случай стыка минут или задержки сети)
if &Self::compute_tag(&self.auth_key, current_step - 1) == received_tag {
tracing::debug!("Auth tag valid (matched previous minute window)");
return true;
}
// Если ни один не подошел — тег невалиден
false
}
// Вспомогательная функция для генерации конкретного тега
} }
+1 -1
View File
@@ -7,7 +7,7 @@ pub fn logger_init() {
.with_line_number(true); // Показывать строку кода (очень полезно для дебага) .with_line_number(true); // Показывать строку кода (очень полезно для дебага)
let filter_layer = EnvFilter::try_from_default_env() let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info")) // По умолчанию уровень info .or_else(|_| EnvFilter::try_new("trace")) // По умолчанию уровень info
.unwrap(); .unwrap();
tracing_subscriber::registry() tracing_subscriber::registry()
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::tlseng::extension::ExtensionStack;
use crate::tlseng::handshake::{ClientHello, HelloHeader, ServerHello}; use crate::tlseng::handshake::{ClientHello, HelloHeader, ServerHello};
use crate::tlseng::profile::BrowserProfile; use crate::tlseng::profile::BrowserProfile;
use crate::tlseng::tls_record::TlsRecord; use crate::tlseng::tls_record::TlsRecord;
use crate::tlseng::types::{ContentType, HelloType, ProtocolVersion}; use crate::tlseng::types::{ContentType, HelloType};
use crate::tlseng::ApplicationData; use crate::tlseng::ApplicationData;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
+39 -12
View File
@@ -119,6 +119,14 @@ impl Codec {
) -> Result<Bytes, TlsError> { ) -> Result<Bytes, TlsError> {
let padding = Padding::generate_padding(); let padding = Padding::generate_padding();
let tag = self.session_keys.generate_auth_tag();
tracing::debug!(
step = %(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() / 60),
auth_key_hash = %hex::encode(&self.session_keys.auth_key[..4]),
generated_tag = %hex::encode(&tag[..4]),
"OUTBOUND: Generated auth tag"
);
let header = FrameHeader { let header = FrameHeader {
auth_tag: [0u8; 16], auth_tag: [0u8; 16],
stream_id, stream_id,
@@ -132,8 +140,7 @@ impl Codec {
payload, payload,
padding: padding.data, padding: padding.data,
}; };
let mut frame_bytes = frame.into_bytes(&tag);
let mut frame_bytes = frame.into_bytes();
// ВАЖНО: вызываем шифрование ОДИН РАЗ. // ВАЖНО: вызываем шифрование ОДИН РАЗ.
// Метод encrypt возвращает Result<Bytes, chacha20poly1305::Error> // Метод encrypt возвращает Result<Bytes, chacha20poly1305::Error>
@@ -161,20 +168,17 @@ impl Codec {
} }
pub fn inbound(&mut self, buffer: &mut BytesMut) -> Result<Option<Frame>, TlsError> { pub fn inbound(&mut self, buffer: &mut BytesMut) -> Result<Option<Frame>, TlsError> {
// 1. Сначала проверяем, нет ли уже готового фрейма в staging с прошлого раза // 1. Проверка старых данных
if !self.staging.is_empty() { if !self.staging.is_empty() {
if let Some(frame) = self.try_parse_frame()? { if let Some(frame) = self.try_parse_frame()? {
return Ok(Some(frame)); return Ok(Some(frame));
} }
} }
// 2. Распаковываем ВСЕ доступные TLS-рекорды из сетевого буфера // 2. Цикл обработки новых рекордов
while let Some(app_data) = TlsBridge::unpack_app_data(buffer)? { while let Some(app_data) = TlsBridge::unpack_app_data(buffer)? {
// Берем Bytes напрямую (app_data.payload — это уже Bytes)
let mut data_to_decrypt = BytesMut::from(app_data.payload); let mut data_to_decrypt = BytesMut::from(app_data.payload);
// Дешифруем "на месте" (In-place decryption)
// Твоя библиотека ChaCha скорее всего поддерживает дешифровку прямо в том же буфере
let decrypted = self.crypto.decrypt(&mut data_to_decrypt).map_err(|_| { let decrypted = self.crypto.decrypt(&mut data_to_decrypt).map_err(|_| {
TlsError::new( TlsError::new(
ErrorStage::Tls("Decr error"), ErrorStage::Tls("Decr error"),
@@ -183,11 +187,35 @@ impl Codec {
) )
})?; })?;
// ВАЖНО: Вместо extend_from_slice (копирование), используем split_off/unsplit или просто Bytes // --- КРИТИЧЕСКАЯ ПРОВЕРКА ТЕГА ---
// Если staging — это BytesMut, используй put или reserve if decrypted.len() < 16 {
self.staging.extend_from_slice(&decrypted); // Увы, BytesMut требует копирования для конкатенации return Err(TlsError::new(
ErrorStage::Tls("Packet too short for auth"),
ErrorAction::Drop,
Bytes::new(),
));
}
let mut received_tag = [0u8; 16];
received_tag.copy_from_slice(&decrypted[..16]);
// Используем метод verify_auth_tag, который мы обсуждали ранее
if !self.session_keys.verify_auth_tag(&received_tag) {
tracing::error!(
expected_hash = %hex::encode(&self.session_keys.auth_key[..4]),
received = %hex::encode(&received_tag[..4]),
"AUTH MISMATCH: Potential replay or MITM attack. Dropping connection."
);
return Err(TlsError::new(
ErrorStage::Tls("Auth tag mismatch"),
ErrorAction::Drop, // Убиваем соединение
Bytes::new(),
));
}
// ---------------------------------
self.staging.extend_from_slice(&decrypted);
// НО! Мы можем попытаться распарсить фрейм сразу после добавления каждого рекорда
if let Some(frame) = self.try_parse_frame()? { if let Some(frame) = self.try_parse_frame()? {
return Ok(Some(frame)); return Ok(Some(frame));
} }
@@ -195,7 +223,6 @@ impl Codec {
Ok(None) Ok(None)
} }
// Выносим парсинг в отдельный метод, чтобы не дублировать код // Выносим парсинг в отдельный метод, чтобы не дублировать код
fn try_parse_frame(&mut self) -> Result<Option<Frame>, TlsError> { fn try_parse_frame(&mut self) -> Result<Option<Frame>, TlsError> {
match Frame::parse(&mut self.staging) { match Frame::parse(&mut self.staging) {
+2 -3
View File
@@ -35,13 +35,12 @@ pub const FRAME_HEADER_SIZE: u16 =
AUTH_TAG_SIZE + STREAM_ID_SIZE + FRAME_TYPE_SIZE + PAYLOAD_LEN_SIZE + PADDING_LEN_SIZE; AUTH_TAG_SIZE + STREAM_ID_SIZE + FRAME_TYPE_SIZE + PAYLOAD_LEN_SIZE + PADDING_LEN_SIZE;
impl Frame { impl Frame {
pub fn into_bytes(self) -> BytesMut { pub fn into_bytes(self, auth_key: &[u8; 16]) -> BytesMut {
let updated_padding = Padding::generate_padding(); let updated_padding = Padding::generate_padding();
let total_size = FRAME_HEADER_SIZE as usize + self.payload.len() + self.padding.len(); let total_size = FRAME_HEADER_SIZE as usize + self.payload.len() + self.padding.len();
let mut buf = BytesMut::with_capacity(total_size); let mut buf = BytesMut::with_capacity(total_size);
let hmac = [0; 16]; //generate_auth_tag(&[0; 16]);
buf.put_slice(&hmac); buf.put_slice(auth_key);
buf.put_u32(self.header.stream_id); buf.put_u32(self.header.stream_id);
buf.put_u8(self.header.frame_type as u8); buf.put_u8(self.header.frame_type as u8);
buf.put_u16(self.header.payload_len); buf.put_u16(self.header.payload_len);
+2 -1
View File
@@ -1,4 +1,5 @@
use crate::protocol::codec::frame::FrameType; use crate::protocol::codec::frame::FrameType;
use crate::proxy::connection::connection::BUF_SIZE;
use crate::proxy::connection::muxer::{MuxMessage, Muxer}; use crate::proxy::connection::muxer::{MuxMessage, Muxer};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -13,7 +14,7 @@ pub async fn run_proxy_bridge<R, W>(
R: tokio::io::AsyncReadExt + Unpin, R: tokio::io::AsyncReadExt + Unpin,
W: tokio::io::AsyncWriteExt + Unpin, W: tokio::io::AsyncWriteExt + Unpin,
{ {
let mut buf = BytesMut::with_capacity(16384); let mut buf = BytesMut::with_capacity(BUF_SIZE);
loop { loop {
tokio::select! { tokio::select! {
+1 -5
View File
@@ -25,10 +25,6 @@ use crate::{
}, },
}; };
pub struct BufPair {
pub write_buf: BytesMut,
pub read_buf: BytesMut,
}
pub const BUF_SIZE: usize = 16384; pub const BUF_SIZE: usize = 16384;
@@ -208,7 +204,7 @@ impl Connection {
info!("Acting as TLS Server, waiting for ClientHello"); info!("Acting as TLS Server, waiting for ClientHello");
// Создаем Muxer для сервера // Создаем Muxer для сервера
let (mux_tx, mux_rx) = mpsc::channel(16384); let (mux_tx, mux_rx) = mpsc::channel(BUF_SIZE);
let muxer = Muxer::new(mux_tx.clone(), false); // false, так как это Сервер let muxer = Muxer::new(mux_tx.clone(), false); // false, так как это Сервер
// 1. TLS Handshake // 1. TLS Handshake
+5 -5
View File
@@ -1,9 +1,9 @@
use crate::{ use crate::{
protocol::errors::ErrorAction, protocol::errors::ErrorAction,
proxy::connection::{ proxy::connection::{
connection::{Connection, ConnectionRole}, connection::{Connection, ConnectionRole, BUF_SIZE},
engine::TunnelEngine, engine::TunnelEngine,
muxer::{MuxMessage, Muxer}, muxer::Muxer,
}, },
tlseng::profile::BrowserProfile, tlseng::profile::BrowserProfile,
}; };
@@ -12,7 +12,7 @@ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
net::{TcpListener, TcpStream}, net::{TcpListener, TcpStream},
}; };
use tracing::{debug, error, info, instrument}; // Импортируем макросы use tracing::{error, info, instrument}; // Импортируем макросы
pub struct Network { pub struct Network {
port: u16, port: u16,
@@ -99,7 +99,7 @@ impl Network {
// --- TLS Handshake --- // --- TLS Handshake ---
let ch = codec let ch = codec
.make_client_handshake(&BrowserProfile::CHROME_131, "proxy.server") .make_client_handshake(&BrowserProfile::CHROME_131, "google.com")
.map_err(|e| format!("{:?}", e))?; .map_err(|e| format!("{:?}", e))?;
outbound.write_all(&ch).await.map_err(|e| e.to_string())?; outbound.write_all(&ch).await.map_err(|e| e.to_string())?;
@@ -122,7 +122,7 @@ impl Network {
} }
// --- Запуск инфраструктуры --- // --- Запуск инфраструктуры ---
let (mux_tx, mux_rx) = tokio::sync::mpsc::channel(16384); let (mux_tx, mux_rx) = tokio::sync::mpsc::channel(BUF_SIZE);
let muxer = Muxer::new(mux_tx, true); let muxer = Muxer::new(mux_tx, true);
let handler = std::sync::Arc::new(crate::proxy::connection::handler::StreamHandler::new( let handler = std::sync::Arc::new(crate::proxy::connection::handler::StreamHandler::new(