davebaker has asked for the wisdom of the Perl Monks concerning the following question:

Yikes. Another 8-1/2 billion passwords have been leaked.

Have a login system for my web app that has a couple hundred users. To log in, a user enters a password, the submitted password is run through my authentication script that uses crypt() and compares the result to a hashed string (i.e., a digest) stored in a text file on my server (because crypt() had created that digest when the user first created his or her account). If the crypt() result is the same as the digest, we have a match, so c'mon in.

I've done enough research to see that such a system is a commonly-used one and has the advantage of not storing passwords in plain text, or even storing passwords at all, but that crypt() uses an encryption method that isn't very strong, at least nowadays.

So I'm thinking there is a security vulnerability in the event a black hat were to obtain the plain text file by breaking into my server. Then the black hat runs a rainbow table against each user's digest to find a password that produces that same string. Then the black hat takes the user's password to the user's bank web site and uses that same password to empty the account, because so many people use the same password for multiple websites. It's not so much the user's information on my website that's confidential, it's the empty-the-bank-account scenario that's troubling.

This issue always has been present with the use of a digest for purposes of password checking, but I'm thinking it becomes more of an issue when a couple billion more possible passwords are released that can be used to make the rainbow table more effective. Hence the risks of damages to my users has gone up, and my risk of getting sued has gone up. Do you agree?

So I'm thinking that it would be good to replace:

