#!/usr/bin/env perl

##
## Author......: See docs/credits.txt
## License.....: MIT
##

# based off m10500 but added the owner password part ($o) to be able to test the edit password

# easy test shortcut for debugging
# a=$(echo 1 | tools/test.pl passthrough 25400 | tail -n1); echo $a; echo 1 | ./hashcat --potfile-disable --runtime 400 --hwmon-disable -O -D 2 --backend-vector-width 4 -a 0 -m 25400 $a

use strict;
use warnings;

use Crypt::RC4;
use Digest::MD5 qw (md5);

my $PDF_PADDING =
[
  0x28, 0xbf, 0x4e, 0x5e, 0x4e, 0x75, 0x8a, 0x41, 0x64, 0x00, 0x4e, 0x56,
  0xff, 0xfa, 0x01, 0x08, 0x2e, 0x2e, 0x00, 0xb6, 0xd0, 0x68, 0x3e, 0x80,
  0x2f, 0x0c, 0xa9, 0xfe, 0x64, 0x53, 0x69, 0x7a
];

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

sub pdf_compute_encryption_key_user
{
  my $word  = shift;
  my $padding   = shift;
  my $id        = shift;
  my $u         = shift;
  my $o         = shift;
  my $P         = shift;
  my $V         = shift;
  my $R         = shift;
  my $enc       = shift;

  ## start

  my $data;

  $data .= $word;

  $data .= substr ($padding, 0, 32 - length $word);

  $data .= pack ("H*", $o);
  $data .= pack ("I",  $P);
  $data .= pack ("H*", $id);

  if ($R >= 4)
  {
    if (!$enc)
    {
      $data .= pack ("I", -1);
    }
  }

  my $res = md5 ($data);

  if ($R >= 3)
  {
    for (my $i = 0; $i < 50; $i++)
    {
      $res = md5 ($res);
    }
  }

  return $res;
}


sub pdf_compute_encryption_key_owner
{
  my $word  = shift;
  my $padding   = shift;
  my $id        = shift;
  my $u         = shift;
  my $o         = shift;
  my $P         = shift;
  my $V         = shift;
  my $R         = shift;
  my $enc       = shift;

  my $data;
  $data .= $word;
  $data .= substr ($padding, 0, 32 - length $word);
  my $o_digest = md5 ($data);

  if ($R >= 3)
  {
    for (my $i = 0; $i < 50; $i++)
    {
      $o_digest = md5 ($o_digest);
    }
  }

  my $o_key;
  if ($R == 2)
  {
    $o_key = substr ($o_digest, 0, 8);  # rc4 key is always 5 for revision 2, but for 3 or greather is dependent on the value of the encryption dictionaries length entry
  }
  else
  {
    $o_key = substr ($o_digest, 0, 16); # length is always 128 bits or 16 bytes
  }

  return $o_key;
}

