Crypt::Eksblowfish::Bcrypt - Blowfish-based Unix crypt() password hash

Veröffentlicht von Thomas Fahle am (Permalink)

 

Crypt::Eksblowfish::Bcrypt

Das CPAN-Modul Crypt::Eksblowfish::Bcrypt - Blowfish-based Unix crypt() password hash von Andrew Main (Zefram) implementiert den Blowfish-basierten Unix crypt()-Passwort-Hash-Algorithmus, bekannt als bcrypt.

Dieser Hash verwendet eine Variante von Blowfish, bekannt als Eksblowfish, die so modifiziert wurde, dass sie eine besonders teure Schlüsselplanung hat. Eksblowfish und bcrypt wurden von Niels Provos und David Mazieres für OpenBSD entwickelt. Das Design ist in USENIX 99 - A Future-Adaptable Password Scheme beschrieben.

Hauptanwendungsgebiet ist das sichere, verschlüsselte Speichern von Passwörtern - also so, dass das Original-Passwort nicht wiederhergestellt werden kann.

 

Funktionen

Crypt::Eksblowfish::Bcrypt stellt vier Funktionen zur Verfügung:

  • bcrypt_hash(), die eigentliche Hash-Funktion
  • bcrypt(), eine Funktion zur Erzeugung von crypt() kompatiblen Strings aus bcrypt-Hashes
  • en_base64(), Base64-Kodierung der Octets/Bytes
  • de_base64(), welche die Base64-Kodierung umkehrt

 

Beispiel Hash berechnen mit bcrypt_hash()

Das folgende Beispiel verwendet die Funktion bcrypt_hash() um den Bcrypt-Hash eines Passwortes aus einem zufälligem Salt (exakt 16 Byte) zu erzeugen. Über den Parameter cost wird die Anzahl der Rechenrunden beeinflußt. key_nul dient der Rückwärtskompatibilität und sollte für aktuelle Implementierungen stets auf 1 gesetzt werden.

#!/usr/bin/perl

use strict;
use warnings;

use feature 'say';

use Crypt::URandom qw( urandom );

use Crypt::Eksblowfish::Bcrypt qw(bcrypt_hash en_base64);

my $password = 'supercalifragilisticexpialidocious';

# Non-negative integer controlling the cost of the hash function.
# The number of operations is proportional to 2^cost.
my $cost = 5;

# Exactly sixteen octets of salt from a strong non-blocking random source
my $salt = urandom(16);

my $hash = bcrypt_hash(
    {
        key_nul => 1,
        cost    => $cost,
        salt    => $salt,
    },
    $password
);

# Encodes the octet string textually using the form of base 64
# that is conventionally used with bcrypt.
my $encoded = en_base64($hash);
say $encoded;

# Encodes the octet string as a hex string (high nybble first).
my $hex = unpack( 'H*', $hash );
say $hex;

Das Programm liefert z.B. folgende Ausgabe:

.EgtpXKVjY3XPuD0NulV5GK5vA2kDYe
0068afad931795ae594701763f09d7ec833bc42e2615a8

 

Bcrypt Hashes als Strings speichern

Die so errechneten Hashes werden als Strings im Password Storage, z.B. /etc/shadow, zusammen mit dem Cost Factor und dem Salt gespeichert. Diese Zeichenkette hat die allgemeine Form:

$2[a|b|y]$[cost]$[22 character salt][31 character hash]

Beispiel:

$2y$10$GI7ziJuT2wADu1BQMhnDKOJL5dYnpJj2zoGWwS2MrKgdgmGYHHzrq
\__/\/ \____________________/\_____________________________/
 Alg Cost      Salt                        Hash

Hier bedeutet

  • $2a$ oder $2b$ oder $2y$: The hash algorithm identifier (bcrypt)
  • 10: Cost factor (2^10 ==> 1,024 rounds)
  • GI7ziJuT2wADu1BQMhnDKO: 16-byte (128-bit) salt, base64-encoded to 22 characters
  • JL5dYnpJj2zoGWwS2MrKgdgmGYHHzrq: 24-byte (192-bit) hash, base64-encoded to 31 characters

 

Beispiel bcrypt() - Bcrypt Hashes als Strings

Das folgende Beispiel nutzt die Funktion bcrypt(PASSWORD, SETTINGS) zur Erzeugung eines crypt()-kompatiblen Strings.

Die Funktion erwartet das Passwort und einen speziell konstruierten String von Settings, der u.a. cost und salt enthält.

