41

I'd like to encrypt in JavaScript, decrypt in PHP, using public-key cryptography. I've been trying to find libraries that can accomplish this, but am having issues.

I am currently looking at openpgpjs, but I need support in all browsers, and even the test page has errrors on the only listed as supported browser (Google Chrome).

Notes about the final goal:

The TCP connection is already protected by SSL. The main purpose of this layer of protection is defending against intentional or unintentional webserver logging, crash dumps, etc.

On the PHP side, a temporary private key will be generated (it will expire after a short time). The caller (in Javascript) is responsible for asking for a new public key when it expires. The reason for private key expiration is to prevent logged encrypted data decryption, in case the server which stores the private key is later compromised.

Servers compromised scenario: someone gets his hands on backups for all machines except the database server (and cannot access the database due to firewalling, even if he finds out the user and password). Since the private key which encrypted the logged data no longer exists, there is nothing the attacker can do.

Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
oxygen
  • 5,891
  • 6
  • 37
  • 69
  • 1
    Hope you are still interested on that... You could use PHP 7.2 `sodium` functions and NaCL https://github.com/tonyg/js-nacl client-siden or any similar javascript implementation of libsosium. –  Aug 09 '18 at 14:54

5 Answers5

45

I've used something similar for my login page; it encrypts login credentials using the given public key information (N, e) which can be decrypted in PHP.

It uses the following files that are part of JSBN:

  • jsbn.js - to work with big integers
  • rsa.js - for RSA encryption only (uses jsbn.js)
  • rng.js - basic entropy collector
  • prng4.js - ARC4 RNG backend

To encrypt data:

$pk = '-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----';
$kh = openssl_pkey_get_private($pk);
$details = openssl_pkey_get_details($kh);

function to_hex($data)
{
    return strtoupper(bin2hex($data));
}

?>
<script>
var rsa = new RSAKey();
rsa.setPublic('<?php echo to_hex($details['rsa']['n']) ?>', '<?php echo to_hex($details['rsa']['e']) ?>');

// encrypt using RSA
var data = rsa.encrypt('hello world');
</script>

This is how you would decode the sent data:

