1use std::io::Write;
6
7use anyhow::Context;
8use secrecy::SecretString;
9use tracing::info;
10
11use crate::{
12 base::{Err, Res, Void},
13 utils,
14};
15
16pub fn get_home() -> Res<String> {
18 let home = homedir::my_home()
19 .context("Failed to get home directory.")?
20 .ok_or_else(|| Err::msg("Failed to get home directory."))?
21 .to_string_lossy()
22 .to_string();
23
24 Ok(home)
25}
26
27pub fn resolve_keypath<P>(path: P) -> Res<String>
29where
30 P: Into<Option<String>>,
31{
32 let home = get_home()?;
33 let path = match path.into() {
34 Some(path) => path,
35 None => format!("{}/.ratrod", home),
36 };
37
38 Ok(path)
39}
40
41pub fn generate<P>(print: bool, path: P) -> Void
43where
44 P: AsRef<str>,
45{
46 let pair = utils::generate_key_pair()?;
47
48 if print {
49 info!("📢 Public key: `{}`", pair.public_key);
50 info!("🔑 Private key: `{}`", pair.private_key);
51 }
52
53 std::fs::create_dir_all(path.as_ref()).context("Failed to create directory")?;
54
55 let key_file = format!("{}/key", path.as_ref());
56
57 if !std::fs::exists(&key_file)? {
58 std::fs::write(&key_file, pair.private_key).context("Failed to write private key")?;
59 std::fs::write(format!("{}.pub", key_file), pair.public_key).context("Failed to write public key")?;
60 }
61
62 let known_hosts_file = format!("{}/known_hosts", path.as_ref());
63 let authorized_keys_file = format!("{}/authorized_keys", path.as_ref());
64
65 if !std::fs::exists(&known_hosts_file)? {
66 std::fs::write(&known_hosts_file, "").context("Failed to write known hosts")?;
67 }
68
69 if !std::fs::exists(&authorized_keys_file)? {
70 std::fs::write(&authorized_keys_file, "").context("Failed to write authorized keys")?;
71 }
72
73 info!("📦 Security files written to `{}`", key_file);
74
75 Ok(())
76}
77
78pub fn ensure_security_files<P>(path: P) -> Void
80where
81 P: Into<Option<String>>,
82{
83 let path = resolve_keypath(path)?;
84 let key_path = format!("{}/key", path);
85
86 if !std::fs::exists(&key_path)? {
87 info!("No security files present in `{}` ...", path);
88
89 print!("Would you like to have the security files (public / private key pair, known hosts, and authorized keys) generated (y/n)? ");
90 std::io::stdout().flush().context("Failed to flush stdout")?;
91 let mut input = String::new();
92 std::io::stdin().read_line(&mut input).context("Failed to read user input")?;
93 let input = input.trim().to_lowercase();
94
95 if input != "y" {
96 return Err(Err::msg("User declined to generate security files."));
97 }
98
99 info!("Generating security files ...");
100 generate(false, &path)?;
101 }
102
103 Ok(())
104}
105
106pub fn resolve_private_key<P>(path: P) -> Res<SecretString>
108where
109 P: AsRef<str>,
110{
111 let file = format!("{}/key", path.as_ref());
112
113 Ok(std::fs::read_to_string(&file)
114 .context("Failed to read private key (you may need to run `generate-keypair`)")
115 .map(|s| s.trim().to_string())?
116 .into())
117}
118
119pub fn resolve_public_key<P>(path: P) -> Res<String>
121where
122 P: AsRef<str>,
123{
124 let file = format!("{}/key.pub", path.as_ref());
125
126 std::fs::read_to_string(&file)
127 .context("Failed to read public key (you may need to run `generate-keypair`)")
128 .map(|s| s.trim().to_string())
129}
130
131pub fn resolve_known_hosts<P>(path: P) -> Vec<String>
133where
134 P: AsRef<str>,
135{
136 let file = format!("{}/known_hosts", path.as_ref());
137
138 std::fs::read_to_string(&file).unwrap_or_default().lines().map(|s| s.trim().to_string()).collect()
139}
140
141pub fn resolve_authorized_keys<P>(path: P) -> Vec<String>
143where
144 P: AsRef<str>,
145{
146 let file = format!("{}/authorized_keys", path.as_ref());
147
148 std::fs::read_to_string(&file).unwrap_or_default().lines().map(|s| s.trim().to_string()).collect()
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::utils::{generate_challenge, sign_challenge, validate_signed_challenge};
155
156 #[test]
157 fn test_generate() {
158 generate(true, "./target/test").unwrap();
159
160 let private_key = resolve_private_key("./target/test").unwrap();
161 let public_key = resolve_public_key("./target/test").unwrap();
162
163 let challenge = generate_challenge();
164 let signature = sign_challenge(&challenge, &private_key).unwrap();
165
166 validate_signed_challenge(&challenge, &signature, &public_key).unwrap();
167 }
168
169 #[test]
170 fn test_get_authorized_keys() {
171 let keys = resolve_authorized_keys("./test/server");
172
173 assert_eq!(keys.len(), 1);
174 assert_eq!(keys[0], "iFOM_F9if7PwXmaCMttge8lhJHYjjS_hYUOZwZkHsi0");
175 }
176
177 #[test]
178 fn test_get_known_hosts() {
179 let keys = resolve_known_hosts("./test/client");
180
181 assert_eq!(keys.len(), 1);
182 assert_eq!(keys[0], "HQYY0BNIhdawY2Jw62DudkUsK2GKj3hGO3qSVBlCinI");
183 }
184}