1
2use chacha20poly1305::aead::OsRng;
3use clap::builder::OsStr;
4use horcrux::{HorcruxHeader, Horcrux};
5use rand::RngCore;
6use sharks::{Share, Sharks};
7use std::{
8 fs::{self, File, OpenOptions},
9 io::{self, LineWriter, Seek, SeekFrom, Write},
10 path::{Path, PathBuf},
11 time::SystemTime,
12};
13use anyhow::{anyhow, Error};
14
15pub mod horcrux;
16pub mod crypto;
17
18pub fn split(
19 source: &PathBuf,
20 destination: &PathBuf,
21 total: u8,
22 threshold: u8,
23) -> Result<(), anyhow::Error> {
24 let mut key = [0u8; 32];
25 let mut nonce = [0u8; 19];
26 OsRng.fill_bytes(&mut key);
27 OsRng.fill_bytes(&mut nonce);
28
29 let crypto_shark = Sharks(threshold);
30
31 let key_dealer = crypto_shark.dealer(key.as_slice());
33 let key_fragments: Vec<Share> = key_dealer.take(total as usize).collect();
34
35 let nonce_dealer = crypto_shark.dealer(nonce.as_slice());
36 let nonce_fragments: Vec<Share> = nonce_dealer.take(total as usize).collect();
37
38 let timestamp = SystemTime::now();
39
40 if !destination.exists() {
41 let err = format!(
42 "Cannot place horcruxes in directory `{}`. Try creating them in a different directory.",
43 destination.to_string_lossy()
44 );
45 fs::create_dir_all(destination).expect(&err);
46 }
47 let default_file_name = OsStr::from("secret.txt");
48 let default_file_stem = OsStr::from("secret");
49
50 let canonical_file_name = &source
51 .file_name()
52 .unwrap_or(&default_file_name)
53 .to_string_lossy();
54 let file_stem = &source
55 .file_stem()
56 .unwrap_or(&default_file_stem)
57 .to_string_lossy();
58 let mut horcrux_files: Vec<File> = Vec::with_capacity(total as usize);
59
60 for i in 0..total {
61 let index = i + 1;
62 let key_fragment = Vec::from(&key_fragments[i as usize]);
63 let nonce_fragment = Vec::from(&nonce_fragments[i as usize]);
64 let header = HorcruxHeader {
65 canonical_file_name: canonical_file_name.to_string(),
66 timestamp,
67 index,
68 total,
69 threshold,
70 nonce_fragment,
71 key_fragment,
72 };
73
74 let json_header = serde_json::to_string(&header)?;
75 let horcrux_filename = format!("{}_{}_of_{}.horcrux", file_stem, index, total);
76
77 let horcrux_path = Path::new(&destination).join(&horcrux_filename);
78
79 let horcrux_file: File = OpenOptions::new()
80 .read(true)
81 .create(true)
82 .write(true)
83 .truncate(true)
84 .open(&horcrux_path)?;
85
86 let contents = Horcrux::formatted_header(index, total, json_header);
87 let mut line_writer = LineWriter::new(&horcrux_file);
88
89 line_writer.write_all(contents.as_bytes())?;
90 drop(line_writer);
91 horcrux_files.push(horcrux_file);
92 }
93
94 let mut contents_to_encrypt = File::open(source)?;
102 let mut initial_horcrux: &File = &horcrux_files[0];
103
104 let read_pointer: u64 = initial_horcrux.seek(SeekFrom::End(0))?;
105
106 let mut horcrux_handle = initial_horcrux.try_clone()?;
107
108 crypto::encrypt_file(&mut contents_to_encrypt, &mut horcrux_handle, &key, &nonce)?;
109
110 for horcrux in horcrux_files.iter().skip(1) {
111 initial_horcrux.seek(SeekFrom::Start(read_pointer))?;
112 io::copy(&mut initial_horcrux, &mut horcrux.to_owned())?;
113 }
114 Ok(())
115}
116
117
118fn find_horcrux_file_paths(directory: &PathBuf) -> Result<Vec<PathBuf>, std::io::Error> {
120 let paths = fs::read_dir(directory)?;
121
122 let horcruxes: Vec<PathBuf> = paths
123 .flat_map(|entry| {
124 let entry = entry.expect("Failed to read directory entry.");
125 let path = entry.path();
126
127 if path.is_file() {
128 if let Some(extension) = path.extension() {
129 if extension == "horcrux" || extension == "hx" {
130 return Some(path);
131 }
132 }
133 }
134
135 None
136 })
137 .collect();
138 Ok(horcruxes)
139}
140
141pub fn bind(source: &PathBuf, destination: &PathBuf) -> Result<(), anyhow::Error> {
143 let horcrux_paths = find_horcrux_file_paths(source)?;
144
145 if horcrux_paths.is_empty() {
146 let err = format!(
147 "No horcrux files found in directory {}",
148 source.to_string_lossy()
149 );
150 return Err(anyhow!(err));
151 }
152
153 let horcruxes: Vec<Horcrux> = horcrux_paths.into_iter().try_fold(
154 Vec::new(),
155 |mut acc: Vec<Horcrux>, entry: PathBuf| -> Result<Vec<Horcrux>, Error> {
156 let hx = Horcrux::from_path(&entry)?;
157 acc.push(hx);
158 Ok(acc)
159 },
160 )?;
161
162 let initial_horcrux = &horcruxes[0];
163 let initial_header: &HorcruxHeader = &initial_horcrux.header;
164 let threshold: u8 = initial_header.threshold;
165
166 let mut key_shares: Vec<Share> = Vec::with_capacity(initial_header.total as usize);
167 let mut nonce_shares: Vec<Share> = Vec::with_capacity(initial_header.total as usize);
168 let mut matching_horcruxes: Vec<&Horcrux> = Vec::with_capacity(initial_header.total as usize);
169
170 if !destination.exists() {
171 fs::create_dir_all(destination)?;
172 }
173
174 for horcrux in &horcruxes {
175 if horcrux.header.canonical_file_name == initial_header.canonical_file_name
176 && horcrux.header.timestamp == initial_header.timestamp
177 {
178 let kshare: Share = Share::try_from(horcrux.header.key_fragment.as_slice())
179 .map_err(|op| anyhow!(op))?;
180 let nshare: Share = Share::try_from(horcrux.header.nonce_fragment.as_slice())
181 .map_err(|op| anyhow!(op))?;
182 key_shares.push(kshare);
183 nonce_shares.push(nshare);
184 matching_horcruxes.push(horcrux);
185 }
186 }
187
188 if !(matching_horcruxes.is_empty() || matching_horcruxes.len() >= threshold.to_owned() as usize)
189 {
190 return Err(anyhow!(
191 format!("Cannot find enough horcruxes to recover `{}` found {} matching horcruxes and {} matches are required to recover the file.",initial_header.canonical_file_name, matching_horcruxes.len(), threshold)
192 ));
193 }
194 let crypto_shark = Sharks(threshold);
196
197 let key_result = crypto_shark
198 .recover(&key_shares)
199 .map_err(|_e| anyhow!("Not enough key fragments."))?;
200
201 let nonce_result = crypto_shark
202 .recover(&nonce_shares)
203 .map_err(|_e| anyhow!("Not enough nonce fragments."))?;
204
205 let key: [u8; 32] = key_result
206 .try_into()
207 .map_err(|_e| anyhow!("Cannot recover key fragment."))?;
208 let nonce: [u8; 19] = nonce_result
209 .try_into()
210 .map_err(|_e| anyhow!("Cannot recover nonce fragment."))?;
211
212 let mut recovered_file: File = OpenOptions::new()
213 .create(true)
214 .write(true)
215 .truncate(true)
216 .open(destination.join(&initial_horcrux.header.canonical_file_name))?;
217
218 let mut contents = initial_horcrux.contents.try_clone().unwrap();
219
220 crypto::decrypt_file(&mut contents, &mut recovered_file, &key, &nonce)?;
221 Ok(())
222}