fpass 0.2.6

CLI Password Manager
use std::{
    io::{self, stdin, stdout, Write},
    thread,
    time::Duration,
};

use anyhow::{anyhow, Result};
use cli_clipboard::{ClipboardContext, ClipboardProvider};
use rand::RngCore;
use rand_core::OsRng;
use rpassword::read_password;

use crate::db::{edit_entry, get_json, DataSchema};
use crate::decrypt::decrypt;
use crate::encrypt::encrypt;

pub fn add(
    json_path: &str,
    data_name: String,
    email: &[u8],
    password: &[u8],
    notes: &[u8],
    vaultpass: &[u8],
) -> Result<DataSchema> {
    let mut id: u8 = 0;
    let data_vec = get_json(json_path).map_err(|e| anyhow!("Error accessing database: {}", e))?;

    for _item in &data_vec {
        id += 1;
    }
    id += 1;

    let mut salt = [0u8; 16];
    OsRng.fill_bytes(&mut salt);

    let mut nonce = [0u8; 12];
    OsRng.fill_bytes(&mut nonce);

    let email_enc = encrypt(email, vaultpass, &salt, &nonce)?;
    let password_enc = encrypt(password, vaultpass, &salt, &nonce)?;
    let notes_enc = encrypt(notes, vaultpass, &salt, &nonce)?;

    let entry_from_cli = DataSchema {
        id,
        nonce: nonce.to_vec(),
        salt: salt.to_vec(),
        data_name,
        email: email_enc.to_vec(),
        password: password_enc.to_vec(),
        notes: notes_enc.to_vec(),
    };

    Ok(entry_from_cli)
}

pub fn edit(argid: u8, vaultpass: &[u8], json_path: &str) -> Result<DataSchema> {
    let data_vec = get_json(json_path).map_err(|e| anyhow!("Error reading JSON: {}", e))?;

    for item in data_vec.iter() {
        if item.id != argid {
            continue;
        }

        let (email, password, notes) = decrypt_things(
            &item.email,
            &item.salt,
            &item.nonce,
            &item.password,
            &item.notes,
            vaultpass,
        )?;

        let email_str = String::from_utf8(email)?;
        let password_str = String::from_utf8(password)?;
        let notes_str = String::from_utf8(notes)?;

        let new_data = input(&format!("Data Name [{}]", item.data_name))?;
        let new_email = input(&format!("Email [{email_str}]"))?;
        let new_password = input(&format!("Password [{password_str}]"))?;
        let new_notes = input(&format!("Notes [{notes_str}]"))?;

        let mut salt = [0u8; 16];
        OsRng.fill_bytes(&mut salt);

        let mut nonce = [0u8; 12];
        OsRng.fill_bytes(&mut nonce);

        let email_enc = encrypt(new_email.trim().as_bytes(), vaultpass, &salt, &nonce)?;
        let password_enc = encrypt(new_password.trim().as_bytes(), vaultpass, &salt, &nonce)?;
        let notes_enc = encrypt(new_notes.trim().as_bytes(), vaultpass, &salt, &nonce)?;

        let updated = edit_entry(
            item.id,
            &nonce,
            &salt,
            new_data,
            email_enc,
            password_enc,
            notes_enc,
            json_path,
        )?;

        if updated {
            return Ok(item.clone());
        }
    }

    Err(anyhow!("Item with id {} not found", argid))
}

fn decrypt_things(
    email: &[u8],
    salt: &[u8],
    nonce: &[u8],
    password: &[u8],
    notes: &[u8],
    vaultpass: &[u8],
) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
    let email_vec = decrypt(email, salt, nonce, vaultpass)
        .map_err(|e| anyhow!("Failed to decrypt email: {}", e))?;

    let password_vec = decrypt(password, salt, nonce, vaultpass)
        .map_err(|e| anyhow!("Failed to decrypt password: {}", e))?;

    let notes_vec = decrypt(notes, salt, nonce, vaultpass)
        .map_err(|e| anyhow!("Failed to decrypt notes: {}", e))?;

    Ok((email_vec, password_vec, notes_vec))
}

