Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

**/.vscode/
IMPLEMENTATION_PLAN.md
AUDIT.md
46 changes: 44 additions & 2 deletions smb-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ impl<T: Into<Box<dyn Error + Send + Sync>>> From<T> for SMBServerError {

impl Display for SMBServerError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "Parse failed with error: {}", self.error)
write!(f, "Server operation failed with error: {}", self.error)
}
}

Expand All @@ -201,4 +201,46 @@ impl Display for SMBError {
}
}

impl std::error::Error for SMBError {}
impl std::error::Error for SMBError {}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn server_error_display_says_server() {
let err = SMBError::server_error("something broke");
let msg = format!("{}", err);
assert!(msg.contains("Server operation failed"), "ServerError Display should say 'Server operation failed', got: {}", msg);
assert!(msg.contains("something broke"));
}

#[test]
fn parse_error_display_says_parse() {
let err = SMBError::parse_error("bad bytes");
let msg = format!("{}", err);
assert!(msg.contains("Parse failed"), "ParseError Display should say 'Parse failed', got: {}", msg);
}

#[test]
fn crypto_error_display_says_crypto() {
let err = SMBError::crypto_error("bad key");
let msg = format!("{}", err);
assert!(msg.contains("Crypto operation failed"), "CryptoError Display should say 'Crypto operation failed', got: {}", msg);
}

#[test]
fn payload_too_small_display() {
let err = SMBError::payload_too_small(64usize, 32usize);
let msg = format!("{}", err);
assert!(msg.contains("64"), "should mention expected size");
assert!(msg.contains("32"), "should mention actual size");
}

#[test]
fn response_error_display() {
let err = SMBError::response_error(NTStatus::AccessDenied);
let msg = format!("{}", err);
assert!(msg.contains("AccessDenied"), "should mention the NTStatus variant");
}
}
4 changes: 2 additions & 2 deletions smb/src/byte_helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub(crate) fn bytes_to_u64(bytes: &[u8]) -> u64 {
((bytes[4] as u64) << 32) |
((bytes[5] as u64) << 40) |
((bytes[6] as u64) << 48) |
((bytes[7] as u64) << 54)
((bytes[7] as u64) << 56)
}

pub(crate) fn u64_to_bytes(num: u64) -> [u8; 8] {
Expand All @@ -42,7 +42,7 @@ pub(crate) fn u64_to_bytes(num: u64) -> [u8; 8] {
((num >> 32) & 0xFF) as u8,
((num >> 40) & 0xFF) as u8,
((num >> 48) & 0xFF) as u8,
((num >> 54) & 0xFF) as u8,
((num >> 56) & 0xFF) as u8,
]
}

Expand Down
42 changes: 41 additions & 1 deletion smb/src/server/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ impl<S: Server> SMBSession<S> {
_ => ("SMB2AESCMAC", &smb_sign_bytes),
};
self.signing_key = match dialect {
SMBDialect::V3_0_0 | SMBDialect::V3_1_1 => generate_key(&self.session_key, signing_key_label, signing_key_context, signing_key_len),
SMBDialect::V3_0_0 | SMBDialect::V3_0_2 | SMBDialect::V3_1_1 => generate_key(&self.session_key, signing_key_label, signing_key_context, signing_key_len),
_ => self.session_key.clone().to_vec(),
};

Expand Down Expand Up @@ -355,4 +355,44 @@ impl<S: Server<Session=Self>> Session<S::Connection, S::AuthProvider, S::Open> f
fn signing_key(&self) -> &[u8] {
&self.signing_key
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn generate_key_produces_correct_length() {
let session_key = [0xAA; 16];
let key = generate_key(&session_key, "SMB2AESCMAC", b"SmbSign\0", 16);
assert_eq!(key.len(), 16);
}

#[test]
fn generate_key_is_not_raw_session_key() {
let session_key = [0xBB; 16];
let key = generate_key(&session_key, "SMB2AESCMAC", b"SmbSign\0", 16);
assert_ne!(key, session_key.to_vec(), "KDF output must differ from raw session key");
}

/// Regression test for B3: V3_0_2 must take the KDF branch, not the raw
/// session key fallback. We verify this by checking that the signing key
/// label/context selection and the match arm both cover V3_0_2.
#[test]
fn v3_0_2_uses_kdf_for_signing_key() {
let session_key = [0xCC; 16];
let smb_sign_bytes = [b"SmbSign".as_slice(), &[0]].concat();

// V3_0_2 should use the same label/context as V3_0_0 (not V3_1_1)
let label = "SMB2AESCMAC";
let context: &[u8] = &smb_sign_bytes;

let key_v302 = generate_key(&session_key, label, context, 16);
let key_v300 = generate_key(&session_key, label, context, 16);

// Both 3.0 and 3.0.2 use the same KDF path, so keys must match
assert_eq!(key_v302, key_v300, "V3_0_2 and V3_0_0 should derive identical signing keys");
// And neither should be the raw session key
assert_ne!(key_v302, session_key.to_vec(), "V3_0_2 signing key must not be the raw session key");
}
}
Loading