1use alloc::string::{String, ToString};
40
41use bip39::{Language, Mnemonic};
42use hmac::{Hmac, KeyInit, Mac};
43use sha2::Sha256;
44use zeroize::Zeroizing;
45
46use crate::DeriveError;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
53#[non_exhaustive]
54pub enum Version {
55 #[default]
57 V1,
58}
59
60impl Version {
61 #[must_use]
63 pub const fn iterations(self) -> u32 {
64 match self {
65 Self::V1 => 600_000,
66 }
67 }
68
69 #[must_use]
71 pub const fn salt(self) -> &'static [u8] {
72 match self {
73 Self::V1 => b"kobe-mnemonic-camouflage-v1",
74 }
75 }
76}
77
78const MAX_ENTROPY_LEN: usize = 32;
80
81pub fn encrypt(phrase: &str, password: &str) -> Result<Zeroizing<String>, DeriveError> {
91 transform(Language::English, phrase, password, Version::default())
92}
93
94pub fn encrypt_in(
101 language: Language,
102 phrase: &str,
103 password: &str,
104) -> Result<Zeroizing<String>, DeriveError> {
105 transform(language, phrase, password, Version::default())
106}
107
108pub fn encrypt_with(
116 language: Language,
117 phrase: &str,
118 password: &str,
119 version: Version,
120) -> Result<Zeroizing<String>, DeriveError> {
121 transform(language, phrase, password, version)
122}
123
124pub fn decrypt(camouflaged: &str, password: &str) -> Result<Zeroizing<String>, DeriveError> {
134 transform(Language::English, camouflaged, password, Version::default())
135}
136
137pub fn decrypt_in(
144 language: Language,
145 camouflaged: &str,
146 password: &str,
147) -> Result<Zeroizing<String>, DeriveError> {
148 transform(language, camouflaged, password, Version::default())
149}
150
151pub fn decrypt_with(
159 language: Language,
160 camouflaged: &str,
161 password: &str,
162 version: Version,
163) -> Result<Zeroizing<String>, DeriveError> {
164 transform(language, camouflaged, password, version)
165}
166
167fn transform(
171 language: Language,
172 phrase: &str,
173 password: &str,
174 version: Version,
175) -> Result<Zeroizing<String>, DeriveError> {
176 if password.is_empty() {
177 return Err(DeriveError::Input(String::from(
178 "password must not be empty",
179 )));
180 }
181
182 let mnemonic = Mnemonic::parse_in(language, phrase)?;
183 let entropy = Zeroizing::new(mnemonic.to_entropy());
184 let entropy_len = entropy.len();
185
186 let key = derive_key(password, entropy_len, version)?;
187
188 let mut new_entropy = Zeroizing::new([0u8; MAX_ENTROPY_LEN]);
189 for (dst, (ent, kb)) in new_entropy
190 .iter_mut()
191 .zip(entropy.iter().zip(key.iter()))
192 .take(entropy_len)
193 {
194 *dst = ent ^ kb;
195 }
196
197 let new_mnemonic = Mnemonic::from_entropy_in(
198 language,
199 new_entropy.get(..entropy_len).ok_or_else(|| {
200 DeriveError::Crypto(String::from("camouflage: entropy truncation failed"))
201 })?,
202 )?;
203 Ok(Zeroizing::new(new_mnemonic.to_string()))
204}
205
206const HMAC_SHA256_LEN: usize = 32;
208
209#[inline]
213fn xor_buf(dest: &mut [u8], src: &[u8]) {
214 dest.iter_mut().zip(src).for_each(|(d, s)| *d ^= s);
215}
216
217fn pbkdf2_hmac_sha256(
231 password: &[u8],
232 salt: &[u8],
233 iterations: u32,
234 output: &mut [u8],
235) -> Result<(), DeriveError> {
236 let prf = Hmac::<Sha256>::new_from_slice(password)
237 .map_err(|_| DeriveError::Crypto(String::from("pbkdf2: HMAC key init failed")))?;
238
239 for (i, chunk) in output.chunks_mut(HMAC_SHA256_LEN).enumerate() {
240 chunk.fill(0);
241
242 let mut mac = prf.clone();
244 mac.update(salt);
245 let block_num = u32::try_from(i + 1)
246 .map_err(|_| DeriveError::Crypto(String::from("pbkdf2: block counter overflow")))?;
247 mac.update(&block_num.to_be_bytes());
248 let mut u = mac.finalize().into_bytes();
249 chunk.copy_from_slice(
250 u.get(..chunk.len()).ok_or_else(|| {
251 DeriveError::Crypto(String::from("pbkdf2: output buffer overrun"))
252 })?,
253 );
254
255 for _ in 1..iterations {
257 let mut inner_mac = prf.clone();
258 inner_mac.update(&u);
259 u = inner_mac.finalize().into_bytes();
260 xor_buf(chunk, &u);
261 }
262 }
263
264 Ok(())
265}
266
267fn derive_key(
272 password: &str,
273 len: usize,
274 version: Version,
275) -> Result<Zeroizing<[u8; MAX_ENTROPY_LEN]>, DeriveError> {
276 let mut key = Zeroizing::new([0u8; MAX_ENTROPY_LEN]);
277 pbkdf2_hmac_sha256(
278 password.as_bytes(),
279 version.salt(),
280 version.iterations(),
281 key.get_mut(..len).ok_or_else(|| {
282 DeriveError::Crypto(String::from("pbkdf2: key buffer truncation failed"))
283 })?,
284 )?;
285 Ok(key)
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 const TEST_12: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
293 const TEST_15: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon address";
294 const TEST_18: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent";
295 const TEST_21: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon admit";
296 const TEST_24: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";
297 const PASSWORD: &str = "my-secret-password-2024";
298
299 #[test]
300 fn roundtrip_24_words() {
301 let camouflaged = encrypt(TEST_24, PASSWORD).unwrap();
302
303 assert_ne!(camouflaged.as_str(), TEST_24);
305
306 assert!(Mnemonic::parse_in(Language::English, camouflaged.as_str()).is_ok());
308
309 let recovered = decrypt(&camouflaged, PASSWORD).unwrap();
311 assert_eq!(recovered.as_str(), TEST_24);
312 }
313
314 #[test]
315 fn roundtrip_12_words() {
316 let camouflaged = encrypt(TEST_12, PASSWORD).unwrap();
317 assert_ne!(camouflaged.as_str(), TEST_12);
318
319 let recovered = decrypt(&camouflaged, PASSWORD).unwrap();
320 assert_eq!(recovered.as_str(), TEST_12);
321 }
322
323 #[test]
324 fn roundtrip_15_words() {
325 let camouflaged = encrypt(TEST_15, PASSWORD).unwrap();
326 assert_ne!(camouflaged.as_str(), TEST_15);
327 assert!(Mnemonic::parse_in(Language::English, camouflaged.as_str()).is_ok());
328
329 let recovered = decrypt(&camouflaged, PASSWORD).unwrap();
330 assert_eq!(recovered.as_str(), TEST_15);
331 }
332
333 #[test]
334 fn roundtrip_18_words() {
335 let camouflaged = encrypt(TEST_18, PASSWORD).unwrap();
336 assert_ne!(camouflaged.as_str(), TEST_18);
337 assert!(Mnemonic::parse_in(Language::English, camouflaged.as_str()).is_ok());
338
339 let recovered = decrypt(&camouflaged, PASSWORD).unwrap();
340 assert_eq!(recovered.as_str(), TEST_18);
341 }
342
343 #[test]
344 fn roundtrip_21_words() {
345 let camouflaged = encrypt(TEST_21, PASSWORD).unwrap();
346 assert_ne!(camouflaged.as_str(), TEST_21);
347 assert!(Mnemonic::parse_in(Language::English, camouflaged.as_str()).is_ok());
348
349 let recovered = decrypt(&camouflaged, PASSWORD).unwrap();
350 assert_eq!(recovered.as_str(), TEST_21);
351 }
352
353 #[test]
354 fn different_passwords_produce_different_results() {
355 let c1 = encrypt(TEST_24, "password-alpha").unwrap();
356 let c2 = encrypt(TEST_24, "password-beta").unwrap();
357 assert_ne!(c1.as_str(), c2.as_str());
358 }
359
360 #[test]
361 fn wrong_password_does_not_recover() {
362 let camouflaged = encrypt(TEST_24, PASSWORD).unwrap();
363 let wrong = decrypt(&camouflaged, "wrong-password").unwrap();
364 assert_ne!(wrong.as_str(), TEST_24);
365 }
366
367 #[test]
368 fn deterministic_output() {
369 let c1 = encrypt(TEST_24, PASSWORD).unwrap();
370 let c2 = encrypt(TEST_24, PASSWORD).unwrap();
371 assert_eq!(c1.as_str(), c2.as_str());
372 }
373
374 #[test]
375 fn camouflaged_is_valid_mnemonic() {
376 let camouflaged = encrypt(TEST_24, PASSWORD).unwrap();
377 let wallet = crate::Wallet::from_mnemonic(&camouflaged, None);
378 assert!(
379 wallet.is_ok(),
380 "camouflaged mnemonic must produce a valid wallet"
381 );
382 }
383
384 #[test]
385 fn empty_password_rejected() {
386 let result = encrypt(TEST_24, "");
387 assert!(result.is_err());
388 }
389
390 #[test]
391 fn preserves_word_count() {
392 for (phrase, expected_words) in [
393 (TEST_12, 12),
394 (TEST_15, 15),
395 (TEST_18, 18),
396 (TEST_21, 21),
397 (TEST_24, 24),
398 ] {
399 let camouflaged = encrypt(phrase, PASSWORD).unwrap();
400 let word_count = camouflaged.split_whitespace().count();
401 assert_eq!(word_count, expected_words);
402 }
403 }
404
405 #[test]
409 fn pbkdf2_rfc7914_vector1() {
410 #[rustfmt::skip]
411 let expected: [u8; 64] = [
412 0x55, 0xac, 0x04, 0x6e, 0x56, 0xe3, 0x08, 0x9f,
413 0xec, 0x16, 0x91, 0xc2, 0x25, 0x44, 0xb6, 0x05,
414 0xf9, 0x41, 0x85, 0x21, 0x6d, 0xde, 0x04, 0x65,
415 0xe6, 0x8b, 0x9d, 0x57, 0xc2, 0x0d, 0xac, 0xbc,
416 0x49, 0xca, 0x9c, 0xcc, 0xf1, 0x79, 0xb6, 0x45,
417 0x99, 0x16, 0x64, 0xb3, 0x9d, 0x77, 0xef, 0x31,
418 0x7c, 0x71, 0xb8, 0x45, 0xb1, 0xe3, 0x0b, 0xd5,
419 0x09, 0x11, 0x20, 0x41, 0xd3, 0xa1, 0x97, 0x83,
420 ];
421 let mut dk = [0u8; 64];
422 pbkdf2_hmac_sha256(b"passwd", b"salt", 1, &mut dk).unwrap();
423 assert_eq!(dk, expected);
424 }
425
426 #[test]
428 fn pbkdf2_rfc7914_vector2() {
429 #[rustfmt::skip]
430 let expected: [u8; 64] = [
431 0x4d, 0xdc, 0xd8, 0xf6, 0x0b, 0x98, 0xbe, 0x21,
432 0x83, 0x0c, 0xee, 0x5e, 0xf2, 0x27, 0x01, 0xf9,
433 0x64, 0x1a, 0x44, 0x18, 0xd0, 0x4c, 0x04, 0x14,
434 0xae, 0xff, 0x08, 0x87, 0x6b, 0x34, 0xab, 0x56,
435 0xa1, 0xd4, 0x25, 0xa1, 0x22, 0x58, 0x33, 0x54,
436 0x9a, 0xdb, 0x84, 0x1b, 0x51, 0xc9, 0xb3, 0x17,
437 0x6a, 0x27, 0x2b, 0xde, 0xbb, 0xa1, 0xd0, 0x78,
438 0x47, 0x8f, 0x62, 0xb3, 0x97, 0xf3, 0x3c, 0x8d,
439 ];
440 let mut dk = [0u8; 64];
441 pbkdf2_hmac_sha256(b"Password", b"NaCl", 80_000, &mut dk).unwrap();
442 assert_eq!(dk, expected);
443 }
444
445 #[test]
447 fn pbkdf2_rfc7914_vector1_single_block() {
448 #[rustfmt::skip]
449 let expected: [u8; 32] = [
450 0x55, 0xac, 0x04, 0x6e, 0x56, 0xe3, 0x08, 0x9f,
451 0xec, 0x16, 0x91, 0xc2, 0x25, 0x44, 0xb6, 0x05,
452 0xf9, 0x41, 0x85, 0x21, 0x6d, 0xde, 0x04, 0x65,
453 0xe6, 0x8b, 0x9d, 0x57, 0xc2, 0x0d, 0xac, 0xbc,
454 ];
455 let mut dk = [0u8; 32];
456 pbkdf2_hmac_sha256(b"passwd", b"salt", 1, &mut dk).unwrap();
457 assert_eq!(dk, expected);
458 }
459
460 #[test]
462 fn pbkdf2_truncated_output() {
463 #[rustfmt::skip]
464 let expected: [u8; 20] = [
465 0x55, 0xac, 0x04, 0x6e, 0x56, 0xe3, 0x08, 0x9f,
466 0xec, 0x16, 0x91, 0xc2, 0x25, 0x44, 0xb6, 0x05,
467 0xf9, 0x41, 0x85, 0x21,
468 ];
469 let mut dk = [0u8; 20];
470 pbkdf2_hmac_sha256(b"passwd", b"salt", 1, &mut dk).unwrap();
471 assert_eq!(dk, expected);
472 }
473
474 #[test]
476 fn pbkdf2_empty_output() {
477 let mut dk = [0u8; 0];
478 pbkdf2_hmac_sha256(b"passwd", b"salt", 1, &mut dk).unwrap();
479 }
480}