passwors 0.1.1

Passwors is a simple password handling library that utilises Rust's type system to enfore better password handling. Use it as a basic building block for more complex authentication systems.

use super::{ HashedPassword, PasswordHasher, HashSalt };
use std::io;
use std::ptr;
use std::mem;
use std::intrinsics::volatile_set_memory;



/// The maximum length of the clear text password buffer.
///
/// If you decide to lower this length, you are a bad
/// person. If, however, you decide that you need more
/// password bytes for your hashing function, then...
/// well... go on!
pub const CLEAR_TEXT_LEN: usize = 512 - 8;
// You might want to ask why I chose this weird number
// `512 - 8` instead of just `512`. Why at all `512`
// and not `777`? Well, many allocators organise their
// memory in size classes of powers of two. Chunks of
// 64 bytes, chunks of 8 bytes, chunks of 2KiB, and so on.
// The password's data additionally contains a length
// indicator, which might be up to 8 bytes in size depending
// on the target platform. So, this "weird" size ensures
// that the password data's size is close to or exactly a
// big power of two, making most use of the actual memory
// reserved by an allocator.
//
// Btw., sorry that you can only change this number by
// changing the source code directly. I'd like to make it
// more like this:
//
//     = parse_int!(option_env!("PASSWORS_CLEAR_TEXT_LEN").unwrap_or("0"))
//
// However, this isn't possible without extra fancy compiler
// plugins and yet even more constant functions.

/// All clear text passwords must have at least this
/// size in bytes.
///
/// Obviously, `default` does not fulfill this constraint.
pub const MIN_CLEAR_TEXT_LEN: usize = 16;



/// A clear text password is a fixed-sized buffer
/// of raw password bytes data.
///
/// # Why?
///
/// ## Why fixed-sized?
///
/// Well, to avoid giving your password hashing functions way
/// too much work, pretty much everyone sets a maximum length
/// for passwords. The important thing here is to set this
/// maximum length to a reasonably high limit. `passwors`
/// enforces a minimum buffer size of 248 bytes, which should
/// be small enough to not kill your hashing functions and
/// large enough to make even the most paranoid password owners
/// happy.
///
/// ## Why risk breaking multibyte characters?
///
/// Keeping unicode characters intact only matters if you handle
/// passwords like text someone is meant to read. However, noöne
/// should be reading passwords, except for maybe the password's
/// owner while he/she/it is typing the password into some form.
/// Okay, right, the hashing functions should be able to read the
/// password data as well. However, they don't care whether the
/// password is a markdown document or a PNG image. All they care
/// about is raw bytes.
///
/// ## Why not make it `Copy` or `Clone`?
///
/// Clear text passwords have only one raison d'être: Getting
/// hashed. This is also the reason why there are no functions
/// for inspecting the password's contents, not even its length.
/// And as they only should be hashed, there is no reason to
/// have multiple copies of clear text password's in memory.
///
/// ## Why zero-filling the password on `Drop`?
///
/// As mentioned before, clear text passwords only exist to be
/// hashed. Thus, there shouldn't be multiple copies of the
/// password running around in memory. Create it, hash it, forget
/// it. And zero-filling prevents others from allocating old
/// password memory and reading its old contents.
///
/// ## Why boxing the password data?
///
/// Although this sadly complicates making this crate `no_std`
/// compatible, this is a necessary step for ensuring a password's
/// uniqueness in memory. As there currently is no easy way to
/// modify how Rust moves values around, I cannot prevent Rust
/// from leaving non-zeroed password copies behind. I also cannot
/// prevent Rust from allowing to move clear text passwords around.
/// Therefore, I use boxed password data, including the password's
/// size. The boxed pointer can safely leave non-nulled pointer
/// copies behind, without leaking any information about the
/// corresponding password data.
///
/// ## Why not dropping the clear text after hashing?
///
/// Although this might be a sensible thing to do, I also have to
/// keep the possibility of upgraded hashers in mind. In the case
/// of upgrading password hashers, you'd have to hash a user's
/// clear text password twice, using the old and the new method.
/// So, either clear text passwords are copyable, or hashing
/// does not drop them.
pub struct ClearTextPassword {
    data: Box<ClearTextPasswordData>,
}

struct ClearTextPasswordData {
    text: [u8; CLEAR_TEXT_LEN],
    len:  usize,
}

