Skip to main content

dark_crystal_web3_core/
lib.rs

1//! Create encrypted share payloads, which are encoded into links
2//! to the dark-crystal-web3 dapp. Recover a secret from a set
3//! of decrypted shares.
4
5use core::num::TryFromIntError;
6use cryptoxide::{blake2b::Blake2b, mac::Mac};
7use dark_crystal_key_backup_rust::ShareAndEncryptError;
8pub use dark_crystal_key_backup_rust::{
9    combine_authenticated, default_threshold, share_and_encrypt_detached_nonce,
10    share_authenticated, EncryptedShareSet, Error, RecoveryError,
11};
12use hex;
13use serde::{Deserialize, Serialize};
14use serde_qs;
15use std::collections::HashMap;
16use std::collections::HashSet;
17use std::fmt;
18
19pub mod secret;
20
21const DAPP_URL: &str = "https://web3.darkcrystal.pw/store";
22
23pub const SHARE_LENGTH: usize = 33;
24
25/// Encrypted share payload as hex strings
26/// Field names are abbreviated to keep link short
27#[derive(Serialize, Deserialize, Debug)]
28struct ShareOutput {
29    /// Ephemeral public key
30    pk: String,
31    /// Ciphertext
32    c: String,
33    /// Shares array
34    s: Vec<String>,
35    /// Lookup keys array
36    lk: Vec<String>,
37}
38
39/// Encode share payload as hex strings
40fn parse_encrypted_shares(
41    encrypted_share_set: EncryptedShareSet,
42    lookup_keys: Vec<Vec<u8>>,
43) -> ShareOutput {
44    ShareOutput {
45        pk: hex::encode(encrypted_share_set.eph_public_key.as_bytes()),
46        c: hex::encode(&encrypted_share_set.ciphertext),
47        s: encrypted_share_set
48            .encrypted_shares
49            .iter()
50            .map(|share| hex::encode(share))
51            .collect(),
52        lk: lookup_keys.iter().map(|k| hex::encode(k)).collect(),
53    }
54}
55
56/// Build a URL from the given share payload
57fn build_url(share_output: &ShareOutput) -> String {
58    format!(
59        "{}?{}",
60        DAPP_URL,
61        serde_qs::to_string(share_output).unwrap() // TODO
62    )
63}
64
65/// Error when creating shares
66#[derive(Debug)]
67pub struct ShareError {
68    pub message: String,
69}
70
71impl fmt::Display for ShareError {
72    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
73        write!(f, "Error when creating shares {}", self.message)
74    }
75}
76
77impl From<Error> for ShareError {
78    fn from(error: Error) -> Self {
79        ShareError {
80            message: error.to_string(),
81        }
82    }
83}
84
85impl From<ShareAndEncryptError> for ShareError {
86    fn from(error: ShareAndEncryptError) -> Self {
87        ShareError {
88            message: error.to_string(),
89        }
90    }
91}
92
93impl From<TryFromIntError> for ShareError {
94    fn from(_error: TryFromIntError) -> Self {
95        ShareError {
96            message: "It is not possible to create more than 256 shares".to_string(),
97        }
98    }
99}
100
101/// Hash recovery partner's public keys together with the lookup
102/// key to create a lookup key for each share
103fn generate_lookup_keys(lookup_key: Vec<u8>, public_keys: &Vec<[u8; 32]>) -> Vec<Vec<u8>> {
104    public_keys
105        .iter()
106        .map(|pk| {
107            let mut context = Blake2b::new_keyed(32, &pk.clone());
108            context.input(&lookup_key);
109            context.result().code().to_vec()
110        })
111        .collect()
112}
113
114/// Create an encrypted share payload link
115pub fn share(
116    public_keys: Vec<[u8; 32]>,
117    secret: Vec<u8>,
118    lookup_key: Vec<u8>,
119) -> Result<String, ShareError> {
120    let threshold = default_threshold(public_keys.len().try_into()?);
121    let lookup_keys = generate_lookup_keys(lookup_key, &public_keys);
122    let encrypted_share_set = share_and_encrypt_detached_nonce(public_keys, secret, threshold)?;
123    Ok(build_url(&parse_encrypted_shares(
124        encrypted_share_set,
125        lookup_keys,
126    )))
127}
128
129/// A plaintext share together with associated ciphertext
130#[derive(Debug)]
131pub struct RecoveredShare {
132    share: Vec<u8>,
133    ciphertext: Vec<u8>,
134}
135
136impl RecoveredShare {
137    pub fn from_concat(share_and_ciphertext: &Vec<u8>) -> Self {
138        // TODO handle the case where share_and_ciphertext.len() < SHARE_LENGTH
139        RecoveredShare {
140            share: (&share_and_ciphertext[..SHARE_LENGTH]).to_vec(),
141            ciphertext: (&share_and_ciphertext[SHARE_LENGTH..]).to_vec(),
142        }
143    }
144}
145
146impl fmt::Display for RecoveredShare {
147    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
148        write!(
149            f,
150            "{}{}",
151            hex::encode(&self.share),
152            hex::encode(&self.ciphertext)
153        )
154    }
155}
156
157/// Attempt recovery for a set of shares, handling the case that
158/// shares from different share sets are mixed together,
159/// and also handling duplicate shares.
160/// This will only return the first secret found. If there is more
161/// than one complete set, multiple secrets will not be recovered
162pub fn recover(recovered_shares: Vec<RecoveredShare>) -> Result<Vec<u8>, RecoveryError> {
163    let mut share_sets: HashMap<Vec<u8>, HashSet<Vec<u8>>> = HashMap::new();
164    let mut last_error = RecoveryError {
165        message: "No shares".to_string(),
166    };
167    for recovered_share in recovered_shares.iter() {
168        let share_set = share_sets
169            .entry(recovered_share.ciphertext.clone())
170            .or_insert(HashSet::new());
171        share_set.insert(recovered_share.share.clone());
172    }
173
174    // For each shareset with > 1 share, attempt recovery
175    for (ciphertext, share_set) in share_sets.iter() {
176        if share_set.len() > 1 {
177            // TODO this is probably not the best way to convert HashSet to vector
178            let share_set_vec: Vec<Vec<u8>> = share_set.into_iter().map(|s| s.clone()).collect();
179            // if let Ok(secret) = combine_authenticated(share_set_vec, ciphertext.to_vec()) {
180            //     return Ok(secret);
181            // }
182            match combine_authenticated(share_set_vec, ciphertext.to_vec()) {
183                Ok(secret) => return Ok(secret),
184                Err(error) => last_error = error,
185            }
186        }
187    }
188
189    Err(RecoveryError {
190        message: match share_sets.len() {
191            0 | 1 => last_error.message,
192            _ => format!(
193                "Shares from {} different sets. {}",
194                share_sets.len(),
195                last_error.message
196            ),
197        },
198    })
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn create_share_payload() {
207        let public_keys = vec![
208            "cf61ca2d786bc57cff2455003862ee70ca64d0dddb8b6ae0d1927e81411bdd21",
209            "d92d16240856d9fa136712942943e0869e7d7b93e8b3417ee4cf20946e042b0e",
210        ]
211        .iter()
212        .map(|pk| hex::decode(pk).unwrap().try_into().unwrap())
213        .collect();
214
215        // TODO parse the querystring rather than just checking its length
216        assert_eq!(
217            share(
218                public_keys,
219                b"the treasure is under the bed".to_vec(),
220                b"thirty two bytesthirty two bytes".to_vec()
221            )
222            .unwrap()
223            .len(),
224            593
225        )
226    }
227
228    #[test]
229    fn recover_secret() {
230        let original_secret = b"hello";
231        let (shares, ciphertext) = share_authenticated(&original_secret[..], 5, 3).unwrap();
232        let mut recovered_shares: Vec<RecoveredShare> = Vec::new();
233        for share in shares.iter() {
234            recovered_shares.push(RecoveredShare {
235                share: share.to_vec(),
236                ciphertext: ciphertext.clone(),
237            })
238        }
239        assert_eq!(recover(recovered_shares).unwrap(), b"hello");
240    }
241
242    #[test]
243    fn recover_secret_with_mixed_shares() {
244        let original_secret = b"hello";
245        let (shares, ciphertext) = share_authenticated(&original_secret[..], 5, 3).unwrap();
246
247        let another_secret = b"flip flop";
248        let (other_shares, other_ciphertext) =
249            share_authenticated(&another_secret[..], 5, 3).unwrap();
250
251        let mut recovered_shares: Vec<RecoveredShare> = Vec::new();
252        recovered_shares.push(RecoveredShare {
253            share: other_shares[1].to_vec(),
254            ciphertext: other_ciphertext.clone(),
255        });
256        for share in shares.iter().take(3) {
257            recovered_shares.push(RecoveredShare {
258                share: share.to_vec(),
259                ciphertext: ciphertext.clone(),
260            })
261        }
262        recovered_shares.push(RecoveredShare {
263            share: other_shares[2].to_vec(),
264            ciphertext: other_ciphertext.clone(),
265        });
266        assert_eq!(recover(recovered_shares).unwrap(), b"hello");
267    }
268
269    #[test]
270    fn recover_from_hex_strings() {
271        let recovered_share_strings = vec![
272            "015b8f00d950afdc48570f8d091eb6df04e0d7b08bfea5d8b1ed0c77ccebe705219f24e166cc5ad44ed5804c094783734e18ed12ac3169ab493c7bc96152f29800daba4c58360d886d5ff24d3f8d",
273            "03ad5048bda782df8bad8ca4978ff9c8f1031db045dc53dda176702883e84619b99f24e166cc5ad44ed5804c094783734e18ed12ac3169ab493c7bc96152f29800daba4c58360d886d5ff24d3f8d",
274            "04e4442cb6b5344904cf0b56a405683313f789299741f82858d986407f5a1f784d9f24e166cc5ad44ed5804c094783734e18ed12ac3169ab493c7bc96152f29800daba4c58360d886d5ff24d3f8d"
275        ];
276        let mut recovered_shares: Vec<RecoveredShare> = Vec::new();
277        for recovered_share_string in recovered_share_strings.iter() {
278            recovered_shares.push(RecoveredShare::from_concat(
279                &hex::decode(recovered_share_string).unwrap(),
280            ));
281        }
282        assert_eq!(recover(recovered_shares).unwrap(), b"hello");
283    }
284
285    #[test]
286    fn ignore_duplicate_share() {
287        let recovered_share_strings = vec![
288            "015b8f00d950afdc48570f8d091eb6df04e0d7b08bfea5d8b1ed0c77ccebe705219f24e166cc5ad44ed5804c094783734e18ed12ac3169ab493c7bc96152f29800daba4c58360d886d5ff24d3f8d",
289            "015b8f00d950afdc48570f8d091eb6df04e0d7b08bfea5d8b1ed0c77ccebe705219f24e166cc5ad44ed5804c094783734e18ed12ac3169ab493c7bc96152f29800daba4c58360d886d5ff24d3f8d",
290            "03ad5048bda782df8bad8ca4978ff9c8f1031db045dc53dda176702883e84619b99f24e166cc5ad44ed5804c094783734e18ed12ac3169ab493c7bc96152f29800daba4c58360d886d5ff24d3f8d",
291            "04e4442cb6b5344904cf0b56a405683313f789299741f82858d986407f5a1f784d9f24e166cc5ad44ed5804c094783734e18ed12ac3169ab493c7bc96152f29800daba4c58360d886d5ff24d3f8d"
292        ];
293        let mut recovered_shares: Vec<RecoveredShare> = Vec::new();
294        for recovered_share_string in recovered_share_strings.iter() {
295            recovered_shares.push(RecoveredShare::from_concat(
296                &hex::decode(recovered_share_string).unwrap(),
297            ));
298        }
299        assert_eq!(recover(recovered_shares).unwrap(), b"hello");
300    }
301}