grant_website_access() if ( $password_entered_on_web_form eq crypt( $u +ser_specific_hash_stored_in_text_file_on_server, $password_entered_on +_web_form );

with

grant_website_access() if ( $password_entered_on_web_form eq something +_stronger_than_crypt( $user_specific_hash_stored_in_text_file_on_serv +er, $password_entered_on_web_form );

An article on Password Storage with Salted Hashes recommends Crypt::SaltedHash, with an example that seems to use the module's methods to achieve the same effect as my code. The author also recommends the use of SHA-512 with Crypt::SaltedHash rather than the considerably less secure SHA-1 or MD-5.

That's a good solution, it appears, to my perceived problem. I've checked threads back through 2012 here on PM and find none better, although Authen::Passphrase::BlowfishCrypt is intriguing.

Replies are listed 'Best First'.
Re: Replacing crypt() for password login via a digest - looking for stronger alternative
by cavac (Curate) on Jun 11, 2021 at 21:08 UTC

    I'm using Digest::BCrypt on heavily salted passwords for my systems. I have everything in a database, so my code is probably slightly more complex than yours. Take a look at my Password handler:

    package PageCamel::Helpers::Passwords; #---AUTOPRAGMASTART--- use 5.030; use strict; use warnings; use diagnostics; use mro 'c3'; use English; use Carp qw[carp croak confess cluck longmess shortmess]; our $VERSION = 3.5; use autodie qw( close ); use Array::Contains; use utf8; use Data::Dumper; use PageCamel::Helpers::UTF; #---AUTOPRAGMAEND--- # PAGECAMEL (C) 2008-2020 Rene Schickbauer # Developed under Artistic license use Digest; use Data::Entropy::Algorithms qw(rand_bits); use MIME::Base64; use PageCamel::Helpers::DateStrings; use Time::HiRes qw[sleep]; use base qw(Exporter); our @EXPORT= qw(update_password verify_password gen_textsalt); ## no c +ritic (Modules::ProhibitAutomaticExportation) sub gen_textsalt { my $saltbase = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN +OPQRSTUVWXYZ'; my $salt = ''; my $count = int(rand(20))+20; for(1..$count) { my $pos = int(rand(length($saltbase))); $salt .= substr($saltbase, $pos, 1); } return $salt; } sub update_password { my ($dbh, $username, $password) = @_; # While pre- and postsalt does not much for complexity, it helps p +reventing rainbow tables attacks. # I know, the bcrypt salt already does that, in case of a general +bcrypt breach, this should # make it a bit more difficult. my $presalt = gen_textsalt(); my $postsalt = gen_textsalt(); my $bsalt = rand_bits(16*8); # 16 octets (16 bytes at 8 bits) #print length($bsalt) . "\n"; #print $bsalt . "\n"; my $bsalt_b64 = encode_base64($bsalt, ''); #my $cost = getCurrentYear() - 2000 + 3; my $cost = 5; # FIXME: Make SystemSetting my $bcrypt = Digest->new('Bcrypt'); $bcrypt->cost($cost); $bcrypt->salt($bsalt); $bcrypt->add($presalt); $bcrypt->add($password); $bcrypt->add($postsalt); my $pwsalted = $bcrypt->b64digest; my $upsth = $dbh->prepare("UPDATE users SET password_prefix = ?, password_postfix = ?, password_bcrypt_hash = ?, password_bcrypt_salt = ?, password_bcrypt_cost = ?, next_password_change = now() + interval +'12 weeks' WHERE username = ?") or croak($dbh->errstr); if(!$upsth->execute($presalt, $postsalt, $pwsalted, $bsalt_b64, $c +ost, $username)) { return 0; } return 1; } sub verify_password { my ($dbh, $username, $password) = @_; # Pre-initialize for random pw calculations in case no user is fou +nd (there should be no # measurable time difference for unknown users. This will make it +harder to guess is a username # exists) my $presalt = gen_textsalt(); my $postsalt = gen_textsalt(); my $bsalt = rand_bits(16*8); # 16 octets (16 bytes at 8 bits) #my $cost = getCurrentYear() - 2000 + 3; my $cost = 16; # FIXME: Make SystemSetting my $pwhash = ''; my $isLocked = 0; my $selsth = $dbh->prepare("SELECT account_locked, password_prefix, password_postfix, password_bcrypt_hash, password_bcrypt_salt, password_bcrypt_cost FROM users WHERE username = ? AND password_prefix != '' AND password_postfix != '' AND password_bcrypt_hash != '' AND password_bcrypt_salt != '' ") or croak($dbh->errstr); if(!$selsth->execute($username)) { return 0; } my $found = 0; while((my $line = $selsth->fetchrow_arrayref)) { my $bsalt_b64; ($isLocked, $presalt, $postsalt, $pwhash, $bsalt_b64, $cost) = + @{$line}; $bsalt = decode_base64($bsalt_b64); $found = 1; last; } $selsth->finish; my $bcrypt = Digest->new('Bcrypt'); $bcrypt->cost($cost); $bcrypt->salt($bsalt); $bcrypt->add($presalt); $bcrypt->add($password); $bcrypt->add($postsalt); my $pwsalted = $bcrypt->b64digest; # sleep for a random amount of time, up to a second fo further lim +it # bruteforcing and "unknown user" detection my $sleeptime = int(rand(900) + 100) / 1000; sleep($sleeptime); if($isLocked || !$found || $pwsalted ne $pwhash) { return 0; } return 1; } 1; __END__ =head1 NAME PageCamel::Helpers::Passwords - handle passwords in a PageCamel databa +se =head1 SYNOPSIS use PageCamel::Helpers::Passwords; =head1 DESCRIPTION This central module does all the actual password handling for PageCame +l projects. This way, changing the hashing algorithm or adapting its +strengh (vs time) can be done in one central place in the code. =head2 gen_textsalt Randomly generate a salt used for hashing passwords. =head2 update_password Update a password in the database (also generates a new salt). =head2 verify_password Verify correctness of a password. =head1 IMPORTANT NOTE This module is part of the PageCamel framework. Currently, only limite +d support and documentation exists outside my DarkPAN repositories. This source +is currently only provided for your reference and usage in other projects + (just copy&paste what you need, see license terms below). To see PageCamel in action and for news about the project, visit my blog at L<https://cavac.at>. =head1 AUTHOR Rene Schickbauer, E<lt>cavac@cpan.orgE<gt> =head1 COPYRIGHT AND LICENSE Copyright (C) 2008-2020 Rene Schickbauer This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.10.0 or, at your option, any later version of Perl 5 you may have available. =cut

    I'm also enforcing quite complex passwords that are very hard to guess.

    perl -e 'use Crypt::Digest::SHA256 qw[sha256_hex]; print substr(sha256_hex("the Answer To Life, The Universe And Everything"), 6, 2), "\n";'

      May I make a suggestions? in a post in this thread I mention not to underestimate the danger in using not-cryptographically secure RNG (and Perl's isn't).

      Also, for checking password suitability one could run candidate password through the 8billion leaked passwords! Too bad it can't be implemented with an online password checker service!!! Or can it? I surely have a hunch it will be popular. https://www.security.org/how-secure-is-my-password hehe

      Another thought (i.e. just "theory") for selecting a password: how about checking its statistical distribution? Although finding the S.D. is not reliable with just 8-16 chars I find it a good idea as part of a wider suite of tools, even if only for checking if it resembles english or german, etc., language's S.D. Ideally, I guess, a good password must have a uniform S.D. and also conditional probabilities between adjacent characters to follow uniform S.D., e.g. P(pass[i]|pass[i-1]) = 0.5. That can be extended to pass[i]|pass[j] perhaps.

      bw, bliako

Re: Replacing crypt() for password login via a digest - looking for stronger alternative
by haukex (Bishop) on Jun 12, 2021 at 11:10 UTC
Re: Replacing crypt() for password login via a digest - looking for stronger alternative
by kikuchiyo (Friar) on Jun 12, 2021 at 18:05 UTC

    +1 for Authen::Passphrase::BlowfishCrypt, we use it in our main product at $work. It creates salted hashes, and it has an adjustable cost parameter to decrease efficiency of brute-force attacks on faster hardware in the future.

    It's compatible with pgcrypto, which we've used in the past.

Re: Replacing crypt() for password login via a digest - looking for stronger alternative
by navalned (Sexton) on Jun 12, 2021 at 03:37 UTC

    You are welcome to try my password hashing module for use with OpenSMTPD. http://www.pettijohn-web.com/OpenSMTPD-Password-0.03.tar.gz
    Just note the tests require opensmtpd be installed. But its not a requirement of the module itself.

Re: Replacing crypt() for password login via a digest - looking for stronger alternative
by davebaker (Pilgrim) on Jun 17, 2021 at 17:27 UTC

    I settled on:

    use Crypt::SaltedHash; # Individual registers as a new user, supplying a username # and a password/passphrase in a form on a web page. # But, for illustration: my $desired_password = 'Owning 2 boats is 1 boat too many!'; my $csh = Crypt::SaltedHash->new(algorithm => 'SHA-256'); $csh->add($desired_password); my $digest = $csh->generate; # Some might prefer to call 'digest' # something like 'hash_string' instead # Then the value of $digest is stored to a file or database # along with the corresponding username (not shown). # # By the way, for the $desired_password shown, the digest that # was generated is this string: # # {SSHA256}rn8PJZd//2EsEgHTBQ0izsN2T7AJMsLRH19oLIA5unaf8OaJ # # ...but it would be very different for you, because Crypt::SaltedHash # randomizes the "salt" used in the creation of the digest. And it # would be very different for me, if this script were to be run a # second time, even though it would be processing the same 'Owning 2 # boats is 1 boat too many!' desired password.

    In another program, when the individual enters a username and a password/passphrase into a login form:

    use Crypt::SaltedHash; my $asserted_password = ___; # Some process to retrieve the value # entered on a web form by the user, not illustrated here my $digest = ___; # Some retrieval process that looks up the earlier-s +tored # digest corresponding to the username, not illustrated here my $is_valid = Crypt::SaltedHash->validate( $digest, $asserted_passwor +d ); grant_account_access() if ( $is_valid );
Re: Replacing crypt() for password login via a digest - looking for stronger alternative
by Bod (Deacon) on Jun 13, 2021 at 18:46 UTC

    In the comments of the article you refer to it says...

    In a more real life scenario, $shash would likely be retrieved from the DB you use to maintain credentials.

    When passwords are leaked, I have always assumed that someone has somehow maliciously extracted the data from the User (or equivalent) database table. From this they have got a list of email addresses and password hashes. But if the database table also has the salts in it, is it any more secure than just using a strong encryption algorithm without salting?

    Personally I keep name and email address data in a different table to the user ID and encrypted passwords. That table exists in a different schema, albeit within the same RDBMS.

      The salt (as in "a set of random bytes") is appended or prepended mixed into the password and the result is then hashed. Hashing is a one way function. Rainbow tables are hash-realpassword pairs and can be very fast in telling you the password given the hashed-password (as fast as a perl-hashtable=dictionary lookup-by-key, which is, in theory, O(1)). So, if your hashed-password contains a random set of values intermixed with the real password, then the given rainbow does not work anymore! Or at least you will need a different rainbow table for each possible salt (for example, 60^10 for a 60-characters-range, 10-byte-long salts). For these reasons, salts can be stored in your hacked db alongside the password and even in plain text. The cost for the hacker will be enormous. But of course not impossible.

      there are a few points that need to be clarified, which I don't know. 1) Should the character set of the salt be the same as the password or what it should be, should it match the statistical distribution of the password? I guess it does not matter, but I am not an expert. 2) Is it really paranoid to store salts in a different db, even in a different host? 3) how about hashing them? I think that would work if salt length is random and character-range in 0-255, so as hacker not to be able to know if unhashing of salt succeeded.

      What I am sure about is to never use the built-in RNG for encryption ops! It is looking for trouble big time. Use one of the so-called "cryptographically secure RNG".

      bw, bliako

        Rainbow tables are hash-realpassword pairs and can be very fast in telling you the password given the hashed-password

        Yes - I get that salting helps from attack using a rainbow table.

        But passwords are typically rather short so not too difficult to crack by brute force. Assuming the password is made up only of upper case, lower case letters and numbers then there are only (!) 218,340,105,584,896 permutations. That is 628. Whilst that's a lots of permutations, I read recently that 2011 technology could run through all those in 23 minutes. Imagine what 2021 technology can do and there is no need to go through them all. You stop when you get it right!

        So, to take a simple example - let's say the hashed password was 1234abcdxxyyzz. Without salting you assume a minimum length of 3 characters so start at aaa then aab, aac, etc. If instead you have salts stored in the same table as the hashed password you just add the salt to the end and try the permutations. So instead of aaa you try aaaSaLT123, aabSaLT123, etc. until you get the result 1234abcdxxyyzz.

        Or am I missing something? Is that not how it works?