Skip to main content

dark_crystal_secret_sharing_rust/
lib.rs

1//! Secret sharing with authentication
2//!
3//! Internally this uses [sharks](https://docs.rs/sharks/0.5.0/sharks/) for Shamirs secret sharing.
4//!
5//! This is part of a work-in-progress Rust implementation of
6//! the [Dark Crystal Key Backup Protocol](https://darkcrystal.pw/protocol-specification/).
7
8use rand::{thread_rng, Rng};
9use sharks::{Share, Sharks};
10use std::collections::HashSet;
11use std::convert::{TryFrom, TryInto};
12use std::fmt;
13use xsalsa20poly1305::aead::generic_array::GenericArray;
14use zeroize::Zeroize;
15
16pub mod encrypt;
17
18/// Generate random indexes to take a subset of 255 shares
19fn get_shares_to_take(num_shares: u8) -> HashSet<u8> {
20    let mut rng = thread_rng();
21    let mut shares_to_take = HashSet::new();
22    while shares_to_take.len() < num_shares.into() {
23        let next_index = rng.gen::<u8>();
24        if next_index > 0 {
25            shares_to_take.insert(next_index);
26        }
27    }
28    shares_to_take
29}
30
31/// Create a set of shares for a given secret
32pub fn share(secret: &[u8], num_shares: u8, threshold: u8) -> Result<Vec<Vec<u8>>, ShareError> {
33    if num_shares < 2 {
34        return Err(ShareError {
35            message: "At least 2 shares required".to_string(),
36        });
37    }
38
39    if threshold < 2 {
40        return Err(ShareError {
41            message: "Threshold must be at least 2".to_string(),
42        });
43    }
44
45    if threshold > num_shares {
46        return Err(ShareError {
47            message: "Threshold must not be greater than the number of shares".to_string(),
48        });
49    }
50
51    let sharks = Sharks(threshold);
52    let dealer = sharks.dealer(&secret[..]);
53    let shares_to_take = get_shares_to_take(num_shares);
54
55    Ok(dealer
56        .take(255)
57        .map(|s| Vec::from(&s))
58        .filter(|s| shares_to_take.contains(&s[0]))
59        .collect())
60}
61
62/// Recover a secret from a given set of shares
63pub fn combine(shares_bytes: Vec<Vec<u8>>) -> Result<Vec<u8>, RecoveryError> {
64    let shares: Vec<Share> = shares_bytes
65        .iter()
66        .map(|s| Share::try_from(s.as_slice()).unwrap())
67        .collect();
68
69    let sharks = Sharks(shares.len().try_into().unwrap());
70    match sharks.recover(&shares) {
71        Ok(val) => Ok(val),
72        Err(err) => Err(err.into()),
73    }
74}
75
76/// Encrypt a secret and create shares of its key.
77/// This gives authentication so we know whether recovery was successful
78/// It also reduces duplication with long (> 32 bytes) secrets,
79/// and improves security when using non-uniformly random secrets such
80/// as passwords.
81pub fn share_authenticated(
82    secret: &[u8],
83    num_shares: u8,
84    threshold: u8,
85) -> Result<(Vec<Vec<u8>>, Vec<u8>), ShareError> {
86    let mut key = encrypt::generate_key();
87    match share(&key, num_shares, threshold) {
88        Ok(shares) => {
89            let ciphertext = encrypt::encrypt(key, secret.to_vec()).unwrap();
90            key.zeroize();
91            Ok((shares, ciphertext))
92        }
93        Err(err) => Err(err),
94    }
95}
96
97/// Combine a set of shares and ciphertext produced by share_authenticated
98pub fn combine_authenticated(
99    shares: Vec<Vec<u8>>,
100    ciphertext: Vec<u8>,
101) -> Result<Vec<u8>, RecoveryError> {
102    let recovered_key = combine(shares)?;
103    let key = GenericArray::from_slice(&recovered_key[..]);
104    match encrypt::decrypt(*key, ciphertext) {
105        Ok(val) => Ok(val),
106        Err(err) => Err(err.into()),
107    }
108}
109
110/// Error created when share fn fails
111#[derive(Debug)]
112pub struct ShareError {
113    pub message: String,
114}
115
116impl fmt::Display for ShareError {
117    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
118        write!(f, "Cannot create shares: {}", self.message)
119    }
120}
121
122/// Error created when recovery fails
123#[derive(Debug)]
124pub struct RecoveryError {
125    pub message: String,
126}
127
128impl fmt::Display for RecoveryError {
129    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
130        write!(f, "Error during recovery {}", self.message)
131    }
132}
133
134impl From<xsalsa20poly1305::aead::Error> for RecoveryError {
135    fn from(error: xsalsa20poly1305::aead::Error) -> Self {
136        RecoveryError {
137            message: error.to_string(),
138        }
139    }
140}
141
142impl From<&str> for RecoveryError {
143    fn from(error: &str) -> Self {
144        RecoveryError {
145            message: String::from(error),
146        }
147    }
148}
149
150/// Give a recommended threshold value for a given number of shares
151pub fn default_threshold(number_of_shares: u8) -> u8 {
152    if number_of_shares == 2 {
153        return 2;
154    };
155    (number_of_shares as f32 * 0.75) as u8
156}
157
158/// Gives a threshold 'sanity' factor, given a threshold and number of shares
159/// 0 is ideal.  Positive values represent the level of danger of
160/// loosing access to the secret.
161/// Negative values represent the level of danger of an attacker gaining it.
162pub fn thresold_sanity(threshold: u8, number_of_shares: u8) -> i32 {
163    (threshold as i32) - (default_threshold(number_of_shares) as i32)
164}
165
166#[cfg(test)]
167mod test {
168    use super::*;
169
170    #[test]
171    fn passes_on_all_shares() {
172        let original_secret = b"hello";
173        let shares = share(&original_secret[..], 5, 3).unwrap();
174        assert_eq!(shares.len(), 5);
175        let recovered_secret = combine(shares).unwrap();
176        assert_eq!(recovered_secret, b"hello");
177    }
178
179    #[test]
180    fn passes_on_threshold_amount() {
181        let original_secret = b"hello";
182        let mut shares = share(&original_secret[..], 5, 3).unwrap();
183        assert_eq!(shares.len(), 5);
184        shares.remove(0);
185        shares.remove(1);
186        assert_eq!(shares.len(), 3);
187        let recovered_secret = combine(shares).unwrap();
188        assert_eq!(recovered_secret, b"hello");
189    }
190
191    #[test]
192    fn authenticated() {
193        let original_secret = b"hello";
194        let (shares, ciphertext) = share_authenticated(&original_secret[..], 5, 3).unwrap();
195        assert_eq!(shares.len(), 5);
196        let recovered_secret = combine_authenticated(shares, ciphertext).unwrap();
197        assert_eq!(recovered_secret, b"hello");
198    }
199
200    #[test]
201    fn fails_on_insufficient_shares() {
202        let original_secret = b"hello";
203        let (mut shares, ciphertext) = share_authenticated(&original_secret[..], 5, 3).unwrap();
204        assert_eq!(shares.len(), 5);
205        shares.remove(0);
206        shares.remove(1);
207        shares.remove(2);
208        assert_eq!(shares.len(), 2);
209
210        let res = combine_authenticated(shares, ciphertext);
211        assert!(res.is_err());
212    }
213
214    #[test]
215    fn fails_on_impossible_threshold() {
216        let original_secret = b"hello";
217        let share_result = share(&original_secret[..], 5, 6);
218        assert!(share_result.is_err());
219    }
220
221    #[test]
222    fn fails_on_threshold_of_one() {
223        let original_secret = b"hello";
224        let share_result = share(&original_secret[..], 5, 1);
225        assert!(share_result.is_err());
226    }
227
228    #[test]
229    fn fails_on_creating_one_share() {
230        let original_secret = b"hello";
231        let share_result = share(&original_secret[..], 1, 1);
232        assert!(share_result.is_err());
233    }
234
235    #[test]
236    fn fails_on_bad_share() {
237        let original_secret = b"hello";
238        let (mut shares, ciphertext) = share_authenticated(&original_secret[..], 5, 3).unwrap();
239        shares[0] = b"bad share".to_vec();
240
241        let res = combine_authenticated(shares, ciphertext);
242        assert!(res.is_err());
243    }
244
245    #[test]
246    fn check_default_threshold() {
247        assert_eq!(thresold_sanity(7, 10), 0);
248        assert_eq!(thresold_sanity(5, 7), 0);
249        assert_eq!(thresold_sanity(3, 5), 0);
250        assert_eq!(thresold_sanity(2, 3), 0);
251        assert_eq!(thresold_sanity(2, 2), 0);
252
253        assert_eq!(thresold_sanity(2, 10), -5);
254        assert_eq!(thresold_sanity(9, 10), 2);
255    }
256}