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.

#[cfg(any(test, feature = "use_serde", feature = "default"))] use serde;

use rand::os::OsRng;
use rand::Rng;
use std::fmt;
use std::io;
use std::cmp::{ PartialEq, Eq };



/// The length of a hash salt in bytes.
///
/// Should be at least 16 bytes for really good salts.
/// Doesn't need to be a huge number.
pub const HASH_SALT_LEN: usize = 32;



/// A salt for password hashing.
///
/// Salts should be unique per user to provide maximum
/// security. They can also only be created using a
/// CSPRNG. Additionally, hash salts can be serialised
/// and deserialised, as they should be stored next to
/// your users' password hashes in your databases.
///
/// # Why salts?
///
/// Salts are a known random component that will be hashed
/// together with a user's clear text password. The uniqueness
/// of a salt, together with its unpredictability thanks to a
/// CSPRNG, ensure, that two users having the same clear text
/// passwords won't have the same password hashes, which makes
/// it way harder for attackers to crack a whole password
/// database.
pub struct HashSalt {
    data: [u8; HASH_SALT_LEN],
}

impl HashSalt {
    /// Generates a new salt using an OS-provided CSPRNG.
    pub fn new() -> io::Result<HashSalt> {
        let mut osrng = OsRng::new()?;
        let mut spicy = HashSalt { data: [0; HASH_SALT_LEN] };
        osrng.fill_bytes(&mut spicy.data[..]);
        Ok(spicy)
    }

    /// Creates a hash salt from a reader.
    ///
    /// Only use this function to load serialised salts
    /// that have been created using `new`.
    ///
    /// # Errors
    ///
    /// This function fails if the reader has not enough
    /// bytes to completely fill the hash salt.
    pub fn from_reader<R: io::Read>(r: &mut R) -> io::Result<HashSalt> {
        let mut spicy = HashSalt { data: [0; HASH_SALT_LEN] };
        r.read_exact(spicy.as_slice_mut())?;
        Ok(spicy)
    }

    /// Get a slice of the hash salt's bytes.
    pub fn as_slice(&self) -> &[u8] { &self.data[..] }



    fn as_slice_mut(&mut self) -> &mut [u8] { &mut self.data[..] }

    fn copy_from_slice(&mut self, other: &[u8]) {
        self.as_slice_mut().copy_from_slice(other);
    }

    fn fmt_impl(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "HashSalt[ ")?;
        for x in &self.data[..] { write!(f, "{:02X}", *x)?; }
        write!(f, " ]")
    }
}



// Due to the lack of literal/constant values as generic parameters,
// arrays and tuples are handled using lots of duplicate specialisations
// up to a specific size. In typical Rust programs, this size is 32 entries.
// Too bad that our arrays can be way bigger than that, which is why we
// can't just derive.
#[cfg(any(test, feature = "use_serde", feature = "default"))]
impl serde::Serialize for HashSalt {
    fn serialize<S: serde::Serializer>(&self, ser: &mut S) -> Result<(), S::Error> {
        ser.serialize_newtype_struct("HashSalt", serde::bytes::Bytes::from(self.as_slice()))
    }
}

#[cfg(any(test, feature = "use_serde", feature = "default"))]
impl serde::Deserialize for HashSalt {
    fn deserialize<D: serde::Deserializer>(de: &mut D) -> Result<Self, D::Error> {
        de.deserialize_newtype_struct("HashSalt", HashSaltSerdeVisitor {})
    }
}

#[cfg(any(test, feature = "use_serde", feature = "default"))]
struct HashSaltSerdeVisitor {}

#[cfg(any(test, feature = "use_serde", feature = "default"))]
impl serde::de::Visitor for HashSaltSerdeVisitor {
    type Value = HashSalt;

