diff --git a/sasl/src/client/mechanisms/scram.rs b/sasl/src/client/mechanisms/scram.rs index 5d8287697626def0390c392e9806f1910f9690c3..262c8db2fe264fab1c54d0e03b7d00f151a67501 100644 --- a/sasl/src/client/mechanisms/scram.rs +++ b/sasl/src/client/mechanisms/scram.rs @@ -27,6 +27,8 @@ pub struct Scram { name_plus: String, username: String, password: Password, + client_first_extensions: String, + client_final_extensions: String, client_nonce: String, state: ScramState, channel_binding: ChannelBinding, @@ -49,6 +51,8 @@ impl Scram { name_plus: format!("SCRAM-{}-PLUS", S::name()), username: username.into(), password: password.into(), + client_first_extensions: String::new(), + client_final_extensions: String::new(), client_nonce: generate_nonce()?, state: ScramState::Init, channel_binding, @@ -56,6 +60,22 @@ impl Scram { }) } + /// Sets extension data to be inserted into the client's first message. + /// Extension data must be in the format of a comma seperated list of SCRAM extensions to be used e.g. `foo=true,bar=baz` + /// If not called, no extensions will be used for the clients first message. + pub fn with_first_extensions(mut self, extensions: String) -> Self { + self.client_first_extensions = extensions; + self + } + + /// Sets extension data to be inserted into the client's final message. + /// Extension data must be in the format of a comma seperated list of SCRAM extensions to be used e.g. `foo=true,bar=baz` + /// If not called, no extensions will be used for the clients final message. + pub fn with_final_extensions(mut self, extensions: String) -> Self { + self.client_final_extensions = extensions; + self + } + // Used for testing. #[doc(hidden)] #[cfg(test)] @@ -69,6 +89,8 @@ impl Scram { name_plus: format!("SCRAM-{}-PLUS", S::name()), username: username.into(), password: password.into(), + client_first_extensions: String::new(), + client_final_extensions: String::new(), client_nonce: nonce, state: ScramState::Init, channel_binding: ChannelBinding::None, @@ -107,6 +129,10 @@ impl Mechanism for Scram { bare.extend(self.username.bytes()); bare.extend(b",r="); bare.extend(self.client_nonce.bytes()); + if !self.client_first_extensions.is_empty() { + bare.extend(b","); + bare.extend(self.client_first_extensions.bytes()); + } let mut data = Vec::new(); data.extend(&gs2_header); data.extend(&bare); @@ -142,6 +168,10 @@ impl Mechanism for Scram { client_final_message_bare.extend(Base64.encode(&cb_data).bytes()); client_final_message_bare.extend(b",r="); client_final_message_bare.extend(server_nonce.bytes()); + if !self.client_final_extensions.is_empty() { + client_final_message_bare.extend(b","); + client_final_message_bare.extend(self.client_final_extensions.bytes()); + } let salted_password = S::derive(&self.password, &salt, iterations)?; let client_key = S::hmac(b"Client Key", &salted_password)?; let server_key = S::hmac(b"Server Key", &salted_password)?; @@ -247,4 +277,54 @@ mod tests { ); // again, depends on ordering… mechanism.success(&server_final[..]).unwrap(); } + + #[test] + fn scram_kafka_token_delegation_works() { + // credentials and raw messages taken from a real kafka SCRAM token delegation authentication + let username = "6Lbb79aSTs-mDWUPc64D9Q"; + let password = "O574x+7mB0B8R9Yt8DqwWbIzBgEm3lUE+fy7VWdvCwcLvGvwJK9GM4y0Qaz/MxiIxDHEnxDfSuB13uycXiUqyg=="; + let client_nonce = "o6wj2xqdu0fxe4nmnukkj076m"; + let client_init = b"n,,n=6Lbb79aSTs-mDWUPc64D9Q,r=o6wj2xqdu0fxe4nmnukkj076m,tokenauth=true"; + let server_init = b"r=o6wj2xqdu0fxe4nmnukkj076m1eut816hvmsycqw2qzyn14zxvr,s=MWVtNWw1Mzc1MnFianNoYWhqMjhyYzVzZHM=,i=4096"; + let client_final = b"c=biws,r=o6wj2xqdu0fxe4nmnukkj076m1eut816hvmsycqw2qzyn14zxvr,p=qVfqg28hDgroc6pal4qCF+8hO1/wiB84o7snGRDZKuE="; + let server_final = b"v=2ZSkAlHEUj6WehcizLhQRiiVGn+VDVtmAqj1v/IPa28="; + let mut mechanism = + Scram::::new_with_nonce(username, password, client_nonce.to_owned()) + .with_first_extensions("tokenauth=true".to_owned()); + let init = mechanism.initial(); + assert_eq!( + std::str::from_utf8(&init).unwrap(), + std::str::from_utf8(client_init).unwrap() + ); // depends on ordering… + let resp = mechanism.response(server_init).unwrap(); + assert_eq!( + std::str::from_utf8(&resp).unwrap(), + std::str::from_utf8(client_final).unwrap() + ); // again, depends on ordering… + mechanism.success(server_final).unwrap(); + } + + #[test] + fn scram_final_extension_works() { + let username = "some_user"; + let password = "a_password"; + let client_nonce = "client_nonce"; + let client_init = b"n,,n=some_user,r=client_nonce"; + let server_init = + b"r=client_nonceserver_nonce,s=MWVtNWw1Mzc1MnFianNoYWhqMjhyYzVzZHM=,i=4096"; + let client_final = b"c=biws,r=client_nonceserver_nonce,foo=true,p=T9XQLmykBv74DzbaCtX90/ElJYJU2XWM/jHmHJ+BI/w="; + let mut mechanism = + Scram::::new_with_nonce(username, password, client_nonce.to_owned()) + .with_final_extensions("foo=true".to_owned()); + let init = mechanism.initial(); + assert_eq!( + std::str::from_utf8(&init).unwrap(), + std::str::from_utf8(client_init).unwrap() + ); // depends on ordering… + let resp = mechanism.response(server_init).unwrap(); + assert_eq!( + std::str::from_utf8(&resp).unwrap(), + std::str::from_utf8(client_final).unwrap() + ); // again, depends on ordering… + } }