the-bastion/lib/perl/OVH/Bastion/password.inc
Thomas SOËTE 2a51a78b54 fix: Enable perl-tidy.sh test
* Move to ubuntu-20.04 runner
* Remove check in dockers tests
2020-11-22 21:37:34 +00:00

156 lines
4.9 KiB
Perl

# vim: set filetype=perl ts=4 sw=4 sts=4 et:
package OVH::Bastion;
use common::sense;
use Digest::SHA qw{ hmac_sha256 };
# PBKDF2 like - HMAC-SHA256 - return 256 bits key
sub _get_key_from_password {
my %params = @_;
my $password = $params{'password'};
if (not $password) {
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'password'");
}
my $salt = 'JPYWrLpoXcXFA46m9DUI5z02SqUd2baG';
my $iterations = 10_000;
my $hash = hmac_sha256($salt . pack('N', 0), $password);
my $result = $hash;
for my $iter (2 .. $iterations) {
$hash = hmac_sha256($hash, $password);
$result ^= $hash;
}
return R('OK', value => $result);
}
# generate a fixed salt given (a password AND a nonce AND a salt len)
sub _get_salt_for_password {
my %params = @_;
my $password = $params{'password'};
my $nonce = $params{'nonce'} || $password;
my $len = $params{'len'} || 4;
if ($len > 16) {
return R('ERR_INVALID_PARAMETER', msg => "Expected a len <= 16");
}
# get a derived key from what we've been given
my $fnret = _get_key_from_password(password => $password . $nonce . $len . $nonce . $password);
$fnret or return $fnret;
# then generate the salt from the key
my @u16 = unpack('S*', $fnret->value);
my $s;
foreach my $i (1 .. $len) {
my $r = $u16[$i - 1] % (10 + 26 + 26);
if ($r < 10) { $s .= $r }
elsif ($r < 36) { $s .= chr($r - 10 + ord('a')) }
else { $s .= chr($r - 36 + ord('A')) }
}
return R('OK', value => $s);
}
sub get_hashes_from_password {
my %params = @_;
my $password = $params{'password'};
if (not $password) {
return R('ERR_MISSING_PARAMETER', msg => "Missing parameter 'password'");
}
my %ret;
$ret{'md5crypt'} = crypt($password, '$1$' . _get_salt_for_password(password => $password, nonce => '$1', len => 4)->value . '$');
$ret{'sha256crypt'} = crypt($password, '$5$' . _get_salt_for_password(password => $password, nonce => '$5', len => 8)->value . '$');
$ret{'sha512crypt'} = crypt($password, '$6$' . _get_salt_for_password(password => $password, nonce => '$6', len => 8)->value . '$');
# some OSes have a broken crypt() that doesn't generate invalid hashes, undef those
$ret{'sha256crypt'} = undef if $ret{'sha256crypt'} !~ m{^\$5\$};
$ret{'sha512crypt'} = undef if $ret{'sha512crypt'} !~ m{^\$6\$};
return R('OK', value => \%ret);
}
sub get_hashes_list {
my %params = @_;
my $context = $params{'context'};
my $group = $params{'group'};
my $account = $params{'account'};
my $fnret;
my $shortGroup;
my $homepass;
my $passfile;
if ($context eq 'group') {
$fnret = OVH::Bastion::is_valid_group_and_existing(group => $group, groupType => "key");
$fnret or return $fnret;
$group = $fnret->value->{'group'};
$shortGroup = $fnret->value->{'shortGroup'};
$homepass = "/home/$group/pass";
$passfile = "$homepass/$shortGroup";
}
elsif ($context eq 'account') {
$fnret = OVH::Bastion::is_bastion_account_valid_and_existing(account => $account);
$fnret or return $fnret;
$account = $fnret->value->{'account'};
$homepass = "/home/$account/pass";
$passfile = "$homepass/$account";
}
else {
return R('ERR_INVALID_PARAMETER', msg => "Expected a context of 'group' or 'account'");
}
my @ret;
foreach my $inc ('', 1 .. 99) {
my $currentname = $passfile;
$currentname .= ".$inc" if (length($inc) > 0);
if (open(my $fdin, '<', $currentname)) {
my %current;
my $pass = <$fdin>;
close($fdin);
chomp $pass;
$fnret = OVH::Bastion::get_hashes_from_password(password => $pass);
undef $pass;
$fnret or return $fnret;
my $desc = (length($inc) == 0 ? "Current password" : "Fallback password $inc");
my %metadata;
if (open(my $metadatafd, '<', "$currentname.metadata")) {
while (<$metadatafd>) {
chomp;
m{^([A-Z0-9_-]+)=(.+)$} or next;
my ($key, $val) = (lc($1), $2);
$metadata{$key} = $val;
# int-ize if it's an int, for json:
$metadata{$key} += 0 if ($val =~ /^\d+$/);
}
close($metadatafd);
}
if (%metadata) {
$current{'metadata'} = \%metadata;
$desc .= " created at $metadata{'creation_time'} by $metadata{'created_by'}";
}
$current{'description'} = $desc;
$current{'hashes'} = $fnret->value;
push @ret, \%current;
}
else {
last;
}
}
return R('OK', value => \@ret);
}
1;