Let's not mince words: Rolling your own cryptography is almost always a mistake. Almost always, the correct answer is DON'T DO IT.
If you roll your own crypto, you do so at your own risk.
If you're writing your own cryptography code for educational purposes or you're a cryptographer, you can sometimes get away with discarding this advice. However, most developers simply ignore it and proceed to design their own broken cryptography protocols.
If you're going to throw all caution to the wind, be prepared for disastrous outcomes. If proceeding down this perilous path is necessary, it's important to take every precaution possible.
What Problem Are You Trying to Solve?
Before you dive off the deep end, it's important to have an idea of where you'd like to land. Adobe famously eschewed this step and encrypted their user's passwords (with 3DES in ECB mode).
Some questions to consider:
- What information am I trying to protect?
- What's an appropriate threat model to adopt?
- What's the simplest use-case that our solution should address?
- What is the minimum access our application needs to this information to solve our business needs?
It helps to be intimately familiar with cryptography terms and concepts. You don't want to specify "encrypted passwords" when you should be using a password hashing function to store passwords instead.
One (uncommon but not particularly rare) use case for a custom cryptography protocol is allowing database look-ups on encrypted records. We'll examine this in detail in a later section.
Be the Right Kind of Lazy
If you were to implement secure public key encryption in a vanilla PHP 7 application, these are the steps you would need to take:
- Generate two random 256-bit keys from
- Generate a random 128-bit nonce from
- Encrypt the message with
openssl_encrypt()in AES-256-CTR mode, using one 256-bit key and the nonce.
- MAC the nonce (step 2) and ciphertext (step 3), using HMAC-SHA-256 with the other 256-bit key.
- Encrypt both 256-bit keys with
openssl_public_encrypt()and the recipient's RSA public key, taking care to specify
- Concatenate and send the output of steps 2, 3, 4, and 5.
Decryption would involve:
- Split the message into constituent components.
- Decrypt the AES and HMAC keys using
- Recalculate the HMAC of the ciphertext, compare to the MAC contained in the message using
- If step 3 succeeds, decrypt the ciphertext, using the AES key and nonce.
This is a lot of work and involves a lot of moving parts. Alternatively, you could use libsodium and your protocol simplifies to:
$ciphertext = \Sodium\crypto_box_seal($message, $recipient_public_key);
$plaintext = \Sodium\crypto_box_seal_open($ciphertext, $recipient_keypair);
Rule of thumb: If you're building a high-level protocol, use secure implementations of lower-level protocols from a well-studied library.
Avoid the Obvious Pitfalls
- Understand the difference between encryption and authentication.
- Use a CSPRNG.
- Avoid side-channel attacks.
- Fail closed, but gracefully. PHP exceptions are ideal here.
- If you're using something like RSA, don't encrypt the message directly (i.e. RSA-ECB).
Generally, if you're using a secure library (in the spirit of laziness), this will be taken care of for you. A great deal of caution is still warranted.
Get Your Design and Implementation Reviewed by Experts
Disclaimer: This is a service that Paragon Initiative Enterprises, LLC offers.
Before you deploy your solution in a production environment, or share it in such a way that encourages others to deploy it, you must get cryptography and security experts to review your work. Get in touch today if you'd like us to review your work.
If you don't have the security talent on-staff, nor the budget to hire cryptographers, to review your solution, then you shouldn't be deploying your own cryptography in the first place.
What follows are some problems that developers (whom will not be named herein) have encountered that prompted them to consider writing their own cryptography, and possible solutions.
There is currently only one example, but we may revisit this article and add more as time goes on.
Problem: You need to store social security numbers in your database. Regulations dictate that they must be encrypted at rest. Complication: Your software needs to be able to perform an exact-match lookup on the database to determine if a SSN is present in a given table.
There are a lot of unsafe ways to solve this problem, including:
- Eschewing secure symmetric cryptography in favor of ECB mode (or even XOR).
- Decrypting and comparing every row in the table. (RAM and CPU wasteful.)
- Rearchitecting the entire stack to support theoretical and often-untested cryptography features like homomorphic encryption.
But those are bad ideas. Don't do them. A safer solution is to use a blind index:
- Use secure encryption, such as what Halite and defuse/php-encryption provide.
- In a separate column (
ssn_idx), store the output of
hmac_hash('sha256', $socialSecurity, $someSecretKey).
- Use the column in step 2 in your SELECT queries.
As long as the HMAC key isn't stored in the database and the database server sits on separate hardware (e.g. Amazon RDS) from the webserver (e.g. Amazon EC2), an attacker capable of stealing database records will be unable to obtain real social security numbers. However, since your application has the key, it can still query for the presence/absence of a given social security number.
This is an optimal trade-off between security and usability.
I hope you'll strongly consider not designing and implementing your own cryptography to solve business problems; but if you do, you'll follow the guidelines on this page:
- Identify your use case, specific requirements, and threat model.
- If you stop after this step and hire security experts to design and implement a solution, you'll be much happier.
- Understand the nuances of basic cryptography terms and concepts. (e.g. Encryption vs Authentication)
- Use an existing, well-audited library for as much as you can.
- Get your solution audited by experts.
(If you read this far and were still wondering: Yes, the title is tongue-in-cheek. I really should've included "military grade" but it disrupts the flow.)