ssh_vault/cli/actions/
create.rs

1use crate::cli::actions::{Action, process_input};
2use crate::vault::{SshVault, crypto, dio, find, online, remote};
3use anyhow::{Result, anyhow};
4use secrecy::SecretSlice;
5use serde::{Deserialize, Serialize};
6use ssh_key::PublicKey;
7use std::io::{Read, Write};
8
9#[derive(Serialize, Deserialize)]
10pub struct JsonVault {
11    vault: String,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    private_key: Option<String>,
14}
15
16/// Handle the create action
17///
18/// # Errors
19///
20/// Returns an error if arguments are invalid, the editor fails, I/O fails, or
21/// encryption cannot be completed.
22pub fn handle(action: Action) -> Result<()> {
23    match action {
24        Action::Create {
25            fingerprint,
26            key,
27            user,
28            vault,
29            json,
30            input,
31        } => {
32            // print the url from where to download the key
33            let mut helper: Option<String> = None;
34
35            let ssh_key: PublicKey = if let Some(user) = user {
36                // if user equals "new" ignore the key and fingerprint
37                if user == "new" && (key.is_some() || fingerprint.is_some()) {
38                    return Err(anyhow!("Options -k and -f not required when using -u new"));
39                }
40
41                let int_key: Option<u32> = key.as_ref().and_then(|s| s.parse::<u32>().ok());
42
43                // get keys from GitHub or remote server
44                let keys = remote::get_keys(&user)?;
45
46                // search key using -k or -f options
47                let ssh_key = remote::get_user_key(&keys, int_key, &fingerprint)?;
48
49                // if user equals "new" then we need to create a new key
50                if let Ok(key) = online::get_private_key_id(&ssh_key, &user)
51                    && !key.is_empty()
52                {
53                    helper = Some(key);
54                }
55
56                ssh_key
57            } else {
58                find::public_key(key)?
59            };
60
61            let key_type = find::key_type(&ssh_key.algorithm())?;
62
63            let v = SshVault::new(&key_type, Some(ssh_key), None)?;
64
65            let mut buffer = Vec::new();
66
67            // check if we need to skip the editor filename == "-"
68            let skip_editor = input.as_ref().is_some_and(|stdin| stdin == "-");
69
70            // setup Reader(input) and Writer (output)
71            let (mut input, output) = dio::setup_io(input, vault)?;
72
73            if !output.is_empty()? {
74                return Err(anyhow!("Vault file already exists"));
75            }
76
77            if input.is_terminal() {
78                if skip_editor {
79                    input.read_to_end(&mut buffer)?;
80                } else {
81                    // use editor to handle input
82                    process_input(&mut buffer, None)?;
83                }
84            } else {
85                // read from stdin
86                input.read_to_end(&mut buffer)?;
87            }
88
89            // generate password (32 rand chars)
90            let password: SecretSlice<u8> = crypto::gen_password()?;
91
92            // create vault
93            let vault = v.create(password, &mut buffer)?;
94
95            // return JSON or plain text, the helper is used to decrypt the vault
96            format(output, vault, json, helper)?;
97        }
98        _ => unreachable!(),
99    }
100    Ok(())
101}
102
103fn format<W: Write>(
104    mut output: W,
105    vault: String,
106    json: bool,
107    helper: Option<String>,
108) -> Result<()> {
109    // format the vault in json or plain text
110    if json {
111        let json_vault = JsonVault {
112            vault,
113            private_key: helper,
114        };
115
116        let json = serde_json::to_string(&json_vault)?;
117
118        output.write_all(json.as_bytes())?;
119    } else if let Some(helper) = helper {
120        let format = format!("echo \"{vault}\" | ssh-vault view -k {helper}");
121        output.write_all(format.as_bytes())?;
122    } else {
123        output.write_all(vault.as_bytes())?;
124    }
125
126    Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_format() -> Result<(), Box<dyn std::error::Error>> {
135        let mut output = Vec::new();
136        let vault = "vault".to_string();
137        let json = false;
138        let helper = None;
139
140        format(&mut output, vault, json, helper)?;
141
142        assert_eq!(output, b"vault");
143        Ok(())
144    }
145
146    #[test]
147    fn test_format_helper() -> Result<(), Box<dyn std::error::Error>> {
148        let mut output = Vec::new();
149        let vault = "vault".to_string();
150        let json = false;
151        let helper = Some("helper".to_string());
152
153        format(&mut output, vault, json, helper)?;
154
155        assert_eq!(output, b"echo \"vault\" | ssh-vault view -k helper");
156        Ok(())
157    }
158
159    #[test]
160    fn test_format_json() -> Result<(), Box<dyn std::error::Error>> {
161        let mut output = Vec::new();
162        let vault = "vault".to_string();
163        let json = true;
164        let helper = None;
165
166        format(&mut output, vault, json, helper)?;
167
168        assert_eq!(output, b"{\"vault\":\"vault\"}");
169        Ok(())
170    }
171
172    #[test]
173    fn test_format_helper_json() -> Result<(), Box<dyn std::error::Error>> {
174        let mut output = Vec::new();
175        let vault = "vault".to_string();
176        let json = true;
177        let helper = Some("helper".to_string());
178
179        format(&mut output, vault, json, helper)?;
180
181        assert_eq!(output, b"{\"vault\":\"vault\",\"private_key\":\"helper\"}");
182        Ok(())
183    }
184}