Added two operations RSADecryptPublic and RSAEncryptPrivate using nodes RSA tooling.
This commit is contained in:
parent
cc38f3a53f
commit
7a5d4d2736
@ -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",
|
||||
|
||||
79
src/core/operations/RSADecryptPublic.mjs
Normal file
79
src/core/operations/RSADecryptPublic.mjs
Normal file
@ -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
src/core/operations/RSAEncryptPrivate.mjs
Normal file
90
src/core/operations/RSAEncryptPrivate.mjs
Normal file
@ -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
tests/operations/tests/RSADecryptPublic.mjs
Normal file
102
tests/operations/tests/RSADecryptPublic.mjs
Normal file
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
119
tests/operations/tests/RSAEncryptPrivate.mjs
Normal file
119
tests/operations/tests/RSAEncryptPrivate.mjs
Normal file
@ -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+/="]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
Loading…
x
Reference in New Issue
Block a user