Skip to content

Instantly share code, notes, and snippets.

@stigtsp
Last active January 9, 2024 16:53
Show Gist options
  • Save stigtsp/7fcafa782795d87cce2e3e341db9acc3 to your computer and use it in GitHub Desktop.
Save stigtsp/7fcafa782795d87cce2e3e341db9acc3 to your computer and use it in GitHub Desktop.

Sometimes bcrypt() returns a hash it cannot validate with the same passphrase.

So the following code fails:

bcrypt_check($pass, bcrypt($pass, "2b", 4, urandom(16)))

I've seen this happen in CI runners, and started to dig a bit deeper. Usually after ~10 invocations of bcrypt() or so from process startup.

Ubuntu 22.04

  • Perl and modules from apt repo
  • Common KVM processor
root@ubuntu:~# perl bcrypt.pl 
Hashing |=================[10000] |^C                                
root@ubuntu:~# perl bcrypt.pl 
Hashing |===================[570] |^C                                
root@ubuntu:~# perl bcrypt.pl 
Hashing |===================[570] |^C                                
root@ubuntu:~# perl bcrypt.pl 
Hashing |====[7]                  |                                  

Linux ubuntu 5.15.0-84-generic #93-Ubuntu SMP Tue Sep 5 17:16:10 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

Perl v5.34.0
Crypt::Bcrypt::VERSION=0.007
Crypt::URandom::VERSION=0.36
Session::Token::VERSION=1.503

FAILED after 8 iterations.

  bcrypt("7zm3k2olcfcfzjul6nzmg5ihvd57", "2b", "4", pack("h*", "7a6d05ef8dbb2335de7cabb4f363da6e"));

  # Returns:   $2b$04$n7XO9rg5KjNrv5nJNxYr3e6ttJLOUoRPLOPa8X3wzXzvvcRhBiP1.
  # 2nd time:  $2b$04$n7XO9rg5KjNrv5nJNxYr3e6ttJLOUoRPLOPa8X3wzXzvvcRhBiP1.
  # 3rd time:  $2b$04$n7XO9rg5KjNrv5nJNxYr3e6ttJLOUoRPLOPa8X3wzXzvvcRhBiP1.

  Test in other process:
  /usr/bin/perl -MCrypt::Bcrypt -E 'say Crypt::Bcrypt::bcrypt("7zm3k2olcfcfzjul6nzmg5ihvd57", "2b", "4", pack("h*", "7a6d05ef8dbb2335de7cabb4f363da6e"));'

  # Returns:   $2b$04$n7XO9rg5KjNrv5nJNxYr3exspCVOe3vyZpvd5S//s1TsMmNelqULe

NixOS

  • Perl and modules from nixpkgs
$ perl bcrypt.pl
Hashing |=[2]                     |                                  

Linux nixos 6.6.7 #1-NixOS SMP PREEMPT_DYNAMIC Wed Dec 13 17:45:36 UTC 2023 x86_64 GNU/Linux

Perl v5.38.2
Crypt::Bcrypt::VERSION=0.011
Crypt::URandom::VERSION=0.39
Session::Token::VERSION=1.503

FAILED after 3 iterations.

  bcrypt("qr6nbgd6tqiakxeaqj4gf5fze9jv", "2b", "4", pack("h*", "1ad4a9ae450342696417030179823db0"));

  # Returns:   $2b$04$mS0Y4jOuHHXEaR.OjwhRAuwUqQDyU6S8ZC0qhi27Dctb3DKySeekm
  # 2nd time:  $2b$04$mS0Y4jOuHHXEaR.OjwhRAuwUqQDyU6S8ZC0qhi27Dctb3DKySeekm
  # 3rd time:  $2b$04$mS0Y4jOuHHXEaR.OjwhRAuwUqQDyU6S8ZC0qhi27Dctb3DKySeekm

  Test in other process:
  perl -MCrypt::Bcrypt -E 'say Crypt::Bcrypt::bcrypt("qr6nbgd6tqiakxeaqj4gf5fze9jv", "2b", "4", pack("h*", "1ad4a9ae450342696417030179823db0"));'

  # Returns:   $2b$04$mS0Y4jOuHHXEaR.OjwhRAuTmnYMFi498885cHE5WvFLnjfcY1hsnm


Docker

  • Using perl:latest
  • Modules from CPAN
