Skip to main content

hightower_kv/
crypto.rs

1use aes_gcm::aead::{Aead, KeyInit};
2use aes_gcm::{Aes256Gcm, Key, Nonce};
3use argon2::Argon2;
4use argon2::password_hash::rand_core::OsRng;
5use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
6use rand::RngCore;
7use serde::{Deserialize, Serialize};
8
9use crate::error::{Error, Result};
10
11/// Trait for hashing and verifying secrets like passwords and API keys
12pub trait SecretHasher: Send + Sync {
13    /// Hashes a secret using a cryptographically secure algorithm
14    fn hash_secret(&self, secret: &[u8]) -> Result<SecretHash>;
15    /// Verifies a secret against a previously computed hash
16    fn verify_secret(&self, secret: &[u8], hash: &SecretHash) -> Result<bool>;
17}
18
19/// A cryptographically secure hash of a secret
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21pub struct SecretHash(String);
22
23impl SecretHash {
24    /// Returns the hash as a string slice
25    pub fn as_str(&self) -> &str {
26        &self.0
27    }
28}
29
30impl From<String> for SecretHash {
31    fn from(value: String) -> Self {
32        Self(value)
33    }
34}
35
36impl AsRef<str> for SecretHash {
37    fn as_ref(&self) -> &str {
38        self.as_str()
39    }
40}
41
42/// Secret hasher using the Argon2 algorithm
43pub struct Argon2SecretHasher {
44    argon2: Argon2<'static>,
45}
46
47impl Argon2SecretHasher {
48    /// Creates a new Argon2 secret hasher with default settings
49    pub fn new() -> Self {
50        Self {
51            argon2: Argon2::default(),
52        }
53    }
54}
55
56impl Default for Argon2SecretHasher {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl SecretHasher for Argon2SecretHasher {
63    fn hash_secret(&self, secret: &[u8]) -> Result<SecretHash> {
64        let salt = SaltString::generate(&mut OsRng);
65        let hash = self
66            .argon2
67            .hash_password(secret, &salt)
68            .map_err(|err| Error::Crypto(err.to_string()))?;
69        Ok(SecretHash(hash.to_string()))
70    }
71
72    fn verify_secret(&self, secret: &[u8], hash: &SecretHash) -> Result<bool> {
73        let parsed =
74            PasswordHash::new(hash.as_str()).map_err(|err| Error::Crypto(err.to_string()))?;
75        Ok(self.argon2.verify_password(secret, &parsed).is_ok())
76    }
77}
78
79/// Trait for encrypting and decrypting data using envelope encryption
80pub trait EnvelopeEncryptor: Send + Sync {
81    /// Encrypts plaintext and returns an encrypted blob with nonce
82    fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptedBlob>;
83    /// Decrypts an encrypted blob and returns the plaintext
84    fn decrypt(&self, blob: &EncryptedBlob) -> Result<Vec<u8>>;
85}
86
87/// An encrypted blob containing a nonce and ciphertext
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
89pub struct EncryptedBlob {
90    /// Random nonce used for encryption
91    pub nonce: [u8; 12],
92    /// Encrypted ciphertext
93    pub ciphertext: Vec<u8>,
94}
95
96/// Encryptor using AES-256-GCM authenticated encryption
97pub struct AesGcmEncryptor {
98    cipher: Aes256Gcm,
99}
100
101impl AesGcmEncryptor {
102    /// Creates a new AES-GCM encryptor with the provided 32-byte key
103    pub fn new(key_bytes: [u8; 32]) -> Self {
104        Self {
105            cipher: Aes256Gcm::new(&Key::<Aes256Gcm>::from_slice(&key_bytes)),
106        }
107    }
108
109    /// Creates a new AES-GCM encryptor from a byte slice, returning an error if not exactly 32 bytes
110    pub fn from_slice(slice: &[u8]) -> Result<Self> {
111        let key: [u8; 32] = slice
112            .try_into()
113            .map_err(|_| Error::Crypto("encryption key must be 32 bytes".into()))?;
114        Ok(Self::new(key))
115    }
116}
117
118impl EnvelopeEncryptor for AesGcmEncryptor {
119    fn encrypt(&self, plaintext: &[u8]) -> Result<EncryptedBlob> {
120        let mut nonce_bytes = [0u8; 12];
121        rand::thread_rng().fill_bytes(&mut nonce_bytes);
122        let nonce = Nonce::from_slice(&nonce_bytes);
123        let ciphertext = self
124            .cipher
125            .encrypt(nonce, plaintext)
126            .map_err(|err| Error::Crypto(err.to_string()))?;
127        Ok(EncryptedBlob {
128            nonce: nonce_bytes,
129            ciphertext,
130        })
131    }
132
133    fn decrypt(&self, blob: &EncryptedBlob) -> Result<Vec<u8>> {
134        let nonce = Nonce::from_slice(&blob.nonce);
135        self.cipher
136            .decrypt(nonce, blob.ciphertext.as_ref())
137            .map_err(|err| Error::Crypto(err.to_string()))
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn argon2_round_trips() {
147        let hasher = Argon2SecretHasher::default();
148        let hash = hasher.hash_secret(b"secret").unwrap();
149        assert!(hasher.verify_secret(b"secret", &hash).unwrap());
150        assert!(!hasher.verify_secret(b"wrong", &hash).unwrap());
151    }
152
153    #[test]
154    fn aes_gcm_encrypt_decrypt() {
155        let encryptor = AesGcmEncryptor::new([42u8; 32]);
156        let blob = encryptor.encrypt(b"payload").unwrap();
157        let plaintext = encryptor.decrypt(&blob).unwrap();
158        assert_eq!(plaintext, b"payload");
159    }
160}