diff --git a/flake.nix b/flake.nix index 8fac28dd..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"; @@ -267,6 +308,8 @@ CatalystViewTT CatalystXScriptServerStarman CatalystXRoleApplicator + CryptPassphrase + CryptPassphraseArgon2 CryptRandPasswd DBDPg DBDSQLite diff --git a/src/lib/Hydra/Schema/Users.pm b/src/lib/Hydra/Schema/Users.pm index ca650d9e..e11e0354 100644 --- a/src/lib/Hydra/Schema/Users.pm +++ b/src/lib/Hydra/Schema/Users.pm @@ -195,6 +195,7 @@ __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; @@ -216,7 +217,28 @@ sub json_hint { sub check_password { my ($self, $password) = @_; - return String::Compare::ConstantTime::equals($self->password, sha1_hex($password)); + my $authenticator = Crypt::Passphrase->new( + encoder => 'Argon2', + validators => [ + (sub { + my ($password, $hash) = @_; + + return String::Compare::ConstantTime::equals($hash, sha1_hex($password)); + }) + ], + ); + + if ($authenticator->verify_password($password, $self->password)) { + if ($authenticator->needs_rehash($self->password)) { + $self->update({ + "password" => $authenticator->hash_password($password), + }); + } + + return 1; + } else { + return 0; + } } 1; diff --git a/t/Schema/Users.t b/t/Schema/Users.t index edfb9002..5f31af76 100644 --- a/t/Schema/Users.t +++ b/t/Schema/Users.t @@ -11,11 +11,20 @@ use Test2::V0; my $db = Hydra::Model::DB->new; hydra_setup($db); -# Catalyst's default password checking is not constant time. To improve -# the security of the system, we replaced the check password routine. -# Verify comparing correct and incorrect passwords work. +# 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 a sha1 password +# Starting the user with an unsalted sha1 password my $user = $db->resultset('Users')->create({ "username" => "alice", "emailaddress" => 'alice@nixos.org', @@ -24,6 +33,10 @@ my $user = $db->resultset('Users')->create({ 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;