How to ImportKey() to get CryptoObject from ExportedKey? - javascript

Trying to use the SubtleCrypto Web API to generate a public/private key pair, then use that keypair for encryption from a user inputting the public key, but importKey() keeps giving result as undefine. For public/private key generation, I have the following:
window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048, // can be 1024, 2048 or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"} // or SHA-512
},
true,
["encrypt", "decrypt"]
).then(function(keyPair) {
console.log(keyPair);
window.crypto.subtle.exportKey(
"pkcs8",
keyPair.privateKey
).then(function(exportedPrivateKey) {
// converting exported private key to PEM format
var pem = toPem(exportedPrivateKey);
console.log(pem);
}).catch(function(err) {
console.log(err);
});
window.crypto.subtle.exportKey(
"spki",
keyPair.publicKey
).then(function(exportedPublicKey) {
// converting exported private key to PEM format
var pem = toPemP(exportedPublicKey);
console.log(pem);
let resultingKey = importPublicKey(pem)
console.log('keyPairPublic: ', keyPair.publicKey)
console.log('resultingKey: ', resultingKey)
}).catch(function(err) {
console.log(err);
});
});
the toPem() and toPemP() functions are as follows:
function toPem(privateKey) {
var b64 = addNewLines(arrayBufferToBase64(privateKey));
var pem = "-----BEGIN PRIVATE KEY-----\n" + b64 + "-----END PRIVATE KEY-----";
return pem;
}
function toPemP(publicKey) {
var b64 = addNewLines(arrayBufferToBase64(publicKey));
var pem = "-----BEGIN PUBLIC KEY-----\n" + b64 + "-----END PUBLIC KEY-----";
return pem;
}
function arrayBufferToBase64(arrayBuffer) {
console.log('arrayBuffer', arrayBuffer);
var byteArray = new Uint8Array(arrayBuffer);
var byteString = '';
for(var i=0; i < byteArray.byteLength; i++) {
byteString += String.fromCharCode(byteArray[i]);
}
var b64 = window.btoa(byteString);
return b64;
}
function addNewLines(str) {
var finalString = '';
while(str.length > 0) {
finalString += str.substring(0, 64) + '\n';
str = str.substring(64);
}
return finalString;
}
The importKey() function is as follows:
function importPublicKey(pem) {
// base64 decode the string to get the binary data
// fetch the part of the PEM string between header and footer
const pemHeader = "-----BEGIN PUBLIC KEY-----";
const pemFooter = "-----END PUBLIC KEY-----";
let pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length)
pemContents = pemContents.replace(/\s/g, '');
console.log('pemContents:', pemContents)
let binaryDerString = window.atob(pemContents);
// convert from a binary string to an ArrayBuffer
const binaryDer = str2ab(binaryDerString);
console.log('binaryDer', binaryDer)
window.crypto.subtle.importKey(
"spki",
binaryDer,
{
name: "RSA-OAEP",
hash: { name: "SHA-256" }
},
true,
["encrypt"]
).then(function(result) {
return result;
}).catch(function(err) {
console.log('err: ', err);
})
}
Where str2ab() is as follows:
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;
}
All of the above functions were from the SubtleCrypto documentation. When I run the above, I get the following:
I'm not sure why it comes out as undefined, but I can notice that arrayBuffer and binaryDer don't have the exact same size. Don't know if it means anything and not sure how to fix this, any help is appreciated!

Related

Why does signing algorithm in C# give different result than the one in Javascript

