Encryption in PHP. Encrypt, decrypt data using a key in PHP Encryption and authentication keys

  • Translation
  • Tutorial

From the translator: in the process of programming, I never forget that I am dangerously incompetent in cryptography, and I advise everyone to proceed from this thesis (well, maybe except for you and that cool guy over there). However, one way or another, in the process of work problems arise related to data protection, and they must be solved. Therefore, I bring to your attention a translation of an article by Finnish developer Timo H, which I found quite interesting and useful.

This is a quick guide on how to avoid common pitfalls with symmetric encryption in PHP.

We will consider the case when the data is processed on the server side (in particular, encryption occurs on the server, and the data can be received, for example, from the client in the form of clear text, password, etc.), which is a typical case for PHP applications .

The information in this guide should not be used to create encrypted network connections that have more complex requirements. For such cases, you need to use spied or TLS.

Naturally, the recommendations given here are not the “only possible way” to organize encryption in PHP. The purpose of this guide is to try to leave less room for mistakes and difficult, ambiguous decisions.

Encryption functions in PHP

Use Mcrypt or OpenSSL extensions.

Encryption algorithm and its mode of operation, one-time code (initialization vector)

Use AES-256 in CTR mode with a random one-time code ( approx. translation: nonce). AES is a standard, so you can use the functions of any of the extensions - Mcrypt or OpenSSL.

Always generate a new one-time code. In this case, you must use a cryptographically secure source of random numbers. Read a little more about random number generation below. The one-time code is not a secret, and can be concatenated with ciphertext for transmission and subsequent decryption.

The one-time code must be 128 bits (16 bytes) long, just a string of bytes without any encoding.

In the Mcrypt extension, AES is known as Rijndael-128 ( approx. transl.: despite the fact that we are talking about AES-256, this is not an error. AES-256 != Rijndael-256). In OpenSSL, respectively, AES-256-CTR.

Mcrypt usage example:
OpenSSL example:
Verify that encryption is working correctly using test vectors ( approx. translation: for AES-256-CTR see paragraph F.5.5 on page 57).

For CTR mode, there are some restrictions on the total volume of encrypted data. You may not encounter this in practice, but keep in mind that you should not encrypt more than 2^64 bytes of data with one key, regardless of whether it is one long message or many short ones.

The CTR mode remains stable only if you do not use the same one-time code with the same key. For this reason, it is important to generate one-time codes using a cryptographically strong source of randomness. Additionally, this means that you should not encrypt more than 2^64 messages with a single key. Since the length of the one-time code is 128 bits, the limit on the number of messages (and their corresponding one-time codes) of 2^128/2 is important due to the Birthday Paradox ( approx. translation:).

And remember that encryption won't hide the fact how much data you're sending. As an example of an extreme case, if you encrypt messages containing only "yes" or "no", obviously encryption will not hide that information.

Data authentication

Always check the authenticity and integrity of the data.
To do this, use MAC after encryption. Those. First the data is encrypted, and then HMAC-SHA-256 is taken from the resulting ciphertext, including the ciphertext itself and the one-time code.

When decrypting, first check the HMAC using a comparison algorithm that is resistant to timing attacks. Do not directly compare $user_submitted_mac and $calculated_mac using the == or === comparison operators. It's even better to use "HMAC double check".

If the HMAC check is successful, decryption can be performed safely. If HMAC is not suitable, shut down immediately.

Encryption and authentication keys

Ideally, use keys derived from a cryptographically strong source of randomness. AES-256 requires 32 bytes of random data (a "raw" string - a sequence of bits without using any encoding).

If the application is running under PHP version below 5.5, which does not have a built-in implementation of PBKDF2, then you will have to use your own implementation in PHP, an example of which can be found here: https://defuse.ca/php-pbkdf2.htm. Be aware that relying on your own implementation may not resolve the key properly as the built-in hash_pbkdf2() function does.

