Skip to main content

hyde_core/
passphrase.rs

1use crate::{
2    backend::{BackendType, WrappedKey},
3    error::{HydeError, Result},
4    recovery::{BackupBundle, RecoveryStrategy, RecoveryType},
5};
6
7/// Passphrase-based recovery using Argon2id key derivation + AES-256-GCM.
8///
9/// # Example
10/// ```ignore
11/// use hyde::recovery::PassphraseRecovery;
12///
13/// let strategy = PassphraseRecovery;
14/// let backup = ctx.backup(&protected, &strategy, Some(b"my-passphrase"))?;
15/// let restored = ctx.restore(&backup, &protected.ciphertext, &strategy, b"my-passphrase")?;
16/// ```
17pub struct PassphraseRecovery;
18
19impl RecoveryStrategy for PassphraseRecovery {
20    fn backup(&self, key: &WrappedKey, secret: Option<&[u8]>) -> Result<BackupBundle> {
21        let passphrase = secret.ok_or_else(|| {
22            HydeError::RecoveryFailed("passphrase is required for PassphraseRecovery".into())
23        })?;
24
25        let mut derived = derive_key_from_passphrase(passphrase)?;
26        let encrypted = aes_gcm_encrypt(&derived.key, &key.blob);
27        zeroize::Zeroize::zeroize(&mut derived.key);
28
29        let encrypted = encrypted?;
30
31        // Format: [16 bytes salt] [encrypted blob (nonce + ciphertext + tag)]
32        let mut data = Vec::with_capacity(16 + encrypted.len());
33        data.extend_from_slice(&derived.salt);
34        data.extend_from_slice(&encrypted);
35
36        Ok(BackupBundle {
37            recovery_type: RecoveryType::Passphrase,
38            data,
39            user_secret: None,
40        })
41    }
42
43    fn restore(&self, bundle: &BackupBundle, secret: &[u8]) -> Result<WrappedKey> {
44        if bundle.data.len() < 16 + 12 + 16 {
45            return Err(HydeError::RecoveryFailed("backup too short".into()));
46        }
47
48        let salt = &bundle.data[..16];
49        let encrypted = &bundle.data[16..];
50
51        let mut derived = derive_key_with_salt(secret, salt)?;
52        let blob = aes_gcm_decrypt(&derived.key, encrypted)
53            .map_err(|_| HydeError::RecoveryFailed("wrong passphrase or corrupted backup".into()));
54        zeroize::Zeroize::zeroize(&mut derived.key);
55
56        Ok(WrappedKey {
57            blob: blob?,
58            backend: BackendType::Tpm,
59        })
60    }
61
62    fn recovery_type(&self) -> RecoveryType {
63        RecoveryType::Passphrase
64    }
65}
66
67// ---------------------------------------------------------------------------
68// AES-256-GCM helpers
69// ---------------------------------------------------------------------------
70
71pub(crate) fn aes_gcm_encrypt(key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>> {
72    use aes_gcm::{aead::Aead, aead::OsRng, Aes256Gcm, AeadCore, KeyInit};
73
74    let cipher = Aes256Gcm::new_from_slice(key)
75        .map_err(|e| HydeError::Serialization(e.to_string()))?;
76
77    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
78
79    let ciphertext = cipher
80        .encrypt(&nonce, plaintext)
81        .map_err(|_| HydeError::SealMismatch)?;
82
83    let mut output = Vec::with_capacity(12 + ciphertext.len());
84    output.extend_from_slice(&nonce);
85    output.extend_from_slice(&ciphertext);
86    Ok(output)
87}
88
89pub(crate) fn aes_gcm_decrypt(key: &[u8], sealed: &[u8]) -> Result<Vec<u8>> {
90    use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
91
92    if sealed.len() < 12 {
93        return Err(HydeError::InvalidKey);
94    }
95
96    let cipher = Aes256Gcm::new_from_slice(key)
97        .map_err(|e| HydeError::Serialization(e.to_string()))?;
98
99    let nonce = Nonce::from_slice(&sealed[..12]);
100    let ciphertext = &sealed[12..];
101
102    cipher
103        .decrypt(nonce, ciphertext)
104        .map_err(|_| HydeError::SealMismatch)
105}
106
107// ---------------------------------------------------------------------------
108// Argon2id key derivation helpers
109// ---------------------------------------------------------------------------
110
111struct DerivedKey {
112    key: Vec<u8>,
113    salt: [u8; 16],
114}
115
116fn derive_key_from_passphrase(passphrase: &[u8]) -> Result<DerivedKey> {
117    let mut salt = [0u8; 16];
118    getrandom::getrandom(&mut salt)
119        .map_err(|e| HydeError::RecoveryFailed(format!("random salt generation failed: {e}")))?;
120    derive_key_with_salt(passphrase, &salt)
121}
122
123fn derive_key_with_salt(passphrase: &[u8], salt: &[u8]) -> Result<DerivedKey> {
124    use argon2::Argon2;
125
126    let mut key = vec![0u8; 32];
127    let argon2 = Argon2::default();
128    argon2
129        .hash_password_into(passphrase, salt, &mut key)
130        .map_err(|e| HydeError::RecoveryFailed(format!("key derivation failed: {e}")))?;
131
132    let mut salt_arr = [0u8; 16];
133    salt_arr.copy_from_slice(&salt[..16]);
134
135    Ok(DerivedKey {
136        key,
137        salt: salt_arr,
138    })
139}