1#![forbid(unsafe_code)]
2
3pub mod argon2_kdf;
16pub mod balloon;
17pub mod bcrypt_kdf;
18pub mod hkdf_label;
19pub mod kbkdf;
20pub mod pbkdf2_kdf;
21pub mod scrypt_kdf;
22pub mod stretcher;
23
24pub const PBKDF2_SHA256_MIN_ITERATIONS: u32 = 600_000;
31
32pub const PBKDF2_SHA512_MIN_ITERATIONS: u32 = 210_000;
38
39pub use argon2_kdf::{
40 argon2d_derive, argon2i_derive, argon2id_derive, argon2id_to_phc_string, argon2id_verify_phc,
41 Argon2Params, Argon2idHasher,
42};
43pub use balloon::{
44 balloon_sha256, balloon_sha256_secret, balloon_sha512, balloon_sha512_secret, BalloonHasher,
45 BalloonParams, BalloonVariant, BALLOON_DELTA,
46};
47pub use bcrypt_kdf::{bcrypt_hash, bcrypt_verify, BcryptHasher, BcryptParams};
48pub use hkdf_label::{hkdf_expand_label_sha256, hkdf_expand_label_sha384};
49pub use kbkdf::{
50 kbkdf_counter_hmac_sha256, kbkdf_counter_hmac_sha256_secret, kbkdf_counter_hmac_sha384,
51 kbkdf_counter_hmac_sha512,
52};
53pub use pbkdf2_kdf::{
54 pbkdf2_sha256, pbkdf2_sha512, Pbkdf2Params, Pbkdf2Sha256Hasher, Pbkdf2Sha512Hasher,
55};
56pub use scrypt_kdf::{scrypt_derive, ScryptHasher, ScryptParams};
57pub use stretcher::{
58 Argon2idStretchParams, BalloonStretchParams, KeyStretcher, Pbkdf2StretchParams,
59 ScryptStretchParams, StretchParams, Stretcher,
60};
61
62use hkdf::Hkdf;
63use oxicrypto_core::{CryptoError, Kdf, PasswordHash};
64use subtle::ConstantTimeEq;
65
66#[derive(Debug, Default, Clone, Copy)]
70pub struct HkdfSha256;
71
72impl Kdf for HkdfSha256 {
73 fn name(&self) -> &'static str {
74 "HKDF-SHA-256"
75 }
76 fn derive(
77 &self,
78 ikm: &[u8],
79 salt: &[u8],
80 info: &[u8],
81 okm_out: &mut [u8],
82 ) -> Result<(), CryptoError> {
83 if okm_out.is_empty() {
84 return Err(CryptoError::BadInput);
85 }
86 let salt_opt = if salt.is_empty() { None } else { Some(salt) };
87 let hk = Hkdf::<sha2::Sha256>::new(salt_opt, ikm);
88 hk.expand(info, okm_out)
89 .map_err(|_| CryptoError::Internal("HKDF expand failed (output too long)"))?;
90 Ok(())
91 }
92}
93
94#[derive(Debug, Default, Clone, Copy)]
98pub struct HkdfSha512;
99
100impl Kdf for HkdfSha512 {
101 fn name(&self) -> &'static str {
102 "HKDF-SHA-512"
103 }
104 fn derive(
105 &self,
106 ikm: &[u8],
107 salt: &[u8],
108 info: &[u8],
109 okm_out: &mut [u8],
110 ) -> Result<(), CryptoError> {
111 if okm_out.is_empty() {
112 return Err(CryptoError::BadInput);
113 }
114 let salt_opt = if salt.is_empty() { None } else { Some(salt) };
115 let hk = Hkdf::<sha2::Sha512>::new(salt_opt, ikm);
116 hk.expand(info, okm_out)
117 .map_err(|_| CryptoError::Internal("HKDF expand failed (output too long)"))?;
118 Ok(())
119 }
120}
121
122#[derive(Debug, Default, Clone, Copy)]
126pub struct HkdfSha384;
127
128impl Kdf for HkdfSha384 {
129 fn name(&self) -> &'static str {
130 "HKDF-SHA-384"
131 }
132 fn derive(
133 &self,
134 ikm: &[u8],
135 salt: &[u8],
136 info: &[u8],
137 okm_out: &mut [u8],
138 ) -> Result<(), CryptoError> {
139 if okm_out.is_empty() {
140 return Err(CryptoError::BadInput);
141 }
142 let salt_opt = if salt.is_empty() { None } else { Some(salt) };
143 let hk = Hkdf::<sha2::Sha384>::new(salt_opt, ikm);
144 hk.expand(info, okm_out)
145 .map_err(|_| CryptoError::Internal("HKDF-SHA-384 expand failed (output too long)"))?;
146 Ok(())
147 }
148}
149
150#[must_use]
159pub fn hkdf_sha256_extract(salt: &[u8], ikm: &[u8]) -> [u8; 32] {
160 let salt_opt = if salt.is_empty() { None } else { Some(salt) };
161 let (prk, _) = Hkdf::<sha2::Sha256>::extract(salt_opt, ikm);
162 let mut out = [0u8; 32];
163 out.copy_from_slice(&prk);
164 out
165}
166
167#[must_use = "HKDF expand result must be checked"]
172pub fn hkdf_sha256_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
173 if okm_out.is_empty() {
174 return Err(CryptoError::BadInput);
175 }
176 let hk = Hkdf::<sha2::Sha256>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
177 hk.expand(info, okm_out)
178 .map_err(|_| CryptoError::Internal("HKDF-SHA-256 expand failed (output too long)"))?;
179 Ok(())
180}
181
182#[must_use]
186pub fn hkdf_sha384_extract(salt: &[u8], ikm: &[u8]) -> [u8; 48] {
187 let salt_opt = if salt.is_empty() { None } else { Some(salt) };
188 let (prk, _) = Hkdf::<sha2::Sha384>::extract(salt_opt, ikm);
189 let mut out = [0u8; 48];
190 out.copy_from_slice(&prk);
191 out
192}
193
194#[must_use = "HKDF expand result must be checked"]
196pub fn hkdf_sha384_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
197 if okm_out.is_empty() {
198 return Err(CryptoError::BadInput);
199 }
200 let hk = Hkdf::<sha2::Sha384>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
201 hk.expand(info, okm_out)
202 .map_err(|_| CryptoError::Internal("HKDF-SHA-384 expand failed (output too long)"))?;
203 Ok(())
204}
205
206#[must_use]
210pub fn hkdf_sha512_extract(salt: &[u8], ikm: &[u8]) -> [u8; 64] {
211 let salt_opt = if salt.is_empty() { None } else { Some(salt) };
212 let (prk, _) = Hkdf::<sha2::Sha512>::extract(salt_opt, ikm);
213 let mut out = [0u8; 64];
214 out.copy_from_slice(&prk);
215 out
216}
217
218#[must_use = "HKDF expand result must be checked"]
220pub fn hkdf_sha512_expand(prk: &[u8], info: &[u8], okm_out: &mut [u8]) -> Result<(), CryptoError> {
221 if okm_out.is_empty() {
222 return Err(CryptoError::BadInput);
223 }
224 let hk = Hkdf::<sha2::Sha512>::from_prk(prk).map_err(|_| CryptoError::InvalidKey)?;
225 hk.expand(info, okm_out)
226 .map_err(|_| CryptoError::Internal("HKDF-SHA-512 expand failed (output too long)"))?;
227 Ok(())
228}
229
230#[must_use = "HKDF derive result must be checked"]
242pub fn hkdf_sha256_derive_to_vec(
243 ikm: &[u8],
244 salt: &[u8],
245 info: &[u8],
246 len: usize,
247) -> Result<Vec<u8>, CryptoError> {
248 if len == 0 {
249 return Err(CryptoError::BadInput);
250 }
251 const MAX_HKDF_SHA256: usize = 255 * 32;
254 if len > MAX_HKDF_SHA256 {
255 return Err(CryptoError::Internal(
256 "requested length exceeds HKDF-SHA-256 maximum (255 * 32)",
257 ));
258 }
259 let mut out = vec![0u8; len];
260 HkdfSha256.derive(ikm, salt, info, &mut out)?;
261 Ok(out)
262}
263
264#[must_use = "HKDF derive result must be checked"]
271pub fn hkdf_sha384_derive_to_vec(
272 ikm: &[u8],
273 salt: &[u8],
274 info: &[u8],
275 len: usize,
276) -> Result<Vec<u8>, CryptoError> {
277 if len == 0 {
278 return Err(CryptoError::BadInput);
279 }
280 const MAX_HKDF_SHA384: usize = 255 * 48;
283 if len > MAX_HKDF_SHA384 {
284 return Err(CryptoError::Internal(
285 "requested length exceeds HKDF-SHA-384 maximum (255 * 48)",
286 ));
287 }
288 let mut out = vec![0u8; len];
289 HkdfSha384.derive(ikm, salt, info, &mut out)?;
290 Ok(out)
291}
292
293#[must_use = "HKDF derive result must be checked"]
300pub fn hkdf_sha512_derive_to_vec(
301 ikm: &[u8],
302 salt: &[u8],
303 info: &[u8],
304 len: usize,
305) -> Result<Vec<u8>, CryptoError> {
306 if len == 0 {
307 return Err(CryptoError::BadInput);
308 }
309 const MAX_HKDF_SHA512: usize = 255 * 64;
312 if len > MAX_HKDF_SHA512 {
313 return Err(CryptoError::Internal(
314 "requested length exceeds HKDF-SHA-512 maximum (255 * 64)",
315 ));
316 }
317 let mut out = vec![0u8; len];
318 HkdfSha512.derive(ikm, salt, info, &mut out)?;
319 Ok(out)
320}
321
322#[must_use = "generated salt result must be checked"]
344pub fn generate_salt(rng: &mut oxicrypto_rand::OxiRng, len: usize) -> Result<Vec<u8>, CryptoError> {
345 if len == 0 {
346 return Err(CryptoError::BadInput);
347 }
348 let mut buf = vec![0u8; len];
349 oxicrypto_core::Rng::fill(rng, &mut buf)?;
350 Ok(buf)
351}
352
353#[must_use = "generated salt result must be checked"]
361pub fn generate_salt_16() -> Result<[u8; 16], CryptoError> {
362 let bytes = oxicrypto_rand::random_bytes(16)?;
363 let mut out = [0u8; 16];
364 out.copy_from_slice(&bytes);
365 Ok(out)
366}
367
368#[must_use = "generated salt result must be checked"]
376pub fn generate_salt_32() -> Result<[u8; 32], CryptoError> {
377 let bytes = oxicrypto_rand::random_bytes(32)?;
378 let mut out = [0u8; 32];
379 out.copy_from_slice(&bytes);
380 Ok(out)
381}
382
383#[must_use = "password verification result must be checked"]
412pub fn verify_password<H>(
413 hasher: &H,
414 password: &[u8],
415 salt: &[u8],
416 expected: &[u8],
417) -> Result<(), CryptoError>
418where
419 H: PasswordHash,
420{
421 if expected.is_empty() {
422 return Err(CryptoError::BadInput);
423 }
424
425 let mut computed = vec![0u8; expected.len()];
429
430 #[derive(Debug)]
432 struct NullParams;
433 impl oxicrypto_core::PasswordHashParams for NullParams {
434 fn memory_cost(&self) -> Option<u32> {
435 None
436 }
437 fn time_cost(&self) -> Option<u32> {
438 None
439 }
440 fn parallelism(&self) -> Option<u32> {
441 None
442 }
443 }
444
445 hasher.hash_password(password, salt, &NullParams, &mut computed)?;
446
447 let ok: bool = computed.ct_eq(expected).into();
449 if ok {
450 Ok(())
451 } else {
452 Err(CryptoError::InvalidTag)
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 fn hex_decode(s: &str) -> Vec<u8> {
461 (0..s.len())
462 .step_by(2)
463 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
464 .collect()
465 }
466
467 #[test]
475 fn hkdf_sha256_rfc5869_tc1() {
476 let ikm = hex_decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
477 let salt = hex_decode("000102030405060708090a0b0c");
478 let info = hex_decode("f0f1f2f3f4f5f6f7f8f9");
479 let expected = hex_decode(
480 "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
481 );
482
483 let kdf = HkdfSha256;
484 let mut okm = vec![0u8; 42];
485 kdf.derive(&ikm, &salt, &info, &mut okm)
486 .expect("HKDF-SHA-256 RFC5869 TC1 failed");
487 assert_eq!(okm, expected, "HKDF-SHA-256 RFC5869 TC1 mismatch");
488 }
489
490 #[test]
491 fn hkdf_sha256_empty_salt() {
492 let kdf = HkdfSha256;
494 let mut okm = [0u8; 32];
495 kdf.derive(b"input key material", b"", b"info", &mut okm)
496 .expect("HKDF with empty salt failed");
497 assert_ne!(okm, [0u8; 32]);
498 }
499
500 #[test]
501 fn hkdf_sha512_round_trip() {
502 let kdf = HkdfSha512;
503 let mut okm1 = [0u8; 64];
504 let mut okm2 = [0u8; 64];
505 kdf.derive(b"secret", b"salt", b"info", &mut okm1).unwrap();
506 kdf.derive(b"secret", b"salt", b"info", &mut okm2).unwrap();
507 assert_eq!(okm1, okm2, "HKDF-SHA-512 must be deterministic");
508 assert_ne!(okm1, [0u8; 64]);
509 }
510
511 #[test]
512 fn hkdf_empty_output_errors() {
513 let kdf = HkdfSha256;
514 let result = kdf.derive(b"ikm", b"salt", b"info", &mut []);
515 assert_eq!(result, Err(CryptoError::BadInput));
516 }
517
518 #[test]
521 fn hkdf_sha384_round_trip() {
522 let kdf = HkdfSha384;
523 let mut okm1 = [0u8; 48];
524 let mut okm2 = [0u8; 48];
525 kdf.derive(b"secret", b"salt", b"info", &mut okm1)
526 .expect("derive 1 failed");
527 kdf.derive(b"secret", b"salt", b"info", &mut okm2)
528 .expect("derive 2 failed");
529 assert_eq!(okm1, okm2, "HKDF-SHA-384 must be deterministic");
530 assert_ne!(okm1, [0u8; 48]);
531 }
532
533 #[test]
534 fn hkdf_sha384_empty_output_errors() {
535 let kdf = HkdfSha384;
536 let result = kdf.derive(b"ikm", b"salt", b"info", &mut []);
537 assert_eq!(result, Err(CryptoError::BadInput));
538 }
539
540 #[test]
543 fn hkdf_sha256_extract_expand_equivalent() {
544 let ikm = b"input key material";
546 let salt = b"salt value";
547 let info = b"info";
548
549 let kdf = HkdfSha256;
551 let mut okm_full = [0u8; 42];
552 kdf.derive(ikm, salt, info, &mut okm_full)
553 .expect("full derive failed");
554
555 let prk = hkdf_sha256_extract(salt, ikm);
557 let mut okm_sep = [0u8; 42];
558 hkdf_sha256_expand(&prk, info, &mut okm_sep).expect("expand failed");
559
560 assert_eq!(okm_full, okm_sep, "Extract+Expand must equal full derive");
561 }
562
563 #[test]
564 fn hkdf_sha384_extract_expand_round_trip() {
565 let prk = hkdf_sha384_extract(b"salt", b"ikm");
566 assert_eq!(prk.len(), 48);
567 let mut okm = [0u8; 32];
568 hkdf_sha384_expand(&prk, b"info", &mut okm).expect("expand failed");
569 assert_ne!(okm, [0u8; 32]);
570 }
571
572 #[test]
573 fn hkdf_sha512_extract_expand_round_trip() {
574 let prk = hkdf_sha512_extract(b"salt", b"ikm");
575 assert_eq!(prk.len(), 64);
576 let mut okm = [0u8; 64];
577 hkdf_sha512_expand(&prk, b"info", &mut okm).expect("expand failed");
578 assert_ne!(okm, [0u8; 64]);
579 }
580
581 #[test]
582 fn hkdf_expand_empty_output_errors() {
583 let prk = hkdf_sha256_extract(b"salt", b"ikm");
584 let result = hkdf_sha256_expand(&prk, b"info", &mut []);
585 assert_eq!(result, Err(CryptoError::BadInput));
586 }
587
588 const VERIFY_SALT: &[u8] = b"0123456789abcdef"; #[test]
593 fn verify_password_argon2id_correct() {
594 let hasher = Argon2idHasher::new(Argon2Params::TEST_PARAMS);
595 let mut expected = [0u8; 32];
596 hasher
597 .hash_password(b"password", VERIFY_SALT, &hasher.params, &mut expected)
598 .expect("hash");
599 verify_password(&hasher, b"password", VERIFY_SALT, &expected)
600 .expect("correct password must pass");
601 }
602
603 #[test]
604 fn verify_password_argon2id_wrong_password() {
605 let hasher = Argon2idHasher::new(Argon2Params::TEST_PARAMS);
606 let mut expected = [0u8; 32];
607 hasher
608 .hash_password(b"password", VERIFY_SALT, &hasher.params, &mut expected)
609 .expect("hash");
610 let result = verify_password(&hasher, b"wrongpassword", VERIFY_SALT, &expected);
611 assert_eq!(result, Err(CryptoError::InvalidTag));
612 }
613
614 #[test]
615 fn verify_password_pbkdf2_correct() {
616 let hasher = Pbkdf2Sha256Hasher::new(1_000);
617 let mut expected = [0u8; 32];
618 hasher
619 .hash_password(b"mypassword", VERIFY_SALT, &hasher.params(), &mut expected)
620 .expect("hash");
621 verify_password(&hasher, b"mypassword", VERIFY_SALT, &expected)
622 .expect("correct password must pass");
623 }
624
625 #[test]
626 fn verify_password_pbkdf2_wrong_password() {
627 let hasher = Pbkdf2Sha256Hasher::new(1_000);
628 let mut expected = [0u8; 32];
629 hasher
630 .hash_password(b"mypassword", VERIFY_SALT, &hasher.params(), &mut expected)
631 .expect("hash");
632 let result = verify_password(&hasher, b"notmypassword", VERIFY_SALT, &expected);
633 assert_eq!(result, Err(CryptoError::InvalidTag));
634 }
635
636 #[test]
637 fn verify_password_empty_expected_errors() {
638 let hasher = Pbkdf2Sha256Hasher::new(1_000);
639 let result = verify_password(&hasher, b"password", VERIFY_SALT, &[]);
640 assert_eq!(result, Err(CryptoError::BadInput));
641 }
642
643 #[test]
646 fn generate_salt_variable_returns_correct_length() {
647 let mut rng = oxicrypto_rand::OxiRng::new().expect("OxiRng::new");
648 for len in [8usize, 16, 32, 64] {
649 let salt = generate_salt(&mut rng, len).expect("generate_salt");
650 assert_eq!(
651 salt.len(),
652 len,
653 "salt length must equal requested len {len}"
654 );
655 }
656 }
657
658 #[test]
659 fn generate_salt_zero_len_errors() {
660 let mut rng = oxicrypto_rand::OxiRng::new().expect("OxiRng::new");
661 let result = generate_salt(&mut rng, 0);
662 assert_eq!(result, Err(CryptoError::BadInput));
663 }
664
665 #[test]
666 fn generate_salt_produces_distinct_outputs() {
667 let mut rng = oxicrypto_rand::OxiRng::new().expect("OxiRng::new");
668 let s1 = generate_salt(&mut rng, 32).expect("salt 1");
669 let s2 = generate_salt(&mut rng, 32).expect("salt 2");
670 assert_ne!(s1, s2, "generate_salt must return distinct salts");
672 }
673
674 #[test]
677 fn pbkdf2_sha256_zero_iterations_returns_bad_input() {
678 let mut out = [0u8; 32];
679 let result = pbkdf2_sha256(b"password", b"saltsalt", 0, &mut out);
680 assert_eq!(
681 result,
682 Err(CryptoError::BadInput),
683 "0 iterations must be rejected"
684 );
685 }
686
687 #[test]
688 fn pbkdf2_sha512_zero_iterations_returns_bad_input() {
689 let mut out = [0u8; 64];
690 let result = pbkdf2_sha512(b"password", b"saltsalt", 0, &mut out);
691 assert_eq!(
692 result,
693 Err(CryptoError::BadInput),
694 "0 iterations must be rejected"
695 );
696 }
697
698 #[test]
701 fn argon2id_salt_too_short_returns_bad_input() {
702 let params = Argon2Params::TEST_PARAMS;
703 let mut out = [0u8; 32];
704 let result = argon2_kdf::argon2id_derive(b"password", b"tooshrt", params, &mut out);
706 assert_eq!(
707 result,
708 Err(CryptoError::BadInput),
709 "7-byte salt must be rejected (minimum is 8)"
710 );
711 }
712
713 #[test]
714 fn argon2id_empty_salt_returns_bad_input() {
715 let params = Argon2Params::TEST_PARAMS;
716 let mut out = [0u8; 32];
717 let result = argon2_kdf::argon2id_derive(b"password", b"", params, &mut out);
718 assert_eq!(
719 result,
720 Err(CryptoError::BadInput),
721 "empty salt must be rejected"
722 );
723 }
724
725 #[test]
728 fn hkdf_sha256_output_exceeding_max_errors() {
729 let kdf = HkdfSha256;
732 let max = 255 * 32 + 1; let mut out = vec![0u8; max];
734 let result = kdf.derive(b"ikm", b"salt", b"info", &mut out);
735 assert!(
736 result.is_err(),
737 "HKDF-SHA-256 derive must fail when output > 255 * HashLen"
738 );
739 }
740
741 #[test]
742 fn hkdf_sha512_output_exceeding_max_errors() {
743 let kdf = HkdfSha512;
745 let max = 255 * 64 + 1;
746 let mut out = vec![0u8; max];
747 let result = kdf.derive(b"ikm", b"salt", b"info", &mut out);
748 assert!(
749 result.is_err(),
750 "HKDF-SHA-512 derive must fail when output > 255 * HashLen"
751 );
752 }
753
754 #[test]
757 fn scrypt_log_n_64_rejected() {
758 let result = scrypt_kdf::ScryptParams::new(64, 8, 1);
760 assert!(result.is_err(), "log_n=64 must be rejected");
761 }
762
763 #[test]
764 fn scrypt_zero_r_rejected() {
765 let result = scrypt_kdf::ScryptParams::new(14, 0, 1);
767 assert!(result.is_err(), "r=0 must be rejected by ScryptParams::new");
768 }
769
770 #[test]
773 fn prop_kdf_hkdf_sha256_is_deterministic() {
774 let kdf = HkdfSha256;
775 let mut out1 = [0u8; 32];
776 let mut out2 = [0u8; 32];
777 kdf.derive(b"ikm", b"salt", b"info", &mut out1)
778 .expect("derive1");
779 kdf.derive(b"ikm", b"salt", b"info", &mut out2)
780 .expect("derive2");
781 assert_eq!(out1, out2, "HKDF-SHA-256 must be deterministic");
782 }
783
784 #[test]
785 fn prop_kdf_hkdf_sha384_is_deterministic() {
786 let kdf = HkdfSha384;
787 let mut out1 = [0u8; 48];
788 let mut out2 = [0u8; 48];
789 kdf.derive(b"ikm", b"salt", b"info", &mut out1)
790 .expect("derive1");
791 kdf.derive(b"ikm", b"salt", b"info", &mut out2)
792 .expect("derive2");
793 assert_eq!(out1, out2, "HKDF-SHA-384 must be deterministic");
794 }
795
796 #[test]
799 fn prop_kdf_different_salts_produce_different_outputs() {
800 let kdf = HkdfSha256;
801 let mut out1 = [0u8; 32];
802 let mut out2 = [0u8; 32];
803 kdf.derive(b"ikm", b"salt_a", b"info", &mut out1)
804 .expect("derive salt_a");
805 kdf.derive(b"ikm", b"salt_b", b"info", &mut out2)
806 .expect("derive salt_b");
807 assert_ne!(
808 out1, out2,
809 "different salts must produce different HKDF outputs"
810 );
811 }
812
813 #[test]
814 fn prop_pbkdf2_different_salts_produce_different_outputs() {
815 let mut out1 = [0u8; 32];
816 let mut out2 = [0u8; 32];
817 pbkdf2_sha256(b"password", b"salt_aaa", 1000, &mut out1).expect("pbkdf2 a");
818 pbkdf2_sha256(b"password", b"salt_bbb", 1000, &mut out2).expect("pbkdf2 b");
819 assert_ne!(
820 out1, out2,
821 "different salts must produce different PBKDF2 outputs"
822 );
823 }
824}