Search code examples
encryptionopensslmcrypt

Upgrade mcrypt to OpenSSL encryption in SagePay form


I have a Form which collects data, then submits this to SagePay, passing the data. This worked fine until we needed to update to PHP 7.2, and as mcrypt is no longer supported, we're switching to OpenSSL.

The form that gathers the data works fine but transporting the data doesn't encrypt and the transaction fails.

Code in functions file:

$protx_encryption_password      = "my_password";
$protx_vendor                   = "my_vendor";

$data = "";
while(list($key,$value) = each($protx)) {
    $data .= $key."=".$value."&";
}
$data = trim($data," &");

$crypt = openssl_encrypt($data, $protx_encryption_password);

function openssl_encrypt($string, $key) {
    $iv = substr($value, 0, 16);
    $ciphertext = substr($value, 16);
    $key = hash('sha256', $key, true);
    $crypt = openssl_encrypt(
        $ciphertext, 'AES-256-CBC', $key, 
        OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, 
        $iv
    );
    return rtrim($crypt, "\0");
}

The $crypt data is sent in a hidden field as before in the mcrypt version, I just need some help in getting the encryption working.

Previously in the mcrypt way:

$crypt = encryptAes($data, $protx_encryption_password);

function encryptAes($string, $key) {
// AES encryption, CBC blocking with PKCS5 padding then HEX encoding.
// Add PKCS5 padding to the text to be encypted.
$string = addPKCS5Padding($string);

// Perform encryption with PHP's MCRYPT module.
$crypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $key);

// Perform hex encoding and return.
return "@" . strtoupper(bin2hex($crypt));
}

function addPKCS5Padding($input) {
    $blockSize = 16;
    $padd = "";

    // Pad input to an even block size boundary.
    $length = $blockSize - (strlen($input) % $blockSize);
    for ($i = 1; $i <= $length; $i++)
    {
        $padd .= chr($length);
    }

    return $input . $padd;
}

This is the first OpenSSL encryption I've done so I'd appreciate any assistance with this. SagePay needs to be encrypted using AES-128 encryption with 128-bit block size in CBC mode with PCKS#5 (AES-128-CBC-PKCS#5). Any help would be truly helpful.


Solution

  • First, this is a bad scheme. Using a (real) password as the key for modern cryptography, even where it is sort of possible, is almost always insecure. Passwords and keys are different things that are designed and specified differently. Of course some people who don't know what they're doing call something that is actually a key a password, and since I don't know SagePay (and of course you don't give a real value) I don't know if that's the case here. Further, using the key (or password) as IV is grossly nonstandard. The whole point of an IV is to be different for every encryption (or at least every data value) under a given key, while a given key by definition is the same as itself. This is so silly I don't recall seeing any analysis of whether and what attacks it enables, although as a general rule violating the 'security contract' usually does lead to problems. Using any fixed value for IV, a rather more common error, is only moderately bad for CBC mode, although it is much worse for some other modes.

    However, you apparently don't have the option to change this scheme, and if you did, it would not really be a programming Q and would belong instead on security.SX or maybe crypto.SX depending.

    Also FYI it is meangingless to say "AES-128 with a 128-bit block". AES always uses an 128-bit block; that was part of the rules of the competition that established it. Rijndael does have options for other block sizes, which is why mcrypt, which implements Rijndael more-or-less, specifies the block size, but OpenSSL implements AES, which does not need to and does not specify the block size -- only the key size.

    Second, your proposed new code is nonsense. First you try to redefine a standard function, which is not allowed, and then the logic you propose was clearly designed to decrypt, not encrypt, a substantially different scheme, not the one you present, and then mangled.

    mcrypt_encrypt zero-pads the plaintext, and in older versions the key and iv, as needed. For Rijndael it also chooses the key size based on the supplied key; since you say you want AES-128 (i.e. a 128-bit key) the so-called 'password' must not be more than 128-bits but may be less. The code you posted does explicit PKCS5 padding of the data, so mcrypt's zero padding isn't used, and as Peter commented, openssl_encrypt (and the OpenSSL routines it uses underneath) do PCKS5/7 padding by default, so all you need is:

    function encryptAes_new ($string, $key) {
      $key = str_pad($key,16,"\0"); # if supplied key is, or may be, less than 16 bytes
      $crypt = openssl_encrypt($string, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $key);
      // Perform hex encoding and return.
      return "@" . strtoupper(bin2hex($crypt));
    }
    

    (Note: Original PKCS5 padding only handled 64-bit/8-byte blocks, and PKCS7 extended it to other sizes, but PKCS5v2.1 in 2017 also extended it referencing CMS which is the IETF version of PKCS7, so they are now the same.)