javascriptphpencryptionphp-openssl

Encrypt on PHP and Decrypt on Javascript


Recently I am trying to find a way to encrypt data on PHP with a Public Key generated on javascript using window.crypto.subtle.generateKey()

async function GenerateKeys() {
    let key = await window.crypto.subtle.generateKey(
        {
            name: "RSA-OAEP",
            modulusLength: 4096,
            publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
            hash: {name: "SHA-256"}
        },
        true,
        ["encrypt", "decrypt"]
    )

    let pvkey = await window.crypto.subtle.exportKey(
        "pkcs8",
        key.privateKey
    )

    let pbkey = await window.crypto.subtle.exportKey(
        "spki",
        key.publicKey
    )

    let pemPvKey = `-----BEGIN PRIVATE KEY-----\n${window.btoa(ab2str(pvkey))}\n-----END PRIVATE KEY-----`;
    let pemPbKey = `-----BEGIN PUBLIC KEY-----\n${window.btoa(ab2str(pbkey))}\n-----END PUBLIC KEY-----`;

    return [pemPvKey, pemPbKey]
}

I then send the public key to my PHP script and generate data

$pbkey = "-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA52N7Q/kQxNrVaGCLOlD0IgrSROPWt029GTqRKMdFQSFMAZPD0V5TZPLDylbtmvLhDajKugYpjHfDGD44cXiYy1jZeVCiFas09gAGBKmgFY4Ixsl/+opF2hlPjvnugn2aPKpSLgoX9f1DpXBDEpuHJ+AVSTxL+C3uE1PgQPYy2ots15Km4W2AnV+p81UfLdafStQ40gbkUOHvzFkwizazm4q1Scjh2Fc+RR6FXy+ySp54yuRqHuMS8QTdXVMBChqs5lNwuu6V+BEryXMFbDx2fW9qCWShVTc2lq4KzvXRE/65L0sttMA7oP/WzsVW3zeNFgla8g4dxvCftwrwGviMay9N+vsa902TClR2xj7/JSzQXSW0E4Am4ZB4bCJh04f08M0nMBY6yWWV3+QpBG+9CeXHfwPrdqT2OR5N7Jq+xslCfbf4D/9itjDdvaBZP5Z6BYiOF3QzeTk+V2rfAWZdOUcuWAdP3gczQKdJM11bTxOKNCiYmBu773aDXCNFpaHDatqUgnNrVp7guOP1owOdd7qxWsplzylQUcWuhuWczs7/X/UPwT7vBTkg0pb8Ujrr+KyNmsxsJTefg1z4xcbxtgqgoDRxXu92V9iVl1HVssAM8IZdc+naEIyOjt+3OPZkaCwts38U7nYUDd96ovHjMs0hFlTxqsltwyfPFAVygLUCAwEAAQ==
-----END PUBLIC KEY-----";

openssl_public_encrypt("secret", $encrypted, $pbkey, OPENSSL_PKCS1_OAEP_PADDING );

echo base64_encode($encrypted);

Finally, I use the generated cipher on javascript by using decrypt function (the key is stored in localstorage btw in pkcs8 format)

function FormatPrivateKey(pemPvKey) {
    return pemPvKey.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/[\r\n]/gm, "");

}

function GetKeys() {
    const pvkey = localStorage.getItem("pvkey");
    const pbkey = localStorage.getItem("pbkey");

    return [pvkey, pbkey]
}

async function Decrypt(message) {
    const keys = GetKeys();
    let pemPvKey = keys[0];

    const pvkey = await window.crypto.subtle.importKey(
        "pkcs8",
        str2ab(window.atob(FormatPrivateKey(pemPvKey))),
        {
            name: "RSA-OAEP",
            hash: {name: "SHA-256"}
        },
        false,
        ["decrypt"]
    );

    return await window.crypto.subtle.decrypt(
        {
            name: "RSA-OAEP"
        },
        pvkey,
        str2ab(window.atob(message))
    );
}

