package com.bmo.dc.securityapi.era.util.common;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.MGF1ParameterSpec;
import javax.crypto.spec.PSource;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.MessageDigest;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import com.fasterxml.jackson.databind.JsonNode;
/**
* 1:1 port of CG2V_SCCG-RSAAdaptiveAuthentication-CDK_26777/service/lambda-functions/service/utils.js
* decrypt() and encrypt() functions.
*
* Envelope encryption scheme: an AES-256-CBC "data encryption key" (DEK/CEK) is itself
* encrypted with RSA-OAEP, and the result is carried as a dot-delimited string:
* keyVersion.base64(RSA-OAEP-encrypted CEK).base64(SHA-256 hash of CEK).base64(IV)
*
* ============================================================================
* !!! UNVERIFIED — MUST CONFIRM BEFORE PRODUCTION USE !!!
* The old Node code calls crypto.privateDecrypt/publicEncrypt with only
* { key, padding: RSA_PKCS1_OAEP_PADDING } — no oaepHash/mgf1Hash options.
* Node's crypto module defaults BOTH the OAEP hash and the MGF1 hash to SHA-1
* when unspecified. This class assumes that default (OAEPWithSHA-1AndMGF1Padding)
* to match. THIS HAS NOT BEEN EMPIRICALLY VERIFIED against a real round-trip
* between the Node Lambda and this Java code. If decryption fails or produces
* garbage, this is the first thing to check — try SHA-256 next.
* See KT doc — verify via round-trip test (Node-encrypt -> Java-decrypt) with a
* scratch RSA keypair before relying on this in any environment beyond local dev.
* ============================================================================
*/
public final class RSACryptoUtil {
private RSACryptoUtil() {
}
private static final String RSA_TRANSFORM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; // see warning above
private static final String AES_TRANSFORM = "AES/CBC/PKCS5Padding"; // Node aes-256-cbc default padding is PKCS#7,
// equivalent to PKCS5Padding in JCE for this block size
private static final OAEPParameterSpec OAEP_SHA1_SPEC = new OAEPParameterSpec(
"SHA-1", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
/**
* 1:1 port of utils.js decrypt(payload, xApicCryptoKey, secret).
*
* @param payload JsonNode with a "secure" field (base64 ciphertext) — mirrors payload.secure in JS
* @param xApicCryptoKey the dot-delimited key-material string from the x-apic-crypto-key header
* @param secret JsonNode (or Map-like) of secret material, expected to contain a key named
* "APICPayloadDecryptionKey-<keyVersion>" holding a PEM private key string
* @return decrypted payload as a String (caller is expected to JSON.parse it, matching old serviceFunc.js)
*/
public static String decrypt(JsonNode payload, String xApicCryptoKey, JsonNode secret) throws Exception {
// tokens[0]=keyVersion, [1]=encrypted_CEK_B64, [2]=hashCEK_B64 (UNUSED here, see class javadoc /
// KT doc — parsed only because it's positional in the dot-delimited format; it's the CEK integrity
// hash produced by encrypt(), decrypt() never needs it), [3]=iv_B64
String[] tokens = xApicCryptoKey.split("\\.");
String keyVersion = tokens[0];
String encryptedCekB64 = tokens[1];
// String hashCekB64 = tokens[2]; // intentionally unused — see javadoc above
String ivB64 = tokens[3];
String keyName = "APICPayloadDecryptionKey-" + keyVersion;
String pKeyRaw = secret.get(keyName).asText();
// Old JS: pKey.replace(/\\n/g, "\n") — un-escape literal backslash-n sequences from secret storage
// into real newlines, required for PEM parsing. MUST replicate exactly.
String pKeyPem = pKeyRaw.replace("\\n", "\n");
byte[] encryptedDek = Base64.getDecoder().decode(encryptedCekB64);
PrivateKey privateKey = parsePkcs8PrivateKeyFromPem(pKeyPem);
byte[] dataEncryptionKey = rsaOaepDecrypt(privateKey, encryptedDek);
byte[] iv = Base64.getDecoder().decode(ivB64);
Cipher aesCipher = Cipher.getInstance(AES_TRANSFORM);
aesCipher.init(Cipher.DECRYPT_MODE,
new javax.crypto.spec.SecretKeySpec(dataEncryptionKey, "AES"),
new IvParameterSpec(iv));
byte[] cipherBytes = Base64.getDecoder().decode(payload.get("secure").asText());
byte[] plainBytes = aesCipher.doFinal(cipherBytes);
return new String(plainBytes, java.nio.charset.StandardCharsets.UTF_8);
}
/**
* 1:1 port of utils.js encrypt(payload, pApicCryptoKey, secret, secretRKP).
*
* Two branches, matching the old JS exactly:
* - xApicCryptoKey == null: generate a fresh CEK/IV, RSA-OAEP encrypt the CEK with the
* PUBLIC cert from `secret` (APICPayloadEncryptionKey-<currentVersion>). This branch is
* NOT the one exercised by RSAAdapter's actual call path (RSA always passes an existing
* xApiCryptoKey from the request headers) — kept here because encrypt() is a shared
* utility other vendors/flows may use the generate-new-key branch for. Flagging this so
* it isn't mistaken for dead/untested code specific to RSA.
* - xApicCryptoKey != null: parse the existing dot-delimited key material and RSA-OAEP
* DECRYPT the CEK using the PRIVATE key from `secretRKP` (APICPayloadDecryptionKey-<keyVersion>),
* i.e. reuse the session key rather than generating a new one. THIS is the branch RSAAdapter
* actually hits, since analyzeAxios.js (and siblings) always pass a non-null xApiCryptoKey.
*
* @return result object equivalent to JS's { xApicCryptoKey, payload } — see EncryptResult below
*/
public static EncryptResult encrypt(String payload, String xApicCryptoKey, JsonNode secret, JsonNode secretRKP)
throws Exception {
String keyVersion;
String encryptedCekB64;
String hashCekB64;
String ivB64;
byte[] dataEncryptionKey;
if (xApicCryptoKey == null) {
// ---- Branch: generate new ephemeral key (NOT the RSA-adapter code path, see javadoc) ----
keyVersion = secret.get("currentVersion").asText();
SecureRandom random = new SecureRandom();
byte[] cek = new byte[32]; // 256-bit AES key
random.nextBytes(cek);
byte[] ivBytes = new byte[16];
random.nextBytes(ivBytes);
encryptedCekB64 = null; // computed below after RSA-OAEP encrypt
ivB64 = Base64.getEncoder().encodeToString(ivBytes);
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
hashCekB64 = Base64.getEncoder().encodeToString(sha256.digest(cek));
String certName = "APICPayloadEncryptionKey-" + keyVersion;
String certRaw = secret.get(certName).asText();
String certPem = certRaw.replace("\\n", "\n");
PublicKey publicKey = parseX509PublicKeyFromPem(certPem);
byte[] encryptedCek = rsaOaepEncrypt(publicKey, cek);
encryptedCekB64 = Base64.getEncoder().encodeToString(encryptedCek);
dataEncryptionKey = cek;
xApicCryptoKey = keyVersion + "." + encryptedCekB64 + "." + hashCekB64 + "." + ivB64;
} else {
// ---- Branch: reuse existing key material (THIS is RSAAdapter's actual code path) ----
String[] tokens = xApicCryptoKey.split("\\.");
keyVersion = tokens[0];
encryptedCekB64 = tokens[1];
hashCekB64 = tokens[2]; // unused below, same as decrypt() — see class javadoc
ivB64 = tokens[3];
String keyName = "APICPayloadDecryptionKey-" + keyVersion;
String pKeyRaw = secretRKP.get(keyName).asText();
String pKeyPem = pKeyRaw.replace("\\n", "\n");
PrivateKey privateKey = parsePkcs8PrivateKeyFromPem(pKeyPem);
byte[] encryptedCek = Base64.getDecoder().decode(encryptedCekB64);
dataEncryptionKey = rsaOaepDecrypt(privateKey, encryptedCek);
}
byte[] iv = Base64.getDecoder().decode(ivB64);
Cipher aesCipher = Cipher.getInstance(AES_TRANSFORM);
aesCipher.init(Cipher.ENCRYPT_MODE,
new javax.crypto.spec.SecretKeySpec(dataEncryptionKey, "AES"),
new IvParameterSpec(iv));
byte[] plainBytes = payload.getBytes(java.nio.charset.StandardCharsets.UTF_8);
byte[] cipherBytes = aesCipher.doFinal(plainBytes);
String theCipher = Base64.getEncoder().encodeToString(cipherBytes);
return new EncryptResult(xApicCryptoKey, theCipher);
}
/** Mirrors JS's `{ xApicCryptoKey, payload }` return shape from encrypt(). */
public static final class EncryptResult {
public final String xApicCryptoKey;
public final String payload;
public EncryptResult(String xApicCryptoKey, String payload) {
this.xApicCryptoKey = xApicCryptoKey;
this.payload = payload;
}
}
// ---- Internal helpers -----------------------------------------------------------------
private static byte[] rsaOaepDecrypt(PrivateKey privateKey, byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_TRANSFORM);
cipher.init(Cipher.DECRYPT_MODE, privateKey, OAEP_SHA1_SPEC);
return cipher.doFinal(data);
}
private static byte[] rsaOaepEncrypt(PublicKey publicKey, byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_TRANSFORM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey, OAEP_SHA1_SPEC);
return cipher.doFinal(data);
}
/**
* Parses a PEM-encoded PKCS#8 private key (the typical "-----BEGIN PRIVATE KEY-----" format).
* NOTE: if the secret's PEM is actually PKCS#1 ("-----BEGIN RSA PRIVATE KEY-----"), this will
* fail and will need a PKCS#1-aware parser instead (e.g. via BouncyCastle) — confirm the actual
* PEM header format from a real secret value before relying on this.
*/
private static PrivateKey parsePkcs8PrivateKeyFromPem(String pem) throws Exception {
String stripped = pem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] der = Base64.getDecoder().decode(stripped);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(new PKCS8EncodedKeySpec(der));
}
private static PublicKey parseX509PublicKeyFromPem(String pem) throws Exception {
// Handles both "-----BEGIN PUBLIC KEY-----" and "-----BEGIN CERTIFICATE-----" forms.
// If `pem` is a full X.509 certificate (not a bare public key), this strip-based approach
// will need to go through CertificateFactory instead — confirm actual secret format.
String stripped = pem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\s+", "");
byte[] der = Base64.getDecoder().decode(stripped);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(new X509EncodedKeySpec(der));
}
}
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)