Decrypt String in C# That Was Encrypted with PHP Openssl_Encrypt

Decrypt string in C# that was encrypted with PHP openssl_encrypt

Well this was fun to work out and required jumping into the PHP source code with some interesting results. Firstly PHP doesn't even use a key derivation algorithm it just takes the bytes of the passphrase and pads it out with zero's to the required length. That means the entire DeriveKeyAndIV method isn't necessary.

Because of the above that means the IV that is being used is a 16 length byte array containing zeros.

Finally the only other thing wrong with your code is that the source you copied it from used a salt in their implementation of encrypt which then had to be removed, PHP nor you are doing this so removing the salt bytes is incorrect.

So the all of this put together means you need to change the OpenSSLDecrypt method to this.

public static string OpenSSLDecrypt(string encrypted, string passphrase)
{
//get the key bytes (not sure if UTF8 or ASCII should be used here doesn't matter if no extended chars in passphrase)
var key = Encoding.UTF8.GetBytes(passphrase);

//pad key out to 32 bytes (256bits) if its too short
if (key.Length < 32)
{
var paddedkey = new byte[32];
Buffer.BlockCopy(key, 0, paddedkey, 0, key.Length);
key = paddedkey;
}

//setup an empty iv
var iv = new byte[16];

//get the encrypted data and decrypt
byte[] encryptedBytes = Convert.FromBase64String(encrypted);
return DecryptStringFromBytesAes(encryptedBytes, key, iv);
}

And very finally the resulting string has some extra chars at the end namely a set of 3 of the ETX char but these should be easy enough to filter out. I actually can't figure out where these are coming from.

Thanks to @neubert for pointing out the padding is a part of the standard PKCS padding if you want the framework to remove this just specify that as the padding mode when instantiating the RijndaelManaged object.

new RijndaelManaged { Padding = PaddingMode.PKCS7 };

How to decrypt string from openssl_encrypt in c#

There's some junk in here I didn't clean up, so please excuse that. You have two things to change. Search the text in the parentheses to find where the fix is.

  • (Fix 1) I read somewhere that the php method truncates keys that are longer than they should be. Your key was 64 chars but should be 32 chars.
  • (Fix 2) The PaddingMode looks like it should be PKCS7

UPDATE

The code remains the same, but I added one more piece that I forgot I removed:

  • (Fix 3) The key should be text, not hex.
