obnam/
cipher.rs

1//! Encryption cipher algorithms.
2
3use 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; // Or `Aes128Gcm`
9use rand::Rng;
10
11use std::str::FromStr;
12
13const CHUNK_V1: &[u8] = b"0001";
14
15/// An encrypted chunk.
16///
17/// This consists of encrypted ciphertext, and un-encrypted (or
18/// cleartext) additional associated data, which could be the metadata
19/// of the chunk, and be used to, for example, find chunks.
20///
21/// Encrypted chunks are the only chunks that can be uploaded to the
22/// server.
23pub struct EncryptedChunk {
24    ciphertext: Vec<u8>,
25    aad: Vec<u8>,
26}
27
28impl EncryptedChunk {
29    /// Create an encrypted chunk.
30    fn new(ciphertext: Vec<u8>, aad: Vec<u8>) -> Self {
31        Self { ciphertext, aad }
32    }
33
34    /// Return the encrypted data.
35    pub fn ciphertext(&self) -> &[u8] {
36        &self.ciphertext
37    }
38
39    /// Return the cleartext associated additional data.
40    pub fn aad(&self) -> &[u8] {
41        &self.aad
42    }
43}
44
45/// An engine for encrypting and decrypting chunks.
46pub struct CipherEngine {
47    cipher: Aes256Gcm,
48}
49
50impl CipherEngine {
51    /// Create a new cipher engine using cleartext passwords.
52    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    /// Encrypt a chunk.
60    pub fn encrypt_chunk(&self, chunk: &DataChunk) -> Result<EncryptedChunk, CipherError> {
61        // Payload with metadata as associated data, to be encrypted.
62        //
63        // The metadata will be stored in cleartext after encryption.
64        let aad = chunk.meta().to_json_vec();
65        let payload = Payload {
66            msg: chunk.data(),
67            aad: &aad,
68        };
69
70        // Unique random key for each encryption.
71        let nonce = Nonce::new();
72        let nonce_arr = GenericArray::from_slice(nonce.as_bytes());
73
74        // Encrypt the sensitive part.
75        let ciphertext = self
76            .cipher
77            .encrypt(nonce_arr, payload)
78            .map_err(CipherError::EncryptError)?;
79
80        // Construct the blob to be stored on the server.
81        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    /// Decrypt a chunk.
90    pub fn decrypt_chunk(&self, bytes: &[u8], meta: &[u8]) -> Result<DataChunk, CipherError> {
91        // Does encrypted chunk start with the right version?
92        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/// Possible errors when encrypting or decrypting chunks.
130#[derive(Debug, thiserror::Error)]
131pub enum CipherError {
132    /// Encryption failed.
133    #[error("failed to encrypt with AES-GEM: {0}")]
134    EncryptError(aes_gcm::Error),
135
136    /// The encrypted chunk has an unsupported version or is
137    /// corrupted.
138    #[error("encrypted chunk does not start with correct version")]
139    UnknownChunkVersion,
140
141    /// The encrypted chunk lacks a complete nonce value, and is
142    /// probably corrupted.
143    #[error("encrypted chunk does not have a complete nonce")]
144    NoNonce,
145
146    /// Decryption failed.
147    #[error("failed to decrypt with AES-GEM: {0}")]
148    DecryptError(aes_gcm::Error),
149
150    /// The decryption succeeded, by data isn't valid YAML.
151    #[error("failed to parse decrypted data as a DataChunk: {0}")]
152    Parse(serde_yaml::Error),
153
154    /// Error parsing UTF8 data.
155    #[error(transparent)]
156    Utf8Error(#[from] std::str::Utf8Error),
157
158    /// Error parsing JSON data.
159    #[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        // *Almost* a valid chunk header, except it's one byte too short
234        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}