From 7a5d4d2736d7a114441a299470a7d722119d3395 Mon Sep 17 00:00:00 2001
From: Fra3zz <112791937+Fra3zz@users.noreply.github.com>
Date: Mon, 15 Jun 2026 10:49:28 -0500
Subject: [PATCH] Added two operations RSADecryptPublic and RSAEncryptPrivate
using nodes RSA tooling.
---
src/core/config/Categories.json | 2 +
src/core/operations/RSADecryptPublic.mjs | 79 ++++++++++++
src/core/operations/RSAEncryptPrivate.mjs | 90 ++++++++++++++
tests/operations/tests/RSADecryptPublic.mjs | 102 ++++++++++++++++
tests/operations/tests/RSAEncryptPrivate.mjs | 119 +++++++++++++++++++
5 files changed, 392 insertions(+)
create mode 100644 src/core/operations/RSADecryptPublic.mjs
create mode 100644 src/core/operations/RSAEncryptPrivate.mjs
create mode 100644 tests/operations/tests/RSADecryptPublic.mjs
create mode 100644 tests/operations/tests/RSAEncryptPrivate.mjs
diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index ceecd005c0..cd817ada51 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -199,6 +199,8 @@
"RSA Verify",
"RSA Encrypt",
"RSA Decrypt",
+ "RSA Encrypt Private Key",
+ "RSA Decrypt Public Key",
"Generate ECDSA Key Pair",
"ECDSA Signature Conversion",
"ECDSA Sign",
diff --git a/src/core/operations/RSADecryptPublic.mjs b/src/core/operations/RSADecryptPublic.mjs
new file mode 100644
index 0000000000..6feeda6d3c
--- /dev/null
+++ b/src/core/operations/RSADecryptPublic.mjs
@@ -0,0 +1,79 @@
+/**
+ * @author Fra3zz
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import forge from "node-forge";
+
+/**
+ * RSA Decrypt Public Key operation
+ */
+class RSADecryptPublic extends Operation {
+
+ /**
+ * RSADecryptPublic constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "RSA Decrypt Public Key";
+ this.module = "Ciphers";
+ this.description = "Decrypt a message that was encrypted with an RSA private key, using the corresponding public key. Supports PKCS#1 (BEGIN RSA PUBLIC KEY) and X.509 SubjectPublicKeyInfo (BEGIN PUBLIC KEY) formats.
This is the complement of the RSA Encrypt Private Key operation. Standard RSA decryption uses a private key; this operation uses the public key to reverse a private-key-encrypted ciphertext.";
+ this.infoURL = "https://wikipedia.org/wiki/RSA_(cryptosystem)";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "RSA Public Key (PEM)",
+ type: "text",
+ value: "-----BEGIN PUBLIC KEY-----"
+ },
+ {
+ name: "Encryption Scheme",
+ type: "option",
+ value: ["PKCS1 v1.5", "RAW"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [pemKey, scheme] = args;
+
+ if (!pemKey.startsWith("-----BEGIN")) {
+ throw new OperationError("Please enter a public key.");
+ }
+
+ let pubKey;
+ try {
+ pubKey = forge.pki.publicKeyFromPem(pemKey);
+ } catch (err) {
+ throw new OperationError(`Unable to load public key: ${err.message}`);
+ }
+
+ try {
+ if (scheme === "PKCS1 v1.5") {
+ const decrypted = forge.pki.rsa.decrypt(input, pubKey, true, true);
+ return forge.util.decodeUtf8(decrypted);
+ } else {
+
+ return forge.pki.rsa.decrypt(input, pubKey, true, false);
+ }
+ } catch (err) {
+ if (err.message === "Encrypted message length is invalid.") {
+ throw new OperationError(`Input length (${err.length} bytes) does not match key size (${err.expected} bytes). Ensure the input is raw ciphertext, not base64 or hex encoded.`);
+ }
+ throw new OperationError(err);
+ }
+ }
+
+}
+
+export default RSADecryptPublic;
diff --git a/src/core/operations/RSAEncryptPrivate.mjs b/src/core/operations/RSAEncryptPrivate.mjs
new file mode 100644
index 0000000000..8bca845b1d
--- /dev/null
+++ b/src/core/operations/RSAEncryptPrivate.mjs
@@ -0,0 +1,90 @@
+/**
+ * @author Fra3zz
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+
+import Operation from "../Operation.mjs";
+import OperationError from "../errors/OperationError.mjs";
+import forge from "node-forge";
+
+/**
+ * RSA Encrypt Private Key operation
+ */
+class RSAEncryptPrivate extends Operation {
+
+ /**
+ * RSAEncryptPrivate constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "RSA Encrypt Private Key";
+ this.module = "Ciphers";
+ this.description = "Encrypt a message with a PEM encoded RSA private key using the private key transform. Supports PKCS#1 and PKCS#8 key formats, including password-protected keys. The resulting ciphertext can be decrypted by anyone holding the corresponding public key.
Note: standard RSA encryption uses a public key. This operation applies the private key transform (equivalent to signing without hashing) and is sometimes required for compatibility with specific systems.";
+ this.infoURL = "https://wikipedia.org/wiki/RSA_(cryptosystem)";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ name: "RSA Private Key (PEM)",
+ type: "text",
+ value: "-----BEGIN RSA PRIVATE KEY-----"
+ },
+ {
+ name: "Key Password",
+ type: "text",
+ value: ""
+ },
+ {
+ name: "Encryption Scheme",
+ type: "option",
+ value: ["PKCS1 v1.5", "RAW"]
+ }
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const [pemKey, password, scheme] = args;
+
+ if (!pemKey.startsWith("-----BEGIN")) {
+ throw new OperationError("Please enter a private key.");
+ }
+
+ let privKey;
+ try {
+ privKey = forge.pki.decryptRsaPrivateKey(pemKey, password);
+ } catch (err) {}
+
+ if (!privKey) {
+ try {
+ privKey = forge.pki.privateKeyFromPem(pemKey);
+ } catch (err) {
+ throw new OperationError(`Unable to load private key: ${err.message}`);
+ }
+ }
+
+ if (!privKey) {
+ throw new OperationError("Unable to load private key. Check the key format and password.");
+ }
+
+ try {
+ const plaintextBytes = forge.util.encodeUtf8(input);
+ const bt = scheme === "PKCS1 v1.5" ? 0x01 : false;
+ return forge.pki.rsa.encrypt(plaintextBytes, privKey, bt);
+ } catch (err) {
+ if (err.message && err.message.includes("too long")) {
+ throw new OperationError(`Message is too long for this key size.`);
+ }
+ throw new OperationError(err);
+ }
+ }
+
+}
+
+export default RSAEncryptPrivate;
diff --git a/tests/operations/tests/RSADecryptPublic.mjs b/tests/operations/tests/RSADecryptPublic.mjs
new file mode 100644
index 0000000000..1aa4f10489
--- /dev/null
+++ b/tests/operations/tests/RSADecryptPublic.mjs
@@ -0,0 +1,102 @@
+/**
+ * RSA Decrypt Public Key tests.
+ *
+ * @author Fra3zz
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+const PRIV_512_PKCS1 = `-----BEGIN RSA PRIVATE KEY-----
+MIIBOQIBAAJBAPKr0Dp6YdItzOfk6a7ma7L4BF4LnelMYKtboGLrk6ihtqFPZFRL
+NcJi68Hvnt8stMrP50t6jqwWQ2EjMdkj6fsCAwEAAQJAOJUpM0lv36MAQR3WAwsF
+F7DOy+LnigteCvaNWiNVxZ6jByB5Qb7sall/Qlu9sFI0ZwrlVcKS0kldee7JTYlL
+WQIhAP3UKEfOtpTgT1tYmdhaqjxqMfxBom0Ri+rt9ajlzs6vAiEA9L85B8/Gnb7p
+6Af7/wpmafL277OV4X4xBfzMR+TUzHUCIBq+VLQkInaTH6lXL3ZtLwyIf9W9MJjf
+RWeuRLjT5bM/AiBF7Kw6kx5Hy1fAtydEApCoDIaIjWJw/kC7WTJ0B+jUUQIgV6dw
+NSyj0feakeD890gmId+lvl/w/3oUXiczqvl/N9o=
+-----END RSA PRIVATE KEY-----`;
+
+const PUB_512_SPKI = `-----BEGIN PUBLIC KEY-----
+MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPKr0Dp6YdItzOfk6a7ma7L4BF4LnelM
+YKtboGLrk6ihtqFPZFRLNcJi68Hvnt8stMrP50t6jqwWQ2EjMdkj6fsCAwEAAQ==
+-----END PUBLIC KEY-----`;
+
+const PUB_512_PKCS1 = `-----BEGIN RSA PUBLIC KEY-----
+MEgCQQDyq9A6emHSLczn5Omu5muy+AReC53pTGCrW6Bi65OoobahT2RUSzXCYuvB
+757fLLTKz+dLeo6sFkNhIzHZI+n7AgMBAAE=
+-----END RSA PUBLIC KEY-----`;
+
+TestRegister.addTests([
+ {
+ name: "RSA Decrypt Public Key: missing key",
+ input: "anything",
+ expectedOutput: "Please enter a public key.",
+ recipeConfig: [
+ {
+ op: "RSA Decrypt Public Key",
+ args: ["", "PKCS1 v1.5"]
+ }
+ ]
+ },
+ {
+ name: "RSA Decrypt Public Key: PKCS1 v1.5, SPKI public key, round-trip Hello World",
+ input: "Hello World",
+ expectedOutput: "Hello World",
+ recipeConfig: [
+ {
+ op: "RSA Encrypt Private Key",
+ args: [PRIV_512_PKCS1, "", "PKCS1 v1.5"]
+ },
+ {
+ op: "RSA Decrypt Public Key",
+ args: [PUB_512_SPKI, "PKCS1 v1.5"]
+ }
+ ]
+ },
+ {
+ name: "RSA Decrypt Public Key: PKCS1 v1.5, PKCS#1 RSA public key, round-trip Hello World",
+ input: "Hello World",
+ expectedOutput: "Hello World",
+ recipeConfig: [
+ {
+ op: "RSA Encrypt Private Key",
+ args: [PRIV_512_PKCS1, "", "PKCS1 v1.5"]
+ },
+ {
+ op: "RSA Decrypt Public Key",
+ args: [PUB_512_PKCS1, "PKCS1 v1.5"]
+ }
+ ]
+ },
+ {
+ name: "RSA Decrypt Public Key: PKCS1 v1.5, SPKI public key, round-trip empty",
+ input: "",
+ expectedOutput: "",
+ recipeConfig: [
+ {
+ op: "RSA Encrypt Private Key",
+ args: [PRIV_512_PKCS1, "", "PKCS1 v1.5"]
+ },
+ {
+ op: "RSA Decrypt Public Key",
+ args: [PUB_512_SPKI, "PKCS1 v1.5"]
+ }
+ ]
+ },
+ {
+ name: "RSA Decrypt Public Key: PKCS1 v1.5, decrypt from base64 ciphertext",
+ input: "2EoJzsG9F9el3Dwxm+7Ua1Ykdpnh9WeS2prrkAYGfnkN9irgRvNIUkOSUJAgy4yQ5RjqAHkKgAdwHvjWqLuLhA==",
+ expectedOutput: "Hello World",
+ recipeConfig: [
+ {
+ op: "From Base64",
+ args: ["A-Za-z0-9+/=", true, false]
+ },
+ {
+ op: "RSA Decrypt Public Key",
+ args: [PUB_512_SPKI, "PKCS1 v1.5"]
+ }
+ ]
+ }
+]);
diff --git a/tests/operations/tests/RSAEncryptPrivate.mjs b/tests/operations/tests/RSAEncryptPrivate.mjs
new file mode 100644
index 0000000000..6fbc45e4d7
--- /dev/null
+++ b/tests/operations/tests/RSAEncryptPrivate.mjs
@@ -0,0 +1,119 @@
+/**
+ * RSA Encrypt Private Key tests.
+ *
+ * @author Fra3zz
+ * @copyright Crown Copyright 2024
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+const PRIV_512_PKCS1 = `-----BEGIN RSA PRIVATE KEY-----
+MIIBOQIBAAJBAPKr0Dp6YdItzOfk6a7ma7L4BF4LnelMYKtboGLrk6ihtqFPZFRL
+NcJi68Hvnt8stMrP50t6jqwWQ2EjMdkj6fsCAwEAAQJAOJUpM0lv36MAQR3WAwsF
+F7DOy+LnigteCvaNWiNVxZ6jByB5Qb7sall/Qlu9sFI0ZwrlVcKS0kldee7JTYlL
+WQIhAP3UKEfOtpTgT1tYmdhaqjxqMfxBom0Ri+rt9ajlzs6vAiEA9L85B8/Gnb7p
+6Af7/wpmafL277OV4X4xBfzMR+TUzHUCIBq+VLQkInaTH6lXL3ZtLwyIf9W9MJjf
+RWeuRLjT5bM/AiBF7Kw6kx5Hy1fAtydEApCoDIaIjWJw/kC7WTJ0B+jUUQIgV6dw
+NSyj0feakeD890gmId+lvl/w/3oUXiczqvl/N9o=
+-----END RSA PRIVATE KEY-----`;
+
+const PRIV_512_PKCS8 = `-----BEGIN PRIVATE KEY-----
+MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA8qvQOnph0i3M5+Tp
+ruZrsvgEXgud6Uxgq1ugYuuTqKG2oU9kVEs1wmLrwe+e3yy0ys/nS3qOrBZDYSMx
+2SPp+wIDAQABAkA4lSkzSW/fowBBHdYDCwUXsM7L4ueKC14K9o1aI1XFnqMHIHlB
+vuxqWX9CW72wUjRnCuVVwpLSSV157slNiUtZAiEA/dQoR862lOBPW1iZ2FqqPGox
+/EGibRGL6u31qOXOzq8CIQD0vzkHz8advunoB/v/CmZp8vbvs5XhfjEF/MxH5NTM
+dQIgGr5UtCQidpMfqVcvdm0vDIh/1b0wmN9FZ65EuNPlsz8CIEXsrDqTHkfLV8C3
+J0QCkKgMhoiNYnD+QLtZMnQH6NRRAiBXp3A1LKPR95qR4Pz3SCYh36W+X/D/ehRe
+JzOq+X832g==
+-----END PRIVATE KEY-----`;
+
+const PRIV_512_PKCS8_ENC = `-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBrzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQILP5/Mw4jnpUCAggA
+MB0GCWCGSAFlAwQBAgQQ18VGFlqTKWNfEro31Ev+UgSCAWAwara+xKGKPSNGOakz
+DLST6Xhnxv6PcL1+Ei4RR65ns94ofPJl/2lai8R5itkL9gRbqiGi1L7rdwsP8Awb
+O5d4KUnVMSEFTBDyIBU5w5Di4WJFU04PuchVHDRBiqYNHVEu0y5JmQu0cPpaTd/m
+eNuLaymoIduUFLj/aMpda0BTEOwdCJ+G4P5IOwAXkGQ4JUX3NPp3S/bwmS6HYnct
+W3q65mhEIuJb1CK7O1k97L9ArmFQ+p74jELvwfJTaP9FNnee3Rs1hpbQ3AVwtEtq
+3w0P2XXEDDVcwNvNh+sLKhp7WYWvQM9NXnWPQFu1FiJqIfUk6/SMdplH0KEAdexb
+XvERV5TRWqb/ilQPYYiJVgQpe0evZblvERMS3Ul3hSrsEYcr4MBII+RvMZHss/hQ
+2yp5XcQNzP9uceT4hIGAlvTAG4BGaH2tbU4x/T9ae0mx52/8I8RS9gSgPqK2SdSf
+tMER
+-----END ENCRYPTED PRIVATE KEY-----`;
+
+const HELLO_WORLD_PKCS1_B64 = "2EoJzsG9F9el3Dwxm+7Ua1Ykdpnh9WeS2prrkAYGfnkN9irgRvNIUkOSUJAgy4yQ5RjqAHkKgAdwHvjWqLuLhA==";
+const EMPTY_PKCS1_B64 = "rfhNp4omqGuP+7J6Z2MuMeDwBzGcRm9kFIPPAn/S6HPXfXN/4Kasd1FUdr9n0+7egspZNSPg5TWq4GKGXuhBhA==";
+
+TestRegister.addTests([
+ {
+ name: "RSA Encrypt Private Key: missing key",
+ input: "Hello World",
+ expectedOutput: "Please enter a private key.",
+ recipeConfig: [
+ {
+ op: "RSA Encrypt Private Key",
+ args: ["", "", "PKCS1 v1.5"]
+ }
+ ]
+ },
+ {
+ name: "RSA Encrypt Private Key: PKCS#1 key, Hello World",
+ input: "Hello World",
+ expectedOutput: HELLO_WORLD_PKCS1_B64,
+ recipeConfig: [
+ {
+ op: "RSA Encrypt Private Key",
+ args: [PRIV_512_PKCS1, "", "PKCS1 v1.5"]
+ },
+ {
+ op: "To Base64",
+ args: ["A-Za-z0-9+/="]
+ }
+ ]
+ },
+ {
+ name: "RSA Encrypt Private Key: PKCS#8 key, Hello World",
+ input: "Hello World",
+ expectedOutput: HELLO_WORLD_PKCS1_B64,
+ recipeConfig: [
+ {
+ op: "RSA Encrypt Private Key",
+ args: [PRIV_512_PKCS8, "", "PKCS1 v1.5"]
+ },
+ {
+ op: "To Base64",
+ args: ["A-Za-z0-9+/="]
+ }
+ ]
+ },
+ {
+ name: "RSA Encrypt Private Key: encrypted PKCS#8 key with password, Hello World",
+ input: "Hello World",
+ expectedOutput: HELLO_WORLD_PKCS1_B64,
+ recipeConfig: [
+ {
+ op: "RSA Encrypt Private Key",
+ args: [PRIV_512_PKCS8_ENC, "testpassword", "PKCS1 v1.5"]
+ },
+ {
+ op: "To Base64",
+ args: ["A-Za-z0-9+/="]
+ }
+ ]
+ },
+ {
+ name: "RSA Encrypt Private Key: empty input",
+ input: "",
+ expectedOutput: EMPTY_PKCS1_B64,
+ recipeConfig: [
+ {
+ op: "RSA Encrypt Private Key",
+ args: [PRIV_512_PKCS1, "", "PKCS1 v1.5"]
+ },
+ {
+ op: "To Base64",
+ args: ["A-Za-z0-9+/="]
+ }
+ ]
+ }
+]);