renames and tauri app

This commit is contained in:
2026-03-09 17:51:01 +07:00
parent 6ca47336a1
commit 6f4dd88a8e
62 changed files with 5215 additions and 70 deletions
+6
View File
@@ -0,0 +1,6 @@
use bytes::{Bytes, BytesMut};
pub trait AeadPacker {
fn encrypt(&mut self, data: &mut BytesMut) -> Result<Bytes, chacha20poly1305::aead::Error>;
fn decrypt(&mut self, data: &mut BytesMut) -> Result<Bytes, chacha20poly1305::aead::Error>;
}
+136
View File
@@ -0,0 +1,136 @@
use bytes::{Bytes, BytesMut};
use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::{AeadInPlace, ChaCha20Poly1305, Key, KeyInit, Nonce};
use crate::crypto::aead::AeadPacker;
pub struct NonceState {
counter: u64,
base_iv: [u8; 12],
}
impl NonceState {
pub fn new(base_iv: [u8; 12]) -> Self {
Self {
counter: 0,
base_iv,
}
}
// Возвращает nonce для текущего пакета и ПЕРЕХОДИТ к следующему
pub fn next_nonce(&mut self) -> Nonce {
let mut iv = self.base_iv;
// В TLS 1.3 используется Little Endian для счетчика при XOR
// Но если ты сам пишешь протокол, BE тоже пойдет.
// Главное — единообразие.
let counter_bytes = self.counter.to_be_bytes();
for i in 0..8 {
iv[i + 4] ^= counter_bytes[i];
}
self.counter += 1;
*GenericArray::from_slice(&iv)
}
}
pub struct ChaChaCipher {
pub encrypt_cipher: ChaCha20Poly1305,
pub decrypt_cipher: ChaCha20Poly1305,
pub encrypt_state: NonceState,
pub decrypt_state: NonceState,
}
impl ChaChaCipher {
pub fn new() -> Self {
let start_key = Key::from([0u8; 32]);
let encrypt_cipher = ChaCha20Poly1305::new(&start_key);
let decrypt_cipher = ChaCha20Poly1305::new(&start_key);
Self {
encrypt_state: NonceState::new([0u8; 12]),
decrypt_state: NonceState::new([0u8; 12]),
encrypt_cipher,
decrypt_cipher,
}
}
pub fn set_keys(
&mut self,
w_key: [u8; 32],
w_iv: [u8; 12], // Write (исходящие)
r_key: [u8; 32],
r_iv: [u8; 12], // Read (входящие)
) {
self.encrypt_cipher = ChaCha20Poly1305::new(Key::from_slice(&w_key));
self.decrypt_cipher = ChaCha20Poly1305::new(Key::from_slice(&r_key));
self.encrypt_state = NonceState::new(w_iv);
self.decrypt_state = NonceState::new(r_iv);
tracing::debug!("Cipher keys and IVs updated for both directions");
}
}
impl AeadPacker for ChaChaCipher {
fn encrypt(&mut self, data: &mut BytesMut) -> Result<Bytes, chacha20poly1305::aead::Error> {
// Сначала получаем текущий counter для лога (до того, как next_nonce его инкрементирует)
let current_counter = self.encrypt_state.counter;
let nonce = self.encrypt_state.next_nonce();
let data_len = data.len();
// maybe ad should be stream id
match self.encrypt_cipher.encrypt_in_place(&nonce, &nonce, data) {
Ok(_) => {
tracing::trace!(
counter = current_counter,
nonce = %hex::encode(nonce),
len = data_len,
"Encryption successful"
);
Ok(data.split().freeze())
}
Err(e) => {
tracing::error!(
counter = current_counter,
nonce = %hex::encode(nonce),
len = data_len,
error = ?e,
"AEAD encryption failure"
);
Err(e)
}
}
}
fn decrypt(&mut self, data: &mut BytesMut) -> Result<Bytes, chacha20poly1305::aead::Error> {
let current_counter = self.decrypt_state.counter;
let nonce = self.decrypt_state.next_nonce();
let data_len = data.len();
// maybe ad should be stream id
match self.decrypt_cipher.decrypt_in_place(&nonce, &nonce, data) {
Ok(_) => {
tracing::trace!(
counter = current_counter,
nonce = %hex::encode(nonce),
len = data_len,
"Decryption successful"
);
Ok(data.split().freeze())
}
Err(e) => {
let data_prefix = if data.len() >= 8 {
hex::encode(&data[..8])
} else {
hex::encode(data.as_ref())
};
tracing::error!(
counter = current_counter,
nonce = %hex::encode(nonce),
len = data_len,
prefix = %data_prefix,
error = ?e,
"AEAD decryption failure! Verification failed or data malformed"
);
Err(e)
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use aead::OsRng;
use x25519_dalek::{EphemeralSecret, PublicKey};
pub struct ECDH {
pub public_key: PublicKey,
pub private_key: Option<EphemeralSecret>,
}
impl ECDH {
pub fn new() -> Self {
let secret = EphemeralSecret::random_from_rng(&mut OsRng);
let public = PublicKey::from(&secret);
Self {
private_key: Some(secret),
public_key: public,
}
}
pub fn get_shared(&mut self, public: &PublicKey) -> Option<[u8; 32]> {
let private_key = self.private_key.take()?;
let shared = private_key.diffie_hellman(&public);
Some(*shared.as_bytes())
}
}
+22
View File
@@ -0,0 +1,22 @@
use hkdf::Hkdf;
use sha2::Sha256;
pub struct HKDF;
impl HKDF {
pub fn extract_key(salt: &[u8], ikm: &[u8]) -> Hkdf<Sha256> {
let extracted_key = Hkdf::<Sha256>::new(Some(salt), ikm);
extracted_key
}
pub fn expand_key<const N: usize>(
extracted_key: &Hkdf<Sha256>,
mark: &[u8],
) -> Result<[u8; N], String> {
let mut expanded_key: [u8; N] = [0u8; N];
extracted_key
.expand(mark, &mut expanded_key)
.map_err(|e| e.to_string())?;
Ok(expanded_key)
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod aead;
pub mod chacha;
mod ecdh;
mod hkdf;
pub mod session;
+225
View File
@@ -0,0 +1,225 @@
use x25519_dalek::PublicKey;
use crate::{
crypto::{ecdh::ECDH, hkdf::HKDF},
tlseng::extension::ExtensionStack,
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
use aead::{rand_core::RngCore, OsRng};
pub struct SaltPair {
local_salt: [u8; 32],
remote_salt: [u8; 32],
is_initiator: bool,
}
impl SaltPair {
pub fn new(is_initiator: bool) -> Self {
let mut local_salt = [0u8; 32];
OsRng.fill_bytes(&mut local_salt);
Self {
local_salt,
remote_salt: [0; 32],
is_initiator,
}
}
pub fn get_local(&self) -> [u8; 32] {
self.local_salt
}
pub fn set_remote_salt(&mut self, salt: [u8; 32]) {
self.remote_salt = salt
}
pub fn get_total(&self) -> [u8; 64] {
let mut salt = [0u8; 64];
if self.is_initiator {
salt[..32].copy_from_slice(&self.local_salt);
salt[32..].copy_from_slice(&self.remote_salt);
salt
} else {
salt[..32].copy_from_slice(&self.remote_salt);
salt[32..].copy_from_slice(&self.local_salt);
salt
}
}
}
pub struct SessionKeys {
pub salt: SaltPair,
pub ecdh: ECDH,
pub auth_key: [u8; 32],
}
impl SessionKeys {
pub fn new(is_initiator: bool) -> Self {
Self {
salt: SaltPair::new(is_initiator),
ecdh: ECDH::new(),
auth_key: [0u8; 32],
}
}
pub fn update_keys(
&mut self,
salt: [u8; 32],
extensions: &ExtensionStack,
is_server: bool, // true если мы Сервер (парсим ClientHello), false если Клиент
) -> Result<([u8; 32], [u8; 12], [u8; 32], [u8; 12]), String> {
// Сохраняем соль от удаленной стороны
self.salt.set_remote_salt(salt);
tracing::debug!(
remote_salt = %hex::encode(&salt[..8]),
local_salt = %hex::encode(&self.salt.get_local()[..8]),
total_salt = %hex::encode(&self.salt.get_total()[28..36]),
"Updating keys with new salt"
);
const EXT_KEY_SHARE: u16 = 0x0033;
if let Some(dh_data) = extensions.find_by_type(EXT_KEY_SHARE) {
let mut key_bytes = [0u8; 32];
if is_server {
// МЫ СЕРВЕР: Парсим ClientHello KeyShare (там список)
// Минимум: 2 (длина списка) + 2 (группа) + 2 (длина ключа) + 32 (ключ) = 38
if dh_data.len() < 38 {
return Err(format!(
"Client KeyShare too short: {} bytes",
dh_data.len()
));
}
let mut found = false;
// Ищем маркер x25519 (00 1d) и длину 32 (00 20)
for i in 2..=(dh_data.len() - 34) {
if dh_data[i..i + 4] == [0x00, 0x1d, 0x00, 0x20] {
key_bytes.copy_from_slice(&dh_data[i + 4..i + 36]);
found = true;
break;
}
}
if !found {
return Err("Could not find x25519 key in ClientHello".into());
}
} else {
// МЫ КЛИЕНТ: Парсим ServerHello KeyShare (там сразу группа и ключ)
// [Group:2] [KeyLen:2] [Key:32] = 36 байт
if dh_data.len() < 36 {
return Err("Server KeyShare too short".into());
}
key_bytes.copy_from_slice(&dh_data[4..36]);
}
// Проверка на "кривой" ключ
if key_bytes.iter().all(|&x| x == 0) {
return Err("Extracted remote public key is all ZEROS!".into());
}
let public_key = PublicKey::from(key_bytes);
tracing::debug!(
remote_pub = %hex::encode(&key_bytes[..4]),
role = if is_server { "Server" } else { "Client" },
"Key exchange successful, deriving material..."
);
// Вызываем генерацию, которая вернет (w_key, w_iv, r_key, r_iv)
self.generate_keys(&public_key, is_server)
} else {
Err("No KeyShare extension found in handshake".into())
}
}
fn generate_keys(
&mut self,
public_key: &PublicKey,
is_server: bool,
) -> Result<([u8; 32], [u8; 12], [u8; 32], [u8; 12]), String> {
let shared_key = self
.ecdh
.get_shared(public_key)
.ok_or_else(|| "No shared secret".to_string())?;
tracing::debug!(
shared_prefix = %hex::encode(&shared_key[..8]),
"DH Shared secret derived"
);
let hkdf = HKDF::extract_key(&self.salt.get_total(), &shared_key);
let c_key = HKDF::expand_key::<32>(&hkdf, b"client_aead").map_err(|e| e.to_string())?;
let c_iv = HKDF::expand_key::<12>(&hkdf, b"client_iv").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 auth_secret = HKDF::expand_key::<32>(&hkdf, b"auth_key").map_err(|e| e.to_string())?;
self.auth_key = auth_secret;
tracing::info!(
client_key_short = %hex::encode(&c_key[..4]),
server_key_short = %hex::encode(&s_key[..4]),
"HKDF expansion complete"
);
// Распределяем: (write_key, write_iv, read_key, read_iv)
if is_server {
Ok((s_key, s_iv, c_key, c_iv)) // Сервер пишет своим, читает клиентским
} else {
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;
// Вставляем цикл проверки расширенного окна [-2, +2]
// Это дает запас по времени в обе стороны
for step in (current_step.saturating_sub(2))..=(current_step.saturating_add(2)) {
if &Self::compute_tag(&self.auth_key, step) == received_tag {
// Если подошел не текущий, а другой шаг — логируем это для диагностики
if step != current_step {
tracing::debug!(expected = %current_step, matched = %step, "Auth tag valid with time offset");
}
return true;
}
}
// Если ни один не подошел — логируем для отладки
tracing::warn!(
current_step = %current_step,
"AUTH MISMATCH: All tags rejected for current window"
);
false
}
}