Skip to main content

krypton/
crypto.rs

1//! Low-level cryptographic primitives.
2//!
3//! Everything in this module is building-block level: authenticated
4//! encryption with associated data (AES-256-GCM), password key derivation
5//! (Argon2id), per-file subkey derivation (HKDF-SHA256) and constant-time
6//! comparison. The higher-level single-file and vault APIs compose these
7//! primitives; most applications should use [`crate::encrypt_file`],
8//! [`crate::decrypt_file`] or [`crate::Vault`] instead.
9//!
10//! # Design notes
11//!
12//! * All keys live in [`zeroize::Zeroizing`] memory and are overwritten when
13//!   dropped. Keys are passed by reference; no public API hands out raw key
14//!   bytes by value.
15//! * Every AEAD operation takes associated data (AAD). Callers bind all
16//!   unauthenticated header fields through AAD — this is what makes
17//!   truncation, reordering and format-confusion attacks detectable.
18//! * [`Debug`] for keys is manually implemented and never prints key bytes.
19
20use aes_gcm::{
21    aead::{rand_core::RngCore, AeadInPlace, KeyInit, OsRng},
22    Aes256Gcm, Nonce,
23};
24use argon2::{Algorithm, Argon2, Params, Version};
25use hkdf::Hkdf;
26use sha2::Sha256;
27use subtle::ConstantTimeEq;
28use zeroize::{ZeroizeOnDrop, Zeroizing};
29
30use crate::error::{Error, Result};
31use crate::kdf::KdfParams;
32
33/// Symmetric key length in bytes (AES-256).
34pub const KEY_LEN: usize = 32;
35/// GCM nonce length in bytes.
36pub const NONCE_LEN: usize = 12;
37/// GCM authentication tag length in bytes.
38pub const TAG_LEN: usize = 16;
39
40/// Raw salt length used by new (v3+) containers.
41pub const SALT_LEN: usize = 32;
42
43/// A 32-byte symmetric key stored in zeroizing memory.
44#[derive(Clone, ZeroizeOnDrop)]
45pub struct Key(Zeroizing<[u8; KEY_LEN]>);
46
47impl Key {
48    /// Generates a fresh random key from the operating system CSPRNG.
49    pub fn generate() -> Self {
50        Self(Zeroizing::new(random_key_bytes()))
51    }
52
53    /// Constructs a key from existing bytes. The input is copied into
54    /// zeroizing memory; callers are responsible for zeroing their own copy.
55    pub fn from_bytes(bytes: [u8; KEY_LEN]) -> Self {
56        Self(Zeroizing::new(bytes))
57    }
58
59    /// Borrows the raw key bytes.
60    pub fn expose(&self) -> &[u8; KEY_LEN] {
61        &self.0
62    }
63
64    pub(crate) fn cipher(&self) -> Result<Aes256Gcm> {
65        Aes256Gcm::new_from_slice(self.0.as_slice()).map_err(|_| Error::Encryption)
66    }
67}
68
69impl core::fmt::Debug for Key {
70    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71        f.write_str("Key([REDACTED])")
72    }
73}
74
75/// A 96-bit GCM nonce.
76pub type Nonce12 = [u8; NONCE_LEN];
77
78/// Fills `buf` with random bytes from the operating system CSPRNG.
79pub fn fill_random(buf: &mut [u8]) {
80    OsRng.fill_bytes(buf);
81}
82
83pub(crate) fn random_key_bytes() -> [u8; KEY_LEN] {
84    let mut k = [0u8; KEY_LEN];
85    fill_random(&mut k);
86    k
87}
88
89/// Generates a fresh random nonce.
90pub fn random_nonce() -> Nonce12 {
91    let mut n = [0u8; NONCE_LEN];
92    fill_random(&mut n);
93    n
94}
95
96/// Encrypts `plaintext` under `key`, binding `aad`.
97///
98/// Returns `(nonce, ciphertext)` where the ciphertext includes the trailing
99/// 16-byte GCM tag.
100pub fn seal(plaintext: &[u8], key: &Key, aad: &[u8]) -> Result<(Nonce12, Vec<u8>)> {
101    let nonce = random_nonce();
102    let mut buf = plaintext.to_vec();
103    seal_in_place(&nonce, &mut buf, key, aad)?;
104    Ok((nonce, buf))
105}
106
107/// In-place encryption: appends the authentication tag to `buf`.
108pub(crate) fn seal_in_place(
109    nonce: &Nonce12,
110    buf: &mut Vec<u8>,
111    key: &Key,
112    aad: &[u8],
113) -> Result<()> {
114    let cipher = key.cipher()?;
115    cipher
116        .encrypt_in_place(Nonce::from_slice(nonce), aad, buf)
117        .map_err(|_| Error::Encryption)
118}
119
120/// Decrypts `ciphertext_with_tag` under `key` after verifying `aad`.
121///
122/// Returns the plaintext in zeroizing memory. Fails with
123/// [`Error::Authentication`] if the password/key is wrong or the data was
124/// modified.
125pub fn open(
126    ciphertext_with_tag: &[u8],
127    nonce: &Nonce12,
128    key: &Key,
129    aad: &[u8],
130) -> Result<Zeroizing<Vec<u8>>> {
131    let mut buf = Zeroizing::new(ciphertext_with_tag.to_vec());
132    open_in_place(nonce, &mut buf, key, aad)?;
133    Ok(buf)
134}
135
136/// In-place decryption over a buffer holding `ciphertext || tag`.
137pub(crate) fn open_in_place(
138    nonce: &Nonce12,
139    buf: &mut Vec<u8>,
140    key: &Key,
141    aad: &[u8],
142) -> Result<()> {
143    let cipher = key.cipher()?;
144    cipher
145        .decrypt_in_place(Nonce::from_slice(nonce), aad, buf)
146        .map_err(|_| Error::Authentication)
147}
148
149struct Argon2Instance(Argon2<'static>);
150
151fn make_argon2(params: KdfParams) -> Result<Argon2Instance> {
152    let p = Params::new(
153        params.m_cost_kib,
154        params.t_cost,
155        params.p_cost,
156        Some(KEY_LEN),
157    )
158    .map_err(|_| Error::InvalidKdfParams)?;
159    Ok(Argon2Instance(Argon2::new(
160        Algorithm::Argon2id,
161        Version::V0x13,
162        p,
163    )))
164}
165
166/// Derives a 256-bit key from `password` and `salt` using Argon2id.
167///
168/// `salt` must be at least 8 bytes; new containers use 32 raw random bytes.
169pub fn derive_key(password: &[u8], salt: &[u8], params: KdfParams) -> Result<Key> {
170    params.validate()?;
171    if salt.len() < 8 {
172        return Err(Error::InvalidHeader);
173    }
174
175    let argon2 = make_argon2(params)?;
176
177    let mut out = Zeroizing::new([0u8; KEY_LEN]);
178    argon2
179        .0
180        .hash_password_into(password, salt, out.as_mut())
181        .map_err(|_| Error::KeyDerivation)?;
182    Ok(Key(out))
183}
184
185/// Derives an independent per-object subkey from a master key using
186/// HKDF-SHA256.
187///
188/// Each encrypted object uses its own random `salt`, so even if two objects
189/// were to end up with identical nonce sequences they would still be
190/// protected by distinct keys.
191pub(crate) fn derive_subkey(master: &Key, salt: &[u8], info: &[u8]) -> Key {
192    let hk = Hkdf::<Sha256>::new(Some(salt), master.expose());
193    let mut okm = Zeroizing::new([0u8; KEY_LEN]);
194    hk.expand(info, okm.as_mut())
195        .expect("32-byte OKM is valid for SHA-256");
196    Key(okm)
197}
198
199/// Constant-time equality comparison.
200///
201/// Unlike `==`, the runtime does not depend on where the first differing byte
202/// occurs. Length mismatch still returns `false` immediately (lengths are not
203/// secret in this crate).
204pub fn secure_compare(a: &[u8], b: &[u8]) -> bool {
205    if a.len() != b.len() {
206        return false;
207    }
208    bool::from(a.ct_eq(b))
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn seal_open_roundtrip() {
217        let key = Key::generate();
218        let (nonce, ct) = seal(b"attack at dawn", &key, b"context").unwrap();
219        let pt = open(&ct, &nonce, &key, b"context").unwrap();
220        assert_eq!(&pt[..], b"attack at dawn");
221    }
222
223    #[test]
224    fn aad_is_binding() {
225        let key = Key::generate();
226        let (nonce, ct) = seal(b"secret", &key, b"aad-1").unwrap();
227        assert!(open(&ct, &nonce, &key, b"aad-2").is_err());
228    }
229
230    #[test]
231    fn wrong_key_fails() {
232        let (nonce, ct) = seal(b"secret", &Key::generate(), b"").unwrap();
233        assert!(open(&ct, &nonce, &Key::generate(), b"").is_err());
234    }
235
236    #[test]
237    fn tampered_ciphertext_fails() {
238        let key = Key::generate();
239        let (nonce, mut ct) = seal(b"secret", &key, b"").unwrap();
240        ct[0] ^= 1;
241        assert!(open(&ct, &nonce, &key, b"").is_err());
242    }
243
244    #[test]
245    fn derive_key_matches_params_and_salt() {
246        let params = KdfParams {
247            m_cost_kib: 8 * 1024,
248            t_cost: 1,
249            p_cost: 1,
250        };
251        let k1 = derive_key(b"pw", b"0123456789abcdef", params).unwrap();
252        let k2 = derive_key(b"pw", b"0123456789abcdef", params).unwrap();
253        let k3 = derive_key(b"pw", b"fedcba9876543210", params).unwrap();
254        assert_eq!(k1.expose(), k2.expose());
255        assert_ne!(k1.expose(), k3.expose());
256    }
257
258    #[test]
259    fn debug_redacts_keys() {
260        let key = Key::generate();
261        let rendered = format!("{key:?}");
262        assert!(
263            !rendered.contains("Key(") && !rendered.ends_with(')') || rendered.contains("REDACTED")
264        );
265    }
266
267    #[test]
268    fn secure_compare_basics() {
269        assert!(secure_compare(b"abc", b"abc"));
270        assert!(!secure_compare(b"abc", b"abd"));
271        assert!(!secure_compare(b"abc", b"abcd"));
272    }
273
274    #[test]
275    fn subkeys_are_distinct_per_salt() {
276        let master = Key::generate();
277        let a = derive_subkey(&master, b"salt-a", b"info");
278        let b = derive_subkey(&master, b"salt-b", b"info");
279        assert_ne!(a.expose(), b.expose());
280    }
281}