1use hkdf::Hkdf;
4use hmac::{Hmac, KeyInit, Mac};
5use sha2::{Digest, Sha256};
6use zeroize::{Zeroize, Zeroizing};
7
8use crate::{cipher, AuthorshipHasher, CryptoError};
9
10pub(crate) const ENCRYPTION: &[u8] = b"coven/encryption/v1";
11pub(crate) const JOIN_REQUEST: &[u8] = b"coven/join-request/v1";
12pub(crate) const CONFIRMATION_NUMBER: &[u8] = b"coven/confirmation-number/v1";
13pub(crate) const SEALED_BOX: &[u8] = b"coven/sealed-box/v1";
14pub(crate) const AUTHORSHIP: &[u8] = b"coven/row-authorship/v1";
15pub(crate) const KEPT_VALUE: &[u8] = b"coven/kept-value/v1";
16
17pub(crate) fn derive(key: &[u8; 32], context: &[u8]) -> Zeroizing<[u8; 32]> {
18 let mut output = Zeroizing::new([0; 32]);
19 let (mut extracted, hkdf) = Hkdf::<Sha256>::extract(None, key);
20 extracted.as_mut_slice().zeroize();
21 hkdf.expand(context, output.as_mut())
22 .expect("32-byte derived keys fit HKDF-SHA256's output limit");
23 output
24}
25
26pub(crate) fn derive_label(key: &[u8; 32], label: &[u8]) -> Zeroizing<[u8; 32]> {
27 derive(key, &cipher::context(&[label]))
28}
29
30pub(crate) fn mac(key: &[u8; 32]) -> Hmac<Sha256> {
31 Hmac::<Sha256>::new_from_slice(key).expect("HMAC-SHA256 accepts a 32-byte key")
32}
33
34pub struct EncryptionKey(pub(crate) Zeroizing<[u8; 32]>);
36
37impl EncryptionKey {
38 pub fn seal_object_chunk(
43 &self,
44 path: &str,
45 prefix: &[u8],
46 section: u64,
47 index: u64,
48 plaintext: &[u8],
49 ) -> Result<Vec<u8>, CryptoError> {
50 cipher::seal_random(&self.0, &object_chunk_aad(path, prefix, section, index), plaintext)
51 }
52
53 pub fn open_object_chunk(
56 &self,
57 path: &str,
58 prefix: &[u8],
59 section: u64,
60 index: u64,
61 sealed: &[u8],
62 ) -> Result<Vec<u8>, CryptoError> {
63 cipher::open_random(&self.0, &object_chunk_aad(path, prefix, section, index), sealed)
64 }
65}
66
67impl std::fmt::Debug for EncryptionKey {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.write_str("EncryptionKey([REDACTED])")
70 }
71}
72
73pub struct DerivedKeys {
75 encryption: EncryptionKey,
76 authorship: Zeroizing<[u8; 32]>,
77}
78
79impl DerivedKeys {
80 pub(crate) fn new(key: &[u8; 32]) -> Self {
81 Self {
82 encryption: EncryptionKey(derive_label(key, ENCRYPTION)),
83 authorship: derive_label(key, AUTHORSHIP),
84 }
85 }
86
87 pub fn authorship_hasher(&self) -> AuthorshipHasher {
90 AuthorshipHasher::new(&self.authorship)
91 }
92
93 pub fn seal_object_chunk(
96 &self,
97 path: &str,
98 prefix: &[u8],
99 section: u64,
100 index: u64,
101 plaintext: &[u8],
102 ) -> Result<Vec<u8>, CryptoError> {
103 self.encryption.seal_object_chunk(path, prefix, section, index, plaintext)
104 }
105
106 pub fn reseal_object_chunk(
114 &self,
115 path: &str,
116 prefix: &[u8],
117 section: u64,
118 index: u64,
119 plaintext: &[u8],
120 ) -> Vec<u8> {
121 let key = &self.encryption.0;
122 let mut nonce = mac(key);
123 nonce.update(&cipher::context(&[
124 b"coven/object-nonce/v2",
125 cipher::storage_path(path),
126 prefix,
127 §ion.to_be_bytes(),
128 &index.to_be_bytes(),
129 &Sha256::digest(plaintext),
130 ]));
131 let nonce = nonce.finalize().into_bytes();
132 let nonce: &[u8; 24] = nonce[..24].try_into().expect("24-byte nonce");
133 let mut sealed = nonce.to_vec();
134 sealed.extend(cipher::seal(
135 key,
136 nonce,
137 &object_chunk_aad(path, prefix, section, index),
138 plaintext,
139 ));
140 sealed
141 }
142
143 pub fn open_object_chunk(
146 &self,
147 path: &str,
148 prefix: &[u8],
149 section: u64,
150 index: u64,
151 sealed: &[u8],
152 ) -> Result<Vec<u8>, CryptoError> {
153 self.encryption.open_object_chunk(path, prefix, section, index, sealed)
154 }
155
156 pub fn open_identified_header<const N: usize>(
166 &self,
167 prefix: &[u8],
168 sealed: &[u8],
169 identity: impl FnOnce(&[u8; N]) -> Result<String, CryptoError>,
170 ) -> Result<Vec<u8>, CryptoError> {
171 use chacha20::{
172 cipher::{StreamCipher, StreamCipherSeek},
173 KeyIvInit, XChaCha20,
174 };
175 let (nonce, ciphertext) = sealed.split_at_checked(24).ok_or(CryptoError::Malformed)?;
176 if ciphertext.len().checked_sub(cipher::FILE_CHUNK_TAG_LEN).is_none_or(|length| length < N)
177 {
178 return Err(CryptoError::Malformed);
179 }
180 let mut tentative = Zeroizing::new([0; N]);
181 tentative.copy_from_slice(&ciphertext[..N]);
182 let key: &[u8; 32] = &self.encryption.0;
183 let nonce: &[u8; 24] = nonce.try_into().expect("24-byte split");
184 let mut stream = XChaCha20::new(key.into(), nonce.into());
185 stream.seek(64u64);
187 stream.apply_keystream(tentative.as_mut());
188 let path = identity(&tentative)?;
189 self.open_object_chunk(&path, prefix, 0, 0, sealed)
190 }
191}
192
193impl std::fmt::Debug for DerivedKeys {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.write_str("DerivedKeys([REDACTED])")
196 }
197}
198
199fn object_chunk_aad(path: &str, prefix: &[u8], section: u64, index: u64) -> Vec<u8> {
200 cipher::context(&[
201 b"coven/object-chunk/v1",
202 cipher::storage_path(path),
203 prefix,
204 §ion.to_be_bytes(),
205 &index.to_be_bytes(),
206 ])
207}
208
209#[cfg(test)]
210#[path = "derivation_tests.rs"]
211mod tests;