#!/usr/bin/perl ### v.2.0.2 by zio 06/4/2012 ### v.3.0.0 by gine 11/10/2015 ### v.3.0.1 by gine 22/10/2015 ### uses # use DBI; #use Crypt::Random; #use Crypt::Passwd::XS qw(unix_sha256_crypt); #use Crypt::Passwd::XS qw(unix_md5_crypt); use Crypt::Passwd::XS; ### The blowfish crypt() scheme is currently unsupported by Crypt::Passwd::XS::crypt() , use Crypt::Eksblowfish::Bcrypt qw(bcrypt); ### so additional Crypt::Eksblowfish::Bcrypt::bcrypt() is being used use Digest::SHA qw(sha256_hex); use Log::Log4perl qw(get_logger :levels); ### conf # $SCRIPT_DIR="/usr/local/ortiche/pureftpd-auth"; $AUTH_DIR="$SCRIPT_DIR/auth"; require "$AUTH_DIR/pureftpd.pl"; $AUTH_LOG="/var/log/pure-ftpd/auth.log"; ### log kept for debug, to remove in future because of privacy !!! $SHA256_DEFAULT_ROUNDS = 5000; ### variables read from environment ( pure-authd ) # $REQ_USER=$ENV{AUTHD_ACCOUNT}; ### TODO: sanitize input ? $REQ_PASS=$ENV{AUTHD_PASSWORD}; # $SERVER_IP=$ENV{AUTHD_LOCAL_IP}; #AUTHD_LOCAL_PORT #AUTHD_REMOTE_IP #AUTHD_ENCRYPTED @numbers = split(/\./, $SERVER_IP); $ip_number = pack("C4", @numbers); ($server_name) = (gethostbyaddr($ip_number, 2))[0]; if (not $server_name) { $server_name=$SERVER_IP; } ### variables shared with subs # my $DB_conn; my $auth_logger; ### always returns with sub auth_return() ... # sub auth_return { my ( $RET_AUTH, $RET_DIR ) = @_; $auth_logger->info("auth_ok:", $RET_AUTH, " , reason: auth terminated, user: ", $REQ_USER); if ( $RET_AUTH == 1 ) ### add extra checks on fields... { print "auth_ok:1\n"; # print "uid:", $RET_UID, "\n"; #### TODO: fix DB and user creation scripts instead!!! # print "gid:", $RET_GID, "\n"; ## print "uid:33\n"; ## print "gid:33\n"; ## print "dir:", $RET_DIR, "\n"; print "end\n"; exit 0; } else { print "auth_ok:", $RET_AUTH, "\n"; print "end\n"; exit 1; } } ### logging subs # sub auth_log_init { # log4perl levels: # DEBUG INFO WARN ERROR FATAL Log::Log4perl->easy_init({ file => ">> $AUTH_LOG", level => $INFO, layout => "[3.0.1] [%d{yy/MM/dd-HH:mm}] [%p] (%F{2} line %L) %m%n", },{ file => "STDERR", level => $DEBUG, layout => "[3.0.1] [%d{yy/MM/dd-HH:mm}] [%p] (%F{2} line %L) %m%n", }); $auth_logger = Log::Log4perl->get_logger(); } ### digests, salts and crypt() subs # sub check_crypt { my ( $plain_password, $hashed_password ) = @_; my $encrypted; if ( $hashed_password =~ /(\$2a\$[0-9]{2}\$[A-Za-z0-9.\/]{22})/ ) { $encrypted = Crypt::Eksblowfish::Bcrypt::bcrypt($plain_password, $hashed_password); ### Blowfish scheme } else { $encrypted = Crypt::Passwd::XS::crypt($plain_password, $hashed_password); ### MD5, SHA256 schemes } return ( $encrypted eq $hashed_password ); } sub sha256_salt { # my $salt = en_base64(Crypt::Random::makerandom_octet(Length=>16)); my @letters = ('A' .. 'Z', 'a' .. 'z', '0' .. '9', '/', '.'); my $salt; for (1 .. 16) { $salt .= $letters[rand@letters]; } return '$5$'.'rounds='.$SHA256_DEFAULT_ROUNDS.'$'.$salt; } #sub migra_update_pw { # my ( $req_user, $req_pass ) = @_; # my $p_sha256 = sha256_hex($req_pass); # my $p_sha256_crypt_salt = Crypt::Passwd::XS::crypt($req_pass, sha256_salt()); # my $UPD_QUERY = "UPDATE $DB_TABLE SET password = NULL, password_sha256 = '$p_sha256', password_crypt = '$p_sha256_crypt_salt' WHERE username = '$req_user'"; # $auth_logger->info("updated password for hrd style: ".$REQ_USER); # return $DB_conn->do($UPD_QUERY); #} #deprecated sub migra_pw { my ( $req_user, $req_pass ) = @_; my $p_sha256_crypt_salt = Crypt::Passwd::XS::crypt($req_pass, sha256_salt()); $auth_logger->info("going to run password migration, user: ".$REQ_USER); my $UPD_QUERY = "UPDATE $DB_TABLE SET password = NULL, password_crypt = '$p_sha256_crypt_salt' WHERE username = '$req_user'"; return $DB_conn->do($UPD_QUERY); } sub update_hrd_pw { my ( $req_user, $req_pass ) = @_; my $p_sha256 = sha256_hex($req_pass); my $UPD_QUERY = "UPDATE $DB_TABLE SET password_sha256 = '$p_sha256' WHERE username = '$req_user'"; $auth_logger->info("updated password for hrd style, user: ".$REQ_USER); return $DB_conn->do($UPD_QUERY); } ### end of subs ### begins setting up log # auth_log_init(); $auth_logger->info("reason: auth begun , user: ", $REQ_USER); $server_name='web01.indivia.net'; #$auth_logger->debug("server ip: '$SERVER_IP'\n"); #$auth_logger->debug("server name: '$server_name'\n"); if ( $REQ_USER eq "" ) { $auth_logger->fatal("reason: empty user"); auth_return(-1, ""); } $DB_conn = DBI->connect("DBI:mysql:database=$DB_DB;host=$DB_HOST", $DB_USER, $DB_PASS) || $auth_logger->fatal("fatal: connect() to DB") && auth_return(-1, ""); ### runs query and checks # my $QUERY_pureftpd = $DB_conn->prepare("SELECT path, password_sha256, password_crypt FROM $DB_TABLE JOIN directories USING (dir_id) JOIN hosts_urls USING (url_id) JOIN hosts USING (host_id) WHERE username='$REQ_USER' AND ftp = 'Y' AND hostname = '$server_name'") || $auth_logger->fatal("prepare() query, reason: no user? no ftp=Y? wrong \$server_name ?, user: ".$REQ_USER) && auth_return(-1, ""); $QUERY_pureftpd->execute() || $auth_logger->fatal("execute() query, reason: no user? no ftp=Y? wrong \$server_name ?, user: ".$REQ_USER) && auth_return(-1, ""); my ( $READ_path, $READ_hrd_sha256, $READ_new_crypt ) = $QUERY_pureftpd->fetchrow_array(); ### TODO: place here separated checks on ftp = 'Y' AND hostname = '$server_name' ; remove clause from above SELECT ### if ( $READ_path eq "" ) { $auth_logger->warn("warning: empty field PATH"); } if ( $READ_hrd_sha256 eq "" ) { $auth_logger->warn("warning: empty field HRD_SHA256"); } if ( $READ_new_crypt eq "" ) { $auth_logger->warn("warning: empty field NEW_CRYPT"); } if ( $QUERY_pureftpd->rows == 0 ) { $auth_logger->error("user not found, reason: no user? no ftp=Y ? wrong \$server_name ?, user: ".$REQ_USER); auth_return(0, ""); } else { $auth_logger->info("User: ".$REQ_USER); if (check_crypt($REQ_PASS, $READ_new_crypt)) { $auth_logger->info("auth_ok:1, GOOD pwd match!, user: ".$REQ_USER); auth_return(1, $READ_path); } else { $auth_logger->info("auth_ok:-1, reason: wrong password from check_crypt(), user: ".$REQ_USER); auth_return(-1, ""); } }