Coverage Report

Created: 2026-07-13 08:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/bcrypt-0.18.0/src/lib.rs
Line
Count
Source
1
//! Easily hash and verify passwords using bcrypt
2
#![forbid(unsafe_code)]
3
#![cfg_attr(not(feature = "std"), no_std)]
4
5
#[cfg(any(feature = "alloc", feature = "std", test))]
6
extern crate alloc;
7
8
#[cfg(any(feature = "alloc", feature = "std", test))]
9
use alloc::{
10
    string::{String, ToString},
11
    vec::Vec,
12
};
13
14
#[cfg(feature = "zeroize")]
15
use zeroize::Zeroize;
16
17
use base64::{alphabet::BCRYPT, engine::GeneralPurpose, engine::general_purpose::NO_PAD};
18
use core::fmt;
19
#[cfg(any(feature = "alloc", feature = "std"))]
20
use {base64::Engine, core::convert::AsRef, core::str::FromStr};
21
22
mod bcrypt;
23
mod errors;
24
25
pub use crate::bcrypt::bcrypt;
26
pub use crate::errors::{BcryptError, BcryptResult};
27
28
// Cost constants
29
const MIN_COST: u32 = 4;
30
const MAX_COST: u32 = 31;
31
pub const DEFAULT_COST: u32 = 12;
32
pub const BASE_64: GeneralPurpose = GeneralPurpose::new(&BCRYPT, NO_PAD);
33
34
#[cfg(any(feature = "alloc", feature = "std"))]
35
#[derive(Debug, PartialEq, Eq)]
36
/// A bcrypt hash result before concatenating
37
pub struct HashParts {
38
    cost: u32,
39
    salt: String,
40
    hash: String,
41
}
42
43
#[derive(Clone, Debug)]
44
/// BCrypt hash version
45
/// https://en.wikipedia.org/wiki/Bcrypt#Versioning_history
46
pub enum Version {
47
    TwoA,
48
    TwoX,
49
    TwoY,
50
    TwoB,
51
}
52
53
#[cfg(any(feature = "alloc", feature = "std"))]
54
impl HashParts {
55
    /// Creates the bcrypt hash string from all its parts
56
0
    fn format(&self) -> String {
57
0
        self.format_for_version(Version::TwoB)
58
0
    }
59
60
    /// Get the bcrypt hash cost
61
0
    pub fn get_cost(&self) -> u32 {
62
0
        self.cost
63
0
    }
64
65
    /// Get the bcrypt hash salt
66
0
    pub fn get_salt(&self) -> String {
67
0
        self.salt.clone()
68
0
    }
69
70
    /// Creates the bcrypt hash string from all its part, allowing to customize the version.
71
0
    pub fn format_for_version(&self, version: Version) -> String {
72
        // Cost need to have a length of 2 so padding with a 0 if cost < 10
73
0
        alloc::format!("${}${:02}${}{}", version, self.cost, self.salt, self.hash)
74
0
    }
75
}
76
77
#[cfg(any(feature = "alloc", feature = "std"))]
78
impl FromStr for HashParts {
79
    type Err = BcryptError;
80
81
0
    fn from_str(s: &str) -> Result<Self, Self::Err> {
82
0
        split_hash(s)
83
0
    }
84
}
85
86
#[cfg(any(feature = "alloc", feature = "std"))]
87
impl fmt::Display for HashParts {
88
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89
0
        write!(f, "{}", self.format())
90
0
    }
91
}
92
93
impl fmt::Display for Version {
94
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95
0
        let str = match self {
96
0
            Version::TwoA => "2a",
97
0
            Version::TwoB => "2b",
98
0
            Version::TwoX => "2x",
99
0
            Version::TwoY => "2y",
100
        };
101
0
        write!(f, "{}", str)
102
0
    }
