1use anyhow::{Result, anyhow};
2use base64ct::{Base64, Encoding};
3
4pub 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}