BOfH has asked for the wisdom of the Perl Monks concerning the following question:

Hello Monks,

I have been reading the guide to MIX files at https://moddingwiki.shikadi.net/wiki/MIX_Format_(Westwood) and have been having a go at decoding some. My problem is dealing with the RSA encrypted header using Crypt:Openssl::RSA, specifically loading the key.

The aforementioned site lists the modulus as AihRvNoIbTn85FZRYNZRcT+i6KpU+maCsEqr3Q5q+LDB5tH7Tz2qQ38V and the private exponent as AigKVje8mROcR8QixnxUEF5b29Curkq01DNDWCdOG99XBqH79OaCiTCB (both as BASE64 / DER encoded integers) and the public exponent as 0x10001. This not being the most helpful format, I figured that the best way to get it into Crypt::Openssl::RSA was via the new_key_from_parameters() method.

I converted both the modulus and private exponent to hex via an online decoder for use with Crypt::OpenSSL::Bignum->new_from_hex.

The issue appears to be that after loading the key material, check_key returns false, triggering the die with "invalid key"

#!/usr/bin/perl use strict; use warnings; use Crypt::OpenSSL::Bignum; use Crypt::OpenSSL::RSA; my $pub = '51BCDA086D39FCE4565160D651713FA2E8AA54FA6682B04AABDD0E6AF8B +0C1E6D1FB4F3DAA437F15'; my $priv = '0A5637BC99139C47C422C67C54105E5BDBD0AEAE4AB4D4334358274E1B +DF5706A1FBF4E682893081'; my $expo = '10001'; my $n = Crypt::OpenSSL::Bignum->new_from_hex($pub); my $e = Crypt::OpenSSL::Bignum->new_from_hex($expo); my $d = Crypt::OpenSSL::Bignum->new_from_hex($priv); my $rsa = Crypt::OpenSSL::RSA->new_key_from_parameters($n, $e, $d); die "invalid key" unless $rsa->check_key;

There doesn't appear to be a lot to screw up, so I can't see where I've got it wrong, unless I've done something stupid decoding the BASE64/DER data. I've been over it several times. If anyone can offer any words of wisdom, I would be extremely grateful.

Thanks,
BOfH

Replies are listed 'Best First'.
Re: Importing keys into Crypt::Openssl::RSA
by jo37 (Curate) on Jan 27, 2024 at 11:04 UTC

    Using rsaconverter it is easy to recover the primes and afterwards construct the rsa private key:

    #!/usr/bin/perl use strict; use warnings; use Crypt::OpenSSL::Bignum; use Crypt::OpenSSL::RSA; my $pub = '51BCDA086D39FCE4565160D651713FA2E8AA54FA6682B04AABDD0E6AF8B +0C1E6D1FB4F3DAA437F15'; my $priv = '0A5637BC99139C47C422C67C54105E5BDBD0AEAE4AB4D4334358274E1B +DF5706A1FBF4E682893081'; my $expo = '10001'; my $prime1 = '0145D2582F15952704BA91878C88E486464D67753F'; my $prime2 = '4038C78C871A1B9790BBCD713C28DBAB84E652AB'; my $n = Crypt::OpenSSL::Bignum->new_from_hex($pub); my $e = Crypt::OpenSSL::Bignum->new_from_hex($expo); my $d = Crypt::OpenSSL::Bignum->new_from_hex($priv); my $p = Crypt::OpenSSL::Bignum->new_from_hex($prime1); my $q = Crypt::OpenSSL::Bignum->new_from_hex($prime2); my $rsa = Crypt::OpenSSL::RSA->new_key_from_parameters($n, $e, $d, $p, + $q); die "invalid key" unless $rsa->check_key;

    Greetings,
    -jo

    $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
Re: Importing keys into Crypt::Openssl::RSA
by jo37 (Curate) on Jan 27, 2024 at 09:33 UTC

    The documentation for new_key_from_parameters is a bit terse. I ran your example with another (valid) private key and got the same result. However, if new_key_from_parameter() is called with the prime factors p and q as additional parameters, then the constructed key turns out to be valid.

    I am a bit puzzled by the remark that p and q would not be necessary.

    Greetings,
    -jo

    $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