103
}
104
105
/// The main meat: actually does the hashing and does some verification with
106
/// the cost to ensure it's a correct one. If err_on_truncation, this method will return
107
/// `BcryptError::Truncation`; otherwise it will truncate the password.
108
#[cfg(any(feature = "alloc", feature = "std"))]
109
0
fn _hash_password(
110
0
    password: &[u8],
111
0
    cost: u32,
112
0
    salt: [u8; 16],
113
0
    err_on_truncation: bool,
114
0
) -> BcryptResult<HashParts> {
115
0
    if !(MIN_COST..=MAX_COST).contains(&cost) {
116
0
        return Err(BcryptError::CostNotAllowed(cost));
117
0
    }
118
119
    // Passwords need to be null terminated
120
0
    let mut vec = Vec::with_capacity(password.len() + 1);
121
0
    vec.extend_from_slice(password);
122
0
    vec.push(0);
123
    // We only consider the first 72 chars; truncate if necessary.
124
    // `bcrypt` below will panic if len > 72
125
0
    let truncated = if vec.len() > 72 {
126
0
        if err_on_truncation {
127
0
            return Err(BcryptError::Truncation(vec.len()));
128
0
        }
129
0
        &vec[..72]
130
    } else {
131
0
        &vec
132
    };
133
134
0
    let output = bcrypt::bcrypt(cost, salt, truncated);
135
136
    #[cfg(feature = "zeroize")]
137
0
    vec.zeroize();
138
139
0
    Ok(HashParts {
140
0
        cost,
141
0
        salt: BASE_64.encode(salt),
142
0
        hash: BASE_64.encode(&output[..23]), // remember to remove the last byte
143
0
    })
144
0
}
145
146
/// Takes a full hash and split it into 3 parts:
147
/// cost, salt and hash
148
#[cfg(any(feature = "alloc", feature = "std"))]
149
0
fn split_hash(hash: &str) -> BcryptResult<HashParts> {
150
0
    let mut parts = HashParts {
151
0
        cost: 0,
152
0
        salt: "".to_string(),
153
0
        hash: "".to_string(),
154
0
    };
155
156
    // Should be [prefix, cost, hash]
157
0
    let raw_parts: Vec<_> = hash.split('$').filter(|s| !s.is_empty()).collect();
158
159
0
    if raw_parts.len() != 3 {
160
0
        return Err(BcryptError::InvalidHash("the hash format is malformed"));
161
0
    }
162
163
0
    if raw_parts[0] != "2y" && raw_parts[0] != "2b" && raw_parts[0] != "2a" && raw_parts[0] != "2x"
164
    {
165
0
        return Err(BcryptError::InvalidHash(
166
0
            "the hash prefix is not a bcrypt prefix",
167
0
        ));
168
0
    }
169
170
0
    if let Ok(c) = raw_parts[1].parse::<u32>() {
171
0
        parts.cost = c;
172
0
    } else {
173
0
        return Err(BcryptError::InvalidHash("the cost value is not a number"));
174
    }
175
176
0
    if raw_parts[2].len() == 53 && raw_parts[2].is_char_boundary(22) {
177
0
        parts.salt = raw_parts[2][..22].chars().collect();
178
0
        parts.hash = raw_parts[2][22..].chars().collect();
179
0
    } else {
180
0
        return Err(BcryptError::InvalidHash("the hash format is malformed"));
181
    }
182
183
0
    Ok(parts)
184
0
}
185
186
/// Generates a password hash using the cost given.
187
/// The salt is generated randomly using the OS randomness
188
#[cfg(any(feature = "alloc", feature = "std"))]
189
0
pub fn hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> {
190
0
    hash_with_result(password, cost).map(|r| r.format())
Unexecuted instantiation: bcrypt::hash::<alloc::string::String>::{closure#0}
Unexecuted instantiation: bcrypt::hash::<_>::{closure#0}
191
0
}
Unexecuted instantiation: bcrypt::hash::<alloc::string::String>
Unexecuted instantiation: bcrypt::hash::<_>
192
193
/// Generates a password hash using the cost given.
194
/// The salt is generated randomly using the OS randomness
195
/// Will return BcryptError::Truncation if password is longer than 72 bytes
196
#[cfg(any(feature = "alloc", feature = "std"))]
197
0
pub fn non_truncating_hash<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<String> {
198
0
    non_truncating_hash_with_result(password, cost).map(|r| r.format())
199
0
}
200
201
/// Generates a password hash using the cost given.
202
/// The salt is generated randomly using the OS randomness.
203
/// The function returns a result structure and allows to format the hash in different versions.
204
#[cfg(any(feature = "alloc", feature = "std"))]
205
0
pub fn hash_with_result<P: AsRef<[u8]>>(password: P, cost: u32) -> BcryptResult<HashParts> {
206
0
    let salt = {
207
0
        let mut s = [0u8; 16];
208
0
        getrandom::fill(&mut s).map(|_| s)
209
0
    }?;
210
211
0
    _hash_password(password.as_ref(), cost, salt, false)
212
0
}
Unexecuted instantiation: bcrypt::hash_with_result::<alloc::string::String>
Unexecuted instantiation: bcrypt::hash_with_result::<_>
213
214
/// Generates a password hash using the cost given.
215
/// The salt is generated randomly using the OS randomness.
216
/// The function returns a result structure and allows to format the hash in different versions.
217
/// Will return BcryptError::Truncation if password is longer than 72 bytes
218
#[cfg(any(feature = "alloc", feature = "std"))]
219
0
pub fn non_truncating_hash_with_result<P: AsRef<[u8]>>(
220
0
    password: P,
221
0
    cost: u32,
222
0
) -> BcryptResult<HashParts> {
223
0
    let salt = {
224
0
        let mut s = [0u8; 16];
225
0
        getrandom::fill(&mut s).map(|_| s)
226
0
    }?;
227
228
0
    _hash_password(password.as_ref(), cost, salt, true)
229
0
}
230
231
/// Generates a password given a hash and a cost.
232
/// The function returns a result structure and allows to format the hash in different versions.
233
#[cfg(any(feature = "alloc", feature = "std"))]
234
0
pub fn hash_with_salt<P: AsRef<[u8]>>(
235
0
    password: P,
236
0
    cost: u32,
237
0
    salt: [u8; 16],
238
0
) -> BcryptResult<HashParts> {
239
0
    _hash_password(password.as_ref(), cost, salt, false)
240
0
}
241
242
/// Generates a password given a hash and a cost.
243
/// The function returns a result structure and allows to format the hash in different versions.
244
/// Will return BcryptError::Truncation if password is longer than 72 bytes
245
#[cfg(any(feature = "alloc", feature = "std"))]
246
0
pub fn non_truncating_hash_with_salt<P: AsRef<[u8]>>(
247
0
    password: P,
248
0
    cost: u32,
249
0
    salt: [u8; 16],
250
0
) -> BcryptResult<HashParts> {
251
0
    _hash_password(password.as_ref(), cost, salt, true)
252
0
}
253
254
/// Verify the password against the hash by extracting the salt from the hash and recomputing the
255
/// hash from the password. If `err_on_truncation` is set to true, then this method will return
256
/// `BcryptError::Truncation`.
257
#[cfg(any(feature = "alloc", feature = "std"))]
258
0
fn _verify<P: AsRef<[u8]>>(password: P, hash: &str, err_on_truncation: bool) -> BcryptResult<bool> {
259
    use subtle::ConstantTimeEq;
260
261
0
    let parts = split_hash(hash)?;
262
0
    let salt = BASE_64
263
0
        .decode(&parts.salt)
264
0
        .map_err(|_| BcryptError::InvalidHash("the salt part is not valid base64"))?;
265
0
    let generated = _hash_password(
266
0
        password.as_ref(),
267
0
        parts.cost,
268
0
        salt.try_into()
269
0
            .map_err(|_| BcryptError::InvalidHash("the salt length is not 16 bytes"))?,
270
0
        err_on_truncation,
271
0
    )?;
272
0
    let source_decoded = BASE_64
273
0
        .decode(parts.hash)
274
0
        .map_err(|_| BcryptError::InvalidHash("the hash to verify against is not valid base64"))?;
275
0
    let generated_decoded = BASE_64.decode(generated.hash).map_err(|_| {
276
0
        BcryptError::InvalidHash("the generated hash for the password is not valid base64")
277
0
    })?;
Unexecuted instantiation: bcrypt::_verify::<alloc::string::String>::{closure#3}
Unexecuted instantiation: bcrypt::_verify::<_>::{closure#3}
278
279
0
    Ok(source_decoded.ct_eq(&generated_decoded).into())
280
0
}
Unexecuted instantiation: bcrypt::_verify::<alloc::string::String>
Unexecuted instantiation: bcrypt::_verify::<_>
281
282
/// Verify that a password is equivalent to the hash provided
283
#[cfg(any(feature = "alloc", feature = "std"))]
284
0
pub fn verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> {
285
0
    _verify(password, hash, false)
286
0
}
Unexecuted instantiation: bcrypt::verify::<alloc::string::String>
Unexecuted instantiation: bcrypt::verify::<_>
287
288
/// Verify that a password is equivalent to the hash provided.
289
/// Only use this if you are only using `non_truncating_hash` to generate the hash.
290
/// It will return an error for inputs that will work if generated from other sources.
291
#[cfg(any(feature = "alloc", feature = "std"))]
292
0
pub fn non_truncating_verify<P: AsRef<[u8]>>(password: P, hash: &str) -> BcryptResult<bool> {
293
0
    _verify(password, hash, true)
294
0
}
295
296
#[cfg(all(test, any(feature = "alloc", feature = "std")))]
297
mod tests {
298
    use crate::non_truncating_hash;
299
300
    use super::{
301
        _hash_password, BcryptError, BcryptResult, DEFAULT_COST, HashParts, Version,
302
        alloc::{
303
            string::{String, ToString},
304
            vec,
305
            vec::Vec,
306
        },
307
        hash, hash_with_salt, non_truncating_verify, split_hash, verify,
308
    };
309
    use core::convert::TryInto;
310
    use core::iter;
311
    use core::str::FromStr;
312
    use quickcheck::{TestResult, quickcheck};
313
314
    #[test]
315
    fn can_split_hash() {
316
        let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
317
        let output = split_hash(hash).unwrap();
318
        let expected = HashParts {
319
            cost: 12,
320
            salt: "L6Bc/AlTQHyd9liGgGEZyO".to_string(),
321
            hash: "FLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u".to_string(),
322
        };
323
        assert_eq!(output, expected);
324
    }
325
326
    #[test]
327
    fn can_output_cost_and_salt_from_parsed_hash() {
328
        let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
329
        let parsed = HashParts::from_str(hash).unwrap();
330
        assert_eq!(parsed.get_cost(), 12);
331
        assert_eq!(parsed.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO".to_string());
332
    }
333
334
    #[test]
335
    fn returns_an_error_if_a_parsed_hash_is_baddly_formated() {
336
        let hash1 = "$2y$12$L6Bc/AlTQHyd9lGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
337
        assert!(HashParts::from_str(hash1).is_err());
338
339
        let hash2 = "!2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
340
        assert!(HashParts::from_str(hash2).is_err());
341
342
        let hash3 = "$2y$-12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u";
343
        assert!(HashParts::from_str(hash3).is_err());
344
    }
345
346
    #[test]
347
    fn can_verify_hash_generated_from_some_online_tool() {
348
        let hash = "$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96";
349
        assert!(verify("password", hash).unwrap());
350
    }
351
352
    #[test]
353
    fn can_verify_hash_generated_from_python() {
354
        let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie";
355
        assert!(verify("correctbatteryhorsestapler", hash).unwrap());
356
    }
357
358
    #[test]
359
    fn can_verify_hash_generated_from_node() {
360
        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi";
361
        assert!(verify("correctbatteryhorsestapler", hash).unwrap());
362
    }
363
364
    #[test]
365
    fn can_verify_hash_generated_from_go() {
366
        /*
367
            package main
368
            import (
369
                "io"
370
                "os"
371
                "golang.org/x/crypto/bcrypt"
372
            )
373
            func main() {
374
                buf, err := io.ReadAll(os.Stdin)
375
                if err != nil {
376
                    panic(err)
377
                }
378
                out, err := bcrypt.GenerateFromPassword(buf, bcrypt.MinCost)
379
                if err != nil {
380
                    panic(err)
381
                }
382
                os.Stdout.Write(out)
383
                os.Stdout.Write([]byte("\n"))
384
            }
385
        */
386
        let binary_input = vec![
387
            29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124,
388
            168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135,
389
        ];
390
        let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9aDw5eFdstYLR8nFCNaOQmsH9XD23w.";
391
        assert!(verify(binary_input, hash).unwrap());
392
    }
393
394
    #[test]
395
    fn invalid_hash_does_not_panic() {
396
        let binary_input = vec![
397
            29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124,
398
            168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135,
399
        ];
400
        let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9a.";
401
        assert!(verify(binary_input, hash).is_err());
402
    }
403
404
    #[test]
405
    fn a_wrong_password_is_false() {
406
        let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie";
407
        assert!(!verify("wrong", hash).unwrap());
408
    }
409
410
    #[test]
411
    fn errors_with_invalid_hash() {
412
        // there is another $ in the hash part
413
        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi";
414
        assert!(verify("correctbatteryhorsestapler", hash).is_err());
415
    }
416
417
    #[test]
418
    fn errors_with_non_number_cost() {
419
        // the cost is not a number
420
        let hash = "$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi";
421
        assert!(verify("correctbatteryhorsestapler", hash).is_err());
422
    }
423
424
    #[test]
425
    fn errors_with_a_hash_too_long() {
426
        // the cost is not a number
427
        let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri";
428
        assert!(verify("correctbatteryhorsestapler", hash).is_err());
429
    }
430
431
    #[test]
432
    fn can_verify_own_generated() {
433
        let hashed = hash("hunter2", 4).unwrap();
434
        assert_eq!(true, verify("hunter2", &hashed).unwrap());
435
    }
436
437
    #[test]
438
    fn long_passwords_truncate_correctly() {
439
        // produced with python -c 'import bcrypt; bcrypt.hashpw(b"x"*100, b"$2a$05$...............................")'
440
        let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2";
441
        assert!(verify(iter::repeat("x").take(100).collect::<String>(), hash).unwrap());
442
    }
443
444
    #[test]
445
    fn non_truncating_operations() {
446
        assert!(matches!(
447
            non_truncating_hash(iter::repeat("x").take(72).collect::<String>(), DEFAULT_COST),
448
            BcryptResult::Err(BcryptError::Truncation(73))
449
        ));
450
        assert!(matches!(
451
            non_truncating_hash(iter::repeat("x").take(71).collect::<String>(), DEFAULT_COST),
452
            BcryptResult::Ok(_)
453
        ));
454
455
        let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2";
456
        assert!(matches!(
457
            non_truncating_verify(iter::repeat("x").take(100).collect::<String>(), hash),
458
            Err(BcryptError::Truncation(101))
459
        ));
460
    }
461
462
    #[test]
463
    fn generate_versions() {
464
        let password = "hunter2".as_bytes();
465
        let salt = vec![0; 16];
466
        let result =
467
            _hash_password(password, DEFAULT_COST, salt.try_into().unwrap(), false).unwrap();
468
        assert_eq!(
469
            "$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
470
            result.format_for_version(Version::TwoA)
471
        );
472
        assert_eq!(
473
            "$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
474
            result.format_for_version(Version::TwoB)
475
        );
476
        assert_eq!(
477
            "$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
478
            result.format_for_version(Version::TwoX)
479
        );
480
        assert_eq!(
481
            "$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm",
482
            result.format_for_version(Version::TwoY)
483
        );
484
        let hash = result.to_string();
485
        assert_eq!(true, verify("hunter2", &hash).unwrap());
486
    }
487
488
    #[test]
489
    fn allow_null_bytes() {
490
        // hash p1, check the hash against p2:
491
        fn hash_and_check(p1: &[u8], p2: &[u8]) -> Result<bool, BcryptError> {
492
            let fast_cost = 4;
493
            match hash(p1, fast_cost) {
494
                Ok(s) => verify(p2, &s),
495
                Err(e) => Err(e),
496
            }
497
        }
498
        fn assert_valid_password(p1: &[u8], p2: &[u8], expected: bool) {
499
            match hash_and_check(p1, p2) {
500
                Ok(checked) => {
501
                    if checked != expected {
502
                        panic!(
503
                            "checked {:?} against {:?}, incorrect result {}",
504
                            p1, p2, checked
505
                        )
506
                    }
507
                }
508
                Err(e) => panic!("error evaluating password: {} for {:?}.", e, p1),
509
            }
510
        }
511
512
        // bcrypt should consider all of these distinct:
513
        let test_passwords = vec![
514
            "\0",
515
            "passw0rd\0",
516
            "password\0with tail",
517
            "\0passw0rd",
518
            "a",
519
            "a\0",
520
            "a\0b\0",
521
        ];
522
523
        for (i, p1) in test_passwords.iter().enumerate() {
524
            for (j, p2) in test_passwords.iter().enumerate() {
525
                assert_valid_password(p1.as_bytes(), p2.as_bytes(), i == j);
526
            }
527
        }
528
529
        // this is a quirk of the bcrypt algorithm: passwords that are entirely null
530
        // bytes hash to the same value, even if they are different lengths:
531
        assert_valid_password("\0\0\0\0\0\0\0\0".as_bytes(), "\0".as_bytes(), true);
532
    }
533
534
    #[test]
535
    fn hash_with_fixed_salt() {
536
        let salt = [
537
            38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18,
538
        ];
539
        let hashed = hash_with_salt("My S3cre7 P@55w0rd!", 5, salt)
540
            .unwrap()
541
            .to_string();
542
        assert_eq!(
543
            "$2b$05$HlFShUxTu4ZHHfOLJwfmCeDj/kuKFKboanXtDJXxCC7aIPTUgxNDe",
544
            &hashed
545
        );
546
    }
547
548
    quickcheck! {
549
        fn can_verify_arbitrary_own_generated(pass: Vec<u8>) -> BcryptResult<bool> {
550
            let mut pass = pass;
551
            pass.retain(|&b| b != 0);
552
            let hashed = hash(&pass, 4)?;
553
            verify(pass, &hashed)
554
        }
555
556
        fn doesnt_verify_different_passwords(a: Vec<u8>, b: Vec<u8>) -> BcryptResult<TestResult> {
557
            let mut a = a;
558
            a.retain(|&b| b != 0);
559
            let mut b = b;
560
            b.retain(|&b| b != 0);
561
            if a == b {
562
                return Ok(TestResult::discard());
563
            }
564
            let hashed = hash(a, 4)?;
565
            Ok(TestResult::from_bool(!verify(b, &hashed)?))
566
        }
567
    }
568
569
    #[test]
570
    fn does_no_error_on_char_boundary_splitting() {
571
        // Just checks that it does not panic
572
        let _ = verify(
573
            &[],
574
            "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO",
575
        );
576
    }
577
}