1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
use anyhow::{anyhow, Result};
use base64ct::{Base64, Encoding};

// check if it's a valid SSH-VAULT file and return the data
pub fn parse(data: &str) -> Result<(&str, String, Vec<u8>, Vec<u8>)> {
    let tokens: Vec<_> = data.split(';').collect();

    if tokens[0] != "SSH-VAULT" || (tokens[1] != "AES256" && tokens[1] != "CHACHA20-POLY1305") {
        return Err(anyhow!("Not a valid SSH-VAULT file"));
    }

    if tokens[1] == "AES256" {
        if tokens.len() != 4 {
            return Err(anyhow!("Not a valid SSH-VAULT file"));
        }

        let mut lines = tokens[2].lines();

        let fingerprint = lines
            .next()
            .ok_or_else(|| anyhow!("Not a valid SSH-VAULT file"))?;

        let password = lines.collect::<Vec<&str>>().join("");
        let password = Base64::decode_vec(&password)?;

        lines = tokens[3].lines();

        let data = lines.collect::<Vec<&str>>().join("");
        let data = Base64::decode_vec(&data)?;

        return Ok((tokens[1], fingerprint.to_string(), password, data));
    } else if tokens[1] == "CHACHA20-POLY1305" {
        if tokens.len() != 6 {
            return Err(anyhow!("Not a valid SSH-VAULT file"));
        }

        let fingerprint = tokens[2].lines().collect::<Vec<&str>>().join("");

        let epk = tokens[3].lines().collect::<Vec<&str>>().join("");
        let epk = Base64::decode_vec(&epk)?;

        let password = tokens[4].lines().collect::<Vec<&str>>().join("");
        let password = Base64::decode_vec(&password)?;

        let mut epk_and_password = Vec::new();
        epk_and_password.extend_from_slice(&epk);
        epk_and_password.extend_from_slice(&password);

        let data = tokens[5].lines().collect::<Vec<&str>>().join("");
        let data = Base64::decode_vec(&data)?;

        return Ok((tokens[1], fingerprint, epk_and_password, data));
    }

    Err(anyhow!("Not a valid SSH-VAULT file"))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_invalid_headers() {
        let data = r"SSH-VAULT:CHACHA20-POLY1305;0;0;0;0";
        assert!(parse(data).is_err());
    }

    #[test]
    fn test_parse_invalid_vault() {
        let data = r"SSH-VAULTCHACHA20-POLY1305SHA256:ZnlGYSmE8yBioOm+jhTxPAk4JagMu
mruoD1rf+WcpFY;EExFHBkGr4L2e0SS0y2Yw9lglLBGVmcho7r3EWSSZHU=;p3kQ
AVM09aZlRhfTZ4Gpp3WJ6AfurNqLo2Y8aDtQVj9uVx8FTJ+pVOTzphZMbCgzbSiU
pqwAZIHYhzss";
        assert!(parse(data).is_err());
    }

    #[test]
    fn test_parse_missing_data() {
        let data = r"SSH-VAULT;CHACHA20-POLY1305;SHA256:ZnlGYSmE8yBioOm+jhTxPAk4JagMu
mruoD1rf+WcpFY;EExFHBkGr4L2e0SS0y2Yw9lglLBGVmcho7r3EWSSZHU=;p3kQ
AVM09aZlRhfTZ4Gpp3WJ6AfurNqLo2Y8aDtQVj9uVx8FTJ+pVOTzphZMbCgzbSiU
pqwAZIHYhzss";
        assert!(parse(data).is_err());
    }

    #[test]
    fn test_parse_invalid_rsa_vault() {
        let data = r"SSH-VAULT;AES256;SHA256:ZnlGYSmE8yBioOm+jhTxPAk4JagMu";
        assert!(parse(data).is_err());
    }

    #[test]
    fn test_parse_no_fingerprint() {
        let data = r"SSH-VAULT;AES256";
        assert!(parse(data).is_err());
    }

    #[test]
    fn test_parse_no_payload() {
        let data = r"SSH-VAULT;AES256;;0";
        assert!(parse(data).is_err());
    }
}