The PHP documentation for the openssl_encrypt functions states
string openssl_encrypt ( string $data , string $method , string
$password [, int $options = 0 [, string $iv = "" ]] )
Can somebody help me understand what the argument named $password is?
An answer could include a confirmation or rejection of the idea, that besides named $password the parameter indeed is used as the key for the encryption.
What is the password parameter to openssl_encrypt? Is it a password string (with only printable characters) or is it a key (with non-prinatable characters and ASCII-Z terminators)?
Explanation
I am stuck with the documention of PHP's openssl_encrypt. Being a nice guy and trying to do the "RTM" I cannot make much sense with the imho unsatisfying documentation.
The problem is that for me there is a difference between a password and a key when it comes to encryption. A key is directly the parameter used for encryption and hence necessarily of a specific size - "the keylength" - i.e. 128/256/512 bits depending on the cipher and keylength desired. A password on the other hand is a to my understanding a human readable string entered via the keyboard which may in difference be of any length and which is before being used to encrypt first converted into a key.
hence schematic difference:
password => key => encryption
key => encryption
Unfortunatelly in the PHP openssl_encrypt documentation I cannot find any information how to use a key. The only thing suggested is a parameter "password".
Can anybody give me a glue how a key can be used?
Surely I donot want to enter the key as the password parameter as I want a specific key to be used in encryption. I do not want this key to be simply missunderstood as a parameter and serve for another key being calculated from my "key mistaken as password".
Additionally the mistery continues looking at the documention regarding the initialization vector parameter in the same openssl_encrypt function. It simply states:
iv A non-NULL Initialization Vector.
and that iv should be a string. Given that the iv is normaly a binary data of a certain length and for instance a string terminating \0 (hex 0x00) can be occuring inside I am puzzled what format is desired.
In essence I feel very much left alone with the PHP documentation which also states
WARNING This function is currently not documented; only its argument
list is available.
Update
I did some testing, and "trying around" to help me figure out what the password parameter is.
using this code:
$pass="0123456789abcdefghijklmnob";
$iv="0123456789abcdef";
echo "using $pass results:\n";
echo openssl_encrypt("test secret string", 'aes-128-cbc', $pass,NULL,$iv);
I get this result:
using 0123456789abcdefghijklmnob results:
XjEeaLucY38Y6XEUceYMYKTebR4kOp3s727ipMl5KNc=
Then changing the length of the "password" parameter:
$pass="0123456789abcdefg"; //hijklmnob";
$iv="0123456789abcdef";
echo "using $pass results:\n";
echo openssl_encrypt("test secret string", 'aes-128-cbc', $pass,NULL,$iv);
I get still the same encrypted code:
using 0123456789abcdefg results:
XjEeaLucY38Y6XEUceYMYKTebR4kOp3s727ipMl5KNc
It seems by way of testing yet not by way of being informed by the documentation that
the password is indeed only considered up to the first 16 bytes which seem to be the 128 bit that would be the key.
It frightens me that in such a sensitive function (used for encryption) the documentation is bad and excessive input of one poorly documented parameter is not even warned about. Also I am quite convinced that password should rather be named key as it seems those 16 bytes do directly represent the key.
The $password parameter is indeed the key. The key size depends on the AES mode you're using (as you know).
As you noted in your update, for AES-128, only the first 16 characters/bytes count for the key. For AES-256, it would be the first 32 characters.
When one uses the openssl_encrypt() and openssl_decrypt() functions, one can simply pass a 32 character human-readable password for the $password/key parameter.
For example, my input for the password parameter might be $password = "This is 32 characters long......";
Most people don't like having to write up a plaintext password that is a fixed length, so they might compute a hash and truncate it to the correct length. They would use this hash as their encryption key/password.
For example, I could compute an MD5 hash of a password/phrase of any length that I would like and then use that as my AES password/key:
$plaintextPass = 'This is my password. This password is not exactly 32 bytes, but that is okay because I am hashing this.';
$password = hash('md5', $plaintextPass); /* the encryption key */
With that in place, no matter what plaintext password I use, I can have a valid 32 character/byte string as my encryption key/password. This does reduce the entropy of the encryption key/password, though, because a normal 32 character string has a larger key space than an MD5 hash output (256 possibilities per byte vs. 16 possibilities per byte); however, 16^32 is still certainly out of the range of brute force.
Off topic: In some of my personal programs, I have been working on using 32 randomly generated bytes with values between 0-255. This would make the entropy of the encryption key 256^32 which is infeasible to bruteforce. This will also be resistant to dictionary attacks because the 32-bytes are randomly generated using a cryptographically secure pseudo-random number generator (CSPRNG).
So, to sum this all up, yes, the $password parameter is indeed the key used for encryption. I agree with you that is should be written as $key in the documentation, but oh well. The password/key which you select for encryption can be humanly readable as long as it satisfies the length requirements of the hashing key. To satisfy these length requirements, one can hash a human-readable/plaintext password and use the hash of that password as the key ($password parameter), but the human-readable password should still be long and unique.
The documentation is not very clear, however this comment on the openssl_encrypt() PHP doc page really helped me when I was trying to understand the function. The comment provides an example as well as useful information. To direct quote the author of the comment:
Because the password parameter documented here is not the password.
...
It is the key!
[Comment minorly edited]
Related
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.
i have my old code back from 2011 which calculate hash
private static $key = 'G#W351T35.cz#€2011GAMESITES';
/**
* Computes salted password hash.
* #param string
* #return string
*/
public static function calculateHash($password)
{
$text = $password;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$crypttext = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, self::$key, $text, MCRYPT_MODE_ECB, $iv);
return base64_encode($crypttext);
}
When i try to run it now I get an error:
Warning: mcrypt_encrypt(): Key of size 29 not supported by this
algorithm. Only keys of sizes 16, 24 or 32 supported in ..\Hash.php
on line 27
I know it takes a long time from 2011 and there can be better ways to do it now, but I need to make it work from previous version for some historical issue. What i am doing wrong?
I cant even see what size 29 does it mean.
Or alternativly is there a way how to break a hash if I still got a function?
with this i can potencialy start using new way of calculating hash.
Thanks for any advise
If you consult the changelog in the documentation for mcrypt_encrypt, you should see that since PHP 5.6.0...
Invalid key and iv sizes are no longer accepted. mcrypt_encrypt() will now throw a warning and return FALSE if the inputs are invalid. Previously keys and IVs were padded with '\0' bytes to the next valid size.
The solution is therefore to replace your key by one that is padded with null characters to 32 bytes.
Unfortunately, there is a non-ASCII character in there (the euro sign), so there are multiple possibilities how that is supposed to be encoded. It's probably best to manually encode this character. In Unicode, the euro sign has codepoint U+20AC, which would translate to '\xE2\x82\xAC' (which explains why mcrypt counts 29 bytes instead of 27), making your new key
private static $key = 'G#W351T35.cz#\xE2\x82\xAC2011GAMESITES\0\0\0';
Note that we have to assume some character encoding for your code; I have assumed UTF-8. It's unlikely but possible that, in 2011, it was supposed to be encoded in another character encoding (e.g. ISO-8859-1), which results in a very different encoding for the euro sign.
$keyis the key and must be a supported size of 16, 24 or 32 bytes in length. You are passing a length of 29 bytes, you need to use a key of appropriate size.
The code is not calculating a hash, it is encrypting $text.
It is using ECB mode which is not considered secure. Note that ECB mode does not take an iv $iv so there is no point in creating one. CBC mode is better and does use an iv.
If you really want to create a hash use a hash function such as SHA-256. If you need a "keyed" or salted hash use a HMAC.
Even "way back to 2011" encryption was not used to create hashes, there really isn't anything new since then.
Iterate over an HMAC with a random salt for about a 100ms duration (the salt needs to be saved with the hash). Use functions such as password_hash, PBKDF2, Bcrypt and similar functions. The point is to make the attacker spend a lot of time finding passwords by brute force.
See OWASP (Open Web Application Security Project) Password Storage Cheat Sheet.
See How to securely hash passwords, The Theory on Security Stackexchange.
Ok so here's the sample code from the page http://phpseclib.sourceforge.net/crypt/examples.html
<?php
include('Crypt/AES.php');
include('Crypt/Random.php');
$cipher = new Crypt_AES(); // could use CRYPT_AES_MODE_CBC
// keys are null-padded to the closest valid size
// longer than the longest key and it's truncated
//$cipher->setKeyLength(128);
$cipher->setKey('abcdefghijklmnop');
// the IV defaults to all-NULLs if not explicitly defined
$cipher->setIV(crypt_random_string($cipher->getBlockLength() >> 3));
$size = 10 * 1024;
$plaintext = str_repeat('a', $size);
echo $cipher->decrypt($cipher->encrypt($plaintext));
?>
Before anything, why is the line //$cipher->setKeyLength(128); is being commented out?
And if I want to set my key to '1234568790', is there anything I should do? Because it's much shorter than they length of the key in the example above (abcdefghijklmnop).
And finally if the plaintext that I want to encrypt is short, something like "my name is james bond", is there anything extra that I should do? Because from the code above, it seems the plaintext's length should be 10 x 1024. (Why is that?)
Before anything, why is the line //$cipher->setKeyLength(128); is being commented out?
Say you pass a 17 byte key to phpseclib via setKey. What do you suppose ought to happen? In the 1.0 and 2.0 versions if setKeyLength isn't called then the key is null-padded to 24 bytes (eg. 192-bits). But if setKeyLength is called it'll be truncated to 16 bytes. In the master branch (which, as I understand it, will eventually become the 3.0 branch), an exception is thrown if the key length isn't valid. In that version it may still be desirable to call setKeyLength if, for example, if you want to set the key length as being 128-bits in one part of the code and then actually set the key in another part of the code. ie. you could do the length checking or phpseclib could do the length checking.
And if I want to set my key to '1234568790', is there anything I should do? Because it's much shorter than they length of the key in the example above (abcdefghijklmnop).
1234568790 isn't technically long enough to be a valid key. It's 80 bytes long. The smallest key AES supports is 128-bits. Maybe you should consider doing something like setPassword('1234568790') instead of setKey('1234568790'). setPassword will use a key-derivation function to generate a key from the password.
In phpseclib 1.0 and 2.0 setKey will, none-the-less, accept that as a key. It'll just null pad the key. The master branch otoh will throw an exception since the key isn't actually long enough.
And finally if the plaintext that I want to encrypt is short, something like "my name is james bond", is there anything extra that I should do? Because from the code above, it seems the plaintext's length should be 10 x 1024. (Why is that?)
10 * 1024 is just an example. phpseclib, by default, will use PKCS7 padding to make strings long enough. eg. your string is 21 bytes long (if I counted correctly). So if you call $cipher->encrypt('my name is james bond') then chr(11) will be appending to the plaintext 11 times, by phpseclib and then the string will be encrypted. This is because block algorithms need to have the the plaintext be a multiple of the block length. So the ciphertext will be 32 bytes long. When you then call $cipher->decrypt('...') you'll get the orig 21-byte string back. I could elaborate further on PKCS7 padding, but I think I've answered your immediate question.
The function in php string hash ( string $algo , string $data [, bool $raw_output = false ] ) where algo=Name of selected hashing algorithm (i.e. "md5", "sha256", "haval160,4", etc..), data=Message to be hashed., raw_output=When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits. so if I have this example
<?php
echo hash('ripemd128', 'The quick brown fox jumped over the lazy dog.');
?>
The above example output (which looks completely random): 51d43720fd516108ef5ed20e9031bb865ede861e
So I'm wondering where such functions is used and why? also Is there a way or a function to revert the output to the original string back again?
So I'm wondering where such functions is used and why?
They're used in digital signature algorithms, digital fingerprinting algorithms, content-addressable storage, constructing tamper-resistant data structures that can be traversed rapidly and securely, and for fast lookups that also resist complexity attacks.
also Is there a way or a function to revert the output to the original string back again?
No. In many cases, having this ability would defeat the point of the hash. Also, it is trivial to prove that this is, as stated, impossible, using a counting argument.
It is use for digital signatures like hashing the concatenated string with the secret key against the other to check if both hash string is correct, its like a key in order for you to gain access to do something.
There's no way to decrypt it because that is how it's made, it is a 1-way hash method.
if you want a method that encrypts and decrypts the string use mcrypt_encrypt and mcrypt_decrypt
this functions are used to compute a kind of "fingerprint" for some data. in your example this will be your string. same algorithm will produce the same hash for the same input data. if you change input data the computed hash will be different.
a popular usage is storing passwords. so you don't store passwords in clear text but hashed values.
for the second part of your question: hash algorithms are "one-way" only (should be ;)). so you can not restore the input data from hashed value.
I need to get the basics of this function. The php.net documentation states, for the blowfish algorithm, that:
Blowfish hashing with a salt as follows: "$2a$", a two digit cost parameter, "$", and 22 base 64 digits from the alphabet "./0-9A-Za-z". Using characters outside of this range in the salt will cause crypt() to return a zero-length string
So this, by definition, should not work:
echo crypt('rasmuslerdorf', '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringforsalt$');
However, it spits out:
$2a$07$usesomadasdsadsadsadaeMTUHlZEItvtV00u0.kb7qhDlC0Kou9e
Where it seems that crypt() has cut the salt itself to a length of 22. Could somebody please explain this?
Another aspect of this function I can't get my head around is when they use crypt() to compare passwords. http://php.net/manual/en/function.crypt.php (look at ex. #1). Does this mean that if I use the same salt for all encrypting all my passwords, I have to crypt it first? ie:
$salt = "usesomadasdsadsadsadae";
$salt_crypt = crypt($salt);
if (crypt($user_input, $salt) == $password) {
// FAIL WONT WORK
}
if (crypt($user_input, $salt_crypt) == $password) {
// I HAVE TO DO THIS?
}
Thanks for your time
Following code example may answer your questions.
To generate hashed password using Blowfish, you first need to generate a salt, which starts with $2a$ followed by iteration count and 22 characters of Base64 string.
$salt = '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringfors';
$digest = crypt('rasmuslerdorf', $salt);
Store the whole $digest in database, it has both the salt and digest.
When comparing password, just do this,
if (crypt($user_input, $digest) == $digest)
You are reusing the digest as salt. crypt knows how long is the salt from the algorithm identifier.
New salt for every password
$password = 'p#ssw0rd';
$salt = uniqid('', true);
$algo = '6'; // CRYPT_SHA512
$rounds = '5042';
$cryptSalt = '$'.$algo.'$rounds='.$rounds.'$'.$salt;
$hashedPassword = crypt($password, $cryptSalt);
// Store complete $hashedPassword in DB
echo "<hr>$password<hr>$algo<hr>$rounds<hr>$cryptSalt<hr>$hashedPassword";
Authentication
if (crypt($passwordFromPost, $hashedPasswordInDb) == $hashedPasswordInDb) {
// Authenticated
Quoting from the manual
CRYPT_BLOWFISH - Blowfish hashing with
a salt as follows: "$2a$", a two digit
cost parameter, "$", and 22 base 64
digits from the alphabet
Note: 22 base 64 digits
BCrypt uses 128 bits for salt, so 22 bytes Base64, with only two bits of the last byte being used.
The hash is computed using the salt and the password. When you pass the crypted password, the algorithm reads the strength, the salt (ignoring everything beyond it), and the password you gave, and computes the hash, appending it. If you have PostgreSQL and pg_crypto handy, SELECT gen_salt('bf'); will show you what of $salt is being read.
Here's a code sample for salt generation, from my .NET implementation's test-vector-gen.php, alternatively:
$salt = sprintf('$2a$%02d$%s', [strength goes here],
strtr(str_replace(
'=', '', base64_encode(openssl_random_pseudo_bytes(16))
),
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
'./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'));
There is no reason to use the same salt for all of your passwords. The salt is part of the output anyway so you gain nothing in convenience... though I grant PHP ought to have a built-in gen_salt function.
First question:
So this, by definition, should not work:
echo crypt('rasmuslerdorf', '$2a$07$usesomadasdsadsadsadasdasdasdsadesillystringforsalt$');
Where it seems that crypt() has cut
the salt itself to a length of 22.
Could somebody please explain this?
There isn't a problem with having too many characters... the phrase Using characters outside of this range in the salt will cause crypt() to return a zero-length string referse to outside the range of base 64 not the range of 22 characters. Try putting an illegal character in the salt string, and you should find that you get an empty output (or if you put < 22 characters in, resulting in illegal empty bytes).
Second question:
You pass in the encrypted stored password as salt because the salt string always appears (by design) in the encrypted string, and this way you ensure that you have the same salt for both encryption of stored and user-entered password.
This question is in relation to my response to ZZ Coder's answer. Basically my question is regarding storing the crypt() result in the database. Am I supposed to store the entire output in the database, so that my database looks like this:
--------------------------------------------------------------------------------
| ID | Username | Password |
--------------------------------------------------------------------------------
| 32 | testuser | $2a$07$usesomadasdsadsadsadaeMTUHlZEItvtV00u0.kb7qhDlC0Kou9e |
--------------------------------------------------------------------------------
If yes, then doesn't this kind of defy the purpose of using a salt in the first place? If someone gains access to the db, they can clearly see the salt used for the encryption?
Bonus question: Is it secure to use the same salt for every password?