This is the algorithm for signing the data in C# using a private key from a certificate that is used from both me and the client in order to define an unique key to identify the user:
X509Certificate2 keyStore = new X509Certificate2(AppDomain.CurrentDomain.BaseDirectory + "Certifikatat\\" + certPath, certPass, X509KeyStorageFlags.Exportable);
RSA privateKey = keyStore.GetRSAPrivateKey();
byte[] iicSignature = privateKey.SignData(Encoding.ASCII.GetBytes("K31418036C|2022-5-16 13:30:41|406|st271ir481|al492py609|zz463gy579|340"), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
byte[] iic = ((HashAlgorithm)CryptoConfig,CreateFromName("MD5")).ComputeHash(iicSignature);
I then pass the private key to my Javascript using Bouncy Castle:
X509Certificate2 keyStore = new X509Certificate2(AppDomain.CurrentDomain.BaseDirectory + "Certifikatat\\" + certPath, certPass, X509KeyStorageFlags.Exportable);
RSA privateKey = keyStore.GetRSAPrivateKey();
var eky = DotNetUtilities.GetRsaKeyPair(privateKey);
Pkcs8Generator pkcs8Gen = new Pkcs8Generator(eky.Private);
Org.BouncyCastle.Utilities.IO.Pem.PemObject pkcs8 = pkcs8Gen.Generate();
PemWriter pemWriter = new PemWriter(new StringWriter());
pemWriter.WriteObject(pkcs8);
pemWriter.Writer.Flush();
return pemWriter.Writer.ToString();
This one is the algorithm used in Javascript:
window.crypto.subtle.importKey(
"pkcs8",
pemToArrayBuffer(pkcs8Pem), {
name: "RSASSA-PKCS1-v1_5",
hash: {
name: "SHA-256"
},
},
false, ["sign"]
)
.then(function(privateKey) {
console.log(privateKey);
// Sign: RSA with SHA256 and PKCS#1 v1.5 padding
window.crypto.subtle.sign({
name: "RSASSA-PKCS1-v1_5",
},
privateKey,
new TextEncoder().encode("K31418036C|2022-5-16 13:30:41|406|st271ir481|al492py609|zz463gy579|340")
)
.then(function(signature) {
var iic = md5(signature);
console.log(ab2b64(signature));
})
.catch(function(err) {
console.error(err);
});
})
.catch(function(err) {
console.error(err);
});
function ab2b64(arrayBuffer) {
return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
function removeLines(str) {
str = str.replace("\r", "");
return str.replace("\n", "");
}
function base64ToArrayBuffer(b64) {
var byteString = atob(b64);
var byteArray = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
byteArray[i] = byteString.charCodeAt(i);
}
return byteArray;
}
function pemToArrayBuffer(pem) {
var b64Lines = removeLines(pem);
var b64Prefix = b64Lines.replace('-----BEGIN PRIVATE KEY-----', '');
var b64Final = b64Prefix.replace('-----END PRIVATE KEY-----', '');
return base64ToArrayBuffer(b64Final);
}
The signatures returned are different for some reason. I need them to be the same or else it's all pointless because the client won't be authenticated.
The results are as follow:
C#:
57CF663ACBEDE6305309682BA7261412
Javascript:
c099d176dcd95c59d748d6066dcd462e
I had to convert my signature to base64 and then encode it with atob() after that i needed this md5 library to hash the data and then use .toUpperCase() to reproduce the correct result.
The complete code looks like this:
md5(atob(ab2b64(signature))).toUpperCase();
Now i get the same result from both C# and JS.

This is a Python code to get a md5 digest of a signature, Need help to implement this thing in JavaScript

Python Code that works fine and I checked this Python code for message "a" it gives me the result "52F17E7031982DE1744A57F6EE9BD3A3"
from Crypto.Hash import SHA256, MD5
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from OpenSSL import crypto
message = "a".encode('utf-8')
p12 = crypto.load_pkcs12(company_p12_certificate, certificate_password)
key_bytes = crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey())
key = RSA.import_key(key_bytes)
h = SHA256.new(message)
signer = PKCS1_v1_5.new(key)
signature = signer.sign(h)
md5_digest = MD5.new(signature)
result = str(md5_digest.digest().hex()).upper()
I implemented this using JavaScript but not getting same result. It gives me "4EB5DB7F5459E832DE3E0638A8F4C4A0" for message "a"
My JavaScript code is:
$(document).ready(function () {
function _privateKeyToPkcs8(privateKey) {
var rsaPrivateKey = forge.pki.privateKeyToAsn1(privateKey);
var privateKeyInfo = forge.pki.wrapRsaPrivateKey(rsaPrivateKey);
var privateKeyInfoDer = forge.asn1.toDer(privateKeyInfo).getBytes();
var privateKeyInfoDerBuff = stringToArrayBuffer(privateKeyInfoDer);
return privateKeyInfoDerBuff;
}
function stringToArrayBuffer(data) {
var arrBuff = new ArrayBuffer(data.length);
var writer = new Uint8Array(arrBuff);
for (var i = 0, len = data.length; i < len; i++) {
writer[i] = data.charCodeAt(i);
}
return arrBuff;
}
function arrayBufferToString(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return binary;
}
$("#file").change(function () {
var file = this.files[0];
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function (e) {
var contents = e.target.result;
var pkcs12Der = arrayBufferToString(contents)
var pkcs12B64 = forge.util.encode64(pkcs12Der);
var pkcs12Der = forge.util.decode64(pkcs12B64);
var pkcs12Asn1 = forge.asn1.fromDer(pkcs12Der);
var pkcs12 = forge.pkcs12.pkcs12FromAsn1(pkcs12Asn1, false, '123456');
var privateKey
for (var sci = 0; sci < pkcs12.safeContents.length; ++sci) {
var safeContents = pkcs12.safeContents[sci];
for (var sbi = 0; sbi < safeContents.safeBags.length; ++sbi) {
var safeBag = safeContents.safeBags[sbi];
// this bag has a private key
if (safeBag.type === forge.pki.oids.keyBag) {
//Found plain private key
privateKey = safeBag.key;
} else if (safeBag.type === forge.pki.oids.pkcs8ShroudedKeyBag) {
// found encrypted private key
privateKey = safeBag.key;
} else if (safeBag.type === forge.pki.oids.certBag) {
// this bag has a certificate...
}
}
}
var privateKeyInfoDerBuff = _privateKeyToPkcs8(privateKey);
//Import the webcrypto key
crypto.subtle.importKey(
'pkcs8',
privateKeyInfoDerBuff,
{ name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
true,
["sign"]).
then(function (cryptoKey) {
var sha256 = forge.md.sha256.create();
sha256.update('a'); // Message will come here
var digestToSignBuf = stringToArrayBuffer(sha256.digest().toHex());
crypto.subtle.sign({ name: "RSASSA-PKCS1-v1_5" }, cryptoKey, digestToSignBuf)
.then(function (signature) {
var signatureB64 = forge.util.encode64(arrayBufferToString(signature))
var md5 = forge.md.md5.create();
md5.update(signatureB64);
console.log("final result", md5.digest().toHex().toUpperCase());
});
})
}
})
});
I also checked https://github.com/digitalbazaar/forge
WebCrypto implicitly generates the hash of the data during signing using the digest SHA-256 specified in the key, so explicit hashing with SHA-256 is not necessary.
Also, the generated signature is directly hashed with MD5, i.e. without prior Base64 encoding.
With these changes, the JavaScript code is (using a test key):
// Import private test key
var pkcs8pem = `-----BEGIN PRIVATE KEY-----
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEA2gdsVIRmg5IH0rG3
u3w+gHCZq5o4OMQIeomC1NTeHgxbkrfznv7TgWVzrHpr3HHK8IpLlG04/aBo6U5W
2umHQQIDAQABAkEAu7wulGvZFat1Xv+19BMcgl3yhCdsB70Mi+7CH98XTwjACk4T
+IYv4N53j16gce7U5fJxmGkdq83+xAyeyw8U0QIhAPIMhbtXlRS7XpkB66l5DvN1
XrKRWeB3RtvcUSf30RyFAiEA5ph7eWXbXWpIhdWMoe50yffF7pW+C5z07tzAIH6D
Ko0CIQCyveSTr917bdIxk2V/xNHxnx7LJuMEC5DcExorNanKMQIgUxHRQU1hNgjI
sXXZoKgfaHaa1jUZbmOPlNDvYYVRyS0CIB9ZZee2zubyRla4qN8PQxCJb7DiICmH
7nWP7CIvcQwB
-----END PRIVATE KEY-----`;
var pkcs8 = convertDER(pkcs8pem);
crypto.subtle.importKey(
'pkcs8',
pkcs8,
{ name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
true,
["sign"])
.then(function (cryptoKey) {
// Sign message
var message = 'The quick brown fox jumps over the lazy dog';
crypto.subtle.sign(
{ name: "RSASSA-PKCS1-v1_5" },
cryptoKey,
stringToArrayBuffer(message))
.then(function (signature) {
// Create MD5 hash
var md5 = forge.md.md5.create();
md5.update(arrayBufferToString(signature));
console.log("Final result", md5.digest().toHex().toUpperCase()); // Final result 30FD001CFD12D0A3DF000D216C82C47E
});
});
// Helper
function stringToArrayBuffer(data) {
var arrBuff = new ArrayBuffer(data.length);
var writer = new Uint8Array(arrBuff);
for (var i = 0, len = data.length; i < len; i++) {
writer[i] = data.charCodeAt(i);
}
return arrBuff;
}
function arrayBufferToString(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return binary;
}
function convertDER(pem){
var pemHeader = "-----BEGIN PRIVATE KEY-----";
var pemFooter = "-----END PRIVATE KEY-----";
var pemContents = pkcs8pem.substring(pemHeader.length, pkcs8pem.length - pemFooter.length);
var binaryDerString = window.atob(pemContents);
var pkcs8 = stringToArrayBuffer(binaryDerString);
return pkcs8;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/forge/0.10.0/forge.min.js"></script>
with the output:
Final result 30FD001CFD12D0A3DF000D216C82C47E
The Python code returns the same result for the same key and plaintext.
from Crypto.Hash import SHA256, MD5
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
key_bytes = '''-----BEGIN PRIVATE KEY-----
MIIBVQI...
-----END PRIVATE KEY-----'''
message = "The quick brown fox jumps over the lazy dog".encode('utf-8')
key = RSA.import_key(key_bytes)
h = SHA256.new(message)
signer = PKCS1_v1_5.new(key)
signature = signer.sign(h)
md5_digest = MD5.new(signature)
result = str(md5_digest.digest().hex()).upper()
print(result) # 30FD001CFD12D0A3DF000D216C82C47E
As already noted in the comments, due to the MD5 hash, verification using the public key is not possible (but apparently not intended).
Also, WebCrypto provides the SubtleCrypto.digest() function to determine a hash, so the forge library is actually not really necessary (at least for hashing).
I've not analyzed the key import in more detail, so there may be problems here as well.

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
It wont reload the private key from armored text block
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>
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.

RSA-OAEP encryption in Javascript and decryption in Java

I am currently implementing the RSA-OAEP encryption on Javascript and decryption at Java.
My javascript code has the following
function stringToArrayBuffer(str){
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i=0, strLen=str.length; i<strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function arrayBufferToString(str){
var byteArray = new Uint8Array(str);
var byteString = '';
for(var i=0; i < byteArray.byteLength; i++) {
byteString += String.fromCodePoint(byteArray[i]);
}
return byteString;
}
function encryptDataWithPublicKey(data, key) {
data = stringToArrayBuffer(data);
return window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
//label: Uint8Array([...]) //optional
},
key, //from generateKey or importKey above
data //ArrayBuffer of data you want to encrypt
);
}
var pem = Config.encryption.publicKey;
// fetch the part of the PEM string between header and footer
const pemHeader = "-----BEGIN PUBLIC KEY-----";
const pemFooter = "-----END PUBLIC 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 = stringToArrayBuffer(binaryDerString);
window.crypto.subtle.importKey(
"spki",
binaryDer,
{
name: "RSA-OAEP",
hash: { name: "SHA-256" }
},
true,
["encrypt"]
).then(function (publicKey) {
encryptDataWithPublicKey(text, publicKey).then((result) => {
var rdata = arrayBufferToString(result);
resolve(rdata);
});
}).catch(function (err) {
console.log(err);
reject(err);
});
I also have a Java function to decrypt the text. Assume "rsaOaepCipherText" is a string text.
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
OAEPParameterSpec oaepParams = new OAEPParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-256"), PSource.PSpecified.DEFAULT);
cipher.init(Cipher.DECRYPT_MODE, getPrivateKey(), oaepParams);
return new String(cipher.doFinal(Base64.decodeBase64(rsaOaepCipherText)), "UTF-8");
However I keep getting decryption error on Java, and currently stuck at this portion, is there any error I have done on my encryption on Javascript?
Ok found it. I forgot to include btoa on the string before send to backend.
it should be
encryptDataWithPublicKey(text, publicKey).then((result) => {
var rdata = arrayBufferToString(result);
var rResult = window.btoa(rdata);
resolve(rResult);
});

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);
});
}
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);
});
});
});
I am posting additional working code:
(NOTE: Code is without -----BEGIN PRIVATE KEY----- and ----- END PRIVATE KEY ----- base64 only)
function addNewLines(str) {
var finalString = '';
while(str.length > 0) {
finalString += str.substring(0, 64) + '\n';
str = str.substring(64);
}
return finalString;
}
function removeLines(str) {
return str.replace("\n", "");
}
function arrayBufferToBase64(arrayBuffer) {
var byteArray = new Uint8Array(arrayBuffer);
var byteString = '';
for(var i=0; i < byteArray.byteLength; i++) {
byteString += String.fromCharCode(byteArray[i]);
}
var b64 = window.btoa(byteString);
return b64;
}
function base64ToArrayBuffer(b64) {
var byteString = window.atob(b64);
var byteArray = new Uint8Array(byteString.length);
for(var i=0; i < byteString.length; i++) {
byteArray[i] = byteString.charCodeAt(i);
}
return byteArray;
}
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 pem = addNewLines(arrayBufferToBase64(exportedPrivateKey));
importKey(pem);
});
});
function importKey(b64) {
b64 = removeLines(b64);
arrayBuffer = base64ToArrayBuffer(b64);
window.crypto.subtle.importKey(
"pkcs8",
arrayBuffer,
{
name: "RSA-OAEP",
hash: {name: "SHA-256"}
},
true,
["decrypt"]
).then(function(importedPrivateKey) {
console.log(importedPrivateKey);
});
}
UPDATE:
I wrote a little crypto library that you can use for PEM converting and many more.
https://github.com/PeterBielak/OpenCrypto
Usage example:
var crypt = new OpenCrypto();
crypt.getKeyPair().then(function(keyPair) {
crypt.cryptoPrivateToPem(keyPair.privateKey).then(function(pemPrivateKey) {
console.log(pemPrivateKey);
});
});

Categories