romp 0.5.2

STOMP server and WebSockets platform
Documentation
//! Loads and provides access to the data in the `romp.passwd` file.

use std::collections::HashMap;
use std::fs::File;
use std::io::{prelude::*, BufReader};

use log::*;

use serde_derive::Deserialize;
use sha2::{Sha256,Digest};

/// read a properties file of name:pass_hash

#[derive(Deserialize, Debug)]
#[serde(default)]
pub struct UserConfig {
    // map from username to sha256 of password + admin flag
    users: HashMap<String, (Box<[u8]>, bool)>,
}

impl Default for UserConfig {
    fn default() -> Self {
        UserConfig {
            users: HashMap::default(),
        }
    }
}

impl UserConfig {

    pub fn new() -> UserConfig {
        UserConfig { ..Default::default() }
    }

    /// return the number of configured users
    pub fn len(&self) -> usize {
        self.users.len()
    }

    pub fn init(&mut self, user_file: &String) {
        debug!("loading...");
        let file = File::open(user_file).expect(format!("file not found {}", user_file).as_str());
        let buf = BufReader::new(file);
        buf.lines()
            .map(|line| line.unwrap())
            .for_each(|line| {

            // discard blank line and comments
            if line.len() == 0 || line.chars().next().unwrap() == '#' {
                return;
            }

            // split name:hex
            let mut parts = line.split(':');

            if let (Some(name), Some(hex)) = (parts.next(), parts.next()) {
                if ! sanename::validate_sanename_word(name) {
                    info!("failed to load line \"{}\"", line);
                    return;
                }
                let admin;
                match parts.next() {
                    Some(flags) => admin = flags.eq("true"),
                    _ => admin = false
                }
                self.users.insert(String::from(name.trim()), (self.decode_hex(hex.trim()), admin));
            } else {
                info!("failed to load line \"{}\"", line);
            }
        });

        info!("loaded {} users from {}", self.users.len(), user_file);
    }

    /// validate the password, against the sha256 has stored in the user file
    /// returns (password_ok, is_admin)
    pub fn check_passcode(&self, login: &str, passcode: &str) -> (bool, bool) {
        if let Some(bin) = self.users.get(login) {
            // sha256(passcode)
            let mut sha256 = Sha256::new();
            sha256.input(passcode.as_bytes());
            sha256.input(b"\n");
            // compare iterators over u8
            return (sha256.result().iter().eq(bin.0.iter()), bin.1);
        }
        debug!("login not found {}", login);
        (false, false)
    }

    /// decode typical sha256 hex string or panic
    fn decode_hex(&self, hex: &str) -> Box<[u8]> {
        let decoded = base16::decode(hex).unwrap_or_else(|_| panic!("not a hex value"));
        if decoded.len() != 256 / 8 {
            panic!("not a sha256 hash");
        }
        let mut hash: Box<[u8]> = Box::new([0u8; 256 /8]);
        hash.copy_from_slice(decoded.as_slice());
        hash
    }
}



#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_init() {

        let mut user_config = UserConfig::new();
        user_config.init(&String::from("conf/romp.passwd"));

        // bob & alice, despite all their years working in infosec, still use common passwords in test code.

        assert!(user_config.check_passcode("bob", "password").0);
        assert!(user_config.check_passcode("alice", "password").0);

        assert!( ! user_config.check_passcode("bob", "dE4Rfd").0);
        assert!( ! user_config.check_passcode("alice", "ESIKEDSDD").0);


        assert!(user_config.check_passcode("admin2", "password").0);
        assert!(user_config.check_passcode("admin2", "password").1);
        assert!( ! user_config.check_passcode("bob", "password").1);
    }
}