ssh_vault/vault/
parse.rs

1use anyhow::{Result, anyhow};
2use base64ct::{Base64, Encoding};
3
4/// Check if it's a valid SSH-VAULT file and return the parsed components.
5///
6/// # Errors
7///
8/// Returns an error if the input is malformed or any Base64 decoding fails.
9pub fn parse(data: &str) -> Result<(&str, String, Vec<u8>, Vec<u8>)> {
10    let tokens: Vec<_> = data.split(';').collect();
11
12    let vault_marker = tokens
13        .first()
14        .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?;
15    let algorithm = tokens
16        .get(1)
17        .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?;
18
19    if *vault_marker != "SSH-VAULT" || (*algorithm != "AES256" && *algorithm != "CHACHA20-POLY1305")
20    {
21        return Err(anyhow!("Not a valid SSH-VAULT file"));
22    }
23
24    if *algorithm == "AES256" {
25        if tokens.len() != 4 {
26            return Err(anyhow!("Not a valid SSH-VAULT file"));
27        }
28
29        let mut lines = tokens
30            .get(2)
31            .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?
32            .lines();
33
34        let fingerprint = lines
35            .next()
36            .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?;
37
38        let password = lines.collect::<Vec<&str>>().join("");
39        let password = Base64::decode_vec(&password)?;
40
41        lines = tokens
42            .get(3)
43            .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?
44            .lines();
45
46        let data = lines.collect::<Vec<&str>>().join("");
47        let data = Base64::decode_vec(&data)?;
48
49        return Ok((algorithm, fingerprint.to_string(), password, data));
50    } else if *algorithm == "CHACHA20-POLY1305" {
51        if tokens.len() != 6 {
52            return Err(anyhow!("Not a valid SSH-VAULT file"));
53        }
54
55        let fingerprint = tokens
56            .get(2)
57            .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?
58            .lines()
59            .collect::<Vec<&str>>()
60            .join("");
61
62        let epk = tokens
63            .get(3)
64            .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?
65            .lines()
66            .collect::<Vec<&str>>()
67            .join("");
68        let epk = Base64::decode_vec(&epk)?;
69
70        let password = tokens
71            .get(4)
72            .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?
73            .lines()
74            .collect::<Vec<&str>>()
75            .join("");
76        let password = Base64::decode_vec(&password)?;
77
78        let mut epk_and_password = Vec::new();
79        epk_and_password.extend_from_slice(&epk);
80        epk_and_password.extend_from_slice(&password);
81
82        let data = tokens
83            .get(5)
84            .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?
85            .lines()
86            .collect::<Vec<&str>>()
87            .join("");
88        let data = Base64::decode_vec(&data)?;
89
90        return Ok((algorithm, fingerprint, epk_and_password, data));
91    }
92
93    Err(anyhow!("Not a valid SSH-VAULT file"))
94}
95
96#[cfg(test)]
97#[allow(clippy::unwrap_used)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_parse_invalid_headers() {
103        let data = r"SSH-VAULT:CHACHA20-POLY1305;0;0;0;0";
104        assert!(parse(data).is_err());
105    }
106
107    #[test]
108    fn test_parse_invalid_vault() {
109        let data = r"SSH-VAULTCHACHA20-POLY1305SHA256:ZnlGYSmE8yBioOm+jhTxPAk4JagMu
110mruoD1rf+WcpFY;EExFHBkGr4L2e0SS0y2Yw9lglLBGVmcho7r3EWSSZHU=;p3kQ
111AVM09aZlRhfTZ4Gpp3WJ6AfurNqLo2Y8aDtQVj9uVx8FTJ+pVOTzphZMbCgzbSiU
112pqwAZIHYhzss";
113        assert!(parse(data).is_err());
114    }
115
116    #[test]
117    fn test_parse_missing_data() {
118        let data = r"SSH-VAULT;CHACHA20-POLY1305;SHA256:ZnlGYSmE8yBioOm+jhTxPAk4JagMu
119mruoD1rf+WcpFY;EExFHBkGr4L2e0SS0y2Yw9lglLBGVmcho7r3EWSSZHU=;p3kQ
120AVM09aZlRhfTZ4Gpp3WJ6AfurNqLo2Y8aDtQVj9uVx8FTJ+pVOTzphZMbCgzbSiU
121pqwAZIHYhzss";
122        assert!(parse(data).is_err());
123    }
124
125    #[test]
126    fn test_parse_invalid_rsa_vault() {
127        let data = r"SSH-VAULT;AES256;SHA256:ZnlGYSmE8yBioOm+jhTxPAk4JagMu";
128        assert!(parse(data).is_err());
129    }
130
131    #[test]
132    fn test_parse_no_fingerprint() {
133        let data = r"SSH-VAULT;AES256";
134        assert!(parse(data).is_err());
135    }
136
137    #[test]
138    fn test_parse_no_payload() {
139        let data = r"SSH-VAULT;AES256;;0";
140        assert!(parse(data).is_err());
141    }
142
143    #[test]
144    fn test_parse_empty_string() {
145        let data = "";
146        let result = parse(data);
147        assert!(result.is_err());
148        assert!(
149            result
150                .unwrap_err()
151                .to_string()
152                .contains("Not a valid SSH-VAULT file")
153        );
154    }
155
156    #[test]
157    fn test_parse_single_token() {
158        let data = "SSH-VAULT";
159        let result = parse(data);
160        assert!(result.is_err());
161        assert!(
162            result
163                .unwrap_err()
164                .to_string()
165                .contains("Not a valid SSH-VAULT file")
166        );
167    }
168
169    #[test]
170    fn test_parse_no_tokens() {
171        let data = ";";
172        let result = parse(data);
173        assert!(result.is_err());
174    }
175
176    #[test]
177    fn test_parse_malformed_header() {
178        let data = "INVALID";
179        let result = parse(data);
180        assert!(result.is_err());
181    }
182
183    #[test]
184    fn test_parse_wrong_crypto_type() {
185        let data = "SSH-VAULT;INVALID_CRYPTO";
186        let result = parse(data);
187        assert!(result.is_err());
188        assert!(
189            result
190                .unwrap_err()
191                .to_string()
192                .contains("Not a valid SSH-VAULT file")
193        );
194    }
195}