diff --git a/src/authentication.rs b/src/authentication.rs new file mode 100644 index 0000000..4d2c6b7 --- /dev/null +++ b/src/authentication.rs @@ -0,0 +1,101 @@ +// Copyright 2014 Alexis Mousset. See the COPYRIGHT +// file at the top-level directory of this distribution. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Provides authentication mecanisms + +use serialize::base64::{self, ToBase64, FromBase64}; +use serialize::hex::ToHex; +use crypto::hmac::Hmac; +use crypto::md5::Md5; +use crypto::mac::Mac; + +use NUL; +use error::Error; +use extension::Extension; + +/// Represents an authentication mecanism +pub trait Mecanism { + /// Returns the matching `Extension` + fn extension() -> Extension; + /// Returns the initial response support + fn supports_initial_response() -> bool; + /// Returns the response + fn response(username: &str, password: &str, challenge: Option<&str>) -> Result; +} + +/// PLAIN authentication mecanism +/// RFC 4616: https://tools.ietf.org/html/rfc4616 +#[derive(Copy,Clone)] +pub struct Plain; + +impl Mecanism for Plain { + fn extension() -> Extension { + Extension::PlainAuthentication + } + + fn supports_initial_response() -> bool { + true + } + + fn response(username: &str, password: &str, challenge: Option<&str>) -> Result { + match challenge { + Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")), + None => Ok(format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD)), + } + } +} + +/// CRAM-MD5 authentication mecanism +/// RFC 2195: https://tools.ietf.org/html/rfc2195 +#[derive(Copy,Clone)] +pub struct CramMd5; + +impl Mecanism for CramMd5 { + fn extension() -> Extension { + Extension::CramMd5Authentication + } + + fn supports_initial_response() -> bool { + false + } + + fn response(username: &str, password: &str, challenge: Option<&str>) -> Result { + let encoded_challenge = match challenge { + Some(challenge) => challenge, + None => return Err(Error::ClientError("This mecanism does expect a challenge")), + }; + + let decoded_challenge = match encoded_challenge.from_base64() { + Ok(challenge) => challenge, + Err(error) => return Err(Error::ChallengeParsingError(error)), + }; + + let mut hmac = Hmac::new(Md5::new(), password.as_bytes()); + hmac.input(&decoded_challenge); + + Ok(format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD)) + } +} + +#[cfg(test)] +mod test { + use super::{Mecanism, Plain, CramMd5}; + + #[test] + fn test_plain() { + assert_eq!(Plain::response("username", "password", None).unwrap(), "AHVzZXJuYW1lAHBhc3N3b3Jk"); + } + + #[test] + fn test_cram_md5() { + assert_eq!(CramMd5::response("alice", "wonderland", + Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")).unwrap(), + "YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA="); + } +} diff --git a/src/client/authentication.rs b/src/client/authentication.rs deleted file mode 100644 index b3c8a5c..0000000 --- a/src/client/authentication.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2014 Alexis Mousset. See the COPYRIGHT -// file at the top-level directory of this distribution. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! Provides authentication functions - -use serialize::base64::{self, ToBase64, FromBase64}; -use serialize::hex::ToHex; -use crypto::hmac::Hmac; -use crypto::md5::Md5; -use crypto::mac::Mac; - -use NUL; -use error::Error; - -/// Returns a PLAIN mecanism response -pub fn plain(username: &str, password: &str) -> String { - format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD) -} - -/// Returns a CRAM-MD5 mecanism response -pub fn cram_md5(username: &str, password: &str, encoded_challenge: &str) -> Result { - let challenge = match encoded_challenge.from_base64() { - Ok(challenge) => challenge, - Err(error) => return Err(Error::ChallengeParsingError(error)), - }; - - let mut hmac = Hmac::new(Md5::new(), password.as_bytes()); - hmac.input(&challenge); - - Ok(format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD)) -} - -#[cfg(test)] -mod test { - use super::{plain, cram_md5}; - - #[test] - fn test_plain() { - assert_eq!(plain("username", "password"), "AHVzZXJuYW1lAHBhc3N3b3Jk"); - } - - #[test] - fn test_cram_md5() { - assert_eq!(cram_md5("alice", "wonderland", - "PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==").unwrap(), - "YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA="); - } -} diff --git a/src/client/mod.rs b/src/client/mod.rs index 5dbd5c4..405a4bc 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -16,13 +16,12 @@ use std::io::{BufRead, Read, Write}; use bufstream::BufStream; use response::ResponseParser; +use authentication::{Mecanism, CramMd5, Plain}; use error::{Error, SmtpResult}; use client::net::{Connector, SmtpStream}; -use client::authentication::{plain, cram_md5}; use {CRLF, MESSAGE_ENDING}; pub mod net; -mod authentication; /// Returns the string after adding a dot at the beginning of each line starting with a dot /// @@ -170,7 +169,7 @@ impl Client { /// Sends an AUTH command with PLAIN mecanism pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult { - self.command(&format!("AUTH PLAIN {}", plain(username, password))) + self.command(&format!("AUTH PLAIN {}", try!(Plain::response(username, password, None)))) } /// Sends an AUTH command with CRAM-MD5 mecanism @@ -180,7 +179,7 @@ impl Client { None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")), }; - let cram_response = try!(cram_md5(username, password, &encoded_challenge)); + let cram_response = try!(CramMd5::response(username, password, Some(&encoded_challenge))); self.command(&format!("AUTH CRAM-MD5 {}", cram_response)) } diff --git a/src/lib.rs b/src/lib.rs index cfdc21f..46437c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,6 +155,7 @@ pub mod response; pub mod error; pub mod sendable_email; pub mod email; +pub mod authentication; // Registrated port numbers: // https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml