Throughout the written history of humankind, the need to hide information has been omnipresent in almost all aspects of our lives. From safely transmitting messages to our fighting troops in ancient times to the simplest click we execute on our browser today to log in to our favourite social media site, there is an abundance of information that is being transmitted between participants who wish their information to be safe, secure and available only to them.
The most common way of achieving this goal is to use some form of encryption scheme, which takes an existing message and – using a series of operations – transforms the message into a scrambled form that cannot be read until a corresponding, but inverse, operation called decryption is applied to it, which will reveal the original message.
In this article, we will focus on a beginner level introduction to cryptography that is applicable to our everyday tasks. This requires the encrypting and decrypting of messages as a form of safe communication between two (or more) participants in a communication channel. There will be a short discussion on cryptography in order to have a better understanding and overview of the terminology we use, and we will provide practical examples on how to use cryptographic functions in our code.
In order to not to scare the reader away and also to keep the size under a certain digestible limit, the article intentionally skips the advanced cryptographical terminologies, and we will not dive into the deep mathematical foundations that the theory of cryptopgraphy is built upon.
There are several excellent books written about cryptography by renowned cryprographers whose prestigious work has greatly contributed to the advancements in the field, so we do not even try to condense all that information here, but just give a generic overview of what we should know about cryptography at a level where we can start using it in our daily work.
The article can be read either by starting from the beginning and reading through the terminologies part followed then by the code, or the other way around, by starting directly with the code and referencing backward into the terminologies part when something unknown pops up.
And, of course, if you become interested in cryptography after reading this article I always recommend reading more and more material in this field and even attend a dedicated training course, since this article can be just a ‘teaser’ into this huge field. It is impossible to cover everything while still keeping it readable, not forgetting to mention that the physical size of this journal cannot compete with the size of a book, so don’t be afraid to do extra research and invest extra time into deepening your knowledge.
The Ultimate goal of Cryptography
There are three main goals in cryptography that can be viewed as the holy grail:
- C stands for Confidentiality: Ensuring that only authorized parties are able to understand the encrypted data.
- I stands for Integrity: Ensures that only authorized parties can modify the data and to ensure that the data that has left the sender is the same as the data received by the recipient.
- A stands for Authentication: Ensures that anyone who supplies or accesses sensitive data is an authorized party.
And since we used the Authorized term frequently, here is a loose definition for it:
anyone who has a particular secret and has permissions (usually obtained directly from the source of the plaintext) or more mundanely put: knows the password, so he can log in and has proper rights to perform operations in the system (read, write, execute, access, ...).
There is a difference between Authenticated and Authorized where authenticated refers to someone who is verified to be whom he or she is supposed to be and Authorized we’ve seen before. Or more mundanely: Authenticated knows the password and is allowed log in with provided credentials.
And last, but not least: the integrity aspect comes closely tied to the non-repudiation facet of a message interchange, ie.: if you have sent it, you cannot deny that you have sent it.
Terminology in cryptography
Before we start with the practical (read: writing code) part of the article, there is a need for a very short introduction presenting a few definitions in order to have a brief understanding of the terms used in the field. Without this introduction it would be more difficult to understand the example code.
Plaintext
Plaintext is nothing else but the message we wish to transform using cryptographical algorithms in order to protect the information it stores. Plaintext is usually easily interpretable by humans using various techniques, such as reading its content.
Ciphertext
Ciphertext is the resulting data of a cryptographical algorithm when it is applied to a plaintext. It is supposed to be unreadable by humans or machines and only by using the correct algorithm should the originating plaintext be revealed.
XOR
Since the bitwise operation XOR
(Exclusive OR) is mentioned several times in the article, just a quick reminder that XOR is the logical operation that outputs true (1) only for different inputs.
The following is the truth table of the XOR operator.
A | B | A XOR B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
The cipher
According the the Oxford dictionary a cipher is: (NOUN) ‘A secret or disguised way of writing’. For us programmers, the cipher is just another word for a pair of encryption/decryption algorithms.
- Encryption, as per the definition, is the process of converting the plaintext into the unreadable code known as ciphertext in order to prevent unauthorized access to it.
- Decryption is the inverse operation of the encryption.
The encryption algorithm uses a key to scramble the data, which can be viewed as your secret password (but it’s actually more), and the decryption algorithm must use also a key (same or different) to retrieve the plaintext.
In cryptography there are two mainstream cipher types utilized today:
- symmetric ciphers are algorithms that use the same secret key for encryption and decryption of the data.
- asymmetric ciphers are algorithms that use a pair of keys for the encrypting and decrypting process.
For anyone interested, [Golodetz08] has an excellent description on the inner works of probably the most well known public key algorithm (RSA) so for this article we will be focusing on symmetric ciphers only, and from now on any reference to a cipher in this article must be interpreted as ‘symmetric cipher’.
And finally, the definition of a Secret Key is: a piece of information, which is used to encrypt and decrypt messages in a cipher. It is confusable with password; however, it should not be due to the following major differences:
- a password is created by a ‘user’ by choosing a secret, but it is rarely used in properly set up systems to directly encrypt/decrypt data due to being considered ‘cryptographically weak’ (humans tend to choose data that they are familiar with when choosing a password, such as dictionary words, pet names, birth dates, or just simply ‘password’).
- a secret key is data which is the result of an algorithm applied to a password which gives ‘cryptographically strong’ data that can be safely used in the algorithms.
Attack of the Ciphers (and how to defend ourselves)
Since most of the encryption activities happen with a very specific purpose (ie: hide something the enemy is not supposed to know), in the adversary camp there is usually someone with the specialized role of Cryptanalyst who tries to obtain the secret information. Cryptanalysts are highly skilled in the dark art of breaking code (or just have access to a multi-billion-dollar super computer doing brute force attacks) but the worst of all is that they have access to our ciphertext. They can manipulate the ciphertext in order to obtain the plaintext and to derive the method used to encrypt which will allow them to decrypt other messages too and even reveal the secret key.
Types of attack
Several types of attack have been devised during the history of cryptography. We will present shortly a few (but not all), because when implementing cryptography in a system, it is wise to know what behaviour to expect from someone who tries to break your system.
Ciphertext-only attack
In this case, the cryptanalyst has access to a set of ciphertexts, and his ultimate goal is to retrieve the plaintext. A possible attack scenario is that the cipher was chosen with a small key space, thus via brute force the attacker can try all the possible keys. For example, DES (Data Encryption Standard) has keys of 56 bits, which are easily broken using modern technologies [DESCRACK]. Attacks on the ciphers used in GSM technology (A5/1 and A5/2) are also ciphertext-only attacks when intercepted message streams from phone conversations can be decrypted using dedicated solutions.
Chosen plaintext attack
In this scenario, the attackers can obtain the ciphertexts for chosen plaintexts. By analyzing the result ciphertext, they can gain information regarding the security of the encryption cipher and the algorithm it is using. In this case, the attacker has access to a ‘black box’ which generates ciphertext from individual plaintexts.
Chosen ciphertext attack
For this attack, the attacker can obtain the decrypted form of chosen ciphertexts. By analyzing it, information regarding the key can be obtained. As in the previous case, the attacker has access to a ‘black box’ which it will query with the chosen encrypted sequences.
Known plaintext attack
In this scenario, the attacker has access to both the plaintext and the ciphertext. These two can be used to reveal further information, such as encryption keys or algorithms.
Hardening your ciphertext
To make the life of the Cryptanalyst harder, extra protection steps can be taken to obtain a more secure ciphertext. These involve introducing a Salt (which is just a sequence of random bytes) to the encryption algorithm (more specifically, the password is ‘salted’ with it) which among other benefits, makes the usage of ‘rainbow tables’ (which are precomputed tables usually for obtaining the hashes of passwords) impossible.
Another element used in the encryption/decryption process is the Initialization vector which, similarly to the salt, is also a sequence of random numbers and is used in the initialization phase of the encryption algorithm in order to prevent the same plaintext generating the same ciphertext when the same algorithm is applied to it.
And the last element which will make our encryption safer is a Nonce which is just a plain number (coming from ‘number used once’) again used (only once) in order to make different ciphertext for the same input data.
The salt and the initialization vector are not considered private information, thus it is widely accepted to have them being sent over communication channels.
Types of ciphers
Currently two mainstream types of symmetric ciphers are in use:
- Block Ciphers
- Stream Ciphers
A Block Cipher is a deterministic pair of algorithms which operates on fixed-length groups of bits, which are called a block. One of the algorithms is used for encryption, the other one for decryption (which in mathematical terms is defined to be the inverse function of the encryption).
The algorithms have two inputs: a block (size: N bits) and a key (size: K bits). Both algorithms return an output block (size: N bits).
For a detailed description of the mode of operation of a block cipher, please consult [BlockCipher]. And in order to keep this article in a digestible size, we will focus our attention on block ciphers.
For the sake of brevity, let’s just mention that a Stream Cipher is a symmetric key algorithm (the same key is used for encryption and decryption) where the bits of the plaintext are combined (practically XOR-ed) with the bits of a pseudorandom cipher stream (called keystream).
Block cipher modes of operation
By definition, a block cipher operates on fixed length blocks of data, so the first operation that is done by the algorithm is the splitting of the plaintext into blocks of the required size and then each block is encrypted independently. This mode (called the ECB – Electronic Codebook) has the disadvantage that equal plaintext block will always generate the same ciphertext block.
You always should avoid using ECB while performing encryption, here is a proof of everyone’s favourite penguin image encrypted with ECB [ECB_TUX]:
In order to overcome this limitation, several algorithms have been designed that use randomization of the plaintext using an additional value (such as the Initialization vector mentioned earlier) to obtain a different ciphertext for identical plaintext.
The most commonly used of these modes are:
- CBC: Cipher Block Chaining – In this mode, the current block of plaintext is combined (using XOR) with the previous ciphertext block before being encrypted. The first block is combined with the initialization vector.
- PCBC: Propagating Cipher Block Chaining – In this mode, the current block of plaintext is combined (using XOR) with both the previous plaintext block and the previous ciphertext block before being encrypted. The first block is combined with the initialization vector.
- CTR: Counter – This mode of operation acts like a stream cipher. It generates the next keystream block by encrypting successive values of a ‘counter’ which can be generated by using a nonce and combining it with a nonrepetitive value generated by a function. Usually an increment by 1 of the value is the simplest operation.
A very detailed description of these is presented at [BlockCipherModes].
Padding
Since we cannot always expect the length of a message to be the exact multiple of the size of the block the algorithm operates on, some modes of operation (CBC, for example) require that the last block is padded with bytes of various origin. For this article, we will stick to the PKCS#7 padding mode presented in [RFC5652], which pads the input data with a number of D = N - K bytes, with their value being exactly D [PKCS7].
Hashing
Hashing is a method of ensuring the integrity (one of the presented goals) of data by applying a hash function to it and retrieving the associated hash value. A hash function is a function which maps the data (of arbitrary length) to a data of fixed size in a deterministic manner (ie: same input data will always yield the same output value). It should be impossible to retrieve the input data using only the hash value. For purposes of cryptographic needs, cryptographic hash function are used which are widely presented in [CRHASH]. The output of the hash function is often called a digest.
For this article we will be focusing our attention to SHA-256, however this does not block anyone from experimenting with other functions.
SHA-256
As presented in [SHA256], SHA-2 is a set of cryptographic hash functions designed by the United States National Security Agency (NSA) from which we use the one which provides a digest of 256 bits. It is implemented and used in lots of applications and protocols, and as per 2018 it is considered safe for widespread use.
MAC or HMAC
The term MAC stand for Message Authentication Code, which is basically just a way to confirm the integrity of a message with a given key. Since these functions are usually constructed using a hashing function, the term HMAC (Hash-based MAC) is also used. What this does is nothing else than to calculate a cryptographically secure hash of a given data combined with a given secret key [HMAC].
Key derivation functions
In Cryptography, a Key Derivation Function is a scheme which takes an initial key and, through a series of operations, derives a cryptographically secure, uniformly distributed, strong secret key which can be used in cryptography operations.
For this article we will focus on PBKDF2 [PBKDF2], which is fairly modern and secure implementation of this function.
PBKDF2 derives a key of a specified length from a password, using a randomly generated salt, through a number of iterations on the basis that ‘more is better’ and, since ‘more is slower’, this is also a practical defense against brute force attacks.
AES
AES in cryptographical terms stands for ‘Advanced Encryption Standard’ is also known as ‘Rijndael’ and was developed by two Belgian cryptographers, Vincent Rijmen and Joan Daemen. The cipher was adopted worldwide for encryption of electronic data after it won the competition of U.S. NIST (National Institute of Standards and Technology) in 2001.
AES is a fast (to the level that modern microprocessors include a dedicated set of instructions [AES-NI]), safe and secure algorithm according to the cryptographical community. Also, Bruce Schneier [Schneier00], who developed Twofish (a competitor for Rijndael) for the same NIST competition, acknowledged:
I do not believe that anyone will ever discover an attack that will allow someone to read Rijndael traffic.
AES works on blocks with a fixed block size of 128 bits and a key size of 128, 192, or 256 bits, which makes it secure enough for all required cryptographical needs of modern systems.
A full specification of the algorithm is presented in [AES] and [AES-NIST], and a more detailed overview is presented in Cryptography: Theory and Practice [Stinson05] chapter 3.6.1 (page 105) so anyone interested can follow up there after reading the article.
For the moment we should just know that, in the practical part of the article, we will use AES for encryption and decryption.
Practicing cryptography
In the ‘practical’ part of the article, we will focus on a real-life scenario, where a hypothetical application created in javascript needs to send messages to an imaginary server, which was written in C++.
Please note: the code presented has just the purpose of being an example, it is definitely not to be put into production as it is. It is intentionally kept simple and readable, and it is upon the future developer to expand it in order to reach production grade.
In order to keep the article compact in size, we will not introduce several topics here that are out of scope for this article. We just assume that there is some agreement between the parties on how to securely transmit the password between the two endpoints, as you can see right now it is hardcoded into the source file. You definitely should NOT do this in production code: use a proper key management system for this.
For now, we just pretend that there is a proper protocol for sending the message between the two endpoints; again, for the example, we just ‘copy/pasted’ the encrypted message into the decryption code. This is definitely not a real-life situation.
The client-side libraries
For javascript, there are several libraries available (you can take a look at: [ClientCryptLibs]) which perform the required encryption/decryption operations on user data; for example, cryptico or cryptojs.
Following the documentation, crypto-js ([CryptoJS]) is easy to set up and use, but this should not hinder anyone trying out any other libraries.
The server-side library
On the server side, we can choose from a wide variety of libraries as per [ServCryptoLibs]. For demonstration purposes, I decided to use Botan [Botan] since it’s written in a fairly modern C++ dialect and it has implemented a huge variety of standard, safe and even not so well known algorithms.
The Botan site has an excellent ‘getting started’ section, which covers the build steps and has a wide selection of examples which can be instantly taken over into your code.
Another nice feature I appreciated with Botan is that it can create an amalgamation build thus enabling you to effectively include the source of the entire library (or just required parts of it, since the build tool is highly configurable) into your project to not to have to worry about libraries, linking and missing dependencies.
Client-side code
Please note, that in order to properly run the code presented in this section in a browser, I had to set up a local web server serving a static HTML page from my local file system, which had all the necessary HTML syntax … but this is out of the scope for this article. (Appendix A will present the full HTML page.)
The javascript code is in Listing 1.
var iterations = 1000; var keySize = 256; function encrypt (msg, pass) { var salt = CryptoJS.lib.WordArray.random(128 / 8); var key = CryptoJS.PBKDF2(pass, salt, { keySize: keySize / 32, iterations: iterations, hasher: CryptoJS.algo.SHA256 }); var iv = CryptoJS.lib.WordArray.random(128 / 8); var encrypted = CryptoJS.AES.encrypt(msg, key, { iv: iv, padding: CryptoJS.pad.Pkcs7, mode: CryptoJS.mode.CBC }); var hash = CryptoJS.HmacSHA256(msg, key); var hashInBase64 = CryptoJS.enc.Base64.stringify(hash); var result = hashInBase64.toString() + "_" + salt.toString()+ iv.toString() + encrypted.toString(); return result; } var encrypted = encrypt("Hello World", "S3cr3tP4sw"); window.alert(encrypted); |
Listing 1 |
Let’s step through it.
var iterations = 1000;
iterations
is the number of iterations which will be used by PBKDF2 in order to generate the key from our ‘master’ key (the pass
parameter) which is practically used in encrypting the data (msg
). There are various recommendations about the size of this number, but all of them agree that it should be a big one.
For the sake of the demonstration I chose it to be 1000; however, for real life situations a much bigger number is recommended.
var keySize = 256;
Tells us that we will attempt to use a key size of 256 bits; this is what we send to the PBKDF2
call.
var salt = CryptoJS.lib.WordArray.random(128 / 8);
The line will create a random salt, using the random
CryptoJS function, of length 16 to be used together with the password in the PBKDF2
call below.
var key = CryptoJS.PBKDF2(pass, salt, { keySize: keySize / 32, iterations: iterations });
This line is the one which actually creates the key that is used in the encryption. The ingoing parameters are the password we received as parameter, the salt we have generated, the size of the key we expect back and the iterations we want to spend on generating the key. The `keySize` parameter is the size of the key in words where a word on today’s architectures is typically 32 bits.
By default, the CryptoJS implementation
var iv = CryptoJS.lib.WordArray.random(128 / 8);
will create another random sequence of 16 bytes with the role of initialization vector that will be used in the encryption phase.
var encrypted = CryptoJS.AES.encrypt(msg, key, { iv: iv, padding: CryptoJS.pad.Pkcs7, mode: CryptoJS.mode.CBC });
This is the actual encryption step. Here we see how everything comes together when we are trying to encrypt the message with the key that was generated from a (theoretically weak) password, and using the initialization vector, specifying the padding (PKCS7, presented in a previous paragraph) and the operation mode (CBC) we also discussed before.
var hash = CryptoJS.HmacSHA256(msg, key); var hashInBase64 = CryptoJS.enc.Base64.stringify(hash);
These lines calculate an HMAC for the given message and the key.For the purposes of message integrity, it is recommended to supply the MAC for the message in order to be able to verify whether someone has tampered with it or not.
var result = hashInBase64.toString() + "_" + salt.toString()+ iv.toString() + encrypted.toString();
This is the line which calculates the result by simply concatenating the MAC, a separator (_
), the salt, the initialization vector (remember, these are not considered private information) and the encrypted message. Finally we return the result, obtaining for example the following string:
Fo/7rjwOjHUO0iK/REOpl4uq4L+12zA4tfc/YnNLeTg= _be9df31d0005ebff75c68790f7730100fc588ee586cee4cc 777327d2a010c4a1Pww/3i54DbH77FHr3+SJyg==
This can be decomposed into:
- the HMAC of the message:
Fo/7rjwOjHUO0iK/REOpl4uq4L+12zA4tfc/YnNLeTg=
- the salt =
be9df31d0005ebff75c68790f7730100
- the initialization vector =
fc588ee586cee4cc777327d2a010c4a1
- the actually encrypted data =
Pww/3i54DbH77FHr3+SJyg==
Please note the followings:
- since salt and iv are random, this method will return a different string every time
_
can be used as a separator, because the B64 alphabet does not contain this symbol.
Server side code
With Botan, creating a decrypter is just a few lines of code, should not be more than Listing 2.
std::string decrypt(const std::string& encrypted, const std::string& password, const std::vector<uint8_t>& salt, const std::vector<uint8_t>& iv, std::size_t iterations, const std::string& expected_mac) try { Botan::PKCS5_PBKDF2 pbkdf2(new Botan::HMAC( new Botan::SHA_256)); Botan::SymmetricKey key(pbkdf2.derive_key(32, password, &salt[0], salt.size(), iterations).bits_of()); Botan::InitializationVector the_iv(iv.data(), iv.size()); Botan::Pipe pipe(new Botan::Base64_Decoder, Botan::get_cipher("AES-256/CBC/PKCS7", key, the_iv, Botan::DECRYPTION)); pipe.process_msg(encrypted); std::string result = pipe.read_all_as_string(); Botan::Pipe mac_pipe( new Botan::MAC_Filter("HMAC(SHA-256)", key), new Botan::Base64_Encoder); mac_pipe.process_msg(result); std::string mac_result = mac_pipe.read_all_as_string(0); if (mac_result != expected_mac) { return ""; } return result; } catch (const std::exception& ) { return ""; } |
Listing 2 |
In Appendix B, we will present the full C++ source that uses the output from the cryptojs source and decodes the text fully, but for now let’s examine this snippet line by line.
std::string decrypt(const std::string& encrypted, const std::string& password, const std::vector<uint8_t>& salt, const std::vector<uint8_t>& iv, std::size_t iterations)
is just the declaration of the method: it expects all necessary input data to be sent in. Since some Botan functions might throw std::exception
derived exceptions, I have found improved readability for this specific purpose by packing the body of the function into a function-try-block, hence the try. Certainly, if you wish to fine-grain your error reporting, you always can have several try
-catch
blocks on the various steps.
Botan::PKCS5_PBKDF2 pbkdf2(new Botan::HMAC( new Botan::SHA_256));
will create the PBKDF2 object that we will use at a later stage to derive the master key from the provided password. Please note that the hasher method obviously has to match the one which was used in creating the encrypted text in the javascript code CryptoJS.algo.SHA256
or the decryption will fail. Botan takes care of the dynamically allocated object, by storing it in a std::unique_ptr
.
Botan::SymmetricKey key(pbkdf2.derive_key(32, password, &salt[0], salt.size(), iterations ).bits_of() );
This line creates the key which will be used in the decryption of the data. Again, the number of iterations and the size of the key must match the one that we have used in the javascript code.
Botan::InitializationVector the_iv(iv.data(), iv.size());
Creates an initialization vector object Botan can work with from the data we have procided.
Botan::Pipe pipe(new Botan::Base64_Decoder, Botan::get_cipher("AES-256/CBC/PKCS7", key, the_iv, Botan::DECRYPTION ) );
A Botan pipe is very similar to the notion of pipe that exists in many operating systems. Data comes in at the beginning, goes through various steps and comes out at the end. For our needs, we require a Botan::Base64_Decoder
object, since cryptojs provided B64 encoded data, and the output of this object (Botan calls them filters) will go into a Cipher object, obtained via:
Botan::get_cipher("AES-256/CBC/PKCS7", key, the_iv, Botan::DECRYPTION)
The syntax is straightforward: we ask Botan to provide a cipher for decryption (Botan::DECRYPTION
) for the given key and initialization vector. We would like to use AES-256
, with operation mode CBC
and padding PKCS7
.
The Botan pipe will own these objects so we don’t need to worry about freeing them at a later stage.
When we have the pipe set up, we simply ask it to process our message:
pipe.process_msg(encrypted);
and finally retrieve the result as a string:
std::string result = pipe.read_all_as_string();
Now comes the verification of the integrity of the message:
Botan::Pipe mac_pipe( new Botan::MAC_Filter("HMAC(SHA-256)", key), new Botan::Base64_Encoder); mac_pipe.process_msg(result); std::string mac_result = mac_pipe.read_all_as_string(0);
Will create another botan pipe in order to calculate the MAC of the message with the key.
if (mac_result != expected_mac) { return ""; }
And these lines simply verify that the MAC we have received as part of the message matches with the one we have calculated from the decrypted message and the key.
And that’s it.
Appendix A
My web server is set up in a way that all the required javascript files (cryptojs) are to be found inside the js folder in the root of the page, however you can set it up any way you desire, and you even can use online CDN sites to load cryptojs files. See Listing 3.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>hypothetical client</title> <script type="text/javascript" src="js/aes.js"></script> <script type="text/javascript" src="js/hmac.js"></script> <script type="text/javascript" src="js/pbkdf2.js"></script> <script type="text/javascript" src="js/sha256.js"></script> <script type="text/javascript"> var iterations = 1000; var keySize = 256; function encrypt (msg, pass) { var salt = CryptoJS.lib.WordArray.random(16); var key = CryptoJS.PBKDF2(pass, salt, { keySize: keySize/32, iterations: iterations, hasher: CryptoJS.algo.SHA256 }); var iv = CryptoJS.lib.WordArray.random(16); var encrypted = CryptoJS.AES.encrypt(msg, key, { iv: iv, padding: CryptoJS.pad.Pkcs7, mode: CryptoJS.mode.CBC }); var hash = CryptoJS.HmacSHA256(msg, key); var hashInBase64 = CryptoJS.enc.Base64.stringify(hash); var result = hashInBase64.toString() + "_" + salt.toString()+ iv.toString() + encrypted.toString(); return result; } var encrypted = encrypt("Hello World", "S3cr3tP4sw"); window.alert(encrypted); </script> </head> <body> </body> </html> |
Listing 3 |
Appendix B
I have used unhex
from boost::algorithm
in order to convert a hex string into its corresponding binary vector. If you don’t have experience with (or access to) boost algorithms feel free to use any other mechanism that will achieve the same results.
Splitting up the incoming string in a much more programmatical manner than presented in Listing 4 is left as an exercise for the reader.
#include <botan/key_filt.h> #include <botan/aes.h> #include <botan/pbkdf2.h> #include <botan/hmac.h> #include <botan/pipe.h> #include <botan/sha2_32.h> #include <botan/b64_filt.h> #include <botan/filters.h> #include <boost/algorithm/hex.hpp> #include <string> #include <vector> #include <iostream> std::string decrypt(const std::string& encrypted, const std::string& password, const std::vector<uint8_t>& salt, const std::vector<uint8_t>& iv, std::size_t iterations, const std::string& expected_mac) try { Botan::PKCS5_PBKDF2 pbkdf2(new Botan::HMAC( new Botan::SHA_256)); Botan::SymmetricKey key(pbkdf2.derive_key(32, password, &salt[0], salt.size(), iterations).bits_of()); Botan::InitializationVector the_iv(iv.data(), iv.size()); Botan::Pipe pipe(new Botan::Base64_Decoder, Botan::get_cipher("AES-256/CBC/PKCS7", key, the_iv, Botan::DECRYPTION)); pipe.process_msg(encrypted); std::string result = pipe.read_all_as_string(); Botan::Pipe mac_pipe(new Botan::MAC_Filter( "HMAC(SHA-256)", key), new Botan::Base64_Encoder); mac_pipe.process_msg(result); std::string mac_result = mac_pipe.read_all_as_string(0); if (mac_result != expected_mac) { return ""; } return result; } catch (const std::exception& ) { return ""; } std::vector<uint8_t> hex_string_to_vector( const std::string &in) try { std::vector<uint8_t> out; boost::algorithm::unhex(in.begin(), in.end(), std::back_inserter(out)); return out; } catch (const std::exception&) { return std::vector<uint8_t>(); } std::string decrypt(std::string salt, std::string iv, std::string encrypted, const std::string password, size_t iterations, const std::string& expected_mac) { std::vector<uint8_t> salt_v = hex_string_to_vector(salt); std::vector<uint8_t> iv_v = hex_string_to_vector(iv); return decrypt(encrypted, password, salt_v, iv_v, iterations, expected_mac); } int main() { // // Assuming the following message was received: // // |--------------- HMAC ---------------------|_|---------- SALT --------------||---------------- IV ----------||------- MESSAGE ------| //"Fo/7rjwOjHUO0iK/REOpl4uq4L+12zA4tfc/YnNLeTg=_be9df31d0005ebff75c68790f7730100fc588ee586cee4cc777327d2a010c4a1Pww/3i54DbH77FHr3+SJyg==" // std::string expected_mac = "Fo/7rjwOjHUO0iK/REOpl4uq4L+12zA4tfc/YnNLeTg="; std::string salt = "be9df31d0005ebff75c68790f7730100"; std::string iv = "fc588ee586cee4cc777327d2a010c4a1"; std::string encrypted = "Pww/3i54DbH77FHr3+SJyg=="; size_t iterations = 1000; std::string pass = "S3cr3tP4sw"; std::string decrypted = decrypt(salt, iv, encrypted, pass, iterations, expected_mac); std::cout << decrypted << std::endl; } |
Listing 4 |
References
[AES]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
[AES-NI]: https://en.wikipedia.org/wiki/AES_instruction_set
[AES-NIST]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
[BlockCipher]: https://en.wikipedia.org/wiki/Block_cipher
[BlockCipherModes]: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
[Botan]: https://botan.randombit.net/
[ClientCryptLibs]: https://github.com/gabrielizalo/JavaScript-Crypto-Libraries
[CRHASH]: https://en.wikipedia.org/wiki/Cryptographic_hash_function
[CryptoJS]: https://github.com/brix/crypto-js
[DESCRACK]: https://en.wikipedia.org/wiki/EFF_DES_cracker
[ECB_TUX]: http://en.wikipedia.org/wiki/Image:Tux_ecb.jpg This image is derived from File:Tux.jpg, owned by Larry Ewing (lewing@isc.tamu.edu) and created using The GIMP (https://www.gimp.org/)
[Golodetz08]: Stuart Golodetz ‘RSA Made Simple’, Overload, June 2008
[HMAC]: https://en.wikipedia.org/wiki/Hash-based_message_authentication_code
[PBKDF2]: https://en.wikipedia.org/wiki/PBKDF2
[PKCS7]: https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7
[RFC5652]: https://tools.ietf.org/html/rfc5652#section-6.3
[Schneier00]: https://www.schneier.com/crypto-gram/archives/2000/1015.html
[ServCryptoLibs]: https://en.wikipedia.org/wiki/Comparison_of_cryptography_libraries
[SHA256]: https://en.wikipedia.org/wiki/SHA-256
[Stinson05]: Douglas R. Stinson (2005) Cryptography: Theory and Practice, Third Edition (ISBN: 1584885084)
Overload Journal #144 - April 2018 + Design of applications and programs
Browse in : |
All
> Journals
> Overload
> o144
(7)
All > Topics > Design (236) Any of these categories - All of these categories |