-
-
Save deiu/2c3208c89fbc91d23226 to your computer and use it in GitHub Desktop.
<!-- 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> |
@tylerchilds thanks a lot! I have completely forgot about this gist. If you are interested in a fully fledged implementation, I suggest you take a look at https://github.com/AKASHAorg/easy-web-crypto, which I also maintain.
Is it possible to export a private key? I keep getting errors in Node.js, Deno, and Bun.
@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!
Thanks for writing this, it has been super helpful for a project I'm working on!
After poking around with the code for a bit, there's a bug where
_signedData
isn't defined here: https://gist.github.com/deiu/2c3208c89fbc91d23226#file-webcryptoapi-html-L224Looks like the root cause is that _signedData gets defined in the promise above, which hasn't resolved when testVerifySig runs. The fix on my end was to wrap the
// load keys and re-check signature
block into anotherthen()
after the data has been signed.