Do not use the same key for encryption and authentication. As stated above, 32 bytes are required for the encryption key and 32 bytes for the authentication key (HMAC). With PBKDF2 you can take 64 bytes from the password and use, say, the first 32 bytes as the encryption key, and the remaining 32 bytes for the authentication key.

If your passwords are stored in a file, for example, as a HEX string, do not re-encode them before feeding them to the encryption functions. Instead, use PBKDF2 to convert HEX-encoded keys directly into a high-quality encryption or authentication key. Or use SHA-256 with no additional encoding output (just a 32 byte string) to hash passwords. Using regular password hashing provides enough entropy. More details are provided in the following paragraphs.

Key stretch

First, you should avoid using low entropy keys. But still, if you need to use, for example, user passwords, then you must definitely use PBKDF2 with a large number of iterations in order to maximize key security.

One of the parameters of PBKDF2 is the number of hashing iterations. And the higher it is, the greater the security of the key you can count on. If your code is running on a 64-bit platform, use SHA-512 as the hashing algorithm for PBKDF2. For a 32-bit platform, use SHA-256.

However, it is not possible to use a relatively high number of iterations in online applications due to the risk of a DoS attack. Therefore, the key quality will not be as high as in offline applications, which can afford a large number of iterations without such risk. As a rule, for online applications, such a number of hashing iterations is selected so that PBKDF2 takes no more than 100 ms.

In case you can use high entropy passwords, it is not necessary to stretch it as you would for low entropy passwords. For example, if you create an "encryption_master_key" and an "auth_master_key" using /dev/urandom, then there is no need for PBKDF2 at all. Just be sure to use the keys as sequences of bits, without any encoding.

In addition, with PBKDF2 it is not difficult to obtain both encryption and authentication keys from a single master password (just use a small number of iterations or even one). This is useful if you only have one "master password" used for both encryption and authentication.

Key storage and management

The best thing is to use a separate dedicated key storage device (HSM).

If this is not possible, then to complicate the attack, one can use encryption of the key file or configuration file (which stores the actual encryption/authentication keys) using a key stored in a separate location (outside the home directory or site root). For example, you can use an Apache environment variable in httpd.conf to store the key needed to decrypt the actual keys file:
SetEnv keyfile_key crypto_strong_high_entropy_key # You can access this variable in PHP using $_SERVER["keyfile_key"] # Rest of the config
Now, if files at the root of the site and below, including files with keys, are compromised (for example, if a backup is leaked), the encrypted data will remain safe because the key stored in the environment variable has not been compromised. It is important to remember that httpd.conf files should be backed up separately, and not to compromise the keyfile_key variable through, for example, the output of phpinfo().

If you use a file instead of a configuration parameter, it is possible to organize key rotation. In the worst case scenario, if an adversary has obtained your encryption and authentication keys without being noticed, then rotating the keys at some intervals can limit their access (assuming they cannot obtain new keys). This technique will help reduce damage because the enemy will not be able to use compromised keys indefinitely.

Data compression

In general, you should not compress the source text before encrypting it. This can give the enemy an additional tool for analysis.

For example, if you store session data in encrypted cookies, some of which is user-supplied, and some of which represents secret information, an adversary can learn additional information about the secret by sending, as an ordinary user, some specially crafted data and measuring how the length of the resulting ciphertexts changes.

Text is compressed more efficiently if there are repeating areas. By manipulating user data, you can select it so that it partially coincides with secret data. The greater the match, the smaller the ciphertext will be in the output. This type of attack is called CRIME.

If you don't have a hard need to compress the data, don't compress it.

Server environment

As a general rule, you should not host security-sensitive applications on a shared server. For example, on shared hosting, where an adversary can access a virtual machine on the same physical server as you.

There are various reasons that make shared servers a dubious place to host security-critical applications. For example, attacks between virtual servers have recently been demonstrated: eprint.iacr.org/2014/248.pdf. This is a good reminder that offensive techniques do not degrade, but rather are honed and improved over time. Such pitfalls must always be taken into account.

