user700694
user700694

Reputation: 23

RSA Encryption, Encrypt in PHP (phpseclib) and Decrypt in JavaScript (crypto.subtle)

What I'm trying to do is generate a keypair within javascript, and use these with encrypting in PHP and then decrypt with JS.

I'm having two problems within the code attached

  1. It wont reload the private key from armored text block
  2. And it wont decrypt what PHP has encrypted

Both throw the error DOMException, instead of a useful error.

So this is my code...

PHP/JAVASCRIPT

<?php
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
if ($_POST) {
    $public=$_POST['public'];
    $data='some text to encrypt';
    $key = PublicKeyLoader::load($public);
    $key = $key->withPadding(RSA::ENCRYPTION_OAEP);
    $encoded=base64_encode($key->encrypt($data));
    header('Content-Type: application/json');
    echo json_encode(array('encrypted'=>$encoded));
    exit;
}
?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"></script>
<script>
jQuery(document).ready(function($) {
    function ab2str(buf) {
        return String.fromCharCode.apply(null, new Uint8Array(buf));
    }
    function str2ab(str) {
        var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
        var bufView = new Uint16Array(buf);
        for (var i=0, strLen=str.length; i < strLen; i++) {
            bufView[i] = str.charCodeAt(i);
        }
        return buf;
    }
    function importPrivateKey(pem) {
        // fetch the part of the PEM string between header and footer
        const pemHeader = "-----BEGIN PRIVATE KEY-----\n";
        const pemFooter = "\n-----END PRIVATE KEY-----";
        const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length);
        // base64 decode the string to get the binary data
        const binaryDerString = window.atob(pemContents);
        // convert from a binary string to an ArrayBuffer
        const binaryDer = str2ab(binaryDerString);

        return window.crypto.subtle.importKey(
            "pkcs8",
            binaryDer,
            {
                name: "RSA-OAEP",
                modulusLength: 1024,
                publicExponent: new Uint8Array([1, 0, 1]),
                hash: {name: "SHA-256"}
            },
            true,
            ["decrypt"]
          );
    }
    (async() => {
        let keyPair = await window.crypto.subtle.generateKey(
          {
                name: "RSA-OAEP",
                modulusLength: 1024,
                publicExponent: new Uint8Array([1, 0, 1]),
                hash: {name: "SHA-256"}
          },
          true,
          ["encrypt", "decrypt"]
        );
        
        var exported=await window.crypto.subtle.exportKey("pkcs8",keyPair.privateKey);
        var exportedAsString = ab2str(exported);
        var exportedAsBase64 = window.btoa(exportedAsString);
        var private = `-----BEGIN PRIVATE KEY-----\n${exportedAsBase64}\n-----END PRIVATE KEY-----`;
        
        var exported = await window.crypto.subtle.exportKey(
            "spki",
            keyPair.publicKey
        );
        var exportedAsString = ab2str(exported);
        var exportedAsBase64 = window.btoa(exportedAsString);
        var public = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`;

        console.log(public);
        console.log(private);
        
        $.ajax({
            url:window.location,
            type:'POST',
            data:{
                public:public
            },
            success:function(data) {
                (async() => {
                    console.log('*ENCRYPTED BY PHP*',data.encrypted);
                    // HELP!!! NEED TO BE ABLE TO RELOAD THE KEY FROM ARMORED STRING
                    var key=await importPrivateKey(private); // Error - Uncaught (in promise) DOMException
                    var buffer=str2ab(window.atob(data.encrypted));
                    // HELP!!! WONT DECRYPT WHAT PHP ENCODED USING THE PUBLIC KEY
                    var decrypted=await window.crypto.subtle.decrypt({name:"RSA-OAEP"},key,buffer);
                    
                    console.log('DECRYPTED',decrypted);
                })();
            }
        });
    })();
});
</script>

Upvotes: 2

Views: 1492

Answers (1)

Topaco
Topaco

Reputation: 49251

The bug is in the str2ab() function, which uses a Uint16Array instead of a Uint8Array.

If this is fixed, the private key can be imported and the ciphertext generated with the PHP code can be decrypted:

function ab2str(buf) {
    return String.fromCharCode.apply(null, new Uint8Array(buf));
}

function str2ab(str) {
    // Fix: Don't double the size 
    var buf = new ArrayBuffer(str.length);                  
    // Fix: Apply a Uint8Array!
    var bufView = new Uint8Array(buf);                      
    for (var i=0, strLen=str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

function importPrivateKey(pem) {

    const pemHeader = "-----BEGIN PRIVATE KEY-----\n";
    const pemFooter = "\n-----END PRIVATE KEY-----";
    const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length);

    const binaryDerString = window.atob(pemContents);
    const binaryDer = str2ab(binaryDerString);

    return window.crypto.subtle.importKey(
        "pkcs8",
        binaryDer,
        {
            name: "RSA-OAEP",
            modulusLength: 1024,
            publicExponent: new Uint8Array([1, 0, 1]),
            hash: {name: "SHA-256"}
        },
        true,
        ["decrypt"]
    );
}

(async function() {

    // Apply the private key from key pair generated with the posted JavaScript code
    var privateKey = 
    `-----BEGIN PRIVATE KEY-----                     
        MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKeQUdBu3zTX6QyfGfRWYxWWOnxd2xssTOIu6XczDByQEMfBbpQO9iM3u/Mn84zZFPFNvOKUNxcnftmrPiqUO9fBI2aAh77d2m65FBGsm4k/oUPzMNORGaDdBY4gg8FPMKo60kqBaMXAwzF8I4EUS/ot2fkBzSL0BGXT9o1NaO8bAgMBAAECgYAO2OPW8ywF86ervaFAHDN1YzVVdb+HXdqGJB/9tuE42q8R9BrHNbgrkLGvrveOoGGRrBCzhuyGubIsuVat0SqoI6qEnB9uahaIBfF5FZ7+bNW5OfkgerUUYP1S1MGFxUqINnUY1YHITmo6pUKHsiJtP7sihnCT6uEx8LqVNf1quQJBANs+VCZVUDq6eMy3E/u03HiAB8cyqLVMVQ4cLyoiWmFlnEFzZwMd20ZMjtcxICiizW3dlDvyxWYKH93irL0JyM0CQQDDp/VFsh83vKICVvM9IZHwE/Z8vZA3eTkGbWmgnr6qaxqge3FU02kUvIHHlvLmXYIt30lTq0Rn+Lz+TGV/jDeHAkBHYSaSiGojhLx5og1+gKbbEIv3vbWRuTVj76cnZ6HXXfaelIzwRdMzMw+6XgMjV8XcRCzTy7ma/Cbd3cPxk/LtAkEAwkehMVexz/KrHI+icG1JMI9iDnNdJPhmO4+hdzCqOyanBfwNiSF0Encslze4ci8f+NTjRwWlo2hGomzRzFk7OQJAPPd/o0azkg9nF+JxLiz7hF+/6MLVZgIfw04u05ANtOSVVQP4UTmJ/tNAe3OBUQVlRQAJ1m3jzUlir0ACPypC1Q==
    -----END PRIVATE KEY-----`;

    // Use the ciphertext generated with the PHP code
    var ciphertext = 'a8gEZ6/DymB8dTGPytQPNS8QiYFuUULK+/c0vtie1l722isC0Z/jSeC2ytA6MjVUuTdq7sPuNW850gEZ2XvKujLQzl9sjJ8pcsxznBzMK8v03YJCTBr2lbfHpEEtuSLaAR2UbovXDoCyIIvOnMjqlIS3Ug2PG4hALThn/aAUwE0=';

    var key = await importPrivateKey(privateKey);
    var decryptedBuffer = str2ab(window.atob(ciphertext));
    var decrypted = await window.crypto.subtle.decrypt(
        {name:"RSA-OAEP"},
        key,
        decryptedBuffer
    );
    
    console.log(ab2str(decrypted)); // some text to encrypt
})();

Here, the posted JavaScript code was applied to generate an RSA key pair. The public key:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCnkFHQbt801+kMnxn0VmMVljp8XdsbLEziLul3MwwckBDHwW6UDvYjN7vzJ/OM2RTxTbzilDcXJ37Zqz4qlDvXwSNmgIe+3dpuuRQRrJuJP6FD8zDTkRmg3QWOIIPBTzCqOtJKgWjFwMMxfCOBFEv6Ldn5Ac0i9ARl0/aNTWjvGwIDAQAB
-----END PUBLIC KEY-----

was used to perform the encryption with the posted PHP code and the private key is applied in the code above for decryption.

Upvotes: 2

Related Questions