📜 ⬆️ ⬇️

Anubis PHP Encryption Algorithm

Anubis
Continuing the original week of cryptography on Habré, I decided to share my implementation of the Anubis encryption algorithm in PHP. Anubis is a block encryption algorithm, which is, in essence, a modification of the Rijndael algorithm adopted as the encryption standard in the United States. The authors of the cipher are Vincent Ramen - one of the developers of Rijndael and Paulo S. L. M. Barreto - a well-known cryptographer, one of the developers of the hash function of Whirlpool.

Why did I choose Anubis? This is not a proprietary algorithm available for free use. Anubis meets modern security requirements - the block size is 128 bits, as in AES, and the key length can vary from 128 to 320 bits. In addition, since publication in 2000, there are no weak points in the Anubis algorithm. He did not get into the NESSIE project , but only because of his similarity to Rijndael.

Below there is a link to the official page of the algorithm, where those interested can find its full description, as well as examples of implementation in C and Java. For my implementation, I used a modified version of the algorithm (“tweaked” Anubis), which differs in that it uses not the pseudo-random S-Box , but the optimal one selected by the authors. As a result, I got a class with the following interface:

class Anubis { /* Properties */ string $key //,  string $KDF_salt //     string $KDF_algo //-     int $file_blocksize //     /* Methods */ string function encrypt($data) //  string function decrypt($data) //  void function encryptFile($src, $dest) //  void function decryptFile($src, $dest) //  void function setKey($key, $raw_key = false) //  } 

')
The Anubis::setKey($key, $raw_key = false) , as its name implies, is intended to set the key. In my implementation, I provided both the possibility of using a simple text key (for example, "VeryStrongPassword" ) and the possibility of specifying a bit string (example: hex2bin('575a42654a85020b4f6eaeff03aecb0e') ). The $raw_key parameter is used to use the bit string as the key.

If we use a bit string for a key, we control the content of each bit of the key and its length (from 128 to 320 bits in 32-bit increments), but we also take care of its correctness. In particular, if the key is too long, too short, or not a multiple of 32 bits, the Anubis::setKey() method will throw an exception. If we use a simple text key, then before encryption it will be converted by the key output function (in the literature such translations as the key generation function or the key grinding function can also be found).

As a key derivation function, I used the usual HMAC , whose parameters are set by the corresponding properties ( Anubis::KDF_salt , Anubis::KDF_algo ). By default, the string of 16 zero octets "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" used as a salt, and The quality of the algorithm is SHA-256 . In principle, it would be possible to use ordinary MD5 without any salt at all, because when outputting a key we only need to destroy the limitations of the original password, such as improper length (less than 128, more than 320 or not a multiple of 32 bits), a limited set of characters (usually, a-zA-Z0-9!@#$%^&*(){}[]\/*-+.,?';:~`_"| ) and statistical text dependencies (if the password is a text phrase), but I decided to leave more room for customizing the algorithm for specific tasks.

The Anubis::key property is also intended to set a key, but there is no possibility to specify a bit string. Personally, I always like to use exactly properties (note, not fields that are object variables accessible from the outside, namely properties — when each read or write operation calls for the corresponding methods), in my opinion, this is more convenient and concise.

A little more about using text keys. As I said, the Anubis cipher key must be from 128 bits (16 bytes) to 320 bits (40 bytes) in 32-bit increments (4 bytes). A text key assigned, for example, via Anubis::key , very often will not fit the specified sizes. In principle, using the key derivation function, one could go through the least resistance and simply equalize all keys in size - either make them minimal, or turn on the paranoid mode and vice versa, maximize the length to 320 bits. But I considered this approach wrong, because if I can install keys of different lengths, accordingly, I can expect that the encryption system will use keys of different lengths.

As a result, in my implementation, the key, after being processed by the key output function, will always have the minimum possible length, but not less than the length of the original key. Those. if I, for example, enter the key "010101010101" (12 bytes), then during encryption the key will be expanded to 16 bytes (128 bits). If I enter "10101010101010101" (17 bytes), then the key used for encryption will be 20 bytes long (160 bits), and so on.

About the key has already been said enough, so it's time to return to the description of other methods.

The names of the methods Anubis::encrypt($data) and Anubis::decrypt($data) speak for themselves. It is clear that decrypt(encrypt($data)) == $data .

Here it is necessary to clarify how the message is encrypted. By itself, Anubis is a block cipher, and is able to encrypt blocks, as the name implies. Those. The original message is divided into blocks, in our case with a length of 128 bits, and each block is encrypted with a key separately. I think it is clear that it is impossible to encrypt messages with a length of more than one block (and we’ll remind you that it is only 16 bytes) - some statistical dependencies remain, so the ciphertext can be analyzed, especially if it is big enough, or if the messages encrypted with one key a lot. The problem here is that the same block of text when using the same key will always be represented by the same block of the encrypted message.

To overcome the problems described, I used the ciphertext block concatenation mode , in which each next block is encrypted using the information obtained by encrypting the previous one. When encrypting the first block, instead of the information from the previous encryption, a special initialization vector is used. Thus, subject to the use of one-time initialization vectors, the same block of source text each time will give a different ciphertext, which deprives the potential hacker of any statistical information for analysis.

In my implementation, to generate the initialization vector, the time stamp is used with an accuracy of up to microseconds and a pseudo-random number (in case you somehow manage to simultaneously create two initialization vectors for one key).

After we have generated and applied the initialization vector, it must somehow be sent along with the encrypted message, otherwise it will not be possible to decrypt our text correctly. Since there is no need to keep the initialization vector in secret, in my implementation I simply insert it with the first block into the encrypted message. When decrypting, the initialization vector is read from the ciphertext first, and only then directly the encrypted data is read.

Another feature arising from the block cipher property is that the message size should always be a multiple of the block size. If this is not the case, then the rest of the message must be supplemented to the size of the block. The problem here is that when decrypting, the padded bytes must somehow be deleted. The simplest solution would be to add the remainder to zero characters, and when decrypting, simply delete them. But, first, who said that there can be no null characters in the original message, and we simply simply will not remove part of the message itself? And secondly, why give a potential hacker additional information that at the end of the message is often from 1 to 15 null characters?

Because I went a little different way. The remainder of the message is supplemented with pseudo-random bytes, and the last byte contains information on the number of characters added. If suddenly the length of the original message is already a multiple of the block size, and there is no residue, then you have to add one block completely filled with pseudo-random characters - after all, the algorithm cannot know in advance whether bytes have been added or not, and will delete as many characters at the end in the last byte. Of course, certain dependencies also remain here - the last byte of the original message now always has a value from chr(1) to chr(16) , only 16 variants. But it is, nevertheless, slightly better than a series of well-defined characters, especially if the original message is pre-processed to destroy the characteristics of the source text, which can be known to a potential cracker (for example, a text message can be compressed by an algorithm that does not involve a specific "tail") .

The Anubis::encryptFile($src, $dest) and Anubis::decryptFile($src, $dest) Anubis::encrypt($data) similar to Anubis::encrypt($data) and Anubis::decrypt($data) with the exception that the original message here it is read from a file called $src , and the result is written to the $dest file. The data is read and written by chunks (here I avoid the word “block” so as not to let it with the block that encrypts the data), the size of which can be set via the Anubis::file_blocksize , selecting it in such a way that Records were made as quickly as possible, while consuming a reasonable amount of memory (the chunk is read into the entire memory). By default, the chunk size is set to half a megabyte.

Well, actually, and all the description. It remains only to give a small example of use:

 <?php require_once 'anubis.class.php'; $src = 'secret_message.txt'; $encrypted = 'encrypted.file'; $decrypted = 'decrypted_message.txt'; $cypher = new Anubis(); $cypher->key = 'strong password'; $cypher->encryptFile($src, $encrypted); $cypher->decryptFile($encrypted, $decrypted); $src_hash = md5_file($src); $decrypted_hash = md5_file($decrypted); echo "Src: $src_hash\n"; echo "Dest: $decrypted_hash\n"; 


Finally, I will say that encryption is not so fast. With the maximum key length, my system could encrypt about 100 KB / s (approximately 6400 blocks per second), with a minimum key length - about 150-160 KB / s (approximately 10,000 blocks per second). So if anyone has any suggestions for optimizing my implementation of the algorithm (the profiler says that all this time is eaten up by the private method Anubis::crypt() - directly by the encryption / decryption function), I will listen very much.

Project repositories on GitHub (source code, examples, wiki): https://github.com/kolonist/php-anubis
The official Anubis algorithm page: http://www.larc.usp.br/~pbarreto/AnubisPage.html

Source: https://habr.com/ru/post/145621/


All Articles