secure_edit/
cli.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
use super::secure;
use super::{SALT_BYTES, SECURE_EDIT_DIR, SECURE_FILE_EXT};
use anyhow::{anyhow, Result};
use clap::Parser;
use colored::Colorize;
use std::path::PathBuf;
use std::{fs, process};

type Version = u32;

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
    #[arg(short, long)]
    pub dir: Option<PathBuf>,
}

fn user_input(message: &str, align: Option<usize>) -> Result<String> {
    use std::io::{self, Write};
    print!("{message}");
    if let Some(pad) = align {
        print!("{:width$}", " ", width = pad);
    }
    print!("{}", " > ".blink().purple());
    io::stdout().flush()?;
    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    Ok(input.trim().to_string())
}

fn should_proceed(message: &str) -> Result<bool> {
    Ok(user_input(
        &format!("{} ({}/{})", message, "y".green(), "n".red()),
        None,
    )?
    .trim()
    .to_lowercase()
        == "y")
}

pub fn run(args: Args) -> anyhow::Result<()> {
    let dir = args.dir.unwrap_or_else(|| PathBuf::from("."));
    if is_secure_edit_dir(&dir)? {
        edit_secure_dir(&dir)?;
    } else {
        if !should_proceed(&format!(
            "{} `{}`{}",
            "Create secure edit directory in".bold().blue(),
            dir.display().to_string().italic(),
            "?".bold().blue()
        ))? {
            println!("{}\n", "Goodbye!".bold().italic().blue());
            return Ok(());
        }
        create_secure_dir(&dir)?;
    }
    Ok(())
}

fn is_secure_edit_dir(dir: &PathBuf) -> Result<bool> {
    if !dir.exists() {
        return Ok(false);
    }
    if !dir.is_dir() {
        return Err(anyhow!("Path is not a directory"));
    }
    let secure_edit_fp = dir.join(SECURE_EDIT_DIR);
    Ok(secure_edit_fp.exists())
}

fn secure_file_fp(dir: &PathBuf, version: Version) -> PathBuf {
    dir.join(format!(".{version}{SECURE_FILE_EXT}"))
}

fn create_secure_dir(dir: &PathBuf) -> Result<()> {
    println!("{}", "Creating secure edit directory".green());
    if !dir.exists() {
        fs::create_dir(&dir)?;
    }
    let secure_edit_fp = dir.join(SECURE_EDIT_DIR);
    fs::write(&secure_edit_fp, "")?;
    println!(
        "{} `{}`",
        "Secure edit directory created at".green().bold(),
        secure_edit_fp.display().to_string().italic()
    );
    let new_secure_version = create_secure_file(&dir)?;
    edit_secure_file(
        &secure_file_fp(dir, new_secure_version),
        &secure_file_fp(dir, new_secure_version + 1),
    )?;
    Ok(())
}

fn edit_secure_dir(dir: &PathBuf) -> Result<()> {
    println!("{}", "Editing secure directory".green());
    let mut secure_files = Vec::new();
    for entry in dir.read_dir()? {
        let entry = entry?;
        let file_name = entry.file_name();
        let file_name = file_name.to_string_lossy();
        if file_name.ends_with(".secure") {
            let version = file_name[1..].trim_end_matches(SECURE_FILE_EXT);
            let version = version.parse::<Version>()?;
            secure_files.push(version);
        }
    }
    secure_files.sort();
    if secure_files.is_empty() {
        let new_secure_version = create_secure_file(dir)?;
        secure_files.push(new_secure_version);
    }
    let latest_version = *secure_files.last().unwrap();
    edit_secure_file(
        &secure_file_fp(dir, latest_version),
        &secure_file_fp(dir, latest_version + 1),
    )?;
    Ok(())
}

fn create_secure_file(dir: &PathBuf) -> Result<Version> {
    println!(
        "{} {}",
        "No secure files found.".yellow(),
        "Creating secure file".green()
    );
    let version = 0;
    let secure_fp = secure_file_fp(dir, version);
    let pwd1 = user_input(&format!("{}", "Enter password".blue().bold()), Some(3))?;
    let pwd2 = user_input(&format!("{}", "Re-enter password".blue().bold()), None)?;
    if pwd1 != pwd2 {
        return Err(anyhow!("Passwords do not match"));
    }
    let salt = secure::generate_salt();
    let encrypted_data = secure::encrypt_data_formatted(&pwd1, &salt, &[])?;
    fs::write(&secure_fp, &encrypted_data)?;
    Ok(version)
}

fn edit_secure_file(open_fp: &PathBuf, write_fp: &PathBuf) -> Result<()> {
    use std::io::Write;
    println!(
        "{} `{}`",
        "Opening secure file".green(),
        open_fp.display().to_string().italic()
    );
    let pwd = user_input(&format!("{}", "Enter password".blue().bold()), None)?;
    let data = fs::read(open_fp)?;
    let decrypted_data = secure::decrypt_data_formatted(&pwd, &data)?;

    let cmd = process::Command::new("vipe")
        .stdin(process::Stdio::piped())
        .stdout(process::Stdio::piped())
        .spawn()?;
    let mut stdin = cmd.stdin.as_ref().unwrap();
    stdin.write_all(&decrypted_data)?;
    let output = cmd.wait_with_output()?;
    if !output.status.success() {
        return Err(anyhow!("Failed to edit file"));
    }

    println!(
        "{} `{}`",
        "Saving new secure file at".bold().green(),
        write_fp.display().to_string().italic()
    );
    let new_data = output.stdout;
    let encrypted_data = secure::encrypt_data_formatted(&pwd, &data[..SALT_BYTES], &new_data)?;
    fs::write(write_fp, &encrypted_data)?;
    Ok(())
}