1use crate::chunk::DataChunk;
4use crate::chunkmeta::ChunkMeta;
5use crate::passwords::Passwords;
6
7use aes_gcm::aead::{generic_array::GenericArray, Aead, NewAead, Payload};
8use aes_gcm::Aes256Gcm; use rand::Rng;
10
11use std::str::FromStr;
12
13const CHUNK_V1: &[u8] = b"0001";
14
15pub struct EncryptedChunk {
24 ciphertext: Vec<u8>,
25 aad: Vec<u8>,
26}
27
28impl EncryptedChunk {
29 fn new(ciphertext: Vec<u8>, aad: Vec<u8>) -> Self {
31 Self { ciphertext, aad }
32 }
33
34 pub fn ciphertext(&self) -> &[u8] {
36 &self.ciphertext
37 }
38
39 pub fn aad(&self) -> &[u8] {
41 &self.aad
42 }
43}
44
45pub struct CipherEngine {
47 cipher: Aes256Gcm,
48}
49
50impl CipherEngine {
51 pub fn new(pass: &Passwords) -> Self {
53 let key = GenericArray::from_slice(pass.encryption_key());
54 Self {
55 cipher: Aes256Gcm::new(key),
56 }
57 }
58
59 pub fn encrypt_chunk(&self, chunk: &DataChunk) -> Result<EncryptedChunk, CipherError> {
61 let aad = chunk.meta().to_json_vec();
65 let payload = Payload {
66 msg: chunk.data(),
67 aad: &aad,
68 };
69
70 let nonce = Nonce::new();
72 let nonce_arr = GenericArray::from_slice(nonce.as_bytes());
73
74 let ciphertext = self
76 .cipher
77 .encrypt(nonce_arr, payload)
78 .map_err(CipherError::EncryptError)?;
79
80 let mut vec: Vec<u8> = vec![];
82 push_bytes(&mut vec, CHUNK_V1);
83 push_bytes(&mut vec, nonce.as_bytes());
84 push_bytes(&mut vec, &ciphertext);
85
86 Ok(EncryptedChunk::new(vec, aad))
87 }
88
89 pub fn decrypt_chunk(&self, bytes: &[u8], meta: &[u8]) -> Result<DataChunk, CipherError> {
91 if !bytes.starts_with(CHUNK_V1) {
93 return Err(CipherError::UnknownChunkVersion);
94 }
95 let version_len = CHUNK_V1.len();
96 let bytes = &bytes[version_len..];
97
98 let (nonce, ciphertext) = match bytes.get(..NONCE_SIZE) {
99 Some(nonce) => (GenericArray::from_slice(nonce), &bytes[NONCE_SIZE..]),
100 None => return Err(CipherError::NoNonce),
101 };
102
103 let payload = Payload {
104 msg: ciphertext,
105 aad: meta,
106 };
107
108 let payload = self
109 .cipher
110 .decrypt(nonce, payload)
111 .map_err(CipherError::DecryptError)?;
112 let payload = Payload::from(payload.as_slice());
113
114 let meta = std::str::from_utf8(meta)?;
115 let meta = ChunkMeta::from_str(meta)?;
116
117 let chunk = DataChunk::new(payload.msg.to_vec(), meta);
118
119 Ok(chunk)
120 }
121}
122
123fn push_bytes(vec: &mut Vec<u8>, bytes: &[u8]) {
124 for byte in bytes.iter() {
125 vec.push(*byte);
126 }
127}
128
129#[derive(Debug, thiserror::Error)]
131pub enum CipherError {
132 #[error("failed to encrypt with AES-GEM: {0}")]
134 EncryptError(aes_gcm::Error),
135
136 #[error("encrypted chunk does not start with correct version")]
139 UnknownChunkVersion,
140
141 #[error("encrypted chunk does not have a complete nonce")]
144 NoNonce,
145
146 #[error("failed to decrypt with AES-GEM: {0}")]
148 DecryptError(aes_gcm::Error),
149
150 #[error("failed to parse decrypted data as a DataChunk: {0}")]
152 Parse(serde_yaml::Error),
153
154 #[error(transparent)]
156 Utf8Error(#[from] std::str::Utf8Error),
157
158 #[error("failed to parse JSON: {0}")]
160 JsonParse(#[from] serde_json::Error),
161}
162
163const NONCE_SIZE: usize = 12;
164
165#[derive(Debug)]
166struct Nonce {
167 nonce: Vec<u8>,
168}
169
170impl Nonce {
171 fn from_bytes(bytes: &[u8]) -> Self {
172 assert_eq!(bytes.len(), NONCE_SIZE);
173 Self {
174 nonce: bytes.to_vec(),
175 }
176 }
177
178 fn new() -> Self {
179 let mut bytes: Vec<u8> = vec![0; NONCE_SIZE];
180 let mut rng = rand::thread_rng();
181 for x in bytes.iter_mut() {
182 *x = rng.gen();
183 }
184 Self::from_bytes(&bytes)
185 }
186
187 fn as_bytes(&self) -> &[u8] {
188 &self.nonce
189 }
190}
191
192#[cfg(test)]
193mod test {
194 use crate::chunk::DataChunk;
195 use crate::chunkmeta::ChunkMeta;
196 use crate::cipher::{CipherEngine, CipherError, CHUNK_V1, NONCE_SIZE};
197 use crate::label::Label;
198 use crate::passwords::Passwords;
199
200 #[test]
201 fn metadata_as_aad() {
202 let sum = Label::sha256(b"dummy data");
203 let meta = ChunkMeta::new(&sum);
204 let meta_as_aad = meta.to_json_vec();
205 let chunk = DataChunk::new("hello".as_bytes().to_vec(), meta);
206 let pass = Passwords::new("secret");
207 let cipher = CipherEngine::new(&pass);
208 let enc = cipher.encrypt_chunk(&chunk).unwrap();
209
210 assert_eq!(meta_as_aad, enc.aad());
211 }
212
213 #[test]
214 fn round_trip() {
215 let sum = Label::sha256(b"dummy data");
216 let meta = ChunkMeta::new(&sum);
217 let chunk = DataChunk::new("hello".as_bytes().to_vec(), meta);
218 let pass = Passwords::new("secret");
219
220 let cipher = CipherEngine::new(&pass);
221 let enc = cipher.encrypt_chunk(&chunk).unwrap();
222
223 let bytes: Vec<u8> = enc.ciphertext().to_vec();
224 let dec = cipher.decrypt_chunk(&bytes, enc.aad()).unwrap();
225 assert_eq!(chunk, dec);
226 }
227
228 #[test]
229 fn decrypt_errors_if_nonce_is_too_short() {
230 let pass = Passwords::new("our little test secret");
231 let e = CipherEngine::new(&pass);
232
233 let bytes = {
235 let mut result = [0; CHUNK_V1.len() + NONCE_SIZE - 1];
236 for (i, x) in CHUNK_V1.iter().enumerate() {
237 result[i] = *x;
238 }
239 result
240 };
241
242 let meta = [0; 0];
243
244 assert!(matches!(
245 e.decrypt_chunk(&bytes, &meta),
246 Err(CipherError::NoNonce)
247 ));
248 }
249}