cotp 0.1.3

Trustworthy command line authenticator app compatible with backups from andOTP, Aegis and so on..
use std::{error, fmt};
use std::convert::TryInto;
use sodiumoxide::crypto::pwhash;
use sodiumoxide::crypto::secretstream::{Stream, Tag, KEYBYTES};
use sodiumoxide::crypto::secretstream::xchacha20poly1305::{Header, Key};

const SIGNATURE: [u8;4] = [0xC1, 0x0A, 0x4B, 0xED];

#[derive(Debug)]
struct CoreError {
    message: String,
}

impl CoreError {
    fn new(msg: &str) -> Self { CoreError{message: msg.to_string()} }
}

impl fmt::Display for CoreError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Error: {}", self.message)
    }
}

impl error::Error for CoreError {}

fn argon_derive_key(key: &mut[u8;32],password_bytes: &[u8],salt: &pwhash::argon2id13::Salt) -> Result<Key,String>{
    let result = pwhash::argon2id13::derive_key(key, password_bytes, salt,
        pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
        pwhash::argon2id13::MEMLIMIT_INTERACTIVE);
    match result{
        Err(()) => Err(String::from("Failed to derive encryption key")),
        _=> Ok(Key(*key)),
    }
}

pub fn encrypt_string(plaintext: String,password: &str) -> String {
    let mut encrypted = String::new();
    encrypted.push_str(&base64::encode(SIGNATURE));
    encrypted.push('|');
    let salt = pwhash::argon2id13::gen_salt();
    encrypted.push_str(&base64::encode(salt.0));
    encrypted.push('|');
    let key = argon_derive_key(&mut [0u8; KEYBYTES],password.as_bytes(),&salt).unwrap();
    let (mut enc_stream, header) = Stream::init_push(&key).unwrap();

    encrypted.push_str(&base64::encode(header.0));
    encrypted.push('|');

    let encrypted_string = enc_stream.push(plaintext.as_bytes(), None, Tag::Message).expect("Cannot encrypt");

    encrypted.push_str(&base64::encode(encrypted_string));
    encrypted
}

pub fn decrypt_string(encrypted_text: &str,password: &str) -> Result<String, String> {
    let split = encrypted_text.split('|');
    let vec: Vec<&str> = split.collect();
    if vec.len() != 4{
        return Err(String::from("Corrupted database file"));
    }
    let byte_salt = base64::decode(vec[1]).unwrap();
    let salt = pwhash::argon2id13::Salt(byte_vec_to_byte_array(byte_salt));
    let byte_header = base64::decode(vec[2]).unwrap();
    let header = Header(header_vec_to_header_array(byte_header));
    let cipher = base64::decode(vec[3]).unwrap();

    let mut key = [0u8; KEYBYTES];
    pwhash::argon2id13::derive_key(&mut key, password.as_bytes(), &salt,
        pwhash::argon2id13::OPSLIMIT_INTERACTIVE,
        pwhash::argon2id13::MEMLIMIT_INTERACTIVE)
        .map_err(|_| CoreError::new("Deriving key failed")).unwrap();
    let key = Key(key);

    let mut stream = Stream::init_pull(&header, &key)
        .map_err(|_| CoreError::new("init_pull failed")).unwrap();

    let (decrypted, _tag) = stream.pull(&cipher, None).unwrap_or((vec![0],Tag::Message));

    if decrypted == vec![0]{
        return Err(String::from("Wrong password"));
    }
    Ok(String::from_utf8(decrypted).unwrap())
}

fn byte_vec_to_byte_array(byte_vec: Vec<u8>) -> [u8;16]{
    byte_vec.try_into()
        .unwrap_or_else(|v: Vec<u8>| panic!("Expected a Vec of length {} but it was {}", 16, v.len()))
}

fn header_vec_to_header_array(byte_vec: Vec<u8>) -> [u8;24]{
    byte_vec.try_into()
        .unwrap_or_else(|v: Vec<u8>| panic!("Expected a Vec of length {} but it was {}", 24, v.len()))
}

pub fn prompt_for_passwords(message: &str,minimum_password_legth: usize) -> String{
    let mut password;
    loop{
        password = rpassword::prompt_password_stdout(message).unwrap();
        if password.len() >= minimum_password_legth {
            break;
        }
        println!("Please insert a password with at least {} digits.",minimum_password_legth);
    }
    password
}


#[cfg(test)]
mod tests {
    use super::{encrypt_string,decrypt_string};
    #[test]
    fn test_encryption() {
        assert_eq!(Ok(()),sodiumoxide::init());
        assert_eq!(
            String::from("Secret data@#[]ò"), 
            decrypt_string(
                &mut encrypt_string(String::from("Secret data@#[]ò"),"pa$$w0rd"),
                "pa$$w0rd"
            ).unwrap()
        );
    }
}