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}