impl ClearTextPassword {
    /// Takes a string and extracts its contents as a
    /// clear text password.
    ///
    /// The string's data will be truncated to the clear
    /// text password buffer's size with no respect for
    /// multibyte unicode characters. I.e. the string
    /// will be treated like a vector of raw bytes.
    ///
    /// In addition to that, the whole string's allocated
    /// buffer will be zero-filled to avoid having multiple
    /// copies of passwords in memory, before finally dropping
    /// the string.
    ///
    /// If the given password is too short, `None` will be returned.
    pub fn from_string(mut take: String) -> Option<ClearTextPassword> {
        // Cheap overflow avoidance.
        debug_assert!((take.len() as isize) >= 0, "Wtf, just how long is this string?!");
        if take.len() < MIN_CLEAR_TEXT_LEN { return None; }
        let mut ret = ClearTextPassword::empty_password();

        (*ret.data).len = if take.len() > CLEAR_TEXT_LEN { CLEAR_TEXT_LEN } else { take.len() };
        let src: *mut u8 = unsafe { mem::transmute(take.as_ptr()) };
        let tgt: *mut u8 = (*ret.data).as_ptr_mut();
        for i in 0..((*ret.data).len as isize) {
            unsafe { ptr::write(tgt.offset(i), ptr::read(src.offset(i))) };
        }

        unsafe { volatile_set_memory(src, 0, take.capacity()) };
        take.clear(); // Sets its `len` to zero. `capacity` could still leak a maximum length, though.

        Some(ret)
    }

    /// Takes a vector of raw bytes and extracts its contents
    /// as a clear text password.
    ///
    /// The vector's data will be truncated to the clear text
    /// password buffer's size. In addition to that, the whole
    /// vector's allocated buffer will be zero-filled to avoid
    /// having multiple copies of passwords in memory, before
    /// finally dropping the vector.
    ///
    /// If the given password is too short, `None` will be returned.
    pub fn from_vec(mut take: Vec<u8>) -> Option<ClearTextPassword> {
        if take.len() < MIN_CLEAR_TEXT_LEN { return None; }
        let mut ret = ClearTextPassword::empty_password();

        (*ret.data).len = if take.len() > CLEAR_TEXT_LEN { CLEAR_TEXT_LEN } else { take.len() };
        for i in 0..(*ret.data).len { (*ret.data).text[i] = take[i]; }

        unsafe { volatile_set_memory(take.as_mut_ptr(), 0, take.capacity()) };
        take.clear();

        Some(ret)
    }

    /// Grabs a clear text password out of a slice of bytes.
    ///
    /// The whole slice will be overridden with zero bytes.
    /// This is meant to avoid having multiple copies of
    /// passwords in memory.
    ///
    /// Returns `None` if the slice is too short.
    pub fn from_slice(steal: &mut [u8]) -> Option<ClearTextPassword> {
        if steal.len() < MIN_CLEAR_TEXT_LEN { return None; }
        let mut ret = ClearTextPassword::empty_password();

        (*ret.data).len = if steal.len() > CLEAR_TEXT_LEN { CLEAR_TEXT_LEN } else { steal.len() };
        for i in 0..(*ret.data).len {
            (*ret.data).text[i] = steal[i];
            steal[i] = 0;
        }
        for i in (*ret.data).len..steal.len() { steal[i] = 0; }

        Some(ret)
    }

    /// Reads a password from a reader and stores it into
    /// a new clear text password buffer.
    ///
    /// Note that this method has no influence on whether
    /// the reader keeps its copy of the password or whether
    /// an internal read buffer containing the password would
    /// be overridden.
    ///
    /// Returns `None` if there is not enough data to read.
    ///
    /// # Errors
    ///
    /// If the read operation failed, the clear text password
    /// buffer will be zero-filled as usual and an error code
    /// will be returned.
    pub fn from_reader<R: io::Read>(r: &mut R) -> io::Result<Option<ClearTextPassword>> {
        let mut ret = ClearTextPassword::empty_password();

        let len = r.read((*ret.data).as_slice_mut())?;
        if len < MIN_CLEAR_TEXT_LEN { return Ok(None); }
        (*ret.data).len = len;

        Ok(Some(ret))
    }

    /// Generates a hashed password using a given password hasher
    /// and a hash salt.
    ///
    /// Choose and configure your password hasher wisely. Otherwise,
    /// guessing the clear text password corresponding to the
    /// resulting hash might be too easy.
    pub fn hash<H: PasswordHasher>(&self, hasher: &H, salt: &HashSalt) -> HashedPassword {
        let mut hash = HashedPassword::new();
        unsafe { hasher.hash(hash.as_slice_mut(), (*self.data).get_password(), salt.as_slice()) };
        hash
    }



