diff --git a/Cargo.toml b/Cargo.toml index ae64e75d..6688edc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,6 @@ minidom = "0.1.0" [dependencies.jid] git = "https://gitlab.com/lumi/jid-rs.git" + +[dependencies.sasl] +git = "https://gitlab.com/lumi/sasl-rs.git" diff --git a/src/lib.rs b/src/lib.rs index e741157f..ec06e5b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ extern crate openssl; extern crate minidom; extern crate base64; pub extern crate jid; +pub extern crate sasl; pub mod ns; pub mod transport; @@ -12,6 +13,5 @@ pub mod plugin; pub mod event; pub mod plugins; pub mod connection; -pub mod sasl; mod locked_io; diff --git a/src/sasl/mechanisms/anonymous.rs b/src/sasl/mechanisms/anonymous.rs deleted file mode 100644 index 32538e5b..00000000 --- a/src/sasl/mechanisms/anonymous.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Provides the SASL "ANONYMOUS" mechanism. - -use sasl::{SaslMechanism, SaslCredentials, SaslSecret}; - -pub struct Anonymous; - -impl Anonymous { - pub fn new() -> Anonymous { - Anonymous - } -} - -impl SaslMechanism for Anonymous { - fn name(&self) -> &str { "ANONYMOUS" } - - fn from_credentials(credentials: SaslCredentials) -> Result { - if let SaslSecret::None = credentials.secret { - Ok(Anonymous) - } - else { - Err("the anonymous sasl mechanism requires no credentials".to_owned()) - } - } -} diff --git a/src/sasl/mechanisms/mod.rs b/src/sasl/mechanisms/mod.rs deleted file mode 100644 index 59463ac0..00000000 --- a/src/sasl/mechanisms/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -///! Provides a few SASL mechanisms. - -mod anonymous; -mod plain; -mod scram; - -pub use self::anonymous::Anonymous; -pub use self::plain::Plain; -pub use self::scram::{Scram, Sha1, Sha256, ScramProvider}; diff --git a/src/sasl/mechanisms/plain.rs b/src/sasl/mechanisms/plain.rs deleted file mode 100644 index b3739a87..00000000 --- a/src/sasl/mechanisms/plain.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Provides the SASL "PLAIN" mechanism. - -use sasl::{SaslMechanism, SaslCredentials, SaslSecret}; - -pub struct Plain { - username: String, - password: String, -} - -impl Plain { - pub fn new, P: Into>(username: N, password: P) -> Plain { - Plain { - username: username.into(), - password: password.into(), - } - } -} - -impl SaslMechanism for Plain { - fn name(&self) -> &str { "PLAIN" } - - fn from_credentials(credentials: SaslCredentials) -> Result { - if let SaslSecret::Password(password) = credentials.secret { - Ok(Plain::new(credentials.username, password)) - } - else { - Err("PLAIN requires a password".to_owned()) - } - } - - fn initial(&mut self) -> Result, String> { - let mut auth = Vec::new(); - auth.push(0); - auth.extend(self.username.bytes()); - auth.push(0); - auth.extend(self.password.bytes()); - Ok(auth) - } -} diff --git a/src/sasl/mechanisms/scram.rs b/src/sasl/mechanisms/scram.rs deleted file mode 100644 index 41fb2c52..00000000 --- a/src/sasl/mechanisms/scram.rs +++ /dev/null @@ -1,326 +0,0 @@ -//! Provides the SASL "SCRAM-*" mechanisms and a way to implement more. - -use base64; - -use sasl::{SaslMechanism, SaslCredentials, SaslSecret}; - -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; - -use std::collections::HashMap; - -use std::string::FromUtf8Error; - -#[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 parse_frame(frame: &[u8]) -> Result, FromUtf8Error> { - let inner = String::from_utf8(frame.to_owned())?; - let mut ret = HashMap::new(); - for s in inner.split(',') { - let mut tmp = s.splitn(2, '='); - let key = tmp.next(); - let val = tmp.next(); - match (key, val) { - (Some(k), Some(v)) => { - ret.insert(k.to_owned(), v.to_owned()); - }, - _ =>(), - } - } - Ok(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 { "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 - } -} - -pub struct Sha256; - -impl ScramProvider for Sha256 { // TODO: look at all these unwraps - fn name() -> &'static str { "SHA-256" } - - fn hash(data: &[u8]) -> Vec { - hash(MessageDigest::sha256(), data).unwrap() - } - - fn hmac(data: &[u8], key: &[u8]) -> Vec { - let pkey = PKey::hmac(key).unwrap(); - let mut signer = Signer::new(MessageDigest::sha256(), &pkey).unwrap(); - signer.update(data).unwrap(); - signer.finish().unwrap() - } - - fn derive(data: &[u8], salt: &[u8], iterations: usize) -> Vec { - let mut result = vec![0; 32]; - pbkdf2_hmac(data, salt, iterations, MessageDigest::sha256(), &mut result).unwrap(); - result - } -} - -enum ScramState { - Init, - SentInitialMessage { initial_message: Vec, gs2_header: Vec}, - GotServerData { server_signature: Vec }, -} - -pub struct Scram { - name: String, - username: String, - password: String, - client_nonce: String, - state: ScramState, - channel_binding: Option>, - _marker: PhantomData, -} - -impl Scram { - pub fn new, P: Into>(username: N, password: P) -> Result, Error> { - Ok(Scram { - name: format!("SCRAM-{}", S::name()), - username: username.into(), - password: password.into(), - client_nonce: generate_nonce()?, - state: ScramState::Init, - channel_binding: None, - _marker: PhantomData, - }) - } - - pub fn new_with_nonce, P: Into>(username: N, password: P, nonce: String) -> Scram { - Scram { - name: format!("SCRAM-{}", S::name()), - username: username.into(), - password: password.into(), - client_nonce: nonce, - state: ScramState::Init, - channel_binding: None, - _marker: PhantomData, - } - } - - pub fn new_with_channel_binding, P: Into>(username: N, password: P, channel_binding: Vec) -> Result, Error> { - Ok(Scram { - name: format!("SCRAM-{}-PLUS", S::name()), - username: username.into(), - password: password.into(), - client_nonce: generate_nonce()?, - state: ScramState::Init, - channel_binding: Some(channel_binding), - _marker: PhantomData, - }) - } -} - -impl SaslMechanism for Scram { - fn name(&self) -> &str { // TODO: this is quite the workaround… - &self.name - } - - fn from_credentials(credentials: SaslCredentials) -> Result, String> { - if let SaslSecret::Password(password) = credentials.secret { - if let Some(binding) = credentials.channel_binding { - Scram::new_with_channel_binding(credentials.username, password, binding) - .map_err(|_| "can't generate nonce".to_owned()) - } - else { - Scram::new(credentials.username, password) - .map_err(|_| "can't generate nonce".to_owned()) - } - } - else { - Err("SCRAM requires a password".to_owned()) - } - } - - fn initial(&mut self) -> Result, String> { - let mut gs2_header = Vec::new(); - if let Some(_) = self.channel_binding { - gs2_header.extend(b"p=tls-unique,,"); - } - else { - gs2_header.extend(b"n,,"); - } - let mut bare = Vec::new(); - bare.extend(b"n="); - bare.extend(self.username.bytes()); - bare.extend(b",r="); - bare.extend(self.client_nonce.bytes()); - let mut data = Vec::new(); - data.extend(&gs2_header); - data.extend(bare.clone()); - self.state = ScramState::SentInitialMessage { initial_message: bare, gs2_header: gs2_header }; - Ok(data) - } - - fn response(&mut self, challenge: &[u8]) -> Result, String> { - let next_state; - let ret; - match self.state { - ScramState::SentInitialMessage { ref initial_message, ref gs2_header } => { - let frame = parse_frame(challenge).map_err(|_| "can't decode challenge".to_owned())?; - let server_nonce = frame.get("r"); - let salt = frame.get("s").and_then(|v| base64::decode(v).ok()); - let iterations = frame.get("i").and_then(|v| v.parse().ok()); - 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="); - let mut cb_data: Vec = Vec::new(); - cb_data.extend(gs2_header); - if let Some(ref cb) = self.channel_binding { - cb_data.extend(cb); - } - 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()); - 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 frame = parse_frame(data).map_err(|_| "can't decode success response".to_owned())?; - match self.state { - ScramState::GotServerData { ref server_signature } => { - if let Some(sig) = frame.get("v").and_then(|v| base64::decode(&v).ok()) { - 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(); - } - - #[test] - fn scram_sha256_works() { // Source: RFC 7677 - let username = "user"; - let password = "pencil"; - let client_nonce = "rOprNGfwEbeRWgbNEkqO"; - let client_init = b"n,,n=user,r=rOprNGfwEbeRWgbNEkqO"; - let server_init = b"r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"; - let client_final = b"c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ="; - let server_final = b"v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4="; - 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 deleted file mode 100644 index 1c489f57..00000000 --- a/src/sasl/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Provides the `SaslMechanism` trait and some implementations. - -/// A struct containing SASL credentials. -pub struct SaslCredentials { - pub username: String, - pub secret: SaslSecret, - pub channel_binding: Option>, -} - -/// Represents a SASL secret, like a password. -pub enum SaslSecret { - /// No extra data needed. - None, - /// Password required. - Password(String), -} - -pub trait SaslMechanism { - /// The name of the mechanism. - fn name(&self) -> &str; - - /// Creates this mechanism from `SaslCredentials`. - fn from_credentials(credentials: SaslCredentials) -> Result where Self: Sized; - - /// Provides initial payload of the SASL mechanism. - fn initial(&mut self) -> Result, String> { - Ok(Vec::new()) - } - - /// Creates a response to the SASL challenge. - 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(()) - } -} - -pub mod mechanisms;