From: Russ Allbery Date: Wed, 5 Feb 2014 01:30:39 +0000 (-0800) Subject: Add password history implementation for Heimdal X-Git-Tag: release/3.0~39 X-Git-Url: https://git.eyrie.org/?a=commitdiff_plain;h=193fba8fdf499c6742233ece9762608c5dd770b8;p=kerberos%2Fkrb5-strength.git Add password history implementation for Heimdal A password history implementation for Heimdal is now included. This is a separate Perl program, heimdal-history, that stacks with the external program implementation of strength checking. It is not available in the form of a plugin, only as a Heimdal external password quality check. (MIT Kerberos provides its own password history mechanism.) This program has more extensive Perl module dependencies than the other programs in this distribution. --- diff --git a/Makefile.am b/Makefile.am index c1f2e12..14c8f44 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,7 +1,7 @@ # Automake makefile for krb5-strength. # # Written by Russ Allbery -# Copyright 2007, 2009, 2010, 2012, 2013 +# Copyright 2007, 2009, 2010, 2012, 2013, 2014 # The Board of Trustees of the Leland Stanford Junior University # # See LICENSE for licensing terms. @@ -82,10 +82,11 @@ tools_heimdal_strength_LDADD += util/libutil.a portable/libportable.la \ $(KRB5_LIBS) $(CDB_LIBS) # Other tools. -dist_bin_SCRIPTS = tools/cdbmake-wordlist +dist_bin_SCRIPTS = tools/cdbmake-wordlist tools/heimdal-history # Man pages for all tools. -dist_man_MANS = tools/heimdal-strength.1 tools/cdbmake-wordlist.1 +dist_man_MANS = tools/heimdal-history.1 tools/heimdal-strength.1 \ + tools/cdbmake-wordlist.1 # Handle the standard stuff that make maintainer-clean should probably remove # but doesn't. This breaks the GNU coding standard, but in this area the GNU diff --git a/NEWS b/NEWS index 4c3b208..e8c73f1 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,15 @@ User-Visible krb5-strength Changes +krb5-strength 3.0 (unreleased) + + A password history implementation for Heimdal is now included. This + is a separate Perl program, heimdal-history, that stacks with the + external program implementation of strength checking. It is not + available in the form of a plugin, only as a Heimdal external password + quality check. (MIT Kerberos provides its own password history + mechanism.) This program has more extensive Perl module dependencies + than the other programs in this distribution. + krb5-strength 2.2 (2013-12-16) More complex character class requirements can be specified with the diff --git a/README b/README index a5cbc36..7ca26f0 100644 --- a/README +++ b/README @@ -13,13 +13,14 @@ BLURB krb5-strength provides a password quality plugin for the MIT Kerberos - KDC (specifically the kadmind server) and an external password quality - program for use with the Heimdal kpasswdd server. Passwords can be - tested with CrackLib, checked against a CDB database of known weak - passwords, checked for length, checked for non-printable or non-ASCII - characters that may be difficult to enter reproducibly, required to - contain particular character classes, or any combination of these tests. - It supports both Heimdal and MIT Kerberos (1.9 or later). + KDC (specifically the kadmind server), an external password quality + program for use with Heimdal, and a password history implementation for + use with Heimdal. Passwords can be tested with CrackLib, checked + against a CDB database of known weak passwords, checked for length, + checked for non-printable or non-ASCII characters that may be difficult + to enter reproducibly, required to contain particular character classes, + or any combination of these tests. It supports both Heimdal and MIT + Kerberos (1.9 or later). DESCRIPTION @@ -66,6 +67,13 @@ DESCRIPTION behavior, at which point this package can likely wither away in favor of much simpler plugins that link to the standard CrackLib library. + krb5-strength also includes a password history implementation for + Heimdal. This is separate from the password strength implementation but + can be stacked with it so that both strength and history checks are + performed. This history implementation is available only via the + Heimdal external password quality interface. MIT Kerberos includes its + own password history implementation. + REQUIREMENTS For Heimdal, you may use either the external password quality check @@ -107,12 +115,22 @@ REQUIREMENTS bulky, often covered by murky copyrights, and easily locatable on the Internet with a modicum of searching, none are included in this toolkit. - To run the test suite, you will also need Perl 5.006 or later. The - following additional Perl modules will be used by the test suite if - present: + The password history program, heimdal-history, requires Perl 5.010 or + later plus the following CPAN modules: + DB_File::Lock + Crypt::PBKDF2 + Getopt::Long::Descriptive IPC::Run JSON + Readonly + + and their dependencies. + + To run the test suite, you will need Perl 5.010 or later and the + dependencies of the heimdal-history program. The following additional + Perl modules will be used by the test suite if present: + Perl6::Slurp Test::MinimumVersion Test::Perl::Critic @@ -274,6 +292,30 @@ CONFIGURATION pass the --check-library argument to kpasswdd specifying the library to load. + Additional configuration is required to use the history implementation. + Ensure that its dependencies are installed, and then examine the local + configuration settings at the top of the heimdal-history program. By + default, it requires a _history user and _history group be present on + the system, and all history information will be read and written as that + user and group. It also requires a nobody user and nogroup group to be + present, and all strength checking will be done as that user and group. + It uses various files in /var/lib/heimdal-history to store history and + statistical information by default, so if using the defaults, create + that directory and ensure it is writable by the _history user. + + Once that setup is done, change your [password_quality] configuration + to: + + [password_quality] + policies = external-check + external_program = /usr/local/bin/heimdal-history + + The heimdal-history program will automatically also run heimdal-strength + as well, looking for it in /usr/local/bin, /usr/bin, and /bin. Change + the PATH setting at the top of the script if you have different + requirements. You should continue to configure heimdal-strength as if + you were running it directly. + MIT Kerberos To add this module to the list of password quality checks, add a section diff --git a/autogen b/autogen index d30d3de..f53f66c 100755 --- a/autogen +++ b/autogen @@ -11,6 +11,8 @@ autoreconf -i --force version=`grep '^krb5-strength' NEWS | head -1 | cut -d' ' -f2` pod2man --release="$version" --center='krb5-strength' \ tools/cdbmake-wordlist > tools/cdbmake-wordlist.1 +pod2man --release="$version" --center='krb5-strength' \ + tools/heimdal-history > tools/heimdal-history.1 pod2man --release="$version" --center='krb5-strength' \ tools/heimdal-strength.pod > tools/heimdal-strength.1 diff --git a/tests/data/perl.conf b/tests/data/perl.conf index 68b6b64..6d91bc9 100644 --- a/tests/data/perl.conf +++ b/tests/data/perl.conf @@ -6,10 +6,7 @@ @STRICT_IGNORE = qw(cracklib); # Default minimum version requirement for included Perl scripts. -$MINIMUM_VERSION = '5.006'; - -# Scripts used only by autogen can use a newer version. -%MINIMUM_VERSION = ('5.010' => ['tests/data']); +$MINIMUM_VERSION = '5.010'; # File must end with this line. 1; diff --git a/tools/heimdal-history b/tools/heimdal-history new file mode 100755 index 0000000..1309e90 --- /dev/null +++ b/tools/heimdal-history @@ -0,0 +1,764 @@ +#!/usr/bin/perl +# +# Password history via Heimdal external strength checking. +# +# This script is meant to be called via the Heimdal external password strength +# checking interface and maintains per-user password history that also rejects +# one-character permutations. Password history is stored as Crypt::PBKDF2 +# hashes with random salt for each password. +# +# Written by Russ Allbery +# Copyright 2013, 2014 +# The Board of Trustees of the Leland Stanford Junior University + +############################################################################## +# Declarations and configuration +############################################################################## + +require 5.010; +use autodie; +use strict; +use warnings; + +use DB_File::Lock; +use Crypt::PBKDF2; +use Fcntl qw(O_CREAT O_RDWR); +use File::Basename qw(basename); +use Getopt::Long::Descriptive qw(describe_options); +use IPC::Run qw(run); +use JSON qw(encode_json decode_json); +use POSIX qw(setgid setuid); +use Readonly; +use Sys::Syslog qw(openlog syslog LOG_AUTH LOG_INFO LOG_WARNING); + +# The most convenient interface to Berkeley DB files is ties. +## no critic (Miscellanea::ProhibitTies) + +# The number of PBKDF2 iterations to use when hashing passwords. This number +# should be chosen so as to force the hash operation to take approximately 0.1 +# seconds on current hardware. +Readonly my $HASH_ITERATIONS => 65536; + +# Path to the history database. Currently, this must be a Berkeley DB file in +# the old DB_HASH format. Keys will be principal names, and values will be a +# JSON array of hashes. Each hash will have two keys: timestamp, which holds +# the seconds since UNIX epoch at which the history entry was stored, and +# hash, which holds the Crypt::PBKDF2 LDAP-style password hash. +Readonly my $HISTORY_PATH => '/var/lib/heimdal-history/history.db'; + +# User and group used to do all password history lookups and writes, assuming +# that this program is invoked as root and can therefore change UID and GID. +Readonly my $HISTORY_USER => '_history'; +Readonly my $HISTORY_GROUP => '_history'; + +# Path to the Berkeley DB file (DB_HASH format) that stores statistics on +# password length of accepted passwords. Each successful password validation +# will increase the counter for that length. This is read and written with +# $HISTORY_USER and $HISTORY_GROUP. +Readonly my $LENGTH_STATS_PATH => '/var/lib/heimdal-history/lengths.db'; + +# The message to return to the user if we reject the password because it was +# found in the user's history. +Readonly my $REJECT_MESSAGE => 'password was previously used'; + +# The path to the external strength checking program to run. This is done +# first before checking history, and if it fails, that failure is returned as +# the failure for this program. +Readonly my $STRENGTH_PROGRAM => '/usr/bin/heimdal-strength'; + +# User and group used to do password strength checking. Generally, this +# doesn't require any privileges since the strength dictionary is +# world-readable. +Readonly my $STRENGTH_USER => 'nobody'; +Readonly my $STRENGTH_GROUP => 'nogroup'; + +############################################################################## +# Utility functions +############################################################################## + +# Change real and effective UID and GID to those for the given user and group. +# Does nothing if not running as root. +# +# $user - User to change the UID to +# $group - Group to change the GID to (and clear all supplemental groups) +# +# Returns: undef +# Throws: Text exception on any failure +sub drop_privileges { + my ($user, $group) = @_; + + # If running as root, drop privileges. Fail if we can't get the UID and + # GID corresponding to our users. + if ($> == 0 || $< == 0) { + my $uid = getpwnam($user) + or die "$0: cannot get UID for $user\n"; + my $gid = getgrnam($group) + or die "$0: cannot get GID for $group\n"; + setgid($gid) or die "$0: cannot setgid to $gid: $!\n"; + setuid($uid) or die "$0: cannot setuid to $uid: $!\n"; + if ($> == 0 || $< == 0) { + die "$0: failed to drop permissions\n"; + } + } + return; +} + +############################################################################## +# Logging +############################################################################## + +# Given a list of keys and values for a log message as a hash reference, +# return in encoded format following our logging protocol. The log format is +# a set of = parameters separated by a space. Values containing +# whitespace are quoted with double quotes, with any internal double quotes +# doubled. +# +# Here also is defined a custom sort order for the encoded key/value pairs to +# keep them in a reasonable order for a human to read. +# +# $params_ref - Reference to a hash of key/value pairs +# +# Returns: The encoded format as a string +sub encode_log_message { + my ($params_ref) = @_; + + # Define the custom sort order for keys. + my $order = 1; + my %order + = map { $_ => $order++ } qw(action principal error result reason); + + # Build the message from the parameters. + my $message; + for my $key (sort { $order{$a} <=> $order{$b} } keys %{$params_ref}) { + my $value = $params_ref->{$key}; + $value =~ s{ \" }{\"\"}xmsg; + if ($value =~ m{ [ \"] }xms) { + $value = qq{"$value"}; + } + $message .= qq{$key=$value }; + } + chomp($message); + return $message; +} + +# Log a non-fatal error encountered while trying to check or store password +# history. This is used for errors where the password is accepted, but we ran +# into some anomalous event such as corrupted history data that should be +# drawn to the attention of an administrator. The log format is a set of +# = parameters, with the following keys: +# +# - action: the action performed (currently always "check") +# - principal: the principal to check a password for +# - error: an error message explaining the anomalous situation +# +# Values containing whitespace are quoted with double quotes, with any +# internal double quotes doubled. +# +# $principal - Principal for which we checked a password +# $error - The error message +# +# Returns: undef +sub log_error { + my ($principal, $error) = @_; + my $message = encode_log_message( + action => 'check', + principal => $principal, + error => $error, + ); + syslog(LOG_WARNING, '%s', $message); + return; +} + +# Log the disposition of a particular password strength checking request. All +# log messages are logged through syslog at class info. The log format is a +# set of = parameters, with the following keys: +# +# - action: the action performed (currently always "check") +# - principal: the principal to check a password for +# - result: either "accepted" or "rejected" +# - reason: the reason for a rejection +# +# Values containing whitespace are quoted with double quotes, with any +# internal double quotes doubled. +# +# $principal - Principal for which we checked a password +# $result - "accepted" or "rejected" per above +# $reason - On rejection, the reason +# +# Returns: undef +sub log_result { + my ($principal, $result, $reason) = @_; + + # Create the message. + my %message = ( + action => 'check', + principal => $principal, + result => $result, + ); + if ($result eq 'rejected' && defined($reason)) { + $message{reason} = $reason; + } + my $message = encode_log_message(\%message); + + # Log the message. + syslog(LOG_INFO, '%s', $message); + return; +} + +############################################################################## +# Crypto +############################################################################## + +# Given a password, return the hash for that password. Hashing is done with +# PBKDF2 using SHA-2 as the underlying hash function. As of version 0.133330, +# this uses SHA-256. +# +# $password - Password to hash +# +# Returns: Hash encoded in the LDAP-compatible Crypt::PBKDF2 format +sub password_hash { + my ($password) = @_; + my $hasher = Crypt::PBKDF2->new( + hash_class => 'HMACSHA2', + iterations => $HASH_ITERATIONS, + ); + return $hasher->generate($password); +} + +# Given a password and the password history for the user as a reference to a +# array, check whether that password is found in the history. The history +# array is expected to contain anonymous hashes. The only key of interest is +# the "hash" key, whose value is expected to be a hash in the LDAP-compatible +# Crypt::PBKDF2 format. +# +# Invalid history entries are ignored for the purposes of this check and +# treated as if the entry did not exist. +# +# $principal - Principal to check (solely for logging purposes) +# $password - Password to check +# $history_ref - Reference to array of anonymous hashes with "hash" keys +# +# Returns: True if the password matches one of the history hashes, false +# otherwise +sub is_in_history { + my ($principal, $password, $history_ref) = @_; + my $hasher = Crypt::PBKDF2->new(hash_class => 'HMACSHA2'); + + # Walk the history looking at each hash key. + for my $entry (@{$history_ref}) { + my $hash = $entry->{hash}; + next if !defined($hash); + + # validate throws an exception if the hash is in an invalid format. + # Treat that case the same as a miss, but log it. + if (eval { $hasher->validate($hash, $password) }) { + return 1; + } elsif ($@) { + log_error($principal, "hash validate failed: $@"); + } + } + + # No match. + return; +} + +############################################################################## +# Database +############################################################################## + +# Given a principal and a password, determine whether the password was found +# in the password history for that user. +# +# $path - Path to the history file +# $principal - Principal for which to check history +# $password - Check history for this password +# +# Returns: True if $password is found in history, false otherwise +# Throws: On failure to open, lock, or tie the database +sub check_history { + my ($path, $principal, $password) = @_; + + # Open and lock the database and retrieve the history for the user. + # We have to lock for write so that we can create the database if it + # doesn't already exist. Password change should be infrequent enough + # and our window is fast enough that it shouldn't matter. We do this + # in a separate scope so that the history hash goes out of scope and + # is freed and unlocked. + my $history_json; + { + my %history; + my $mode = O_CREAT | O_RDWR; + tie(%history, 'DB_File::Lock', [$path, $mode, oct(600)], 'write') + or die "$0: cannot open $path: $!\n"; + $history_json = $history{$principal}; + } + + # If there is no history for the user, return the trivial false. + if (!defined($history_json)) { + return; + } + + # Decode history from JSON. If this fails (corrupt history), treat it as + # if the user has no history, but log the error message. + my $history_ref = eval { decode_json($history_json) }; + if (!defined($history_ref)) { + log_error($principal, "history JSON decoding failed: $@"); + return; + } + + # Finally, check the password against the hashes in history. + return is_in_history($principal, $password, $history_ref); +} + +# Write a new history entry to the database given the principal and the +# password to record. History records are stored as JSON arrays of objects, +# with keys "timestamp" and "hash". +# +# $path - Path to the history file +# $principal - Principal for which to check history +# $password - Check history for this password +# +# Returns: undef +# Throws: On failure to open, lock, or tie the database +sub write_history { + my ($path, $principal, $password) = @_; + + # Open and lock the database for write. + my %history; + my $mode = O_CREAT | O_RDWR; + tie(%history, 'DB_File::Lock', [$path, $mode, oct(600)], 'write') + or die "$0: cannot open $path: $!\n"; + + # Read the existing history. If the existing history is corrupt, treat + # that as equivalent to not having any history, but log an error. + my $history_json = $history{$principal}; + my $history_ref; + if (defined($history_json)) { + $history_ref = eval { decode_json($history_json) }; + if ($@) { + log_error($principal, "history JSON decoding failed: $@"); + } + } + if (!defined($history_ref)) { + $history_ref = []; + } + + # Add a new history entry. + my $entry = { timestamp => time(), hash => password_hash($password) }; + unshift(@{$history_ref}, $entry); + + # Store the encoded data back in the history database. + $history{$principal} = encode_json($history_ref); + + # The database is closed and unlocked when %history goes out of scope. + # Unfortunately, we lose on error detection here, since there doesn't + # appear to be a way to determine whether all the writes succeeded. But + # losing a bit of history in the rare error case of failing to write to + # local disk is probably not a big deal. + return; +} + +# Write statistics about password length. Given the length of the password +# and the path to the length statistics database, increments the counter for +# that password length. +# +# Any failure to open or write to the database is ignored, since this is +# considered optional logging and should not block the password change. +# +# $path - Path to the length statistics file +# $length - Length of the accepted password +# +# Returns: undef +sub update_length_counts { + my ($path, $length) = @_; + + # Open and lock the database for write. + my %lengths; + my $mode = O_CREAT | O_RDWR; + tie(%lengths, 'DB_File::Lock', [$path, $mode, oct(600)], 'write') + or return; + + # Write each of the hashes. + $lengths{$length}++; + + # The database is closed and unlocked when %lengths goes out of scope. + return; +} + +############################################################################## +# Heimdal password strength protocol +############################################################################## + +# Run another external password strength checker and return the results. This +# allows us to chain to another program that handles the actual strength +# checking prior to handling history. +# +# $principal - Principal attempting to change their password +# $password - The new password +# +# Returns: Scalar context: true if the password was accepted, false otherwise +# List context: whether the password is okay, the exit status of the +# strength checking program, and the error message if the first +# element is false +# Throws: Text exception on failure to execute the program, or read or write +# from it or to it, or if it fails without an error +sub strength_check { + my ($principal, $password) = @_; + + # Run the external strength checking program. If we're root, we'll run it + # as the strength checking user and group. + my $in = "principal: $principal\nnew-password: $password\nend\n"; + my $init = sub { drop_privileges($STRENGTH_USER, $STRENGTH_GROUP) }; + my ($out, $err); + run([$STRENGTH_PROGRAM, $principal], \$in, \$out, \$err, init => $init); + my $status = ($? >> 8); + + # Check the results. + my $okay = ($status == 0 && $out eq "APPROVED\n"); + + # If the program failed, collect the error message. + if (!$okay) { + if ($err) { + $err =~ s{ \n .* }{}xms; + } else { + die "$0: password strength checking failed without an error\n"; + } + } + + # Return the results. + return wantarray ? ($okay, $err, $status) : $okay; +} + +# Read a Heimdal external password strength checking request from the provided +# file handle and return the principal (ignored for our application) and the +# password. +# +# The protocol expects the following data (without leading whitespace) on +# standard input, in precisely this order: +# +# principal: +# new-password: +# end +# +# There is one and only one space after the colon, and any subsequent spaces +# are part of the value (such as leading spaces in the password). +# +# $fh - File handle from which to read +# +# Returns: Scalar context: the password +# List context: a list of the password and the principal +# Throws: Text exception on any protocol violations or IO errors +sub read_change_data { + my ($fh) = @_; + my @keys = qw(principal new-password); + my %data; + + # Read the data elements we expect. Verify that they come in the correct + # order and the correct format. + local $/ = "\n"; + for my $key (@keys) { + my $line = readline($fh); + if (!defined($line)) { + die "$0: truncated input before $key: $!\n"; + } + chomp($line); + if ($line =~ s{ \A \Q$key\E : [ ] }{}xms) { + $data{$key} = $line; + } else { + die "$0: unrecognized input line before $key\n"; + } + } + + # The final line of input must be a literal "end\n"; + my $line = readline($fh); + if (!defined($line)) { + die "$0: truncated input before end: $!\n"; + } elsif ($line ne "end\n") { + die "$0: unrecognized input line before end\n"; + } + + # Return the results. + my $password = $data{'new-password'}; + my $principal = $data{principal}; + return wantarray ? ($password, $principal) : $password; +} + +############################################################################## +# Main routine +############################################################################## + +# Always flush output. +STDOUT->autoflush; + +# Clean up the script name for error reporting. +my $fullpath = $0; +local $0 = basename($0); + +# Parse the argument list. +my ($opt, $usage) = describe_options( + '%c %o', + ['database|d=s', 'Path to the history database, overriding the default'], + ['help|h', 'Print usage message and exit'], + ['manual|man|m', 'Print full manual and exit'], + ['stats|S=s', 'Path to hash of length statistics'], + ['strength|s=s', 'Path to strength checking program to run'], +); +if ($opt->help) { + print {*STDOUT} $usage->text + or die "$0: cannot write to standard output: $!\n"; + exit(0); +} elsif ($opt->manual) { + say {*STDOUT} 'Feeding myself to perldoc, please wait...' + or die "$0: cannot write to standard output: $!\n"; + exec('perldoc', '-t', $fullpath); +} +my $database = $opt->database || $HISTORY_PATH; +my $stats_db = $opt->stats || $LENGTH_STATS_PATH; + +# Open syslog for result reporting. +openlog($0, 'pid', LOG_AUTH); + +# Read the principal and password that we're supposed to check. +my ($password, $principal) = read_change_data(\*STDIN); + +# Delegate to the external strength checking program. +my ($okay, $error, $status) = strength_check($principal, $password); +if (!$okay) { + log_result($principal, 'rejected', $error); + warn "$error\n"; + exit($status); +} + +# Drop privileges for the rest of the program. +drop_privileges($HISTORY_USER, $HISTORY_GROUP); + +# Hash the password and check history. Exit if a hash is in history. +if (check_history($database, $principal, $password)) { + log_result($principal, 'rejected', $REJECT_MESSAGE); + warn "$REJECT_MESSAGE\n"; + exit(0); +} + +# The password is accepted. Record it, update the length counter, and return +# success. +log_result($principal, 'accepted'); +write_history($database, $principal, $password); +say {*STDOUT} 'APPROVED' + or die "$0: cannot write to standard output: $!\n"; +update_length_counts($stats_db, length($password)); +exit(0); + +__END__ + +############################################################################## +# Documentation +############################################################################## + +=for stopwords +heimdal-history heimdal-strength Heimdal -hm BerkeleyDB timestamps POSIX +whitespace API Allbery sublicense MERCHANTABILITY NONINFRINGEMENT syslog +pseudorandom JSON LDAP-compatible PBKDF2 SHA-256 + +=head1 NAME + +heimdal-history - Password history via Heimdal external strength checking + +=head1 SYNOPSIS + +B [B<-hm>] [B<-d> I] [B<-S> I] + [B<-s> I] [B] + +=head1 DESCRIPTION + +B is an implementation of password history via the +Heimdal external password strength checking interface. It stores separate +history for each principal, hashed using Crypt::PBKDF2 with +randomly-generated salt. (The randomness is from a weak pseudorandom +number generator, not strongly random.) + +Password history is stored in a BerkeleyDB DB_HASH file. The key is the +principal. The value is a JSON array of objects, each of which has two +keys. C contains the time when the history entry was added (in +POSIX seconds since UNIX epoch), and C contains the hash of a +previously-used password in the Crypt::PBKDF2 LDAP-compatible format. +Passwords are hashed using PBKDF2 (from PKCS#5) with SHA-256 as the +underlying hash function using a number of rounds configured in this +script. See L for more information. + +B also checks password strength before checking history. +It does so by invoking another program that also uses the Heimdal external +password strength checking interface. By default, it runs +B. Only if that program approves the password +does it hash it and check history. + +As with any implementation of the Heimdal external password strength +checking protocol, B expects, on standard input: + + principal: + new-password: + end + +(with no leading whitespace). is the principal changing its +password (passed to the other password strength checking program but +otherwise unused here), and is the new password. There must +be exactly one space after the colon. Any subsequent spaces are taken to +be part of the principal or password. + +If invoked as root, B will run the external strength +checking program as user C and group C, and will check +and write to the history database as user C<_history> and group +C<_history>. These users must exist on the system if it is run as root. + +The result of each password check will be logged to syslog (priority +LOG_INFO, facility LOG_AUTH). Each log line will be a set of key/value +pairs in the format C<< I=I >>. The keys are: + +=over 4 + +=item action + +The action performed (currently always C). + +=item principal + +The principal for which a password was checked. + +=item error + +An internal error message that did not stop the history check, but which +may indicate that something is wrong with the history database (such as +corrupted entries or invalid hashes). If this key is present, neither +C nor C will be present. There will be a subsequent log +message from the same invocation giving the final result of the history +check (assuming B doesn't exit with a fatal error). + +=item result + +Either C or C. + +=item reason + +If the password was rejected, the reason for the rejection. + +=back + +The value will be surrounded with double quotes if it contains a double +quote or space. Any double quotes in the value will be doubled, so C<"> +becomes C<"">. + +=head1 OPTIONS + +=over 4 + +=item B<-d> I, B<--database>=I + +Use I as the history database file instead of the default +(F). Primarily used for testing, +since Heimdal won't pass this argument. + +=item B<-h>, B<--help> + +Print a short usage message and exit. + +=item B<-m>, B<--manual>, B<--man> + +Display this manual and exit. + +=item B<-S> I, B<--stats>=I + +Use I as the database file for password length statistics +instead of the default (F). +Primarily used for testing, since Heimdal won't pass this argument. + +=item B<-s> I, B<--strength>=I + +Run I as the external strength-checking program instead +of the default (F). Primarily used for +testing, since Heimdal won't pass this argument. + +=back + +=head1 RETURN STATUS + +On approval of the password, B will print C and +a newline to standard output and exit with status 0. + +If the password is rejected by the strength checking program or if it (or +a version with a single character removed) matches one of the hashes stored +in the password history, B will print the reason for +rejection to standard error and exit with status 0. + +On any internal error, B will print the error to standard +error and exit with a non-zero status. + +=head1 FILES + +=over 4 + +=item F + +The default password strength checking program. This program must follow +the Heimdal external password strength checking API. + +=item F + +The default database path. If B is run as root, this +file needs to be readable and writable by user C<_history> and group +C<_history>. If it doesn't exist, it will be created with mode 0600. + +=item F + +The lock file used to synchronize access to the history database. As with +the history database, if B is run as root, this file +needs to be readable and writable by user C<_history> and group +C<_history>. + +=item F + +The default length statistics path, which will be a BerkeleyDB DB_HASH +file of password lengths to counts of passwords with that length. If +B is run as root, this file needs to be readable and +writable by user C<_history> and group C<_history>. If it doesn't exist, +it will be created with mode 0600. + +=item F + +The lock file used to synchronize access to the length statistics +database. As with the length statistics database, if B +is run as root, this file needs to be readable and writable by user +C<_history> and group C<_history>. + +=back + +=head1 AUTHOR + +Russ Allbery + +=head1 COPYRIGHT AND LICENSE + +Copyright 2013, 2014 The Board of Trustees of the Leland Stanford Junior +University + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +=head1 SEE ALSO + +L, L + +=cut