horcrux_rs/
lib.rs

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    //Break up key, nonce into same number of n fragments
32    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    /* Strategy:
95    In this state all the horcrux files contain their headers and an empty body.
96    In order to avoid calling `encrypt_file` on each file, instead, we
97    calculate the byte length after the header of the first file and store it as a variable. 
98    Next we encrypt the first file, and then use seek to skip over the index file headers and copy only the necessary contents to the rest.
99    This is possible because the body content is the same for each file.
100    */
101    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
118//Strategy is to find all files ending in .horcrux or .hx and then parse them. Next we filter them by matching timestamp and file name.
119fn 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
141//Find all horcruxes in a directory that matches the first one found and attempt recovery.
142pub 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    //Recover the secret
195    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}