
package MasterServer::Core::Secure;

use strict;
use warnings;
use POSIX qw/strftime/;
use Exporter 'import';

our @EXPORT = qw| secure_string validated_beacon validated_request validate_string charshift get_validate_string|;

################################################################################
# generate a random string of 6 characters long for the \secure\ challenge
# returns string
################################################################################
sub secure_string {
  # spit out a random string, only uppercase characters
  my @c = ('A'..'Z');
  my $s = "";
  $s .= $c[rand @c] for 1..6;
  
  # return random string
  return $s;
}

################################################################################
# authenticate the \validate\ response for the \secure\ challenge. 
# returns 1 on valid response, 0 on invalid
################################################################################
sub validated_beacon {
  my ($self, $gamename, $secure, $enctype, $validate) = @_;
  
  # debugging enabled? Then don't care about validation
  return 1 if ($self->{debug_validate});
  
  # enctype given? 
  $enctype = 0 unless $enctype;
  
  if ($self->{ignore_beacon_key} =~ m/$gamename/i){
    $self->log("secure", "ignored beacon validation for $gamename");
    return 1;
  }
  
  # compare received response with expected response
  return ($self->validate_string($gamename, $secure, $enctype) eq $validate) || 0;
}

################################################################################
# authenticate the \validate\ response for the \secure\ challenge. 
# returns 1 on valid response, 0 on invalid
################################################################################
sub validated_request {
  my ($self, $gamename, $secure, $enctype, $validate) = @_;
  
  # debugging enabled? Then don't care about validation
  return 1 if ($self->{debug_validate});
  
  # enctype given? 
  $enctype = 0 unless $enctype;
  
  # ignore games and beacons that are listed
  if ($self->{ignore_browser_key} =~ m/$gamename/i){
    $self->log("secure", "ignored browser validation for $gamename");
    return 1;
  }
  
  # compare received response with expected response
  return ($self->validate_string($gamename, $secure, $enctype) eq $validate) || 0;
}

################################################################################
# process the validate string as a response to the secure challenge
# returns: validate string (usually 8 characters long)
################################################################################
sub validate_string {
  my ($self, $game, $sec, $enc)  = @_;
  
  # get cipher from gamename
  my $cip = $self->{game}->{$game}->{key} || "000000";
  
  # don't accept challenge longer than 16 characters (because vulnerable in UE)
  if (length $sec > 16) {
    return "invalid!"}
  
  # check for valid encryption choices
  my $enc_val = (defined $enc && 0 <= $enc && $enc <= 2) ? $enc : 0;
  
  # calculate and return validate string
  return $self->get_validate_string($cip, $sec, $enc_val);
}

################################################################################
# rotate characters as part of the secure/validate algorithm.
# arg and return: int (representing a character)
################################################################################
sub charshift {
  my ($self, $reg) = @_;
    return($reg + 65) if ($reg <  26);
    return($reg + 71) if ($reg <  52);
    return($reg - 4)  if ($reg <  62);
    return(43)        if ($reg == 62);
    return(47)        if ($reg == 63);
    
    # if all else fails
    return(0);
}

################################################################################
# algorithm to process the response to the secure/validate query. processes
# the secure_string and returns the challenge_string with which GameSpy secure
# protocol authenticates games.
#
# the following algorithm is based on gsmsalg.h in GSMSALG 0.3.3 by Luigi 
# Auriemma, aluigi@autistici.org, aluigi.org, copyright 2004-2008. GSMSALG 0.3.3
# was released under the GNU General Public License, for more information, see
# the original software at http://aluigi.altervista.org/papers.htm#gsmsalg
#
# conversion and modification of the algorithm by Darkelarious, June 2014.
#
# args: game cipher, 6-char challenge string, encryption type
# returns: validate string (usually 8 characters long)
# !! requires cipher hash to be configured in config! (imported or otherwise)
################################################################################
sub get_validate_string {
  my ($self, $cipher_string, $secure_string, $enctype) = @_;
  
  # import pre-built rotations from config for enctype 
  # -- see GSMSALG 0.3.3 reference for copyright and more information
  my @enc_chars = $self->{enc_chars};

  # convert to array of characters
  my @cip = split "", $cipher_string;
  my @sec = split "", $secure_string;

  # length of strings/arrays which should be 6
  my $sec_len = scalar @sec;
  my $cip_len = scalar @cip;
  
  # from this point on, work with ordinal values
  for (0..$sec_len-1) { $sec[$_] = ord $sec[$_]; }
  for (0..$cip_len-1) { $cip[$_] = ord $cip[$_]; }
  
  # helper vars
  my ($i,$j,$k,$l,$m,$n,$p);

  # too short or too long -- return empty string
  return "" if ($sec_len <= 0 || $sec_len >= 32);
  return "" if ($cip_len <= 0 || $cip_len >= 32);
  
  # temporary array with ascii characters
  my @enc;
  for(0..255) {$enc[$_] = $_;}

  $j = 0;
  for(0..255) {
      $j += $enc[$_] + $cip[$_ % $cip_len];
      $j = $j % 256;
      $l = $enc[$j];
      $enc[$j] = $enc[$_];
      $enc[$_] = $l;
  }

  # store temporary positions  
  my @tmp;
  
  $j = 0;
  $k = 0;
  for($i = 0; $sec[$i]; $i++) {
      $j += $sec[$i] + 1;
      $j = $j % 256;
      $l = $enc[$j];
      $k += $l;
      $k = $k % 256;
      $m = $enc[$k];
      $enc[$k] = $l;
      $enc[$j] = $m;
      $tmp[$i] = $sec[$i] ^ $enc[($l + $m) & 0xff];
  }
  
# part of the enctype 1-2 process
  for($sec_len = $i; $sec_len % 3; $sec_len++) {
      $tmp[$sec_len] = 0;
  }
  
  if ($enctype == 1) {
    for (0..$sec_len-1) {
      $tmp[$_] = $enc_chars[$tmp[$_]];
    }
  }
  elsif ($enctype == 2) {
    for (0..$sec_len-1) {
      $tmp[$_] ^= $cip[$_ % $cip_len];
    }
  }  
  
  # parse the validate array
  $p = 0;
  my @val;
  for($i = 0; $i < $sec_len; $i += 3) {
      $l = $tmp[$i];
      $m = $tmp[$i + 1];
      $m = $m % 256;
      $n = $tmp[$i + 2];
      $n = $n % 256;
      $val[$p++] = $self->charshift($l >> 2);
      $val[$p++] = $self->charshift((($l & 3 ) << 4) | ($m >> 4));
      $val[$p++] = $self->charshift((($m & 15) << 2) | ($n >> 6));
      $val[$p++] = $self->charshift($n & 63);
  }
  
  # return to ascii characters
  my $str = "";
  for (@val) { $str .= chr $_}
  
  return $str;
}

1;
