I'm implementing a third party API
that asks me to encrypt the payload of a POST
request "as done in this PHP
example":
class RESTfulAPI {
function __construct($DomainOrIP, $Key, $Secret) {
$this->BaseUri = $DomainOrIP ."/api/v2/";
$this->ApiKey = $Key;
$this->ApiSecret = $Secret;
// Encription vector initialization
$this->SecretIV = substr(hash("SHA256", $this->ApiKey, true), 0, 16);
}
function base64Url_Encode($data) {
return rtrim(strtr(base64_encode($data), ' /', '-_'), '=');
}
// Encription for properties
function APIEncryptData($Data) {
$output = openssl_encrypt(
$Data,
"AES-256-CBC",
md5($this->ApiSecret),
OPENSSL_RAW_DATA,
$this->SecretIV);
return $this->base64Url_Encode($output);
}
}
I can't implement it in PHP
, but in C#
. It seems very difficult to get the same result in the .NET
world.
What I've tried
These two methods are battle tested with PHP
equivalence. I tried them many times to compare C#
version results and PHP
version results and they are the same (I tried the PHP version here)
private static byte[] Sha256(string input)
{
using (SHA256 sha = SHA256.Create())
{
return sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
}
}
private static byte[] Md5(string input)
{
using (MD5 md5 = MD5.Create())
{
return md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
}
}
private string Base64UrlEncode(byte[] data)
{
var base64 = Convert.ToBase64String(data);
base64 = base64.Replace(" ", "-").Replace("/", "_").TrimEnd('=');
return base64;
}
The method I can't get to work is the APIEncryptData
. In this C#
I've tried:
_secretIV = Sha256(Key).Take(16).ToArray();
_hashedApiKey = Md5(Secret);
private string APIEncryptDataV1(string data)
{
using (var aes = Aes.Create())
{
aes.Key = _hashedApiKey;
aes.IV = _secretIV;
aes.Mode = CipherMode.CBC;
using (var encryptor = aes.CreateEncryptor())
{
using (var msEncrypt = new MemoryStream())
{
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (var swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(data);
}
return Base64UrlEncode(msEncrypt.ToArray());
}
}
}
}
}
// Using BouncyCastle
public string APIEncryptDataV2(string data)
{
var dataBytes = Encoding.UTF8.GetBytes(data);
// Create AES Engine
AesEngine engine = new AesEngine();
// Create CBC Mode
CbcBlockCipher blockCipher = new CbcBlockCipher(engine);
// Create Padding
Pkcs7Padding padding = new Pkcs7Padding();
// Create BufferedBlockCipher
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(blockCipher, padding);
// Create Key Parameter
KeyParameter keyParam = new KeyParameter(_hashedApiKey);
// Create IV Parameter
ParametersWithIV ivParam = new ParametersWithIV(keyParam, _secretIV);
// Init Cipher
cipher.Init(true, ivParam);
// Encrypt Data
var output = new byte[cipher.GetOutputSize(dataBytes.Length)];
int outputLength = cipher.ProcessBytes(dataBytes, output, 0);
cipher.DoFinal(output, outputLength);
return Base64UrlEncode(output);
}
...and many other methods I found here and in other parts of the web. I don't have experience with cryptography algorithms, so I'm in trouble.
What Am I doing wrong?
Sample inputs and outputs:
Key: Q8ZHQ8P5RH5V1RVYS29S3XHDV4PTS7XX
Secret: 527L1MDDQ7WDNDZ13ZHWLNY2D7JV5LXX
Data: Test123
PHP APIEncryptData result: xwMzbdEVqer8Py-c9hapFQ
C# APIEncryptData result: fcklnK82vuNT3DlLJF8h1A
C# APIEncryptDataV2 result: fcklnK82vuNT3DlLJF8h1A
CodePudding user response:
The C# code generates the same ciphertext as the PHP code when _hashedApiKey
is derived as follows:
byte[] _hashedApiKey = Encoding.UTF8.GetBytes(Convert.ToHexString(Md5(Secret)).ToLower());
The reason is that by default, md5()
in the PHP code returns the result as a hexadecimal encoded string in lowercase.
The porting flaw in the C# code is a direct result of the PHP code's key and IV derivation vulnerabilities:
- A key should be a random byte sequence, i.e. 32 bytes for AES-256. If a hexdecimal encoded 16 bytes value is used for this (which corresponds to 32 hexdigits or 32 bytes), each byte has a reduced value range of only 16 instead of 256 values, which means a reduction in security.
In addition, some libraries apply lowercase letters, some uppercase letters, which can lead to incompatibility (as in this case).
Another weakness is the use of a fast hash function like MD5 as key derivation function (KDF), which is made worse by the fact that MD5 itself is considered insecure.
The correct way is to apply a dedicated key derivation function such as Argon2, scrypt or at least PBKDF2 in conjunction with a random salt, directly generating a key of the required length, i.e. 32 bytes. - The use of a static IV is also a vulnerability, since it leads to reuse of key/IV pairs (for a fixed key). Instead, a random IV should be applied for each encryption, which is passed along with the ciphertext to the decrypting side, usually concatenated (note that the IV is not secret).