git_simple_encrypt/crypt/
file.rs1use 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
21pub(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
38pub 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 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
86pub 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 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
126pub 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
137pub fn decrypt_file(path: &Path, master_key: &[u8]) -> Result<()> {
139 decrypt_file_to(path, path, master_key).map(|_| ())
140}
141
142pub 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}