use Authen::Passphrase;
use Authen::Passphrase::BlowfishCrypt;
# https://rt.cpan.org/Ticket/Display.html?id=72743
+use Data::Password qw(:all);
-our $DEBUG = 1;
+our $DEBUG = 0;
our $conf;
FS::UID->install_callback( sub {
$conf = FS::Conf->new;
#eval "use Authen::Passphrase::BlowfishCrypt;";
});
+our @pw_set;
+
our $me = '[' . __PACKAGE__ . ']';
our $BLOWFISH_COST = 10;
This MUST NOT be called from check(). It should be called by the office UI,
self-service ClientAPI, or other I<user-interactive> code that processes a
password change, and only if the user has taken some action with the intent
-of changing the password.
+of setting the password.
=cut
my $self = shift;
my $password = shift;
- # check length and complexity here
+ my $cust_main = $self->cust_main;
+
+ # workaround for non-inserted services
+ if ( !$cust_main and $self->get('pkgnum') ) {
+ my $cust_pkg = FS::cust_pkg->by_key($self->get('pkgnum'));
+ $cust_main = $cust_pkg->cust_main if $cust_pkg;
+ }
+ # selfservice signup invokes this without customer, but it checks this conf separately
+ warn "is_password_allowed: no customer could be identified" if !$cust_main;
+ return '' if $cust_main && $conf->config_bool('password-insecure', $cust_main->agentnum);
+
+ # basic checks using Data::Password;
+ # options for Data::Password
+ $DICTIONARY = 0; # minimum length of disallowed words, false value disables dictionary checking
+ $MINLEN = $conf->config('passwordmin') || 6;
+ $MAXLEN = $conf->config('passwordmax') || 8;
+ $GROUPS = 4; # must have all 4 'character groups': numbers, symbols, uppercase, lowercase
+ # other options use the defaults listed below:
+ # $FOLLOWING = 3; # disallows more than 3 chars in a row, by alphabet or keyboard (ie abcd or asdf)
+ # $SKIPCHAR = undef; # set to true to skip checking for bad characters
+ # # lists of disallowed words
+ # @DICTIONARIES = qw( /usr/share/dict/web2 /usr/share/dict/words /usr/share/dict/linux.words );
+
+ # first, no dictionary checking but require 4 char groups
+ my $error = IsBadPassword($password);
+
+ # but they can get away with 3 char groups, so long as they're not using a word
+ if ($error eq 'contains less than 4 character groups') {
+ $DICTIONARY = 4; # default from Data::Password is 5
+ $GROUPS = 3;
+ $error = IsBadPassword($password);
+ # take note--we never actually report dictionary word errors;
+ # 4 char groups is the rule, 3 char groups and no dictionary words is an acceptable exception
+ $error = 'should contain at least one each of numbers, symbols, lowercase and uppercase letters'
+ if $error;
+ }
+
+ # maybe also at some point add an exception for any passwords of sufficient length,
+ # see https://xkcd.com/936/
+
+ $error = 'Invalid password - ' . $error if $error;
+ return $error if $error;
+
+ #check against service fields
+ $error = $self->password_svc_check($password);
+ return $error if $error;
+
+ return '' unless $self->get($self->primary_key); # for validating new passwords pre-insert
+
+ #check against customer fields
+ if ($cust_main) {
+ my @words;
+ # words from cust_main
+ foreach my $field ( qw( last first daytime night fax mobile ) ) {
+ push @words, split(/\W/,$cust_main->get($field));
+ }
+ # words from cust_location
+ foreach my $loc ($cust_main->cust_location) {
+ foreach my $field ( qw(address1 address2 city county state zip) ) {
+ push @words, split(/\W/,$loc->get($field));
+ }
+ }
+ # do the actual checking
+ foreach my $word (@words) {
+ next unless length($word) > 2;
+ if ($password =~ /$word/i) {
+ return qq(Password contains account information '$word');
+ }
+ }
+ }
if ( $conf->config('password-no_reuse') =~ /^(\d+)$/ ) {
'';
}
+=item password_svc_check
+
+Override to run additional service-specific password checks.
+
+=cut
+
+sub password_svc_check {
+ my ($self, $password) = @_;
+ return '';
+}
+
=item password_history_key
Returns the name of the field in L<FS::password_history> that's the foreign
$auth = $self->_blowfishcrypt( $auth->passphrase );
}
- } elsif ( $encoding eq 'plain' ) {
+ } else {
+ warn "unrecognized password encoding '$encoding'; treating as plain text"
+ unless $encoding eq 'plain';
$auth = $self->_blowfishcrypt( $password );
}
+=item delete_password_history;
+
+Removes all password history records attached to this object, in preparation
+to delete the object.
+
+=cut
+
+sub delete_password_history {
+ my $self = shift;
+ my @records = qsearch('password_history', {
+ $self->password_history_key => $self->get($self->primary_key)
+ });
+ my $error = '';
+ foreach (@records) {
+ $error ||= $_->delete;
+ }
+ return $error . ' (clearing password history)' if $error;
+ '';
+}
+
=item _blowfishcrypt PASSWORD
For internal use: takes PASSWORD and returns a new
=back
+=head1 CLASS METHODS
+
+=over 4
+
+=item pw_set
+
+Returns the list of characters allowed in random passwords (from the
+C<password-generated-characters> config).
+
+=cut
+
+sub pw_set {
+ my $class = shift;
+ if (!@pw_set) {
+ my $pw_set = $conf->config('password-generated-characters');
+ $pw_set =~ s/\s//g; # don't ever allow whitespace
+ if ( $pw_set =~ /[[:lower:]]/
+ && $pw_set =~ /[[:upper:]]/
+ && $pw_set =~ /[[:digit:]]/
+ && $pw_set =~ /[[:punct:]]/ ) {
+ @pw_set = split('', $pw_set);
+ } else {
+ warn "password-generated-characters set is insufficient; using default.";
+ @pw_set = split('', 'abcdefghijkmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ23456789()#.,');
+ }
+ }
+ return @pw_set;
+}
+
+=back
+
=head1 SEE ALSO
L<FS::password_history>