lit/commands/
migrate_encryption.rs1use crate::core::{find_repo_root, Object};
13use crate::crypto::encryption::{EncryptionConfig, EncryptionManager};
14use crate::response::MigrateEncryptionResponse;
15use crate::storage::{pack, ObjectStore};
16use std::fs;
17use std::path::{Path, PathBuf};
18
19const ENCRYPTION_VERSION: u8 = 1;
21
22fn already_encrypted(data: &[u8], encryption: &EncryptionManager) -> bool {
27 data.first() == Some(&ENCRYPTION_VERSION) && encryption.decrypt(data).is_ok()
28}
29
30fn encrypt_file(
36 path: &Path,
37 encryption: &EncryptionManager,
38) -> Result<bool, crate::errors::LitError> {
39 let data = fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
40
41 if already_encrypted(&data, encryption) {
42 return Ok(false);
43 }
44
45 let encrypted = encryption.encrypt(&data)?;
46 let temp = path.with_extension("migrating");
47 fs::write(&temp, &encrypted)
48 .map_err(|e| format!("Failed to write {}: {}", temp.display(), e))?;
49 fs::rename(&temp, path).map_err(|e| format!("Failed to replace {}: {}", path.display(), e))?;
50
51 Ok(true)
52}
53
54fn files_under(dir: &Path) -> Vec<PathBuf> {
56 if !dir.exists() {
57 return Vec::new();
58 }
59 walkdir::WalkDir::new(dir)
60 .into_iter()
61 .filter_map(|e| e.ok())
62 .filter(|e| e.file_type().is_file())
63 .map(|e| e.path().to_path_buf())
64 .collect()
65}
66
67pub fn execute() -> Result<MigrateEncryptionResponse, crate::errors::LitError> {
69 let repo = find_repo_root()?;
70 let config = EncryptionConfig::load(&repo)?;
71
72 if !config.enabled {
73 return Err(
74 "Encryption is not enabled for this repository. Set enabled = true in \
75 .lit/encryption.toml first."
76 .into(),
77 );
78 }
79
80 let encryption = EncryptionManager::new_auto(config.clone(), &repo);
82 encryption.encrypt(b"probe")?;
83
84 let lit = repo.join(".lit");
85 let mut objects_encrypted = 0usize;
86 let mut already = 0usize;
87
88 for path in files_under(&lit.join("objects")) {
96 if encrypt_file(&path, &encryption)? {
97 objects_encrypted += 1;
98 } else {
99 already += 1;
100 }
101 }
102
103 let mut refs_encrypted = 0usize;
107 for prefix in ["heads", "tags", "remotes"] {
108 for reference in crate::core::list_refs(&repo, prefix).unwrap_or_default() {
109 crate::core::write_ref(
110 &repo,
111 &format!("{}/{}", prefix, reference.name),
112 &reference.hash,
113 )?;
114 refs_encrypted += 1;
115 }
116 }
117
118 for path in files_under(&lit.join("refs")) {
120 fs::remove_file(&path)
121 .map_err(|e| format!("Failed to remove {}: {}", path.display(), e))?;
122 }
123
124 let head = lit.join("HEAD");
125 if head.exists() {
126 if encrypt_file(&head, &encryption)? {
127 refs_encrypted += 1;
128 } else {
129 already += 1;
130 }
131 }
132
133 let index = lit.join("index");
134 let index_encrypted = if index.exists() {
135 encrypt_file(&index, &encryption)?
136 } else {
137 false
138 };
139 if index.exists() && !index_encrypted {
140 already += 1;
141 }
142
143 let packs_dir = pack::packs_dir(&repo);
150 let packed = pack::load_all(&packs_dir);
151 let mut objects_unpacked = 0usize;
152 let mut packs_expanded = 0usize;
153
154 if !packed.is_empty() {
155 let plaintext = EncryptionManager::new(EncryptionConfig {
156 enabled: false,
157 ..config.clone()
158 });
159 let store = ObjectStore::new(&repo);
160
161 for (hash, (pack_path, offset)) in &packed {
162 let object: Object = pack::read_pack_object(pack_path, *offset, &plaintext)
164 .or_else(|_| pack::read_pack_object(pack_path, *offset, &encryption))
165 .map_err(|e| format!("Failed to read {} from its pack: {}", &hash[..8], e))?;
166
167 store.write(&object)?;
168 objects_unpacked += 1;
169 }
170
171 for entry in files_under(&packs_dir) {
172 fs::remove_file(&entry)
173 .map_err(|e| format!("Failed to remove {}: {}", entry.display(), e))?;
174 packs_expanded += 1;
175 }
176 }
177
178 Ok(MigrateEncryptionResponse {
179 objects_encrypted,
180 objects_unpacked,
181 refs_encrypted,
182 index_encrypted,
183 packs_expanded,
184 already_encrypted: already,
185 message: format!(
186 "Encrypted {} loose objects, {} unpacked from {} pack files and {} refs; \
187 index {}; {} already encrypted",
188 objects_encrypted,
189 objects_unpacked,
190 packs_expanded,
191 refs_encrypted,
192 if index_encrypted {
193 "encrypted"
194 } else {
195 "left as it was"
196 },
197 already
198 ),
199 })
200}