Skip to main content

qp_human_checkphrase/
lib.rs

1use std::{
2	fs::File,
3	io::{self, BufRead, BufReader},
4	path::Path,
5};
6
7use pbkdf2::pbkdf2_hmac;
8use sha2::Sha256;
9
10// Constants
11const WORD_LIST_FILE: &str = "final_wordlist.txt";
12const WORD_COUNT: usize = 2048;
13const SALT: &str = "human-readable-checksum";
14const ITERATIONS: u32 = 40000;
15const CHECKSUM_LEN: usize = 5;
16const KEY_BYTECOUNT: usize = (CHECKSUM_LEN * 11).div_ceil(8);
17
18pub fn load_word_list() -> io::Result<Vec<String>> {
19	if !Path::new(WORD_LIST_FILE).exists() {
20		return Err(io::Error::new(
21			io::ErrorKind::NotFound,
22			format!("Word list file '{}' not found", WORD_LIST_FILE),
23		));
24	}
25
26	let file = File::open(WORD_LIST_FILE)?;
27	let reader = BufReader::new(file);
28	let words: Vec<String> = reader.lines().collect::<io::Result<_>>()?;
29
30	if words.len() != WORD_COUNT {
31		return Err(io::Error::new(
32			io::ErrorKind::InvalidData,
33			format!("Word list must contain exactly {} words, found {}", WORD_COUNT, words.len()),
34		));
35	}
36
37	println!("Loaded {} words from {}", words.len(), WORD_LIST_FILE);
38	Ok(words)
39}
40
41pub fn address_to_checksum(address: &str, word_list: &[String]) -> Vec<String> {
42	// PBKDF2-HMAC-SHA256
43	let mut key = [0u8; KEY_BYTECOUNT];
44	pbkdf2_hmac::<Sha256>(address.as_bytes(), SALT.as_bytes(), ITERATIONS, &mut key);
45	// println!("Key : {}", hex::encode(&key));
46
47	// Convert key bytes to a big integer
48	let mut key_int = 0u128; // Using u128 to handle larger checksum lengths
49	for &byte in key.iter().take(KEY_BYTECOUNT) {
50		key_int = (key_int << 8) | byte as u128;
51	}
52
53	// take only the first CHECKSUM_LEN * 11 bits
54	key_int >>= (8 * KEY_BYTECOUNT) % 11;
55	// println!("Key Int: {}", &key_int);
56	// key_int &= (1 << (CHECKSUM_LEN * 11)) - 1;
57
58	// Split into 11-bit indices
59	let mut indices = Vec::with_capacity(CHECKSUM_LEN);
60	for i in 0..CHECKSUM_LEN {
61		let shift = (CHECKSUM_LEN - 1 - i) * 11;
62		let index = ((key_int >> shift) & 0x7FF) as usize;
63		indices.push(index);
64	}
65	// println!("Indexes: {:?}", indices);
66
67	// Map to words
68	indices.iter().map(|&i| word_list[i].clone()).collect()
69}
70
71#[cfg(test)]
72mod tests {
73	use super::*;
74
75	#[test]
76	fn test_checksum() -> io::Result<()> {
77		let bip39_list = load_word_list()?;
78		let combos = (bip39_list.len() as u64).pow(CHECKSUM_LEN as u32);
79		println!("Total combos: {}", combos);
80		println!(
81			"Sample: {:?} ... {:?}",
82			&bip39_list[..CHECKSUM_LEN],
83			&bip39_list[bip39_list.len() - CHECKSUM_LEN..]
84		);
85
86		let test_addresses = [
87			"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", // Bitcoin (Satoshi's address)
88			"1A1zP1eP5QGefi2DMPTfTL5SLmv7DixfNa", // Poisoned version (one char different)
89			"0x742d35Cc6634C0532925a3b844Bc9e7595f5bE21", // Ethereum
90			"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", // Polkadot
91			"cosmos1hsk6jryyqjfhp5dhc55tc9jtckygx0eph6dd02", // Cosmos
92			"bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", // Bitcoin (bech32)
93			"qzk7h3xH4Fmv2RqKpN8sT5jW9cY6gB1dL3mX0vQwEaUoZrJtS", // Quantus
94			"qzkABCDEF123456789abcdefGHIJKLMNOPQRSTUVWXYZ000001", // Quantus 2
95			"qzkXyZ987654321FeDcBaAbCdEfGhIjKlMnOpQrStUvWxYz99", // Quantus 3
96		];
97		for addr in test_addresses {
98			let four_words = address_to_checksum(addr, &bip39_list);
99			println!("Address: {}", addr);
100			println!("Checksum: {}", four_words.join("-"));
101		}
102
103		Ok(())
104	}
105}