Last active
July 31, 2024 08:27
-
-
Save deiu/2c3208c89fbc91d23226 to your computer and use it in GitHub Desktop.
Web Crypto API example: RSA keygen & export & import & sign & verify & encrypt & decrypt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- MIT License --> | |
<html> | |
<head> | |
<script> | |
function generateKey(alg, scope) { | |
return new Promise(function(resolve) { | |
var genkey = crypto.subtle.generateKey(alg, true, scope) | |
genkey.then(function (pair) { | |
resolve(pair) | |
}) | |
}) | |
} | |
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 arrayBufferToText(arrayBuffer) { | |
var byteArray = new Uint8Array(arrayBuffer) | |
var str = '' | |
for (var i=0; i<byteArray.byteLength; i++) { | |
str += String.fromCharCode(byteArray[i]) | |
} | |
return str | |
} | |
function arrayBufferToBase64(arr) { | |
return btoa(String.fromCharCode.apply(null, new Uint8Array(arr))) | |
} | |
function convertBinaryToPem(binaryData, label) { | |
var base64Cert = arrayBufferToBase64String(binaryData) | |
var pemCert = "-----BEGIN " + label + "-----\r\n" | |
var nextIndex = 0 | |
var lineLength | |
while (nextIndex < base64Cert.length) { | |
if (nextIndex + 64 <= base64Cert.length) { | |
pemCert += base64Cert.substr(nextIndex, 64) + "\r\n" | |
} else { | |
pemCert += base64Cert.substr(nextIndex) + "\r\n" | |
} | |
nextIndex += 64 | |
} | |
pemCert += "-----END " + label + "-----\r\n" | |
return pemCert | |
} | |
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('-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), signAlgorithm, true, ["verify"]) | |
importer.then(function(key) { | |
resolve(key) | |
}) | |
}) | |
} | |
function importPrivateKey(pemKey) { | |
return new Promise(function(resolve) { | |
var importer = crypto.subtle.importKey("pkcs8", convertPemToBinary(pemKey), signAlgorithm, true, ["sign"]) | |
importer.then(function(key) { | |
resolve(key) | |
}) | |
}) | |
} | |
function exportPublicKey(keys) { | |
return new Promise(function(resolve) { | |
window.crypto.subtle.exportKey('spki', keys.publicKey). | |
then(function(spki) { | |
resolve(convertBinaryToPem(spki, "RSA PUBLIC KEY")) | |
}) | |
}) | |
} | |
function exportPrivateKey(keys) { | |
return new Promise(function(resolve) { | |
var expK = window.crypto.subtle.exportKey('pkcs8', keys.privateKey) | |
expK.then(function(pkcs8) { | |
resolve(convertBinaryToPem(pkcs8, "RSA PRIVATE KEY")) | |
}) | |
}) | |
} | |
function exportPemKeys(keys) { | |
return new Promise(function(resolve) { | |
exportPublicKey(keys).then(function(pubKey) { | |
exportPrivateKey(keys).then(function(privKey) { | |
resolve({publicKey: pubKey, privateKey: privKey}) | |
}) | |
}) | |
}) | |
} | |
function signData(key, data) { | |
return window.crypto.subtle.sign(signAlgorithm, key, textToArrayBuffer(data)) | |
} | |
function testVerifySig(pub, sig, data) { | |
return crypto.subtle.verify(signAlgorithm, pub, sig, data) | |
} | |
function encryptData(vector, key, data) { | |
return crypto.subtle.encrypt( | |
{ | |
name: "RSA-OAEP", | |
iv: vector | |
}, | |
key, | |
textToArrayBuffer(data) | |
) | |
} | |
function decryptData(vector, key, data) { | |
return crypto.subtle.decrypt( | |
{ | |
name: "RSA-OAEP", | |
iv: vector | |
}, | |
key, | |
data | |
) | |
} | |
// Test everything | |
var signAlgorithm = { | |
name: "RSASSA-PKCS1-v1_5", | |
hash: { | |
name: "SHA-256" | |
}, | |
modulusLength: 2048, | |
extractable: false, | |
publicExponent: new Uint8Array([1, 0, 1]) | |
} | |
var encryptAlgorithm = { | |
name: "RSA-OAEP", | |
modulusLength: 2048, | |
publicExponent: new Uint8Array([1, 0, 1]), | |
extractable: false, | |
hash: { | |
name: "SHA-256" | |
} | |
} | |
var crypto = window.crypto || window.msCrypto | |
if (crypto.subtle) { | |
var _signedData | |
var _data = "test" | |
var scopeSign = ["sign", "verify"] | |
var scopeEncrypt = ["encrypt", "decrypt"] | |
var vector = crypto.getRandomValues(new Uint8Array(16)) | |
// Test signature | |
generateKey(signAlgorithm, scopeSign).then(function(pair) { | |
exportPemKeys(pair).then(function(keys) { | |
var title = document.createElement('h2') | |
title.innerHTML = 'Signature' | |
document.querySelector('body').appendChild(title) | |
var divS = document.createElement('div') | |
var divP = document.createElement('div') | |
divS.innerHTML = JSON.stringify(keys.privateKey) | |
divP.innerHTML = JSON.stringify(keys.publicKey) | |
document.querySelector('body').appendChild(divS) | |
document.querySelector('body').appendChild(document.createElement('br')) | |
document.querySelector('body').appendChild(divP) | |
signData(pair.privateKey, _data).then(function(signedData) { | |
var sigT = document.createElement('h2') | |
sigT.innerHTML = 'Signature:' | |
document.querySelector('body').appendChild(sigT) | |
var divSig = document.createElement('div') | |
divSig.innerHTML = arrayBufferToBase64(signedData) | |
document.querySelector('body').appendChild(divSig) | |
_signedData = signedData | |
testVerifySig(pair.publicKey, signedData, textToArrayBuffer(_data)).then(function(result) { | |
var verT = document.createElement('h2') | |
verT.innerHTML = 'Signature outcome:' | |
document.querySelector('body').appendChild(verT) | |
var divOut = document.createElement('div') | |
divOut.innerHTML = (result)?'Success':'Failed'; | |
document.querySelector('body').appendChild(divOut) | |
}) | |
}) | |
// load keys and re-check signature | |
importPublicKey(keys.publicKey).then(function(key) { | |
testVerifySig(key, _signedData, textToArrayBuffer(_data)).then(function(result) { | |
console.log("Signature verified after importing PEM public key:", result) | |
}) | |
}) | |
// should output `Signature verified: true` twice in the console | |
}) | |
}) | |
// Test encryption | |
generateKey(encryptAlgorithm, scopeEncrypt).then(function(keys) { | |
var title = document.createElement('h2') | |
title.innerHTML = 'Encryption' | |
document.querySelector('body').appendChild(title) | |
encryptData(vector, keys.publicKey, _data).then(function(encryptedData) { | |
var sigT = document.createElement('h2') | |
sigT.innerHTML = 'Encrypted text:' | |
document.querySelector('body').appendChild(sigT) | |
var divSig = document.createElement('div') | |
divSig.innerHTML = arrayBufferToBase64(encryptedData) | |
document.querySelector('body').appendChild(divSig) | |
decryptData(vector, keys.privateKey, encryptedData).then(function(result) { | |
var verT = document.createElement('h2') | |
verT.innerHTML = 'Encryption outcome:' | |
document.querySelector('body').appendChild(verT) | |
var divOut = document.createElement('div') | |
divOut.innerHTML = (arrayBufferToText(result) === _data)?'Success':'Failed'; | |
document.querySelector('body').appendChild(divOut) | |
}) | |
}) | |
}) | |
} | |
</script> | |
</head> | |
<body></body> | |
</html> |
@guest271314 please remember that Web Crypto API is only available in browsers, so it will most likely fail in Node/Deno, etc.
Web Cryptography API is available in Node.js, Deno, and Bun via
import * as crypto from "node:crypto";
const { webcrypto } = crypto;
Working example https://github.com/guest271314/webbundle.
I'm trying to export the generated private key with
const algorithm = {
name: "Ed25519",
hash: {
name: "SHA-256",
},
modulusLength: 2048,
extractable: false,
publicExponent: new Uint8Array([1, 0, 1]),
};
// https://github.com/tQsW/webcrypto-curve25519/blob/master/explainer.md
const cryptoKey = await webcrypto.subtle.generateKey(
algorithm,
true, /* extractable */
["sign", "verify"],
);
console.log(await webcrypto.subtle.exportKey("spki", cryptoKey.privateKey));
I'm getting this error in Node.js
(node:18098) ExperimentalWarning: The Ed25519 Web Crypto API algorithm is an experimental feature and might change at any time
node:internal/crypto/webcrypto:360
throw lazyDOMException(
^
DOMException [InvalidAccessError]: Unable to export a raw Ed25519 private key
at exportKeySpki (node:internal/crypto/webcrypto:360:9)
at SubtleCrypto.exportKey (node:internal/crypto/webcrypto:520:25)
at file:///home/user/webbundle/index.js:31:36
Node.js v22.0.0-nightly2024010657c22e4a22
@deiu I think I figured it out
generateWebCryptoKeys.js
import { writeFileSync } from "node:fs";
import { webcrypto } from "node:crypto";
const algorithm = { name: "Ed25519" };
const encoder = new TextEncoder();
const cryptoKey = await webcrypto.subtle.generateKey(
algorithm,
true, /* extractable */
["sign", "verify"],
);
const privateKey = JSON.stringify(
await webcrypto.subtle.exportKey("jwk", cryptoKey.privateKey),
);
writeFileSync("./privateKey.json", encoder.encode(privateKey));
const publicKey = JSON.stringify(
await webcrypto.subtle.exportKey("jwk", cryptoKey.publicKey),
);
writeFileSync("./publicKey.json", encoder.encode(publicKey));
index.js
const privateKey = fs.readFileSync("./privateKey.json");
const publicKey = fs.readFileSync("./publicKey.json");
// https://github.com/tQsW/webcrypto-curve25519/blob/master/explainer.md
const cryptoKey = {
privateKey: await webcrypto.subtle.importKey(
"jwk",
JSON.parse(decoder.decode(privateKey)),
algorithm.name,
true,
["sign"],
),
publicKey: await webcrypto.subtle.importKey(
"jwk",
JSON.parse(decoder.decode(publicKey)),
algorithm.name,
true,
["verify"],
),
};
Great, so JWT seems to do the trick. I'll keep it in mind, thanks!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Is it possible to export a private key? I keep getting errors in Node.js, Deno, and Bun.