Skip to main content

git_simple_encrypt/crypt/
file.rs

1use std::{
2    fs,
3    io::{Read, Seek, SeekFrom},
4    path::Path,
5};
6
7use chacha20poly1305_simd::XChaCha20Poly1305;
8use log::{debug, warn};
9use tempfile::NamedTempFile;
10
11use crate::{
12    crypt::{
13        header::{FILE_ID_LEN, FileHeader, HEADER_LEN, MAGIC, SALT_LEN, is_encrypted_version},
14        key::{KeyCache, get_or_derive_key, split_keys},
15        stream::{decrypt_body, encrypt_into},
16    },
17    error::{Error, Result},
18    salt_cache::{CacheRef, CachedEntry},
19};
20
21/// Persist a `NamedTempFile` to `dst` atomically, optionally copying metadata.
22pub(super) fn persist_temp_file(
23    temp_file: NamedTempFile,
24    dst: &Path,
25    metadata_source: Option<&Path>,
26) -> Result<()> {
27    if let Some(src) = metadata_source
28        && let Err(e) = copy_metadata::copy_metadata(src, temp_file.path())
29    {
30        warn!("Could not copy metadata from {}: {}", src.display(), e);
31    }
32    temp_file
33        .persist(dst)
34        .map_err(|e| Error::AtomicPersist(dst.to_path_buf(), e.to_string()))?;
35    Ok(())
36}
37
38/// Encrypt `src` into `dst`.
39pub fn encrypt_file_to(
40    src: &Path,
41    dst: &Path,
42    derived_key: &[u8; 32],
43    salt: [u8; SALT_LEN],
44    file_id: Option<[u8; FILE_ID_LEN]>,
45    zstd: Option<u8>,
46) -> Result<Option<FileHeader>> {
47    let mut src_file = fs::File::open(src)?;
48
49    let mut header_bytes = [0u8; HEADER_LEN];
50    if src_file.read_exact(&mut header_bytes).is_ok()
51        && &header_bytes[0..5] == MAGIC
52        && is_encrypted_version(header_bytes[5])
53    {
54        warn!("Source file already encrypted, skipping: {}", src.display());
55        return Ok(None);
56    }
57    src_file.seek(SeekFrom::Start(0))?;
58
59    debug!("Encrypting {} → {}", src.display(), dst.display());
60
61    let dst_parent = dst.parent().unwrap_or_else(|| Path::new("."));
62    // Skip the stat+mkdir round-trip when the destination already lives in a
63    // directory the source is in (the in-place path, or same-dir writes) —
64    // that directory necessarily exists.
65    let src_parent = src.parent().unwrap_or_else(|| Path::new("."));
66    if src_parent != dst_parent {
67        fs::create_dir_all(dst_parent)?;
68    }
69    let mut temp_file = NamedTempFile::new_in(dst_parent)?;
70
71    let header = encrypt_into(
72        &mut src_file,
73        &mut temp_file,
74        derived_key,
75        salt,
76        file_id,
77        zstd,
78    )?;
79
80    drop(src_file);
81    persist_temp_file(temp_file, dst, Some(src))?;
82
83    Ok(Some(header))
84}
85
86/// Decrypt `src` into `dst`.
87pub fn decrypt_file_to(src: &Path, dst: &Path, master_key: &[u8]) -> Result<Option<FileHeader>> {
88    let mut src_file = fs::File::open(src)?;
89
90    let mut header_bytes = [0u8; HEADER_LEN];
91    if src_file.read_exact(&mut header_bytes).is_err() {
92        debug!(
93            "File too small to be encrypted, skipping: {}",
94            src.display()
95        );
96        return Ok(None);
97    }
98    if &header_bytes[0..5] != MAGIC || !is_encrypted_version(header_bytes[5]) {
99        debug!("File not encrypted (no magic), skipping: {}", src.display());
100        return Ok(None);
101    }
102
103    debug!("Decrypting {} → {}", src.display(), dst.display());
104
105    let header = *FileHeader::from_bytes(&header_bytes)?;
106    let derived_key = super::key::derive_key(master_key, &header.salt)?;
107
108    let dst_parent = dst.parent().unwrap_or_else(|| Path::new("."));
109    // See encrypt_file_to: skip mkdir when src and dst share a parent.
110    let src_parent = src.parent().unwrap_or_else(|| Path::new("."));
111    if src_parent != dst_parent {
112        fs::create_dir_all(dst_parent)?;
113    }
114    let mut temp_file = NamedTempFile::new_in(dst_parent)?;
115
116    let (key_enc, _) = split_keys(&derived_key);
117    let cipher = XChaCha20Poly1305::new(*key_enc);
118    decrypt_body(&mut src_file, &mut temp_file, &cipher, &header)?;
119
120    drop(src_file);
121    persist_temp_file(temp_file, dst, Some(src))?;
122
123    Ok(Some(header))
124}
125
126/// Encrypt a single file **in place**.
127pub fn encrypt_file(
128    path: &Path,
129    derived_key: &[u8; 32],
130    salt: &[u8; SALT_LEN],
131    file_id: Option<[u8; FILE_ID_LEN]>,
132    zstd: Option<u8>,
133) -> Result<Option<FileHeader>> {
134    encrypt_file_to(path, path, derived_key, *salt, file_id, zstd)
135}
136
137/// Decrypt a single file **in place**.
138pub fn decrypt_file(path: &Path, master_key: &[u8]) -> Result<()> {
139    decrypt_file_to(path, path, master_key).map(|_| ())
140}
141
142/// Decrypt a single file with a thread-safe Argon2 key cache and optional
143/// salt/`file_id` cache.
144pub fn decrypt_file_with_cache(
145    path: &Path,
146    key_cache: &KeyCache,
147    cache: Option<CacheRef<'_>>,
148    master_key: &[u8],
149) -> Result<()> {
150    let mut file = fs::File::open(path)?;
151
152    let mut header_bytes = [0u8; HEADER_LEN];
153    if file.read_exact(&mut header_bytes).is_err() {
154        debug!(
155            "File too small to be encrypted, skipping: {}",
156            path.display()
157        );
158        return Ok(());
159    }
160    if &header_bytes[0..5] != MAGIC || !is_encrypted_version(header_bytes[5]) {
161        debug!(
162            "File not encrypted (no magic), skipping: {}",
163            path.display()
164        );
165        return Ok(());
166    }
167
168    debug!("Decrypting: {}", path.display());
169    let header = *FileHeader::from_bytes(&header_bytes)?;
170
171    if let Some(cache) = cache {
172        cache.sender.insert(cache.key, CachedEntry {
173            salt: header.salt,
174            file_id: header.file_id,
175        });
176    }
177
178    let derived_key = get_or_derive_key(key_cache, master_key, &header.salt)?;
179
180    let (key_enc, _key_mac) = split_keys(&derived_key);
181    let cipher = XChaCha20Poly1305::new(*key_enc);
182    let parent_dir = path.parent().unwrap_or_else(|| Path::new("."));
183    let mut temp_file = NamedTempFile::new_in(parent_dir)?;
184
185    decrypt_body(&mut file, &mut temp_file, &cipher, &header)?;
186    drop(file);
187
188    persist_temp_file(temp_file, path, Some(path))?;
189
190    Ok(())
191}