sub module_generate_hash
{
  my $word = shift;
  my $id   = shift;
  my $u    = shift;
  my $o    = shift;
  my $P    = shift;
  my $V    = shift;
  my $R    = shift;
  my $enc  = shift;
  my $u_pass  = shift;

  if (defined $u == 0)
  {
    $u = "0" x 64;
  }

  my $u_save = $u;

  if (defined $o == 0)
  {
    $o = "0" x 64;
  }

  my $o_save = $u;

  if (defined $R == 0)
  {
    $R = random_number (3, 4);
  }

  if (defined $V == 0)
  {
    $V = ($R == 3) ? 2 : 4;
  }

  if (defined $P == 0)
  {
    $P = ($R == 3) ? -4 : -1028;
  }

  if (defined $enc == 0)
  {
    $enc = ($R == 3) ? 1 : random_number (0, 1);
  }

  if (!defined $u_pass)
  {
    $u_pass="";
  }

  my $padding;

  for (my $i = 0; $i < 32; $i++)
  {
    $padding .= pack ("C", $PDF_PADDING->[$i]);
  }


  ################ USER PASSWORD #################
  # do not change $u if it exists, keep this the same, as we don't know the user password,
  #  we cannot calculate this part of the hash again

  if ($u eq "0000000000000000000000000000000000000000000000000000000000000000")
  {
    my $res;
    if ($u_pass eq "")
    {
      # we don't know the user-password so calculate $u based on the owner-password
      $res = pdf_compute_encryption_key_user ($word, $padding, $id, $u, $o, $P, $V, $R, $enc);
    }
    else
    {
      # we do know the user-password, so we can generate $u
      $res = pdf_compute_encryption_key_user ($u_pass, $padding, $id, $u, $o, $P, $V, $R, $enc);
    }

    my $digest = md5 ($padding . pack ("H*", $id));

    my $m = Crypt::RC4->new ($res);
    $u = $m->RC4 ($digest);

    my @ress = split "", $res;

    # do xor of rc4 19 times
    for (my $x = 1; $x <= 19; $x++)
    {
    my @xor;

    for (my $i = 0; $i < 16; $i++)
    {
      $xor[$i] = chr (ord ($ress[$i]) ^ $x);
    }

    my $s = join ("", @xor);

    my $m2 = Crypt::RC4->new ($s);

    $u = $m2->RC4 ($u);
    }
    $u .= substr (pack ("H*", $u_save), 16, 16);
  }
  else
  {
    $u = pack ("H*", $u)
  }

  ################ OWNER PASSWORD #################
  my $o_key = pdf_compute_encryption_key_owner ($word, $padding, $id, $u, $o, $P, $V, $R, $enc);

  my $n = Crypt::RC4->new ($o_key);
  if ($u_pass eq "")
  {
     $o = $n->RC4 (substr ($padding, 0, 32 - length ""));
  }
  else
  {
    # dynamically add user password including padding to the RC4 input for the computation of the pdf o-value
    $o = $n->RC4 ($u_pass . substr ($padding, 0, 32 - length $u_pass));
  }

  my @ress2 = split "", $o_key;

  if ($R >= 3)
  {
    # do xor of rc4 19 times
    for (my $x = 1; $x <= 19; $x++)
    {
      my @xor;

      for (my $i = 0; $i < 16; $i++)
      {
        $xor[$i] = chr (ord ($ress2[$i]) ^ $x);
      }

      my $s = join ("", @xor);
      my $n2 = Crypt::RC4->new ($s);

      $o = $n2->RC4 ($o);
    }
  }

  my $hash;
  if ($u_pass eq "")
  {
    $hash = sprintf ('$pdf$%d*%d*128*%d*%d*16*%s*32*%s*32*%s', $V, $R, $P, $enc, $id, unpack ("H*", $u), unpack ("H*", $o));
  }
  else
  {
    $hash = sprintf ('$pdf$%d*%d*128*%d*%d*16*%s*32*%s*32*%s*%s', $V, $R, $P, $enc, $id, unpack ("H*", $u), unpack ("H*", $o), $u_pass);
  }
  return $hash;
}

sub module_verify_hash
{
  my $line = shift;

  my ($hash_in, $word) = split ":", $line;

  return unless defined $hash_in;
  return unless defined $word;

  my @data = split /\*/, $hash_in;

  my $i_data = scalar @data;
  return unless ($i_data == 11) || ($i_data == 12); # or 12 if user-password is included

  my $V        = shift @data; $V = substr ($V, 5, 1);
  my $R        = shift @data;
  return unless (shift @data eq '128'); # length is always 128 here
  my $P        = shift @data;
  my $enc      = shift @data;
  return unless (shift @data eq '16');
  my $id       = shift @data;
  return unless (shift @data eq '32');
  my $u        = shift @data;
  return unless (shift @data eq '32');
  my $o        = shift @data;

  my $u_pass = "";
  if ($i_data == 12)
  {
    $u_pass = shift @data;
  }

  return unless defined $id;
  return unless defined $word;

  $word = pack_if_HEX_notation ($word);

  my $new_hash = module_generate_hash ($word, $id, $u, $o, $P, $V, $R, $enc, $u_pass);

  return ($new_hash, $word);
}

1;