$kh = openssl_pkey_get_private($pk);
$details = openssl_pkey_get_details($kh);
// convert data from hexadecimal notation
$data = pack('H*', $data);
if (openssl_private_decrypt($data, $r, $kh)) {
   echo $r;
}
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • +1 for simplicity. This is a good and clean solution. Note that pidCrypt uses jsbn (nicely formatted and "namespaced"). – oxygen Sep 25 '12 at 11:23
  • @Tiberiu-IonuțStan thanks for mentioning pidCrypt. I did the namespacing for my own project, but it seems that I could have saved some time :) – Ja͢ck Sep 26 '12 at 02:24
  • To clarify, should one leave the `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----` in the key? Is this required? – JVE999 Jun 20 '14 at 22:22
  • 1
    @jve although some libraries may not have this requirement, it would be recommended to leave those strings :) – Ja͢ck Jun 21 '14 at 00:16
  • Also! `prng4.js` must be included before `rng.js`. It's pretty simple to figure out, but this might save a little bit of a hassle. – JVE999 Jun 21 '14 at 01:30
  • I'm finding this actually gives the error `Warning: openssl_pkey_get_details() expects parameter 1 to be resource, boolean given`. I found an implementation that gets closer, but I still cannot get it to work. I've posted a question [on this page](http://stackoverflow.com/questions/24347415/openssl-pkey-get-detailsres-returns-no-public-exponent) with the implementation I'm using – JVE999 Jun 22 '14 at 04:47
  • 1
    FYI this defaults to PKCS1v1.5 padding, which recently led to a [CVE in Zend\Crypt](http://framework.zend.com/security/advisory/ZF2015-10). – Scott Arciszewski Dec 03 '15 at 05:20
  • @ScottArciszewski thanks! let me see what can be done about that, it's been a while since I've had to touch this code :) – Ja͢ck Dec 03 '15 at 10:06
  • I don't know what JSBN lets you do, but you can specify padding and the hash function in `openssl_private_decrypt()`. – Scott Arciszewski Dec 03 '15 at 17:12
  • 1
    In first block of code it is openssl_pkey_get_public (since you will encrypt with public key) and not openssl_pkey_get_private – Hamboy75 May 16 '18 at 13:33
  • As already mentioned by @hamboy75, the public key shoud be used for encryption but the example uses private key instead on lines 1, 3 and 4. It is confusing. Public key needs to be used for encryption (javascript) and private key for decryption (PHP). – eNca Jul 12 '21 at 12:03
  • @eNca it has been a while, but I believe the modulus and exponent must be pulled from the private key. – Ja͢ck Jul 18 '21 at 03:05
  • I have written the following working demo. https://github.com/danielleevandenbosch/gpg_encrypt_js_decrypt_php_demo . would someone please enlighten me to if it is possible to simply use the public key vs extracting it. That way we can avoid code mixing. – Daniel L. VanDenBosch Jul 11 '22 at 20:17
24

Check out node-rsa.

It's a node.js module

This module provides access to RSA public-key routines from OpenSSL. Support is limited to RSAES-OAEP and encryption with a public key, decryption with a private key.

Maybe you can port it to run in the browser.

UPDATE

RSA client side library for javascript: (pidcrypt has been officially discontinued and the website domain is expired - see @jack's answer which contains the same libraries as pidcrypt contained). https://www.pidder.com/pidcrypt/?page=rsa

PHP server side component: http://phpseclib.sourceforge.net/

Good luck!

oxygen
  • 5,891
  • 6
  • 37
  • 69
Vlad Balmos
  • 3,372
  • 19
  • 34
  • 1
    node.js is server side. The server side javascript module cannot be reused on the browser side, because it depends on OpenSSL. – oxygen Sep 17 '12 at 10:49
  • pidCrypt offers only symmetric encryption or hashing. My question is about public-key encryption. – oxygen Sep 17 '12 at 11:32
  • the link I posted points to the pidCrypt asymetric rsa. How is that symetric? – Vlad Balmos Sep 17 '12 at 11:36
  • Sorry, I didn't read the RSA part in the supported methods list. Checking it out now. Thanks. – oxygen Sep 17 '12 at 11:40
  • The RSA implementation in pidCrypt is based on http://www-cs-students.stanford.edu/~tjw/jsbn/, same as answer as RezaSh. – oxygen Sep 17 '12 at 11:43
  • Please edit your answer to refer to pidcrypt as a solution for Javascript side. Also add http://phpseclib.sourceforge.net/ for the PHP side in your answer. I would like to accept your answer (pidCrypt is better than jsbn at lease because it is namespaced). – oxygen Sep 17 '12 at 12:41
  • Another exceptionally easy to use crypto library written by experts: http://crypto.stanford.edu/sjcl/ – Joe G Sep 21 '12 at 07:27
  • 1
    Be careful with RSA, even with PHPSecLib. – Scott Arciszewski Dec 03 '15 at 05:19
16

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...

  1. 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).
  2. Add libsodium.js to your project.
  3. Use crypto_box_seal() with a public key to encrypt your messages, client-side.
  4. 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

  1. EasyRSA generates a random 128-bit key for symmetric key cryptography (via AES).
  2. Your plaintext message is encrypted with defuse/php-encryption.
  3. Your AES key is encrypted with RSA, provided by phpseclib, using the correct mode (mentioned above).
  4. 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
);
Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
  • Where is the documentation for libsodium.js? Nobody is going to use it if it's not documented, even if it is the superior option. – RedShift May 27 '19 at 19:15
  • @RedShift https://github.com/jedisct1/libsodium.js#usage-as-a-module + https://libsodium.gitbook.io/doc/ – Scott Arciszewski May 28 '19 at 19:23
  • @RedShift In the months since, I've created a safer option which is documented [here](https://github.com/paragonie/sodium-plus/tree/master/docs). – Scott Arciszewski Oct 22 '19 at 05:10
  • @ScottArciszewski Using the same key in both frontend and backend is safer? Where is the private key here? – Rajkumar R May 31 '21 at 04:58
