Skip to content

Commit

Permalink
Item14506: Implement Argon2i password hashing
Browse files Browse the repository at this point in the history
Fix an issue in the Password rest handlers -they were not passing
through errors reported in the Password manager

Add unit tests for argon2i, and update the test_htpasswd_auto test to
bypass individual tests with missing dependencies, rather than skipping
the whole test.
  • Loading branch information
gac410 committed Dec 8, 2017
1 parent adb7199 commit 48aaff0
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 53 deletions.
Expand Up @@ -487,11 +487,13 @@ sub _RESTchangePassword {
}

unless ( $users->checkPassword( $login, $oldpassword ) ) {
my $error = $users->passwordError($login) || '';
throw Foswiki::OopsException(
'password',
web => $webName,
topic => $topic,
def => 'wrong_password'
web => $webName,
topic => $topic,
def => 'wrong_password',
params => [$error],
);
}
}
Expand All @@ -515,9 +517,15 @@ sub _RESTchangePassword {
}
catch Error::Simple with {
my $error = shift;
$result = $error->{-text};
Foswiki::Func::writeWarning( "Error in setPassword: ",
( split /\n/, $error->{-text} )[0] );
$result = "Internal error";
};

unless ($ok) {
$result ||= $users->passwordError($user) || '';
}

if ( !$ok ) {
throw Foswiki::OopsException(
'password',
Expand Down Expand Up @@ -646,9 +654,10 @@ sub _RESTchangeEmail {
unless ( $users->checkPassword( $login, $password ) ) {
throw Foswiki::OopsException(
'password',
web => $webName,
topic => $topic,
def => 'wrong_password'
web => $webName,
topic => $topic,
def => 'wrong_password',
params => [ $users->passwordError($login) || '' ],
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion PasswordManagementPlugin/templates/passwordmessages.tmpl
Expand Up @@ -132,7 +132,7 @@
%{==============================================================================}%
%TMPL:DEF{"wrong_password"}%
---+++ %MAKETEXT{"Incorrect Password"}%
%MAKETEXT{"The password you entered in the *old password* field is incorrect."}%
%MAKETEXT{"The password you entered in the *old password* field is incorrect."}% %PARAM1%

%MAKETEXT{"Please go back in your browser and try again."}%
%TMPL:END%
Expand Down
133 changes: 105 additions & 28 deletions UnitTestContrib/test/unit/PasswordTests.pm
Expand Up @@ -91,11 +91,19 @@ sub doTests {
$encrapted{$user} = $impl->fetchPass($user);
$this->assert_null( $impl->error() );
$this->assert( $encrapted{$user} );
$this->assert_str_equals(
$encrapted{$user},
$impl->encrypt( $user, $this->{users1}->{$user}->{pass} ),
"fails for $user"
);

$this->assert(
$impl->checkPassword( $user, $this->{users1}->{$user}->{pass} ),
"checkPass failed" );

# argon2i generates a different key for each execution, cannot test by comparing hashes.
unless ( $encrapted{$user} =~ m/^\$argon2i\$/ ) {
$this->assert_str_equals(
$encrapted{$user},
$impl->encrypt( $user, $this->{users1}->{$user}->{pass} ),
"fails for $user"
);
}
$this->assert_str_equals(
$this->{users1}->{$user}->{emails},
join( ";", $impl->getEmails($user) )
Expand All @@ -107,7 +115,8 @@ sub doTests {
$this->assert(
$impl->checkPassword( $user, $this->{users1}->{$user}->{pass} ) );
$this->assert_str_equals( $encrapted{$user},
$impl->encrypt( $user, $this->{users1}->{$user}->{pass} ) );
$impl->encrypt( $user, $this->{users1}->{$user}->{pass} ) )
unless ( $encrapted{$user} =~ m/^\$argon2i\$/ );
}

# try changing with wrong pass
Expand Down Expand Up @@ -252,9 +261,14 @@ sub skip {
condition => { without_dep => 'Crypt::Eksblowfish::Bcrypt' },
tests => {
'PasswordTests::test_htpasswd_bcrypt' =>
'Missing Crypt::Eksblowfish::Bcrypt',
'PasswordTests::test_htpasswd_auto' =>
'Missing Crypt::Eksblowfish::Bcrypt',
'Missing Crypt::Argon2',
}
},
{
condition => { without_dep => 'Crypt::Argon2' },
tests => {
'PasswordTests::test_htpasswd_argon2i' =>
'Missing Crypt::Argon2',
}
},
);
Expand Down Expand Up @@ -548,26 +562,73 @@ DONE
$this->assert_str_equals( 'apache-md5', $encoded{$user}->{enc} );
}

$Foswiki::cfg{Htpasswd}{Encoding} = 'bcrypt';
$impl = new Foswiki::Users::HtPasswdUser( $this->{session} );
if (
$this->check_conditions_met(
( with_dep => 'Crypt::Eksblowfish::Bcrypt' )
)
)
{
$Foswiki::cfg{Htpasswd}{Encoding} = 'bcrypt';
$Foswiki::cfg{Htpasswd}{BCryptCost} = 3;
$impl = new Foswiki::Users::HtPasswdUser( $this->{session} );

# force-change them to users2 password again, migrating to bcrypt.
foreach my $user (@users) {
my $added = $impl->setPassword(
$user,
$this->{users2}->{$user}->{pass},
$this->{users2}->{$user}->{pass}
);
$this->assert_null( $impl->error() );
$this->assert_str_not_equals( $encrapted{$user},
$impl->fetchPass($user) );
$this->assert_null( $impl->error() );
$this->assert_str_equals(
$this->{users1}->{$user}->{emails},
join( ";", $impl->getEmails($user) )
);
( $encrapted{$user}, $encoded{$user} ) = $impl->fetchPass($user);
$this->assert_str_equals( 'bcrypt', $encoded{$user}->{enc} );
# force-change them to users2 password again, migrating to bcrypt.
foreach my $user (@users) {
my $added = $impl->setPassword(
$user,
$this->{users2}->{$user}->{pass},
$this->{users2}->{$user}->{pass}
);
$this->assert_null( $impl->error() );
$this->assert_str_not_equals( $encrapted{$user},
$impl->fetchPass($user) );
$this->assert_null( $impl->error() );
$this->assert_str_equals(
$this->{users1}->{$user}->{emails},
join( ";", $impl->getEmails($user) )
);
( $encrapted{$user}, $encoded{$user} ) = $impl->fetchPass($user);
$this->assert_str_equals( 'bcrypt', $encoded{$user}->{enc} );
$this->assert_matches( qr'\$2a\$03\$', $encrapted{$user},
"bcrypt settings do not match" );
}
}
else {
print STDERR
"SKIPPING bcrypt auto recognition, Crypt::Eksblowfish::Bcrypt is not installed.\n";
}

if ( $this->check_conditions_met( ( with_dep => 'Crypt::Argon2' ) ) ) {
$Foswiki::cfg{Htpasswd}{Encoding} = 'argon2i';
$Foswiki::cfg{Htpasswd}{Argon2Memcost} = '16M';
$Foswiki::cfg{Htpasswd}{Argon2Threads} = 1;
$Foswiki::cfg{Htpasswd}{Argon2Timecost} = 2;
$impl = new Foswiki::Users::HtPasswdUser( $this->{session} );

# force-change them to users2 password again, migrating to bcrypt.
foreach my $user (@users) {
my $added = $impl->setPassword(
$user,
$this->{users2}->{$user}->{pass},
$this->{users2}->{$user}->{pass}
);
$this->assert_null( $impl->error() );
$this->assert_str_not_equals( $encrapted{$user},
$impl->fetchPass($user) );
$this->assert_null( $impl->error() );
$this->assert_str_equals(
$this->{users1}->{$user}->{emails},
join( ";", $impl->getEmails($user) )
);
( $encrapted{$user}, $encoded{$user} ) = $impl->fetchPass($user);
$this->assert_str_equals( 'argon2i', $encoded{$user}->{enc} );
$this->assert_matches( qr'\$m=16384,t=2,p=1\$', $encrapted{$user},
"Argon2i settings do not match" );
}
}
else {
print STDERR
"SKIPPING argon2i auto recognition, Crypt::Argon2 is not installed.\n";
}

#dumpFile();
Expand Down Expand Up @@ -614,6 +675,22 @@ sub test_htpasswd_bcrypt {
#dumpFile();
}

sub test_htpasswd_argon2i {
my $this = shift;

$Foswiki::cfg{Htpasswd}{AutoDetect} = 0;
$Foswiki::cfg{Htpasswd}{Encoding} = 'argon2i';
$Foswiki::cfg{Htpasswd}{Argon2Memcost} = '16M';
$Foswiki::cfg{Htpasswd}{Argon2Threads} = 2;
$Foswiki::cfg{Htpasswd}{Argon2Timecost} = 2;
my $impl = new Foswiki::Users::HtPasswdUser( $this->{session} );
$this->assert($impl);
$impl->ClearCache() if $impl->can('ClearCache');
$this->doTests( $impl, $SALTED );

#dumpFile();
}

sub test_htpasswd_crypt_crypt {
my $this = shift;
$Foswiki::cfg{Htpasswd}{AutoDetect} = 0;
Expand Down
22 changes: 21 additions & 1 deletion core/data/System/ReleaseNotes02x02.txt
@@ -1,4 +1,4 @@
%META:TOPICINFO{author="ProjectContributor" date="1494776564" format="1.1" version="1"}%
%META:TOPICINFO{author="ProjectContributor" date="1512691253" format="1.1" version="1"}%
%META:TOPICPARENT{name="ReleaseHistory"}%
---+!! Foswiki Release 2.2.0

Expand Down Expand Up @@ -90,6 +90,26 @@ See [[%BUGS%/Item13696][Item13696]] for up-to-date details.

---+++ Security issues addressed in this release.

---+++ Changes in =htpasswd= support

The =Argon2i= hashing algorithm is available if the CPAN:Crypt::Argon2 module is installed. This algorithm was the winner of Password Hashing Competition in July 2015.
=Argon2i= has 3 associated tuning parameters, Time cost, Memory cost, and parallelism (threads). They can be adjusted for a reasonable operation time on the deployed hardware.
=Argon2= is not compatible with Apache login. It can only be used with Template login.

The =bcrypt= algorithm now has limited compatibility with Apache's BCrypt implementation. Apache uses the ==$2y$== bcrypt type, signifying it does not
have a bug in the algorithm that was introduced in the =PHP= implementation. The Perl module CPAN:Crypt::EKSBlowfish::BCrypt does not have that bug and is
does not recognized the =$2y= indicator.
* Password hashes generated by Foswiki will work with Apache login without changes.
* Password entries generated by the Apache =htpasswd= tool must be edited to work with Foswiki Login. Change the =$2y$= string to =$2a= in each hash entry.

---+++ General note on password security

No password is secure if the login is done over the HTTP protocol without SSL encryption. Use HTTPS / SSL for best security. The choice of a hashing
algorithm is for the protection of users should your =.htpasswd= file be compromised. Choices like argon2 and bcrypt have a much greater resistance
to various cracking tools.

Note that the =bcrypt= algorithm truncates passwords at 72 bytes. For this reason, =argon2= is preferred if users use long passphrases.

---+++ The default Attachment Link formats have changed.

The attachment link format has been changed to be more flexible. If you are using the default link formats, no changes are needed. If you have
Expand Down
51 changes: 39 additions & 12 deletions core/lib/Foswiki.spec
Expand Up @@ -700,19 +700,30 @@ $Foswiki::cfg{Htpasswd}{GlobalCache} = $FALSE;
# if Foswiki is running in a =mod_perl= or =fcgi= environment.
$Foswiki::cfg{Htpasswd}{DetectModification} = $FALSE;

# **SELECT bcrypt,'htdigest-md5','apache-md5',sha1,'crypt-md5',crypt,plain LABEL="Password Encoding" DISPLAY_IF="/htpasswd/i.test({PasswordManager})" CHECK="iff:'{PasswordManager}=~/htpasswd/i'"**
# Password encryption, for the =Foswiki::Users::HtPasswdUser= password
# **SELECT argon2,bcrypt,'htdigest-md5','apache-md5',sha1,'crypt-md5',crypt,plain LABEL="Password Encoding" DISPLAY_IF="/htpasswd/i.test({PasswordManager})" CHECK="iff:'{PasswordManager}=~/htpasswd/i'"**
# Password hashing, for the =Foswiki::Users::HtPasswdUser= password
# manager. This specifies the type of password hash to generate when
# writing entries to =.htpasswd=. It is also used when reading password
# entries unless {Htpasswd}{AutoDetect} is enabled.
#
#
# *No password is secure unless https: is in use*
#
# The choices in order of strongest to lowest strength:
# * =bcrypt= - Hash based upon blowfish algorithm, strength of hash
# controlled by a cost parameter.
# controlled by a cost parameter. *Caution:* bcrypt has a maximum
# password length of 72 bytes. Passwords longer than 72 will be
# truncated and will generate identical hashes.
# See [[System.ReleaseNotes02x02]] for details on Apache compatibility.
# * =argon2i= - Hash based upon the Argon2, the 2015 Password hash competition winner.
# Argon2 is tunable by specifying the cpu cost, memory cost and parallelism (threads).
# Argon2 would be considered stronger than bcrypt, but it is relatively new and not
# yet completely proven.
# *Not compatible with Apache Authentication*
# * =htdigest-md5= - Strongest only when combined with the
# =Foswiki::LoginManager::ApacheLogin=. Useful on sites where
# password files are required to be portable. The {AuthRealm}
# * =htdigest-md5= - Recommended only when combined with the
# =Foswiki::LoginManager::ApacheLogin=, or required for portability.
# Digest authentication provides some basic protection for non-SSL
# (http://) sites. The password is protected with
# simple encryption during browser authentication. The {AuthRealm}
# value is used with the username and password to generate the
# hashed form of the password, thus: =user:{AuthRealm}:hash=.
# This encoding is generated by the Apache =htdigest= command.
Expand All @@ -721,21 +732,21 @@ $Foswiki::cfg{Htpasswd}{DetectModification} = $FALSE;
# 32-bit salt and the password (=userid:$apr1$salt$hash=).
# This is the default, and is the encoding generated by the
# =htpasswd -m= command.
# * =sha1= - has the strongest hash, however does not use a salt
# and is therefore more vulnerable to dictionary attacks. This
# * =sha1= does not use a salt
# and is therefore highly vulnerable to dictionary attacks. This
# is the encoding generated by the =htpasswd -s= command
# (=userid:{SHA}hash=).
# * =crypt-md5= - Enable use of standard libc (/etc/shadow)
# crypt-md5 password (like =user:$1$salt$hash:email=). Unlike
# =crypt= encoding, it does not suffer from password truncation.
# Passwords are salted, and the salt is stored in the encrypted
# Passwords are salted, and the salt is stored in the hashed
# password string as in normal crypt passwords. This encoding is
# understood by Apache but cannot be generated by the =htpasswd=
# command.
# * =crypt= - encoding uses the first 8 characters of the password.
# This is the default generated by the Apache =htpasswd= command
# (=user:hash:email=). *Not Recommended.*
# * =plain= - stores passwords as plain text (no encryption). Useful
# * =plain= - stores passwords as plain text (no hashing). Useful
# for testing
# If you need to create entries in =.htpasswd= before Foswiki is operational,
# you can use the =htpasswd= or =htdigest= Apache programs to create a new
Expand Down Expand Up @@ -771,12 +782,28 @@ $Foswiki::cfg{Htpasswd}{ForceChangeEncoding} = $FALSE;
# can require extreme amounts of CPU time.
$Foswiki::cfg{Htpasswd}{BCryptCost} = 8;

# **NUMBER LABEL="Argon2 Time Cost" DISPLAY_IF="{PasswordManager}=='Foswiki::Users::HtPasswdUser' && {Htpasswd}{Encoding}=='argon2'" CHECK="min:0 max:99 iff:'{PasswordManager}=~/:HtPasswdUser/ && {Htpasswd}{Encoding} eq q<bcrypt>'"**
# Specify the cost (iterations) that should be incurred when computing the hash of a
# password. This number should be increased as CPU speeds increase.
$Foswiki::cfg{Htpasswd}{Argon2Timecost} = 32;

# **STRING LABEL="Argon2 Memory Cost" DISPLAY_IF="{PasswordManager}=='Foswiki::Users::HtPasswdUser' && {Htpasswd}{Encoding}=='argon2'" CHECK="iff:'{PasswordManager}=~/:HtPasswdUser/ && {Htpasswd}{Encoding} eq q<argon2>'"**
# Specify the cost in memory that should be incurred when computing the hash of a
# password. Minimum is 64k (or 65536). Can be specified as "k", "M" or "G" for killobytes, Megabytes and Gigabytes respectively.
# (k is lower case, M and G must be uppercase.
$Foswiki::cfg{Htpasswd}{Argon2Memcost} = '16M';

# **NUMBER LABEL="Argon2 Parallelism" DISPLAY_IF="{PasswordManager}=='Foswiki::Users::HtPasswdUser' && {Htpasswd}{Encoding}=='argon2'" CHECK="min:1 max:16 iff:'{PasswordManager}=~/:HtPasswdUser/ && {Htpasswd}{Encoding} eq q<bcrypt>'"**
# Specify the number of threads that will be required for executing the
# algorithm.
$Foswiki::cfg{Htpasswd}{Argon2Threads} = 4;

# **PASSWORD LABEL="Internal Admin Password" CHECK_ON_CHANGE="{FeatureAccess}{Configure}" CHECK="also:{FeatureAccess}{Configure}" ONSAVE**
# If set, this password permits use of the _internal admin_ login, and the
# sudo facility. *As it is a "shared password", this is no longer
# recommended per good security practices. Clear this field to disable use
# of the internal admin login.
# NOTE: this field is encrypted, and the value can only be set using the
# NOTE: this field is hashed, and the value can only be set using the
# =configure= interface.
$Foswiki::cfg{Password} = '';

Expand Down
7 changes: 7 additions & 0 deletions core/lib/Foswiki/Configure/Checkers/Htpasswd/Encoding.pm
Expand Up @@ -38,6 +38,13 @@ my %methods = (
usage => 'use or auto-detection of crypt-md5'
}
],
'argon2i' => [
{
name => 'Crypt::Argon2',
search => ':$argon2i$',
usage => 'use or auto-detection of argon2'
}
],
'bcrypt' => [
{
name => 'Crypt::Eksblowfish::Bcrypt',
Expand Down

0 comments on commit 48aaff0

Please sign in to comment.