Skip to main content

nook_vault/
crypto.rs

1use crate::{NookError, Result};
2use argon2::{Algorithm, Argon2, Params, Version};
3use chacha20poly1305::aead::{Aead, KeyInit, Payload};
4use chacha20poly1305::{XChaCha20Poly1305, XNonce};
5use hkdf::Hkdf;
6use rand::rngs::OsRng;
7use rand::RngCore;
8use sha2::Sha256;
9use zeroize::Zeroize;
10
11#[derive(Clone)]
12pub struct VaultKey(pub [u8; 32]);
13
14impl VaultKey {
15    pub fn as_bytes(&self) -> &[u8; 32] {
16        &self.0
17    }
18}
19
20impl Drop for VaultKey {
21    fn drop(&mut self) {
22        self.0.zeroize();
23    }
24}
25
26#[derive(Clone)]
27pub struct DataKey(pub [u8; 32]);
28
29impl DataKey {
30    pub fn as_bytes(&self) -> &[u8; 32] {
31        &self.0
32    }
33}
34
35impl Drop for DataKey {
36    fn drop(&mut self) {
37        self.0.zeroize();
38    }
39}
40
41#[derive(Clone, Debug)]
42pub struct WrappedKey(pub Vec<u8>);
43
44pub fn generate_vault_key() -> VaultKey {
45    let mut bytes = [0u8; 32];
46    OsRng.fill_bytes(&mut bytes);
47    VaultKey(bytes)
48}
49
50pub fn generate_data_key() -> DataKey {
51    let mut bytes = [0u8; 32];
52    OsRng.fill_bytes(&mut bytes);
53    DataKey(bytes)
54}
55
56fn derive_wrap_key(vault: &VaultKey) -> Result<[u8; 32]> {
57    let hk = Hkdf::<Sha256>::new(None, vault.as_bytes());
58    let mut out = [0u8; 32];
59    hk.expand(b"nook-wrap-key", &mut out)
60        .map_err(|e| NookError::Crypto(format!("wrap key derivation failed: {e}")))?;
61    Ok(out)
62}
63
64pub fn wrap_data_key(vault: &VaultKey, data: &DataKey) -> WrappedKey {
65    let key_bytes = derive_wrap_key(vault).expect("hkdf expansion to fixed size never fails");
66    let cipher = XChaCha20Poly1305::new((&key_bytes).into());
67    let mut nonce = [0u8; 24];
68    OsRng.fill_bytes(&mut nonce);
69    let ciphertext = cipher
70        .encrypt(
71            XNonce::from_slice(&nonce),
72            Payload {
73                msg: data.as_bytes(),
74                aad: b"nook-key-wrap",
75            },
76        )
77        .expect("encryption with generated key must succeed");
78    let mut out = Vec::with_capacity(nonce.len() + ciphertext.len());
79    out.extend_from_slice(&nonce);
80    out.extend_from_slice(&ciphertext);
81    WrappedKey(out)
82}
83
84pub fn unwrap_data_key(vault: &VaultKey, wrapped: &WrappedKey) -> Result<DataKey> {
85    if wrapped.0.len() < 24 {
86        return Err(NookError::Crypto("wrapped key too short".into()));
87    }
88    let key_bytes = derive_wrap_key(vault)?;
89    let cipher = XChaCha20Poly1305::new((&key_bytes).into());
90    let (nonce, body) = wrapped.0.split_at(24);
91    let plaintext = cipher
92        .decrypt(
93            XNonce::from_slice(nonce),
94            Payload {
95                msg: body,
96                aad: b"nook-key-wrap",
97            },
98        )
99        .map_err(|_| NookError::Crypto("failed to unwrap data key".into()))?;
100    let mut out = [0u8; 32];
101    out.copy_from_slice(&plaintext);
102    Ok(DataKey(out))
103}
104
105pub fn encrypt_chunk(
106    key: &DataKey,
107    nonce: &[u8; 24],
108    associated_data: &[u8],
109    plaintext: &[u8],
110) -> Vec<u8> {
111    let cipher = XChaCha20Poly1305::new_from_slice(key.as_bytes()).unwrap();
112    cipher
113        .encrypt(
114            XNonce::from_slice(nonce),
115            Payload {
116                msg: plaintext,
117                aad: associated_data,
118            },
119        )
120        .expect("encrypt_chunk: encrypt should not fail with correct sizes")
121}
122
123pub fn decrypt_chunk(
124    key: &DataKey,
125    nonce: &[u8; 24],
126    associated_data: &[u8],
127    ciphertext: &[u8],
128) -> Result<Vec<u8>> {
129    let cipher = XChaCha20Poly1305::new_from_slice(key.as_bytes()).unwrap();
130    cipher
131        .decrypt(
132            XNonce::from_slice(nonce),
133            Payload {
134                msg: ciphertext,
135                aad: associated_data,
136            },
137        )
138        .map_err(|_| NookError::Crypto("decrypt_chunk failed".into()))
139}
140
141/// Length in bytes of the random salt used for `derive_passphrase_key`.
142pub const PASSPHRASE_SALT_LEN: usize = 16;
143
144/// Derives a 32-byte wrapping key from a user passphrase via Argon2id, for
145/// encrypting the Vault Master Key at rest when no OS keychain is available.
146/// Uses a conservative interactive profile (19 MiB memory, 2 iterations, 1
147/// lane), per OWASP password-hashing guidance.
148pub fn derive_passphrase_key(passphrase: &[u8], salt: &[u8]) -> Result<DataKey> {
149    let params = Params::new(19_456, 2, 1, Some(32))
150        .map_err(|e| NookError::Crypto(format!("invalid argon2 parameters: {e}")))?;
151    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
152    let mut out = [0u8; 32];
153    argon2
154        .hash_password_into(passphrase, salt, &mut out)
155        .map_err(|e| NookError::Crypto(format!("argon2 key derivation failed: {e}")))?;
156    Ok(DataKey(out))
157}
158
159pub fn derive_head_object_id(vault: &VaultKey) -> [u8; 32] {
160    let hk = Hkdf::<Sha256>::new(None, vault.as_bytes());
161    let mut out = [0u8; 32];
162    hk.expand(b"nook-head", &mut out)
163        .expect("hkdf expansion to fixed size never fails");
164    out
165}