secure_edit/
cli.rs

1use super::secure;
2use super::{SALT_BYTES, SECURE_EDIT_DIR, SECURE_FILE_EXT};
3use anyhow::{anyhow, Result};
4use clap::Parser;
5use colored::Colorize;
6use std::path::PathBuf;
7use std::{fs, process};
8
9type Version = u32;
10
11#[derive(Parser, Debug)]
12#[command(version, about, long_about = None)]
13pub struct Args {
14    #[arg(short, long)]
15    pub dir: Option<PathBuf>,
16    #[arg(short, long)]
17    pub version: Option<Version>,
18}
19
20fn user_input(message: &str, align: Option<usize>, sensitive: bool) -> Result<String> {
21    use std::io::{self, Write};
22    print!("{message}");
23    if let Some(pad) = align {
24        print!("{:width$}", " ", width = pad);
25    }
26    print!("{}", " > ".blink().purple());
27    io::stdout().flush()?;
28    let mut input = String::new();
29    if sensitive {
30        input = rpassword::read_password()?;
31    } else {
32        io::stdin().read_line(&mut input)?;
33    }
34    Ok(input.trim().to_string())
35}
36
37fn should_proceed(message: &str) -> Result<bool> {
38    Ok(user_input(
39        &format!("{} ({}/{})", message, "y".green(), "n".red()),
40        None,
41        false,
42    )?
43    .trim()
44    .to_lowercase()
45        == "y")
46}
47
48pub fn run(args: Args) -> anyhow::Result<()> {
49    let dir = args.dir.unwrap_or_else(|| PathBuf::from("."));
50    if is_secure_edit_dir(&dir)? {
51        edit_secure_dir(&dir, args.version)?;
52    } else {
53        if !should_proceed(&format!(
54            "{} `{}`{}",
55            "Create secure edit directory in".bold().blue(),
56            dir.display().to_string().italic(),
57            "?".bold().blue()
58        ))? {
59            println!("{}\n", "Goodbye!".bold().italic().blue());
60            return Ok(());
61        }
62        create_secure_dir(&dir)?;
63    }
64    Ok(())
65}
66
67fn is_secure_edit_dir(dir: &PathBuf) -> Result<bool> {
68    if !dir.exists() {
69        return Ok(false);
70    }
71    if !dir.is_dir() {
72        return Err(anyhow!("Path is not a directory"));
73    }
74    let secure_edit_fp = dir.join(SECURE_EDIT_DIR);
75    Ok(secure_edit_fp.exists())
76}
77
78fn secure_file_fp(dir: &PathBuf, version: Version) -> PathBuf {
79    dir.join(format!(".{version}{SECURE_FILE_EXT}"))
80}
81
82fn create_secure_dir(dir: &PathBuf) -> Result<()> {
83    println!("{}", "Creating secure edit directory".green());
84    if !dir.exists() {
85        fs::create_dir(&dir)?;
86    }
87    let secure_edit_fp = dir.join(SECURE_EDIT_DIR);
88    fs::write(&secure_edit_fp, "")?;
89    println!(
90        "{} `{}`",
91        "Secure edit directory created at".green().bold(),
92        secure_edit_fp.display().to_string().italic()
93    );
94    let new_secure_version = create_secure_file(&dir)?;
95    edit_secure_file(
96        &secure_file_fp(dir, new_secure_version),
97        &secure_file_fp(dir, new_secure_version + 1),
98    )?;
99    Ok(())
100}
101
102fn edit_secure_dir(dir: &PathBuf, version: Option<Version>) -> Result<()> {
103    println!("{}", "Editing secure directory".green());
104    let mut secure_files = Vec::new();
105    for entry in dir.read_dir()? {
106        let entry = entry?;
107        let file_name = entry.file_name();
108        let file_name = file_name.to_string_lossy();
109        if file_name.ends_with(".secure") {
110            let version = file_name[1..].trim_end_matches(SECURE_FILE_EXT);
111            let version = version.parse::<Version>()?;
112            secure_files.push(version);
113        }
114    }
115    secure_files.sort();
116    if secure_files.is_empty() {
117        let new_secure_version = create_secure_file(dir)?;
118        secure_files.push(new_secure_version);
119    }
120    let latest_version = *secure_files.last().unwrap();
121    let open_version = version.unwrap_or(latest_version);
122    edit_secure_file(
123        &secure_file_fp(dir, open_version),
124        &secure_file_fp(dir, latest_version + 1),
125    )?;
126    Ok(())
127}
128
129fn create_secure_file(dir: &PathBuf) -> Result<Version> {
130    println!(
131        "{} {}",
132        "No secure files found.".yellow(),
133        "Creating secure file".green()
134    );
135    let version = 0;
136    let secure_fp = secure_file_fp(dir, version);
137    let pwd1 = user_input(
138        &format!("{}", "Enter password".blue().bold()),
139        Some(3),
140        true,
141    )?;
142    let pwd2 = user_input(
143        &format!("{}", "Re-enter password".blue().bold()),
144        None,
145        true,
146    )?;
147    if pwd1 != pwd2 {
148        return Err(anyhow!("Passwords do not match"));
149    }
150    let salt = secure::generate_salt();
151    let encrypted_data = secure::encrypt_data_formatted(&pwd1, &salt, &[])?;
152    fs::write(&secure_fp, &encrypted_data)?;
153    Ok(version)
154}
155
156fn edit_secure_file(open_fp: &PathBuf, write_fp: &PathBuf) -> Result<()> {
157    use std::io::Write;
158    println!(
159        "{} `{}`",
160        "Opening secure file".green(),
161        open_fp.display().to_string().italic()
162    );
163
164    if !open_fp.exists() {
165        return Err(anyhow!(
166            "Secure file to read at `{}` does not exist",
167            open_fp.display()
168        ));
169    }
170    if write_fp.exists() {
171        return Err(anyhow!(
172            "Secure file to write at `{}` already exists",
173            write_fp.display()
174        ));
175    }
176
177    let pwd = user_input(&format!("{}", "Enter password".blue().bold()), None, true)?;
178    let data = fs::read(open_fp)?;
179    let decrypted_data = secure::decrypt_data_formatted(&pwd, &data)
180        .map_err(|_| anyhow!("Failed to decrypt file. Wrong password?"))?;
181
182    let cmd = process::Command::new("vipe")
183        .stdin(process::Stdio::piped())
184        .stdout(process::Stdio::piped())
185        .spawn()?;
186    let mut stdin = cmd.stdin.as_ref().unwrap();
187    stdin.write_all(&decrypted_data)?;
188    let output = cmd.wait_with_output()?;
189    if !output.status.success() {
190        return Err(anyhow!("Failed to edit file"));
191    }
192
193    println!(
194        "{} `{}`",
195        "Saving new secure file at".bold().green(),
196        write_fp.display().to_string().italic()
197    );
198    let new_data = output.stdout;
199    let encrypted_data = secure::encrypt_data_formatted(&pwd, &data[..SALT_BYTES], &new_data)?;
200    fs::write(write_fp, &encrypted_data)?;
201    Ok(())
202}