package Anvil::Tools::Account; # # This module contains methods used to handle user accounts, logging in and out. # use strict; use warnings; use Digest::SHA qw(sha256_base64 sha384_base64 sha512_base64); use Scalar::Util qw(weaken isweak); our $VERSION = "3.0.0"; my $THIS_FILE = "Account.pm"; ### Methods; # encrypt_password # read_details # validate_password =pod =encoding utf8 =head1 NAME Anvil::Tools::Account Provides all methods related to user management and log in/out features. =head1 SYNOPSIS use Anvil::Tools; # Get a common object handle on all Anvil::Tools modules. my $anvil = Anvil::Tools->new(); # Access to methods using '$anvil->Account->X'. =head1 METHODS Methods in the core module; =cut sub new { my $class = shift; my $self = {}; bless $self, $class; return ($self); } # Get a handle on the Anvil::Tools object. I know that technically that is a sibling module, but it makes more # sense in this case to think of it as a parent. sub parent { my $self = shift; my $parent = shift; $self->{HANDLE}{TOOLS} = $parent if $parent; # Defend against memory leads. See Scalar::Util'. if (not isweak($self->{HANDLE}{TOOLS})) { weaken($self->{HANDLE}{TOOLS});; } return ($self->{HANDLE}{TOOLS}); } ############################################################################################################# # Public methods # ############################################################################################################# =head2 encrypt_password This takes a string (a new password from a user), generates a salt, appends the salt to the string and hashes that using C<< sys::password::algorithm >>, the re-hashes the string C<< sys::password::hash_count >> times. The default algorithm is 'sha512' and the default rehashing count is '500,000' times. This method returns a hash reference with the following keys; * user_password_hash: The final encrypted hash. * user_salt: The salt created (or used) to generate the hash. * user_algorithm: The algorithm used to compute the hash. * user_hash_count: The number of re-encryptions of the initial hash. If anything goes wrong, all four keys will have empty strings. Parameters =head3 password (required) This is the password (string) to encrypt. =cut sub encrypt_password { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $user_password_hash = defined $parameter->{password} ? $parameter->{password} : ""; my $user_hash_count = $anvil->data->{sys}{password}{hash_count} =~ /^\d+$/ ? $anvil->data->{sys}{password}{hash_count} : 500000; my $user_algorithm = $anvil->data->{sys}{password}{algorithm} ? $anvil->data->{sys}{password}{algorithm} : "sha512"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { password => $anvil->Log->secure ? $user_password_hash : "--", }}); # We'll fill these out below if we succeed. my $answer = { user_password_hash => "", user_salt => "", user_hash_count => "", user_algorithm => "", }; # Make sure we got a string if (not $user_password_hash) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Account->encrypt_password()", parameter => "password" }}); return($answer); } # Generate a salt. my $user_salt = $anvil->Get->_salt; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_salt => $user_salt }}); ### TODO: Look at using/support bcrypt as the default algorithm. Needed RPMs are already in the el7 AN!Repo. # We support sha256, sha384 and sha512, possible new ones later. if ($user_algorithm eq "sha256" ) { $user_password_hash = sha256_base64($user_password_hash.$user_salt); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }}); if ($user_hash_count > 0) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }}); for (1..$user_hash_count) { $user_password_hash = sha256_base64($user_password_hash); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }}); } } elsif ($user_algorithm eq "sha384" ) { $user_password_hash = sha384_base64($user_password_hash.$user_salt); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }}); if ($user_hash_count > 0) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }}); for (1..$user_hash_count) { $user_password_hash = sha384_base64($user_password_hash); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }}); } } elsif ($user_algorithm eq "sha512" ) { $user_password_hash = sha512_base64($user_password_hash.$user_salt); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }}); if ($user_hash_count > 0) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }}); for (1..$user_hash_count) { $user_password_hash = sha512_base64($user_password_hash); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash }}); } } else { # Bash algorith. $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0171", variables => { user_algorithm => $user_algorithm }}); return($answer); } $answer = { user_password_hash => $user_password_hash, user_salt => $user_salt, user_hash_count => $user_hash_count, user_algorithm => $user_algorithm, }; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 'answer->user_password_hash' => $answer->{user_password_hash}, 'answer->user_salt' => $answer->{user_salt}, 'answer->user_hash_count' => $answer->{user_hash_count}, 'answer->user_algorithm' => $answer->{user_algorithm}, }}); return($answer); } =head2 read_details This method takes a user name and, if the user is found, reads in the details and sets C<< sys::user:: >>. If the user is found, C<< 1 >> is returned. If not, C<< 0 >> is returned. Parameters; =head3 user_name (required) This is the user name being searched for. It is case sensitive. =cut sub read_details { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $user_name = defined $parameter->{user_name} ? $parameter->{user_name} : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_name => $user_name }}); my $query = " SELECT user_uuid, user_password_hash, user_salt, user_algorithm, user_hash_count, user_language, user_is_admin, user_is_experienced, user_is_trusted FROM users WHERE user_name = ".$anvil->data->{sys}{use_db_fh}->quote($user_name)." ;"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__}); my $count = @{$results}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { results => $results, count => $count, }}); if (not $count) { # User doesn't exist. return(0); } my $user_uuid = $results->[0]->[0]; my $user_password_hash = $results->[0]->[1]; my $user_salt = $results->[0]->[2]; my $user_algorithm = $results->[0]->[3]; my $user_hash_count = $results->[0]->[4]; my $user_language = $results->[0]->[5]; my $user_is_admin = $results->[0]->[6]; my $user_is_experienced = $results->[0]->[7]; my $user_is_trusted = $results->[0]->[8]; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_uuid => $user_uuid, user_password_hash => $user_password_hash, user_salt => $user_salt, user_algorithm => $user_algorithm, user_hash_count => $user_hash_count, user_language => $user_language, user_is_admin => $user_is_admin, user_is_experienced => $user_is_experienced, user_is_trusted => $user_is_trusted, }}); $anvil->data->{sys}{user}{user_name} = $user_name; $anvil->data->{sys}{user}{user_uuid} = $user_uuid; $anvil->data->{sys}{user}{user_password_hash} = $user_password_hash, $anvil->data->{sys}{user}{user_salt} = $user_salt, $anvil->data->{sys}{user}{user_algorithm} = $user_algorithm, $anvil->data->{sys}{user}{user_hash_count} = $user_hash_count, $anvil->data->{sys}{user}{user_language} = $user_language, $anvil->data->{sys}{user}{user_is_admin} = $user_is_admin, $anvil->data->{sys}{user}{user_is_experienced} = $user_is_experienced, $anvil->data->{sys}{user}{user_is_trusted} = $user_is_trusted, $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { 'sys::user::user_name' => $anvil->data->{sys}{user}{user_name}, 'sys::user::user_uuid' => $anvil->data->{sys}{user}{user_uuid}, 'sys::user::user_password_hash' => $anvil->data->{sys}{user}{user_password_hash}, 'sys::user::user_salt' => $anvil->data->{sys}{user}{user_salt}, 'sys::user::user_algorithm' => $anvil->data->{sys}{user}{user_algorithm}, 'sys::user::user_hash_count' => $anvil->data->{sys}{user}{user_hash_count}, 'sys::user::user_language' => $anvil->data->{sys}{user}{user_language}, 'sys::user::user_is_admin' => $anvil->data->{sys}{user}{user_is_admin}, 'sys::user::user_is_experienced' => $anvil->data->{sys}{user}{user_is_experienced}, 'sys::user::user_is_trusted' => $anvil->data->{sys}{user}{user_is_trusted}, }}); return(1); } =head2 validate_password This method takes a user name and password and checks to see if the password matches. If the password is wrong, or if the user isn't found, C<< 0 >> is returned. If the password matches, C<< 1 >> is returned. Parameters; =head3 password (required) This is the password to test. =head3 user (required) This is the user whose password we're testing. =cut sub validate_password { my $self = shift; my $parameter = shift; my $anvil = $self->parent; my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $password = defined $parameter->{password} ? $parameter->{password} : ""; my $user = defined $parameter->{user} ? $parameter->{user} : ""; my $valid = 0; my $hash = ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { password => $anvil->Log->secure ? $password : "--", user => $user, }}); if (not $password) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Account->validate_password()", parameter => "password" }}); return($valid); } if (not $user) { $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Account->validate_password()", parameter => "user" }}); return($valid); } my $query = " SELECT user_password_hash, user_salt, user_algorithm, user_hash_count FROM users WHERE user_name = ".$anvil->data->{sys}{use_db_fh}->quote($user)." ;"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__}); my $count = @{$results}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { results => $results, count => $count, }}); if (not $count) { # User not found. $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0172", variables => { user => $user }}); return($valid); } my $user_password_hash = $results->[0]->[0]; my $user_salt = $results->[0]->[1]; my $user_algorithm = $results->[0]->[2]; my $user_hash_count = $results->[0]->[3]; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_password_hash => $user_password_hash, user_salt => $user_salt, user_algorithm => $user_algorithm, user_hash_count => $user_hash_count, }}); if ($user_algorithm eq "sha256" ) { $hash = sha256_base64($password.$user_salt); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }}); if ($user_hash_count > 0) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }}); for (1..$user_hash_count) { $hash = sha256_base64($hash); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }}); } } elsif ($user_algorithm eq "sha384" ) { $hash = sha384_base64($password.$user_salt); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }}); if ($user_hash_count > 0) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }}); for (1..$user_hash_count) { $hash = sha384_base64($hash); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }}); } } elsif ($user_algorithm eq "sha512" ) { $hash = sha512_base64($password.$user_salt); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }}); if ($user_hash_count > 0) { $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { user_hash_count => $user_hash_count }}); for (1..$user_hash_count) { $hash = sha512_base64($hash); } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { hash => $hash }}); } } else { # Bad algorith. $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0173", variables => { user_algorithm => $user_algorithm }}); return($valid); } # Test. if ($hash eq $user_password_hash) { # Good password. $valid = 1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { valid => $valid }}); } return($valid); } 1;