1

RSA example usage for pidCrypt (js) and phpseclib (php).

Do not reuse the private key in this working example.

pidCrypt encryption

//From the pidCrypt example sandbox
function certParser(cert) {
    var lines = cert.split('\n');
    var read = false;
    var b64 = false;
    var end = false;
    var flag = '';
    var retObj = {
    };
    retObj.info = '';
    retObj.salt = '';
    retObj.iv;
    retObj.b64 = '';
    retObj.aes = false;
    retObj.mode = '';
    retObj.bits = 0;
    for (var i = 0; i < lines.length; i++) {
        flag = lines[i].substr(0, 9);
        if (i == 1 && flag != 'Proc-Type' && flag.indexOf('M') == 0)//unencrypted cert?
        b64 = true;
        switch (flag) {
            case '-----BEGI':
                read = true;
                break;
            case 'Proc-Type':
                if (read)retObj.info = lines[i];
                break;
            case 'DEK-Info:':
                if (read) {
                    var tmp = lines[i].split(',');
                    var dek = tmp[0].split(': ');
                    var aes = dek[1].split('-');
                    retObj.aes = (aes[0] == 'AES') ? true : false;
                    retObj.mode = aes[2];
                    retObj.bits = parseInt(aes[1]);
                    retObj.salt = tmp[1].substr(0, 16);
                    retObj.iv = tmp[1];
                }
                break;
            case '':
                if (read)b64 = true;
                break;
            case '-----END ':
                if (read) {
                    b64 = false;
                    read = false;
                }
                break;
                default : if (read && b64)retObj.b64 += pidCryptUtil.stripLineFeeds(lines[i]);
        }
    }
    return retObj;
}

var strCreditCardPublicKey="-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC\/tI7cw+gnUPK2LqWp50XboJ1i\njrLDn+4\/gPOe+pB5kz4VJX2KWwg9iYMG9UJ1M+AeN33qT7xt9ob2dxgtTh7Mug2S\nn1TLz4donuIzxCmW+SZdU1Y+WNDINds194hWsAVhMC1ClMQTfldUGzQnI5sXvZTF\nJWp\/9jheCNLDRIkAnQIDAQAB\n-----END PUBLIC KEY-----\n";

var objParams=certParser(strCreditCardPublicKey);
var binaryPrivateKey=pidCryptUtil.decodeBase64(objParams.b64);

var rsa=new pidCrypt.RSA();

var asn=pidCrypt.ASN1.decode(pidCryptUtil.toByteArray(key));
var tree=asn.toHexTree();
rsa.setPublicKeyFromASN(tree);

var strHexSensitiveDataEncrypted=rsa.encrypt("4111111111111111");

var strBase64SensitiveDataEncrypted=pidCryptUtil.fragment(pidCryptUtil.encodeBase64(pidCryptUtil.convertFromHex(strHexSensitiveDataEncrypted)), 64))

console.log(strBase64SensitiveDataEncrypted);

.

phpseclib decryption

require_once("Crypt/RSA.php");

