Skip to main content

ssh_vault/vault/ssh/
rsa.rs

1use crate::vault::{
2    Vault, crypto::Crypto, crypto::aes256::Aes256Crypto, fingerprint::md5_fingerprint,
3};
4use anyhow::{Context, Result};
5use base64ct::{Base64, Encoding};
6use rsa::{BigUint, Oaep, RsaPrivateKey, RsaPublicKey, rand_core::OsRng, sha2::Sha256};
7use secrecy::{ExposeSecret, SecretSlice};
8use ssh_key::{PrivateKey, PublicKey, private::KeypairData, public::KeyData};
9use zeroize::Zeroize;
10
11#[derive(Debug)]
12pub struct RsaVault {
13    public_key: RsaPublicKey,
14    private_key: Option<RsaPrivateKey>,
15}
16
17impl Vault for RsaVault {
18    fn new(public: Option<PublicKey>, private: Option<PrivateKey>) -> Result<Self> {
19        match (public, private) {
20            (Some(public), None) => match public.key_data() {
21                KeyData::Rsa(key_data) => {
22                    let public_key =
23                        RsaPublicKey::try_from(key_data).context("Could not load key")?;
24
25                    Ok(Self {
26                        public_key,
27                        private_key: None,
28                    })
29                }
30                _ => Err(anyhow::anyhow!("Invalid key type for RsaVault")),
31            },
32
33            (None, Some(private)) => match private.key_data() {
34                KeypairData::Rsa(rsa_keypair) => {
35                    if private.is_encrypted() {
36                        return Err(anyhow::anyhow!("Private key is encrypted"));
37                    }
38
39                    // Extract components from ssh-key's RSA representation
40                    // Use as_bytes() or a similar method to get the &[u8] from Mpint
41                    //
42                    // <https://docs.rs/ssh-key/latest/ssh_key/private/struct.RsaPrivateKey.html>
43                    //
44                    // pub struct RsaPrivateKey {
45                    //     pub d: Mpint,
46                    //     pub iqmp: Mpint,
47                    //     pub p: Mpint,
48                    //     pub q: Mpint,
49                    // }
50                    let modulus = BigUint::from_bytes_be(rsa_keypair.public.n.as_ref());
51                    let public_exponent = BigUint::from_bytes_be(rsa_keypair.public.e.as_ref());
52                    let private_exponent = BigUint::from_bytes_be(rsa_keypair.private.d.as_ref());
53                    let prime_p = BigUint::from_bytes_be(rsa_keypair.private.p.as_ref());
54                    let prime_q = BigUint::from_bytes_be(rsa_keypair.private.q.as_ref());
55
56                    // Create the RSA private key
57                    //
58                    // Constructs an RSA key pair from individual components:
59                    //
60                    // n: RSA modulus
61                    // e: public exponent (i.e. encrypting exponent)
62                    // d: private exponent (i.e. decrypting exponent)
63                    // primes: prime factors of n: typically two primes p and q. More than two
64                    // primes can be provided for multiprime RSA, however this is generally not
65                    // recommended. If no primes are provided, a prime factor recovery algorithm
66                    // will be employed to attempt to recover the factors (as described in NIST SP
67                    // 800-56B Revision 2 Appendix C.2). This algorithm only works if there are
68                    // just two prime factors p and q (as opposed to multiprime), and e is between
69                    // 2^16 and 2^256.
70                    let private_key = RsaPrivateKey::from_components(
71                        modulus,
72                        public_exponent,
73                        private_exponent,
74                        vec![prime_p, prime_q],
75                    )?;
76
77                    // let private_key = RsaPrivateKey::try_from(key_data)?;
78
79                    let public_key = private_key.to_public_key();
80
81                    Ok(Self {
82                        public_key,
83                        private_key: Some(private_key),
84                    })
85                }
86                _ => Err(anyhow::anyhow!("Invalid key type for RsaVault")),
87            },
88
89            (Some(_), Some(_)) => Err(anyhow::anyhow!(
90                "Only one of public and private key is required"
91            )),
92
93            _ => Err(anyhow::anyhow!("Missing public and private key")),
94        }
95    }
96
97    fn create(&self, password: SecretSlice<u8>, data: &mut [u8]) -> Result<String> {
98        let crypto = Aes256Crypto::new(password.clone());
99
100        let fingerprint = md5_fingerprint(&self.public_key)?;
101
102        let encrypted_data = crypto.encrypt(data, fingerprint.as_bytes())?;
103
104        // zeroize data
105        data.zeroize();
106
107        // Keep the RSA boundary on rsa::rand_core::OsRng. The rest of the crate
108        // uses rand 0.10, but current rsa/ssh-key releases still depend on the
109        // older rand_core line. Revisit this when upstream removes that split.
110        let encrypted_password =
111            self.public_key
112                .encrypt(&mut OsRng, Oaep::new::<Sha256>(), password.expose_secret())?;
113
114        // create vault payload
115        let payload = format!(
116            "{};{}",
117            Base64::encode_string(&encrypted_password),
118            Base64::encode_string(&encrypted_data)
119        )
120        .chars()
121        .collect::<Vec<_>>()
122        .chunks(64)
123        .map(|chunk| chunk.iter().collect::<String>())
124        .collect::<Vec<_>>()
125        .join("\n");
126
127        Ok(format!("SSH-VAULT;AES256;{fingerprint}\n{payload}"))
128    }
129
130    fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String> {
131        let get_fingerprint = md5_fingerprint(&self.public_key)?;
132
133        if get_fingerprint != fingerprint {
134            return Err(anyhow::anyhow!("Fingerprint mismatch, use correct key"));
135        }
136
137        match &self.private_key {
138            Some(private_key) => {
139                let password: SecretSlice<u8> =
140                    SecretSlice::new(private_key.decrypt(Oaep::new::<Sha256>(), password)?.into());
141
142                let crypto = Aes256Crypto::new(password);
143
144                let out = crypto.decrypt(data, fingerprint.as_bytes())?;
145                Ok(String::from_utf8(out)?)
146            }
147            None => Err(anyhow::anyhow!("Private key is required to view vault")),
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::vault::Vault;
156    use anyhow::Result;
157    use ssh_key::{PrivateKey, PublicKey};
158    use std::path::Path;
159
160    #[test]
161    fn test_rsa_vault_using_both_keys() -> Result<()> {
162        let public_key_file = Path::new("test_data/id_rsa.pub");
163        let private_key_file = Path::new("test_data/id_rsa");
164        let public_key = PublicKey::read_openssh_file(public_key_file)?;
165        let private_key = PrivateKey::read_openssh_file(private_key_file)?;
166        let vault = RsaVault::new(Some(public_key), Some(private_key));
167        assert!(vault.is_err());
168
169        let Err(err) = vault else {
170            unreachable!("expected error when both keys provided")
171        };
172
173        // Convert the error to a string and check the message
174        assert_eq!(
175            err.to_string(),
176            "Only one of public and private key is required"
177        );
178
179        Ok(())
180    }
181
182    #[test]
183    fn test_rsa_vault_using_public_key() -> Result<()> {
184        let public_key_file = Path::new("test_data/id_rsa.pub");
185        let public_key = PublicKey::read_openssh_file(public_key_file)?;
186        let vault = RsaVault::new(Some(public_key), None);
187        assert!(vault.is_ok());
188        Ok(())
189    }
190
191    #[test]
192    fn test_rsa_vault_using_private_key() -> Result<()> {
193        let private_key_file = Path::new("test_data/id_rsa");
194        let private_key = PrivateKey::read_openssh_file(private_key_file)?;
195        let vault = RsaVault::new(None, Some(private_key));
196        assert!(vault.is_ok());
197        Ok(())
198    }
199}