package Qpsmtpd::Auth; # See the documentation in 'perldoc docs/authentication.pod' use strict; use warnings; use Qpsmtpd::Constants; use Digest::HMAC_MD5 qw(hmac_md5_hex); use MIME::Base64; sub e64 { my ($arg) = @_; my $res = encode_base64($arg); chomp($res); return $res; } sub SASL { # $DB::single = 1; my ($session, $mechanism, $prekey) = @_; my ($user, $passClear, $passHash, $ticket, $loginas); if ($mechanism eq 'plain') { ($loginas, $user, $passClear) = get_auth_details_plain($session, $prekey); return DECLINED if !$user || !$passClear; } elsif ($mechanism eq 'login') { ($user, $passClear) = get_auth_details_login($session, $prekey); return DECLINED if !$user || !$passClear; } elsif ($mechanism eq 'cram-md5') { ($ticket, $user, $passHash) = get_auth_details_cram_md5($session); return DECLINED if !$user || !$passHash; } else { #this error is now caught in SMTP.pm's sub auth $session->respond(500, "Internal server error"); return DECLINED; } # try running the specific hooks first my ($rc, $msg) = $session->run_hooks("auth-$mechanism", $mechanism, $user, $passClear, $passHash, $ticket); # try running the polymorphous hooks next if (!$rc || $rc == DECLINED) { ($rc, $msg) = $session->run_hooks("auth", $mechanism, $user, $passClear, $passHash, $ticket); } if ($rc == OK) { $msg = uc($mechanism) . " authentication successful for $user" . ($msg ? " - $msg" : ''); $session->respond(235, $msg); $session->connection->relay_client(1); if ($session->connection->notes('naughty')) { $session->log(LOGINFO, "auth success cleared naughty"); $session->connection->notes('naughty', 0); } $session->log(LOGDEBUG, $msg); # already logged by $session->respond $session->{_auth_user} = $user; $session->{_auth_mechanism} = $mechanism; s/[\r\n].*//s for ($session->{_auth_user}, $session->{_auth_mechanism}); return OK; } else { $msg = uc($mechanism) . " authentication failed for $user" . ($msg ? " - $msg" : ''); $session->respond(535, $msg); $session->log(LOGDEBUG, $msg); # already logged by $session->respond return DENY; } } sub get_auth_details_plain { my ($session, $prekey) = @_; if (!$prekey) { $session->respond(334, ' '); $prekey = ; } my ($loginas, $user, $passClear) = split /\x0/, decode_base64($prekey); if (!$user) { if ($loginas) { $session->respond(535, "Authentication invalid ($loginas)"); } else { $session->respond(535, "Authentication invalid"); } return; } # Authorization ID must not be different from Authentication ID if ($loginas ne '' && $loginas ne $user) { $session->respond(535, "Authentication invalid for $user"); return; } return $loginas, $user, $passClear; } sub get_auth_details_login { my ($session, $prekey) = @_; my $user; if ($prekey) { $user = decode_base64($prekey); } else { $user = get_base64_response($session, 'Username:') or return; } my $passClear = get_base64_response($session, 'Password:') or return; return $user, $passClear; } sub get_auth_details_cram_md5 { my ($session, $ticket) = @_; if (!$ticket) { # ticket is only passed in during testing # rand() is not cryptographic, but we only need to generate a globally # unique number. The rand() is there in case the user logs in more than # once in the same second, or if the clock is skewed. $ticket = sprintf('<%x.%x@%s>', rand(1000000), time(), $session->config('me')); } # send the base64 encoded ticket $session->respond(334, encode_base64($ticket, '')); my $line = ; if ($line eq '*') { $session->respond(501, "Authentication canceled"); return; } my ($user, $passHash) = split(/ /, decode_base64($line)); unless ($user && $passHash) { $session->respond(504, "Invalid authentication string"); return; } $session->{auth}{ticket} = $ticket; return $ticket, $user, $passHash; } sub get_base64_response { my ($session, $question) = @_; $session->respond(334, e64($question)); my $answer = decode_base64(); if ($answer eq '*') { $session->respond(501, "Authentication canceled"); return; } return $answer; } sub validate_password { my ($self, %a) = @_; my ($pkg, $file, $line) = caller(); $file = (split /\//, $file)[-1]; # strip off the path my $src_clear = $a{src_clear}; my $src_crypt = $a{src_crypt}; my $attempt_clear = $a{attempt_clear}; my $attempt_hash = $a{attempt_hash}; my $method = $a{method} or die "missing method"; my $ticket = $a{ticket} || $self->{auth}{ticket}; my $deny = $a{deny} || DENY; if (!$src_crypt && !$src_clear) { $self->log(LOGINFO, "fail: missing password"); return $deny, "$file - no such user"; } if (!$src_clear && $method =~ /CRAM-MD5/i) { $self->log(LOGINFO, "skip: cram-md5 not supported w/o clear pass"); return DECLINED, $file; } if (defined $attempt_clear) { if ($src_clear && $src_clear eq $attempt_clear) { $self->log(LOGINFO, "pass: clear match"); return OK, $file; } if ($src_crypt && $src_crypt eq crypt($attempt_clear, $src_crypt)) { $self->log(LOGINFO, "pass: crypt match"); return OK, $file; } } if (defined $attempt_hash && $src_clear) { if (!$ticket) { $self->log(LOGERROR, "skip: missing ticket"); return DECLINED, $file; } if ($attempt_hash eq hmac_md5_hex($ticket, $src_clear)) { $self->log(LOGINFO, "pass: hash match"); return OK, $file; } } $self->log(LOGINFO, "fail: wrong password"); return $deny, "$file - wrong password"; } # tag: qpsmtpd plugin that sets RELAYCLIENT when the user authenticates 1;