Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to load a public key in PEM format for encryption?

Until now I used JSEncrypt which is able to load a public key from a PEM formatted string. And then use it with RSA in order to encrypt a string. For example :

<textarea id="pubkey">-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+iOltdDtDdUq6u67L2Mb4HW5j
7E1scmYtg2mnnQD85LxFICZv3I3rQ4wMulfcH+n9VCrifdu4vN89lRLKgsb9Kzim
GUrbOWEZdKZ9D5Sfo90EXocM5NtHou14aN8xkRWbN7x/RK5o9jfJwKmrC1fCm6tx
2Qwvx5kypWQUN6UpCQIDAQAB
-----END PUBLIC KEY-----
</textarea>

and then:

var encrypt = new JSEncrypt();
encrypt.setPublicKey($('#pubkey').val());

I'd like to do the same with WebCrypto but I don't understand how to do. I've tried the following steps:

  1. Remove the PEM header
  2. Remove the PEM footer
  3. Remove CR/LF
  4. Trim string
  5. Decode the Base64 string
  6. Convert the result to an ArrayBuffer

Then I tried to import the key:

cryptoSubtle.importKey("spki", publicKey, {name: "RSA-OAEP", hash: {name: "SHA-256"}}, false, ["encrypt"]);

I tried many ways (unpack the ASN/DER format, etc.) But I get various errors (DOMException data, etc.). I don't know if the PEM format is acceptable as a supported format or if I must convert the key in JSON Web Key format, etc.

Is there a simple way to do it without a 3rd-party JS library ?

like image 748
Benjamin BALET Avatar asked Jan 15 '16 15:01

Benjamin BALET


People also ask

Does PEM include public key?

The PEM file supplied to the Hybrid Data Pipeline server must include the SSL certificate private and public keys, any intermediate certificates, and the root certificate. A PEM encoded file includes Base64 data.

What is PEM encoded public key?

Privacy Enhanced Mail (PEM) files are a type of Public Key Infrastructure (PKI) file used for keys and certificates. PEM, initially invented to make e-mail secure, is now an Internet security standard.


1 Answers

I've found an answer after some tests. In my case, I used JSEncrypt with PHP/openssl or phpseclib as a fallback.

With JSEncrypt, you can't choose the encryption algorithm. And this has an impact to the padding used when PHP deciphers the encrypted value. JSEncrypt uses:

  • RSASSA-PKCS1-v1_5
  • SHA-1 as a hash method

If you want to decipher a message, you have to use the default padding option:

openssl_private_decrypt(base64_decode($_POST['CipheredValue']), $ouput, $privateKey, OPENSSL_PKCS1_PADDING);

But WebCrypto is not compatible with JSEncrypt (we cannot decrypt the message with PHP with the same options), because:

  • WebCrypto can use SHA-1 as a hash method, even if it is not recommended.
  • But WebCrypto forbids you to use RSASSA-PKCS1-v1_5 for encryption purpose (it is only allowed for signing purposes). You should use RSA-OAEP instead.

If you try to decode the encrypted value with the default options, you'll get this message:

RSA_EAY_PRIVATE_DECRYPT:padding check failed

So, you have to change the padding option as follow (in PHP):

openssl_private_decrypt(base64_decode($_POST['CipheredValue']), $ouput, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);

Regarding my original question, yes you can import a key in PEM format if you follow the steps I've mentionned in the post

  1. Remove the PEM header
  2. Remove thePEM footer
  3. Remove CR/LF
  4. Trim string
  5. Decode the Base64 string
  6. Convert the result to an ArrayBuffer

Complete code:

var crypto = window.crypto || window.msCrypto;
var encryptAlgorithm = {
  name: "RSA-OAEP",
  hash: {
    name: "SHA-1"
  }
};

function arrayBufferToBase64String(arrayBuffer) {
  var byteArray = new Uint8Array(arrayBuffer)
  var byteString = '';
  for (var i=0; i<byteArray.byteLength; i++) {
    byteString += String.fromCharCode(byteArray[i]);
  }
  return btoa(byteString);
}

function base64StringToArrayBuffer(b64str) {
  var byteStr = atob(b64str);
  var bytes = new Uint8Array(byteStr.length);
  for (var i = 0; i < byteStr.length; i++) {
    bytes[i] = byteStr.charCodeAt(i);
  }
  return bytes.buffer;
}

function textToArrayBuffer(str) {
  var buf = unescape(encodeURIComponent(str)); // 2 bytes for each char
  var bufView = new Uint8Array(buf.length);
  for (var i=0; i < buf.length; i++) {
    bufView[i] = buf.charCodeAt(i);
  }
  return bufView;
}

function convertPemToBinary(pem) {
  var lines = pem.split('\n');
  var encoded = '';
  for(var i = 0;i < lines.length;i++){
    if (lines[i].trim().length > 0 &&
        lines[i].indexOf('-BEGIN RSA PRIVATE KEY-') < 0 && 
        lines[i].indexOf('-BEGIN RSA PUBLIC KEY-') < 0 &&
        lines[i].indexOf('-BEGIN PUBLIC KEY-') < 0 &&
        lines[i].indexOf('-END PUBLIC KEY-') < 0 &&
        lines[i].indexOf('-END RSA PRIVATE KEY-') < 0 &&
        lines[i].indexOf('-END RSA PUBLIC KEY-') < 0) {
      encoded += lines[i].trim();
    }
  }
  return base64StringToArrayBuffer(encoded);
}

function importPublicKey(pemKey) {
  return new Promise(function(resolve) {
    var importer = crypto.subtle.importKey("spki", convertPemToBinary(pemKey), encryptAlgorithm, false, ["encrypt"]);
    importer.then(function(key) { 
      resolve(key);
    });
  });
}


if (crypto.subtle) {

      start = new Date().getTime();
      importPublicKey($('#pubkey').val()).then(function(key) {
        crypto.subtle.encrypt(encryptAlgorithm, key, textToArrayBuffer($('#txtClear').val())).then(function(cipheredData) {
            cipheredValue = arrayBufferToBase64String(cipheredData);
            console.log(cipheredValue);

        });
      });
}
like image 71
Benjamin BALET Avatar answered Sep 19 '22 08:09

Benjamin BALET