function decrypt($strBase64CipherText)
{
    //CRYPT_RSA_MODE_INTERNAL is slow
    //CRYPT_RSA_MODE_OPENSSL is fast, but requires openssl to be installed, configured and accessible.
    define("CRYPT_RSA_MODE", CRYPT_RSA_MODE_INTERNAL);

    $rsa=new Crypt_RSA();


    //$strPrivateKey=file_get_contents("private.pem");
    //This private key is for example purposes
    //DO NOT REUSE
    $strPrivateKey="-----BEGIN RSA PRIVATE KEY-----
        MIICXQIBAAKBgQDBNHK7R2CCYGqljipbPoj3Pwyz4cF4bL5rsm1t8S30gbEbMnKn
        1gpzteoPlKp7qp0TnsgKab13Fo1d+Yy8u3m7JUd/sBrUa9knY6dpreZ9VTNul8Bs
        p2LNnAXOIA5xwT10PU4uoWOo1v/wn8eMeBS7QsDFOzIm+dptHYorB3DOUQIDAQAB
        AoGBAKgwGyxy702v10b1omO55YuupEU3Yq+NopqoQeCyUnoGKIHvgaYfiwu9sdsM
        ZPiwxnqc/7Eo6Zlw1XGYWu61GTrOC8MqJKswJvzZ0LrO3oEb8IYRaPxvuRn3rrUz
        K7WnPJyQ2FPL+/D81NK6SH1eHZjemb1jV9d8uGb7ifvha5j9AkEA+4/dZV+dZebL
        dRKtyHLfbXaUhJcNmM+04hqN1DUhdLAfnFthoiSDw3i1EFixvPSiBfwuWC6h9mtL
        CeKgySaOkwJBAMSdBhn3C8NHhsJA8ihQbsPa6DyeZN+oitiU33HfuggO3SVIBN/7
        HmnuLibqdxpnDOtJT+9A+1D29TkNENlTWgsCQGjVIC8xtFcV4e2s1gz1ihSE2QmU
        JU9sJ3YeGMK5TXLiPpobHsnCK8LW16WzQIZ879RMrkeDT21wcvnwno6U6c8CQQCl
        dsiVvXUmyOE+Rc4F43r0VRwxN9QI7hy7nL5XZUN4WJoAMBX6Maos2Af7NEM78xHK
        SY59+aAHSW6irr5JR351AkBA+o7OZzHIhvJfaZLUSwTPsRhkdE9mx44rEjXoJsaT
        e8DYZKr84Cbm+OSmlApt/4d6M4YA581Os1eC8kopewpy
        -----END RSA PRIVATE KEY-----
    ";
    $strPrivateKey=preg_replace("/[ \t]/", "", $strPrivateKey);//this won't be necessary when loading from PEM


    $rsa->loadKey($strPrivateKey);

    $binaryCiphertext=base64_decode($strBase64CipherText);

    $rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1);
    $strBase64DecryptedData=$rsa->decrypt($binaryCiphertext);

    return base64_decode($strBase64DecryptedData);
}

//The pidCrypt example implementation will output a base64 string of an encrypted base64 string which contains the original data, like this one:
$strBase64CipherText="JDlK7L/nGodDJodhCj4uMw0/LW329HhO2EvxNXNUuhe+C/PFcJBE7Gp5GWZ835fNekJDbotsUFpLvP187AFAcNEfP7VAH1xLhhlB2a9Uj/z4Hulr4E2EPs6XgvmLBS3MwiHALX2fES5hSKY/sfSUssRH10nBHHO9wBLHw5mRaeg=";

$binaryDecrypted=decrypt($strBase64CipherText);

//should output '4111111111111111'
var_export($binaryDecrypted);
oxygen
  • 5,891
  • 6
  • 37
  • 69
0

This is based on the Tiny Encryption Algorithm, which is a symmetric (private key) encryption system. It may nevertheless be of use to you because of its light weight.

This is now at: http://babelfish.nl/Projecten/JavascriptPhpEncryption

Pum Walters
  • 339
  • 1
  • 3
  • 11
  • The website seems a bit outdated...and as you said...the algo is symmetrical (which means logged data is easy to break, if the key was logged also). I'm not even sure that's cross browser or if it works with UTF-8 and byte arrays. Thanks anyways :) – oxygen Sep 24 '12 at 20:21
  • Thanks Tony. I updated the site but didn't notice this. This stuff is now at http://babelfish.nl/Projecten/JavascriptPhpEncryption – Pum Walters Nov 14 '15 at 14:03