diff --git a/sasl/Cargo.toml b/sasl/Cargo.toml index 7e06571..93e8271 100644 --- a/sasl/Cargo.toml +++ b/sasl/Cargo.toml @@ -2,7 +2,7 @@ name = "sasl" version = "0.1.1" authors = ["lumi "] -description = "A crate for SASL authentication. Still needs a bunch of documenation." +description = "A crate for SASL authentication." homepage = "https://gitlab.com/lumi/sasl-rs" repository = "https://gitlab.com/lumi/sasl-rs" documentation = "https://docs.rs/sasl" diff --git a/sasl/README.md b/sasl/README.md index 5524494..0b66d0f 100644 --- a/sasl/README.md +++ b/sasl/README.md @@ -6,6 +6,11 @@ What's this? A crate which handles SASL authentication. +Can I see an example? +--------------------- + +Look at the documentation [here](https://docs.rs/sasl). + What license is it under? ------------------------- diff --git a/sasl/src/lib.rs b/sasl/src/lib.rs index fc81505..15dbd95 100644 --- a/sasl/src/lib.rs +++ b/sasl/src/lib.rs @@ -5,14 +5,12 @@ //! # Examples //! //! ```rust -//! use sasl::{SaslCredentials, SaslSecret, SaslMechanism, Error}; +//! use sasl::{SaslCredentials, SaslMechanism, Error}; //! use sasl::mechanisms::Plain; //! -//! let creds = SaslCredentials { -//! username: "user".to_owned(), -//! secret: SaslSecret::Password("pencil".to_owned()), -//! channel_binding: None, -//! }; +//! let creds = SaslCredentials::default() +//! .with_username("user") +//! .with_password("pencil"); //! //! let mut mechanism = Plain::from_credentials(creds).unwrap(); //! @@ -39,16 +37,75 @@ mod error; pub use error::Error; /// A struct containing SASL credentials. +#[derive(Clone, Debug)] pub struct SaslCredentials { /// The requested username. - pub username: String, // TODO: change this since some mechanisms do not use it + pub username: Option, /// The secret used to authenticate. pub secret: SaslSecret, - /// Optionally, channel binding data, for *-PLUS mechanisms. - pub channel_binding: Option>, + /// Channel binding data, for *-PLUS mechanisms. + pub channel_binding: ChannelBinding, +} + +impl Default for SaslCredentials { + fn default() -> SaslCredentials { + SaslCredentials { + username: None, + secret: SaslSecret::None, + channel_binding: ChannelBinding::None, + } + } +} + +impl SaslCredentials { + /// Creates a new SaslCredentials with the specified username. + pub fn with_username>(mut self, username: N) -> SaslCredentials { + self.username = Some(username.into()); + self + } + + /// Creates a new SaslCredentials with the specified password. + pub fn with_password>(mut self, password: P) -> SaslCredentials { + self.secret = SaslSecret::Password(password.into()); + self + } + + /// Creates a new SaslCredentials with the specified chanel binding. + pub fn with_channel_binding(mut self, channel_binding: ChannelBinding) -> SaslCredentials { + self.channel_binding = channel_binding; + self + } +} + +/// Channel binding configuration. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ChannelBinding { + /// No channel binding data. + None, + /// p=tls-unique channel binding data. + TlsUnique(Vec), +} + +impl ChannelBinding { + /// Return the gs2 header for this channel binding mechanism. + pub fn header(&self) -> &[u8] { + match *self { + ChannelBinding::None => b"n,,", + ChannelBinding::TlsUnique(_) => b"p=tls-unique,,", + } + } + + /// Return the channel binding data for this channel binding mechanism. + pub fn data(&self) -> &[u8] { + match *self { + ChannelBinding::None => &[], + ChannelBinding::TlsUnique(ref data) => data, + } + } } /// Represents a SASL secret, like a password. +#[derive(Clone, Debug, PartialEq, Eq)] pub enum SaslSecret { /// No extra data needed. None, diff --git a/sasl/src/mechanisms/plain.rs b/sasl/src/mechanisms/plain.rs index d77b70c..2b4a270 100644 --- a/sasl/src/mechanisms/plain.rs +++ b/sasl/src/mechanisms/plain.rs @@ -30,7 +30,11 @@ impl SaslMechanism for Plain { fn from_credentials(credentials: SaslCredentials) -> Result { if let SaslSecret::Password(password) = credentials.secret { - Ok(Plain::new(credentials.username, password)) + if let Some(username) = credentials.username { + Ok(Plain::new(username, password)) + } else { + Err("PLAIN requires a username".to_owned()) + } } else { Err("PLAIN requires a password".to_owned()) } diff --git a/sasl/src/mechanisms/scram.rs b/sasl/src/mechanisms/scram.rs index 757e8cc..7b7551a 100644 --- a/sasl/src/mechanisms/scram.rs +++ b/sasl/src/mechanisms/scram.rs @@ -2,6 +2,7 @@ use base64; +use ChannelBinding; use SaslCredentials; use SaslMechanism; use SaslSecret; @@ -153,18 +154,20 @@ pub struct Scram { password: String, client_nonce: String, state: ScramState, - channel_binding: Option>, + channel_binding: ChannelBinding, _marker: PhantomData, } impl Scram { - /// Constructs a new struct for authenticating using the SASL SCRAM-* mechanism. + /// Constructs a new struct for authenticating using the SASL SCRAM-* and SCRAM-*-PLUS + /// mechanisms, depending on the passed channel binding. /// /// It is recommended that instead you use a `SaslCredentials` struct and turn it into the /// requested mechanism using `from_credentials`. pub fn new, P: Into>( username: N, password: P, + channel_binding: ChannelBinding, ) -> Result, Error> { Ok(Scram { name: format!("SCRAM-{}", S::name()), @@ -172,17 +175,14 @@ impl Scram { password: password.into(), client_nonce: generate_nonce()?, state: ScramState::Init, - channel_binding: None, + channel_binding: channel_binding, _marker: PhantomData, }) } - /// Constructs a new struct for authenticating using the SASL SCRAM-* mechanism. - /// - /// This one takes a nonce instead of generating it. - /// - /// It is recommended that instead you use a `SaslCredentials` struct and turn it into the - /// requested mechanism using `from_credentials`. + // Used for testing. + #[doc(hidden)] + #[cfg(test)] pub fn new_with_nonce, P: Into>( username: N, password: P, @@ -194,33 +194,10 @@ impl Scram { password: password.into(), client_nonce: nonce, state: ScramState::Init, - channel_binding: None, + channel_binding: ChannelBinding::None, _marker: PhantomData, } } - - /// Constructs a new struct for authenticating using the SASL SCRAM-*-PLUS mechanism. - /// - /// This means that this function will also take the channel binding data. - /// - /// It is recommended that instead you use a `SaslCredentials` struct and turn it into the - /// requested mechanism using `from_credentials`. - pub fn new_with_channel_binding, P: Into>( - username: N, - password: P, - channel_binding: Vec, - ) -> Result, Error> { - // TODO: channel binding modes other than tls-unique - 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 { @@ -231,12 +208,11 @@ impl SaslMechanism for Scram { 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) + if let Some(username) = credentials.username { + Scram::new(username, password, credentials.channel_binding) .map_err(|_| "can't generate nonce".to_owned()) } else { - Scram::new(credentials.username, password) - .map_err(|_| "can't generate nonce".to_owned()) + Err("SCRAM requires a username".to_owned()) } } else { Err("SCRAM requires a password".to_owned()) @@ -245,11 +221,7 @@ impl SaslMechanism for Scram { 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,,"); - } + gs2_header.extend(self.channel_binding.header()); let mut bare = Vec::new(); bare.extend(b"n="); bare.extend(self.username.bytes()); @@ -286,9 +258,7 @@ impl SaslMechanism for Scram { 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); - } + cb_data.extend(self.channel_binding.data()); 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());