Peter Bielak Peter Bielak - 4 months ago 67
Javascript Question

CryptoKey ArrayBuffer to base64 and Back

I was wondering how do I solve this problem. I generate RSA-OAEP keypair using WebCrypto API, then I export private key in pkcs8 from the keypair which exports as ArrayBuffer and I want to encode this ArrayBuffer into base64 so I can store it as a PEM.

In this testing example I am exporting key as pkcs8 and importing this pkcs8 back to CryptoKey. The problem is that sometimes it works and sometimes it does not.

These are results of the code:
NOTE: Only happens one of these states not all at once.
NOTE2: This example does not contain -----BEGIN PRIVATE KEY----- prefix and suffix I am only encoding the key.

Case1: Uncaught (in promise) URIError: URI malformed(…)b64DecodeUnicode @ try.php:20b64toab @ try.php:70wayBack @ try.php:66(anonymous function) @ try.php:56

Case2: undefined:1 Uncaught (in promise) DOMException

Case3: OK - works all the way back.

I don't know what causes the errors but I think it has something to do with base64 encoding. As I said sometimes private key generates OK and sometimes not.

Thank you very much for every help in advance.

function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}

function b64DecodeUnicode(str) {
return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}

function addNewLines(str) {
var finalString = '';
for(var i=0; i < str.length; i++) {
finalString += str.substring(0, 64) + '\n';
str = str.substring(64);
}

return finalString;
}

window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"}
},
true,
["encrypt", "decrypt"]
).then(function(keyPair) {
window.crypto.subtle.exportKey(
"pkcs8",
keyPair.privateKey
).then(function(exportedPrivateKey) {
var byteArray = new Uint8Array(exportedPrivateKey);
console.log(byteArray);
var byteString = '';
for(var i=0; i < byteArray.byteLength; i++) {
byteString += String.fromCodePoint(byteArray[i]);
}

wayBack(addNewLines(b64EncodeUnicode(byteString)));
});
});

function wayBack(pem) {
var lines = pem.split('\n');
var encodedString = '';
for(var i=0; i < lines.length; i++) {
encodedString += lines[i].trim();
}
b64toab(encodedString);
}

function b64toab(b64) {
var byteString = b64DecodeUnicode(b64);
console.log(byteString);
var byteArray = new Uint8Array(byteString.length);
for(var i=0; i < byteString.length; i++) {
byteArray[i] = byteString.codePointAt(i);
}
console.log(byteArray);

window.crypto.subtle.importKey(
"pkcs8",
byteArray,
{
name: "RSA-OAEP",
hash: {name: "SHA-256"}
},
true,
["decrypt"]
).then(function(importedPrivateKey) {
console.log(importedPrivateKey);
});
}

Answer

You forgot to include the last part of PEM when you split the string in blocks of 64 characters. Just add finalString += str; to addNewLines

function addNewLines(str) {
    var finalString = '';
    for(var i=0; i < str.length; i++) {
        finalString += str.substring(0, 64) + '\n';
        str = str.substring(64);
    }
    finalString += str;

    return finalString;
}

I have refactorized your example to see what is happening. Use the below code if you consider it useful

function b64EncodeUnicode(str) {
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
        return String.fromCharCode('0x' + p1);
    }));
}

function b64DecodeUnicode(str) {
    return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
}

function addNewLines(str) {
    var finalString = '';
    for(var i=0; i < str.length; i++) {
        finalString += str.substring(0, 64) + '\n';
        str = str.substring(64);
    }
    finalString += str;

    return finalString;
}

function removeLines(pem) {
    var lines = pem.split('\n');
    var encodedString = '';
    for(var i=0; i < lines.length; i++) {
        encodedString += lines[i].trim();
    }
    return encodedString;
}

function stringToArrayBuffer(byteString){
    var byteArray = new Uint8Array(byteString.length);
    for(var i=0; i < byteString.length; i++) {
        byteArray[i] = byteString.codePointAt(i);
    }
    return byteArray;
}

function  arrayBufferToString(exportedPrivateKey){
    var byteArray = new Uint8Array(exportedPrivateKey);
    var byteString = '';
    for(var i=0; i < byteArray.byteLength; i++) {
        byteString += String.fromCodePoint(byteArray[i]);
    }
    return byteString;
}

window.crypto.subtle.generateKey(
    {
        name: "RSA-OAEP",
        modulusLength: 2048,
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: {name: "SHA-256"}
    },
    true,
    ["encrypt", "decrypt"]
).then(function(keyPair) {
    window.crypto.subtle.exportKey(
        "pkcs8",
        keyPair.privateKey
    ).then(function(exportedPrivateKey) {

        var privateKeyDer = arrayBufferToString(exportedPrivateKey); //pkcs#8 to DER
        var privateKeyB64 = b64EncodeUnicode(privateKeyDer); //btoa(privateKeyDer);
        var privateKeyPEMwithLines = addNewLines(privateKeyB64);  //split PEM into 64 character strings
        var privateKeyPEMwithoutLines = removeLines(privateKeyPEMwithLines);  //join PEM
        var privateKeyDerDecoded = b64DecodeUnicode(privateKeyPEMwithoutLines);  // atob(privateKeyB64);
        var privateKeyArrayBuffer = stringToArrayBuffer(privateKeyDerDecoded);  //DER to arrayBuffer
        window.crypto.subtle.importKey(  //importKEy
            "pkcs8",
            privateKeyArrayBuffer,
            {
                name: "RSA-OAEP",
                hash: {name: "SHA-256"}
            },
            true,
            ["decrypt"]
        ).then(function(importedPrivateKey) {
            console.log(importedPrivateKey);
        });
    });
});
Comments