Expert consultation

Last but not least, consult an expert to review your security code.

(PHP 4, PHP 5, PHP 7)

crypt — One-way string hashing

Warning

This function is not (yet) binary safe!

Description

crypt (string $str [, string $salt]): string

crypt() will return a hashed string using the standard Unix DES -based algorithm or alternative algorithms that may be available on the system.

The salt parameter is optional. However, crypt() creates a weak hash without the salt . PHP 5.6 or later raise an E_NOTICE error without it. Make sure to specify a strong enough salt for better security.

password_hash() uses a strong hash, generates a strong salt, and applies proper rounds automatically. password_hash() is a simple crypt() wrapper and compatible with existing password hashes. Use of password_hash() is encouraged.

Some operating systems support more than one type of hash. In fact, sometimes the standard DES-based algorithm is replaced by an MD5-based algorithm. The hash type is triggered by the salt argument. Prior to 5.3, PHP would determine the available algorithms at install-time based on the system"s crypt(). If no salt is provided, PHP will auto-generate either a standard two character (DES) salt, or a twelve character ( MD5), depending on the availability of MD5 crypt(). PHP sets a constant named CRYPT_SALT_LENGTH which indicates the longest valid salt allowed by the available hashes.

The standard DES-based crypt() returns the salt as the first two characters of the output. It also only uses the first eight characters of str , so longer strings that start with the same eight characters will generate the same result (when the same salt is used).

On systems where the crypt() function supports multiple hash types, the following constants are set to 0 or 1 depending on whether the given type is available:

  • CRYPT_STD_DES- Standard DES-based hash with a two character salt from the alphabet "./0-9A-Za-z". Using invalid characters in the salt will cause crypt() to fail.
  • CRYPT_EXT_DES- Extended DES-based hash. The "salt" is a 9-character string consisting of an underscore followed by 4 bytes of iteration count and 4 bytes of salt. These are encoded as printable characters, 6 bits per character, least significant character first. The values ​​0 to 63 are encoded as "./0-9A-Za-z". Using invalid characters in the salt will cause crypt() to fail.
  • CRYPT_MD5- MD5 hashing with a twelve character salt starting with $1$
  • CRYPT_BLOWFISH- Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters 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. The two digit cost parameter is the base-2 logarithm of the iteration count for the underlying Blowfish-based hashing algorithmeter and must be in range 04-31, values ​​outside this range will cause crypt() to fail. Versions of PHP before 5.3.7 only support "$2a$" as the salt prefix: PHP 5.3.7 introduced the new prefixes to fix a security weakness in the Blowfish implementation. Please refer to for full details of the security fix, but to summarize, developers targeting only PHP 5.3.7 and later should use "$2y$" in preference to "$2a$".
  • CRYPT_SHA256- SHA-256 hash with a sixteen character salt prefixed with $5$. If the salt string starts with "rounds=
  • CRYPT_SHA512- SHA-512 hash with a sixteen character salt prefixed with $6$. If the salt string starts with "rounds= $", the numeric value of N is used to indicate how many times the hashing loop should be executed, much like the cost parameter on Blowfish. The default number of rounds is 5000, there is a minimum of 1000 and a maximum of 999,999,999. Any selection of N outside this range will be truncated to the nearest limit.

As of PHP 5.3.0, PHP contains its own implementation and will use that if the system lacks support for one or more of the algorithms.

Parameters

The string to be hashed.

Caution

Using the CRYPT_BLOWFISH algorithm, will result in the str parameter being truncated to a maximum length of 72 characters.

An optional salt string to base the hashing on. If not provided, the behavior is defined by the algorithm implementation and can lead to unexpected results.

Return Values

Returns the hashed string or a string that is shorter than 13 characters and is guaranteed to differ from the salt on failure.