When I try to decrypt the data, I receive this error: Uncaught DOMException: The operation failed for an operation-specific reason

I do not know why this happens.


Solution

  • PHP/OpenSSL only supports OAEP with SHA1 for both digests, see here.

    So to decrypt the ciphertext generated with PHP OpenSSL, SHA-1 must be specified in Decrypt() or more precisely in importKey() instead of SHA-256.

    Consequently, SHA-1 should also be specified in the key generation. However, this is not mandatory because of the export/reimport. It would be necessary if the generated private key was used directly (i.e. without reimport) for decryption, which is not the case in this scenario.

    In the example below, I generated the keys with the unmodified JavaScript code and the ciphertext with the PHP code. The decryption is successful when using the described bugfix.

    (async () => {
    
    async function Decrypt(message) {
        const keys = GetKeys();
        let pemPvKey = keys[0];
        const pvkey = await window.crypto.subtle.importKey(
            "pkcs8",
            str2ab(window.atob(FormatPrivateKey(pemPvKey))),
            {
                name: "RSA-OAEP",
                hash: {name: "SHA-1"} // Fix!
            },
            false,
            ["decrypt"]
        );
    
        return await window.crypto.subtle.decrypt(
            {
                name: "RSA-OAEP"
            },
            pvkey,
            str2ab(window.atob(message))
        );
    }
    
    function GetKeys() {
        const pvkey = `-----BEGIN PRIVATE KEY-----
    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
    -----END PRIVATE KEY-----`;//localStorage.getItem("pvkey");
        const pbkey = "";//localStorage.getItem("pbkey");
    
        return [pvkey, pbkey]
    }
    
    function FormatPrivateKey(pemPvKey) {
        return pemPvKey.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/[\r\n]/gm, "");
    
    }
    
    // https://stackoverflow.com/a/11058858
    function str2ab(str) {
        const buf = new ArrayBuffer(str.length);
        const bufView = new Uint8Array(buf);
        for (let i = 0, strLen = str.length; i < strLen; i++) {
                bufView[i] = str.charCodeAt(i);
        }
        return buf;
    }
        
    function ab2str(buf) {
        return String.fromCharCode.apply(null, new Uint8Array(buf));
    }
    
    var ciphertext = "Oc2XZkps7x345NtfOjFLQZPIu3TPJ6Lmws8RYdCC42RlSnI3MMUm5RNSdXV3uzDpusOYXH7kZAndbfqaak5t3hC8W7P+RWlUcDXgKrlqr3RfQmHZYKwZG3wfbgPmJMPS3+FHNG19fKISUAntCvkF625UdvPL5PPSymlDQf3VrD+jjeTXephuBpTEENwZg5lPuV2mI+QvhFzq2qaG5ij7UHCBfkTKRmWfXTS1gUUnYf1zzsWXX6THVV3PS4V6cKrDYm/hLVnU0giL2pdLRWWlyrrnPTWbSVlYSla1iheOdg+QCt9ujCkQ73aJA7nxIB711XYWxkm9VNq6sauzvaTk3QyJAabKwK8jQn0DBuvbgiaHoOhpCwOOqJWb10B7qnqxzj9u+ufmdZkWtVS++IibXXbZy2nN0P/DCT43RJHrQsUxDNfLqXp/ZnTQEjyHvGMvk6c5yL2KA3Q94wDBSiukBp8Y8Gtcuxmse+EjTbi2QE/eNqBjovxtLuU2GqjgTAgfEmgvncpa4W8cq71Der0khVopg67tIUgPhAZXJKCzz+C1Rw/L40vED7+2g5OYhMid/nyj5oFn3BwQ52feZVq2KdR14feH2bHnJnLYHTRVYBSU42LDTA+dNO8sbQ2lJyF24+y+rHx1X80Xz8YiSfJWnW4D4hpDjK7cRO9WLWQhYaE=";
    console.log(ab2str(await Decrypt(ciphertext)));
    
    })();


    To use OAEP with SHA-256 on the PHP side phpseclib is an option.