Re: Importing keys into Crypt::Openssl::RSA
by BOfH (Novice) on Jan 27, 2024 at 19:30 UTC

    Hi Jo,

    Thanks very much for your assistance. I agree that that docs leave some room for development. I think I did read somewhere that check_key only checks for the presence of the various parameters. I think I discarded that from my mind, as it didn't make a lot of sense. I can see if I included the parameters or not, I don't think I need a function to tell me. Nevertheless, I made the changes that you suggested and the check_key function now returns true.

    The next thing that I tried to do was to append the following to the code:

    my $buf = 't'; $cipher = $rsa->encrypt($buf);

    The returned the following error: "RSA.xs:227: OpenSSL error: data too large for key size at ./rsa.pl line 37."

    I understand that for RSA to work, the input has to be less than the key. Given that they key reports to be 319 bits, it's hard to see how $buf = 't' pushes it over the edge, regardless of how it's encoded. I've tried to find some code examples of Crypt:Openssl::RSA but they are very hard to come by. A lot of them read the data from a file, so I had a go at that too:

    open(my $fd, '<:raw', 'test') or die("Failed to open file: $!"); binmode($fd); read($fd, my $buf, 4); close($fd);

    That yielded the same error. It's not clear how data should be passed to $rsa->encrypt. The manual seems to say "binary string" (their quotes). I tried dumping the public and private keys using get_public_key_string and get_private_key_string and then using them to encrypt and decrypt some arbitary ("test") data with the openssl command and that worked just fine, so I think the keys must be good

    I am also starting to wonder if it would be easier to do the decryption maths myself with use bignum; or if I'm just opening an even bigger can of worms?

    Any advise would be gratefully receieved.

    Thanks,
    BOfH.

      After specifying a padding mode, it works:

      $rsa->use_pkcs1_padding; my $buf = 't'; my $cipher = $rsa->encrypt($buf); my $plain = $rsa->decrypt($cipher); print "$plain\n";

      Update:

      The linked article suggests "no padding". In that case we need a buffer of exact the size of the modulus, i.e. 319 bit. This is weird, as 39 byte are too short and 40 byte are too long. Prepending some bits (e.g. 0x1) in front of the data and filling the buffer to a total length of 40 byte does the trick:

      $rsa->use_no_padding; my $in = join '', 'a' .. 'z'; my $buf = pack 'Ca39', 0x1, $in; my $cipher = $rsa->encrypt($buf); my $plain = $rsa->decrypt($cipher); print unpack 'xZ39', $plain;

      Update 2:

      When custom padding ("no padding") is in effect, the buffer must be the same size as the modulus and the to-be-encrypted value must be less than the modulus. So here we need a buffer of 40 byte. The modulus starts with 0x51. If the first byte of the to-be-encrypted value is less than 0x51 then the remaining 39 bytes may be arbitrary.

      Greetings,
      -jo

      $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$

      Hi Jo,

      Thanks again for all your input and continued assistance.

      That's interesting about the padding as I note the following from the docs:

      use_pkcs1_oaep_padding Use EME-OAEP padding as defined in PKCS #1 v2.0 with SHA-1, MGF1 a +nd an empty encoding parameter. This mode of padding is recommended f +or all new applications. It is the default mode used by Crypt::OpenSS +L::RSA.

      I tried adding the padding option, as you suggested and was able to encrypt and decrypt text.

      Perhaps something that I should have done earlier was to dig out the hex editor and see if the data that I was reading from the file was really what I was reading from the file. As it turned out, I wasn't quite on the mark and so wasn't reading the correct data, which meant that sometimes it was > modulus. After correcting the read(), the blocks all decrypted and the data looked as expected, the second block containing lots of 0x00 padding. The blocks contain a 56 byte blowfish key, which I had believe was 40 bytes in the first block and the remaining 16 bytes in the second block, padded out with zeros. I too had wondered how to fit the data into the first block if it needs to be less than the modulus. As it turns out the answer was rather obvious. There's 39 bytes of the key in the first block, with a leading 0x00 pad and the remaining 17 in the second block with lots of 0x00 padding.

      Thanks again for your assistance with this, it's been eating far too much of my time recently!

      BOfH