Be careful with implementing RSA. In fact, you probably shouldn't use RSA at all. (Use libsodium instead!)
Even if you're using a library (e.g. PHP's OpenSSL extension directly or, until recently, Zend\Crypt
), there's still plenty that can go wrong. In particular:
- PKCS1v1.5 padding, which is the default (and in many cases the only supported padding mode), is vulnerable to a class of chosen-ciphertext attacks called a padding oracle. This was first discovered by Daniel Bleichenbacher. In 1998.
- RSA is not suitable for encrypting large messages, so what implementors often do is take a long message, break it up into fixed-size blocks, and encrypt each block separately. Not only is this slow, it's analogous to the dreaded ECB mode for symmetric-key cryptography.
The Best Thing to Do, with Libsodium
You might want to read JavaScript Cryptography Considered Harmful a few times before going down this route. But that said...
- Use TLSv1.2 with HSTS and HPKP, preferably with ChaCha20-Poly1305 and/or AES-GCM and an ECDSA-P256 certificate (important: when the IETF christens Curve25519 and Ed25519, switch to that instead).
- Add libsodium.js to your project.
- Use
crypto_box_seal()
with a public key to encrypt your messages, client-side.
- In PHP, use
\Sodium\crypto_box_seal_open()
with the corresponding secret key for the public key to decrypt the message.
I need to use RSA to solve this problem.
Please don't. Elliptic curve cryptography is faster, simpler, and far easier to implement without side-channels. Most libraries do this for you already. (Libsodium!)
But I really want to use RSA!
Fine, follow these recommendations to the letter and don't come crying to StackOverflow when you make a mistake (like SaltStack did) that renders your cryptography useless.
One option (which does not come with a complementary JavaScript implementation, and please don't ask for one) that aims to provide simple and easy RSA encryption is paragonie/easyrsa.
- It avoids the padding oracles by using RSA-OAEP with MGF1+SHA256 instead of PKCS1v1.5.
- It avoids the ECB mode by clever protocol design:
The EasyRSA Encryption Protocol
- EasyRSA generates a random 128-bit key for symmetric key cryptography (via AES).
- Your plaintext message is encrypted with defuse/php-encryption.
- Your AES key is encrypted with RSA, provided by phpseclib, using the correct mode (mentioned above).
- This information is packed together as a simple string (with a checksum).
But, really, if you find a valid use case for public key cryptography, you want libsodium instead.
Bonus: Encryption with JavaScript, Decryption with PHP
We're going to use sodium-plus to accomplish this goal. (Adopted from this post.)
const publicKey = X25519PublicKey.from('fb1a219011c1e0d17699900ef22723e8a2b6e3b52ddbc268d763df4b0c002e73', 'hex');
async function sendEncryptedMessage() {
let key = await getExampleKey();
let message = $("#user-input").val();
let encrypted = await sodium.crypto_box_seal(message, publicKey);
$.post("/send-message", {"message": encrypted.toString('hex')}, function (response) {
console.log(response);
$("#output").append("<li><pre>" + response.message + "</pre></li>");
});
}
And then the congruent PHP code:
<?php
declare(strict_types=1);
require 'vendor/autoload.php'; // Composer
header('Content-Type: application/json');
$keypair = sodium_hex2bin(
'0202040a9fbf98e1e712b0be8f4e46e73e4f72e25edb72e0cdec026b370f4787' .
'fb1a219011c1e0d17699900ef22723e8a2b6e3b52ddbc268d763df4b0c002e73'
);
$encrypted = $_POST['message'] ?? null;
if (!$encrypted) {
echo json_encode(
['message' => null, 'error' => 'no message provided'],
JSON_PRETTY_PRINT
);
exit(1);
}
$plaintext = sodium_crypto_box_seal_open(sodium_hex2bin($encrypted), $keypair);
echo json_encode(
['message' => $plaintext, 'original' => $encrypted],
JSON_PRETTY_PRINT
);