From fada7954f4d4a0c63cafca3d7f24202f390269bc Mon Sep 17 00:00:00 2001 From: MikeCAT Date: Fri, 8 Aug 2025 17:53:13 +0000 Subject: [PATCH] Add new operation: Generate EdDSA Key Pair --- package-lock.json | 28 ++- package.json | 2 + src/core/config/Categories.json | 3 +- src/core/operations/GenerateEdDSAKeyPair.mjs | 206 +++++++++++++++++++ 4 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 src/core/operations/GenerateEdDSAKeyPair.mjs diff --git a/package-lock.json b/package-lock.json index b374df4bc9..dc833837b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@astronautlabs/amf": "^0.0.6", "@babel/polyfill": "^7.12.1", "@blu3r4y/lzma": "^2.3.3", + "@noble/ed25519": "^2.3.0", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", "@xmldom/xmldom": "^0.8.10", "argon2-browser": "^1.18.0", @@ -38,6 +39,7 @@ "d3-hexbin": "^0.2.2", "diff": "^5.2.0", "dompurify": "^3.2.5", + "ed448-js": "^2.0.0", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -3932,6 +3934,14 @@ "archiver": "^5.3.1" } }, + "node_modules/@noble/ed25519": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-2.3.0.tgz", + "integrity": "sha512-M7dvXL2B92/M7dw9+gzuydL8qn/jiqNHaoR3Q+cb1q1GHV7uwE17WCyFMG+Y+TZb5izcaXk5TdJRrDUxHXL78A==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/hashes": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz", @@ -8520,6 +8530,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ed448-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ed448-js/-/ed448-js-2.0.0.tgz", + "integrity": "sha512-5xft1V4bJ0ji4SB3eQu4J7REsBGdv0dgLXMXu7t0ifrRBycM1fKDP+1gI0Se88cakX/VE6HEz6P23rWNBzEV6w==", + "dependencies": { + "jsbn": "^1.1.0", + "jssha": "^3.2.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -12379,7 +12398,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "dev": true, "license": "MIT" }, "node_modules/jsdoc-type-pratt-parser": { @@ -12566,6 +12584,14 @@ "url": "https://github.com/kjur/jsrsasign#donations" } }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "engines": { + "node": "*" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", diff --git a/package.json b/package.json index 9191ab6f03..0c730e801e 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@astronautlabs/amf": "^0.0.6", "@babel/polyfill": "^7.12.1", "@blu3r4y/lzma": "^2.3.3", + "@noble/ed25519": "^2.3.0", "@wavesenterprise/crypto-gost-js": "^2.1.0-RC1", "@xmldom/xmldom": "^0.8.10", "argon2-browser": "^1.18.0", @@ -124,6 +125,7 @@ "d3-hexbin": "^0.2.2", "diff": "^5.2.0", "dompurify": "^3.2.5", + "ed448-js": "^2.0.0", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 434c8bb619..416e244190 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -197,7 +197,8 @@ "Public Key from Certificate", "Public Key from Private Key", "SM2 Encrypt", - "SM2 Decrypt" + "SM2 Decrypt", + "Generate EdDSA Key Pair" ] }, { diff --git a/src/core/operations/GenerateEdDSAKeyPair.mjs b/src/core/operations/GenerateEdDSAKeyPair.mjs new file mode 100644 index 0000000000..5974674e22 --- /dev/null +++ b/src/core/operations/GenerateEdDSAKeyPair.mjs @@ -0,0 +1,206 @@ +/** + * @author mikecat + * @copyright Crown Copyright 2025 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { cryptNotice } from "../lib/Crypt.mjs"; +import forge from "node-forge"; +import { isWorkerEnvironment } from "../Utils.mjs"; +import * as Ed25519 from "@noble/ed25519"; +import createEd448 from "ed448-js"; + +/** + * Generate EdDSA Key Pair operation + */ +class GenerateEdDSAKeyPair extends Operation { + + /** + * GenerateEdDSAKeyPair constructor + */ + constructor() { + super(); + + this.name = "Generate EdDSA Key Pair"; + this.module = "Ciphers"; + this.description = `Generate an EdDSA (Ed25519 and Ed448) key pair.

${cryptNotice}`; + this.infoURL = "https://datatracker.ietf.org/doc/html/rfc8032"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Instance", + "type": "option", + "value": ["Ed25519", "Ed448"] + }, + { + "name": "Output Format", + "type": "option", + "value": ["PEM", "JWK", "OpenSSH", "Raw"] + } + ]; + // create Ed448 instance later (creating here resulted in errors in bundling) + this.Ed448 = null; + this.getRandomBytes = (length) => { + if (isWorkerEnvironment() && self.crypto) { + const result = new Uint8Array(length); + self.crypto.getRandomValues(result); + return Array.from(result); + } else { + const randomStr = forge.random.getBytesSync(length); + return Array.from(randomStr).map((e) => e.charCodeAt(0)); + } + }; + this.bytesToHex = (byteArray) => Ed25519.etc.bytesToHex(new Uint8Array(byteArray)); + this.bytesToBase64 = (byteArray) => btoa(byteArray.map((c) => String.fromCharCode(c)).join("")); + this.bytesToBase64url = (byteArray) => ( + this.bytesToBase64(byteArray).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") + ); + this.insertNewlines = (str, length) => { + let result = ""; + for (;;) { + result += str.substring(0, length); + str = str.substring(length); + if (str.length > 0) { + result += "\n"; + } else { + return result; + } + } + }; + this.textEncoder = new TextEncoder(); + this.strToBytes = (str) => Array.from(this.textEncoder.encode(str)); + this.uint32ToBytes = (value) => { + const arrayBuffer = new ArrayBuffer(4); + const dataView = new DataView(arrayBuffer); + dataView.setUint32(0, value); + return Array.from(new Uint8Array(arrayBuffer)); + }; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + async run(input, args) { + const instance = args[0], outputFormat = args[1]; + let privateKey, publicKey, crv, objectIdentifier, opensshKeyType; + switch (instance) { + case "Ed448": + if (!this.Ed448) this.Ed448 = createEd448(); + privateKey = this.getRandomBytes(57); + publicKey = Array.from(this.Ed448.getPublicKey(new Uint8Array(privateKey))); + crv = "Ed448"; + objectIdentifier = [1 * 40 + 3, 101, 113]; + opensshKeyType = "ssh-ed448"; + break; + default: // Ed25519 + privateKey = this.getRandomBytes(32); + publicKey = Array.from(await Ed25519.getPublicKeyAsync(new Uint8Array(privateKey))); + crv = "Ed25519"; + objectIdentifier = [1 * 40 + 3, 101, 112]; + opensshKeyType = "ssh-ed25519"; + break; + } + switch (outputFormat) { + case "PEM": + { + // assuming data to deal with here is short enough + const objectIdentifierSequence = [0x30, objectIdentifier.length + 2, 6, objectIdentifier.length].concat(objectIdentifier); + const privateKeyOctetString = [4, privateKey.length + 2, 4, privateKey.length].concat(privateKey); + const privateKeySequenceData = [2, 1, 0].concat(objectIdentifierSequence, privateKeyOctetString); + const privateKeyBytes = [0x30, privateKeySequenceData.length].concat(privateKeySequenceData); + const publicKeyBitString = [3, publicKey.length + 1, 0].concat(publicKey); + const publicKeySequenceData = objectIdentifierSequence.concat(publicKeyBitString); + const publicKeyBytes = [0x30, publicKeySequenceData.length].concat(publicKeySequenceData); + return ( + "-----BEGIN PUBLIC KEY-----\n" + + this.insertNewlines(this.bytesToBase64(publicKeyBytes), 64) + + "\n-----END PUBLIC KEY-----\n\n-----BEGIN PRIVATE KEY-----\n" + + this.insertNewlines(this.bytesToBase64(privateKeyBytes), 64) + + "\n-----END PRIVATE KEY-----\n" + ); + } + case "JWK": + { + const publicKeyJWK = { + kty: "OKP", + crv, + x: this.bytesToBase64url(publicKey) + }; + return JSON.stringify({ + keys: [ + { + ...publicKeyJWK, + d: this.bytesToBase64url(privateKey), + key_ops: ["sign"], // eslint-disable-line camelcase + kid: "PrivateKey" + }, + { + ...publicKeyJWK, + key_ops: ["verify"], // eslint-disable-line camelcase + kid: "PublicKey" + } + ] + }, null, 4); + } + case "OpenSSH": + { + const comment = "cyberchef"; + const commentBytes = this.strToBytes(comment); + const encryptMethodBytes = this.strToBytes("none"); + const kdfMethodBytes = this.strToBytes("none"); + const kdfParameterBytes = []; + const checkValue = this.getRandomBytes(4); + const keyTypeBytes = this.strToBytes(opensshKeyType); + const publicKeyBytes = [].concat( + this.uint32ToBytes(keyTypeBytes.length), + keyTypeBytes, + this.uint32ToBytes(publicKey.length), + publicKey + ); + const privateKeyDataBytes = [].concat( + checkValue, + checkValue, + publicKeyBytes, + this.uint32ToBytes(privateKey.length + publicKey.length), + privateKey, + publicKey, + this.uint32ToBytes(commentBytes.length), + commentBytes + ); + for (let i = 1; privateKeyDataBytes.length % 8 !== 0; i++) { + privateKeyDataBytes.push(i % 256); + } + const privateKeyBytes = [].concat( + this.strToBytes("openssh-key-v1"), + [0], + this.uint32ToBytes(encryptMethodBytes.length), + encryptMethodBytes, + this.uint32ToBytes(kdfMethodBytes.length), + kdfMethodBytes, + this.uint32ToBytes(kdfParameterBytes.length), + kdfParameterBytes, + this.uint32ToBytes(1), + this.uint32ToBytes(publicKeyBytes.length), + publicKeyBytes, + this.uint32ToBytes(privateKeyDataBytes.length), + privateKeyDataBytes + ); + return ( + opensshKeyType + " " + this.bytesToBase64(publicKeyBytes) + " " + comment + + "\n\n-----BEGIN OPENSSH PRIVATE KEY-----\n" + + this.insertNewlines(this.bytesToBase64(privateKeyBytes), 70) + + "\n-----END OPENSSH PRIVATE KEY-----\n" + ); + } + default: // Raw + return this.bytesToHex(new Uint8Array(privateKey.concat(publicKey))); + } + } + +} + +export default GenerateEdDSAKeyPair;