feat: add secretshare

This commit is contained in:
2023-08-26 00:05:54 +08:00
parent 27ee89dd8a
commit a26e3e1ffe
6 changed files with 744 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
2015-02-03:
* I changed the CRC-24 checksum computation to include the coding parameter
K and the share number N so that these numbers are also protected.
If you have older shares generated with a previous version, you can still
decode the secret by simply removing the checksum part of the shares.
* The README now includes more information about the inner workings of
secretshare and also a note on "perfect secrecy".

121
__crypto/secretshare/Cargo.lock generated Normal file
View File

@@ -0,0 +1,121 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "crc24"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0"
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "rand"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
dependencies = [
"libc",
"rand 0.4.6",
]
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "rustc-serialize"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
[[package]]
name = "secretshare"
version = "0.1.6"
dependencies = [
"crc24",
"getopts",
"rand 0.3.23",
"rustc-serialize",
]
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@@ -0,0 +1,13 @@
[package]
name = "secretshare"
version = "0.1.6"
authors = ["Sebastian Gesemann <s.gesemann@gmail.com>"]
description = "This is an implementation of Shamir's secret sharing scheme."
license = "GPLv3"
readme = "README.md"
[dependencies]
getopts = "0.2"
rustc-serialize = "0.3"
crc24 = "0.1"
rand = "0.3"

View File

