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+/="] + } + ] + } +]);