ssh_vault/vault/
mod.rs

1pub mod crypto;
2pub mod dio;
3pub mod find;
4pub mod fingerprint;
5pub mod online;
6pub mod remote;
7pub mod ssh;
8
9pub mod parse;
10pub use self::parse::parse;
11
12use anyhow::Result;
13use secrecy::SecretSlice;
14use ssh_key::{PrivateKey, PublicKey};
15
16/// SSH key types supported by ssh-vault
17#[derive(Debug, PartialEq, Eq)]
18pub enum SshKeyType {
19    /// Ed25519 keys using X25519 Diffie-Hellman and ChaCha20-Poly1305
20    Ed25519,
21    /// RSA keys using RSA-OAEP and AES-256-GCM
22    Rsa,
23}
24
25/// Main vault interface for encrypting and decrypting data using SSH keys
26///
27/// `SshVault` provides a unified interface for working with both Ed25519 and RSA
28/// encryption schemes. It handles key type detection and delegates operations to
29/// the appropriate underlying implementation.
30pub struct SshVault {
31    vault: Box<dyn Vault>,
32}
33
34impl SshVault {
35    /// Creates a new vault instance with the specified key type
36    ///
37    /// # Arguments
38    ///
39    /// * `key_type` - The SSH key type (Ed25519 or RSA)
40    /// * `public` - Optional public key for encryption operations
41    /// * `private` - Optional private key for decryption operations
42    ///
43    /// # Errors
44    ///
45    /// Returns an error if:
46    /// - The key type doesn't match the provided keys
47    /// - Both public and private keys are provided (only one should be provided)
48    /// - The keys are invalid or encrypted without proper decryption
49    ///
50    /// # Examples
51    ///
52    /// ```no_run
53    /// use ssh_vault::vault::{SshVault, SshKeyType};
54    /// use ssh_key::PublicKey;
55    /// use std::path::Path;
56    ///
57    /// # fn main() -> anyhow::Result<()> {
58    /// let public_key = PublicKey::read_openssh_file(Path::new("id_ed25519.pub"))?;
59    /// let vault = SshVault::new(&SshKeyType::Ed25519, Some(public_key), None)?;
60    /// # Ok(())
61    /// # }
62    /// ```
63    pub fn new(
64        key_type: &SshKeyType,
65        public: Option<PublicKey>,
66        private: Option<PrivateKey>,
67    ) -> Result<Self> {
68        let vault = match key_type {
69            SshKeyType::Ed25519 => {
70                Box::new(ssh::ed25519::Ed25519Vault::new(public, private)?) as Box<dyn Vault>
71            }
72            SshKeyType::Rsa => {
73                Box::new(ssh::rsa::RsaVault::new(public, private)?) as Box<dyn Vault>
74            }
75        };
76        Ok(Self { vault })
77    }
78
79    /// Encrypts data and creates a vault
80    ///
81    /// # Arguments
82    ///
83    /// * `password` - Secret password for encrypting the data
84    /// * `data` - Mutable byte slice to encrypt (will be zeroed after encryption)
85    ///
86    /// # Returns
87    ///
88    /// Returns the vault as a formatted string that can be stored or transmitted.
89    /// The format includes the algorithm, fingerprint, and encrypted payload.
90    ///
91    /// # Security
92    ///
93    /// The input `data` is zeroed after encryption to prevent sensitive data
94    /// from remaining in memory.
95    pub fn create(&self, password: SecretSlice<u8>, data: &mut [u8]) -> Result<String> {
96        self.vault.create(password, data)
97    }
98
99    /// Decrypts and views vault contents
100    ///
101    /// # Arguments
102    ///
103    /// * `password` - Encrypted password bytes from the vault
104    /// * `data` - Encrypted data bytes from the vault
105    /// * `fingerprint` - Expected key fingerprint for verification
106    ///
107    /// # Returns
108    ///
109    /// Returns the decrypted data as a UTF-8 string.
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if:
114    /// - The fingerprint doesn't match the private key
115    /// - Decryption fails (wrong key or corrupted data)
116    /// - The decrypted data is not valid UTF-8
117    pub fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String> {
118        self.vault.view(password, data, fingerprint)
119    }
120}
121
122/// Trait defining the vault operations for different key types
123pub trait Vault {
124    /// Creates a new vault instance with the given keys
125    fn new(public: Option<PublicKey>, private: Option<PrivateKey>) -> Result<Self>
126    where
127        Self: Sized;
128
129    /// Encrypts data and creates a vault string
130    fn create(&self, password: SecretSlice<u8>, data: &mut [u8]) -> Result<String>;
131
132    /// Decrypts vault contents
133    fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String>;
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::vault::{
140        Vault, crypto, parse, ssh::decrypt_private_key, ssh::ed25519::Ed25519Vault,
141        ssh::rsa::RsaVault,
142    };
143    use secrecy::{SecretSlice, SecretString};
144    use ssh_key::PublicKey;
145    use std::path::Path;
146
147    struct Test {
148        public_key: &'static str,
149        private_key: &'static str,
150        passphrase: &'static str,
151    }
152
153    const SECRET: &str = "Take care of your thoughts, because they will become your words. Take care of your words, because they will become your actions. Take care of your actions, because they will become your habits. Take care of your habits, because they will become your destiny";
154
155    #[test]
156    fn test_rsa_vault() -> Result<()> {
157        let public_key_file = Path::new("test_data/id_rsa.pub");
158        let private_key_file = Path::new("test_data/id_rsa");
159        let public_key = PublicKey::read_openssh_file(&public_key_file)?;
160        let private_key = PrivateKey::read_openssh_file(&private_key_file)?;
161
162        let vault = RsaVault::new(Some(public_key), None)?;
163
164        let password: SecretSlice<u8> = crypto::gen_password()?;
165
166        let mut secret = String::from(SECRET).into_bytes();
167
168        // not filled with zeros
169        assert!(secret.iter().all(|&byte| byte != 0));
170
171        let vault = vault.create(password, &mut secret)?;
172
173        // filled with zeros
174        assert!(secret.iter().all(|&byte| byte == 0));
175
176        let (_key_type, fingerprint, password, data) = parse(&vault)?;
177
178        let view = RsaVault::new(None, Some(private_key))?;
179
180        let vault = view.view(&password, &data, &fingerprint)?;
181
182        assert_eq!(vault, SECRET);
183        Ok(())
184    }
185
186    #[test]
187    fn test_ed25519_vault() -> Result<()> {
188        let public_key_file = Path::new("test_data/ed25519.pub");
189        let private_key_file = Path::new("test_data/ed25519");
190        let public_key = PublicKey::read_openssh_file(&public_key_file)?;
191        let private_key = PrivateKey::read_openssh_file(&private_key_file)?;
192
193        let vault = Ed25519Vault::new(Some(public_key), None)?;
194
195        let password: SecretSlice<u8> = crypto::gen_password()?;
196
197        let mut secret = String::from(SECRET).into_bytes();
198
199        // not filled with zeros
200        assert!(secret.iter().all(|&byte| byte != 0));
201
202        let vault = vault.create(password, &mut secret)?;
203
204        // filled with zeros
205        assert!(secret.iter().all(|&byte| byte == 0));
206
207        let (_key_type, fingerprint, password, data) = parse(&vault)?;
208
209        let view = Ed25519Vault::new(None, Some(private_key))?;
210
211        let vault = view.view(&password, &data, &fingerprint)?;
212
213        assert_eq!(vault, SECRET);
214        Ok(())
215    }
216
217    #[test]
218    fn test_vault() -> Result<()> {
219        let tests = [
220            Test {
221                public_key: "test_data/id_rsa.pub",
222                private_key: "test_data/id_rsa",
223                passphrase: "",
224            },
225            Test {
226                public_key: "test_data/ed25519.pub",
227                private_key: "test_data/ed25519",
228                passphrase: "",
229            },
230            Test {
231                public_key: "test_data/id_rsa_password.pub",
232                private_key: "test_data/id_rsa_password",
233                // echo -n "ssh-vault" | openssl dgst -sha1
234                passphrase: "85990de849bb89120ea3016b6b76f6d004857cb7",
235            },
236            Test {
237                public_key: "test_data/ed25519_password.pub",
238                private_key: "test_data/ed25519_password",
239                // echo -n "ssh-vault" | openssl dgst -sha1
240                passphrase: "85990de849bb89120ea3016b6b76f6d004857cb7",
241            },
242        ];
243
244        for test in tests.iter() {
245            // create
246            let public_key = test.public_key.to_string();
247            let public_key = find::public_key(Some(public_key))?;
248            let key_type = find::key_type(&public_key.algorithm())?;
249            let v = SshVault::new(&key_type, Some(public_key), None)?;
250            let password: SecretSlice<u8> = crypto::gen_password()?;
251
252            let mut secret = String::from(SECRET).into_bytes();
253
254            // not filled with zeros
255            assert!(secret.iter().all(|&byte| byte != 0));
256
257            let vault = v.create(password, &mut secret)?;
258
259            // filled with zeros
260            assert!(secret.iter().all(|&byte| byte == 0));
261
262            // view
263            let private_key = test.private_key.to_string();
264            let (key_type, fingerprint, password, data) = parse(&vault)?;
265            let mut private_key = find::private_key_type(Some(private_key), key_type)?;
266
267            if private_key.is_encrypted() {
268                private_key =
269                    decrypt_private_key(&private_key, Some(SecretString::from(test.passphrase)))?;
270            }
271
272            let key_type = find::key_type(&private_key.algorithm())?;
273
274            let v = SshVault::new(&key_type, None, Some(private_key))?;
275
276            let vault = v.view(&password, &data, &fingerprint)?;
277
278            assert_eq!(vault, SECRET);
279        }
280        Ok(())
281    }
282}