Skip to main content

ssh_vault/vault/crypto/
mod.rs

1pub mod aes256;
2pub mod chacha20poly1305;
3
4use anyhow::{Result, anyhow};
5use hkdf::Hkdf;
6use rand::{RngCore, rngs::OsRng};
7use rsa::sha2;
8use secrecy::SecretSlice;
9use sha2::Sha256;
10
11/// Trait defining cryptographic operations for vault encryption
12///
13/// This trait provides a common interface for different authenticated encryption
14/// algorithms used in ssh-vault (AES-256-GCM and ChaCha20-Poly1305).
15pub trait Crypto {
16    /// Creates a new crypto instance with the given key
17    fn new(key: SecretSlice<u8>) -> Self;
18
19    /// Encrypts data using authenticated encryption with associated data (AEAD)
20    ///
21    /// # Arguments
22    ///
23    /// * `data` - The plaintext data to encrypt
24    /// * `fingerprint` - Additional authenticated data (key fingerprint)
25    ///
26    /// # Returns
27    ///
28    /// Returns the encrypted data including nonce/IV prepended to the ciphertext
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if encryption fails.
33    fn encrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>>;
34
35    /// Decrypts data using authenticated encryption with associated data (AEAD)
36    ///
37    /// # Arguments
38    ///
39    /// * `data` - The encrypted data including nonce/IV
40    /// * `fingerprint` - Additional authenticated data for verification
41    ///
42    /// # Returns
43    ///
44    /// Returns the decrypted plaintext data
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if authentication fails or decryption is unsuccessful
49    fn decrypt(&self, data: &[u8], fingerprint: &[u8]) -> Result<Vec<u8>>;
50}
51
52/// Generates a cryptographically secure random password
53///
54/// Creates a 32-byte (256-bit) random password suitable for vault encryption.
55///
56/// # Returns
57///
58/// Returns a secret slice containing the random password
59///
60/// # Security
61///
62/// Uses the operating system's cryptographically secure random number generator
63///
64/// # Errors
65///
66/// Returns an error if secure random bytes cannot be generated.
67pub fn gen_password() -> Result<SecretSlice<u8>> {
68    let mut password = [0_u8; 32];
69    OsRng.fill_bytes(&mut password);
70    Ok(SecretSlice::new(password.into()))
71}
72
73/// HMAC-based Key Derivation Function (HKDF) using SHA-256
74///
75/// Derives a 256-bit key from input keying material using HKDF-SHA256.
76///
77/// # Arguments
78///
79/// * `salt` - Salt value for the KDF (should be 64 bytes for ed25519 vaults)
80/// * `info` - Context and application specific information (fingerprint)
81/// * `ikm` - Input keying material (shared secret from key exchange)
82///
83/// # Returns
84///
85/// Returns a 32-byte derived key
86///
87/// # Security
88///
89/// HKDF provides cryptographic strength key derivation from potentially weak
90/// shared secrets. The salt should be unique per encryption operation.
91///
92/// # Errors
93///
94/// Returns an error if key expansion fails.
95pub fn hkdf(salt: &[u8], info: &[u8], ikm: &[u8]) -> Result<[u8; 32], anyhow::Error> {
96    let mut output_key_material = [0; 32];
97
98    // Expand the input keying material into an output keying material of 32 bytes
99    Hkdf::<Sha256>::new(Some(salt), ikm)
100        .expand(info, &mut output_key_material)
101        .map_err(|err| anyhow!("Error during HKDF expansion: {err}"))?;
102
103    Ok(output_key_material)
104}
105
106#[cfg(test)]
107#[allow(clippy::unwrap_used)]
108mod tests {
109    use super::*;
110    use hex_literal::hex;
111    use secrecy::ExposeSecret;
112
113    #[test]
114    fn test_gen_password() {
115        let password = gen_password().unwrap();
116        assert_eq!(password.expose_secret().len(), 32);
117    }
118
119    #[test]
120    fn test_hkdf() {
121        let ikm = hex!("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
122        let info = hex!("f0f1f2f3f4f5f6f7f8f9");
123        let salt = hex!("000102030405060708090a0b0c");
124        let expected = hex!(
125            "
126                3cb25f25faacd57a90434f64d0362f2a
127                2d2d0a90cf1a5a4c5db02d56ecc4c5bf
128                34007208d5b887185865
129            "
130        );
131        let okm = hkdf(&salt, &info, &ikm).unwrap();
132        assert_eq!(okm[..], expected[..32]);
133    }
134}