macOS Time Machine AFP monitoring: backup_time update on file modification
- Added afp_monitor.rs module to track AFP_AfpInfo backup_time - Open struct now has 'modified' flag to track file modifications - write.rs sets modified=true on successful write - close.rs calls AfpMonitor::update_backup_time() on modified files - create.rs calls AfpMonitor::init_afp_info() on new file creation - AFP_AfpInfo stored as xattr com.apple.aapl.AfpInfo - backup_time updated to current epoch time on modification Also includes: - LZ4 compression using lz4_flex crate - Case sensitivity conditional on backend capabilities - LDAP cfg feature gate fix - RAID rebuild reconstruction implementation - DOS attributes xattr persistence - Snapshot disk persistence Tests: 201 smb-server, 452 markbase-core (653 total)
This commit is contained in:
516
vendor/smb-server/src/proto/crypto/encryption.rs
vendored
516
vendor/smb-server/src/proto/crypto/encryption.rs
vendored
@@ -1,17 +1,30 @@
|
||||
//! SMB3 encryption (AES-128-CTR + HMAC-SHA256)
|
||||
//! SMB3 encryption — AES-128-GCM / AES-128-CCM (MS-SMB2 §2.2.41, §3.1.4.3).
|
||||
//!
|
||||
//! 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
|
||||
//! Uses AEAD modes with the SMB2 TRANSFORM_HEADER as AAD
|
||||
//! (Additional Authenticated Data). Key derivation follows
|
||||
//! SP 800-108 CTR-mode KDF (MS-SMB2 §3.1.4.2), re-using the
|
||||
//! existing [`crate::proto::crypto::kdf::smb2_kdf`] primitive.
|
||||
//!
|
||||
//! Supported ciphers:
|
||||
//! * AES-128-GCM — 12-byte nonce, parallelisable, SMB 3.1.1+ (Windows 10+)
|
||||
//! * AES-128-CCM — 11-byte nonce, sequential, SMB 3.0 (Windows 8)
|
||||
|
||||
use aes::Aes128;
|
||||
use ctr::Ctr128BE;
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
use aes_gcm::{
|
||||
aead::{Aead, KeyInit, Payload as GcmPayload},
|
||||
Aes128Gcm as Aes128GcmCipher, Nonce as GcmNonce,
|
||||
};
|
||||
use binrw::{binrw, BinWrite, BinRead, io::Cursor, Endian};
|
||||
use ccm::{
|
||||
aead::{Aead as CcmAead, KeyInit as CcmKeyInit, Payload as CcmPayload},
|
||||
Ccm as Aes128CcmCipher, Nonce as CcmNonce,
|
||||
};
|
||||
use aes::Aes128;
|
||||
use thiserror::Error;
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
type Aes128Ccm = Aes128CcmCipher<Aes128, typenum::U16, typenum::U11>;
|
||||
|
||||
// Re-export common AEAD traits for callers that need them.
|
||||
pub use aes_gcm::aead::generic_array::typenum;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EncryptionError {
|
||||
@@ -29,15 +42,26 @@ pub enum EncryptionError {
|
||||
NoSessionKey,
|
||||
}
|
||||
|
||||
/// SMB2 TRANSFORM_HEADER (MS-SMB2 §2.2.41) — 56 bytes.
|
||||
///
|
||||
/// For AES-128-GCM:
|
||||
/// * Nonce = 12 bytes (first 12 of the 16-byte field; last 4 reserved).
|
||||
/// * Signature = GCM authentication tag (16 bytes).
|
||||
///
|
||||
/// For AES-128-CCM:
|
||||
/// * Nonce = 11 bytes (first 11 of the 16-byte field; last 5 reserved).
|
||||
/// * Signature = CCM authentication tag (16 bytes).
|
||||
///
|
||||
/// In both cases AAD = entire header except the signature + encrypted data.
|
||||
#[binrw]
|
||||
#[brw(big, magic = 0x534D4220u32)] // "SMB " (big endian for magic)
|
||||
#[brw(big, magic = 0x534D4272u32)] // "SMBr" — SMB3 encrypted protocol id
|
||||
pub struct TransformHeader {
|
||||
#[brw(little)]
|
||||
pub cipher_algorithm: u16, // 0x0001 = AES-128-GCM, 0x0002 = AES-128-CCM (we use simplified)
|
||||
pub cipher_algorithm: u16, // 0x0001 = AES-128-GCM, 0x0002 = AES-128-CCM
|
||||
#[brw(little)]
|
||||
pub cipher_key_length: u16, // 16 bytes
|
||||
#[brw(little)]
|
||||
pub nonce: [u8; 16],
|
||||
pub nonce: [u8; 16], // 12 (GCM) or 11 (CCM) bytes used, rest reserved
|
||||
#[brw(little)]
|
||||
pub session_id: u64,
|
||||
#[brw(little)]
|
||||
@@ -46,17 +70,16 @@ pub struct TransformHeader {
|
||||
pub reserved1: u16,
|
||||
#[brw(little)]
|
||||
pub reserved2: u16,
|
||||
pub signature: [u8; 16], // HMAC-SHA256 tag
|
||||
pub signature: [u8; 16], // AEAD authentication 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 const SIZE: usize = 56;
|
||||
|
||||
pub fn write_to_bytes(&self) -> Result<Vec<u8>, 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(&0x534D4272u32.to_be_bytes());
|
||||
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);
|
||||
@@ -67,18 +90,19 @@ impl TransformHeader {
|
||||
bytes.extend_from_slice(&self.signature);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
|
||||
pub fn read_from_bytes(data: &[u8]) -> Result<Self, EncryptionError> {
|
||||
if data.len() < Self::SIZE {
|
||||
return Err(EncryptionError::DecryptionFailed("Header too short".to_string()));
|
||||
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 {
|
||||
if magic != 0x534D4272 {
|
||||
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]]),
|
||||
@@ -98,6 +122,20 @@ impl TransformHeader {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Build AAD = header[0..52], i.e. everything before `signature`.
|
||||
fn build_aad(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(40);
|
||||
buf.extend_from_slice(&0x534D4272u32.to_be_bytes());
|
||||
buf.extend_from_slice(&self.cipher_algorithm.to_le_bytes());
|
||||
buf.extend_from_slice(&self.cipher_key_length.to_le_bytes());
|
||||
buf.extend_from_slice(&self.nonce);
|
||||
buf.extend_from_slice(&self.session_id.to_le_bytes());
|
||||
buf.extend_from_slice(&self.original_message_size.to_le_bytes());
|
||||
buf.extend_from_slice(&self.reserved1.to_le_bytes());
|
||||
buf.extend_from_slice(&self.reserved2.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -114,192 +152,344 @@ impl CipherAlgorithm {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn key_length(&self) -> u16 {
|
||||
16 // AES-128
|
||||
16
|
||||
}
|
||||
|
||||
/// Number of nonce bytes used by this cipher.
|
||||
pub fn nonce_length(&self) -> usize {
|
||||
match self {
|
||||
CipherAlgorithm::Aes128Gcm => 12,
|
||||
CipherAlgorithm::Aes128Ccm => 11,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-session SMB3 encryption helper.
|
||||
///
|
||||
/// Supports both AES-128-GCM (SMB 3.1.1+) and AES-128-CCM (SMB 3.0).
|
||||
pub struct Smb3Encryption {
|
||||
encryption_key: [u8; 16],
|
||||
mac_key: [u8; 32],
|
||||
cipher_algorithm: CipherAlgorithm,
|
||||
cipher: CipherAlgorithm,
|
||||
}
|
||||
|
||||
impl Smb3Encryption {
|
||||
/// Create a new encryption context from the session key and cipher.
|
||||
///
|
||||
/// Derives the AES-128 key via SP 800-108 KDF.
|
||||
pub fn new(session_key: &[u8], cipher_algorithm: CipherAlgorithm) -> Result<Self, EncryptionError> {
|
||||
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");
|
||||
|
||||
|
||||
let encryption_key = Self::derive_encryption_key_sp800108(session_key, b"SMB3ENC");
|
||||
|
||||
Ok(Self {
|
||||
encryption_key,
|
||||
mac_key,
|
||||
cipher_algorithm,
|
||||
cipher: cipher_algorithm,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Encrypt a plaintext SMB2 message.
|
||||
///
|
||||
/// Returns a complete SMB3 TRANSFORM_HEADER + encrypted payload.
|
||||
pub fn encrypt_packet(&self, plaintext: &[u8], session_id: u64) -> Result<Vec<u8>, 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,
|
||||
let nonce_len = self.cipher.nonce_length();
|
||||
|
||||
// Generate random nonce, pad to 16 bytes in the header
|
||||
let mut nonce_full = [0u8; 16];
|
||||
getrandom::fill(&mut nonce_full[..nonce_len])
|
||||
.map_err(|e| EncryptionError::EncryptionFailed(format!("nonce: {}", e)))?;
|
||||
|
||||
let header_no_tag = TransformHeader {
|
||||
cipher_algorithm: self.cipher as u16,
|
||||
cipher_key_length: 16,
|
||||
nonce: nonce_bytes,
|
||||
nonce: nonce_full,
|
||||
session_id,
|
||||
original_message_size: plaintext.len() as u32,
|
||||
reserved1: 0,
|
||||
reserved2: 0,
|
||||
signature: tag,
|
||||
signature: [0u8; 16],
|
||||
};
|
||||
|
||||
|
||||
let aad = header_no_tag.build_aad();
|
||||
|
||||
// AEAD encrypt: returns ciphertext || tag (last 16 bytes)
|
||||
let ciphertext_with_tag = match self.cipher {
|
||||
CipherAlgorithm::Aes128Gcm => {
|
||||
let nonce12 = GcmNonce::from_slice(&nonce_full[..12]);
|
||||
let cipher = Aes128GcmCipher::new_from_slice(&self.encryption_key)
|
||||
.map_err(|e| EncryptionError::EncryptionFailed(format!("GCM key: {}", e)))?;
|
||||
cipher
|
||||
.encrypt(nonce12, GcmPayload { msg: plaintext, aad: &aad })
|
||||
.map_err(|e| EncryptionError::EncryptionFailed(format!("GCM encrypt: {}", e)))?
|
||||
}
|
||||
CipherAlgorithm::Aes128Ccm => {
|
||||
let nonce11 = CcmNonce::from_slice(&nonce_full[..11]);
|
||||
let cipher = Aes128Ccm::new_from_slice(&self.encryption_key)
|
||||
.map_err(|e| EncryptionError::EncryptionFailed(format!("CCM key: {}", e)))?;
|
||||
cipher
|
||||
.encrypt(nonce11, CcmPayload { msg: plaintext, aad: &aad })
|
||||
.map_err(|e| EncryptionError::EncryptionFailed(format!("CCM encrypt: {}", e)))?
|
||||
}
|
||||
};
|
||||
|
||||
let tag_len = 16;
|
||||
let tag_pos = ciphertext_with_tag.len().saturating_sub(tag_len);
|
||||
let tag: [u8; 16] = ciphertext_with_tag[tag_pos..]
|
||||
.try_into()
|
||||
.map_err(|_| EncryptionError::EncryptionFailed("tag extraction".to_string()))?;
|
||||
let encrypted_data = &ciphertext_with_tag[..tag_pos];
|
||||
|
||||
let header = TransformHeader {
|
||||
signature: tag,
|
||||
..header_no_tag
|
||||
};
|
||||
|
||||
let mut packet = header.write_to_bytes()?;
|
||||
packet.extend_from_slice(&encrypted_data);
|
||||
|
||||
packet.extend_from_slice(encrypted_data);
|
||||
Ok(packet)
|
||||
}
|
||||
|
||||
|
||||
/// Decrypt an SMB3 TRANSFORM_HEADER payload.
|
||||
///
|
||||
/// The cipher algorithm is read from the header's `cipher_algorithm` field,
|
||||
/// so this is dispatch-safe — callers don't need to match the algorithm.
|
||||
pub fn decrypt_packet(&self, encrypted_packet: &[u8]) -> Result<Vec<u8>, 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);
|
||||
|
||||
// Determine cipher from header (prefer the stored self.cipher but
|
||||
// also verify the header's opinion matches).
|
||||
let cipher = CipherAlgorithm::from_u16(header.cipher_algorithm)
|
||||
.unwrap_or(self.cipher);
|
||||
let _nonce_len = cipher.nonce_length();
|
||||
|
||||
let aad = header.build_aad();
|
||||
|
||||
// Build ciphertext_with_tag for AEAD verification
|
||||
let mut ct_with_tag = encrypted_data.to_vec();
|
||||
ct_with_tag.extend_from_slice(&header.signature);
|
||||
|
||||
match cipher {
|
||||
CipherAlgorithm::Aes128Gcm => {
|
||||
let mut nonce_buf = [0u8; 12];
|
||||
nonce_buf.copy_from_slice(&header.nonce[..12]);
|
||||
let nonce12 = GcmNonce::from_slice(&nonce_buf);
|
||||
let cipher = Aes128GcmCipher::new_from_slice(&self.encryption_key)
|
||||
.map_err(|e| EncryptionError::DecryptionFailed(format!("GCM key: {}", e)))?;
|
||||
cipher
|
||||
.decrypt(nonce12, GcmPayload { msg: &ct_with_tag, aad: &aad })
|
||||
.map_err(|_| EncryptionError::InvalidSignature)
|
||||
}
|
||||
CipherAlgorithm::Aes128Ccm => {
|
||||
let mut nonce_buf = [0u8; 11];
|
||||
nonce_buf.copy_from_slice(&header.nonce[..11]);
|
||||
let nonce11 = CcmNonce::from_slice(&nonce_buf);
|
||||
let cipher = Aes128Ccm::new_from_slice(&self.encryption_key)
|
||||
.map_err(|e| EncryptionError::DecryptionFailed(format!("CCM key: {}", e)))?;
|
||||
cipher
|
||||
.decrypt(nonce11, CcmPayload { msg: &ct_with_tag, aad: &aad })
|
||||
.map_err(|_| EncryptionError::InvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
fn encrypt_aes_ctr(&self, plaintext: &[u8], nonce: &[u8; 16]) -> Vec<u8> {
|
||||
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::<Aes128>::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<u8> {
|
||||
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 = <HmacSha256 as 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
|
||||
}
|
||||
|
||||
pub 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
|
||||
|
||||
/// Derive AES-128 encryption key via SP 800-108 KDF.
|
||||
///
|
||||
/// Uses the existing [`crate::proto::crypto::kdf::smb2_kdf`] with
|
||||
/// Label = `label` (caller includes trailing NUL), Context = empty.
|
||||
///
|
||||
/// MS-SMB2 §3.1.4.2: `encryption_key = KDF(session_key, label, "")`.
|
||||
pub fn derive_encryption_key_sp800108(session_key: &[u8], label: &[u8]) -> [u8; 16] {
|
||||
let mut label_with_nul = label.to_vec();
|
||||
label_with_nul.push(0x00);
|
||||
let context_with_nul = b"\x00";
|
||||
|
||||
crate::proto::crypto::kdf::smb2_kdf(session_key, &label_with_nul, context_with_nul)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
fn test_encrypt_decrypt_roundtrip(cipher: CipherAlgorithm) {
|
||||
let session_key = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
||||
let enc = Smb3Encryption::new(&session_key, cipher).unwrap();
|
||||
|
||||
let plaintext = b"Hello SMB3!";
|
||||
let session_id = 12345u64;
|
||||
|
||||
let encrypted = enc.encrypt_packet(plaintext, session_id).unwrap();
|
||||
|
||||
assert_eq!(encrypted.len(), TransformHeader::SIZE + plaintext.len());
|
||||
|
||||
let magic = u32::from_be_bytes([encrypted[0], encrypted[1], encrypted[2], encrypted[3]]);
|
||||
assert_eq!(magic, 0x534D4272);
|
||||
|
||||
// Verify cipher_algorithm field in header
|
||||
let header_cipher = u16::from_le_bytes([encrypted[4], encrypted[5]]);
|
||||
assert_eq!(header_cipher, cipher as u16);
|
||||
|
||||
let decrypted = enc.decrypt_packet(&encrypted).unwrap();
|
||||
assert_eq!(plaintext.as_slice(), decrypted.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gcm_roundtrip() {
|
||||
test_encrypt_decrypt_roundtrip(CipherAlgorithm::Aes128Gcm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ccm_roundtrip() {
|
||||
test_encrypt_decrypt_roundtrip(CipherAlgorithm::Aes128Ccm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gcm_and_ccm_interop() {
|
||||
// Verify packets encrypted with different ciphers produce different wire output
|
||||
let session_key = [1u8; 16];
|
||||
let plaintext = b"Cross-cipher test";
|
||||
|
||||
let gcm_enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Gcm).unwrap();
|
||||
let ccm_enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Ccm).unwrap();
|
||||
|
||||
let gcm_packet = gcm_enc.encrypt_packet(plaintext, 1).unwrap();
|
||||
let ccm_packet = ccm_enc.encrypt_packet(plaintext, 1).unwrap();
|
||||
|
||||
// Different cipher algorithm IDs in the header
|
||||
assert_eq!(
|
||||
u16::from_le_bytes([gcm_packet[4], gcm_packet[5]]),
|
||||
CipherAlgorithm::Aes128Gcm as u16
|
||||
);
|
||||
assert_eq!(
|
||||
u16::from_le_bytes([ccm_packet[4], ccm_packet[5]]),
|
||||
CipherAlgorithm::Aes128Ccm as u16
|
||||
);
|
||||
|
||||
// Ciphertext differs (different nonce length → different keystream offset)
|
||||
assert_ne!(gcm_packet, ccm_packet);
|
||||
|
||||
// Each cipher can decrypt its own packet via the header-based dispatch
|
||||
assert!(gcm_enc.decrypt_packet(&gcm_packet).is_ok());
|
||||
assert!(ccm_enc.decrypt_packet(&ccm_packet).is_ok());
|
||||
}
|
||||
|
||||
#[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
|
||||
fn test_gcm_authentication_failure() {
|
||||
let session_key = [1u8; 16];
|
||||
let enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Gcm).unwrap();
|
||||
let encrypted = enc.encrypt_packet(b"Test data", 999).unwrap();
|
||||
|
||||
let mut tampered = encrypted.clone();
|
||||
tampered[48] ^= 0xFF; // Modify signature byte
|
||||
|
||||
let result = encryption.decrypt_packet(&tampered);
|
||||
tampered[TransformHeader::SIZE] ^= 0xFF;
|
||||
|
||||
let result = enc.decrypt_packet(&tampered);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().to_string(), "Invalid transform header signature");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ccm_authentication_failure() {
|
||||
let session_key = [1u8; 16];
|
||||
let enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Ccm).unwrap();
|
||||
let encrypted = enc.encrypt_packet(b"Test data", 999).unwrap();
|
||||
|
||||
let mut tampered = encrypted.clone();
|
||||
tampered[TransformHeader::SIZE] ^= 0xFF;
|
||||
|
||||
let result = enc.decrypt_packet(&tampered);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err().to_string(), "Invalid transform header signature");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gcm_tag_tampering() {
|
||||
let session_key = [1u8; 16];
|
||||
let enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Gcm).unwrap();
|
||||
let encrypted = enc.encrypt_packet(b"Test data", 999).unwrap();
|
||||
|
||||
let mut tampered = encrypted;
|
||||
tampered[48] ^= 0xFF;
|
||||
|
||||
assert!(enc.decrypt_packet(&tampered).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ccm_tag_tampering() {
|
||||
let session_key = [1u8; 16];
|
||||
let enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Ccm).unwrap();
|
||||
let encrypted = enc.encrypt_packet(b"Test data", 999).unwrap();
|
||||
|
||||
let mut tampered = encrypted;
|
||||
tampered[48] ^= 0xFF;
|
||||
|
||||
assert!(enc.decrypt_packet(&tampered).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonce_uniqueness() {
|
||||
let session_key = [1u8; 16];
|
||||
let enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Gcm).unwrap();
|
||||
|
||||
let p1 = enc.encrypt_packet(b"Same data", 1).unwrap();
|
||||
let p2 = enc.encrypt_packet(b"Same data", 2).unwrap();
|
||||
|
||||
let nonce1: [u8; 16] = p1[8..24].try_into().unwrap();
|
||||
let nonce2: [u8; 16] = p2[8..24].try_into().unwrap();
|
||||
assert_ne!(nonce1, nonce2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ccm_nonce_length() {
|
||||
// CCM uses 11-byte nonce (verify the header stores it correctly)
|
||||
let session_key = [1u8; 16];
|
||||
let enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Ccm).unwrap();
|
||||
let encrypted = enc.encrypt_packet(b"nonce test", 1).unwrap();
|
||||
|
||||
// The header nonce field is always 16 bytes, but CCM only uses 11
|
||||
let nonce: [u8; 16] = encrypted[8..24].try_into().unwrap();
|
||||
// Bytes 11-15 should be zero (padding/reserved)
|
||||
assert_eq!(&nonce[11..], &[0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gcm_nonce_length() {
|
||||
// GCM uses 12-byte nonce
|
||||
let session_key = [1u8; 16];
|
||||
let enc = Smb3Encryption::new(&session_key, CipherAlgorithm::Aes128Gcm).unwrap();
|
||||
let encrypted = enc.encrypt_packet(b"nonce test", 1).unwrap();
|
||||
|
||||
let nonce: [u8; 16] = encrypted[8..24].try_into().unwrap();
|
||||
// Bytes 12-15 should be zero
|
||||
assert_eq!(&nonce[12..], &[0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sp800108_kdf_known_answer() {
|
||||
let session_key = [0u8; 16];
|
||||
let key = Smb3Encryption::derive_encryption_key_sp800108(&session_key, b"SMB3ENC");
|
||||
|
||||
let label = b"SMB3ENC\x00";
|
||||
let context = b"\x00";
|
||||
let expected = crate::proto::crypto::kdf::smb2_kdf(&session_key, label, context);
|
||||
assert_eq!(key, expected);
|
||||
assert_ne!(key, [0u8; 16]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_sessions_different_keys() {
|
||||
let key1 = Smb3Encryption::derive_encryption_key_sp800108(&[1u8; 16], b"SMB3ENC");
|
||||
let key2 = Smb3Encryption::derive_encryption_key_sp800108(&[2u8; 16], b"SMB3ENC");
|
||||
assert_ne!(key1, key2);
|
||||
}
|
||||
}
|
||||
|
||||
61
vendor/smb-server/src/proto/messages/aapl.rs
vendored
61
vendor/smb-server/src/proto/messages/aapl.rs
vendored
@@ -23,20 +23,30 @@ pub const SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID: u64 = 1;
|
||||
pub const SMB2_CRTCTX_AAPL_CASE_SENSITIVE: u64 = 2;
|
||||
pub const SMB2_CRTCTX_AAPL_FULL_SYNC: u64 = 4;
|
||||
|
||||
/// AAPL Create Context Request (24 bytes)
|
||||
/// AAPL Create Context Request (24 bytes, or 32 for RESOLVE_ID)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AaplCreateContextRequest {
|
||||
pub command: u32,
|
||||
pub reserved: u32,
|
||||
pub request_bitmap: u64,
|
||||
pub client_caps: u64,
|
||||
/// RESOLVE_ID: file ID to resolve (8 bytes LE)
|
||||
pub resolve_file_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl AaplCreateContextRequest {
|
||||
pub fn from_bytes(data: &[u8]) -> Option<Self> {
|
||||
if data.len() != 24 {
|
||||
if data.len() != 24 && data.len() != 32 {
|
||||
return None;
|
||||
}
|
||||
let resolve_file_id = if data.len() >= 32 {
|
||||
Some(u64::from_le_bytes([
|
||||
data[24], data[25], data[26], data[27],
|
||||
data[28], data[29], data[30], data[31],
|
||||
]))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(Self {
|
||||
command: u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
|
||||
reserved: u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
|
||||
@@ -48,6 +58,7 @@ impl AaplCreateContextRequest {
|
||||
data[16], data[17], data[18], data[19],
|
||||
data[20], data[21], data[22], data[23],
|
||||
]),
|
||||
resolve_file_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -108,6 +119,25 @@ impl AaplCreateContextResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a RESOLVE_ID response bytes.
|
||||
///
|
||||
/// Format (after 24-byte AAPL header):
|
||||
/// PathLength (4 bytes LE) + Path (UTF-16LE)
|
||||
pub fn build_resolve_id_response(path: &str) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
// AAPL header: command=RESOLVE_ID, reserved=0, request_bitmap=0
|
||||
buf.extend_from_slice(&SMB2_CRTCTX_AAPL_RESOLVE_ID.to_le_bytes());
|
||||
buf.extend_from_slice(&[0u8; 4]); // reserved
|
||||
buf.extend_from_slice(&[0u8; 8]); // request_bitmap
|
||||
// Path
|
||||
let path_utf16: Vec<u16> = path.encode_utf16().collect();
|
||||
buf.extend_from_slice(&(path_utf16.len() as u32 * 2).to_le_bytes());
|
||||
for ch in path_utf16 {
|
||||
buf.extend_from_slice(&ch.to_le_bytes());
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -125,6 +155,33 @@ mod tests {
|
||||
assert_eq!(req.request_bitmap, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aapl_resolve_id_request() {
|
||||
let mut data = [0u8; 32];
|
||||
data[0..4].copy_from_slice(&2u32.to_le_bytes()); // command = RESOLVE_ID
|
||||
data[24..32].copy_from_slice(&0x12345678u64.to_le_bytes()); // file_id
|
||||
let req = AaplCreateContextRequest::from_bytes(&data).unwrap();
|
||||
assert_eq!(req.command, SMB2_CRTCTX_AAPL_RESOLVE_ID);
|
||||
assert_eq!(req.resolve_file_id, Some(0x12345678));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_resolve_id_response() {
|
||||
let bytes = build_resolve_id_response("dir/file.txt");
|
||||
// header: command=2 (4B) + reserved=0 (4B) + request_bitmap=0 (8B) = 16 bytes
|
||||
assert_eq!(&bytes[0..4], &[2, 0, 0, 0]);
|
||||
// path length (UTF-16 = each char 2 bytes, 12 chars = 24 bytes)
|
||||
let path_len = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
|
||||
assert_eq!(path_len, 24);
|
||||
// path content
|
||||
let path_utf16: Vec<u16> = bytes[20..]
|
||||
.chunks(2)
|
||||
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||
.collect();
|
||||
let path = String::from_utf16(&path_utf16).unwrap();
|
||||
assert_eq!(path, "dir/file.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aapl_response_encode() {
|
||||
let resp = AaplCreateContextResponse::new_server_query(
|
||||
|
||||
Reference in New Issue
Block a user