#!/usr/bin/env perl

##
## Author......: See docs/credits.txt
## License.....: MIT
##
## Further credits:
## The password-storage algorithm used by Radmin 3 was analyzed and made public
## by synacktiv:
## https://www.synacktiv.com/publications/cracking-radmin-server-3-passwords.html
##

use strict;
use warnings;

use Digest::SHA qw (sha1 sha1_hex);
use Crypt::OpenSSL::Bignum::CTX;
use Encode;

sub module_constraints { [[0, 256], [32, 32], [-1, -1], [-1, -1], [-1, -1]] }

my $GENERATOR = "05";
my $MODULUS   = "9847fc7e0f891dfd5d02f19d587d8f77aec0b980d4304b0113b406f23e2cec58cafca04a53e36fb68e0c3bff92cf335786b0dbe60dfe4178ef2fcd2a4dd09947ffd8df96fd0f9e2981a32da95503342eca9f08062cbdd4ac2d7cdf810db4db96db70102266261cd3f8bdd56a102fc6ceedbba5eae99e6127bdd952f7a0d18a79021c881ae63ec4b3590387f548598f2cb8f90dea36fc4f80c5473fdb6b0c6bdb0fdbaf4601f560dd149167ea125db8ad34fd0fd45350dec72cfb3b528ba2332d6091acea89dfd06c9c4d18f697245bd2ac9278b92bfe7dbafaa0c43b40a71f1930ebc4fd24c9e5a2e5a4ccf5d7f51544d70b2bca4af5b8d37b379fd7740a682f";

sub module_generate_hash
{
  my $word = shift;
  my $salt = shift;
  my $user = shift;

  if (! defined ($user))
  {
    $user = random_mixedcase_string (int (rand (128)));

    $user = encode ('UTF16-LE', $user);
  }

  my $word_utf16 = encode ("UTF-16LE", $word);

  my $exponent = sha1_hex ($salt . sha1 ($user . ":" . $word_utf16));

  my $g = Crypt::OpenSSL::Bignum->new_from_hex ($GENERATOR);
  my $m = Crypt::OpenSSL::Bignum->new_from_hex ($MODULUS);
  my $e = Crypt::OpenSSL::Bignum->new_from_hex ($exponent);

  my $ctx = Crypt::OpenSSL::Bignum::CTX->new ();

  my $pow = $g->mod_exp ($e, $m, $ctx);

  my $res = $pow->to_bin ();

  # IMPORTANT step:

  $res = "\x00" x (256 - length ($res)) . $res; # pad it to exactly 256 bytes


  my $hash = sprintf ("\$radmin3\$%s*%s*%s", unpack ("H*", $user), unpack ("H*", $salt), unpack ("H*", $res));

  return $hash;
}

sub module_verify_hash
{
  my $line = shift;

  my $idx = index ($line, ':');

  return unless $idx >= 0;

  my $hash = substr ($line, 0, $idx);
  my $word = substr ($line, $idx + 1);

  return unless substr ($hash, 0, 9) eq '$radmin3$';

  my ($user, $salt, $verifier) = split ('\*', substr ($hash, 9));

  return unless defined $user;
  return unless defined $salt;
  return unless defined $verifier;

  return unless length ($salt) == 64;

  return unless $user     =~ m/^[0-9a-fA-F]*$/;
  return unless $salt     =~ m/^[0-9a-fA-F]*$/;
  return unless $verifier =~ m/^[0-9a-fA-F]*$/;

  $salt = pack ("H*", $salt);
  $user = pack ("H*", $user);

  my $word_packed = pack_if_HEX_notation ($word);

  my $new_hash = module_generate_hash ($word_packed, $salt, $user);

  return ($new_hash, $word);
}

1;