renames and tauri app
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod aead;
|
||||
pub mod chacha;
|
||||
mod ecdh;
|
||||
mod hkdf;
|
||||
pub mod session;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user