On a webpage, the end user can see a list with links. These links need to send an ID to a form via the URL. This form then collects the parameter from the URL and uses this ID to retrieve info from a database.
To disable the end user from simply changing the ID in the URL, I went with encrypting the ID via the openssl_encrypt
function in PHP. This function also requires an Initialization Vector(IV) which is 16 characters long because I use AES-128-CTR
as the cypher method. So for every link I also generate a unique IV, then I combine the IV with the encrypted ID. Since this can have characters that are not allowed in URL's I then use urlencode
to make the link safe.
And for testing decrypting, I also decrypt each encrypted message on the page with all the links. This works nicely.
See here the code for this page. $submissionId is a number and all echos are for testing purposes
.
// Display the original string
echo "Original String: " . $submissionId;
// Store the cipher method
$ciphering = "AES-128-CTR";
// Use OpenSSl Encryption method
$iv_length = openssl_cipher_iv_length($ciphering);
$options = 0;
// Create a Initialization Vector
$iv = random_bytes(8);
echo "<br><br>" . bin2hex($iv) . "<br><br>";
// Non-NULL Initialization Vector for encryption
$encryption_iv = $iv;
// Store the encryption key
$encryption_key = "test";
// Use openssl_encrypt() function to encrypt the data
$encryption = openssl_encrypt($submissionId, $ciphering,
$encryption_key, $options, $encryption_iv);
// Display the encrypted string
echo "Encrypted String: " . $encryption . "<br>";
// Encrypted submissionId
$encryptedSubmissionIdAndIv = bin2hex($iv).$encryption;
$encryptedSubmissionIdAndIvUrlEncoded = urlencode( $encryptedSubmissionIdAndIv );
echo "Encrypted Sub ID: " . $encryptedSubmissionIdAndIv . "<br>";
echo "Encrypted Sub ID URL Encoded: " . $encryptedSubmissionIdAndIvUrlEncoded . "<br>";
// BELOW IS DECRYPTING FOR TESTING!!!
// Non-NULL Initialization Vector for decryption
$decryption_iv = $iv;
// Store the decryption key
$decryption_key = "test";
// Use openssl_decrypt() function to decrypt the data
$decryption=openssl_decrypt ($encryption, $ciphering,
$decryption_key, $options, $decryption_iv);
// Display the decrypted string
echo "Decrypted String: " . $decryption;
On the form, I grab the parameter/component from the URL. The component named sId
has both the IV and the encrypted ID decoded. I then go ahead and set the cypher and then collect the IV from the sId
string and I collect the ID($encryption
).
Then I use the same code as on the page with the links to decrypt the ID. But on this page it returns garbage and never the correct ID. While on the links page, it works.
I checked the error logs on the server and there are no errors given. With garbage, I mean, it returns something like this: �b�
.
// Retrieving the encrypted submission id and iv that is url encoded and automatically decode via get.
$encryptedSubmissionIdAndIvUrlDecoded = $_GET['sId'];
echo "encryptedSubmissionIdAndIvUrlDecoded: " . $encryptedSubmissionIdAndIvUrlDecoded . "<br>";
// Store the cipher method
$ciphering = "AES-128-CTR";
// Use OpenSSl Encryption method
$iv_length = openssl_cipher_iv_length($ciphering);
$options = 0;
// Collecting the Initialization Vector
$iv = substr($encryptedSubmissionIdAndIvUrlDecoded, 0, 16);
echo "iv: " . $iv . "<br>";
// Collecting the encrypted message
$encryption = substr($encryptedSubmissionIdAndIvUrlDecoded, 16);
echo "encryption: " . $encryption . "<br>";
// Non-NULL Initialization Vector for decryption
$decryption_iv = $iv;
// Store the decryption key
$decryption_key = "test";
// Use openssl_decrypt() function to decrypt the data
$decryption = openssl_decrypt($encryption, $ciphering,
$decryption_key, $options, $decryption_iv);
// Display the decrypted string
echo "decryption: " . $decryption . "<br>";
- I have tried changing the length of the keys used. (For testing purposes, it is now simply test)
- Tried putting the IV in a separate parameter in the URL so that I did not have to combine the IV and ID, but this resulted in the same thing. Working on the links page, but not on the page of the form.
- Also tried by putting in the IV and encrypted message via hardcoding just to test, but this resulted in the same garbage.
- When I added a wrong key on purpose on the form page
$decryption_key = "testtesttesttest";
it does not show the garbage:�b�
but then it showsK
which is other garbage, but it is different. This makes me assume that the IV and the KEY is correct. - Some excellent suggestions were made in the comments and the above code reflects this.
I am not sure if it is important, but it is all on the same domain/server and PHP 8 version.
Anyone has an idea why it is returning something like this: �b�
on the form page, but is working correctly on the links page, where it returns something like 311
?
CodePudding user response:
The decryption in the 2nd snippet fails because the hex decoding of the IV is missing. With
$decryption = openssl_decrypt($encryption, $ciphering, $decryption_key, $options, hex2bin($decryption_iv));
decryption works.
The solution posted in your answer does not result in a ciphertext that could be decrypted by the 2nd code snippet (at least not without additional modifications). Apart from that, it is insecure as already mentioned in the comments.
Even after fixing the bug, there are some flaws and vulnerabilities in the code:
The IV size corresponds to the blocksize and this is 16 bytes for AES. In the code an 8 byte IV is used, so a too short IV. PHP pads a too short IV with 0x00 values up to the required length. Actually a too short IV triggers an appropriate warning: IV passed is only 8 bytes long, cipher expects an IV of precisely 16 bytes, padding with \0 in...
PHP allows to explicitly determine the IV withopenssl_cipher_iv_length()
, which is used in the code. However, the result is not taken into account.
Also, for security reasons, a random IV must be generated for each encryption, which is what happens in the code (except for the wrong length). The random IV prevents the reuse of key/IV pairs, which would be a very serious vulnerability especially for CTR mode:$iv_length = openssl_cipher_iv_length($ciphering); $iv = random_bytes($iv_length); // apply a random IV of the right length
Also, it is not necessary to hex encode the IV for concatenation when encrypting, instead the raw IV and raw ciphertext should be concatenated and the result then Base64 encoded. To do this, the implicit Base64 encoding of
openssl_encrypt()
must be disabled, which can be done with the flagOPENSSL_RAW_DATA
as 4th parameter. After that the raw ciphertext is returned.$options = OPENSSL_RAW_DATA; $encryption = openssl_encrypt($submissionId, $ciphering, $encryption_key, $options, $encryption_iv); // disable implicit Base64 encoding $encryptedSubmissionIdAndIv = base64_encode($encryption_iv.$encryption); // concatenate IV and ciphertext and Base64 encode
Accordingly, during decryption, first a Base64 decoding has to be done and then IV and ciphertext have to be separated:
$encryptedSubmissionIdAndIvUrlDecoded = base64_decode($encryptedSubmissionIdAndIv); // Base64 decode (URL decoded Data) $iv_length = openssl_cipher_iv_length($ciphering); $decryption_iv = substr($encryptedSubmissionIdAndIvUrlDecoded, 0, $iv_length); // separate IV... $encryption = substr($encryptedSubmissionIdAndIvUrlDecoded, $iv_length); // ...and ciphertext $options = OPENSSL_RAW_DATA; // disable implicit Base64 decoding $decryption = openssl_decrypt($encryption, $ciphering, $decryption_key, $options, $decryption_iv);
The key must not be a string for security reasons, but a random byte sequence of the correct length (16 bytes for AES-128). PHP pads a too short key material with 0x00 values up to the required length.
CodePudding user response:
I found the issue. It was not located as I was thinking on the second page (the form page) but it was actually on the first page with the links.
Here I was using the $iv = random_bytes(8);
to create a unique IV. I changed this to:
// Create a Initialization Vector by making a random integer and then padding it until it is 16 digits long
$iv = random_int(0, 999999);
$iv = str_pad($iv, 16, 0, STR_PAD_LEFT);
It needs to be 16 digits long for the cypher that is used. After making this change and after adding the excellent suggestions from the comments section from the question, it is now working. Thanks everyone for contributing to this question!
Be aware
If you decide to use this code for your own project, make sure to change the $decryption_key = "test"
to a secure key!