MT#59449 enhance password validation and handling

* passwords are now validated based on
  - minlen
  - maxlen
  - min lower case chars
  - min uppper case chars
  - min digits
  - min special chars
* Data::Password::zxcvbn is used to calculate
  password score and reject passwords with score < 3 as weak
  (this library is ported from the Dropbox password validation)
* Add password journals and check last used passwords in the journals
* Improve password generator javascript function to generate a password
  with at least 4 of each of the char group types.
* Currently affected are subcriber and admin entry creation or
  modification via UI/API
* NGCP::Utils::Auth add optional bcrypt_cost support as last argument
  for generate_salted_hash and get_usr_salted_pass

Change-Id: I100c25107d91741d5101bc58d29a3fa558b0b017
mr12.5
Kirill Solomko 9 months ago
parent 43d112bd5e
commit d9f283cbc8

1
debian/control vendored

@ -46,6 +46,7 @@ Depends:
libdata-hal-perl, libdata-hal-perl,
libdata-ical-perl, libdata-ical-perl,
libdata-page-perl, libdata-page-perl,
libdata-password-zxcvbn-perl,
libdata-printer-perl, libdata-printer-perl,
libdata-serializer-perl, libdata-serializer-perl,
libdata-structure-util-perl, libdata-structure-util-perl,