Warning

When validating passwords, a string comparison function that isn't vulnerable to timing attacks should be used to compare the output of crypt() to the previously known hash. PHP 5.6 onwards provides hash_equals() for this purpose.

Changelog

Version Description
5.6.5 When the failure string "*0" is given as the salt , "*1" will now be returned for consistency with other crypt implementations. Prior to this version, PHP 5.6 would incorrectly return a DES hash.
5.6.0 Raise E_NOTICE security warning if salt is omitted.
5.5.21 When the failure string "*0" is given as the salt , "*1" will now be returned for consistency with other crypt implementations. Prior to this version, PHP 5.5 (and earlier branches) would incorrectly return a DES hash.
5.3.7 Added $2x$ and $2y$ Blowfish modes to deal with potential high-bit attacks.
5.3.2 Added SHA-256 and SHA-512 crypt based on Ulrich Drepper"s » implementation.
5.3.2 Fixed Blowfish behavior on invalid rounds to return "failure" string ("*0" or "*1"), instead of falling back to DES.
5.3.0 PHP now contains its own implementation for the MD5 crypt, Standard DES, Extended DES and the Blowfish algorithms and will use that if the system lacks support for one or more of the algorithms.

Examples

Example #1 crypt() examples

$hashed_password = crypt("mypassword"); // let the salt be automatically generated

/* You should pass the entire results of crypt() as the salt for comparing a
password, to avoid problems when different hashing algorithms are used. (As
it says above, standard DES-based password hashing uses a 2-character salt,
but MD5-based hashing uses 12.) */
if (hash_equals ($hashed_password , crypt ($user_input , $hashed_password ))) (
echo "Password verified!" ;
}
?>

Example #2 Using crypt() with htpasswd

// Set the password
$password = "mypassword" ;

// Get the hash, letting the salt be automatically generated
$hash = crypt($password);
?>

Example #3 Using crypt() with different hash types

/* These salts are examples only, and should not be used verbatim in your code.
You should generate a distinct, correctly-formatted salt for each password.
*/
if (CRYPT_STD_DES == 1 ) (
echo "Standard DES: " . crypt ("rasmuslerdorf" , "rl" ) . "\n" ;
}

if (CRYPT_EXT_DES == 1 ) (
echo "Extended DES: " . crypt ("rasmuslerdorf" , "_J9..rasm" ) . "\n" ;
}

if (CRYPT_MD5 == 1 ) (
echo "MD5: " . crypt ("rasmuslerdorf" , "$1$rasmusle$" ) . "\n" ;
}

if (CRYPT_BLOWFISH == 1 ) (
echo "Blowfish: " . crypt("rasmuslerdorf" , "$2a$07$usesomesillystringforsalt$") . "\n" ;
}

if (CRYPT_SHA256 == 1 ) (
echo "SHA-256: " . crypt("rasmuslerdorf" , "$5$rounds=5000$usesomesillystringforsalt$") . "\n" ;
}

if (CRYPT_SHA512 == 1 ) (
echo "SHA-512: " . crypt("rasmuslerdorf" , "$6$rounds=5000$usesomesillystringforsalt$") . "\n" ;
}
?>

Any information can be encrypted or decrypted, including using PHP. This language has many data encryption capabilities, from simple to complex.

Let's look at the basic encryption methods

base64- allows you to encrypt and decrypt data using the MIME base64 algorithm. It doesn't use keys and is often used to hide links in PHP.

Examples:
//encrypt the text
$text = "Link";
echo base64_encode($text); //Produces: PGEgaHJlZj0iIyI+0KHRgdGL0LvQutCwPC9hPg==
//decryption
echo base64_decode("PGEgaHJlZj0iIyI+0KHRgdGL0LvQutCwPC9hPg==");
?>

As you can see, we first used the base64_encode operation and got the cipher: PGEgaHJlZj0iIyI+0KHRgdGL0LvQutCwPC9hPg==, and then substituted it into base64_decode and got the link back.