Leider unterstützt bcrypt() nur den Identifier a als Hash Algorithmus Identifier. Die ebenfalls gültigen Identifier b und y erzeugen eine Fehlermeldung. (Einen möglichen Workaround/Patch findet man in Crypt::Eksblowfish::Bcrypt doesnt support 2y.)

#!/usr/bin/perl

use strict;
use warnings;

use feature 'say';

use Crypt::URandom qw( urandom );

use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);

my $password = 'supercalifragilisticexpialidocious';

# Non-negative integer controlling the cost of the hash function.
# The number of operations is proportional to 2^cost.
my $cost = 5;

# Exactly sixteen octets of salt from a strong non-blocking random source
my $salt         = urandom(16);
my $salt_encoded = en_base64($salt);

# Assemble settings string
my $settings;
# "$2", optional "a", $
# only a is supported - b or y will yield an error
$settings .= '$2a$';
# cost as two digits (leading zero)
$cost = sprintf( "%02d", $cost );
$settings .= $cost;
# $
$settings .= '$';
# 22 base 64 digits (salt)
$settings .= $salt_encoded;

my $bycrypt_storage_string = bcrypt( $password, $settings );

# Debug
say "Settings:      $settings";
say "Bcrypt string: $bycrypt_storage_string";

Das Programm liefert z.B. folgende Ausgabe:

Settings:      $2a$05$aI5KrNhn5/CHtUHT/ZaHou
Bcrypt string: $2a$05$aI5KrNhn5/CHtUHT/ZaHouLpAGo8PA3Yclpv1Y4mNehB1fandFE6O

 

Passwort Check

Um das vom User eingebene Passwort zu prüfen, wird aus diesem Passwort erneut ein Hash mit exakt denselben Parametern (cost,salt) berechnet und Base64-kodiert. Anschließend werden die beiden Hashes verglichen. Stimmen sie überein, gilt das eingegeben Passwort als korrekt.

Die Parameter cost und salt werden aus der im Password Storage hinterlegten Zeichenkette (s.o.) ermittelt.

Das nachfolgende Bespiel verdeutlicht die Vorgehensweise.

#!/usr/bin/perl

use strict;
use warnings;
use feature "say";

use Crypt::Eksblowfish::Bcrypt "en_base64", "de_base64", "bcrypt_hash";

# Cleartext password from user input
my $pass_input = "12345678";

# bcrypt hash string from password storage or other application
# e.g.  my $bcrypt_hash_string = qx{php -r 'echo password_hash($pass_input, PASSWORD_BCRYPT);'};
my $bcrypt_hash_string = '$2y$10$GI7ziJuT2wADu1BQMhnDKOJL5dYnpJj2zoGWwS2MrKgdgmGYHHzrq';

my ( $unused, $algorithm, $cost, $salt_and_pass ) = split /\$/, $bcrypt_hash_string;

# 22 character salt (base64 encoded)
my $salt = substr $salt_and_pass, 0, 22;
# 31 character bcrypt hash (base64 encoded)
my $encoded_pass_hash = substr $salt_and_pass, 22, 31;

# Debug
say "$algorithm, $cost, $salt_and_pass, $salt, $encoded_pass_hash";

# Comput bcrypt hash using identified cost and salt
my $perl_bcrypt_hash = en_base64(
    bcrypt_hash(
        {
            cost    => $cost,
            key_nul => 1,
            salt    => de_base64($salt)
        },
        $pass_input
    )
);

# Method 1
# compare supplied bcrypt hash string from password storage
# with computed (Perl) bcrypt hash string
# Assemble (Perl) bcrypt hash string
my $perl_bcrypt_hash_string = sprintf '$%s$%d$%s%s', $algorithm, $cost, $salt, $perl_bcrypt_hash;

# Debug
say "";
say "Other $bcrypt_hash_string";
say "Perl  $perl_bcrypt_hash_string";

if ( $bcrypt_hash_string eq $perl_bcrypt_hash_string ) {
    say "Okay. Password hash strings match.";
} else {
    say "Sorry. Password hash strings do not match";
}

# Method 2
# or compare encoded hashes
# Debug
say "";
say "Other $encoded_pass_hash";
say "Perl  $perl_bcrypt_hash";
if ( $encoded_pass_hash eq $perl_bcrypt_hash ) {
    say "Okay. Password hashes match.";
} else {
    say "Sorry. Password hashes do not match";
}

 

Siehe auch