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