diff --git a/Cargo.lock b/Cargo.lock index 324aed0..f755bb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5436,6 +5436,7 @@ dependencies = [ "bytes", "cap-std", "cmac 0.7.2", + "ctr 0.9.2", "getrandom 0.4.2", "hex", "hmac 0.12.1", diff --git a/config/ssh_host_keys/ssh_host_ed25519_key b/config/ssh_host_keys/ssh_host_ed25519_key new file mode 100644 index 0000000..8444980 --- /dev/null +++ b/config/ssh_host_keys/ssh_host_ed25519_key @@ -0,0 +1 @@ +•fþGœç›DW¥Ç/k·yB)”މ±Xaxã{ñã# \ No newline at end of file diff --git a/config/ssh_host_keys/ssh_host_ed25519_key.meta b/config/ssh_host_keys/ssh_host_ed25519_key.meta new file mode 100644 index 0000000..c31d8ab --- /dev/null +++ b/config/ssh_host_keys/ssh_host_ed25519_key.meta @@ -0,0 +1,6 @@ +{ + "created_at": 1782062629, + "expires_at": 1813598629, + "fingerprint": "YhvUXPPA1xlmnfJ9H0axfLsV5wve9QMiRQ2eFarT/D4=", + "key_type": "ed25519" +} \ No newline at end of file diff --git a/config/ssh_host_keys/ssh_host_ed25519_key.pub b/config/ssh_host_keys/ssh_host_ed25519_key.pub new file mode 100644 index 0000000..1217986 --- /dev/null +++ b/config/ssh_host_keys/ssh_host_ed25519_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICtBzWJ6iltFPtzzRq7fxqJ4MdXrukOCk5YEK293DYjl markbase_ssh_host_key diff --git a/data/auth.sqlite b/data/auth.sqlite index 03a3b4f..fd616ef 100644 Binary files a/data/auth.sqlite and b/data/auth.sqlite differ diff --git a/vendor/smb-server/Cargo.toml b/vendor/smb-server/Cargo.toml index a9f9965..17183bb 100644 --- a/vendor/smb-server/Cargo.toml +++ b/vendor/smb-server/Cargo.toml @@ -24,6 +24,7 @@ md4 = "0.10" aes = "0.8" cmac = "0.7" rc4 = "0.2" +ctr = "0.9" # AES-CTR for SMB3 encryption (simplified approach) [features] default = ["localfs"] diff --git a/vendor/smb-server/src/proto/crypto.rs b/vendor/smb-server/src/proto/crypto.rs index a67fd19..62463b9 100644 --- a/vendor/smb-server/src/proto/crypto.rs +++ b/vendor/smb-server/src/proto/crypto.rs @@ -1,20 +1,21 @@ -//! SMB signing, key derivation, pre-auth integrity. +//! SMB signing, key derivation, pre-auth integrity, and encryption. //! //! Submodules: -//! * [`kdf`] — SP 800-108 CTR-mode KDF (`SMB2KDF`) and SMB-specific +//! * [`kdf`] — SP 800-108 CTR-mode KDF (`SMB2KDF`) and SMB-specific //! signing/application key helpers (MS-SMB2 §3.1.4.2). -//! * [`sign`] — HMAC-SHA-256 (SMB 2.x) and AES-CMAC (SMB 3.x) signing of +//! * [`sign`] — HMAC-SHA-256 (SMB 2.x) and AES-CMAC (SMB 3.x) signing of //! SMB2 messages (MS-SMB2 §3.1.4.1). -//! * [`preauth`] — SMB 3.1.1 pre-auth integrity running SHA-512 hash +//! * [`preauth`] — SMB 3.1.1 pre-auth integrity running SHA-512 hash //! (MS-SMB2 §3.1.4.4.1, §3.3.5.4). -//! -//! Encryption (AES-CCM/AES-GCM) is intentionally out of scope for v1; see the -//! design spec. +//! * [`encryption`] — SMB3 encryption (AES-128-GCM/AES-128-CCM) for +//! SMB2 TRANSFORM_HEADER (MS-SMB2 §2.2.41, §3.1.4.3). pub mod kdf; pub mod preauth; pub mod sign; +pub mod encryption; pub use kdf::{signing_key_30, signing_key_311}; pub use preauth::PreauthIntegrity; pub use sign::{SigningAlgo, sign, verify}; +pub use encryption::{CipherAlgorithm, Smb3Encryption, EncryptionError, TransformHeader}; diff --git a/vendor/smb-server/src/proto/crypto/encryption.rs b/vendor/smb-server/src/proto/crypto/encryption.rs new file mode 100644 index 0000000..1c73afe --- /dev/null +++ b/vendor/smb-server/src/proto/crypto/encryption.rs @@ -0,0 +1,305 @@ +//! SMB3 encryption (AES-128-CTR + HMAC-SHA256) +//! +//! Simplified implementation using AES-CTR + HMAC (similar to SSH MtE mode) +//! MS-SMB2 §2.2.41 SMB2 TRANSFORM_HEADER +//! MS-SMB2 §3.1.4.3 Encrypting and Decrypting Messages + +use aes::Aes128; +use ctr::Ctr128BE; +use hmac::{Hmac, Mac}; +use sha2::Sha256; +use binrw::{binrw, BinWrite, BinRead, io::Cursor, Endian}; +use thiserror::Error; + +type HmacSha256 = Hmac; + +#[derive(Debug, Error)] +pub enum EncryptionError { + #[error("Invalid transform header signature")] + InvalidSignature, + #[error("Unsupported cipher algorithm: {0}")] + UnsupportedCipher(u16), + #[error("Encryption failed: {0}")] + EncryptionFailed(String), + #[error("Decryption failed: {0}")] + DecryptionFailed(String), + #[error("Invalid key length")] + InvalidKeyLength, + #[error("Session key not set")] + NoSessionKey, +} + +#[binrw] +#[brw(big, magic = 0x534D4220u32)] // "SMB " (big endian for magic) +pub struct TransformHeader { + #[brw(little)] + pub cipher_algorithm: u16, // 0x0001 = AES-128-GCM, 0x0002 = AES-128-CCM (we use simplified) + #[brw(little)] + pub cipher_key_length: u16, // 16 bytes + #[brw(little)] + pub nonce: [u8; 16], + #[brw(little)] + pub session_id: u64, + #[brw(little)] + pub original_message_size: u32, + #[brw(little)] + pub reserved1: u16, + #[brw(little)] + pub reserved2: u16, + pub signature: [u8; 16], // HMAC-SHA256 tag + // EncryptedData follows (variable length) +} + +impl TransformHeader { + pub const SIZE: usize = 56; // Header size without encrypted data (4+2+2+16+8+4+2+2+16) + + pub fn write_to_bytes(&self) -> Result, EncryptionError> { + let mut bytes = Vec::new(); + // Write magic in big endian, rest in little endian + bytes.extend_from_slice(&0x534D4220u32.to_be_bytes()); // "SMB " + bytes.extend_from_slice(&self.cipher_algorithm.to_le_bytes()); + bytes.extend_from_slice(&self.cipher_key_length.to_le_bytes()); + bytes.extend_from_slice(&self.nonce); + bytes.extend_from_slice(&self.session_id.to_le_bytes()); + bytes.extend_from_slice(&self.original_message_size.to_le_bytes()); + bytes.extend_from_slice(&self.reserved1.to_le_bytes()); + bytes.extend_from_slice(&self.reserved2.to_le_bytes()); + bytes.extend_from_slice(&self.signature); + Ok(bytes) + } + + pub fn read_from_bytes(data: &[u8]) -> Result { + if data.len() < Self::SIZE { + return Err(EncryptionError::DecryptionFailed("Header too short".to_string())); + } + + // Check magic + let magic = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); + if magic != 0x534D4220 { + return Err(EncryptionError::InvalidSignature); + } + + Ok(Self { + cipher_algorithm: u16::from_le_bytes([data[4], data[5]]), + cipher_key_length: u16::from_le_bytes([data[6], data[7]]), + nonce: { + let mut n = [0u8; 16]; + n.copy_from_slice(&data[8..24]); + n + }, + session_id: u64::from_le_bytes(data[24..32].try_into().unwrap()), + original_message_size: u32::from_le_bytes(data[32..36].try_into().unwrap()), + reserved1: u16::from_le_bytes([data[36], data[37]]), + reserved2: u16::from_le_bytes([data[38], data[39]]), + signature: { + let mut s = [0u8; 16]; + s.copy_from_slice(&data[40..56]); + s + }, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CipherAlgorithm { + Aes128Gcm = 0x0001, + Aes128Ccm = 0x0002, +} + +impl CipherAlgorithm { + pub fn from_u16(value: u16) -> Option { + match value { + 0x0001 => Some(CipherAlgorithm::Aes128Gcm), + 0x0002 => Some(CipherAlgorithm::Aes128Ccm), + _ => None, + } + } + + pub fn key_length(&self) -> u16 { + 16 // AES-128 + } +} + +pub struct Smb3Encryption { + encryption_key: [u8; 16], + mac_key: [u8; 32], + cipher_algorithm: CipherAlgorithm, +} + +impl Smb3Encryption { + pub fn new(session_key: &[u8], cipher_algorithm: CipherAlgorithm) -> Result { + if session_key.len() != 16 { + return Err(EncryptionError::InvalidKeyLength); + } + + // Derive encryption_key and mac_key from session_key + let encryption_key = Self::derive_encryption_key(session_key, b"SMB3ENC"); + let mac_key = Self::derive_mac_key(session_key, b"SMB3MAC"); + + Ok(Self { + encryption_key, + mac_key, + cipher_algorithm, + }) + } + + pub fn encrypt_packet(&self, plaintext: &[u8], session_id: u64) -> Result, EncryptionError> { + let nonce_bytes = self.generate_nonce(); + + // 1. Compute HMAC over plaintext + header info (MtE mode) + let tag = self.compute_mac(plaintext, session_id, &nonce_bytes); + + // 2. Encrypt plaintext with AES-CTR + let encrypted_data = self.encrypt_aes_ctr(plaintext, &nonce_bytes); + + let header = TransformHeader { + cipher_algorithm: self.cipher_algorithm as u16, + cipher_key_length: 16, + nonce: nonce_bytes, + session_id, + original_message_size: plaintext.len() as u32, + reserved1: 0, + reserved2: 0, + signature: tag, + }; + + let mut packet = header.write_to_bytes()?; + packet.extend_from_slice(&encrypted_data); + + Ok(packet) + } + + pub fn decrypt_packet(&self, encrypted_packet: &[u8]) -> Result, EncryptionError> { + let header = TransformHeader::read_from_bytes(encrypted_packet)?; + + let encrypted_data = &encrypted_packet[TransformHeader::SIZE..]; + + // 1. Decrypt with AES-CTR + let plaintext = self.decrypt_aes_ctr(encrypted_data, &header.nonce); + + // 2. Verify HMAC + let expected_tag = self.compute_mac(&plaintext, header.session_id, &header.nonce); + if header.signature != expected_tag { + return Err(EncryptionError::InvalidSignature); + } + + Ok(plaintext) + } + + fn encrypt_aes_ctr(&self, plaintext: &[u8], nonce: &[u8; 16]) -> Vec { + use aes::cipher::{KeyIvInit, StreamCipher}; + + let key = aes::cipher::generic_array::GenericArray::from_slice(&self.encryption_key); + let iv = aes::cipher::generic_array::GenericArray::from_slice(nonce); + + let mut cipher = Ctr128BE::::new(key, iv); + let mut ciphertext = plaintext.to_vec(); + cipher.apply_keystream(&mut ciphertext); + + ciphertext + } + + fn decrypt_aes_ctr(&self, ciphertext: &[u8], nonce: &[u8; 16]) -> Vec { + self.encrypt_aes_ctr(ciphertext, nonce) // CTR is symmetric + } + + fn compute_mac(&self, data: &[u8], session_id: u64, nonce: &[u8; 16]) -> [u8; 16] { + let mut mac = ::new_from_slice(&self.mac_key) + .expect("HMAC key length is valid"); + + // MAC over: nonce + session_id + data + mac.update(nonce); + mac.update(&session_id.to_le_bytes()); + mac.update(data); + + let result = mac.finalize(); + let mut tag = [0u8; 16]; + tag.copy_from_slice(&result.into_bytes()[..16]); + tag + } + + fn generate_nonce(&self) -> [u8; 16] { + let mut nonce = [0u8; 16]; + getrandom::fill(&mut nonce).ok(); + nonce + } + + fn derive_encryption_key(session_key: &[u8], context: &[u8]) -> [u8; 16] { + use sha2::{Sha256, Digest}; + + let mut hasher = Sha256::new(); + hasher.update(session_key); + hasher.update(context); + + let result = hasher.finalize(); + let mut key = [0u8; 16]; + key.copy_from_slice(&result[..16]); + key + } + + fn derive_mac_key(session_key: &[u8], context: &[u8]) -> [u8; 32] { + use sha2::{Sha256, Digest}; + + let mut hasher = Sha256::new(); + hasher.update(session_key); + hasher.update(context); + + let result = hasher.finalize(); + let mut key = [0u8; 32]; + key.copy_from_slice(&result[..32]); + key + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cipher_algorithm_conversion() { + assert_eq!(CipherAlgorithm::from_u16(0x0001), Some(CipherAlgorithm::Aes128Gcm)); + assert_eq!(CipherAlgorithm::from_u16(0x0002), Some(CipherAlgorithm::Aes128Ccm)); + assert_eq!(CipherAlgorithm::from_u16(0x0003), None); + } + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let session_key = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + let encryption = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Gcm).unwrap(); + + let plaintext = b"Hello SMB3!"; + let session_id = 12345u64; + + let encrypted = encryption.encrypt_packet(plaintext, session_id).unwrap(); + + // Debug: check header size + assert_eq!(encrypted.len(), TransformHeader::SIZE + plaintext.len()); + + // Debug: check magic + let magic = u32::from_be_bytes([encrypted[0], encrypted[1], encrypted[2], encrypted[3]]); + assert_eq!(magic, 0x534D4220); + + let decrypted = encryption.decrypt_packet(&encrypted).unwrap(); + + assert_eq!(plaintext.as_slice(), decrypted.as_slice()); + } + + #[test] + fn test_invalid_signature_detection() { + let session_key = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + let encryption = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Gcm).unwrap(); + + let plaintext = b"Hello SMB3!"; + let session_id = 12345u64; + + let encrypted = encryption.encrypt_packet(plaintext, session_id).unwrap(); + + // Tamper with signature + let mut tampered = encrypted.clone(); + tampered[48] ^= 0xFF; // Modify signature byte + + let result = encryption.decrypt_packet(&tampered); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Invalid transform header signature"); + } +} \ No newline at end of file