ratrodlib/
security.rs

1//! Keypair generation and resolution.
2//!
3//! This module provides functions to generate a keypair, resolve private and public keys from files or strings, and handle errors related to key resolution.
4
5use std::io::Write;
6
7use anyhow::{Context, anyhow};
8use secrecy::SecretString;
9use tracing::info;
10
11use crate::{
12    base::{Res, Void},
13    utils,
14};
15
16/// Gets the user's home directory.
17pub fn get_home() -> Res<String> {
18    let home = homedir::my_home()
19        .context("Failed to get home directory.")?
20        .ok_or_else(|| anyhow!("Failed to get home directory."))?
21        .to_string_lossy()
22        .to_string();
23
24    Ok(home)
25}
26
27/// Computes the concrete keypath.
28pub 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
41/// Generates a keypair and writes them to the specified location.
42pub 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::path::Path::new(&key_file).exists() {
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::path::Path::new(&known_hosts_file).exists() {
66        std::fs::write(&known_hosts_file, "").context("Failed to write known hosts")?;
67    }
68
69    if !std::path::Path::new(&authorized_keys_file).exists() {
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
78/// Ensures all required security files are present and generates them if not.
79pub 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::path::Path::new(&key_path).exists() {
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(anyhow!("User declined to generate security files."));
97        }
98
99        info!("Generating security files ...");
100        generate(false, &path)?;
101    }
102
103    Ok(())
104}
105
106/// Resolves the private key of this instance.
107pub 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
119/// Resolves the public key of this instance.
120pub 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
131/// Resolves to the list of known hosts.
132pub 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
141/// Resolves to the list of authorized keys.
142pub 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}