├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── bin ├── combine.rs └── split.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | - 1.44.0 # minimum supported version 7 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.19" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "atty" 25 | version = "0.2.14" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 28 | dependencies = [ 29 | "hermit-abi", 30 | "libc", 31 | "winapi", 32 | ] 33 | 34 | [[package]] 35 | name = "bitflags" 36 | version = "1.2.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 39 | 40 | [[package]] 41 | name = "cc" 42 | version = "1.0.67" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" 45 | 46 | [[package]] 47 | name = "cfg-if" 48 | version = "0.1.10" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 51 | 52 | [[package]] 53 | name = "cfg-if" 54 | version = "1.0.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 57 | 58 | [[package]] 59 | name = "clap" 60 | version = "2.33.3" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 63 | dependencies = [ 64 | "ansi_term", 65 | "atty", 66 | "bitflags", 67 | "strsim", 68 | "textwrap", 69 | "unicode-width", 70 | ] 71 | 72 | [[package]] 73 | name = "duct" 74 | version = "0.12.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "3640af123c78bedc20c1d3928e43cc0621e57011899d1ef917900c12fdb7a1ee" 77 | dependencies = [ 78 | "lazycell", 79 | "libc", 80 | "os_pipe", 81 | "shared_child", 82 | ] 83 | 84 | [[package]] 85 | name = "env_logger" 86 | version = "0.6.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" 89 | dependencies = [ 90 | "atty", 91 | "humantime", 92 | "log", 93 | "regex", 94 | "termcolor", 95 | ] 96 | 97 | [[package]] 98 | name = "getrandom" 99 | version = "0.1.16" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" 102 | dependencies = [ 103 | "cfg-if 1.0.0", 104 | "libc", 105 | "wasi", 106 | ] 107 | 108 | [[package]] 109 | name = "hermit-abi" 110 | version = "0.1.18" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" 113 | dependencies = [ 114 | "libc", 115 | ] 116 | 117 | [[package]] 118 | name = "humantime" 119 | version = "1.3.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 122 | dependencies = [ 123 | "quick-error", 124 | ] 125 | 126 | [[package]] 127 | name = "lazycell" 128 | version = "1.3.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 131 | 132 | [[package]] 133 | name = "libc" 134 | version = "0.2.93" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" 137 | 138 | [[package]] 139 | name = "log" 140 | version = "0.4.14" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 143 | dependencies = [ 144 | "cfg-if 1.0.0", 145 | ] 146 | 147 | [[package]] 148 | name = "memchr" 149 | version = "2.5.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 152 | 153 | [[package]] 154 | name = "nix" 155 | version = "0.15.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" 158 | dependencies = [ 159 | "bitflags", 160 | "cc", 161 | "cfg-if 0.1.10", 162 | "libc", 163 | "void", 164 | ] 165 | 166 | [[package]] 167 | name = "os_pipe" 168 | version = "0.8.2" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "81e8dfa4c69d7bde595e9a940fcf1d7f60966d3fce8a8c4cad67c60e35ea2a11" 171 | dependencies = [ 172 | "nix", 173 | "winapi", 174 | ] 175 | 176 | [[package]] 177 | name = "ppv-lite86" 178 | version = "0.2.10" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" 181 | 182 | [[package]] 183 | name = "quick-error" 184 | version = "1.2.3" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 187 | 188 | [[package]] 189 | name = "rand" 190 | version = "0.7.3" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 193 | dependencies = [ 194 | "getrandom", 195 | "libc", 196 | "rand_chacha", 197 | "rand_core", 198 | "rand_hc", 199 | ] 200 | 201 | [[package]] 202 | name = "rand_chacha" 203 | version = "0.2.2" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 206 | dependencies = [ 207 | "ppv-lite86", 208 | "rand_core", 209 | ] 210 | 211 | [[package]] 212 | name = "rand_core" 213 | version = "0.5.1" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 216 | dependencies = [ 217 | "getrandom", 218 | ] 219 | 220 | [[package]] 221 | name = "rand_hc" 222 | version = "0.2.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 225 | dependencies = [ 226 | "rand_core", 227 | ] 228 | 229 | [[package]] 230 | name = "regex" 231 | version = "1.6.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 234 | dependencies = [ 235 | "aho-corasick", 236 | "memchr", 237 | "regex-syntax", 238 | ] 239 | 240 | [[package]] 241 | name = "regex-syntax" 242 | version = "0.6.27" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 245 | 246 | [[package]] 247 | name = "shamirsecretsharing" 248 | version = "0.1.4" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "1901d3f828676e399a303273519b8dba039041ce38227dd962d8840851cb888f" 251 | dependencies = [ 252 | "cc", 253 | "libc", 254 | ] 255 | 256 | [[package]] 257 | name = "shamirsecretsharing-cli" 258 | version = "0.1.1" 259 | dependencies = [ 260 | "atty", 261 | "clap", 262 | "duct", 263 | "env_logger", 264 | "libc", 265 | "log", 266 | "rand", 267 | "shamirsecretsharing", 268 | ] 269 | 270 | [[package]] 271 | name = "shared_child" 272 | version = "0.3.5" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a" 275 | dependencies = [ 276 | "libc", 277 | "winapi", 278 | ] 279 | 280 | [[package]] 281 | name = "strsim" 282 | version = "0.8.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 285 | 286 | [[package]] 287 | name = "termcolor" 288 | version = "1.1.2" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 291 | dependencies = [ 292 | "winapi-util", 293 | ] 294 | 295 | [[package]] 296 | name = "textwrap" 297 | version = "0.11.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 300 | dependencies = [ 301 | "unicode-width", 302 | ] 303 | 304 | [[package]] 305 | name = "unicode-width" 306 | version = "0.1.8" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 309 | 310 | [[package]] 311 | name = "void" 312 | version = "1.0.2" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 315 | 316 | [[package]] 317 | name = "wasi" 318 | version = "0.9.0+wasi-snapshot-preview1" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 321 | 322 | [[package]] 323 | name = "winapi" 324 | version = "0.3.9" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 327 | dependencies = [ 328 | "winapi-i686-pc-windows-gnu", 329 | "winapi-x86_64-pc-windows-gnu", 330 | ] 331 | 332 | [[package]] 333 | name = "winapi-i686-pc-windows-gnu" 334 | version = "0.4.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 337 | 338 | [[package]] 339 | name = "winapi-util" 340 | version = "0.1.5" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 343 | dependencies = [ 344 | "winapi", 345 | ] 346 | 347 | [[package]] 348 | name = "winapi-x86_64-pc-windows-gnu" 349 | version = "0.4.0" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 352 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shamirsecretsharing-cli" 3 | version = "0.1.1" 4 | authors = ["Daan Sprenkels "] 5 | 6 | [[bin]] 7 | name = "secret-share-split" 8 | path = "src/bin/split.rs" 9 | 10 | [[bin]] 11 | name = "secret-share-combine" 12 | path = "src/bin/combine.rs" 13 | 14 | [dependencies] 15 | atty = "0.2" 16 | env_logger = "0.6" 17 | libc = "0.2" 18 | log = "0.4" 19 | rand = "0.7" 20 | shamirsecretsharing = "=0.1.4" 21 | 22 | [dependencies.clap] 23 | version = "2.25" 24 | default-features = false 25 | features = [ "suggestions", "color" ] 26 | 27 | [dev-dependencies] 28 | duct = "0.12" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Daan Sprenkels 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `secret-share-{split,combine}` 2 | 3 | [![Build Status](https://travis-ci.org/dsprenkels/sss-cli.svg?branch=master)](https://travis-ci.org/dsprenkels/sss-cli) 4 | 5 | This tool allows for securely splitting and recombining secrets using a secure 6 | implementation of the Shamir secret sharing scheme. It is a wrapper around my 7 | [SSS library](https://github.com/dsprenkels/sss). 8 | 9 | # Usage 10 | 11 | You need [Rust] to build `sss-cli` from source. When you have installed Rust, 12 | you can install these tools using [Cargo][crates.io]: 13 | 14 | ```shell 15 | # Install sss-cli 16 | cargo install --git https://github.com/dsprenkels/sss-cli --branch v0.1 17 | 18 | # Make 4 shares with recombination threshold 3 19 | echo "Tyler Durden isn't real." | secret-share-split -n 4 -t 3 >shares.txt 20 | 21 | # Take the first 3 shares and combine them 22 | head -n 3 shares.txt | secret-share-combine 23 | ``` 24 | 25 | Note that after this installation, you need to add Cargo's installation 26 | directory to your `$PATH` if you don't have it there yet. 27 | 28 | To uninstall the crate you can use a command similar to the install-command 29 | above. 30 | 31 | ```shell 32 | # Uninstall the secret sharing tools 33 | cargo uninstall shamirsecretsharing-cli 34 | ``` 35 | 36 | ## macOS 37 | 38 | To install on macOS system, you can also use [Homebrew](https://brew.sh) package 39 | manager. The package is not yet in the upstream *homebrew-core*, but there exists a tap 40 | with **sss-cli** formula [here](https://github.com/vitkabele/homebrew-tap). 41 | 42 | To install using Homebrew, run: `brew install vitkabele/tap/sss-cli`. 43 | 44 | # F.A.Q. 45 | 46 | ## Why are the shares so much longer than the secrets? 47 | 48 | This Shamir secret sharing library *could* produce shares that are shorter than 49 | their current length. However, while Shamir secret sharing is secure for 50 | confidentiality, this is not the case for integrity. An attacker could tamper 51 | with some of the shares. After restoring the (malicious) secret, you would not 52 | be able to know that it has been tampered with. `sss-cli` uses an AEAD wrapper 53 | so that the shares cannot be tampered with, which takes up some extra space. 54 | 55 | [Rust]: https://www.rust-lang.org/ 56 | [rustup]: https://rustup.rs/ 57 | [crates.io]: https://crates.io/ 58 | 59 | # Questions 60 | 61 | Feel free to [open an issue](https://github.com/dsprenkels/sss-cli/issues/new) 62 | or send me an email on my Github associated e-mail address. 63 | -------------------------------------------------------------------------------- /src/bin/combine.rs: -------------------------------------------------------------------------------- 1 | extern crate atty; 2 | #[macro_use] 3 | extern crate clap; 4 | extern crate env_logger; 5 | #[macro_use] 6 | extern crate log; 7 | extern crate shamirsecretsharing; 8 | extern crate shamirsecretsharing_cli; 9 | 10 | use std::env; 11 | use std::io; 12 | use std::io::prelude::*; 13 | use std::process::exit; 14 | 15 | use clap::{App, ArgMatches}; 16 | use shamirsecretsharing::hazmat::{combine_keyshares, KEYSHARE_SIZE}; 17 | use shamirsecretsharing_cli::*; 18 | 19 | /// Parse the command line arguments 20 | fn argparse<'a>() -> ArgMatches<'a> { 21 | App::new("secret-share-combine") 22 | .version(crate_version!()) 23 | .author(crate_authors!()) 24 | .about("Combine a list of shares (from stdin) that was created with secret-share-split") 25 | .get_matches() 26 | } 27 | 28 | fn main() { 29 | // If not log level has been set, default to info 30 | if env::var_os("RUST_LOG") == None { 31 | env::set_var("RUST_LOG", "secret_share_combine=info"); 32 | } 33 | 34 | // Init env_logger 35 | env_logger::init(); 36 | 37 | let _ = argparse(); 38 | 39 | // Read each line 40 | trace!("reading shares to memory"); 41 | let mut input_file = std::io::stdin(); 42 | let mut shares_string = String::new(); 43 | input_file 44 | .read_to_string(&mut shares_string) 45 | .unwrap_or_else(|err| { 46 | error!("error while reading stdin: {}", err); 47 | exit(1) 48 | }); 49 | let lines = shares_string.lines().filter(|x| !x.is_empty()).collect::>(); 50 | 51 | // Decode the lines 52 | trace!("decoding shares"); 53 | if lines.is_empty() { 54 | error!("no input shares supplied"); 55 | exit(1); 56 | } 57 | let mut decoded_lines = Vec::with_capacity(lines.len()); 58 | for (line_idx, line) in lines.iter().enumerate() { 59 | if line.len() % 2 != 0 { 60 | error!("share {} is of an incorrect length (the length is not even)", 61 | line_idx + 1); 62 | exit(1); 63 | } 64 | let mut decoded_line = Vec::with_capacity(line.len() / 2); 65 | let mut offset = 0; 66 | while offset < line.len() { 67 | let b = match u8::from_str_radix(&line[offset..offset + 2], 16) { 68 | Ok(x) => x, 69 | Err(err) => { 70 | error!("error while decoding share {}: {}", line_idx + 1, err); 71 | exit(1); 72 | } 73 | }; 74 | decoded_line.push(b); 75 | offset += 2; 76 | } 77 | if decoded_line.len() < KEYSHARE_SIZE { 78 | error!("share {} is too short", line_idx + 1); 79 | exit(1); 80 | } 81 | decoded_lines.push(decoded_line); 82 | } 83 | 84 | // Split off the keyshares 85 | trace!("splittings off keyshares from ciphertexts"); 86 | let mut keyshares = Vec::with_capacity(decoded_lines.len()); 87 | let mut ciphertexts = Vec::with_capacity(decoded_lines.len()); 88 | for line in &decoded_lines { 89 | let (keyshare, ciphertext) = line.split_at(KEYSHARE_SIZE); 90 | keyshares.push(keyshare.to_vec()); 91 | ciphertexts.push(ciphertext); 92 | } 93 | 94 | // Error if the ciphertexts are not all the same 95 | for (idx, other) in ciphertexts[1..].iter().enumerate() { 96 | if other != &ciphertexts[0] { 97 | error!("share 1 and {} do not seem to belong to the same secret. \ 98 | Please check if none of the shares are corrupted.", 99 | idx + 1); 100 | exit(1); 101 | } 102 | } 103 | 104 | // Restore the encryption key 105 | trace!("restoring encryption key"); 106 | let key = match combine_keyshares(&keyshares) { 107 | Ok(x) => x, 108 | Err(err) => { 109 | error!("{}", err); 110 | exit(1) 111 | } 112 | }; 113 | 114 | let mut secret = Vec::new(); 115 | trace!("decrypting secret"); 116 | match crypto_secretbox_open(&mut secret as &mut dyn Write, 117 | &mut ciphertexts[0] as &mut dyn Read, 118 | &NONCE, 119 | &key) { 120 | Ok(Some(())) => (), 121 | Ok(None) => { 122 | error!("shares did not combine to a valid secret"); 123 | exit(1); 124 | } 125 | Err(err) => { 126 | error!("{}", err); 127 | exit(1); 128 | } 129 | } 130 | 131 | let bytes = match String::from_utf8(secret) { 132 | Ok(text) => text.into_bytes(), 133 | Err(utf8err) => { 134 | let bytes = utf8err.into_bytes(); 135 | if atty::is(atty::Stream::Stdout) { 136 | warn!("invalid utf-8 text, some symbols may be lost!"); 137 | let hex = &bytes.iter() 138 | .map(|b| format!("{:02x}", b)) 139 | .take(80) 140 | .collect::(); 141 | let ellipsis = if bytes.len() > 80 { "..." } else { "" }; 142 | info!("the hex representation of the secret is '{}{}'.", hex, ellipsis); 143 | } 144 | bytes 145 | } 146 | }; 147 | debug!("writing secret to output file"); 148 | if let Err(err) = io::stdout().write_all(&bytes) { 149 | error!("{}", err); 150 | exit(1); 151 | }; 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | extern crate duct; 157 | 158 | use std::iter::repeat; 159 | use self::duct::cmd; 160 | 161 | macro_rules! cmd { 162 | ( $program:expr, $( $arg:expr ),* ) => ( 163 | { 164 | let args = [ $( $arg ),* ]; 165 | cmd($program, args.iter()) 166 | } 167 | ) 168 | } 169 | 170 | macro_rules! run_self { 171 | ( $( $arg:expr ),* ) => ( 172 | { 173 | let args = ["run", "--quiet", "--bin", "secret-share-combine", "--", $( $arg ),* ]; 174 | cmd(env!("CARGO"), args.iter()) 175 | } 176 | ) 177 | } 178 | 179 | const ERR_RANGE: std::ops::Range = 22..27; 180 | const MSG_RANGE: std::ops::RangeFrom = 50..; 181 | 182 | #[test] 183 | fn functional() { 184 | let secret = "secret"; 185 | let echo = cmd!("echo", secret); 186 | let split = echo.pipe(cmd!(env!("CARGO"), "run", "--quiet", "--bin", "secret-share-split", 187 | "--", "--count", "5", "--threshold", "4")); 188 | let combine = split.pipe(run_self!()); 189 | let output = combine.read().unwrap(); 190 | assert_eq!(output, secret); 191 | } 192 | 193 | #[test] 194 | fn not_enough_shares() { 195 | let shares = repeat("00").take(113).collect::(); 196 | let echo = cmd!("echo", shares); 197 | let combine = echo.pipe(run_self!()).unchecked().stderr_to_stdout(); 198 | let output = combine.read().unwrap(); 199 | assert_eq!(&output[ERR_RANGE], "ERROR"); 200 | assert_eq!(&output[MSG_RANGE], "shares did not combine to a valid secret"); 201 | } 202 | 203 | #[test] 204 | fn no_shares() { 205 | let echo = cmd!("echo", ""); 206 | let combine = echo.pipe(run_self!()).unchecked().stderr_to_stdout(); 207 | let output = combine.read().unwrap(); 208 | assert_eq!(&output[ERR_RANGE], "ERROR"); 209 | assert_eq!(&output[MSG_RANGE], "no input shares supplied"); 210 | } 211 | 212 | #[test] 213 | fn uneven_hex_len() { 214 | let echo = cmd!("echo", "0"); 215 | let combine = echo.pipe(run_self!()).unchecked().stderr_to_stdout(); 216 | let output = combine.read().unwrap(); 217 | assert_eq!(&output[ERR_RANGE], "ERROR"); 218 | assert_eq!(&output[MSG_RANGE], "share 1 is of an incorrect length \ 219 | (the length is not even)"); 220 | } 221 | 222 | #[test] 223 | fn short_hex_len() { 224 | let echo = cmd!("echo", "00"); 225 | let combine = echo.pipe(run_self!()).unchecked().stderr_to_stdout(); 226 | let output = combine.read().unwrap(); 227 | assert_eq!(&output[ERR_RANGE], "ERROR"); 228 | assert_eq!(&output[MSG_RANGE], "share 1 is too short"); 229 | } 230 | 231 | #[test] 232 | fn no_content() { 233 | let shares = " 234 | 01b5d858849053ec0b475b84c580a0a50e13fc283bdebfee35082a1fbe99ef74206efc338ab1f54cbbc63d2807ba07d6f6 235 | 02deb4f2a93e55d8a0a7644723b33ec94fa5ca52e5dfa1cc92c86f937a1d0114fb6efc338ab1f54cbbc63d2807ba07d6f6 236 | ".trim(); 237 | let echo = cmd!("echo", shares); 238 | let combine = echo.pipe(run_self!()); 239 | let output = combine.read().unwrap(); 240 | assert_eq!(output, ""); 241 | } 242 | 243 | /// Test shares generated by the demo page (currently) located at https://dsprenkels.com/sss/ 244 | #[test] 245 | fn demo_shares() { 246 | let shares = " 247 | 017d784898d4ea3ffcbe2fb814542e1a25ff4926cb886ccff926d0fab1cbb299226737bde1c0b5e6\ 248 | c2e4c927ccb26abbc27ef9632ae4903853a569abefbca5882ea0e1e31c54df1a3d9b0ed09e90f653\ 249 | 6d0aeeb5b1654d3348cabcdcf04637a25ee9f001bd6e04dd8b0bee7383c863aa79 250 | 028cd8cb05ee80dab157d352da41e0e2a83a16bc0e975de7e55faf9b93f1c2924e6737bde1c0b5e6\ 251 | c2e4c927ccb26abbc27ef9632ae4903853a569abefbca5882ea0e1e31c54df1a3d9b0ed09e90f653\ 252 | 6d0aeeb5b1654d3348cabcdcf04637a25ee9f001bd6e04dd8b0bee7383c863aa79".trim(); 253 | let echo = cmd!("echo", shares); 254 | let combine = echo.pipe(run_self!()); 255 | let output = combine.stdout_capture().run().unwrap(); 256 | let mut expected: Vec = Vec::with_capacity(64); 257 | expected.extend("Hello World! :D".as_bytes()); 258 | expected.push(0x80); 259 | while expected.len() < 64 { 260 | expected.push(0x00); 261 | } 262 | assert_eq!(output.stdout, expected); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/bin/split.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate clap; 3 | extern crate env_logger; 4 | #[macro_use] 5 | extern crate log; 6 | extern crate rand; 7 | extern crate shamirsecretsharing_cli; 8 | extern crate shamirsecretsharing; 9 | 10 | use std::env; 11 | use std::fmt; 12 | use std::fs::File; 13 | use std::io::prelude::*; 14 | use std::process::exit; 15 | 16 | use clap::{App, Arg, ArgMatches}; 17 | use rand::random; 18 | use shamirsecretsharing::hazmat::create_keyshares; 19 | use shamirsecretsharing::hazmat::KEY_SIZE; 20 | use shamirsecretsharing_cli::*; 21 | 22 | /// Parse the command line arguments 23 | fn argparse<'a>() -> ArgMatches<'a> { 24 | App::new("secret-share-split") 25 | .version(crate_version!()) 26 | .author(crate_authors!()) 27 | .about("Generate n shares of a file with recombination treshold t") 28 | .arg(Arg::with_name("count") 29 | .short("n") 30 | .long("count") 31 | .value_name("n") 32 | .help("The amount of shares that will be created") 33 | .takes_value(true) 34 | .required(true)) 35 | .arg(Arg::with_name("threshold") 36 | .short("t") 37 | .long("threshold") 38 | .value_name("k") 39 | .help("The treshold for restoring the file") 40 | .takes_value(true) 41 | .required(true)) 42 | .arg(Arg::with_name("FILE").help("Specifies the input file that will be secret-shared")) 43 | .get_matches() 44 | } 45 | 46 | fn main() { 47 | // If not log level has been set, default to info 48 | if env::var_os("RUST_LOG") == None { 49 | env::set_var("RUST_LOG", "secret_share_split=info"); 50 | } 51 | 52 | // Init env_logger 53 | env_logger::init(); 54 | 55 | // Parse command line arguments 56 | let matches = argparse(); 57 | let input_fn = matches.value_of("FILE"); 58 | let count = matches 59 | .value_of("count") 60 | .unwrap() 61 | .parse::() 62 | .map_err(|_| { 63 | error!("count is not a valid number"); 64 | exit(1); 65 | }) 66 | .and_then(|x| if 2 <= x && x <= 255 { Ok(x) } else { Err(x) }) 67 | .unwrap_or_else(|x| { 68 | error!("count must be a number between 2 and 255 (instead of {})", x); 69 | exit(1); 70 | }) as u8; 71 | let treshold = matches 72 | .value_of("threshold") 73 | .unwrap() 74 | .parse::() 75 | .map_err(|_| { 76 | error!("threshold is not a valid number"); 77 | exit(1); 78 | }) 79 | .and_then(|x| if 2 <= x && x <= (count as isize) { 80 | Ok(x) 81 | } else { 82 | Err(x) 83 | }) 84 | .unwrap_or_else(|x| { 85 | error!("threshold must be a number between 2 and {} (instead of {})", count, x); 86 | exit(1); 87 | }) as u8; 88 | 89 | // Open the input file and read its contents 90 | let mut input_file: Box = match input_fn { 91 | None | Some("-") => Box::new(std::io::stdin()), 92 | Some(input_fn) => { 93 | Box::new(File::open(input_fn).unwrap_or_else(|err| { 94 | error!("error while opening file '{}': {}", input_fn, err); 95 | exit(1); 96 | })) 97 | } 98 | }; 99 | // We are not able to use the normal API for variable length plaintexts, so we will have to 100 | // use the hazmat API and encrypt the file ourselves 101 | let key: [u8; KEY_SIZE] = random(); 102 | trace!("creating keyshares"); 103 | let keyshares = create_keyshares(&key, count, treshold) 104 | .unwrap_or_else(|err| { 105 | error!("{}", err); 106 | exit(1); 107 | }); 108 | 109 | // Encrypt the contents of the file 110 | let mut ciphertext = Vec::new(); 111 | trace!("encrypting secret"); 112 | crypto_secretbox(&mut ciphertext as &mut dyn Write, 113 | &mut *input_file, 114 | &NONCE, 115 | &key) 116 | .expect("unexpected error during encryption, this is probably a bug"); 117 | 118 | // Construct the full shares 119 | let full_shares = keyshares.iter() 120 | .map(|ks| ks.iter() 121 | .chain(ciphertext.iter())); 122 | 123 | // Write the shares to stdout 124 | let mut buf = String::new(); 125 | let buf_maxsize = 4 * 2u32.pow(20) as usize; // size 4Mb 126 | trace!("writing shares to output file"); 127 | for share in full_shares { 128 | for byte in share { 129 | if let Err(err) = write!(&mut buf as &mut dyn fmt::Write, "{:02x}", byte) { 130 | error!("{}", err); 131 | exit(1); 132 | } 133 | if buf.len() >= buf_maxsize { 134 | print!("{}", buf); 135 | buf.clear() 136 | } 137 | } 138 | println!("{}", buf); 139 | buf.clear() 140 | } 141 | drop(buf); 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | extern crate duct; 147 | use self::duct::cmd; 148 | 149 | macro_rules! cmd { 150 | ( $program:expr, $( $arg:expr ),* ) => ( 151 | { 152 | let args = [ $( $arg ),* ]; 153 | cmd($program, args.iter()) 154 | } 155 | ) 156 | } 157 | 158 | macro_rules! run_self { 159 | ( $( $arg:expr ),* ) => ( 160 | { 161 | let args = ["run", "--quiet", "--bin", "secret-share-split", "--", $( $arg ),* ]; 162 | cmd(env!("CARGO"), args.iter()) 163 | } 164 | ) 165 | } 166 | 167 | const ERR_RANGE: std::ops::Range = 22..27; 168 | const MSG_RANGE: std::ops::RangeFrom = 48..; 169 | 170 | #[test] 171 | fn functional() { 172 | let secret = "Hello World!"; 173 | let echo = cmd!("echo", "-n", secret); 174 | let output = echo.pipe(run_self!("--count", "5", "--threshold", "4")) 175 | .read() 176 | .unwrap(); 177 | let mut idx = 0; 178 | for line in output.lines() { 179 | assert_eq!(line.len(), 2 * (49 + secret.len())); 180 | let x = format!("{:02}", idx + 1); 181 | assert!(line.starts_with(&x)); 182 | idx += 1; 183 | } 184 | assert_eq!(idx, 5); 185 | } 186 | 187 | #[test] 188 | fn no_args() { 189 | let output = run_self!() 190 | .unchecked() 191 | .stderr_to_stdout() 192 | .read() 193 | .unwrap(); 194 | assert!(output.starts_with("error: The following required arguments were not provided: 195 | --count 196 | --threshold ")); 197 | } 198 | 199 | #[test] 200 | fn no_count() { 201 | let output = run_self!("--threshold", "4") 202 | .unchecked() 203 | .stderr_to_stdout() 204 | .read() 205 | .unwrap(); 206 | assert!(output.starts_with("error: The following required arguments were not provided: 207 | --count ")); 208 | } 209 | 210 | #[test] 211 | fn no_threshold() { 212 | let output = run_self!("--count", "5") 213 | .unchecked() 214 | .stderr_to_stdout() 215 | .read() 216 | .unwrap(); 217 | assert!(output.starts_with("error: The following required arguments were not provided: 218 | --threshold ")); 219 | } 220 | 221 | #[test] 222 | fn count_parse() { 223 | let output = run_self!("--count", "not a number", "--threshold", "4") 224 | .unchecked() 225 | .stderr_to_stdout() 226 | .read() 227 | .unwrap(); 228 | assert_eq!(&output[ERR_RANGE], "ERROR"); 229 | assert_eq!(&output[MSG_RANGE], "count is not a valid number"); 230 | } 231 | 232 | #[test] 233 | fn count_range() { 234 | macro_rules! test_bad_count { 235 | ($n:expr, $k:expr) => ( 236 | let output = run_self!("--count", $n, "--threshold", $k) 237 | .unchecked().stderr_to_stdout().read().unwrap(); 238 | assert_eq!(&output[ERR_RANGE], "ERROR"); 239 | assert_eq!(&output[MSG_RANGE], format!("count must be a number between 2 \ 240 | and 255 (instead of {})", $n)); 241 | ) 242 | } 243 | test_bad_count!("0", "4"); 244 | test_bad_count!("1", "4"); 245 | test_bad_count!("256", "4"); 246 | } 247 | 248 | #[test] 249 | fn threshold_parse() { 250 | let output = run_self!("--count", "5", "--threshold", "not a number") 251 | .unchecked() 252 | .stderr_to_stdout() 253 | .read() 254 | .unwrap(); 255 | assert_eq!(&output[ERR_RANGE], "ERROR"); 256 | assert_eq!(&output[MSG_RANGE], "threshold is not a valid number"); 257 | } 258 | 259 | #[test] 260 | fn threshold_range() { 261 | macro_rules! test_bad_threshold { 262 | ($n:expr, $k:expr) => ( 263 | let output = run_self!("--count", $n, "--threshold", $k) 264 | .unchecked().stderr_to_stdout().read().unwrap(); 265 | assert_eq!(&output[ERR_RANGE], "ERROR"); 266 | assert_eq!(&output[MSG_RANGE], format!("threshold must be a number between 2 \ 267 | and 5 (instead of {})", $k)); 268 | ) 269 | } 270 | test_bad_threshold!("5", "0"); 271 | test_bad_threshold!("5", "1"); 272 | test_bad_threshold!("5", "6"); 273 | test_bad_threshold!("5", "256"); 274 | } 275 | 276 | #[test] 277 | fn nonexistent_file() { 278 | let output = run_self!("--count", "5", "--threshold", "4", "nonexistent") 279 | .unchecked() 280 | .stderr_to_stdout() 281 | .read() 282 | .unwrap(); 283 | assert_eq!(&output[ERR_RANGE], "ERROR"); 284 | assert_eq!(&output[MSG_RANGE], 285 | "error while opening file \'nonexistent\': \ 286 | No such file or directory (os error 2)"); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | use std::io; 4 | use libc::{c_ulonglong, c_int}; 5 | use std::io::prelude::*; 6 | 7 | pub const NONCE: [u8; 24] = [0; 24]; 8 | 9 | #[allow(non_upper_case_globals)] 10 | const crypto_secretbox_xsalsa20poly1305_tweet_KEYBYTES: usize = 32; 11 | #[allow(non_upper_case_globals)] 12 | const crypto_secretbox_xsalsa20poly1305_tweet_NONCEBYTES: usize = 24; 13 | #[allow(non_upper_case_globals)] 14 | const crypto_secretbox_xsalsa20poly1305_tweet_ZEROBYTES: usize = 32; 15 | #[allow(non_upper_case_globals)] 16 | const crypto_secretbox_xsalsa20poly1305_tweet_BOXZEROBYTES: usize = 16; 17 | extern "C" { 18 | fn crypto_secretbox_xsalsa20poly1305_tweet(c: *mut u8, 19 | m: *const u8, 20 | mlen: c_ulonglong, 21 | n: *const u8, 22 | k: *const u8) 23 | -> c_int; 24 | fn crypto_secretbox_xsalsa20poly1305_tweet_open(m: *mut u8, 25 | c: *const u8, 26 | clen: c_ulonglong, 27 | n: *const u8, 28 | k: *const u8) 29 | -> c_int; 30 | } 31 | 32 | pub fn crypto_secretbox(w: &mut dyn Write, r: &mut dyn Read, nonce: &[u8], key: &[u8]) -> io::Result<()> { 33 | assert_eq!(key.len(), crypto_secretbox_xsalsa20poly1305_tweet_KEYBYTES); 34 | assert_eq!(nonce.len(), 35 | crypto_secretbox_xsalsa20poly1305_tweet_NONCEBYTES); 36 | 37 | let mut m: Vec = vec![0; crypto_secretbox_xsalsa20poly1305_tweet_ZEROBYTES]; 38 | io::copy(r, &mut m as &mut dyn Write)?; 39 | let mlen = m.len(); 40 | let mut c: Vec = vec![0; mlen]; 41 | 42 | let ret = unsafe { 43 | crypto_secretbox_xsalsa20poly1305_tweet(c.as_mut_ptr(), 44 | m.as_ptr(), 45 | mlen as u64, 46 | nonce.as_ptr(), 47 | key.as_ptr()) 48 | }; 49 | assert_eq!(ret, 0); 50 | io::copy(&mut &c[crypto_secretbox_xsalsa20poly1305_tweet_BOXZEROBYTES..], 51 | w)?; 52 | Ok(()) 53 | } 54 | 55 | pub fn crypto_secretbox_open(w: &mut dyn Write, 56 | r: &mut dyn Read, 57 | nonce: &[u8], 58 | key: &[u8]) 59 | -> io::Result> { 60 | assert_eq!(key.len(), crypto_secretbox_xsalsa20poly1305_tweet_KEYBYTES); 61 | assert_eq!(nonce.len(), 62 | crypto_secretbox_xsalsa20poly1305_tweet_NONCEBYTES); 63 | let mut c: Vec = vec![0; crypto_secretbox_xsalsa20poly1305_tweet_BOXZEROBYTES]; 64 | io::copy(r, &mut c as &mut dyn Write)?; 65 | let clen = c.len(); 66 | let mut m: Vec = vec![0; clen]; 67 | 68 | let ret = unsafe { 69 | crypto_secretbox_xsalsa20poly1305_tweet_open(m.as_mut_ptr(), 70 | c.as_ptr(), 71 | clen as u64, 72 | nonce.as_ptr(), 73 | key.as_ptr()) 74 | }; 75 | if ret == -1 { 76 | return Ok(None); 77 | } 78 | io::copy(&mut &m[crypto_secretbox_xsalsa20poly1305_tweet_ZEROBYTES..], 79 | w)?; 80 | Ok(Some(())) 81 | } 82 | --------------------------------------------------------------------------------