diff --git a/flake.nix b/flake.nix index 6444af6c..87acfa5f 100644 --- a/flake.nix +++ b/flake.nix @@ -70,6 +70,47 @@ }; }; + CryptArgon2 = final.perlPackages.buildPerlModule { + pname = "Crypt-Argon2"; + version = "0.010"; + src = final.fetchurl { + url = "mirror://cpan/authors/id/L/LE/LEONT/Crypt-Argon2-0.010.tar.gz"; + sha256 = "3ea1c006f10ef66fd417e502a569df15c4cc1c776b084e35639751c41ce6671a"; + }; + nativeBuildInputs = [ pkgs.ld-is-cc-hook ]; + meta = { + description = "Perl interface to the Argon2 key derivation functions"; + license = final.lib.licenses.cc0; + }; + }; + + CryptPassphrase = final.buildPerlPackage { + pname = "Crypt-Passphrase"; + version = "0.003"; + src = final.fetchurl { + url = "mirror://cpan/authors/id/L/LE/LEONT/Crypt-Passphrase-0.003.tar.gz"; + sha256 = "685aa090f8179a86d6896212ccf8ccfde7a79cce857199bb14e2277a10d240ad"; + }; + meta = { + description = "A module for managing passwords in a cryptographically agile manner"; + license = with final.lib.licenses; [ artistic1 gpl1Plus ]; + }; + }; + + CryptPassphraseArgon2 = final.buildPerlPackage { + pname = "Crypt-Passphrase-Argon2"; + version = "0.002"; + src = final.fetchurl { + url = "mirror://cpan/authors/id/L/LE/LEONT/Crypt-Passphrase-Argon2-0.002.tar.gz"; + sha256 = "3906ff81697d13804ee21bd5ab78ffb1c4408b4822ce020e92ecf4737ba1f3a8"; + }; + propagatedBuildInputs = with final.perlPackages; [ CryptArgon2 CryptPassphrase ]; + meta = { + description = "An Argon2 encoder for Crypt::Passphrase"; + license = with final.lib.licenses; [ artistic1 gpl1Plus ]; + }; + }; + DirSelf = final.buildPerlPackage { pname = "Dir-Self"; version = "0.11"; @@ -229,6 +270,19 @@ license = with final.stdenv.lib.licenses; [ artistic1 ]; }; }; + + StringCompareConstantTime = final.buildPerlPackage { + pname = "String-Compare-ConstantTime"; + version = "0.321"; + src = final.fetchurl { + url = "mirror://cpan/authors/id/F/FR/FRACTAL/String-Compare-ConstantTime-0.321.tar.gz"; + sha256 = "0b26ba2b121d8004425d4485d1d46f59001c83763aa26624dff6220d7735d7f7"; + }; + meta = { + description = "Timing side-channel protected string compare"; + license = with final.lib.licenses; [ artistic1 gpl1Plus ]; + }; + }; }; hydra = with final; let @@ -254,6 +308,8 @@ CatalystViewTT CatalystXScriptServerStarman CatalystXRoleApplicator + CryptPassphrase + CryptPassphraseArgon2 CryptRandPasswd DBDPg DBDSQLite @@ -279,6 +335,7 @@ SQLSplitStatement SetScalar Starman + StringCompareConstantTime SysHostnameLong TermSizeAny TestMore diff --git a/src/lib/Hydra.pm b/src/lib/Hydra.pm index 6d204431..0ff86f11 100644 --- a/src/lib/Hydra.pm +++ b/src/lib/Hydra.pm @@ -27,27 +27,25 @@ our $VERSION = '0.01'; __PACKAGE__->config( name => 'Hydra', default_view => "TT", - authentication => { + 'Plugin::Authentication' => { default_realm => "dbic", - realms => { - dbic => { - credential => { - class => "Password", - password_field => "password", - password_type => "hashed", - password_hash_type => "SHA-1", - }, - store => { - class => "DBIx::Class", - user_class => "DB::Users", - role_relation => "userroles", - role_field => "role", - }, + + dbic => { + credential => { + class => "Password", + password_field => "password", + password_type => "self_check", + }, + store => { + class => "DBIx::Class", + user_class => "DB::Users", + role_relation => "userroles", + role_field => "role", }, - ldap => $ENV{'HYDRA_LDAP_CONFIG'} ? LoadFile( - file($ENV{'HYDRA_LDAP_CONFIG'}) - ) : undef }, + ldap => $ENV{'HYDRA_LDAP_CONFIG'} ? LoadFile( + file($ENV{'HYDRA_LDAP_CONFIG'}) + ) : undef }, 'Plugin::Static::Simple' => { send_etag => 1, diff --git a/src/lib/Hydra/Controller/Admin.pm b/src/lib/Hydra/Controller/Admin.pm index e2a219ff..8dd3c348 100644 --- a/src/lib/Hydra/Controller/Admin.pm +++ b/src/lib/Hydra/Controller/Admin.pm @@ -6,7 +6,6 @@ use base 'Catalyst::Controller'; use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; use Data::Dump qw(dump); -use Digest::SHA1 qw(sha1_hex); use Config::General; diff --git a/src/lib/Hydra/Controller/Root.pm b/src/lib/Hydra/Controller/Root.pm index e15c4934..62e793e1 100644 --- a/src/lib/Hydra/Controller/Root.pm +++ b/src/lib/Hydra/Controller/Root.pm @@ -7,7 +7,6 @@ use base 'Hydra::Base::Controller::ListBuilds'; use Hydra::Helper::Nix; use Hydra::Helper::CatalystUtils; use Hydra::View::TT; -use Digest::SHA1 qw(sha1_hex); use Nix::Store; use Nix::Config; use Encode; diff --git a/src/lib/Hydra/Controller/User.pm b/src/lib/Hydra/Controller/User.pm index 9cdece8a..b3512a1b 100644 --- a/src/lib/Hydra/Controller/User.pm +++ b/src/lib/Hydra/Controller/User.pm @@ -229,12 +229,6 @@ sub isValidPassword { } -sub setPassword { - my ($user, $password) = @_; - $user->update({ password => sha1_hex($password) }); -} - - sub register :Local Args(0) { my ($self, $c) = @_; @@ -294,7 +288,7 @@ sub updatePreferences { error($c, "The passwords you specified did not match.") if $password ne trim $c->stash->{params}->{password2}; - setPassword($user, $password); + $user->setPassword($password); } my $emailAddress = trim($c->stash->{params}->{emailaddress} // ""); @@ -394,7 +388,7 @@ sub reset_password :Chained('user') :PathPart('reset-password') :Args(0) { unless $user->emailaddress; my $password = Crypt::RandPasswd->word(8,10); - setPassword($user, $password); + $user->setPassword($password); sendEmail( $c->config, $user->emailaddress, diff --git a/src/lib/Hydra/Schema/Users.pm b/src/lib/Hydra/Schema/Users.pm index 7789b42c..55f0f1cb 100644 --- a/src/lib/Hydra/Schema/Users.pm +++ b/src/lib/Hydra/Schema/Users.pm @@ -195,6 +195,10 @@ __PACKAGE__->many_to_many("projects", "projectmembers", "project"); # Created by DBIx::Class::Schema::Loader v0.07049 @ 2020-02-06 12:22:36 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4/WZ95asbnGmK+nEHb4sLQ +use Crypt::Passphrase; +use Digest::SHA1 qw(sha1_hex); +use String::Compare::ConstantTime; + my %hint = ( columns => [ "fullname", @@ -210,4 +214,42 @@ sub json_hint { return \%hint; } +sub _authenticator() { + my $authenticator = Crypt::Passphrase->new( + encoder => 'Argon2', + validators => [ + (sub { + my ($password, $hash) = @_; + + return String::Compare::ConstantTime::equals($hash, sha1_hex($password)); + }) + ], + ); + + return $authenticator; +} + +sub check_password { + my ($self, $password) = @_; + + my $authenticator = _authenticator(); + if ($authenticator->verify_password($password, $self->password)) { + if ($authenticator->needs_rehash($self->password)) { + $self->setPassword($password); + } + + return 1; + } else { + return 0; + } +} + +sub setPassword { + my ($self, $password) = @_;; + + $self->update({ + "password" => _authenticator()->hash_password($password), + }); +} + 1; diff --git a/src/script/hydra-create-user b/src/script/hydra-create-user index 3fde1aad..6e837270 100755 --- a/src/script/hydra-create-user +++ b/src/script/hydra-create-user @@ -5,17 +5,16 @@ use Hydra::Schema; use Hydra::Helper::Nix; use Hydra::Model::DB; use Getopt::Long qw(:config gnu_getopt); -use Digest::SHA1 qw(sha1_hex); sub showHelp { - print <txn_do(sub { $user->update({ emailaddress => $userName, password => "!" }); } else { $user->update({ emailaddress => $emailAddress }) if defined $emailAddress; + if (defined $password && !(defined $passwordHash)) { - $passwordHash = sha1_hex($password); + $user->setPassword($password); } $user->update({ password => $passwordHash }) if defined $passwordHash; } diff --git a/t/Schema/Users.t b/t/Schema/Users.t new file mode 100644 index 00000000..5f31af76 --- /dev/null +++ b/t/Schema/Users.t @@ -0,0 +1,42 @@ +use strict; +use Setup; + +my %ctx = test_init(); + +require Hydra::Schema; +require Hydra::Model::DB; + +use Test2::V0; + +my $db = Hydra::Model::DB->new; +hydra_setup($db); + +# Hydra used to store passwords, by default, as plain unsalted sha1 hashes. +# We now upgrade these badly stored passwords with much stronger algorithms +# when the user logs in. Implementing this meant reimplementing our password +# checking ourselves, so also ensure that basic password checking works. +# +# This test: +# +# 1. creates a user with the legacy password +# 2. validates that the wrong password is not considered valid +# 3. validates that the correct password is valid +# 4. checks that the checking of the correct password transparently upgraded +# the password's storage to a more secure algorithm. + +# Starting the user with an unsalted sha1 password +my $user = $db->resultset('Users')->create({ + "username" => "alice", + "emailaddress" => 'alice@nixos.org', + "password" => "8843d7f92416211de9ebb963ff4ce28125932878" # SHA1 of "foobar" +}); +isnt($user, undef, "My user was created."); + +ok(!$user->check_password("barbaz"), "Checking the password, barbaz, is not right"); + +is($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The unsalted sha1 is in the database."); +ok($user->check_password("foobar"), "Checking the password, foobar, is right"); +isnt($user->password, "8843d7f92416211de9ebb963ff4ce28125932878", "The user has had their password rehashed."); +ok($user->check_password("foobar"), "Checking the password, foobar, is still right"); + +done_testing;