    fn visit_newtype_struct<D: serde::Deserializer>(&mut self, de: &mut D) -> Result<HashSalt, D::Error> {
        use serde::{ Deserialize, Error };

        let bytes = serde::bytes::ByteBuf::deserialize(de)?;
        if bytes.len() != HASH_SALT_LEN { return Err(D::Error::invalid_length(bytes.len())); }
        let mut spicy = HashSalt { data: [0_u8; HASH_SALT_LEN] };
        spicy.copy_from_slice(&bytes[..]);

        Ok(spicy)
    }

    fn visit_bytes<E: serde::Error>(&mut self, v: &[u8]) -> Result<HashSalt, E> {
        if v.len() == HASH_SALT_LEN {
            let mut spicy = HashSalt { data: [0_u8; HASH_SALT_LEN] };
            spicy.copy_from_slice(v);
            Ok(spicy)
        }
        else { Err(E::invalid_length(v.len())) }
    }

    fn visit_seq<V: serde::de::SeqVisitor>(&mut self, mut visitor: V) -> Result<HashSalt, V::Error> {
        use serde::Error;

        let bytes = match visitor.visit::<serde::bytes::ByteBuf>()? {
            Some(bytes) => { visitor.end()?; bytes },
            None        => { visitor.end()?; return Err(V::Error::invalid_length(0)); },
        };
        if bytes.len() != HASH_SALT_LEN { return Err(V::Error::invalid_length(bytes.len())); }
        let mut spicy = HashSalt { data: [0_u8; HASH_SALT_LEN] };
        spicy.copy_from_slice(&bytes[..]);

        Ok(spicy)
    }
}



// Same reason as the serde stuff.
impl PartialEq for HashSalt {
    fn eq(&self, other: &Self) -> bool { self.as_slice() == other.as_slice() }
}

impl Eq for HashSalt {}



impl fmt::Debug for HashSalt {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_impl(f) }
}

impl fmt::Display for HashSalt {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.fmt_impl(f) }
}



#[cfg(test)]
mod test {
    use super::{HashSalt, HASH_SALT_LEN};
    use serde_test::{ Token, assert_tokens };
    use std::io;

    #[test]
    fn salt_len_is_reasonably_large() {
        assert!(HASH_SALT_LEN >= 16, "Why not just make salts a constant 'asdf'?");
    }

    #[test]
    fn generating_salts_works() {
        let spicy = HashSalt::new().unwrap();
        assert_eq!(spicy.as_slice().len(), HASH_SALT_LEN);

        // Generating all-zeros is very unlikely.
        // Why then do I check for not everything being zero?
        // To chek whether something has been generated and stored.
        let mut test = 0;
        for x in spicy.as_slice() { test |= *x; }
        assert!(test != 0, "Try running the test again. Generating all-zeros is not impossible.");
    }

    #[test]
    fn from_reader_works() {
        const ARR1: &'static [u8] = &[42_u8; HASH_SALT_LEN];
        const ARR2: &'static [u8] = &[ 1_u8; 1];

        let mut data1 = io::Cursor::new(ARR1);
        let mut data2 = io::Cursor::new(ARR2); // Way too short.

        assert_eq!(
            HashSalt::from_reader(&mut data1).unwrap(),
            HashSalt { data: [42_u8; HASH_SALT_LEN] }
        );

        assert_eq!(
            HashSalt::from_reader(&mut data2).unwrap_err().kind(),
            io::ErrorKind::UnexpectedEof
        );
    }

    #[test]
    fn serde_works() {
        let spicy = HashSalt::new().unwrap();

        // `serde_test` is very stupid, as in it doesn't
        // know about bytes. Just handle them like ordinary
        // integer arrays, 'cause who cares.
        let mut tokens: Vec<Token> = Vec::with_capacity(3 + (2 * spicy.as_slice().len()));
        tokens.push(Token::StructNewType("HashSalt"));
        tokens.push(Token::SeqStart(Some(HASH_SALT_LEN)));
        for x in spicy.as_slice() {
            tokens.push(Token::SeqSep);
            tokens.push(Token::U8(*x));
        }
        tokens.push(Token::SeqEnd);

        assert_tokens(&spicy, &tokens[..]);
    }
}