Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
79 changes: 79 additions & 0 deletions src/core/operations/RSADecryptPublic.mjs
Original file line number Diff line number Diff line change
@@ -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 (<code>BEGIN RSA PUBLIC KEY</code>) and X.509 SubjectPublicKeyInfo (<code>BEGIN PUBLIC KEY</code>) formats.<br><br>This is the complement of the <b>RSA Encrypt Private Key</b> 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;
90 changes: 90 additions & 0 deletions src/core/operations/RSAEncryptPrivate.mjs
Original file line number Diff line number Diff line change
@@ -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.<br><br>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;
102 changes: 102 additions & 0 deletions tests/operations/tests/RSADecryptPublic.mjs
Original file line number Diff line number Diff line change
@@ -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"]
}
]
}
]);
Loading