Encrypt binary files with openssl_private_encrypt in php [duplicate] - php

I am trying to use the PHP function openssl_private_encrypt() to encrypt an uploaded file prior to saving it (see code snippet below), however it's bool is returning false and the encrypted content return is returning nothing. No errors are being displayed or reported.
$data = file_get_contents($_FILES['files']['tmp_name'][0]);
openssl_private_encrypt($data,$encrypted,$key);
$hash = sha1($encrypted);
file_put_contents('/path/to/folder/'.$hash,$encrypted);
Does anyone have any ideas as to why this isn't working?
Thanks

I'm not sure about PHP but in C/C++(OpenSSL) asymmetric encryption(RSA mainly) works on data with length less than the key size. And normally it is used to encrypt hash values. If you want to encrypt large(more the ~256 bytes)amount of data you'd better use some symmetric(block) cipher like AES or TriDES. Symmetric ciphers are much faster by the way.
PS Sorry I don't have enough reputation to put this post into comments.

You should proper initialize private key (http://pl1.php.net/manual/en/function.openssl-pkey-get-private.php)
$key = openssl_pkey_get_private ('file://path/to/file.pem');
$data = file_get_contents($_FILES['files']['tmp_name'][0]);
openssl_private_encrypt($data,$encrypted,$key);
$hash = sha1($encrypted);
file_put_contents('/path/to/folder/'.$hash,$encrypted);

Related

Nodejs how to implement OpenSSL AES-CBC encryption (from PHP)?

I am currently working on translating an encryption algorithm from PHP to Typescript, to use in a very specific API that requires the posted data to be encrypted with the API key and Secret. Here is the provided example of how to correctly encrypt data in PHP for use with the API (the way of implementing the key and IV can't be changed):
$iv = substr(hash("SHA256", $this->ApiKey, true), 0, 16);
$key = md5($this->ApiSecret);
$output = openssl_encrypt($Data, "AES-256-CBC", $key, OPENSSL_RAW_DATA, $iv);
$completedEncryption = $this->base64Url_Encode($output);
return $completedEncryption;
In the above code, the only thing the base64Url_Encode function does is convert the binary data to a valid Base64URL string.
And now the code as I have implemented it inside Typescript:
import { createHash, createCipheriv } from 'node:crypto'
const secretIV = createHash('sha256').update(this.ApiKey).digest().subarray(0, 16)
// Generate key
/*
Because the OpenSSL function in PHP automatically pads the string with /null chars,
do the same inside NodeJS, so that CreateCipherIV can accept it as a 32-byte key,
instead of a 16-byte one.
*/
const md5 = createHash('md5').update(this.ApiSecret).digest()
const key = Buffer.alloc(32)
key.set(md5, 0)
// Create Cipher
const cipher = createCipheriv('aes-256-cbc', key, secretIV)
let encrypted = cipher.update(data, 'utf8', 'binary');
encrypted += cipher.final('binary');
// Return base64URL string
return Buffer.from(encrypted).toString('base64url');
The above Typescript code only does NOT give the same output as the PHP code given earlier. I have looked into the original OpenSSL code, made sure that the padding algorithms are matching (pcks5 and pcks7) and checked if every input Buffer had the same byte length as the input inside PHP. I am currently thinking if it is some kind of binary malform that is causing the data to change inside Javascript?
I hope some expert can help me out with this question. Maybe I have overlooked something. Thanks in advance.
The stupidity is in the md5 function in PHP, which defaults to hexadecimal output instead of binary output:
md5(string $string, bool $binary = false): string
This is also why the code doesn't complain about the key (constructed from the MD5 hash) is being too small, it is fed 32 bytes after ASCII or UTF8 encoding, instead of the 16 bytes you'd use for AES-128.
Apparently it is using lowercase encoding, although not even that has been specified. You can indicate the encoding for NodeJS as well, see the documentation of the digest method. It also seems to be using lowercase, although I cannot directly find the exact specification of the encoding either.
Once you have completed your assignment, please try and remove the code ASAP, as you should never calculate the IV from the key; they key and IV combination should be unique, so the above code is not IND-CPA secure if the key is reused.
In case you are wondering why it is so stupid: the output of MD5 has been specified in standards, and is binary. Furthermore, it is impossible from the function to see what it is doing, you have to lookup the code. It will also work very badly if you're doing a compare; even if you are comparing strings then it is easy to use upper instead of lowercase (and both are equally valid, uppercase hex is actually easier to read for humans as we focus on the top part of letters more for some reason or other).
Basically it takes the principle of least surprise and tosses it out of the window. The encoding of the output could be made optimal instead, the NodeJS implementation does this correctly.

How to get the fingerprint of a public key?

In PHP, I have a public key (already as an OpenSSL resource). I'd like to calculate that public keys fingerprint (SHA1 or other hash).
How do I do this?
PHP version is 7.2 or older.
EDIT: This is not a duplicate of the other question, because that question is about doing this for an SSH key with SSH (and related) commands. I want to do the same thing using PHP and the PHP-extension openssl.
I've found the solution myself. The basic idea:
Decode the base64 encoded part of the key in PEM format (after removing spaces from it)
Hash the resulting binary data.
In code (PHP):
function getPublicKeyFingerprint(string $pemEncodedKey, string $hashAlgorithm = 'sha1')
{
$keyWithoutPemWrapper = \preg_replace(
'/^-----BEGIN (?:[A-Z]+ )?PUBLIC KEY-----([A-Za-z0-9\\/\\+\\s=]+)-----END (?:[A-Z]+ )?PUBLIC KEY-----$/ms',
'\\1',
$pemEncodedKey
);
$keyDataWithoutSpaces = \preg_replace('/\\s+/', '', $keyWithoutPemWrapper);
$binaryKey = \base64_decode($keyDataWithoutSpaces);
return \hash($hashAlgorithm, $binaryKey);
}
if you need a hash php has multiple hashing functions available:
sha1() - https://php.net/manual/en/function.sha1.php
md5() - https://www.php.net/manual/en/function.md5.php
or use hash() if you need a more specific algorithm - see the docs

Questions about Laravel encryption

I have some questions about the Encrypt class from Laravel. I am hoping somebody could answer them.
It's mostly about the encrypt method on line 70 here:
https://github.com/illuminate/encryption/blob/master/Encrypter.php#L70
public function encrypt($value)
{
$iv = random_bytes(16);
$value = \openssl_encrypt(
serialize($value), $this->cipher, $this->key, 0, $iv
);
if ($value === false) {
throw new EncryptException('Could not encrypt the data.');
}
$mac = $this->hash($iv = base64_encode($iv), $value);
$json = json_encode(compact('iv', 'value', 'mac'));
if (! is_string($json)) {
throw new EncryptException('Could not encrypt the data.');
}
return base64_encode($json);
}
I have learned about openssl_encrypt and it seems like a good fit for a personal use case. I have made encrypt and decrypt methods using it.
The Laravel does a whole lot more than simply encrypting though.
Why does laravel serialize the value on encryption? If it's always a string that this method takes what is the advantages of serializing the data?
Why is base64_encode being used here?
Why are the values json_encoded? Is this used to keep a clean array or for other intents as well? Currently in my class's encrypt() method I simply concat the encrypted $value and $iv(Initialization vector). On decrypt I break them apart again.
Won't the openssl_encrypt throw already throw an exception when it can not encrypt data? Why is the return value checked if this is the case?
If somebody could take a minute to take a look at the github repo on the specified line I would be really happy.
Cheers!
Why does laravel serialize the value on encryption? If it's always a string that this method takes what is the advantages of serializing the data?
The function doesn't take just a string. It takes any (serializable) data type. It serializes the input value to convert it to a string; one that can be returned back to its original type upon decryption. I know the PhpDoc above the function states that the parameter is a string, but that seems like it is wrong and needs to be changed to mixed.
Why is base64_encode being used here?
base64 encoding is used on the $iv to convert the random bytes to a usable string by the hash method.
As far as base64 encoding the return value, I do not know. It seems as if they could have left it as the json encoded string, but maybe they wanted to make sure that the returned string did not have any special characters, or maybe they wanted one more piece of obfuscation.
Why are the values json_encoded? Is this used to keep a clean array or for other intents as well? Currently in my class's encrypt() method I simply concat the encrypted $value and $iv(Initialization vector). On decrypt I break them apart again.
Yes, the json_encoding is just to help facilitate a clean and easily parsible payload. They have three values they need to keep track of, and the easiest and cleanest way to be able to keep the values separated is with an array.
Won't the openssl_encrypt throw already throw an exception when it can not encrypt data? Why is the return value checked if this is the case?
I don't see any indications in the PHP documentation that openssl_encrypt will throw an exception. It does state, however, that it returns false on failure, which is the reason for the strict check on false.
Now, there are two reasons why it may emit a PHP Warning, which Laravel converts to an exception, but those aren't the only two ways that encryption may fail.

openssl_private_encrypt() returns false with output of 0

I am trying to use the PHP function openssl_private_encrypt() to encrypt an uploaded file prior to saving it (see code snippet below), however it's bool is returning false and the encrypted content return is returning nothing. No errors are being displayed or reported.
$data = file_get_contents($_FILES['files']['tmp_name'][0]);
openssl_private_encrypt($data,$encrypted,$key);
$hash = sha1($encrypted);
file_put_contents('/path/to/folder/'.$hash,$encrypted);
Does anyone have any ideas as to why this isn't working?
Thanks
I'm not sure about PHP but in C/C++(OpenSSL) asymmetric encryption(RSA mainly) works on data with length less than the key size. And normally it is used to encrypt hash values. If you want to encrypt large(more the ~256 bytes)amount of data you'd better use some symmetric(block) cipher like AES or TriDES. Symmetric ciphers are much faster by the way.
PS Sorry I don't have enough reputation to put this post into comments.
You should proper initialize private key (http://pl1.php.net/manual/en/function.openssl-pkey-get-private.php)
$key = openssl_pkey_get_private ('file://path/to/file.pem');
$data = file_get_contents($_FILES['files']['tmp_name'][0]);
openssl_private_encrypt($data,$encrypted,$key);
$hash = sha1($encrypted);
file_put_contents('/path/to/folder/'.$hash,$encrypted);

Decrypting the .ASPXAUTH Cookie WITH protection=validation

For quite sometime I've been trying to decipher the ASP .ASPXAUTH cookie and decrypt it using PHP. My reasons are huge and I need to do this, there is no alternative. In PHP so far I have successfully managed to read the data from this cookie, but I cannot seem to do it while it is encrypted. Anyway, here it goes...
First you need to alter your servers Web.config file (protection needs to be set to Validation):
<authentication mode="None">
<forms name=".ASPXAUTH" protection="Validation" cookieless="UseCookies" timeout="10080" enableCrossAppRedirects="true"/>
</authentication>
Then in a PHP script on the same domain, you can do the following to read the data, this is a very basic example, but is proof:
$authCookie = $_COOKIE['_ASPXAUTH'];
echo 'ASPXAUTH: '.$authCookie.'<br />'."\n";//This outputs your plaintext hex cookie
$packed = pack("H*",$authCookie);
$packed_exp = explode("\0",$packed);//This will separate your data using NULL
$random_bytes = array_shift($packed_exp);//This will shift off the random bytes
echo print_r($packed_exp,TRUE); //This will return your cookies data without the random bytes
This breaks down the cookie, or at least the unencrypted data:
Now that I know I can get the data, I removed the 'protection="validation"' string from my Web.config and I tried to decrypt it using PHP mcrypt. I have tried countless methods, but here is a promising example (which fails)...
define('ASP_DECRYPT_KEY','0BC95D748C57F6162519C165E0C5DEB69EA1145676F453AB93DA9645B067DFB8');//This is a decryption key found in my Machine.config file (please note this is forged for example)
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC), MCRYPT_RAND);
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, ASP_DECRYPT_KEY, $authCookie, MCRYPT_MODE_CBC, $iv);//$authCookie is the pack()'d cookie data
This however fails. I've tried variations of IV with all zeros # 16 bytes. I've tried different Rijndael sizes (128 vs 256). I've tried base64_decode()ing, nothing seems to work. I've found this stackoverflow post here and started using variations of the key/iv that are made using sha256, but that isn't really working either.
Anybody have a clue what I should do?
I don't know how encryption is made in .NET AuthCookies, but I can try to answer.
Assuming the encryption occurs in AES CBC-IV mode, with randomly generated IVs, you need to first find out where the IV is.
The code snippet you show cannot work, as you are generating a random IV (which will be incorrect). That being said, even if you get the IV wrong, in CBC mode you will only have the first 16 bytes of your decrypted ciphertext "garbled" and the rest will decrypt properly - you can use this as a test to know if you're doing the rest correctly. In practice when using random IVs, it's very likely that it's prepended to the ciphertext. To check if this correct, you can try to check if len(ciphertext) = len(plaintext) + 16. This would mean that most likely the first 16 bytes are your IV (and therefore it should be removed from the ciphertext before attempting to decrypt it).
Also on your code snippet, it seems you are using the key as an ascii-string, whereas it should be a byte array. Try:
define('ASP_DECRYPT_KEY',hex2bin('0BC95D748C57F6162519C165E0C5DEB69EA1145676F453AB93DA9645B067DFB8'));
Also, this seems to be a 32 byte key, so you need to use AES-256. I don't know how the authcookie looks like, but if it's base64 encoded, you also need to decode it first obviously.
Hope this helps!
Note: I don't recomment doing this for important production code, however - because there are many things that can go wrong if you try to implement even your own decryption routine as you are doing here. In particular, I would guess there should be a MAC tag somewhere that you have to check before attempting decryption, but there are many other things that can go wrong implementing your own crypto.
I understand this may not have been possible for the OP but for other people heading down this route here is a simple alternative.
Create a .net web service with a method like:
public FormsAuthenticationTicket DecryptFormsAuthCookie(string ticket)
{
return FormsAuthentication.Decrypt(ticket);
}
Pass cookie to web service from PHP:
$authCookie = $_COOKIE['.ASPXAUTH'];
$soapClient = new SoapClient("http://localhost/Service1.svc?wsdl");
$params= array(
"ticket" => $authCookie
);
$result = $soapClient->DecryptFormsAuthCookie($params);
I know what a pain is to decrypt in PHP something encrypted in .NET and vice versa.
I had to end up coding myself the Rijndael algorithm ( translated it from another language ).
Here is the link to the source code of the algorithm: http://pastebin.com/EnCJBLSY
At the end of the source code there is some usage example.
But on .NET, you should use zero padding when encrypting. Also test it with ECB mode, I'm not sure if CBC works.
Good luck and hope it helps
edit: the algorithm returns the hexadecimal string when encrypts, and also expects hexadecimal string when decrypting.

Categories