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 index = lit.join("index");
104 let index_encrypted = if index.exists() {
105 encrypt_file(&index, &encryption)?
106 } else {
107 false
108 };
109 if index.exists() && !index_encrypted {
110 already += 1;
111 }
112
113 let packs_dir = pack::packs_dir(&repo);
120 let packed = pack::load_all(&packs_dir);
121 let mut objects_unpacked = 0usize;
122 let mut packs_expanded = 0usize;
123
124 if !packed.is_empty() {
125 let plaintext = EncryptionManager::new(EncryptionConfig {
126 enabled: false,
127 ..config.clone()
128 });
129 let store = ObjectStore::new(&repo);
130
131 for (hash, (pack_path, offset)) in &packed {
132 let object: Object = pack::read_pack_object(pack_path, *offset, &plaintext)
134 .or_else(|_| pack::read_pack_object(pack_path, *offset, &encryption))
135 .map_err(|e| format!("Failed to read {} from its pack: {}", &hash[..8], e))?;
136
137 store.write(&object)?;
138 objects_unpacked += 1;
139 }
140
141 for entry in files_under(&packs_dir) {
142 fs::remove_file(&entry)
143 .map_err(|e| format!("Failed to remove {}: {}", entry.display(), e))?;
144 packs_expanded += 1;
145 }
146 }
147
148 Ok(MigrateEncryptionResponse {
149 objects_encrypted,
150 objects_unpacked,
151 index_encrypted,
152 packs_expanded,
153 already_encrypted: already,
154 message: format!(
155 "Encrypted {} loose objects and {} unpacked from {} pack files; index {}; \
156 {} already encrypted",
157 objects_encrypted,
158 objects_unpacked,
159 packs_expanded,
160 if index_encrypted {
161 "encrypted"
162 } else {
163 "left as it was"
164 },
165 already
166 ),
167 })
168}