md5- allows you to hash data unilaterally. That is, unlike base64, you will no longer be able to decrypt them back. Often md5 is used to store passwords in a database, but recently the encrypted md5 combination has become easy to find in decryption tables, kindly provided by many sites and algorithms. Therefore, to store md5 passwords, it is better to replace algorithms with Blowfish.

Example:

//encrypt the text
echo md5("combination");
?>

Key encryption

And the last example of encryption/decryption that I wanted to talk about uses a key (as a password). That is, you pass a unique key to the encryption function, and the code is encrypted along with it. To decrypt, you must provide the function with the encrypted code and a key that only you know. An example of using functions at the very bottom of the code.

function __encode($text, $key) (



$enc_text=base64_encode(mcrypt_generic($td,$iv.$text));
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $enc_text; ) )
function strToHex($string) (
$hex="";
for ($i=0; $i< strlen($string); $i++) { $hex .= dechex(ord($string[$i])); }
return $hex; )
function __decode($text, $key) (
$td = mcrypt_module_open("tripledes", "", "cfb", "");
$iv_size = mcrypt_enc_get_iv_size($td);
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
if (mcrypt_generic_init ($td, $key, $iv) != -1) (
$decode_text = substr(mdecrypt_generic($td, base64_decode($text)),$iv_size);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $decode_text; ) )
function hexToStr($hex) (
$string="";
for ($i=0; $i< strlen($hex)-1; $i+=2) { $string .= chr(hexdec($hex[$i].$hex[$i+1])); }
return $string; )

$str = "Buns that need to be encrypted!
By key";
$code = strToHex(__encode($str, "My#key-do-36-simvolov"));
echo "Encrypted code: ".$code."
";

$str = __decode(hexToStr($code), "My#key-do-36-simvolov");
echo "Decrypted code: ".$str."
";
?>

You can encrypt html content. The key length must be no more than 36 characters.

This method can be used to encrypt some data and place it in a txt file or database, and receive it using decryption with a key.

Of course, any code can be decrypted/hacked and this is no exception, so use strong encryption methods.

One of the basic truths of cryptography is that you should not invent anything in this area unless you are a professional. This is partly true, because all the best has long been invented, suffered and used for decades in the field of information technology. The other side of the truth is that the development of a certain field of knowledge occurs only with a constant influx of fresh ideas and original solutions in it.

For obvious reasons, we will not take aim at the giants of industrial cryptography like AES, but will plunge, so to speak, into our own cryptographic research with blackjack and joy.

Partly because it’s interesting, partly because by modeling something of your own and comparing it with recognized standards, you clearly see the contrast, effective solutions and outright omissions, and you understand what you can strive for to improve efficiency.

But enough water already.

Let's say our web application is written in PHP, needs reversible encryption, and we believe that we can write our own cipher system.

So, let’s write our own reversible encryption system with private and public keys, one that will have the following features of a more or less secure cryptographic algorithm:

  1. The presence of noise symbols in the final cipher.
  2. Information in each Sender-Destination channel will be encrypted using a private key, and the matching function will be unique for each key.
  3. Each message will receive a digest code - a unique code that is a function of the private key and the original message. This is required in order to achieve uniqueness of the "source symbol" matching function<=>encoded symbol” not only for the “Sender-Receiver” channel, but also for each individual message.

    Thus, even if we imagine that the correspondence of encoded and original symbols for a particular message has become known through the use of cryptographic analysis, for example, frequency analysis, this does not give any preferences when studying another message.

  4. To complicate the frequency analysis, we will encode each initial message symbol with two cipher symbols.
So what happened.

In fact, you can see the final result

The SymCoder class includes encryption and decryption methods.

Encryption is carried out by the code() method, which takes the original message as input.

Here, a message from the generated correspondence table in tab_coded creates an encrypted message, diluted along the edges and inside with noise symbols.

