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;