-
-
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> |
hi man, i have a question can i import the generated ssh-keygen key private key i already tried that but i couldn't do it any advice?
too bad import/export of RSA-OAEP private key does not covered. looks like it does not work as expected
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-L224
Looks 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 another then() after the data has been signed.
@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!
Thank you!