By the way, noise symbols are unique for each sender-destination channel, since they are generated using the channel key, but are not unique for messages. The symbols used for encryption in code_symbols are some punctuation marks and symbols like %, @, etc.

For each encoded symbol, there are two symbols from code_symbols, for obvious reasons that there are several times fewer of them than the encoded symbols.

The create_tab_coded correspondence table is built using a translation of the message key hash into an array with the number of elements equal to the number of elements in the code symbol array. The starting position for traversing two-character codes is also always different and is associated with the channel key. This makes it possible to be sure that the algorithm for traversing encoded symbols and matching code symbols to them will always (or is guaranteed to often) be different.

For example, the “hello world” message, when encoded, looks like this:

Digest-a00bf11d-&?==&!&?.@.@=!=-.?&1.#&?=:.:.1%!&-%@&@%~&1^#=?%% .!%+.?.~=?..&?%&&:%~.#%@&1&1.#=?.#.?.!&1==&=.-=!

And here is the same message, encoded again:

Digest-a00bf11d-=:.?=:&!.?.1&-=:=?.?.=.?.!&=%!=-%@=!%~.=^#.1%%. !%+=:.~.@..==%&&1%~.1%@=?.@.!&=.!&@=:&1.==:=!.1&:

It can be seen that the digest of the same message is the same, but the cipher becomes different - noise symbols are added in an arbitrary match and in an arbitrary order for each new encryption.

Messages have redundancy, which decreases as the volume of the message increases, up to 10% noise (for the shortest messages, noise reaches 90% or higher percent), the minimum length of an encrypted message is 116 characters. One of the disadvantages of this encryption method is that encoded messages are at least doubled.

Decoding consists of reverse translation of the form “code symbol” - the original symbol with noise cut out from the message. What could be the key? Basically, any string that is unique for each destination-receiver pair.

For example, if you are creating a messenger with message encryption, then the simplest version of the private key could be md5($user_id_1. $salt. $user_id_2), then the key will be unique for each message channel.

Let's say you need to exchange data between two servers. To protect data from eavesdropping traffic, the data is encrypted. Well, for example, transferring actions within a botnet. This is what is essentially not encryption, but is called encoding, and well-known functions are used to decode such code.

As another example of pseudo-encryption, I will give an example of “encryption” of passwords in the database of one CMS - there passwords are not encrypted in md5() or , but simply encoded via base64. Those. When the database is leaked, it will not be difficult for a hacker to decrypt all passwords using the built-in PHP function base64_decode().

We need to transmit data without worrying that someone will be able to intercept the text and decrypt it. PHP has a popular data encryption package called Mcrypt, which provides two-way encryption (that is, the actual encryption and decryption of data).

Mcrypt version 2.4.7 supports the following symmetric encryption algorithms: Blowfish, RC2, Safer-sk64 xtea, Cast-256, RC4, Safer-sk128, DES, RC4-iv, Serpent, Enigma, Rijndael-128, Threeway, Rijndael-192, TripleDES, LOKI97, Rijndael-256, Twofish, Panama, Saferplus, etc. More details about each algorithm are written on Wikipedia.

Since symmetric encryption is used, the key must be known to both parties and kept secret.

Example of encryption and decryption of a string

mcrypt_module_open("des", "", "ecb", "")
This function opens the algorithm module and the mode used. For this example, the DES algorithm is in ECB mode.

$key = substr($key, 0, mcrypt_enc_get_key_size($td));
The maximum key size must be obtained by calling the mcrypt_enc_get_key_size() function, and any value less than this will be correct.

$s = mcrypt_generic($td, $source);
When encrypting, the data is padded with zero bytes to ensure that the data is n*blocksize long. The block size blocksize is determined by the algorithm (for DES the block size is 64 bits). Therefore, when decrypting, “\0” may appear at the end of the line, which is removed by the trim() function