From ff7387a92a93ab323fb91a1a0c550ecf93291fd4 Mon Sep 17 00:00:00 2001 From: lumi Date: Fri, 24 Feb 2017 23:42:08 +0100 Subject: [PATCH] support SCRAM-SHA-1 --- examples/client.rs | 7 +- src/client.rs | 13 +- src/sasl/mechanisms/mod.rs | 2 + src/sasl/mechanisms/plain.rs | 4 +- src/sasl/mechanisms/scram.rs | 254 +++++++++++++++++++++++++++++++++++ src/sasl/mod.rs | 13 +- 6 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 src/sasl/mechanisms/scram.rs diff --git a/examples/client.rs b/examples/client.rs index 7a6e2f9..4debe80 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -4,7 +4,7 @@ use xmpp::jid::Jid; use xmpp::client::ClientBuilder; use xmpp::plugins::messaging::{MessagingPlugin, MessageEvent}; use xmpp::plugins::presence::{PresencePlugin, Show}; -use xmpp::sasl::mechanisms::Plain; +use xmpp::sasl::mechanisms::{Scram, Sha1, Plain}; use std::env; @@ -14,7 +14,10 @@ fn main() { client.register_plugin(MessagingPlugin::new()); client.register_plugin(PresencePlugin::new()); let pass = env::var("PASS").unwrap(); - client.connect(&mut Plain::new(jid.node.clone().expect("JID requires a node"), pass)).unwrap(); + let name = jid.node.clone().expect("JID requires a node"); + client.connect(&mut Plain::new(name, pass)).unwrap(); + // Replace with this line if you want SCRAM-SHA-1 authentication: + // client.connect(&mut Scram::::new(name, pass).unwrap()).unwrap(); client.plugin::().set_presence(Show::Available, None).unwrap(); loop { let event = client.next_event().unwrap(); diff --git a/src/client.rs b/src/client.rs index fdabe4f..674c633 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,6 +16,7 @@ use xml::reader::XmlEvent as ReaderEvent; use std::sync::mpsc::{Receiver, channel}; /// Struct that should be moved somewhere else and cleaned up. +#[derive(Debug)] pub struct StreamFeatures { pub sasl_mechanisms: Option>, } @@ -129,7 +130,7 @@ impl Client { /// Connects and authenticates using the specified SASL mechanism. pub fn connect(&mut self, mechanism: &mut S) -> Result<(), Error> { self.wait_for_features()?; - let auth = mechanism.initial(); + let auth = mechanism.initial().map_err(|x| Error::SaslError(Some(x)))?; let mut elem = Element::builder("auth") .ns(ns::SASL) .attr("mechanism", S::name()) @@ -148,7 +149,7 @@ impl Client { else { base64::decode(&text)? }; - let response = mechanism.response(&challenge); + let response = mechanism.response(&challenge).map_err(|x| Error::SaslError(Some(x)))?; let mut elem = Element::builder("response") .ns(ns::SASL) .build(); @@ -158,6 +159,14 @@ impl Client { self.transport.write_element(&elem)?; } else if n.is("success", ns::SASL) { + let text = n.text(); + let data = if text == "" { + Vec::new() + } + else { + base64::decode(&text)? + }; + mechanism.success(&data).map_err(|x| Error::SaslError(Some(x)))?; self.transport.reset_stream(); C2S::init(&mut self.transport, &self.jid.domain, "after_sasl")?; return self.bind(); diff --git a/src/sasl/mechanisms/mod.rs b/src/sasl/mechanisms/mod.rs index 64a47ec..c1d4bc2 100644 --- a/src/sasl/mechanisms/mod.rs +++ b/src/sasl/mechanisms/mod.rs @@ -2,6 +2,8 @@ mod anonymous; mod plain; +mod scram; pub use self::anonymous::Anonymous; pub use self::plain::Plain; +pub use self::scram::{Scram, Sha1, ScramProvider}; diff --git a/src/sasl/mechanisms/plain.rs b/src/sasl/mechanisms/plain.rs index ea43472..5c50828 100644 --- a/src/sasl/mechanisms/plain.rs +++ b/src/sasl/mechanisms/plain.rs @@ -19,12 +19,12 @@ impl Plain { impl SaslMechanism for Plain { fn name() -> &'static str { "PLAIN" } - fn initial(&mut self) -> Vec { + fn initial(&mut self) -> Result, String> { let mut auth = Vec::new(); auth.push(0); auth.extend(self.name.bytes()); auth.push(0); auth.extend(self.password.bytes()); - auth + Ok(auth) } } diff --git a/src/sasl/mechanisms/scram.rs b/src/sasl/mechanisms/scram.rs new file mode 100644 index 0000000..7b426c0 --- /dev/null +++ b/src/sasl/mechanisms/scram.rs @@ -0,0 +1,254 @@ +//! Provides the SASL "SCRAM-*" mechanisms and a way to implement more. + +use base64; + +use sasl::SaslMechanism; + +use error::Error; + +use openssl::pkcs5::pbkdf2_hmac; +use openssl::hash::hash; +use openssl::hash::MessageDigest; +use openssl::sign::Signer; +use openssl::pkey::PKey; +use openssl::rand::rand_bytes; +use openssl::error::ErrorStack; + +use std::marker::PhantomData; + +#[cfg(test)] +#[test] +fn xor_works() { + assert_eq!( xor( &[135, 94, 53, 134, 73, 233, 140, 221, 150, 12, 96, 111, 54, 66, 11, 76] + , &[163, 9, 122, 180, 107, 44, 22, 252, 248, 134, 112, 82, 84, 122, 56, 209] ) + , &[36, 87, 79, 50, 34, 197, 154, 33, 110, 138, 16, 61, 98, 56, 51, 157] ); +} + +fn xor(a: &[u8], b: &[u8]) -> Vec { + assert_eq!(a.len(), b.len()); + let mut ret = Vec::with_capacity(a.len()); + for (a, b) in a.into_iter().zip(b) { + ret.push(a ^ b); + } + ret +} + +fn generate_nonce() -> Result { + let mut data = vec![0; 32]; + rand_bytes(&mut data)?; + Ok(base64::encode(&data)) +} + +pub trait ScramProvider { + fn name() -> &'static str; + fn hash(data: &[u8]) -> Vec; + fn hmac(data: &[u8], key: &[u8]) -> Vec; + fn derive(data: &[u8], salt: &[u8], iterations: usize) -> Vec; +} + +pub struct Sha1; + +impl ScramProvider for Sha1 { // TODO: look at all these unwraps + fn name() -> &'static str { "SCRAM-SHA-1" } + + fn hash(data: &[u8]) -> Vec { + hash(MessageDigest::sha1(), data).unwrap() + } + + fn hmac(data: &[u8], key: &[u8]) -> Vec { + let pkey = PKey::hmac(key).unwrap(); + let mut signer = Signer::new(MessageDigest::sha1(), &pkey).unwrap(); + signer.update(data).unwrap(); + signer.finish().unwrap() + } + + fn derive(data: &[u8], salt: &[u8], iterations: usize) -> Vec { + let mut result = vec![0; 20]; + pbkdf2_hmac(data, salt, iterations, MessageDigest::sha1(), &mut result).unwrap(); + result + } +} + +enum ScramState { + Init, + SentInitialMessage { initial_message: Vec }, + GotServerData { server_signature: Vec }, +} + +pub struct Scram { + name: String, + password: String, + client_nonce: String, + state: ScramState, + _marker: PhantomData, +} + +impl Scram { + pub fn new, P: Into>(name: N, password: P) -> Result, Error> { + Ok(Scram { + name: name.into(), + password: password.into(), + client_nonce: generate_nonce()?, + state: ScramState::Init, + _marker: PhantomData, + }) + } + + pub fn new_with_nonce, P: Into>(name: N, password: P, nonce: String) -> Scram { + Scram { + name: name.into(), + password: password.into(), + client_nonce: nonce, + state: ScramState::Init, + _marker: PhantomData, + } + } +} + +impl SaslMechanism for Scram { + fn name() -> &'static str { + S::name() + } + + fn initial(&mut self) -> Result, String> { + let mut bare = Vec::new(); + bare.extend(b"n="); + bare.extend(self.name.bytes()); + bare.extend(b",r="); + bare.extend(self.client_nonce.bytes()); + self.state = ScramState::SentInitialMessage { initial_message: bare.clone() }; + let mut data = Vec::new(); + data.extend(b"n,,"); + data.extend(bare); + Ok(data) + } + + fn response(&mut self, challenge: &[u8]) -> Result, String> { + let next_state; + let ret; + match self.state { + ScramState::SentInitialMessage { ref initial_message } => { + let chal = String::from_utf8(challenge.to_owned()).map_err(|_| "can't decode challenge".to_owned())?; + let mut server_nonce: Option = None; + let mut salt: Option> = None; + let mut iterations: Option = None; + for s in chal.split(',') { + let mut tmp = s.splitn(2, '='); + let key = tmp.next(); + if let Some(val) = tmp.next() { + match key { + Some("r") => { + if val.starts_with(&self.client_nonce) { + server_nonce = Some(val.to_owned()); + } + }, + Some("s") => { + if let Ok(s) = base64::decode(val) { + salt = Some(s); + } + }, + Some("i") => { + if let Ok(iters) = val.parse() { + iterations = Some(iters); + } + }, + _ => (), + } + } + } + let server_nonce = server_nonce.ok_or_else(|| "no server nonce".to_owned())?; + let salt = salt.ok_or_else(|| "no server salt".to_owned())?; + let iterations = iterations.ok_or_else(|| "no server iterations".to_owned())?; + // TODO: SASLprep + let mut client_final_message_bare = Vec::new(); + client_final_message_bare.extend(b"c=biws,r="); + client_final_message_bare.extend(server_nonce.bytes()); + let salted_password = S::derive(self.password.as_bytes(), &salt, iterations); + let client_key = S::hmac(b"Client Key", &salted_password); + let server_key = S::hmac(b"Server Key", &salted_password); + let mut auth_message = Vec::new(); + auth_message.extend(initial_message); + auth_message.push(b','); + auth_message.extend(challenge); + auth_message.push(b','); + auth_message.extend(&client_final_message_bare); + let stored_key = S::hash(&client_key); + let client_signature = S::hmac(&auth_message, &stored_key); + let client_proof = xor(&client_key, &client_signature); + let server_signature = S::hmac(&auth_message, &server_key); + let mut client_final_message = Vec::new(); + client_final_message.extend(&client_final_message_bare); + client_final_message.extend(b",p="); + client_final_message.extend(base64::encode(&client_proof).bytes()); + next_state = ScramState::GotServerData { + server_signature: server_signature, + }; + ret = client_final_message; + }, + _ => { return Err("not in the right state to receive this response".to_owned()); } + } + self.state = next_state; + Ok(ret) + } + + fn success(&mut self, data: &[u8]) -> Result<(), String> { + let data = String::from_utf8(data.to_owned()).map_err(|_| "can't decode success message".to_owned())?; + let mut received_signature = None; + match self.state { + ScramState::GotServerData { ref server_signature } => { + for s in data.split(',') { + let mut tmp = s.splitn(2, '='); + let key = tmp.next(); + if let Some(val) = tmp.next() { + match key { + Some("v") => { + if let Ok(v) = base64::decode(val) { + received_signature = Some(v); + } + }, + _ => (), + } + } + } + if let Some(sig) = received_signature { + if sig == *server_signature { + Ok(()) + } + else { + Err("invalid signature in success response".to_owned()) + } + } + else { + Err("no signature in success response".to_owned()) + } + }, + _ => Err("not in the right state to get a success response".to_owned()), + } + } +} + +#[cfg(test)] +mod tests { + use sasl::SaslMechanism; + + use super::*; + + #[test] + fn scram_sha1_works() { // Source: https://wiki.xmpp.org/web/SASLandSCRAM-SHA-1 + let username = "user"; + let password = "pencil"; + let client_nonce = "fyko+d2lbbFgONRv9qkxdawL"; + let client_init = b"n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"; + let server_init = b"r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096"; + let client_final = b"c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="; + let server_final = b"v=rmF9pqV8S7suAoZWja4dJRkFsKQ="; + let mut mechanism = Scram::::new_with_nonce(username, password, client_nonce.to_owned()); + let init = mechanism.initial().unwrap(); + assert_eq!( String::from_utf8(init.clone()).unwrap() + , String::from_utf8(client_init[..].to_owned()).unwrap() ); // depends on ordering… + let resp = mechanism.response(&server_init[..]).unwrap(); + assert_eq!( String::from_utf8(resp.clone()).unwrap() + , String::from_utf8(client_final[..].to_owned()).unwrap() ); // again, depends on ordering… + mechanism.success(&server_final[..]).unwrap(); + } +} diff --git a/src/sasl/mod.rs b/src/sasl/mod.rs index 6f6aa0b..400543b 100644 --- a/src/sasl/mod.rs +++ b/src/sasl/mod.rs @@ -5,13 +5,18 @@ pub trait SaslMechanism { fn name() -> &'static str; /// Provides initial payload of the SASL mechanism. - fn initial(&mut self) -> Vec { - Vec::new() + fn initial(&mut self) -> Result, String> { + Ok(Vec::new()) } /// Creates a response to the SASL challenge. - fn response(&mut self, _challenge: &[u8]) -> Vec { - Vec::new() + fn response(&mut self, _challenge: &[u8]) -> Result, String> { + Ok(Vec::new()) + } + + /// Verifies the server success response, if there is one. + fn success(&mut self, _data: &[u8]) -> Result<(), String> { + Ok(()) } }