engine/snapshot/
logic.rs

1// Copyright 2020-2021 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    convert::TryInto,
6    fs::{rename, File, OpenOptions},
7    io::{Read, Write},
8    path::Path,
9    sync::atomic::{AtomicU8, Ordering},
10};
11
12use crypto::{keys::age, utils::rand};
13use thiserror::Error as DeriveError;
14use zeroize::Zeroizing;
15
16use crate::snapshot::{compress, decompress};
17
18/// Magic bytes (bytes 0-4 in a snapshot file) aka PARTI
19pub const MAGIC: [u8; 5] = [0x50, 0x41, 0x52, 0x54, 0x49];
20
21/// Current version bytes (bytes 5-6 in a snapshot file)
22pub const VERSION: [u8; 2] = [0x3, 0x0];
23// pub const OLD_VERSION: [u8; 2] = [0x2, 0x0];
24
25/// Key size for the ephemeral key
26pub const KEY_SIZE: usize = 32;
27/// Key type alias.
28pub type Key = [u8; KEY_SIZE];
29
30#[derive(Debug, DeriveError)]
31pub enum ReadError {
32    #[error("I/O error: {0}")]
33    Io(#[from] std::io::Error),
34
35    #[error("corrupted file: {0}")]
36    CorruptedContent(String),
37
38    #[error("invalid File: not a snapshot")]
39    InvalidFile,
40
41    #[error("unsupported version: expected `{expected:?}`, found `{found:?}`")]
42    UnsupportedVersion { expected: [u8; 2], found: [u8; 2] },
43
44    #[error("unsupported associated data")]
45    UnsupportedAssociatedData,
46
47    #[error("crypto error: {0:?}")]
48    AgeFormatError(age::DecError),
49}
50
51impl From<age::DecError> for ReadError {
52    fn from(e: age::DecError) -> Self {
53        Self::AgeFormatError(e)
54    }
55}
56
57#[derive(Debug, DeriveError)]
58pub enum WriteError {
59    #[error("I/O error: {0}")]
60    Io(#[from] std::io::Error),
61
62    #[error("generating random bytes failed: {0}")]
63    GenerateRandom(String),
64
65    #[error("corrupted data: {0}")]
66    CorruptedData(String),
67
68    #[error("unsupported associated data")]
69    UnsupportedAssociatedData,
70
71    #[error("incorrect work factor")]
72    IncorrectWorkFactor,
73}
74
75// `ENCRYPT_WORK_FACTOR` exposes public access to the default work_factor used in snapshot encryption. Small values of
76// work_factor used together with a weak password will result in insecure snapshot protection and potential leakage of
77// all secrets, including seed and private keys. Too large values will result in significantly slow snapshot
78// encryption/decryption.
79//
80// The public access is exposed as a workaround so that encryption/decryption time can be controllably low during
81// testing. The work_factor must not be modified in production.
82static ENCRYPT_WORK_FACTOR: AtomicU8 = AtomicU8::new(age::RECOMMENDED_MINIMUM_ENCRYPT_WORK_FACTOR);
83
84pub fn get_encrypt_work_factor() -> u8 {
85    ENCRYPT_WORK_FACTOR.load(Ordering::Relaxed)
86}
87
88pub fn try_set_encrypt_work_factor(work_factor: u8) -> Result<(), WriteError> {
89    let _ = age::WorkFactor::try_from(work_factor).map_err(|_| WriteError::IncorrectWorkFactor)?;
90    ENCRYPT_WORK_FACTOR.store(work_factor, Ordering::Relaxed);
91    Ok(())
92}
93
94/// Encrypt snapshot content with key using work factor recommended for password-based (weak) keys.
95///
96/// # Security
97///
98/// Weak low-entropy (password-based) keys must be strengthened with key derivation.
99/// Secure key derivation in this case is resource-consuming, ie. it can use a lot of RAM
100/// and should take approx. 1 second. Strong keys generated with cryptographically secure RNG
101/// don't need strengthening.
102///
103/// This function expects key to be password-based (blake2b hash of the user provided password).
104/// It uses recommended work factor (approx. 20) to derive encryption key.
105/// It is safe to use with strong keys, although computing resources may be wasted.
106/// In this case it is recommended to use `encrypt_content_with_work_factor` with small/zero work factor.
107pub fn encrypt_content<O: Write>(plain: &[u8], output: &mut O, key: &Key) -> Result<(), WriteError> {
108    let work_factor = get_encrypt_work_factor();
109    encrypt_content_with_work_factor(plain, output, key, work_factor)
110}
111
112/// Encrypt snapshot content with key using custom work factor.
113///
114/// # Security warning
115///
116/// Work factor is used to strengthen weak low-entropy (password-based) keys.
117/// Recommended value for such keys is approx. 20, ie. key derivation should take approx. 1 second.
118/// Key derivation time grows exponentially with work factor.
119///
120/// Strong keys generated with cryptographically secure RNG do not need strengthening and
121/// can use minimal (0) work factor.
122///
123/// Using low work factor with weak low-entropy keys can lead to full compromise of encrypted data!
124pub fn encrypt_content_with_work_factor<O: Write>(
125    plain: &[u8],
126    output: &mut O,
127    key: &Key,
128    work_factor: u8,
129) -> Result<(), WriteError> {
130    let work_factor = work_factor.try_into().map_err(|_| WriteError::IncorrectWorkFactor)?;
131    let age = age::encrypt_vec(key, work_factor, plain)
132        .map_err(|e| WriteError::GenerateRandom(format!("failed to generate age randomness: {e:?}")))?;
133    output.write_all(&age[..])?;
134    Ok(())
135}
136
137/// Decrypt snapshot content with key using maximum work factor recommended for password-based (weak) keys.
138///
139/// Decryption may fail if the required amount of computation (work factor) exceeds the recommended value.
140/// In this case `decrypt_content_with_work_factor` with a larger work factor.
141pub fn decrypt_content<I: Read>(input: &mut I, key: &Key) -> Result<Zeroizing<Vec<u8>>, ReadError> {
142    let max_work_factor = age::RECOMMENDED_MAXIMUM_DECRYPT_WORK_FACTOR;
143    decrypt_content_with_work_factor(input, key, max_work_factor)
144}
145
146/// Decrypt snapshot content with key using custom maximum work factor.
147///
148/// Decryption may fail if the required amount of computation (work factor) exceeds the provided value.
149/// In this case a larger maximum work factor value can be used.
150///
151/// Strong keys are expected to use small/zero work factor.
152/// Small/zero maximum work factor can be used in such case.
153///
154/// # Security
155///
156/// Key derivation time grows exponentially with work factor.
157/// Maximum work factor should not be too large.
158/// Large values of maximum work factor when exploited by an attacker can cause Denial-of-Service.
159pub fn decrypt_content_with_work_factor<I: Read>(
160    input: &mut I,
161    key: &Key,
162    max_work_factor: u8,
163) -> Result<Zeroizing<Vec<u8>>, ReadError> {
164    let mut age = Vec::new();
165    input.read_to_end(&mut age)?;
166
167    age::decrypt_vec(key, max_work_factor, &age[..])
168        .map(Zeroizing::new)
169        .map_err(From::from)
170}
171
172/// Put magic and version bytes as file-header, [`encrypt_content`][self::encrypt_content] the specified
173/// plaintext to the specified path.
174///
175/// This is achieved by creating a temporary file in the same directory as the specified path (same
176/// filename with a salted suffix). This is currently known to be problematic if the path is a
177/// symlink and/or if the target path resides in a directory without user write permission.
178pub fn encrypt_file(plain: &[u8], path: &Path, key: &Key) -> Result<(), WriteError> {
179    // TODO: if path exists and is a symlink, resolve it and then append the salt
180    // TODO: if the sibling tempfile isn't writeable (e.g. directory permissions), write to
181    let compressed_plain = Zeroizing::new(compress(plain));
182
183    let mut salt = [0u8; 6];
184    rand::fill(&mut salt).map_err(|e| WriteError::GenerateRandom(format!("{}", e)))?;
185
186    let mut s = path.as_os_str().to_os_string();
187    s.push(".");
188    s.push(hex::encode(salt));
189    let tmp = Path::new(&s);
190
191    let mut f = OpenOptions::new().write(true).create_new(true).open(tmp)?;
192    // write magic and version bytes
193    f.write_all(&MAGIC)?;
194    f.write_all(&VERSION)?;
195    encrypt_content(&compressed_plain, &mut f, key)?;
196    f.sync_all()?;
197
198    rename(tmp, path)?;
199
200    Ok(())
201}
202
203/// Check the file header, [`decrypt_content`][self::decrypt_content], and decompress the ciphertext from the specified
204/// path.
205pub fn decrypt_file(path: &Path, key: &Key) -> Result<Zeroizing<Vec<u8>>, ReadError> {
206    let mut f: File = OpenOptions::new().read(true).open(path)?;
207    check_min_file_len(&mut f)?;
208    // check the header for structure.
209    check_header(&mut f)?;
210    let pt = Zeroizing::new(decrypt_content(&mut f, key)?);
211
212    decompress(&pt)
213        .map(Zeroizing::new)
214        .map_err(|e| ReadError::CorruptedContent(format!("decompression failed: {}", e)))
215}
216
217fn check_min_file_len(input: &mut File) -> Result<(), ReadError> {
218    const AGE_HEADER_LEN: usize = 150;
219    const AGE_TAG_LEN: usize = 16;
220    let min = MAGIC.len() + VERSION.len() + AGE_HEADER_LEN + AGE_TAG_LEN;
221    if input.metadata()?.len() >= min as u64 {
222        Ok(())
223    } else {
224        Err(ReadError::InvalidFile)
225    }
226}
227
228/// Checks the header for a specific structure; explicitly the magic and version bytes.
229fn check_header<I: Read>(input: &mut I) -> Result<(), ReadError> {
230    // check the magic bytes
231    let mut magic = [0u8; 5];
232    input.read_exact(&mut magic)?;
233    if magic != MAGIC {
234        return Err(ReadError::InvalidFile);
235    }
236
237    // check the version
238    let mut version = [0u8; 2];
239    input.read_exact(&mut version)?;
240
241    if version != VERSION {
242        return Err(ReadError::UnsupportedVersion {
243            expected: VERSION,
244            found: version,
245        });
246    }
247
248    Ok(())
249}
250
251#[cfg(test)]
252mod test {
253    use super::*;
254    use stronghold_utils::{
255        random,
256        test_utils::{corrupt, corrupt_file_at},
257    };
258
259    fn random_bytestring() -> Vec<u8> {
260        random::variable_bytestring(4096)
261    }
262
263    fn random_key() -> Key {
264        let mut key: Key = [0u8; KEY_SIZE];
265
266        rand::fill(&mut key).expect("Unable to fill buffer");
267
268        key
269    }
270
271    #[test]
272    fn test_write_read() {
273        let key: Key = random_key();
274        let bs0 = random_bytestring();
275
276        let mut buf = Vec::new();
277        encrypt_content(&bs0, &mut buf, &key).unwrap();
278        let read = decrypt_content(&mut buf.as_slice(), &key).unwrap();
279        assert_eq!(bs0, *read);
280    }
281
282    #[test]
283    #[should_panic]
284    fn test_corrupted_read_write() {
285        let key: Key = random_key();
286        let bs0 = random_bytestring();
287
288        let mut buf = Vec::new();
289        encrypt_content(&bs0, &mut buf, &key).unwrap();
290        corrupt(&mut buf);
291        decrypt_content(&mut buf.as_slice(), &key).unwrap();
292    }
293
294    #[test]
295    fn test_snapshot() {
296        let f = tempfile::tempdir().unwrap();
297        let mut pb = f.into_path();
298        pb.push("snapshot");
299
300        let key: Key = random_key();
301        let bs0 = random_bytestring();
302
303        encrypt_file(&bs0, &pb, &key).unwrap();
304        let bs1 = decrypt_file(&pb, &key).unwrap();
305        assert_eq!(bs0, *bs1);
306    }
307
308    #[test]
309    #[should_panic]
310    fn test_currupted_snapshot() {
311        let f = tempfile::tempdir().unwrap();
312        let mut pb = f.into_path();
313        pb.push("snapshot");
314
315        let key: Key = random_key();
316        let bs0 = random_bytestring();
317
318        encrypt_file(&bs0, &pb, &key).unwrap();
319        corrupt_file_at(&pb);
320        decrypt_file(&pb, &key).unwrap();
321    }
322
323    #[test]
324    fn test_snapshot_overwrite() {
325        let f = tempfile::tempdir().unwrap();
326        let mut pb = f.into_path();
327        pb.push("snapshot");
328
329        encrypt_file(&random_bytestring(), &pb, &random_key()).unwrap();
330
331        let key: Key = random_key();
332        let bs0 = random_bytestring();
333        encrypt_file(&bs0, &pb, &key).unwrap();
334        let bs1 = decrypt_file(&pb, &key).unwrap();
335        assert_eq!(bs0, *bs1);
336    }
337
338    struct TestVector {
339        key: &'static str,
340        data: &'static str,
341        snapshot: &'static str,
342    }
343
344    #[test]
345    fn test_vectors() {
346        let tvs = [
347            TestVector {
348                key: "f6eafe6482445269d3228b3647001c283102116362e870644ba3bfc7f8f109e6",
349                data: "a0dcd6b9a95ca5321cefb443c3d19915eb269072929841d986306982a459229a1866479a64f5ed9ac31ea083ae73859b8e5a3ccd9e3045881602f2ed036d473ef09e88c488f4c0a95e823fd984a1ffd69a9a9d3f7ab63e9bd673020181363b9134f46aa6734a9a9600b01b35740f5161dfad303a8a85ad5edcef31bd76a8d47ae1a46e60824c1023401ddaa5d385f414cc2c1773aca240629e4a80149bdf992d97622c1775399a2c65d5f81f5cfcb79c894971b87a17f655c0c4b88b90ee2ad8bfdcd47d7566b33de7957a5c06d7d5b3cddcf55d45bb78c4d5099753edb51974ab01f9371140b89b56382c7bf28e62e246c2828e0ef45a991cd7b1e5a93ce5587b0e50792c2b44744121e84be0e3d6e01b2488d342622e1602d9a07eca27ffd83fb30e2c0b9700cf45080e415554b75ccfa08913acb9e8",
350                snapshot: "504152544903006167652d656e6372797074696f6e2e6f72672f76310a2d3e20736372797074204d3970496c4d3164662b6353563366716831714465772031310a4e396f794d75686b396e2b75435663314355545555384b656c694f6c733239644d447433376d315a32546b0a2d2d2d204c4f34523579425864416f485865306369365358544d36487265487038527755656842544b444b2b75784d0a2d3ee7908d47abb031c93bcf816a92f09576f4a22301162f4f970c37b20cd242d416632dcdab4b98c50e7f8609945ba035cd2de4feb34bbb681c8541403a5e487602a281f357c8a6682206b277460ca0bf12a1143f5fd5dbbfba5045760bd2b0286a7cca3d5a980708e720424e6efe9b7c183eaadd26cc2f480cf5af1cf5e8175f0a41b8c12ebf7c7b23f6115d9d22ff9288f96099d62ffcd4c8483b30e805011ef172ac0b8aa53c65fa5cbd6ee675bd3c001d92039ab995efd52ad99fdb0156767771c20e73fd90990d6563d78ba923eed588cbd13bfa4288810cdca39541dbf48501b549c9d28cc387ac4c78ac78f753daf8d0418a70cf24d7de61cb9cc861b2be36102e3d3b6079e564b6aa895b1a5880c3d5bc86f10ec0463f076b1351bb5b280c578e5321666fcfe9c0e843a688f60d10b91c48995c4e003067ee8e07acdfa6f39242252d1c5c1442b57deec9c34ab56037d6021b",
351            },
352            TestVector {
353                key: "683bd3d6957bf7276d0a616304f1610c57689a96d90118762f4caa9de9bc5bb6",
354                data: "",
355                snapshot: "504152544903006167652d656e6372797074696f6e2e6f72672f76310a2d3e20736372797074202f754a34476a537776342b562f654546414e43586c512031310a705336736f573047302f63577a62434765534b6f35374b53614d52523842574c37435846664437383956340a2d2d2d206d73664263594c42686a526c785764727235624344367a5749774c586c6c4b5748706e4f65465732314c6b0a7903bc73e10ddb40152023255131650ec8c19d09c1c7c896db83ea0cc6a90d47",
356            },
357            TestVector {
358                key: "cd250a0b070632dc521cfe35805b2846763a4c698d61d85d3b55f115b9a769da",
359                data: "",
360                snapshot: "504152544903006167652d656e6372797074696f6e2e6f72672f76310a2d3e20736372797074204b5042447751786b4e346272584b41343844354a4f412031310a71637a6665775948683378747a5873626b5568662b556b6945796c646b74764b55464f6f525249516265510a2d2d2d20456a6e3850335447474b454a6150576c30432b78382f6a2b5037417755783261553675624a657a6e4767730a823a6568803ea775a18191ef7a9782569fe663f85caf15ecfb651ae485c65665",
361            },
362            TestVector {
363                key: "9cf33b2539a3e9d89d2586ae6783d781de68df155eb2af22abaca3d6094d6db8",
364                data: "",
365                snapshot: "504152544903006167652d656e6372797074696f6e2e6f72672f76310a2d3e20736372797074202b694b446c567a72434f764c77684e7a4535484345772031310a384768764d513331706c784270414b35364e3946646a593335374c71554730744b79516e6a45594d5951770a2d2d2d206d5a456b3139657456464d4e316c636a67765159746762515466752b3131337350684b4545767a457a396f0a081b254bda255364f9b91c8e89b921461db355a3d55a4222aefe66ced11ff6c5",
366            },
367        ];
368
369        for tv in &tvs {
370            let mut key = [0; KEY_SIZE];
371            hex::decode_to_slice(tv.key, &mut key).unwrap();
372            let data = hex::decode(tv.data).unwrap();
373            let snapshot = hex::decode(tv.snapshot).unwrap();
374
375            let mut slice = snapshot.as_slice();
376
377            // check the header for structure.
378            check_header(&mut slice).unwrap();
379            let pt = decrypt_content(&mut slice, &key).unwrap();
380
381            assert_eq!(*pt, data);
382        }
383    }
384}