@@ -0,0 +1,114 @@
> From: https://github.com/sellibitze/secretshare
# secretshare
This program is an implementation of
[Shamir's secret sharing](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing).
A secret can be split into N shares in a way so that
a selectable number of shares K (with K ≤ N) is required
to reconstruct the secret again.
**Warning**: I don't yet recommend the serious use of this tool. The
encoding of the shares might change in a newer version in which case
you would have trouble decoding secrets that have been shared using
an older version of the program. For now, this is experimental.
# Example
Passing a secret to secretshare for encoding:
```
$ echo My secret | ./secretshare -e2,5
2-1-1YAYwmOHqZ69jA-v+mz
2-2-YJZQDGm22Y77Gw-IhSh
2-3-+G9ovW9SAnUynQ-Elwi
2-4-F7rAjX3UOa53KA-b2vm
2-5-j0P4PHsw4lW+rg-XyNl
```
The parameters following the `-e` option tell `secretshare` to create 5 shares of which 2 will be necessary for decoding.
Decoding a subset of shares (one share per line) can be done like this:
```
$ echo -e "2-2-YJZQDGm22Y77Gw-IhSh \n 2-4-F7rAjX3UOa53KA-b2vm" | ./secretshare -d
My secret
```
# Building
This project is Cargo-enabled. So, you should be able to build it with
```
$ cargo build --release
```
once you have made sure that `rustc` (the compiler) and `cargo`
(the build and dependency management tool) are installed.
Visit the [Rust homepage](http://www.rust-lang.org/) if you are
don't know where to get these tools.
# I/O
The secret data does not have to be text. `secretshare` treats it as
binary data. But, of course, you can feed it text as well. In the above
example the echo command terminated the string with a line feed which
is actually part of the secret and output as well after decoding.
Note that, while `secretshare` supports secrets of up to 64 KiB
it makes little sense to use such large secrets directly. In situations
where you want to share larger secrets, you would usually pick a random
password for encryption and use that password as secret for `secretshare`.
The generated shares are lines of ASCII text.
# Structure of the shares
```
2-1-LiTyeXwEP71IUA-Qj6n
^ ^ ^^^^^^^^^^^^^^ ^^^^
K N D C
```
A share is built out of three or four parts separated with a minus: K-N-D-C.
The last part is optional. K is one of the encoding parameters that tell you
how many distinct
shares of a specific secret are necessary to be able to recover the
secret. The number N identifies the share (ranging from 1 to the number
of shares that have been created). The D part is a Base64 encoding of
a specific share's raw data. The optional part C is a Base64 encoding
of a CRC-24 checksum of the concatenation of K and N as bytes followed
by the share's raw data (before Base64 encoding). The same checksum
algorithm is used in the OpenPGP format for “ASCII amoring”.
# A word on the secrecy
Shamir's secret sharing is known to have the perfect secrecy property.
In the context of (K,N)-threshold schemes this means that if you have
less than K shares available, you have absolutely no information about
what the secret is except for its length. The checksums that are included
in the shares
also don't reveal anything about the secret.
They are just a simple integrity protection of the shares themselves.
In other words, given a share without checksum, we can derive a share
with a checksum. This obviously does not add any new information.
# Galois field
Shamir's secret sharing algorithm requires the use of polynomials over
a finite field. One easy way of constructing a finite field is to pick
a prime number p, use the integers 0, 1, 2, ..., p-1 as field elements
and simply use modular arithmetic (mod p) for the field operations.
So, you *could* pick a prime like 257 to apply Shamir's algorithm
byte-wise. The downside of this is that the shares would consist of
sequences of values each between 0 and 256 *inclusive*. So, you would
need more than 8 bits to encode each of them.
But there is another way. We are not restricted to so-called
prime fields. There are also non-prime fields where the number of
elements is a *power* of a prime, for example 2^8=256. It's just
a bit harder to explain how they are constructed. The finite
field I used is the same as the one you can find in the RAID 6
implementation of the Linux kernel or the Anubis block cipher:
Gf(2^8) reduction polynomial is x^8 + x^4 + x^3 + x^2 + 1 or
alternatively 11D in hex.

View File

@@ -0,0 +1,143 @@
//! This module provides the Gf256 type which is used to represent
//! elements of a finite field wich 256 elements.
use std::num::Wrapping;
use std::ops::{Add, Div, Mul, Sub};
use std::sync::Once;
const POLY: u8 = 0x1D; // represents x^8 + x^4 + x^3 + x^2 + 1
/// replicates the least significant bit to every other bit
#[inline]
fn mask(bit: u8) -> u8 {
(Wrapping(0u8) - Wrapping(bit & 1)).0
}
/// multiplies a polynomial with x and returns the residual
/// of the polynomial division with POLY as divisor
#[inline]
fn xtimes(poly: u8) -> u8 {
(poly << 1) ^ (mask(poly >> 7) & POLY)
}
/// Tables used for multiplication and division
struct Tables {
exp: [u8; 256],
log: [u8; 256],
inv: [u8; 256],
}
static INIT: Once = Once::new();
static mut TABLES: Tables = Tables {
exp: [0; 256],
log: [0; 256],
inv: [0; 256],
};
fn get_tables() -> &'static Tables {
INIT.call_once(|| {
// mutable access is fine because of synchronization via INIT
let tabs = unsafe { &mut TABLES };
let mut tmp = 1;
for power in 0..255usize {
tabs.exp[power] = tmp;
tabs.log[tmp as usize] = power as u8;
tmp = xtimes(tmp);
}
tabs.exp[255] = 1;
for x in 1..256usize {
let l = tabs.log[x];
let nl = if l == 0 { 0 } else { 255 - l };
let i = tabs.exp[nl as usize];
tabs.inv[x] = i;
}
});
// We're guaranteed to have TABLES initialized by now
return unsafe { &TABLES };
}
/// Type for elements of a finite field with 256 elements
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Gf256 {
pub poly: u8,
}
impl Gf256 {
/// returns the additive neutral element of the field
#[inline]
pub fn zero() -> Gf256 {
Gf256 { poly: 0 }
}
/// returns the multiplicative neutral element of the field
#[inline]
pub fn one() -> Gf256 {
Gf256 { poly: 1 }
}
#[inline]
pub fn from_byte(b: u8) -> Gf256 {
Gf256 { poly: b }
}
#[inline]
pub fn to_byte(&self) -> u8 {
self.poly
}
pub fn log(&self) -> Option<u8> {
if self.poly == 0 {
None
} else {
let tabs = get_tables();
Some(tabs.log[self.poly as usize])
}
}
pub fn exp(power: u8) -> Gf256 {
let tabs = get_tables();
Gf256 { poly: tabs.exp[power as usize] }
}
/*
pub fn inv(&self) -> Option<Gf256> {
self.log().map(|l| Gf256::exp(255 - l))
}
*/
}
impl Add<Gf256> for Gf256 {
type Output = Gf256;
#[inline]
fn add(self, rhs: Gf256) -> Gf256 {
Gf256::from_byte(self.poly ^ rhs.poly)
}
}
impl Sub<Gf256> for Gf256 {
type Output = Gf256;
#[inline]
fn sub(self, rhs: Gf256) -> Gf256 {
Gf256::from_byte(self.poly ^ rhs.poly)
}
}
impl Mul<Gf256> for Gf256 {
type Output = Gf256;
fn mul(self, rhs: Gf256) -> Gf256 {
if let (Some(l1), Some(l2)) = (self.log(), rhs.log()) {
let tmp = ((l1 as u16) + (l2 as u16)) % 255;
Gf256::exp(tmp as u8)
} else {
Gf256 { poly: 0 }
}
}
}
impl Div<Gf256> for Gf256 {
type Output = Gf256;
fn div(self, rhs: Gf256) -> Gf256 {
let l2 = rhs.log().expect("division by zero");
if let Some(l1) = self.log() {
let tmp = ((l1 as u16) + 255 - (l2 as u16)) % 255;
Gf256::exp(tmp as u8)
} else {
Gf256 { poly: 0 }
}
}
}

View File

@@ -0,0 +1,345 @@
extern crate rustc_serialize as serialize;
extern crate getopts;
extern crate crc24;
extern crate rand;
use std::convert;
use std::env;
use std::error;
use std::fmt;
use std::io;
use std::io::prelude::*;
use std::iter::repeat;
use std::num;
use rand::{Rng, OsRng};
use getopts::Options;
use serialize::base64::{self, FromBase64, ToBase64};
use gf256::Gf256;
mod gf256;
fn new_vec<T: Clone>(n: usize, x: T) -> Vec<T> {
repeat(x).take(n).collect()
}
#[derive(Debug)]
pub struct Error {
descr: &'static str,
detail: Option<String>,
}
impl Error {
fn new(descr: &'static str, detail: Option<String>) -> Error {
Error { descr: descr, detail: detail }
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.detail {
None => write!(f, "{}", self.descr),
Some(ref detail) => write!(f, "{} ({})", self.descr, detail)
}
}
}
impl error::Error for Error {
fn description(&self) -> &str { self.descr }
fn cause(&self) -> Option<&dyn error::Error> { None }
}
impl convert::From<Error> for io::Error {
fn from(me: Error) -> io::Error {
io::Error::new(io::ErrorKind::Other, me)
}
}
// a try!-like macro for Option<T> expressions that takes
// a &'static str as error message as 2nd parameter
// and creates an Error out of it if necessary.
macro_rules! otry {
($o:expr, $e:expr) => (
match $o {
Some(thing_) => thing_,
None => return Err(convert::From::from(Error::new($e, None)))
}
)
}
/// maps a ParseIntError to an io::Error
fn pie2io(p: num::ParseIntError) -> io::Error {
convert::From::from(
Error::new("Integer parsing error", Some(p.to_string()))
)
}
fn other_io_err(descr: &'static str, detail: Option<String>) -> io::Error {
convert::From::from(
Error::new(descr, detail)
)
}
/// evaluates a polynomial at x=1, 2, 3, ... n (inclusive)
fn encode<W: Write>(src: &[u8], n: u8, w: &mut W) -> io::Result<()> {
for raw_x in 1..((n as u16) + 1) {
let x = Gf256::from_byte(raw_x as u8);
let mut fac = Gf256::one();
let mut acc = Gf256::zero();
for &coeff in src.iter() {
acc = acc + fac * Gf256::from_byte(coeff);
fac = fac * x;
}
w.write(&[acc.to_byte()])?;
}
Ok(())
}
/// evaluates an interpolated polynomial at `raw_x` where
/// the polynomial is determined using Lagrangian interpolation
/// based on the given x/y coordinates `src`.
fn lagrange_interpolate(src: &[(u8, u8)], raw_x: u8) -> u8 {
let x = Gf256::from_byte(raw_x);
let mut sum = Gf256::zero();
for (i, &(raw_xi, raw_yi)) in src.iter().enumerate() {
let xi = Gf256::from_byte(raw_xi);
let yi = Gf256::from_byte(raw_yi);
let mut lix = Gf256::one();
for (j, &(raw_xj, _)) in src.iter().enumerate() {
if i != j {
let xj = Gf256::from_byte(raw_xj);
let delta = xi - xj;
assert!(delta.poly != 0, "Duplicate shares");
lix = lix * (x - xj) / delta;
}
}
sum = sum + lix * yi;
}
sum.to_byte()
}
fn secret_share(src: &[u8], k: u8, n: u8) -> io::Result<Vec<Vec<u8>>> {
let mut result = Vec::with_capacity(n as usize);
for _ in 0..(n as usize) {
result.push(new_vec(src.len(), 0u8));
}
let mut col_in = new_vec(k as usize, 0u8);
let mut col_out = Vec::with_capacity(n as usize);
let mut osrng = OsRng::new()?;
for (c, &s) in src.iter().enumerate() {
col_in[0] = s;
osrng.fill_bytes(&mut col_in[1..]);
col_out.clear();
encode(&*col_in, n, &mut col_out)?;
for (&y, share) in col_out.iter().zip(result.iter_mut()) {
share[c] = y;
}
}
Ok(result)
}
enum Action {
Encode(u8, u8),
// k and n parameter
Decode,
}
fn parse_k_n(s: &str) -> io::Result<(u8, u8)> {
let mut iter = s.split(',');
let msg = "K and N have to be separated with a comma";
let s1 = otry!(iter.next(), msg).trim();
let s2 = otry!(iter.next(), msg).trim();
let k = s1.parse().map_err(pie2io)?;
let n = s2.parse().map_err(pie2io)?;
Ok((k, n))
}
/// computes a CRC-24 hash over the concatenated coding parameters k, n
/// and the raw share data
fn crc24_as_bytes(k: u8, n: u8, octets: &[u8]) -> [u8; 3] {
use std::hash::Hasher;
let mut h = crc24::Crc24Hasher::new();
h.write(&[k, n]);
h.write(octets);
let v = h.finish();
[((v >> 16) & 0xFF) as u8,
((v >> 8) & 0xFF) as u8,
(v & 0xFF) as u8]
}
fn perform_encode(k: u8, n: u8, with_checksums: bool) -> io::Result<()> {
let secret = {
let limit: usize = 0x10000;
let stdin = io::stdin();
let mut locked = stdin.lock();
let mut tmp: Vec<u8> = Vec::new();
locked.by_ref().take(limit as u64).read_to_end(&mut tmp)?;
if tmp.len() == limit {
let mut dummy = [0u8];
if locked.read(&mut dummy)? > 0 {
return Err(other_io_err("Secret too large",
Some(format!("My limit is at {} bytes.", limit))));
}
}
tmp
};
let shares = secret_share(&*secret, k, n)?;
let config = base64::Config {
pad: false,
..base64::STANDARD
};
for (index, share) in shares.iter().enumerate() {
let salad = share.to_base64(config);
if with_checksums {
let crc_bytes = crc24_as_bytes(k, (index + 1) as u8, &**share);
println!("{}-{}-{}-{}", k, index + 1, salad, crc_bytes.to_base64(config));
} else {
println!("{}-{}-{}", k, index + 1, salad);
}
}
Ok(())
}
/// reads shares from stdin and returns Ok(k, shares) on success
/// where shares is a Vec<(u8, Vec<u8>)> representing x-coordinates
/// and share data.
fn read_shares() -> io::Result<(u8, Vec<(u8, Vec<u8>)>)> {
let stdin = io::stdin();
let stdin = io::BufReader::new(stdin.lock());
let mut opt_k_l: Option<(u8, usize)> = None;
let mut counter = 0u8;
let mut shares: Vec<(u8, Vec<u8>)> = Vec::new();
for line in stdin.lines() {
let line = line?;
let parts: Vec<_> = line.trim().split('-').collect();
if parts.len() < 3 || parts.len() > 4 {
return Err(other_io_err("Share parse error: Expected 3 or 4 \
parts searated by a minus sign", None));
}
let (k, n, p3, opt_p4) = {
let mut iter = parts.into_iter();
let k = iter.next().unwrap().parse::<u8>().map_err(pie2io)?;
let n = iter.next().unwrap().parse::<u8>().map_err(pie2io)?;
let p3 = iter.next().unwrap();
let opt_p4 = iter.next();
(k, n, p3, opt_p4)
};
if k < 1 || n < 1 {
return Err(other_io_err("Share parse error: Illegal K,N parameters", None));
}
let data = p3.from_base64().map_err(|_| other_io_err(
"Share parse error: Base64 decoding of data block failed", None))?;
if let Some(check) = opt_p4 {
if check.len() != 4 {
return Err(other_io_err("Share parse error: Checksum part is \
expected to be four characters", None));
}
let crc_bytes = check.from_base64().map_err(|_| other_io_err(
"Share parse error: Base64 decoding of checksum failed", None))?;
let mychksum = crc24_as_bytes(k, n, &*data);
if crc_bytes != mychksum {
return Err(other_io_err("Share parse error: Checksum mismatch", None));
}
}
if let Some((ck, cl)) = opt_k_l {
if ck != k || cl != data.len() {
return Err(other_io_err("Incompatible shares", None));
}
} else {
opt_k_l = Some((k, data.len()));
}
if shares.iter().all(|s| s.0 != n) {
shares.push((n, data));
counter += 1;
if counter == k {
return Ok((k, shares));
}
}
}
Err(other_io_err("Not enough shares provided!", None))
}
fn perform_decode() -> io::Result<()> {
let (k, shares) = read_shares()?;
assert!(!shares.is_empty());
let slen = shares[0].1.len();
let mut col_in = Vec::with_capacity(k as usize);
let mut secret = Vec::with_capacity(slen);
for byteindex in 0..slen {
col_in.clear();
for s in shares.iter().take(k as usize) {
col_in.push((s.0, s.1[byteindex]));
}
secret.push(lagrange_interpolate(&*col_in, 0u8));
}
let mut out = io::stdout();
out.write_all(&*secret)?;
out.flush()
}
fn main() {
let mut stderr = io::stderr();
let args: Vec<String> = env::args().collect();
let mut opts = Options::new();
opts.optflag("h", "help", "print this help text");
opts.optflag("d", "decode", "for decoding");
opts.optopt("e", "encode", "for encoding, K is the required number of \
shares for decoding, N is the number of shares \
to generate. 1 <= K <= N <= 255", "K,N");
let opt_matches = match opts.parse(&args[1..]) {
Ok(m) => m,
Err(f) => {
drop(writeln!(&mut stderr, "Error: {}", f));
// env::set_exit_status(1); // FIXME: unstable feature
return;
}
};
if args.len() < 2 || opt_matches.opt_present("h") {
println!(
"The program secretshare is an implementation of Shamir's secret sharing scheme.\n\
It is applied byte-wise within a finite field for arbitrarily long secrets.\n");
println!("{}", opts.usage("Usage: secretshare [options]"));
println!("Input is read from STDIN and output is written to STDOUT.");
return;
}
let action: Result<_, _> =
match (opt_matches.opt_present("e"), opt_matches.opt_present("d")) {
(false, false) => Err("Nothing to do! Use -e or -d"),
(true, true) => Err("Use either -e or -d and not both"),
(false, true) => Ok(Action::Decode),
(true, false) => {
if let Some(param) = opt_matches.opt_str("e") {
if let Ok((k, n)) = parse_k_n(&*param) {
if 0 < k && k <= n {
Ok(Action::Encode(k, n))
} else {
Err("Invalid encoding parameters K,N")
}
} else {
Err("Could not parse K,N parameters")
}
} else {
Err("No parameter for -e or -d provided")
}
}
};
let result =
match action {
Ok(Action::Encode(k, n)) => perform_encode(k, n, true),
Ok(Action::Decode) => perform_decode(),
Err(e) => Err(other_io_err(e, None))
};
if let Err(e) = result {
drop(writeln!(&mut stderr, "{}", e));
// env::set_exit_status(1); // FIXME: unstable feature
}
}