    fn empty_password() -> ClearTextPassword { ClearTextPassword { data: box ClearTextPasswordData {
        text: [0_u8; CLEAR_TEXT_LEN],
        len:   0,
    }}}
}

impl ClearTextPasswordData {
    fn as_ptr_mut(&mut self) -> *mut u8 { &mut self.text[0] }
    fn as_slice_mut(&mut self) -> &mut [u8] { &mut self.text[..] }
    fn get_password(&self) -> &[u8] { &self.text[0..self.len] }

    fn is_cleared(&self) -> bool {
        for x in &self.text[..] {
            if 0 != unsafe { ptr::read_volatile(x) } { return false; }
        }
        self.len == 0
    }
}

impl Drop for ClearTextPasswordData {
    /// Zeros out the clear text password.
    fn drop(&mut self) {
        let data: *mut u8 = &mut self.text[0];
        unsafe { volatile_set_memory(data, 0, self.text.len()) };
        // Not only should you clear the password's data, but
        // also any hint on the clear text password's length.
        unsafe { ptr::write_volatile(&mut self.len, 0) };

        if cfg!(test) { assert!(self.is_cleared()); }
    }
}



#[cfg(test)]
mod test {
    use super::{ClearTextPassword, CLEAR_TEXT_LEN, MIN_CLEAR_TEXT_LEN};
    use std::ptr;
    use test::Bencher;
    use std::io::Cursor;

    #[test]
    fn clear_text_len_is_reasonably_large() {
        assert!(CLEAR_TEXT_LEN >= (256 - 8), "You are a bad person.");
        assert!(MIN_CLEAR_TEXT_LEN >= 14, "Recommended by NIST (2016).");
    }

    #[test]
    fn short_passwords_are_shitty() {
        assert!(
            ClearTextPassword::from_string("lego".to_owned()).is_none(),
            "Really, 4 bytes passwords?"
        );
    }

    // This test **might crash** in case the allocator
    // responsible for allocating the test password
    // just happens to de-commit its memory pages
    // previously used by our string.
    #[cfg(not(skip_unsafe_tests))]
    #[test]
    #[allow(non_snake_case)]
    fn take_string_and_zero___TRY_RERUN_IF_FAILED() {
        let pw   = "P@$$w0rd1!......".to_owned(); // Bad password. Don't you dare using it!
        let data = pw.as_ptr();
        let len  = pw.len();

        ClearTextPassword::from_string(pw).unwrap();

        for i in 0..(len as isize) {
            assert_eq!(unsafe { ptr::read_volatile(data.offset(i)) }, 0);
        }
    }

    #[cfg(not(skip_unsafe_tests))]
    #[test]
    #[allow(non_snake_case)]
    fn take_vec_and_zero___TRY_RERUN_IF_FAILED() {
        let pw: Vec<u8> = vec![3, 4, 7, 42, 1,1,1,1,1,1,1,1,1,1,1,1];
        let data = pw.as_ptr();
        let len  = pw.len();

        ClearTextPassword::from_vec(pw).unwrap();

        for i in 0..(len as isize) {
            assert_eq!(unsafe { ptr::read_volatile(data.offset(i)) }, 0);
        }
    }

    #[test]
    fn take_slice_and_zero() {
        let mut pw: [u8; 16] = [3, 4, 7, 42, 1,1,1,1,1,1,1,1,1,1,1,1];

        ClearTextPassword::from_slice(&mut pw[..]).unwrap();

        for x in &pw[..] { assert_eq!(*x, 0); }
    }

    #[test]
    fn take_reader_and_hope() {
        let pw: [u8; 16] = [3, 4, 7, 42, 1,1,1,1,1,1,1,1,1,1,1,1];
        let mut reader = Cursor::new(&pw[..]);

        ClearTextPassword::from_reader(&mut reader).unwrap().unwrap();
    }

    #[test]
    fn dropping_really_clears() {
        // Checks done by `drop`.
        // Why? Because analysing free'd memory might
        // result in rubbish. Someone else might have
        // claimed and overridden it.
        let _pw = ClearTextPassword::from_string(
            "1234............".to_owned()
        ).unwrap(); // Really, don't you dare!
    }

    // Copy of `dropping_really_clears`. As benchmarks are
    // optimised, this works as a final check that `volatile`
    // really has the expected behaviour.
    #[bench]
    fn dropping_really_clears_okay_im_serious(_: &mut Bencher) {
        let _pw = ClearTextPassword::from_string(
            "asdf............".to_owned()
        ).unwrap(); // This password is fine, however.
    }
}