pub fn show(argid: u8, vaultpass: &[u8], json_path: &str, passhid: bool) -> Result<()> {
    let data_vec = get_json(json_path).map_err(|e| anyhow!("Error reading JSON: {}", e))?;

    for item in &data_vec {
        let nonce = &item.nonce;
        let salt = &item.salt;
        let id = item.id;
        let data_name = &item.data_name;
        let email = &item.email;
        let password = &item.password;
        let notes = &item.notes;

        if id == argid {
            println!("ID: {id}");
            println!("Data Name: {data_name}");
            let (email, password, notes) =
                decrypt_things(email, salt, nonce, password, notes, vaultpass)?;

            let email_str =
                String::from_utf8(email).map_err(|e| anyhow!("Invalid UTF-8 in email: {}", e))?;

            let password_str = String::from_utf8(password)
                .map_err(|e| anyhow!("Invalid UTF-8 in password: {}", e))?;

            let notes_str =
                String::from_utf8(notes).map_err(|e| anyhow!("Invalid UTF-8 in notes: {}", e))?;

            println!("Email: {email_str}");
            if passhid {
                println!("Password: |\x1b[8m{password_str}\x1b[0m|");
            } else {
                println!("Password: {password_str}");
            }
            println!("Notes: {notes_str}");
        }
    }

    Ok(())
}
pub fn copy(argid: u8, vaultpass: &[u8], which: &str, json_path: &str) -> Result<()> {
    let data_vec = get_json(json_path).map_err(|e| anyhow!("Error reading JSON: {}", e))?;

    let mut clip =
        ClipboardContext::new().map_err(|e| anyhow!("Failed to access clipboard: {}", e))?;

    for item in &data_vec {
        let nonce = &item.nonce;
        let salt = &item.salt;
        let id = item.id;
        let email = &item.email;
        let password = &item.password;
        let notes = &item.notes;

        if id == argid {
            let (emaila, passworda, _) =
                decrypt_things(email, salt, nonce, password, notes, vaultpass)?;

            if which == "email" {
                let email_str = String::from_utf8(emaila)
                    .map_err(|e| anyhow!("Invalid UTF-8 in email: {}", e))?;
                clip.set_contents(email_str)
                    .map_err(|e| anyhow!("Failed to copy to clipboard: {}", e))?;
                println!("Clipboard content will be deleted after 15 seconds...");
                for i in (1..=15).rev() {
                    print!("{i}");
                    use std::io::{stdout, Write};
                    stdout().flush().unwrap();

                    thread::sleep(Duration::from_secs(1));
                }
            } else if which == "password" {
                let password_str = String::from_utf8(passworda)
                    .map_err(|e| anyhow!("Invalid UTF-8 in password: {}", e))?;
                println!("Clipboard content will be deleted after 15 seconds...");
                for i in (1..=15).rev() {
                    clip.set_contents(password_str.clone())
                        .map_err(|e| anyhow!("Failed to copy to clipboard: {}", e))?;
                    print!("{i} ");
                    use std::io::{stdout, Write};
                    stdout().flush().unwrap();

                    thread::sleep(Duration::from_secs(1));
                }
            } else {
                return Err(anyhow!(
                    "Invalid option: {}. Use 'email' or 'password'",
                    which
                ));
            }
            return Ok(());
        }
    }

    Err(anyhow!("Entry with ID {} not found", argid))
}

pub fn find(data_name: String, json_path: &str) -> Result<()> {
    let data_vec = get_json(json_path).map_err(|e| anyhow!("Error accessing database: {}", e))?;

    let mut found = false;
    for item in &data_vec {
        let data_namenoenc = &item.data_name;
        let id = item.id;
        if data_namenoenc == &data_name {
            println!("Found ID: [{id}] {data_namenoenc}");
            found = true;
        }
    }

    if !found {
        println!("No entries found with name: {data_name}");
    }

    Ok(())
}

pub fn list(json_path: &str) -> Result<()> {
    let data_vec = get_json(json_path).map_err(|e| anyhow!("Error accessing database: {}", e))?;

    if data_vec.is_empty() {
        println!("No entries found in the database.");
    } else {
        for item in &data_vec {
            let id = item.id;
            let data_name = &item.data_name;

            println!("[{id}] {data_name}");
        }
    }

    Ok(())
}

pub fn input(prompt: &str) -> Result<String> {
    print!("{prompt}: ");
    stdout()
        .flush()
        .map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
    let mut input = String::new();
    stdin()
        .read_line(&mut input)
        .map_err(|e| anyhow!("Readline error: {}", e))?;
    Ok(input.trim().to_string())
}

pub fn master_input(prompt: &str) -> Result<String> {
    print!("{prompt}");
    io::stdout()
        .flush()
        .map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;

    let password = read_password().map_err(|e| anyhow!("Failed to read password: {}", e))?;
    Ok(password)
}