root@7ebec81ca322:/foo# perl bcrypt.pl 
Hashing |=[2]                     |                                  

Linux 7ebec81ca322 6.6.7 #1-NixOS SMP PREEMPT_DYNAMIC Wed Dec 13 17:45:36 UTC 2023 x86_64 GNU/Linux

Perl v5.38.2
Crypt::Bcrypt::VERSION=0.011
Crypt::URandom::VERSION=0.39
Session::Token::VERSION=1.503

FAILED after 3 iterations.

  bcrypt("xba4u9b9zowzijlrggoxhpmkqgoc", "2b", "4", pack("h*", "ebb57107fa6000e433348298a5e77240"));

  # Returns:   $2b$04$tjqVaI6E.C2xOwgHUl2l/./I9jueefy5I4qAPaR5MtnwZoY6w7EYC
  # 2nd time:  $2b$04$tjqVaI6E.C2xOwgHUl2l/./I9jueefy5I4qAPaR5MtnwZoY6w7EYC
  # 3rd time:  $2b$04$tjqVaI6E.C2xOwgHUl2l/./I9jueefy5I4qAPaR5MtnwZoY6w7EYC

  Test in other process:
  /usr/local/bin/perl -MCrypt::Bcrypt -E 'say Crypt::Bcrypt::bcrypt("xba4u9b9zowzijlrggoxhpmkqgoc", "2b", "4", pack("h*", "ebb57107fa6000e433348298a5e77240"));'

  # Returns:   $2b$04$tjqVaI6E.C2xOwgHUl2l/.nkAZVeNi344dIxFVl/xpYwk9OpiCGcC


Test script

#!/usr/bin/env perl
use v5.34;
use Crypt::Bcrypt qw(bcrypt bcrypt_check);
use Crypt::URandom qw(urandom);
use Session::Token;
use Smart::Comments;

my $i=0;
while (++$i) {   ### Hashing |===[%]        |

  my $password = Session::Token->new(alphabet => 'abcdefghijklmnopqrstuvwxyz23456789',
                      length => 28)->get;



  my $type = "2b";
  my $cost = "4";

  my $salt = urandom(16);
  my $hash = bcrypt($password, $type, $cost, $salt);

  my $salt_hex = unpack("h*", $salt);

  if (!bcrypt_check($password, $hash)) {

    my $hash2 = bcrypt($password, $type, $cost, $salt);
    my $hash3 = bcrypt($password, $type, $cost, $salt);

    my $exec_cmd = qq|$^X -MCrypt::Bcrypt -E 'say Crypt::Bcrypt::bcrypt("$password", "$type", "$cost", pack("h*", "$salt_hex"));'|;

    my $exec_res = `$exec_cmd`;
    my $uname = `uname -a`;

    die <<"EOT";


$uname
Perl $^V
Crypt::Bcrypt::VERSION=$Crypt::Bcrypt::VERSION
Crypt::URandom::VERSION=$Crypt::URandom::VERSION
Session::Token::VERSION=$Session::Token::VERSION

FAILED after $i iterations.

  bcrypt("$password", "$type", "$cost", pack("h*", "$salt_hex"));

  # Returns:   $hash
  # 2nd time:  $hash2
  # 3rd time:  $hash3

  Test in other process:
  $exec_cmd

  # Returns:   $exec_res


EOT
  }
}


@stigtsp
Copy link
Author

stigtsp commented Jan 8, 2024

Library used in Crypt::Bcrypt: https://github.com/openwall/crypt_blowfish

Some observations:

git diff crypt-bcrypt/crypt_blowfish.c crypt_blowfish/crypt_blowfish.c
diff --git a/crypt-bcrypt/crypt_blowfish.c b/crypt_blowfish/crypt_blowfish.c
index 488a920..9d3f3be 100644
--- a/crypt-bcrypt/crypt_blowfish.c
+++ b/crypt_blowfish/crypt_blowfish.c
@@ -54,7 +54,7 @@
 #include "crypt_blowfish.h"

 #ifdef __i386__
-#define BF_ASM                         0
+#define BF_ASM                         1
 #define BF_SCALE                       1
 #elif defined(__x86_64__) || defined(__alpha__) || defined(__hppa__)
 #define BF_ASM                         0

@stigtsp
Copy link
Author

stigtsp commented Jan 9, 2024

@Leont discovered that this was caused by Session::Token returning unterminated strings :-)

hoytech/Session-Token#3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment