TT#151751 refactor persisting subscriber webpassword

a multitude of issues popped after introducing bcrypted
webpasswords in the database. most recently the PATCH /api/susbcribers
rail was reported to reset the webpassword unintentionally.
subscriber login fails afterwards, which is a severe issue.

the bugs are adressed by this refactorings. the change also
introduces a global variable

  $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS

to control encrypting webpasswords. it is still enabled as of now,
but it's worth to consider disabling it. there other ways to have
a "cost" for an authentication request, eg. adding a simple
sleep(1sec).

Change-Id: I2d47d54a2d83568546ffdd2b211337a5f56be3a2
mr10.3
Rene Krenn 3 years ago committed by Hans-Peter Herzog
parent 55a8d5568f
commit 5e9066c4fb

@ -136,8 +136,10 @@ sub POST :Allow {
return;
}
$c->log->debug("Updating subscriber password.");
my $webpassword = $form->params->{new_password};
$webpassword = NGCP::Panel::Utils::Auth::generate_salted_hash($webpassword) if $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS;
$subscriber->provisioning_voip_subscriber->update({
webpassword => NGCP::Panel::Utils::Auth::generate_salted_hash($form->params->{new_password}),
webpassword => $webpassword,
});
$rs->delete;
}

@ -629,7 +629,7 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
if ($authrs->first) {
my $password = $authrs->first->webpassword;
if (length $password > 40) {
if (defined $password and length($password) > 40) {
my @splitted_pass = split /\$/, $password;
if (scalar @splitted_pass == 3) {
#password is bcrypted with lower cost
@ -664,8 +664,7 @@ sub login_jwt :Chained('/') :PathPart('login_jwt') :Args(0) :Method('POST') {
}, $pass));
$auth_user = $authrs->search({webpassword => $db_b64salt . '$' . $usr_b64hash})->first;
}
}
else {
} else {
$auth_user = $authrs->search({webpassword => $pass})->first;
}
}