@ -57,7 +57,12 @@ sub create_item {
my $item; my $item;
try { try {
my $pass = delete $resource->{password};
$item = $c->model('DB')->resultset('admins')->create($resource); $item = $c->model('DB')->resultset('admins')->create($resource);
NGCP::Panel::Utils::Admin::insert_password_journal(
$c, $item, $pass
);
} catch($e) { } catch($e) {
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create admin.", $e); $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create admin.", $e);
return; return;

@ -66,8 +66,8 @@ sub GET :Allow {
$config{file} = $c->config->{'Plugin::ConfigLoader'}->{file}; $config{file} = $c->config->{'Plugin::ConfigLoader'}->{file};
$config{numbermanagement}->{auto_sync_cli} = $config_internal{numbermanagement}->{auto_sync_cli}; $config{numbermanagement}->{auto_sync_cli} = $config_internal{numbermanagement}->{auto_sync_cli};
$config{numbermanagement}->{auto_allow_cli} = $config_internal{numbermanagement}->{auto_allow_cli}; $config{numbermanagement}->{auto_allow_cli} = $config_internal{numbermanagement}->{auto_allow_cli};
$config{security}->{password_web_validate} = $config_internal{security}->{password_web_validate}; $config{security}->{password}->{web_validate} = $config_internal{security}->{password}{web_validate};
$config{security}->{password_sip_validate} = $config_internal{security}->{password_sip_validate}; $config{security}->{password}->{sip_validate} = $config_internal{security}->{password}{sip_validate};
$config{privileges} = $config_internal{privileges}; $config{privileges} = $config_internal{privileges};
$config{features} = $config_internal{features}; $config{features} = $config_internal{features};

@ -401,6 +401,8 @@ sub POST :Allow {
my $license_max_pbx_groups = $c->license_max_pbx_groups; my $license_max_pbx_groups = $c->license_max_pbx_groups;
my $current_pbx_groups_count = $c->license_current_pbx_groups; my $current_pbx_groups_count = $c->license_current_pbx_groups;
$c->log->debug("Current pbx groups: ". $current_pbx_groups_count);
$c->log->debug("License max pbx groups: ". $license_max_pbx_groups);
if (is_true($resource->{is_pbx_group}) && if (is_true($resource->{is_pbx_group}) &&
$license_max_pbx_groups >= 0 && $current_pbx_groups_count >= $license_max_pbx_groups) { $license_max_pbx_groups >= 0 && $current_pbx_groups_count >= $license_max_pbx_groups) {
$self->error($c, HTTP_FORBIDDEN, $self->error($c, HTTP_FORBIDDEN,

@ -84,6 +84,7 @@ sub PUT :Allow {
my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c,
contract => $subscriber->contract, contract => $subscriber->contract,
); #apply underrun lock level ); #apply underrun lock level
$c->stash->{subscriber} = $subscriber; # password validation
my $resource = $self->get_valid_put_data( my $resource = $self->get_valid_put_data(
c => $c, c => $c,
id => $id, id => $id,
@ -126,6 +127,7 @@ sub PATCH :Allow {
my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c, my $balance = NGCP::Panel::Utils::ProfilePackages::get_contract_balance(c => $c,
contract => $subscriber->contract, contract => $subscriber->contract,
); #apply underrun lock level ); #apply underrun lock level
$c->stash->{subscriber} = $subscriber; # password validation
my $json = $self->get_valid_patch_data( my $json = $self->get_valid_patch_data(
c => $c, c => $c,
id => $id, id => $id,

@ -5,6 +5,7 @@ use parent 'Catalyst::Controller';
use NGCP::Panel::Form; use NGCP::Panel::Form;
use HTTP::Headers qw(); use HTTP::Headers qw();
use NGCP::Panel::Utils::Admin;
use NGCP::Panel::Utils::Message; use NGCP::Panel::Utils::Message;
use NGCP::Panel::Utils::Navigation; use NGCP::Panel::Utils::Navigation;
use NGCP::Panel::Utils::Auth; use NGCP::Panel::Utils::Auth;
@ -147,8 +148,9 @@ sub create :Chained('list_admin') :PathPart('create') :Args(0) :AllowedRole(admi
} else { } else {
$form->values->{reseller_id} = $c->user->reseller_id; $form->values->{reseller_id} = $c->user->reseller_id;
} }
my $password = delete $form->values->{password};
$form->values->{md5pass} = undef; $form->values->{md5pass} = undef;
$form->values->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash(delete $form->values->{password}); $form->values->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash($password);
if ($form->values->{role_id}) { if ($form->values->{role_id}) {
$form->values->%* = ( $form->values->%*, NGCP::Panel::Utils::UserRole::resolve_flags($c, $form->values->{role_id}) ); $form->values->%* = ( $form->values->%*, NGCP::Panel::Utils::UserRole::resolve_flags($c, $form->values->{role_id}) );
@ -156,8 +158,13 @@ sub create :Chained('list_admin') :PathPart('create') :Args(0) :AllowedRole(admi
$form->values->{role_id} = NGCP::Panel::Utils::UserRole::resolve_role_id($c, $form->values); $form->values->{role_id} = NGCP::Panel::Utils::UserRole::resolve_role_id($c, $form->values);
} }
$c->stash->{admins}->create($form->values); my $admin = $c->stash->{admins}->create($form->values);
delete $c->session->{created_objects}->{reseller}; delete $c->session->{created_objects}->{reseller};
NGCP::Panel::Utils::Admin::insert_password_journal(
$c, $admin, $password
);
NGCP::Panel::Utils::Message::info( NGCP::Panel::Utils::Message::info(
c => $c, c => $c,
desc => $c->loc('Administrator successfully created'), desc => $c->loc('Administrator successfully created'),
@ -254,9 +261,10 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
delete $form->values->{reseller}; delete $form->values->{reseller};
} }
delete $form->values->{password} unless length $form->values->{password}; delete $form->values->{password} unless length $form->values->{password};
if(exists $form->values->{password}) { my $password = delete $form->values->{password} // undef;
if ($password) {
$form->values->{md5pass} = undef; $form->values->{md5pass} = undef;
$form->values->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash(delete $form->values->{password}); $form->values->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash($password);
} }
#should be after other fields, to remove all added values, e.g. reseller_id #should be after other fields, to remove all added values, e.g. reseller_id
if($c->stash->{administrator}->login eq NGCP::Panel::Utils::Auth::get_special_admin_login()) { if($c->stash->{administrator}->login eq NGCP::Panel::Utils::Auth::get_special_admin_login()) {
@ -276,6 +284,13 @@ sub edit :Chained('base') :PathPart('edit') :Args(0) {
$c->stash->{administrator}->update($form->values); $c->stash->{administrator}->update($form->values);
delete $c->session->{created_objects}->{reseller}; delete $c->session->{created_objects}->{reseller};
if ($password) {
NGCP::Panel::Utils::Admin::insert_password_journal(
$c, $c->stash->{administrator}, $password
);
}
NGCP::Panel::Utils::Message::info( NGCP::Panel::Utils::Message::info(
c => $c, c => $c,
data => { $c->stash->{administrator}->get_inflated_columns }, data => { $c->stash->{administrator}->get_inflated_columns },

@ -458,7 +458,7 @@ sub reset_webpassword_nosubscriber :Chained('/') :PathPart('resetwebpassword') :
my ($self, $c) = @_; my ($self, $c) = @_;
$c->detach('/denied_page') $c->detach('/denied_page')
unless($c->config->{security}->{password_allow_recovery}); unless($c->config->{security}->{password}{allow_recovery});
my $posted = $c->req->method eq "POST"; my $posted = $c->req->method eq "POST";
my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Subscriber::RecoverPassword", $c); my $form = NGCP::Panel::Form::get("NGCP::Panel::Form::Subscriber::RecoverPassword", $c);
@ -3031,8 +3031,22 @@ sub edit_master :Chained('master') :PathPart('edit') :Args(0) :Does(ACL) :ACLDet
} }
} }
my $prev_password = $prov_subscriber->password;
$prov_subscriber->update($prov_params); $prov_subscriber->update($prov_params);
if ($form->params->{password} && $form->params->{password} ne $prev_password) {
NGCP::Panel::Utils::Subscriber::insert_password_journal(
$c, $prov_subscriber, $form->params->{password}
);
}
if ($form->params->{webpassword}) {
NGCP::Panel::Utils::Subscriber::insert_webpassword_journal(
$c, $prov_subscriber, $form->params->{webpassword}
);
}
my $new_group_ids = defined $form->value->{group_select} ? my $new_group_ids = defined $form->value->{group_select} ?
decode_json($form->value->{group_select}) : []; decode_json($form->value->{group_select}) : [];
NGCP::Panel::Utils::Subscriber::manage_pbx_groups( NGCP::Panel::Utils::Subscriber::manage_pbx_groups(

@ -61,12 +61,12 @@ override 'update_fields' => sub {
} }
if($c->config->{security}->{password_sip_autogenerate}) { if($c->config->{security}->{password}->{sip_autogenerate}) {
# todo: only set to inactive for certain roles, and only if specified in config # todo: only set to inactive for certain roles, and only if specified in config
$self->field('password')->inactive(1); $self->field('password')->inactive(1);
$self->field('password')->required(0); $self->field('password')->required(0);
} }
if($c->config->{security}->{password_web_autogenerate}) { if($c->config->{security}->{password}->{web_autogenerate}) {
# todo: only set to inactive for certain roles, and only if specified in config # todo: only set to inactive for certain roles, and only if specified in config
$self->field('webpassword')->inactive(1); $self->field('webpassword')->inactive(1);
$self->field('webpassword')->required(0); $self->field('webpassword')->required(0);

@ -17,10 +17,10 @@ override 'update_fields' => sub {
super(); super();
if($c->user->roles eq "subscriberadmin") { if($c->user->roles eq "subscriberadmin") {
if(!$c->config->{security}->{password_sip_expose_subadmin}) { if(!$c->config->{security}->{password}->{sip_expose_subadmin}) {
$self->field('password')->inactive(1); $self->field('password')->inactive(1);
} }
if(!$c->config->{security}->{password_web_expose_subadmin}) { if(!$c->config->{security}->{password}->{web_expose_subadmin}) {
$self->field('webpassword')->inactive(1); $self->field('webpassword')->inactive(1);
} }
} }

@ -24,10 +24,10 @@ sub update_fields {
} }
if($c->user->roles eq "subscriberadmin") { if($c->user->roles eq "subscriberadmin") {
if(!$c->config->{security}->{password_sip_expose_subadmin}) { if(!$c->config->{security}->{password}->{sip_expose_subadmin}) {
$self->field('password')->inactive(1); $self->field('password')->inactive(1);
} }
if(!$c->config->{security}->{password_web_expose_subadmin}) { if(!$c->config->{security}->{password}->{web_expose_subadmin}) {
$self->field('webpassword')->inactive(1); $self->field('webpassword')->inactive(1);
} }
} }

@ -54,11 +54,11 @@ sub update_fields {
); );
} }
if($c->config->{security}->{password_sip_autogenerate}) { if($c->config->{security}->{password}->{sip_autogenerate}) {
$self->field('password')->inactive(1); $self->field('password')->inactive(1);
$self->field('password')->required(0); $self->field('password')->required(0);
} }
if($c->config->{security}->{password_web_autogenerate}) { if($c->config->{security}->{password}->{web_autogenerate}) {
$self->field('webpassword')->inactive(1); $self->field('webpassword')->inactive(1);
$self->field('webpassword')->required(0); $self->field('webpassword')->required(0);
} }

@ -242,11 +242,11 @@ sub update_fields {
); );
} }
if($c->config->{security}->{password_sip_autogenerate} || !$c->user->show_passwords) { if($c->config->{security}->{password}->{sip_autogenerate} || !$c->user->show_passwords) {
$self->field('password')->inactive(1); $self->field('password')->inactive(1);
$self->field('password')->required(0); $self->field('password')->required(0);
} }
if($c->config->{security}->{password_web_autogenerate} || !$c->user->show_passwords) { if($c->config->{security}->{password}->{web_autogenerate} || !$c->user->show_passwords) {
$self->field('webpassword')->inactive(1); $self->field('webpassword')->inactive(1);
$self->field('webpassword')->required(0); $self->field('webpassword')->required(0);
} }

@ -167,11 +167,11 @@ sub update_fields {
); );
} }
if($c->config->{security}->{password_sip_autogenerate}) { if($c->config->{security}->{password}->{sip_autogenerate}) {
$self->field('password')->inactive(1); $self->field('password')->inactive(1);
$self->field('password')->required(0); $self->field('password')->required(0);
} }
if($c->config->{security}->{password_web_autogenerate}) { if($c->config->{security}->{password}->{web_autogenerate}) {
$self->field('webpassword')->inactive(1); $self->field('webpassword')->inactive(1);
$self->field('webpassword')->required(0); $self->field('webpassword')->required(0);
} }

@ -202,11 +202,11 @@ sub update_fields {
my $c = $self->ctx; my $c = $self->ctx;
return unless $c; return unless $c;
if($c->config->{security}->{password_sip_autogenerate} and $self->field('password')) { if($c->config->{security}->{password}->{sip_autogenerate} and $self->field('password')) {
$self->field('password')->inactive(1); $self->field('password')->inactive(1);
$self->field('password')->required(0); $self->field('password')->required(0);
} }
if($c->config->{security}->{password_web_autogenerate} and $self->field('webpassword')) { if($c->config->{security}->{password}->{web_autogenerate} and $self->field('webpassword')) {
$self->field('webpassword')->inactive(1); $self->field('webpassword')->inactive(1);
$self->field('webpassword')->required(0); $self->field('webpassword')->required(0);
} }

@ -245,10 +245,10 @@ sub update_fields {
# make sure we don't use contract, as we have customer # make sure we don't use contract, as we have customer
$self->field('contract')->inactive(1); $self->field('contract')->inactive(1);
if($c->config->{security}->{password_sip_autogenerate}) { if($c->config->{security}->{password}->{sip_autogenerate}) {
$self->field('password')->required(0); $self->field('password')->required(0);
} }
if($c->config->{security}->{password_web_autogenerate}) { if($c->config->{security}->{password}->{web_autogenerate}) {
$self->field('webpassword')->required(0); $self->field('webpassword')->required(0);
} }
return; return;

@ -333,11 +333,11 @@ sub update_fields {
# make sure we don't use contract, as we have customer # make sure we don't use contract, as we have customer
$self->field('contract')->inactive(1); $self->field('contract')->inactive(1);
if($c->config->{security}->{password_sip_autogenerate} && $self->field('password')) { if($c->config->{security}->{password}->{sip_autogenerate} && $self->field('password')) {
$self->field('password')->inactive(1); $self->field('password')->inactive(1);
$self->field('password')->required(0); $self->field('password')->required(0);
} }
if($c->config->{security}->{password_web_autogenerate} && $self->field('webpassword')) { if($c->config->{security}->{password}->{web_autogenerate} && $self->field('webpassword')) {
$self->field('webpassword')->inactive(1); $self->field('webpassword')->inactive(1);
$self->field('webpassword')->required(0); $self->field('webpassword')->required(0);
} }

@ -92,7 +92,6 @@ sub process_form_resource {
NGCP::Panel::Utils::API::apply_resource_reseller_id($c, $resource); NGCP::Panel::Utils::API::apply_resource_reseller_id($c, $resource);
my $pass = $resource->{password}; my $pass = $resource->{password};
delete $resource->{password};
if (defined $pass) { if (defined $pass) {
$resource->{md5pass} = undef; $resource->{md5pass} = undef;
$resource->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash($pass); $resource->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash($pass);
@ -195,6 +194,11 @@ sub update_item {
$self->error($c, HTTP_FORBIDDEN, "Only own user can change password"); $self->error($c, HTTP_FORBIDDEN, "Only own user can change password");
return; return;
} }
NGCP::Panel::Utils::Admin::insert_password_journal(
$c, $item, $pass
);
$resource->{md5pass} = undef; $resource->{md5pass} = undef;
$resource->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash($pass); $resource->{saltedpass} = NGCP::Panel::Utils::Auth::generate_salted_hash($pass);
} }

@ -212,10 +212,10 @@ sub resource_from_item {
if ($c->user->roles eq "subscriberadmin") { if ($c->user->roles eq "subscriberadmin") {
$resource{customer_id} = $contract_id; $resource{customer_id} = $contract_id;
if ($item->id != $c->user->voip_subscriber->id) { if ($item->id != $c->user->voip_subscriber->id) {
if (!$c->config->{security}->{password_sip_expose_subadmin}) { if (!$c->config->{security}->{password}->{sip_expose_subadmin}) {
delete $resource{_password}; delete $resource{_password};
} }
if (!$c->config->{security}->{password_web_expose_subadmin}) { if (!$c->config->{security}->{password}->{web_expose_subadmin}) {
delete $resource{_webpassword}; delete $resource{_webpassword};
} }
} }
@ -530,6 +530,18 @@ sub update_item {
contact_id => $resource->{contact_id}, contact_id => $resource->{contact_id},
}; };
if ($resource->{password} && $resource->{password} ne $prov_subscriber->password) {
NGCP::Panel::Utils::Subscriber::insert_password_journal(
$c, $prov_subscriber, $resource->{password}
);
}
if ($resource->{webpassword}) {
NGCP::Panel::Utils::Subscriber::insert_webpassword_journal(
$c, $prov_subscriber, $resource->{webpassword}
);
}
if (exists $resource->{webpassword} and $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS) { if (exists $resource->{webpassword} and $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS) {
$resource->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($resource->{webpassword}); $resource->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($resource->{webpassword});
} }

@ -0,0 +1,65 @@
package NGCP::Panel::Utils::Admin;
use strict;
use warnings;
use Sipwise::Base;
use NGCP::Panel::Utils::Generic qw(:all);
use DBIx::Class::Exception;
use NGCP::Panel::Utils::Auth;
use HTTP::Status qw(:constants);
sub insert_password_journal {
my ($c, $admin, $password) = @_;
my $bcrypt_cost = 6;
my $keep_last_used = $c->config->{security}{password}{web_keep_last_used} // return;
my $rs = $admin->last_passwords->search({
},{
order_by => { '-desc' => 'created_at' },
});
my @delete_ids = ();
my $idx = 0;
foreach my $row ($rs->all) {
$idx++;
$idx >= $keep_last_used ? push @delete_ids, $row->id : next;
}
my $del_rs = $rs->search({
id => { -in => \@delete_ids },
});
$del_rs->delete;
$admin->last_passwords->create({
admin_id => $admin->id,
value => NGCP::Panel::Utils::Auth::generate_salted_hash($password, $bcrypt_cost),
});
$admin->update({ saltedpass_modify_timestamp => \'current_timestamp()' });
}
1;
=head1 NAME
NGCP::Panel::Utils::Admin
=head1 DESCRIPTION
A temporary helper to manipulate admin data
=head1 AUTHOR
Sipwise Development Team <support@sipwise.com>
=head1 LICENSE
This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
# vim: set tabstop=4 expandtab:

@ -30,24 +30,25 @@ sub get_bcrypt_cost {
sub generate_salted_hash { sub generate_salted_hash {
my $pass = shift; my $pass = shift;
my $bcrypt_cost = shift // get_bcrypt_cost();
my $salt = rand_bits($SALT_LENGTH); my $salt = rand_bits($SALT_LENGTH);
my $b64salt = en_base64($salt); my $b64salt = en_base64($salt);
my $b64hash = en_base64(bcrypt_hash({ my $b64hash = en_base64(bcrypt_hash({
key_nul => 1, key_nul => 1,
cost => get_bcrypt_cost(), cost => $bcrypt_cost,
salt => $salt, salt => $salt,
}, $pass)); }, $pass));
return $b64salt . '$' . $b64hash; return $b64salt . '$' . $b64hash;
} }
sub get_usr_salted_pass { sub get_usr_salted_pass {
my ($saltedpass, $pass) = @_; my ($saltedpass, $pass, $opt_cost) = @_;
my ($db_b64salt, $db_b64hash) = split /\$/, $saltedpass; my ($db_b64salt, $db_b64hash) = split /\$/, $saltedpass;
my $salt = de_base64($db_b64salt); my $salt = de_base64($db_b64salt);
my $usr_b64hash = en_base64(bcrypt_hash({ my $usr_b64hash = en_base64(bcrypt_hash({
key_nul => 1, key_nul => 1,
cost => get_bcrypt_cost(), cost => $opt_cost // get_bcrypt_cost(),
salt => $salt, salt => $salt,
}, $pass)); }, $pass));
return $db_b64salt . '$' . $usr_b64hash; return $db_b64salt . '$' . $usr_b64hash;

@ -1,19 +1,19 @@
package NGCP::Panel::Utils::Form; package NGCP::Panel::Utils::Form;
use Sipwise::Base; use Sipwise::Base;
use Crypt::Cracklib; use Data::Password::zxcvbn qw(password_strength);
use NGCP::Panel::Utils::Auth; use NGCP::Panel::Utils::Auth;
sub validate_password { sub validate_password {
my %params = @_; my %params = @_;
my $c = $params{c}; my $c = $params{c};
my $field = $params{field}; my $field = $params{field};
my $r = $c->config->{security}; my $pw = $c->config->{security}{password};
my $utf8 = $params{utf8} // 1; my $utf8 = $params{utf8} // 1;
my $pass = $field->value; my $pass = $field->value;
my $minlen = $r->{password_min_length} // 6; my $minlen = $pw->{min_length} // 12;
my $maxlen = $r->{password_max_length} // 40; my $maxlen = $pw->{max_length} // 40;
if(length($pass) < $minlen) { if(length($pass) < $minlen) {
$field->add_error($c->loc('Must be at minimum [_1] characters long', $minlen)); $field->add_error($c->loc('Must be at minimum [_1] characters long', $minlen));
@ -21,46 +21,101 @@ sub validate_password {
if(length($pass) > $maxlen) { if(length($pass) > $maxlen) {
$field->add_error($c->loc('Must be at maximum [_1] characters long', $maxlen)); $field->add_error($c->loc('Must be at maximum [_1] characters long', $maxlen));
} }
if($r->{password_musthave_lowercase} && $pass !~ /[a-z]/) { if ($pass =~ /\s/) {
$field->add_error($c->loc('Must contain lower-case characters')); $field->add_error($c->loc("Must not contain spaces"));
} }
if($r->{password_musthave_uppercase} && $pass !~ /[A-Z]/) {
$field->add_error($c->loc('Must contain upper-case characters')); if(my $c_check = $pw->{musthave_lowercase}) {
my $count = 0;
map { $_ =~ /^[a-z]$/ and $count++ } split(//, $pass);
if ($count < $c_check) {
$field->add_error($c->loc("Must contain at least $c_check lower-case characters"));
}
}
if(my $c_check = $pw->{musthave_uppercase}) {
my $count = 0;
map { $_ =~ /^[A-Z]$/ and $count++ } split(//, $pass);
if ($count < $c_check) {
$field->add_error($c->loc("Must contain at least $c_check upper-case characters"));
}
}
if(my $c_check = $pw->{musthave_digit}) {
my $count = 0;
map { $_ =~ /^[0-9]$/ and $count++ } split(//, $pass);
if ($count < $c_check) {
$field->add_error($c->loc("Must contain at least $c_check digits"));
} }
if($r->{password_musthave_digit} && $pass !~ /[0-9]/) {
$field->add_error($c->loc('Must contain digits'));
} }
if($r->{password_musthave_specialchar} && $pass !~ /[^0-9a-zA-Z]/) { if(my $c_check = $pw->{musthave_specialchar}) {
$field->add_error($c->loc('Must contain special characters')); my $count = 0;
map { $_ =~ /^[^0-9a-zA-Z]$/ and $count++ } split(//, $pass);
if ($count < $c_check) {
$field->add_error($c->loc("Must contain at least $c_check special characters"));
}
} }
if (!$utf8 && $pass && !NGCP::Panel::Utils::Auth::check_password($pass)) { if (!$utf8 && $pass && !NGCP::Panel::Utils::Auth::check_password($pass)) {
$field->add_error($c->loc('Contains invalid characters')); $field->add_error($c->loc('Contains invalid characters'));
} }
if($field->name eq "password" && $r->{password_sip_validate}) { my $res = password_strength($pass);
if ($res->{score} < 3) {
$field->add_error($c->loc('Password is too weak'));
}
my $lp_rs;
my $check_last_passwords = 0;
my $prov_sub = $c->stash->{subscriber}
? $c->stash->{subscriber}->provisioning_voip_subscriber
: undef;
my $admin = $c->stash->{administrator} // undef;
if($field->name eq "password" && $pw->{sip_validate}) {
my $user; my $user;
if($field->form->field('username')) { if($field->form->field('username')) {
$user = $field->form->field('username')->value; $user = $field->form->field('username')->value;
} elsif($c->stash->{subscriber}) { } elsif($prov_sub) {
$user = $c->stash->{subscriber}->provisioning_voip_subscriber->username; $user = $prov_sub->username;
} if (defined $user && $pass =~ /$user/i) {
if(defined $user && $pass =~ /$user/i) {
$field->add_error($c->loc('Must not contain username')); $field->add_error($c->loc('Must not contain username'));
} }
unless(Crypt::Cracklib::check($pass)) { } elsif($admin) {
$field->add_error($c->loc('Password is too weak')); $user = $admin->login;
if (defined $user && $pass =~ /$user/i) {
$field->add_error($c->loc('Must not contain login'));
}
}
if ($pass && $prov_sub && $pass ne $prov_sub->password) {
$lp_rs = $prov_sub->last_passwords;
$check_last_passwords = 1;
}
if ($pass && $admin) {
$lp_rs = $admin->last_passwords;
$check_last_passwords = 1;
} }
} elsif($field->name eq "webpassword" && $r->{password_web_validate}) { } elsif($field->name eq "webpassword" && $pw->{web_validate}) {
my $user; my $user;
if($field->form->field('webusername')) { if($field->form->field('webusername')) {
$user = $field->form->field('webusername')->value; $user = $field->form->field('webusername')->value;
} elsif($c->stash->{subscriber}) { } elsif($prov_sub) {
$user = $c->stash->{subscriber}->provisioning_voip_subscriber->webusername; $user = $prov_sub->webusername;
} }
if(defined $user && $pass =~ /$user/i) { if(defined $user && $pass =~ /$user/i) {
$field->add_error($c->loc('Must not contain username')); $field->add_error($c->loc('Must not contain username'));
} }
unless(Crypt::Cracklib::check($pass)) { if ($pass && $prov_sub) {
$field->add_error($c->loc('Password is too weak')); $lp_rs = $prov_sub->last_webpasswords;
$check_last_passwords = 1;
}
}
if ($check_last_passwords) {
my $bcrypt_cost = 6;
foreach my $row ($lp_rs->all) {
my $last_password = $row->value;
my $enc_pass = $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS
? NGCP::Panel::Utils::Auth::get_usr_salted_pass($last_password, $pass, $bcrypt_cost)
: $pass;
if ($last_password eq $enc_pass) {
$field->add_error($c->loc('Password was previously used'));
last;
}
} }
} }
} }

@ -8,6 +8,7 @@ use NGCP::Panel::Utils::Generic qw(:all);
use DBIx::Class::Exception; use DBIx::Class::Exception;
use String::MkPasswd; use String::MkPasswd;
use NGCP::Panel::Utils::Auth;
use NGCP::Panel::Utils::DateTime; use NGCP::Panel::Utils::DateTime;
use NGCP::Panel::Utils::Preferences; use NGCP::Panel::Utils::Preferences;
use NGCP::Panel::Utils::Email; use NGCP::Panel::Utils::Email;
@ -643,20 +644,20 @@ sub create_subscriber {
die("invalid timezone name '$params->{timezone}' detected"); die("invalid timezone name '$params->{timezone}' detected");
} }
my $passlen = $c->config->{security}->{password_min_length} || 8; my $passlen = $c->config->{security}->{password}->{min_length} || 12;
if($c->config->{security}->{password_sip_autogenerate} and not defined $params->{password}) { if($c->config->{security}->{password}->{sip_autogenerate} and not defined $params->{password}) {
$params->{password} = String::MkPasswd::mkpasswd( $params->{password} = String::MkPasswd::mkpasswd(
-length => $passlen, -length => $passlen,
-minnum => 1, -minlower => 1, -minupper => 1, -minspecial => 1, -minnum => 3, -minlower => 3, -minupper => 3, -minspecial => 3,
-distribute => 1, -fatal => 1, -distribute => 1, -fatal => 1,
); );
#otherwise it breaks xml device configs #otherwise it breaks xml device configs
$params->{password} =~s/[<>&]/,/g; $params->{password} =~s/[<>&]/,/g;
} }
if($c->config->{security}->{password_web_autogenerate} and not defined $params->{webpassword}) { if($c->config->{security}->{password}->{web_autogenerate} and not defined $params->{webpassword}) {
$params->{webpassword} = String::MkPasswd::mkpasswd( $params->{webpassword} = String::MkPasswd::mkpasswd(
-length => $passlen, -length => $passlen,
-minnum => 1, -minlower => 1, -minupper => 1, -minspecial => 1, -minnum => 3, -minlower => 3, -minupper => 3, -minspecial => 3,
-distribute => 1, -fatal => 1, -distribute => 1, -fatal => 1,
); );
} }
@ -697,12 +698,15 @@ sub create_subscriber {
primary_number_id => undef, # will be filled in next step primary_number_id => undef, # will be filled in next step
contact_id => $contact ? $contact->id : undef, contact_id => $contact ? $contact->id : undef,
}); });
unless(exists $params->{password}) { unless(exists $params->{password}) {
my ($pass_bin, $pass_str); my ($pass_bin, $pass_str);
UUID::generate($pass_bin); UUID::generate($pass_bin);
UUID::unparse($pass_bin, $pass_str); UUID::unparse($pass_bin, $pass_str);
$params->{password} = $pass_str; $params->{password} = $pass_str;
} }
my $raw_webpassword = $params->{webpassword} // undef;
if (exists $params->{webpassword} and $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS) { if (exists $params->{webpassword} and $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS) {
$params->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($params->{webpassword}); $params->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($params->{webpassword});
} }
@ -726,6 +730,18 @@ sub create_subscriber {
create_timestamp => NGCP::Panel::Utils::DateTime::current_local, create_timestamp => NGCP::Panel::Utils::DateTime::current_local,
}); });
if ($params->{password}) {
NGCP::Panel::Utils::Subscriber::insert_password_journal(
$c, $prov_subscriber, $params->{password}
);
}
if ($raw_webpassword) {
NGCP::Panel::Utils::Subscriber::insert_webpassword_journal(
$c, $prov_subscriber, $raw_webpassword,
);
}
my $aliases_before = NGCP::Panel::Utils::Events::get_aliases_snapshot( my $aliases_before = NGCP::Panel::Utils::Events::get_aliases_snapshot(
c => $c, c => $c,
schema => $schema, schema => $schema,
@ -2674,6 +2690,73 @@ sub get_subscribers_count {
return $rs->count(); return $rs->count();
} }
sub insert_password_journal {
my ($c, $prov_sub, $password) = @_;
my $bcrypt_cost = 6;
my $keep_last_used = $c->config->{security}{password}{sip_keep_last_used} // return;
my $rs = $prov_sub->last_passwords->search({
},{
order_by => { '-desc' => 'created_at' },
});
my @delete_ids = ();
my $idx = 0;
foreach my $row ($rs->all) {
$idx++;
$idx >= $keep_last_used ? push @delete_ids, $row->id : next;
}
my $del_rs = $rs->search({
id => { -in => \@delete_ids },
});
$del_rs->delete;
$prov_sub->last_passwords->create({
subscriber_id => $prov_sub->id,
value => $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS
? NGCP::Panel::Utils::Auth::generate_salted_hash($password, $bcrypt_cost)
: $password,
});
$prov_sub->update({ password_modify_timestamp => \'current_timestamp()' });
}
sub insert_webpassword_journal {
my ($c, $prov_sub, $webpassword) = @_;
my $bcrypt_cost = 6;
my $keep_last_used = $c->config->{security}{password}{web_keep_last_used} // return;
my $rs = $prov_sub->last_webpasswords->search({
},{
order_by => { '-desc' => 'created_at' },
});
my @delete_ids = ();
my $idx = 0;
foreach my $row ($rs->all) {
$idx++;
$idx >= $keep_last_used ? push @delete_ids, $row->id : next;
}
my $del_rs = $rs->search({
id => { -in => \@delete_ids },
});
$del_rs->delete;
$prov_sub->last_webpasswords->create({
subscriber_id => $prov_sub->id,
value => $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS
? NGCP::Panel::Utils::Auth::generate_salted_hash($webpassword, $bcrypt_cost)
: $webpassword,
});
$prov_sub->update({ webpassword_modify_timestamp => \'current_timestamp()' });
}
1; 1;
=head1 NAME =head1 NAME

@ -1,10 +1,27 @@
function generate_password(len) { function generate_password(len) {
var text = ""; var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?/-_%$()[]"; var possible_lower = "abcdefghijklmnopqrstuvwxyz";
for (var i = 0; i < len; i++) { var possible_upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
text += possible.charAt(Math.floor(Math.random() * possible.length)); var possible_digit = "0123456789";
var possible_spec = "@!?/\-_&*^%$;:<>,.()[]{}";
var min_lower = 4;
var min_upper = 4;
var min_digit = 4;
var min_spec = 4;
var chars = ''
for (var i = 0; i < min_lower; i++) {
chars += possible_lower.charAt(Math.floor(Math.random() * possible_lower.length));
} }
return text; for (var i = 0; i < min_upper; i++) {
chars += possible_upper.charAt(Math.floor(Math.random() * possible_upper.length));
}
for (var i = 0; i < min_digit; i++) {
chars += possible_digit.charAt(Math.floor(Math.random() * possible_digit.length));
}
for (var i = 0; i < min_spec; i++) {
chars += possible_spec.charAt(Math.floor(Math.random() * possible_spec.length));
}
return [...chars].sort(()=>Math.random()-.5).join('');
} }
$(document).ready(function() { $(document).ready(function() {
var btn = '<div id="gen_password" class="btn btn-primary pull-right" style="width:10%">Generate</div>'; var btn = '<div id="gen_password" class="btn btn-primary pull-right" style="width:10%">Generate</div>';

Loading…
Cancel
Save