The shortest answer to any question about securely using RSA is: [Don't](https://paragonie.com/blog/2016/12/everything-you-know-about-public-key-encryption-in-php-is-wrong). Because there are [much better cryptography choices](http://latacora.singles/2018/04/03/cryptographic-right-answers.html) available today, if you can avoid using RSA, don't use RSA. Then everything else in this document becomes *not your problem*. Throughout this post, we assume at least a casual understanding of what RSA is, and the role of [asymmetric cryptography](https://paragonie.com/blog/2015/08/you-wouldnt-base64-a-password-cryptography-decoded) in general. If you do not meet these prerequisites, or experience any difficulty understanding the rest of this post, [this is a good introduction to RSA](http://www.iet.unipi.it/g.dini/Teaching/sncs/lectures/handouts/05.02.RSA.pdf) and [this talk by Colin Percival](https://www.youtube.com/watch?v=jzY3m5Kv7Y8) ([slides](http://www.daemonology.net/papers/crypto1hr.pdf)) is a good follow-up. If you're more of a book learner, you can't go wrong with a copy of [*Serious Cryptography*](https://nostarch.com/seriouscrypto) by [Dr. Jean-Philippe Aumasson](https://cryptotraining.ch/#trainers). # The Web Developers' Guide to RSA Security Failures There are a lot of common (and a few [uncommon](http://www.cryptofails.com/post/70059600123/saltstack-rsa-e-d-1)) ways to use RSA insecurely. So many, in fact, that most cryptographers have discouraged its use in favor of elliptic curve cryptography. (Soon, this will be replaced by post-quantum cryptography.)
<?php
declare(strict_types=1);
namespace ParagonIE\BlogExampleCode;
/**
* Implements encryption RSA with PKCS1v1.5 padding
* using the Anti-BB'98 dance.
*
* @ref https://paragonie.com/b/_8qH-xPuGPUoSSsc
*/
class RSAPKCS1v15
{
public static function encrypt(string $message, string $publicKey): array
{
$key = \random_bytes(32);
$C = '';
// This defaults to PKCS1v1.5:
\openssl_public_encrypt($key, $C, $publicKey);
$iv = \random_bytes(16);
$cipher = \openssl_encrypt(
$message,
'aes-256-ctr',
$key,
OPENSSL_RAW_DATA,
$iv
);
$mac = \hash_hmac('sha256', $iv . $cipher, $key, true);
$D = $mac . $iv . $cipher;
return [\bin2hex($C), \bin2hex($D)];
}
public static function decrypt(string $CHex, string $DHex, string $privateKey): string
{
$kPrime = \random_bytes(32);
$C = \hex2bin($CHex);
$D = \hex2bin($DHex);
$k = '';
$success = \openssl_private_decrypt($C, $k, $privateKey);
// Do a constant-time swap:
$key = self::cswap32($k, (string) $kPrime, $success);
$mac = \mb_substr($D, 0, 32, '8bit');
$iv = \mb_substr($D, 32, 16, '8bit');
$cipher = \mb_substr($D, 48, null, '8bit');
$recalc = \hash_hmac('sha256', $iv . $cipher, $key, true);
if (!\hash_equals($recalc, $mac)) {
throw new \Exception('Invalid MAC');
}
return \openssl_decrypt(
$cipher,
'aes-256-ctr',
$key,
OPENSSL_RAW_DATA,
$iv
);
}
/**
* Branch-less, timing safe version of
* $A = ($switch === 0 ? $A : $B);
*
* @param string $A
* @param string $B
* @param bool $success
* @return string
*/
public static function cswap32(string $A, string $B, bool $success): string
{
$A .= \str_repeat("\x00", 32);
$B .= \str_repeat("\x00", 32);
/** @var int $mask 0 if $success is true, otherwise 255 */
$mask = (((int) $success) - 1) & 0xff;
$C = '';
for ($i = 0; $i < 32; ++$i) {
$C .= self::intToChr(
(self::chrToInt($A[$i]) ^ self::chrToInt($B[$i])) & $mask
);
}
return (string) mb_substr($A ^ $C, 0, 32, '8bit');
}
public static function chrToInt(string $chr): int
{
$chunk = \unpack('C', $chr);
return (int) ($chunk[1]);
}
public static function intToChr(int $int): string
{
return \pack('C', $int);
}
}
An online demo for this example code is [available, thanks to 3v4l.org](https://3v4l.org/k4c0tj). The linked snippet demonstrates that, whether you tamper with the RSA or AES ciphertext, the whole system behaves as if the MAC was invalid on the AES ciphertext, and doesn't leak any hints about this discrepancy in timing information.
Thus, the Bleichenbacher attack is stopped short in its tracks.
/**
* WARNING: This example code is naive and should not be used.
*/
function encrypt(message, publicKey) {
var k = crylib.randomBytes(32);
var C = crylib.rsaEncrypt(k, publicKey);
var D = crylib.aesEncrypt(message, k);
return [C, D];
}
function decrypt(C, D, privateKey) {
var k = crylib.rsaDecrypt(C, privateKey);
return crylib.aesDecrypt(D, k);
}
This code is especially dangerous if the hypothetical `crylib` defaults to PKCS1v1.5 padding. If it uses OAEP, it might be secure, but let's say we have no knowledge of whether or not our hypothetical `crylib` is secure against side-channels. What can we do?
For starters, we can reuse the strategy outlined in [the previous section](#rsa-anti-bb98). This protects us if PKCS1v1.5 is accepted by the underlying cryptography library.
We can also change how we handle $k$ to minimize the risk of information leakage during decryption. To this end, we'll use HMAC-SHA256 over the RSA ciphertext ($C$), using the RSA plaintext ($k$) as the HMAC key, and use the output for our symmetric encryption. We'll call it something else ($m$?) for simplicity.
By using HMAC in this way, we can make it so any attempts to attack $C$ (whether or not they leak some information about $k$) result in a pseudorandom message key ($m$) and $D$ will fail to decrypt because of an invalid MAC.
Our protocol now looks like this:
* Encryption
1. Generate a random 32-byte key, $k$.
2. Encrypt $k$ with your RSA public key to get $C$.
3. Calculate the HMAC-SHA256($C$, $k$) to get the message key $m$.
4. Encrypt your message with $m$ to get $D$.
5. Return $C$ and $D$.
* Decryption
1. Generate a random dummy key, which we'll call $k^{\prime}$.
2. Decrypt $C$ using your RSA private key, to obtain $k$ (one-time AES key).
3. If step 2 failed, use a constant-time algorithm to swap $k$ out for $k^{\prime}$.
4. Calculate the HMAC-SHA256($C$, $k$) to get the message key $m$.
5. Decrypt $D$ using $m$.
6. Return the decrypted plaintext (or fail because of the authentication tag).
One possible advantage of this construction is domain separation between derived keys, but the main purpose is to put less burden on RSA alone to keep your keys secret.
Sidenote: We're reasonably sure this construction has a distinct name in the cryptographic literature, but we cannot recall what it was. Our old notes just say "KEM+DEM" but that only comes up when discussing NTRU-Prime.
# Summary
RSA is painful to implement securely. Compare the level of effort here with [using libsodium's `crypto_box_seal()` or `crypto_sign_detached()`](https://paragonie.com/blog/2017/06/libsodium-quick-reference-quick-comparison-similar-functions-and-which-one-use) and you'll quickly see why cryptography experts tell developers, "Just use NaCl/libsodium".
Further Reading: [Cryptographically Secure PHP Development](https://paragonie.com/blog/2017/02/cryptographically-secure-php-development)