The reason why you want to avoid implementing cryptographic algorithms yourself is because of side-channel attacks.
What is a side-channel?
When you communicate with a server, the content of the messages is the "main" channel of communication. However, there are several other ways for you to get information from your communication partner that doesn't directly involve them telling you something.
These include, but are not limited to:
- The time it takes to answer you
- The energy the server consumes to process your request
- How often the server accesses the cache to respond to you.
What is a side-channel attack?
Simply put, a side-channel attack is any attack on a system involving one of these side-channels. Take the following code as an example:
public bool IsCorrectPasswordForUser(string currentPassword, string inputPassword)
{
// If both strings don't have the same length, they're not equal.
if (currentPassword.length != inputPassword.length)
return false;
// If the content of the strings differs at any point, stop and return they're not equal.
for(int i = 0; i < currentPassword.length; i++)
{
if (currentPassword[i] != inputPassword[i])
return false;
}
// If the strings were of equal length and never had any differences, they must be equal.
return true;
}
This code seems functionally correct, and if I didn't make any typos, then it probably does what it's supposed to do. Can you still spot the side-channel attack vector? Here's an example to demonstrate it:
Assume that a user's current password is Bdd3hHzj
(8 characters) and an attacker is attempting to crack it. If the attacker inputs a password that is the same length, both the if
check and at least one iteration of the for
loop will be executed; but should the input password be either shorter or longer than 8 characters, only the if
will be executed. The former case is doing more work and thus will take more time to complete than the latter; it is simple to compare the times it takes to check a 1-char, 2-char, 3-char etc. password and note that 8 characters is the only one that is notably different, and hence likely to be the correct length of the password.
With that knowledge, the attacker can refine their inputs. First they try aaaaaaaa
through aaaaaaaZ
, each of which executes only one iteration of the for
loop. But when they come to Baaaaaaa
, two iterations of the loop occur, which again takes more time to run than an input starting with any other character. This tells the attacker that the first character of the user's password is the letter B
, and they can now repeat this step to determine the remaining characters.
How does this relate to my Crypto code?
Cryptographic code looks very different from "regular" code. When looking at the above example, it doesn't seem wrong in any significant way. As such, when implementing things on your own, it might not be obvious that code which does what it's supposed to do just introduced a serious flaw.
Another problem I can think of is that programmers are not cryptographers. They tend to see the world differently and often make assumptions that can be dangerous. For example, look at the following unit test:
public void TestEncryptDecryptSuccess()
{
string message = "This is a test";
KeyPair keys = MyNeatCryptoClass.GenerateKeyPair();
byte[] cipher = MyNeatCryptoClass.Encrypt(message, keys.Public);
string decryptedMessage = MyNeatCryptoClass.Decrypt(cipher, keys.Private);
Assert.Equals(message, decryptedMessage);
}
Can you guess what's wrong? I have to admit, that wasn't a fair question. MyNeatCryptoClass
implements RSA and is internally set to use a default exponent of 1 if no exponent is explicitly given.
And yes, RSA will work just fine if you use a public exponent of 1. It just won't really "encrypt" anything, since "x1" is still "x".
You might ask yourself who in their right mind would do that, but there are cases of this actually happening.
Implementation Errors
Another reason why you might go wrong implementing your own Code is implementation errors. As user Bakuridu points out in a comment, bugs in Crypto code are fatal in comparison to other bugs. Here are a few examples:
Heartbleed
Heartbleed is probably one of the most well-known implementation bugs when it comes to cryptography. While not directly involving the implementation of cryptographic code, it nonetheless illustrates how monstrously wrong things can go with a comparatively "small" bug.
While the linked Wikipedia article goes much more in-depth on the issue, I would like to let Randall Munroe explain the issue much more concisely than I ever could:
https://xkcd.com/1354/ - Image Licensed under CC 2.5 BY-NC
Debian Weak PRNG Bug
Back in 2008, there was a bug in Debian which affected the randomness of all further key material used. Bruce Schneier explains the change that the Debian team made and why it was problematic.
The basic gist is that tools checking for possible problems in C code complained about the use of uninitialized variables. While ususally this is a problem, seeding a PRNG with essentially random data is not bad. However, since nobody likes staring at warnings and being trained to ignore warnings can lead to its own problems down the line, the "offending" code was removed at some point, thus leading to less entropy for OpenSSL to work with.
Summary
In summary, don't implement your own Crypto unless it's designed to be a learning experience! Use a vetted cryptographic library designed to make it easy to do it right and hard to do it wrong. Because Crypto is very easy to do wrong.