最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to load a public key in PEM format for encryption? - Stack Overflow

programmeradmin6浏览0评论

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 ?

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 ?

Share Improve this question edited Jan 25, 2016 at 15:26 user177800 asked Jan 15, 2016 at 15:30 Benjamin BALETBenjamin BALET 9691 gold badge11 silver badges36 bronze badges 2
  • Shouldn't you Base64-decode the PEM data first? – robertklep Commented Jan 15, 2016 at 15:36
  • If I decode the data and then convert it to an ArrayBuffer I get a DOMException – Benjamin BALET Commented Jan 15, 2016 at 15:39
Add a ment  | 

2 Answers 2

Reset to default 13

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 patible 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 remended.
  • 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);

        });
      });
}

First choose your favourite Base64-to-ArrayBuffer and String-to-ArrayBuffer methods, e.g.

function b64ToArrayBuffer(b64) {
    return new Promise((res, rej) => {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', 'data:application/octet-stream;base64,' + b64);
        xhr.responseType = 'arraybuffer';
        xhr.addEventListener('load', e => res(xhr.response));
        xhr.addEventListener('error', e => rej(xhr));
        xhr.send();
    });
}

function stringToArrayBuffer(str) {
    return new Promise((res, rej) => {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', 'data:text/plain,' + str);
        xhr.responseType = 'arraybuffer';
        xhr.addEventListener('load', e => res(xhr.response));
        xhr.addEventListener('error', e => rej(xhr));
        xhr.send();
    });
}

(There is a chance these will give Uncaught (in promise) DOMException: Only secure origins are allowed (see: https://goo.gl/Y0ZkNV).)

Then you can write an encrypt function in the Promise style

function encrypt(b64_key, clear_text) {
    return b64ToArrayBuffer(b64_key)
        .then(buffer => window.crypto.subtle.importKey("spki", buffer, {name: "RSA-OAEP", hash: {name: "SHA-256"}}, false, ["encrypt"]))
        .then(key => new Promise((res, rej) => stringToArrayBuffer(clear_text).then(buffer => res({key, buffer}))))
        .then(data => window.crypto.subtle.encrypt({name: "RSA-OAEP", hash: {name: "SHA-256"}}, data.key, data.buffer));
}

And finally, using it

encrypt('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+iOltdDtDdUq6u67L2Mb4HW5j\
7E1scmYtg2mnnQD85LxFICZv3I3rQ4wMulfcH+n9VCrifdu4vN89lRLKgsb9Kzim\
GUrbOWEZdKZ9D5Sfo90EXocM5NtHou14aN8xkRWbN7x/RK5o9jfJwKmrC1fCm6tx\
2Qwvx5kypWQUN6UpCQIDAQAB', 'Hello World')
    .then(result => console.log(String.fromCharCode.apply(null, new Uint16Array(result))));
// 䍞鸵즱ය㥬ᬍ㖆淓䛿⫵�ɪꤿᮌ怀跰届쇎偌诔락曶락ه͌쥻쨋沶碅姮갣ꤠ퉥�ﮕ컙郞ꦨꉣ茱닦ꥋ༈쿵⇲蟌赅龙Ⲯ偼幱䋚⫛Ɂౖ勍
发布评论

评论列表(0)

  1. 暂无评论