I'm having an issue encrypting my string in C++ and then decrypting in PHP. On the C++ side, I think everything is going fine. Below is my code for the C++ side.
unsigned char inbuffer[1024];
unsigned char outbuffer[1024];
unsigned char oneKey[] = "abc";
AES_KEY key;
AES_set_encrypt_key(oneKey, 128, &key);
string straa("hello world\n");
memcpy((char*)inbuffer, straa.c_str(), 13);
AES_encrypt(inbuffer, encryptedbuffer, &key);
LPCSTR pszSource = (LPCSTR)encryptedbuffer;
DWORD nDestinationSize;
if (CryptBinaryToString(reinterpret_cast<const BYTE*> (pszSource), strlen(pszSource), CRYPT_STRING_BASE64, nullptr, &nDestinationSize))
{
LPTSTR pszDestination = static_cast<LPTSTR> (HeapAlloc(GetProcessHeap(), HEAP_NO_SERIALIZE, nDestinationSize * sizeof(TCHAR)));
if (pszDestination)
{
if (CryptBinaryToString(reinterpret_cast<const BYTE*> (pszSource), strlen(pszSource), CRYPT_STRING_BASE64, pszDestination, &nDestinationSize))
{
printf("OUT: %s", pszDestination); // Base64 encoded output of encrypted string
}
}
}
This is my PHP code:
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('AES-256-CBC'));
$out = openssl_decrypt(base64_decode("G6g0f/K7EzAw8fxn9BTFzw=="), 'AES-256-CBC', "abc", OPENSSL_RAW_DATA, $iv);
echo $out;
There is no output from the PHP code.
The C++ code would output the following:
OUT: G6g0f/K7EzAw8fxn9BTFzw==
CBC mode, the mode used in the PHP code, does require an iv and it must be the same as the one used for encryption, also the modes must be the same for both encryption and decryption. For CBC mode you need to supply a block sized iv (16-bytes for AES). The encryption key also should to be the correct size.
The iv is not explicitly set in the C++ code and the mode and padding use the default, it is always better to expel;icitly specify all parameters. The iv should be random for each encryption.
You might consider RNCryptor, it is multi language and platform and handles all the details.
Related
I have this code in PHP (can't modify)
<?php
$myPlain = "123456789012345678900000";
$myKey = md5($myPlain, true);
$myKey .= substr($myKey, 0,8);
$encrypted = openssl_encrypt($myPlain, 'des-ede3', $myKey, OPENSSL_RAW_DATA);
print(base64_encode($encrypted));
This code returns
FTYDrZTZMjVBv5Fk/xcfFxJASaizzrmoPts7fGDvWjc=
When I try to replicate this in NodeJS
function testEde3(myKey, myPlain) {
try {
let md5Key = crypto.createHash('md5').update(myKey, 'utf-8').digest("base64").substr(0, 24);
console.log(md5Key); //outputs 4o1aJrSWN3bSfjuIX6VXgA==
console.log(md5Key.length); //outputs 24
const cipher = crypto.createCipheriv('des-ede3', md5Key, null);
let encrypted = cipher.update(myPlain, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
} catch ( ex ) {
return ex;
}
}
const myKey = "123456789012345678900000";
const myPlain = "123456789012345678900000";
const hash = testEd3(myKey, myPlain);
console.log(`Hash is: ${hash}`);
The output is
Hash is: lDQX9OGsopKOt6P9WQwekGsKDQGFpfGW50zbs3HrOfQ=
I'm thinking the problem is on MD5. If I try to encrypt without md5, the results are the same.
Thanks
The key in Php code consists of the 16 bytes of the MD5 hash, to which the first 8 bytes of the MD5 hash are appended, resulting in a 24 bytes key (as required for 3DES). This is currently not implemented in the NodeJS code, but can be achieved e.g. by:
let md5Key = crypto.createHash('md5').update(myKey, 'utf-8').digest();
md5Key = Buffer.concat([md5Key, md5Key.slice(0, 8)]);
With this change, the NodeJS code generates the ciphertext of the PHP code with the same input data.
des-ede3 means 3DES in ECB mode. Note that 3DES is outdated and slow and should be replaced by AES. ECB mode is generally insecure. Encrypting the key also makes little sense (but may only be for testing purposes).
Setup:
session_start();
function set_encryption_method() {
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity']) > 3600) {
unset($_SESSION['cipher']);
unset($_SESSION['iv']);
unset($_SESSION['last_activity']);
}
$cipher = 'aes-256-cbc';
$iv = random_bytes(16);
if (in_array($cipher, openssl_get_cipher_methods())) {
if (!isset($_SESSION['cipher'])) {
$_SESSION['cipher'] = $cipher;
}
if (!isset($_SESSION['iv'])) {
$_SESSION['iv'] = $iv;
}
$_SESSION['last_activity'] = time();
} else {
die('Encryption method not supported!');
}
}
set_encryption_method();
Encrypt:
function encrypt_string($key, $string) {
// $key is a constant stored in a database
return rawurlencode(base64_encode(openssl_encrypt($string, $_SESSION['cipher'], $key, 0, $_SESSION['iv'])));
}
Decrypt:
function decrypt_string($key, $encrypted) {
// $key is a constant stored in a database
return openssl_decrypt(rawurldecode(base64_decode($encrypted)), $_SESSION['cipher'], $key, 0, $_SESSION['iv']);
}
When decrypt_string() is called with the appropriate parameters, it throws this error: digital envelope routines evp_decrypt_final_ex: bad decrypt. If I hardcode the iv, then it works correctly.
What am I doing wrong?
The error message is (indirectly) caused by the fact that your are using different IVs for encryption and decryption. From your description it is not clear how that can happen, but let me propose some suggestions that will avoid your issue altogether.
First, with EAS-CBC it is not a good idea to use the same IV + key combination multiple times. You can find some discussion on that in the answers to Is AES in CBC mode secure if a known and/or fixed IV is used?. You did not mention how you are using the different functions, but during 3600 seconds, you are using the same IV + key combinations.
To work around this, you could generate a random IV for every encryption that you do. You could then store the IV together with the encrypted data; the IV is not required or supposed to be secret. The following two functions are modifications of yours that do exactly that, by concatenating the two after encryption and splitting the two before decryption:
function encrypt_string($key, $string) {
// $key is a constant stored in a database
$iv = random_bytes(16);
$ciphtxt = openssl_encrypt($string, $_SESSION['cipher'], $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($iv.$ciphtxt);
}
function decrypt_string($key, $encrypted) {
// $key is a constant stored in a database
$combo = base64_decode($encrypted);
$iv = substr($combo, 0, 16);
$ciphtxt= substr($combo, 16);
return openssl_decrypt($ciphtxt, $_SESSION['cipher'], $key, OPENSSL_RAW_DATA, $iv);
}
Note the use of the flag OPENSSL_RAW_DATA. As the documentation for openssl_encrypt mentions (not too clearly), the result will be base64-ed if you do not give that flag. You were doing the base64-ing yourself so I added the flag. This also makes it a little easier to handle the (de-)concatenation of the IV and the ciphertext.
A few words about the occurrence of that bad decrypt error: that is emitted when the padding bytes are different than expected. Since you were using an incorrect IV for decryption, the AES decryption did not result in the original data, with a high probability that the padding bytes therefore did not have the right value.
First point to address: Why are you using URL encode/decode?
Those are for passing parameters through browsers; and the browser generally does the decoding for you so as a base rule - if you've written "urldecode" in your PHP, you've probably gone wrong as you're possibly decoding something already decoded. (Or are not using in the right place, or using when unnecessary)
However, Base64 is (or should be) url-safe, so I'd suggest knocking out the urlencode/decode and see what happens.
Second point to address: you need to undo the urlencode/base64 conversion in the reverse order you set.
So rawurlencode(base64_encode(openssl_encrypt( reversed is openssl_decrypt(base64_encode(rawurldecode (you have the base64 and url decode flipped). But knocking out the urlencode/decode entirely (unless you're adamant you need it) will rectify this too.
I'm receiving an encrypted message from an external party. They're using php and mcrypt for encryption with the following configuration
algo: rijndael-256
mode: ecb
example encrypted message: "a364cb3bb55c2788b25f04ef6867d770771a7d0fdd44462b40233ea76a8bd00d". Its original message is "thank you"
The following images showing the configuration
From my side, I'm using nodejs to decrypt. I have a working version with node-mcrypt binding. But I want to avoid native modules so I'm looking for a pure javascript solution.
I tried node's crypto but its aes-256-ecb produces different (unreadable) output
The working code with MCrypt binding in nodejs:
const decryptAES256 = (encrypted: string, password: string): string => {
try {
const desEcb = new MCrypt('rijndael-256', 'ecb');
desEcb.open(md5(password));
const ciphertext = desEcb.decrypt(new Buffer(encrypted, 'hex'));
const plain = ciphertext.toString();
const isUTF8 = plain.split('').every(c => c.charCodeAt(0) <= 256);
if (!isUTF8) {
throw new Error('Invalid Input');
}
const unpadding = plain
.split('')
.filter(c => c.charCodeAt(0))
.join('');
return unpadding;
} catch (ex) {
console.error('Invalid token');
}
};
When you state "rijndael-256" that maybe specifying a 256-bit block size which is not AES. AES has one block size: 128-bits.
This seems to be born out by the encrypted output which is 32-binary bytes and for an input of "thank you" the output should be one block so it seems the block size is 32-bytes (256-bits).
Do not use mcrypt, it will just cause interoperability problems.
Make sure you are using AES which can be rijndael with a 128-bit block size.
Do not confuse the key size with the block size.
If the input data is not always a multiple of the block size then padding must be added to the input data], specify padding, PKCS#7 (née PKCS#5) padding which may be the default. mcrypt does not support standard padding. If you must interoperate with mcrypt you will need to specify no padding and perform the padding yourself.
After realizing that rijndael 256 is not equivalent to aes 256 (with 128 version is), I opt to find a rijndael specific lib in pure javascript and this is a working code:
const Rijndael = require('rijndael-js');
describe('cipher', () => {
it('should work', () => {
const text = 'thank you';
// + String.fromCharCode(0).repeat(5)
const key = '94a08da1fecbb6e8b46990538c7b50b2';
const cipher = new Rijndael(key, 'ecb');
const ciphertext = cipher.encrypt(text, 256);
expect(ciphertext.toString('hex')).toBe(
'a364cb3bb55c2788b25f04ef6867d770771a7d0fdd44462b40233ea76a8bd00d'
);
const plaintext = cipher.decrypt(ciphertext, 256);
const unpadded = plaintext.toString().replace(/\u0000*$/g, '');
expect(unpadded).toBe(text);
});
});
I have a next problem
On Node.js I have a next code
var iv = CryptoJS.enc.Hex.parse('00000000000000000000000000000000'); //it's for tests, later it will be dynamically generated
var key256Bits = 'A5178B6A965AACF3CD60B07A15061719';
var cipher = CryptoJS.AES.encrypt(
'Some text',
key256Bits,
{
iv: iv,
padding:CryptoJS.pad.ZeroPadding
}
).toString();
Then when I try to decode it with phpseclib
$key = 'A5178B6A965AACF3CD60B07A15061719';
$data = /*text encrypted by JS*/;
$cipher = new AES();
$cipher->setKeyLength(256);
$cipher->setKey($key);
$res = $cipher->decrypt($data);
And then $res becomes an empty string
What do I do wrong?
If you pass in a string to CryptoJS.<cipher>.encrypt as a key, CryptoJS treats it as a password and will derive the actual key from that using OpenSSL's EVP_BytesToKey with a random salt and one iteration of MD5.
phpseclib doesn't have an implementation of that, so you could just pass in the actual key:
var key256Bits = CryptoJS.enc.Utf8.parse('A5178B6A965AACF3CD60B07A15061719');
Since this key is only 32 hexits long, it only has 128 bit of entropy, but still uses AES-256. You need 64 hexits which you can decode before use to get 32 bytes for a secure key size.
Also, phpseclib implements PKCS#7 padding, so you need to use
padding: CryptoJS.pad.Pkcs7
in CryptoJS.
Example JavaScript code:
var iv = CryptoJS.enc.Hex.parse('00000000000000000000000000000000'); //it's for tests, later it will be dynamically generated
var key256Bits = CryptoJS.enc.Utf8.parse('A5178B6A965AACF3CD60B07A15061719');
var cipher = CryptoJS.AES.encrypt(
'Some text',
key256Bits,
{
iv: iv,
padding: CryptoJS.pad.Pkcs7
}
).toString();
console.log(cipher)
<script src="https://cdn.rawgit.com/CryptoStore/crypto-js/3.1.2/build/rollups/aes.js"></script>
In PHP, you need to make sure to decode the ciphertext before use:
$data = base64_decode("IWkBG3A46rNrxwWN2JD7xQ==");
$key = 'A5178B6A965AACF3CD60B07A15061719';
$cipher = new AES();
$cipher->setKeyLength(256);
$cipher->setKey($key);
$res = $cipher->decrypt($data);
var_dump($res);
Security consideration:
If you're using only symmetric encryption you need the exact same key at the server and the client. If you send the encryption key from the server to the client or the other way around you need to encrypt your symmetric encryption key. The easiest way to do this would be to use TLS. If you use TLS, then the data as well as key are encrypted, so you don't need to encrypt it yourself. This doesn't provide any security, just a little bit of obfuscation. You should read: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/august/javascript-cryptography-considered-harmful/
I am working on an application which generates RSA encrypted session keys and stores them in a database. Later theses keys are transfered to a C++ application via Javascript. Therefore I want to use the OpenSSL library. I generated a 2048 bit key pair with openssl, which is used in both methods.
My PHP functions works like this:
function encrypt_with_public_key($input, $key)
{
openssl_public_encrypt($input, $crypttext, $key, OPENSSL_PKCS1_OAEP_PADDING);
return $crypttext;
}
and
$fp = fopen("public.pem","r");
$public_pem = fread($fp,8192);
fclose($fp);
$public_key = openssl_get_publickey($public_pem);
$sessionKey = ...;
$encSessionKey = encrypt_with_public_key($sessionKey, $public_key);
I tested this part successfully. The part I have trouble with, is the C++ part. I use MS Visual Studio 2013. Edit: added hex encoding (using Crypto++)
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/err.h>
#include <openssl/crypto.h>
...
string decrypted, encoded;
decrypted.clear();
char privateKey[] =
"-----BEGIN RSA PRIVATE KEY-----\n"\
...
RSA *rsa = NULL;
BIO *keybio;
keybio = BIO_new_mem_buf(privateKey, strlen(privateKey));
rsa = PEM_read_bio_RSAPrivateKey(keybio, &rsa, NULL, NULL);
StringSource ss2(input, true,
new HexEncoder(
new StringSink(encoded)
)
);
RSA_private_decrypt(encoded.length(), (unsigned char *)encoded.data(), (unsigned char *)decrypted.data(), rsa, RSA_PKCS1_OAEP_PADDING);
FBLOG_INFO("", ERR_error_string(ERR_get_error(), NULL));
return decrypted;
Note that the private key is not read from a file.
OpenSSL returns the following error: 0406506C: lib(4): func(101): reason(108).
It means afaik that my input data is longer than the modulus length (please correct me if I'm wrong). Anyone who knows how to handle this? I thought such problems are solved through the padding parameters.
The input data is the direct output of the php encrypt function (no base64 oder anything).