1use num_bigint::BigUint;
8use num_traits::cast::ToPrimitive;
9use thiserror::Error;
10
11use crate::{error::Result, wordlist::EFF_LONG_WORD_LIST, CryptoError};
12
13pub fn fingerprint(fingerprint_material: &str, public_key: &[u8]) -> Result<String> {
19 let hkdf =
20 hkdf::Hkdf::<sha2::Sha256>::from_prk(public_key).map_err(|_| CryptoError::InvalidKeyLen)?;
21
22 let mut user_fingerprint = [0u8; 32];
23 hkdf.expand(fingerprint_material.as_bytes(), &mut user_fingerprint)
24 .map_err(|_| CryptoError::InvalidKeyLen)?;
25
26 hash_word(user_fingerprint)
27}
28
29fn hash_word(hash: [u8; 32]) -> Result<String> {
31 let minimum_entropy = 64;
32
33 let entropy_per_word = (EFF_LONG_WORD_LIST.len() as f64).log2();
34 let num_words = ((minimum_entropy as f64 / entropy_per_word).ceil()).to_owned() as i64;
35
36 let hash_arr: Vec<u8> = hash.to_vec();
37 let entropy_available = hash_arr.len() * 4;
38 if num_words as f64 * entropy_per_word > entropy_available as f64 {
39 return Err(FingerprintError::EntropyTooSmall.into());
40 }
41
42 let mut phrase = Vec::new();
43
44 let mut hash_number = BigUint::from_bytes_be(&hash_arr);
45 for _ in 0..num_words {
46 let remainder = hash_number.clone() % EFF_LONG_WORD_LIST.len();
47 hash_number /= EFF_LONG_WORD_LIST.len();
48
49 let index = remainder
50 .to_usize()
51 .expect("Remainder is less than EFF_LONG_WORD_LIST.len()");
52 phrase.push(EFF_LONG_WORD_LIST[index].to_string());
53 }
54
55 Ok(phrase.join("-"))
56}
57
58#[derive(Debug, Error)]
59pub enum FingerprintError {
60 #[error("Entropy is too small")]
61 EntropyTooSmall,
62}
63
64#[cfg(test)]
65mod tests {
66 use super::fingerprint;
67
68 #[test]
69 fn test_fingerprint() {
70 let user_id = "a09726a0-9590-49d1-a5f5-afe300b6a515";
71 let key: &[u8] = &[
72 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15,
73 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 187, 38, 44, 241, 110, 205, 89, 253, 25, 191, 126,
74 84, 121, 202, 61, 223, 189, 244, 118, 212, 74, 139, 130, 97, 115, 164, 167, 106, 191,
75 188, 233, 218, 196, 250, 187, 146, 125, 160, 150, 49, 198, 224, 176, 10, 0, 143, 99,
76 230, 232, 160, 51, 104, 154, 211, 33, 80, 170, 4, 68, 80, 219, 115, 167, 114, 156, 227,
77 125, 193, 128, 123, 39, 254, 191, 124, 63, 129, 44, 63, 18, 56, 161, 48, 158, 0, 27,
78 146, 2, 99, 136, 75, 21, 135, 6, 118, 12, 26, 251, 184, 172, 249, 53, 78, 210, 46, 143,
79 17, 104, 202, 65, 173, 229, 219, 233, 144, 163, 101, 216, 238, 152, 54, 158, 1, 195,
80 50, 203, 21, 226, 12, 82, 170, 175, 170, 160, 21, 247, 248, 80, 97, 123, 0, 152, 116,
81 229, 126, 221, 199, 155, 194, 192, 51, 207, 177, 240, 160, 84, 241, 41, 88, 176, 53,
82 111, 28, 173, 177, 232, 158, 22, 79, 133, 152, 31, 32, 12, 196, 147, 58, 57, 50, 252,
83 208, 131, 150, 179, 132, 178, 150, 234, 251, 143, 125, 163, 144, 20, 46, 71, 168, 252,
84 164, 86, 120, 124, 56, 252, 206, 210, 236, 212, 139, 127, 189, 236, 40, 46, 2, 238, 13,
85 216, 40, 48, 85, 133, 229, 181, 155, 176, 217, 241, 154, 153, 213, 112, 222, 72, 219,
86 197, 3, 219, 56, 77, 109, 47, 72, 251, 131, 36, 240, 96, 169, 31, 82, 93, 166, 242, 3,
87 33, 213, 2, 3, 1, 0, 1,
88 ];
89
90 assert_eq!(
91 "turban-deftly-anime-chatroom-unselfish",
92 fingerprint(user_id, key).unwrap()
93 );
94 }
95}