using System;
using System.Security.Cryptography;
using System.IO;
using System.Linq;
using System.Collections.Generic;
// https://stackoverflow.com/questions/68673772/how-to-decrypt-string-from-openssl-encrypt-in-c-sharp
//https://stackoverflow.com/questions/19719294/decrypt-string-in-c-sharp-that-was-encrypted-with-php-openssl-encrypt
// https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rijndaelmanaged?view=net-5.0
public class Program
{
// full key 1234567890123456789012345678901234567890123456789012345678901234

// Fix 1: php truncates a key longer than what is supported, so I chopped this off to 32
private static string key = "12345678901234567890123456789012";
private static string originalText = "does this work";
private static string encryptedValue = "a64d63874ba9ee8f5a5028cb40ab70a4yFFQwLuOHeWouyfp0dyrnw==";

public static void Main()
{
// Console.WriteLine(OpenSslDecrypt(encryptedValue, key));
Console.WriteLine("decrypted text: " + DecryptPHP(encryptedValue));
}

public static byte[] FromHex(string hex)
{
hex = hex.Replace("-", "");
byte[] raw = new byte[hex.Length / 2];
for (int i = 0; i < raw.Length; i++)
{
raw[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return raw;
}

public static string DecryptPHP(string encrypted)
{
var hexSalt = encryptedValue.Substring(0, 32);
byte[] stringSalt = FromHex(hexSalt);

var saltBytes = FromHex(hexSalt);
Console.WriteLine("salt length bytes: " + saltBytes.Length);
var unsaltedEncryptedValue = encryptedValue.Substring(32);

Console.WriteLine("salt: " + hexSalt);
Console.WriteLine("unsaltedEncryptedValue: " + unsaltedEncryptedValue);

//get the encrypted data and decrypt
byte[] unsaltedEncryptedBytes = Convert.FromBase64String(unsaltedEncryptedValue);
// byte[] unsaltedEncryptedBytes = System.Text.ASCIIEncoding.UTF8.GetBytes(unsaltedEncryptedValue);

// Fix 3: key should be text, not hex.
// var _key = FromHex(key);
byte[] keyBytes = System.Text.ASCIIEncoding.UTF8.GetBytes(key);

return DecryptStringFromBytesAes(unsaltedEncryptedBytes, keyBytes, saltBytes);
}

public static string DecryptStringFromBytesAes(byte[] cipherText, byte[] key, byte[] iv)
{
// Check arguments.
if (cipherText == null || cipherText.Length <= 0)
throw new ArgumentNullException("cipherText");
if (key == null || key.Length <= 0)
throw new ArgumentNullException("key");
if (iv == null || iv.Length <= 0)
throw new ArgumentNullException("iv");

// Declare the RijndaelManaged object
// used to decrypt the data.
RijndaelManaged aesAlg = null;

// Declare the string used to hold
// the decrypted text.
string plaintext;

// Create a RijndaelManaged object
// with the specified key and IV.
// Fix 2: PaddingMode should be PKCS7, based on my tests. It gets rid of the extra ? chars.
aesAlg = new RijndaelManaged { Mode = CipherMode.CBC, Padding = PaddingMode.PKCS7, KeySize = 256, BlockSize = 128, Key = key, IV = iv };

// Create a decrytor to perform the stream transform.
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for decryption.
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
// Read the decrypted bytes from the decrypting stream
// and place them in a string.
plaintext = srDecrypt.ReadToEnd();
srDecrypt.Close();
}
}
}

return plaintext;
}
}

dotnetfiddle link: https://dotnetfiddle.net/s2tfmM

Convert C# TripleDES ECB decrypt/encrypt into PHP with openssl_encrypt/openssl_decrypt

Okay so I found the solution by reading through related questions. I've found it in a 7 years old question and even though it was asked for deprecated mcrypt functions, somehow it's still working even with openssl functions.

All that had to be done, was to append the first 8 bytes of the raw key to itself like this:

$sKey = md5("<the same random key with umlauts and special characters length of 24 as in c#>", true);
$sKey .= substr( $sKey, 0, 8 );

Which gives the following, working example in PHP:

function encrypt( $key, $value )
{
if( function_exists( 'openssl_encrypt' ) )
{
return base64_encode( openssl_encrypt( $value, 'DES-EDE3', $key, OPENSSL_RAW_DATA ) );
}

return 'openssl missing';
}

function decrypt( $key, $value )
{
if( function_exists( 'openssl_decrypt' ) )
{
return openssl_decrypt( base64_decode( $value ), 'DES-EDE3', $key, OPENSSL_RAW_DATA );
}

return 'openssl missing';
}

$sKey = md5("<the same random key with umlauts and special characters length of 24 as in c#>", true);
$sKey .= substr( $sKey, 0, 8 ); //Added this line to fix it
$number = '1234567890';
$encrypted = encrypt( $sKey, $number );
$decrypted = decrypt( $sKey, $encrypted );

echo 'encrypted: ' . var_export($encrypted, true) . '<br>';
echo 'decrypted: ' . var_export($decrypted, true). '<br>';

If anyone can comment why this is the solution, please do so. While I'm pleased with it working, I'd really like to understand why it's working. I cannot see anything like this happening in C# so why do I have to do it in PHP?

PHP encryption not compatible with .NET/C# decryption (AES-256/CBC)

When using the sample data you provided, encryption with the .NET code:

string vendorKey = "0123456789012345";
string token = "012345";
string pt = @"{""prospectNo"":""SL1000000"",""paymentRequestDateFrom"":""2020-05-28"",""paymentRequestDateTo"":""2020-06-02"",""merchantTransactionId"":""7"",""callerReferenceNo"":""3""}";
string ct = Encrypt(pt, vendorKey, token);
Console.WriteLine(ct);