@ -617,8 +617,10 @@ sub recover_webpassword :Chained('/') :PathPart('recoverwebpassword') :Args(0) {
$c->log->warn("invalid password recovery attempt for uuid '$uuid_string' from '".$c->qs($c->req->address)."'");
$c->detach('/denied_page');
}
my $webpassword = $form->params->{password};
$webpassword = NGCP::Panel::Utils::Auth::generate_salted_hash($webpassword) if $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS;
$subscriber->provisioning_voip_subscriber->update({
webpassword => NGCP::Panel::Utils::Auth::generate_salted_hash($form->params->{password}),
webpassword => $webpassword,
});
$rs->delete;
});
@ -2941,9 +2943,10 @@ sub edit_master :Chained('master') :PathPart('edit') :Args(0) :Does(ACL) :ACLDet
my $prov_params = {};
$prov_params->{pbx_extension} = $form->params->{pbx_extension};
$prov_params->{webusername} = $form->params->{webusername} || undef;
if($form->params->{webpassword}) {
$prov_params->{webpassword} =
NGCP::Panel::Utils::Auth::generate_salted_hash($form->params->{webpassword});
if ($form->params->{webpassword}) {
$prov_params->{webpassword} = $form->params->{webpassword};
$prov_params->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($prov_params->{webpassword})
if $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS;
}
$prov_params->{password} = $form->params->{password}
if($form->params->{password});
@ -3346,8 +3349,10 @@ sub webpass_edit :Chained('base') :PathPart('webpass/edit') :Args(0) {
my $subscriber = $c->stash->{subscriber};
my $prov_subscriber = $subscriber->provisioning_voip_subscriber;
$schema->txn_do(sub {
my $webpassword = $form->values->{webpassword};
$webpassword = NGCP::Panel::Utils::Auth::generate_salted_hash($webpassword) if $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS;
$prov_subscriber->update({
webpassword => NGCP::Panel::Utils::Auth::generate_salted_hash($form->values->{webpassword}),
webpassword => NGCP::Panel::Utils::Auth::generate_salted_hash($webpassword),
});
});
NGCP::Panel::Utils::Message::info(

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

@ -55,18 +55,8 @@ sub resource_from_item {
delete $prov_resource->{domain_id};
delete $prov_resource->{account_id};
my %resource = %{ merge($bill_resource, $prov_resource) };
my $change_passwords = 1;
$resource{administrative} = delete $resource{admin};
if ($c->request->method eq 'PATCH') {
if ($resource{pre_patch_resource}) {
delete $resource{pre_patch_resource};
} else {
$change_passwords = 0;
$resource{pre_patch_resource} = 1;
}
}
unless($customer->product->class eq 'pbxaccount') {
delete $resource{is_pbx_group};
delete $resource{is_pbx_pilot};
@ -86,20 +76,8 @@ sub resource_from_item {
$resource{email} = undef;
$resource{timezone} = undef;
}
# if the webpassword length is 54 or 56 chars and it contains $,
# we assume that the password is encrypted,
# as we do not have an explicit flag for the password field
# wether it's encrypted or not, there is a chance that
# if somebody manages to create a 54 chars password containing
# '$', it will be detected as false positive, but
# - all webpasswords from mr8.5+ are meant to be encrypted
# - in case of the false positive result, the worse that happens
# the password is not returned to the user in plain-text
if ($change_passwords &&
$resource{webpassword} && (length $resource{webpassword}) =~ /^(54|56)$/ &&
$resource{webpassword} =~ /\$/) {
delete $resource{webpassword};
}
$resource{_password} = $resource{password};
$resource{_webpassword} = $resource{webpassword};
if(!$form){
($form) = $self->get_form($c);
}
@ -199,18 +177,20 @@ sub resource_from_item {
if ($c->user->show_passwords) {
foreach my $k(qw/password webpassword/) {
eval {
if ($resource{$k} && $change_passwords) {
$resource{$k} = NGCP::Panel::Utils::Encryption::encrypt_rsa($c,$resource{$k});
if (not NGCP::Panel::Utils::Auth::is_salted_hash($resource{$k})) {
$resource{'_' . $k} = NGCP::Panel::Utils::Encryption::encrypt_rsa($c,$resource{$k});
} else {
delete $resource{'_' . $k};
}
};
if ($@) {
$c->log->error("Failed to encrypt $k: " . $@);
delete $resource{$k} if $change_passwords;
delete $resource{'_' . $k};
}
}
} else {
foreach my $k(qw/password webpassword/) {
delete $resource{$k} if $change_passwords;
delete $resource{'_' . $k};
}
}
} else {
@ -221,18 +201,18 @@ sub resource_from_item {
}
# TODO: make custom filtering configurable!
foreach my $k(qw/password webpassword/) {
delete $resource{$k} if $change_passwords;
foreach my $k (qw/password webpassword/) {
delete $resource{'_' . $k};
}
}
if ($c->user->roles eq "subscriberadmin") {
$resource{customer_id} = $contract_id;
if ($item->id != $c->user->voip_subscriber->id) {
if (!$c->config->{security}->{password_sip_expose_subadmin}) {
delete $resource{password} if $change_passwords;
delete $resource{_password};
}
if (!$c->config->{security}->{password_web_expose_subadmin}) {
delete $resource{webpassword} if $change_passwords;
delete $resource{_webpassword};
}
}
}
@ -253,6 +233,11 @@ sub hal_from_item {
$is_subadm = 0;
}
delete $resource->{password};
delete $resource->{webpassword};
$resource->{password} = delete $resource->{_password} if exists $resource->{_password};
$resource->{webpassword} = delete $resource->{_webpassword} if exists $resource->{_webpassword};
my $hal = Data::HAL->new(
links => [
Data::HAL::Link->new(
@ -525,14 +510,12 @@ sub update_item {
contact_id => $resource->{contact_id},
};
if(defined $resource->{webpassword}) {
if (exists $resource->{webpassword} and $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS) {
$resource->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($resource->{webpassword});
}
my $provisioning_res = {
password => $resource->{password},
webusername => $resource->{webusername},
webpassword => $resource->{webpassword},
is_pbx_pilot => $resource->{is_pbx_pilot} // 0,
is_pbx_group => $resource->{is_pbx_group} // 0,
modify_timestamp => NGCP::Panel::Utils::DateTime::current_local,
@ -541,6 +524,8 @@ sub update_item {
pbx_extension => $resource->{pbx_extension},
$resource->{administrative} ? (admin => $resource->{administrative}) : (),
};
$provisioning_res->{password} = $resource->{password} if exists $resource->{password};
$provisioning_res->{webpassword} = $resource->{webpassword} if exists $resource->{webpassword};
if(is_true($resource->{is_pbx_group})) {
$provisioning_res->{pbx_hunt_policy} = $resource->{pbx_hunt_policy};
$provisioning_res->{pbx_hunt_timeout} = $resource->{pbx_hunt_timeout};

@ -10,6 +10,7 @@ use UUID;
use NGCP::Panel::Utils::Redis;
our $SALT_LENGTH = 128;
our $ENCRYPT_SUBSCRIBER_WEBPASSWORDS = 1;
sub check_password {
my $pass = shift // return;
@ -114,6 +115,18 @@ sub perform_auth {
return $res;
}
sub is_salted_hash {
my $password = shift;
if (length($password)
and (length($password) == 54 or length($password) == 56)
and $password =~ /\$/) {
return 1;
}
return 0;
}
sub perform_subscriber_auth {
my ($c, $user, $domain, $pass) = @_;
my $res;
@ -132,7 +145,7 @@ sub perform_subscriber_auth {
});
my $sub = $authrs->first;
if(defined $sub && $sub->webpassword) {
if(defined $sub && defined $sub->webpassword) {
my $sub_pass = $sub->webpassword;
if (length $sub_pass > 40) {
my @splitted_pass = split /\$/, $sub_pass;

@ -319,9 +319,25 @@ sub prepare_resource {
}
}
if (exists $resource->{webpassword}
and NGCP::Panel::Utils::Auth::is_salted_hash($resource->{webpassword})) {
delete $resource->{webpassword};
}
if (exists $resource->{webpassword} and defined $resource->{webpassword}
and $item and defined $item->provisioning_voip_subscriber->webpassword
and $resource->{webpassword} eq $item->provisioning_voip_subscriber->webpassword) {
delete $resource->{webpassword};
}
if (exists $resource->{password} and defined $resource->{password}
and $item and defined $item->provisioning_voip_subscriber->password
and $resource->{password} eq $item->provisioning_voip_subscriber->password) {
delete $resource->{password};
}
foreach my $k(qw/password webpassword/) {
eval {
if ($resource->{$k}) {
if (exists $resource->{$k}) {
$resource->{$k} = NGCP::Panel::Utils::Encryption::decrypt_rsa($c,$resource->{$k});
}
};
@ -332,12 +348,6 @@ sub prepare_resource {
}
}
my $webpassword;
if (length($resource->{webpassword}) and $item and length($item->provisioning_voip_subscriber->webpassword)
and $resource->{webpassword} eq $item->provisioning_voip_subscriber->webpassword) {
$webpassword = delete $resource->{webpassword};
}
#password is mandatory field, so it cannot be absent from resource, the only reason for that being it was
#deleted in resource_from_item for admins without show_passwords flag; in this case, we are restoring it here
if (not length($resource->{password}) and $item and length($item->provisioning_voip_subscriber->password)) {
@ -345,14 +355,6 @@ sub prepare_resource {
}
return unless &$validate_code($resource);
$resource->{webpassword} = $webpassword if ($webpassword);
#my ($form) = $self->get_form($c);
#return unless $self->validate_form(
# c => $c,
# resource => $resource,
# form => $form,
#);
# this format is expected by NGCP::Panel::Utils::Subscriber::create_subscriber
$resource->{alias_numbers} = [ map {{ e164 => $_ }} @{ $resource->{alias_numbers} // [] } ];
@ -413,7 +415,6 @@ sub prepare_resource {
}
}
my $preferences = {};
my $admin = 0;
unless($customer->product->class eq 'pbxaccount') {
@ -633,7 +634,7 @@ sub create_subscriber {
}
my $passlen = $c->config->{security}->{password_min_length} || 8;
if($c->config->{security}->{password_sip_autogenerate} && !$params->{password}) {
if($c->config->{security}->{password_sip_autogenerate} and not defined $params->{password}) {
$params->{password} = String::MkPasswd::mkpasswd(
-length => $passlen,
-minnum => 1, -minlower => 1, -minupper => 1, -minspecial => 1,
@ -642,7 +643,7 @@ sub create_subscriber {
#otherwise it breaks xml device configs
$params->{password} =~s/[<>&]/,/g;
}
if($c->config->{security}->{password_web_autogenerate} && !$params->{webpassword}) {
if($c->config->{security}->{password_web_autogenerate} and not defined $params->{webpassword}) {
$params->{webpassword} = String::MkPasswd::mkpasswd(
-length => $passlen,
-minnum => 1, -minlower => 1, -minupper => 1, -minspecial => 1,
@ -692,7 +693,7 @@ sub create_subscriber {
UUID::unparse($pass_bin, $pass_str);
$params->{password} = $pass_str;
}
if(defined $params->{webpassword}) {
if (exists $params->{webpassword} and $NGCP::Panel::Utils::Auth::ENCRYPT_SUBSCRIBER_WEBPASSWORDS) {
$params->{webpassword} = NGCP::Panel::Utils::Auth::generate_salted_hash($params->{webpassword});
}
my $prov_subscriber = $schema->resultset('provisioning_voip_subscribers')->create({
@ -700,7 +701,7 @@ sub create_subscriber {
username => $params->{username},
password => $params->{password},
webusername => $params->{webusername} || $params->{username},
webpassword => $params->{webpassword},
(exists $params->{webpassword} ? (webpassword => $params->{webpassword}) : ()),
admin => $params->{administrative} // $administrative,
account_id => $contract->id,
domain_id => $prov_domain->id,

Loading…
Cancel
Save