1use 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#[derive(Serialize, Deserialize, Debug)]
28struct ShareOutput {
29 pk: String,
31 c: String,
33 s: Vec<String>,
35 lk: Vec<String>,
37}
38
39fn 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
56fn build_url(share_output: &ShareOutput) -> String {
58 format!(
59 "{}?{}",
60 DAPP_URL,
61 serde_qs::to_string(share_output).unwrap() )
63}
64
65#[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
101fn 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
114pub 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#[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 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
157pub 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 (ciphertext, share_set) in share_sets.iter() {
176 if share_set.len() > 1 {
177 let share_set_vec: Vec<Vec<u8>> = share_set.into_iter().map(|s| s.clone()).collect();
179 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 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}