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
141pub const PASSPHRASE_SALT_LEN: usize = 16;
143
144pub 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}