returns the following ciphertext:

g163a7jXmZKjH1J3RjC7xkPn5+PJWY6wTX9BgxiTY8hkYjsqImlCuvXOtZgUrrfLnwLy1QGUk6iylc/sInV/XJ9sypJ93tCvjRoj4s4RWGKTqUk3bY31JTM6QuYVclw4zNvyq2WUBCc+EMGGYtn5dBAvqiYdTqrJJTae67EZfgc4Fw5ormmf0rCYXQ2mn7mc1Jdg8v2r3LK9FYiwLEbhOA==

The PHP code below:

<?php
$cipher = "AES-256-CBC";

$array = json_encode(array(
"prospectNo"=> "SL1000000",
"paymentRequestDateFrom"=>"2020-05-28",
"paymentRequestDateTo"=>"2020-06-02",
"merchantTransactionId"=> "7",
"callerReferenceNo"=>"3"
)
);

$token = "012345";
$vendorKey = "0123456789012345";
$key = substr(str_pad($vendorKey . $token, 32, "\0"), 0, 32);
$iv = substr(str_pad($vendorKey, 16, "\0"), 0, 16);

$encrypted_data = openssl_encrypt(zeroPad($array, 16), $cipher, $key, OPENSSL_ZERO_PADDING, $iv);
print($encrypted_data . "\n");

function zeroPad($text, $bs) {
$pad = ($bs - strlen($text) % $bs) % $bs;
return ($pad > 0) ? $text . str_repeat("\0", $pad) : $text;
}
?>

gives the same ciphertext and is thus the PHP counterpart you are looking for, i.e. under the premise that the API can process the data encrypted by the .NET code, it must also process the data encrypted by the PHP code.


As expected, this ciphertext is decrypted into the correct plaintext by the .NET code. The hex encoded plaintext also reveals that the .NET code does not remove the padding bytes (note the 8 0x00 bytes at the end):

string vendorKey = "0123456789012345";
string token = "012345";
string ct = "g163a7jXmZKjH1J3RjC7xkPn5+PJWY6wTX9BgxiTY8hkYjsqImlCuvXOtZgUrrfLnwLy1QGUk6iylc/sInV/XJ9sypJ93tCvjRoj4s4RWGKTqUk3bY31JTM6QuYVclw4zNvyq2WUBCc+EMGGYtn5dBAvqiYdTqrJJTae67EZfgc4Fw5ormmf0rCYXQ2mn7mc1Jdg8v2r3LK9FYiwLEbhOA==";
string dt = Decrypt(ct, vendorKey, token);
Console.WriteLine("Plaintext: " + dt);
Console.WriteLine("Plaintext, hex: " + Convert.ToHexString(Encoding.UTF8.GetBytes(dt)));

with the output:

Plaintext:      {"prospectNo":"SL1000000","paymentRequestDateFrom":"2020-05-28","paymentRequestDateTo":"2020-06-02","merchantTransactionId":"7","callerReferenceNo":"3"}
Plaintext, hex: 7B2270726F73706563744E6F223A22534C31303030303030222C227061796D656E74526571756573744461746546726F6D223A22323032302D30352D3238222C227061796D656E745265717565737444617465546F223A22323032302D30362D3032222C226D65726368616E745472616E73616374696F6E4964223A2237222C2263616C6C65725265666572656E63654E6F223A2233227D0000000000000000

The PHP code posted in this answer differs from your original PHP code essentially only in a more general derivation of $key and $iv (but this makes no difference for the vendorKey used here) and the padding. The original PHP code applied the default PKCS#7 padding used by openssl_encrypt(), while the current PHP code applies Zero padding.

Specifically, for the current plaintext, this means that the original PHP code padded with 0x0808080808080808, while the current PHP code pads with 0x0000000000000000. Since the .NET code does not remove the padding, the padding bytes are still present even when using the current PHP code (just with different values).



Related Topics



Leave a reply



Submit