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    ///
96    /// # Errors
97    ///
98    /// Returns an error if encryption fails.
99    pub fn create(&self, password: SecretSlice<u8>, data: &mut [u8]) -> Result<String> {
100        self.vault.create(password, data)
101    }
102
103    /// Decrypts and views vault contents
104    ///
105    /// # Arguments
106    ///
107    /// * `password` - Encrypted password bytes from the vault
108    /// * `data` - Encrypted data bytes from the vault
109    /// * `fingerprint` - Expected key fingerprint for verification
110    ///
111    /// # Returns
112    ///
113    /// Returns the decrypted data as a UTF-8 string.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if:
118    /// - The fingerprint doesn't match the private key
119    /// - Decryption fails (wrong key or corrupted data)
120    /// - The decrypted data is not valid UTF-8
121    pub fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String> {
122        self.vault.view(password, data, fingerprint)
123    }
124}
125
126/// Trait defining the vault operations for different key types
127pub trait Vault {
128    /// Creates a new vault instance with the given keys
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the provided keys are invalid or mismatched.
133    fn new(public: Option<PublicKey>, private: Option<PrivateKey>) -> Result<Self>
134    where
135        Self: Sized;
136
137    /// Encrypts data and creates a vault string
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if encryption fails.
142    fn create(&self, password: SecretSlice<u8>, data: &mut [u8]) -> Result<String>;
143
144    /// Decrypts vault contents
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if decryption fails or the fingerprint is invalid.
149    fn view(&self, password: &[u8], data: &[u8], fingerprint: &str) -> Result<String>;
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::vault::{
156        Vault, crypto, parse, ssh::decrypt_private_key, ssh::ed25519::Ed25519Vault,
157        ssh::rsa::RsaVault,
158    };
159    use secrecy::{SecretSlice, SecretString};
160    use ssh_key::PublicKey;
161    use std::path::Path;
162
163    struct Test {
164        public_key: &'static str,
165        private_key: &'static str,
166        passphrase: &'static str,
167    }
168
169    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";
170
171    #[test]
172    fn test_rsa_vault() -> Result<()> {
173        let public_key_file = Path::new("test_data/id_rsa.pub");
174        let private_key_file = Path::new("test_data/id_rsa");
175        let public_key = PublicKey::read_openssh_file(public_key_file)?;
176        let private_key = PrivateKey::read_openssh_file(private_key_file)?;
177
178        let vault = RsaVault::new(Some(public_key), None)?;
179
180        let password: SecretSlice<u8> = crypto::gen_password()?;
181
182        let mut secret = String::from(SECRET).into_bytes();
183
184        // not filled with zeros
185        assert!(secret.iter().all(|&byte| byte != 0));
186
187        let vault = vault.create(password, &mut secret)?;
188
189        // filled with zeros
190        assert!(secret.iter().all(|&byte| byte == 0));
191
192        let (_key_type, fingerprint, password, data) = parse(&vault)?;
193
194        let view = RsaVault::new(None, Some(private_key))?;
195
196        let vault = view.view(&password, &data, &fingerprint)?;
197
198        assert_eq!(vault, SECRET);
199        Ok(())
200    }
201
202    #[test]
203    fn test_ed25519_vault() -> Result<()> {
204        let public_key_file = Path::new("test_data/ed25519.pub");
205        let private_key_file = Path::new("test_data/ed25519");
206        let public_key = PublicKey::read_openssh_file(public_key_file)?;
207        let private_key = PrivateKey::read_openssh_file(private_key_file)?;
208
209        let vault = Ed25519Vault::new(Some(public_key), None)?;
210
211        let password: SecretSlice<u8> = crypto::gen_password()?;
212
213        let mut secret = String::from(SECRET).into_bytes();
214
215        // not filled with zeros
216        assert!(secret.iter().all(|&byte| byte != 0));
217
218        let vault = vault.create(password, &mut secret)?;
219
220        // filled with zeros
221        assert!(secret.iter().all(|&byte| byte == 0));
222
223        let (_key_type, fingerprint, password, data) = parse(&vault)?;
224
225        let view = Ed25519Vault::new(None, Some(private_key))?;
226
227        let vault = view.view(&password, &data, &fingerprint)?;
228
229        assert_eq!(vault, SECRET);
230        Ok(())
231    }
232
233    #[test]
234    fn test_vault() -> Result<()> {
235        let tests = [
236            Test {
237                public_key: "test_data/id_rsa.pub",
238                private_key: "test_data/id_rsa",
239                passphrase: "",
240            },
241            Test {
242                public_key: "test_data/ed25519.pub",
243                private_key: "test_data/ed25519",
244                passphrase: "",
245            },
246            Test {
247                public_key: "test_data/id_rsa_password.pub",
248                private_key: "test_data/id_rsa_password",
249                // echo -n "ssh-vault" | openssl dgst -sha1
250                passphrase: "85990de849bb89120ea3016b6b76f6d004857cb7",
251            },
252            Test {
253                public_key: "test_data/ed25519_password.pub",
254                private_key: "test_data/ed25519_password",
255                // echo -n "ssh-vault" | openssl dgst -sha1
256                passphrase: "85990de849bb89120ea3016b6b76f6d004857cb7",
257            },
258        ];
259
260        for test in &tests {
261            // create
262            let public_key = test.public_key.to_string();
263            let public_key = find::public_key(Some(public_key))?;
264            let key_type = find::key_type(&public_key.algorithm())?;
265            let v = SshVault::new(&key_type, Some(public_key), None)?;
266            let password: SecretSlice<u8> = crypto::gen_password()?;
267
268            let mut secret = String::from(SECRET).into_bytes();
269
270            // not filled with zeros
271            assert!(secret.iter().all(|&byte| byte != 0));
272
273            let vault = v.create(password, &mut secret)?;
274
275            // filled with zeros
276            assert!(secret.iter().all(|&byte| byte == 0));
277
278            // view
279            let private_key = test.private_key.to_string();
280            let (key_type, fingerprint, password, data) = parse(&vault)?;
281            let mut private_key = find::private_key_type(Some(private_key), key_type)?;
282
283            if private_key.is_encrypted() {
284                private_key =
285                    decrypt_private_key(&private_key, Some(SecretString::from(test.passphrase)))?;
286            }
287
288            let key_type = find::key_type(&private_key.algorithm())?;
289
290            let v = SshVault::new(&key_type, None, Some(private_key))?;
291
292            let vault = v.view(&password, &data, &fingerprint)?;
293
294            assert_eq!(vault, SECRET);
295        }
296        Ok(())
297    }
298}