Pārlūkot izejas kodu

feat: US-009 - Crypto Node - Encrypt & Decrypt

Add AES-256-CBC, AES-256-GCM, and RSA encryption/decryption support:
- AES supports both raw 256-bit keys (base64) and passphrase-derived keys (PBKDF2)
- IV auto-generated if not provided for encryption
- RSA uses OAEP padding with PEM format keys
- Input/output supports both string and base64 formats
- Clear error messages for invalid keys or corrupted data
fszontagh 6 mēneši atpakaļ
vecāks
revīzija
73b6df9e73
2 mainītis faili ar 1224 papildinājumiem un 12 dzēšanām
  1. 329 12
      nodes/crypto/crypto.js
  2. 895 0
      src/runner/engine/script_engine.cpp

+ 329 - 12
nodes/crypto/crypto.js

@@ -2,8 +2,8 @@
  * @node crypto
  * @name Crypto
  * @category crypto
- * @version 1.0.0
- * @description Hash data, generate HMACs, and perform cryptographic operations for data integrity and message authentication
+ * @version 1.1.0
+ * @description Hash data, generate HMACs, encrypt/decrypt data using AES or RSA, and generate random values
  * @icon lock
  */
 
@@ -14,7 +14,7 @@ const configSchema = {
             type: 'string',
             title: 'Operation',
             enum: ['hash', 'hmac', 'random', 'encrypt', 'decrypt'],
-            enumLabels: ['Hash', 'HMAC', 'Random Generation', 'Encrypt (Coming Soon)', 'Decrypt (Coming Soon)'],
+            enumLabels: ['Hash', 'HMAC', 'Random Generation', 'Encrypt', 'Decrypt'],
             default: 'hash',
             description: 'Cryptographic operation to perform'
         },
@@ -87,6 +87,64 @@ const configSchema = {
             default: true,
             description: 'Include symbols (!@#$%^&*()_+-=[]{}|;:,.<>?)',
             showWhen: { field: 'randomType', value: 'password' }
+        },
+        encryptionAlgorithm: {
+            type: 'string',
+            title: 'Encryption Algorithm',
+            enum: ['aes-256-gcm', 'aes-256-cbc', 'rsa'],
+            enumLabels: ['AES-256-GCM (Recommended)', 'AES-256-CBC', 'RSA'],
+            default: 'aes-256-gcm',
+            description: 'Encryption algorithm to use',
+            showWhen: { field: 'operation', value: ['encrypt', 'decrypt'] }
+        },
+        encryptionKey: {
+            type: 'string',
+            title: 'Encryption Key',
+            description: 'For AES: Base64-encoded 256-bit key OR passphrase (set Use Passphrase to true). For RSA encrypt: PEM public key. Supports {{variable}} interpolation.',
+            showWhen: { field: 'operation', value: 'encrypt' }
+        },
+        decryptionKey: {
+            type: 'string',
+            title: 'Decryption Key',
+            description: 'For AES: Base64-encoded 256-bit key OR passphrase (set Use Passphrase to true). For RSA decrypt: PEM private key. Supports {{variable}} interpolation.',
+            showWhen: { field: 'operation', value: 'decrypt' }
+        },
+        usePassphrase: {
+            type: 'boolean',
+            title: 'Use Passphrase',
+            default: false,
+            description: 'If true, the key is treated as a passphrase and a 256-bit key is derived using PBKDF2',
+            showWhen: { field: 'encryptionAlgorithm', value: ['aes-256-gcm', 'aes-256-cbc'] }
+        },
+        iv: {
+            type: 'string',
+            title: 'IV (Initialization Vector)',
+            description: 'Optional. Base64-encoded IV. If not provided, a random IV will be generated for encryption. Required for decryption.',
+            showWhen: { field: 'encryptionAlgorithm', value: ['aes-256-gcm', 'aes-256-cbc'] }
+        },
+        authTag: {
+            type: 'string',
+            title: 'Authentication Tag',
+            description: 'Required for AES-GCM decryption. Base64-encoded authentication tag.',
+            showWhen: { field: 'operation', value: 'decrypt' }
+        },
+        inputFormat: {
+            type: 'string',
+            title: 'Input Format',
+            enum: ['string', 'base64'],
+            enumLabels: ['String (UTF-8)', 'Base64'],
+            default: 'string',
+            description: 'Format of the input data for encryption, or ciphertext format for decryption',
+            showWhen: { field: 'operation', value: ['encrypt', 'decrypt'] }
+        },
+        outputDataFormat: {
+            type: 'string',
+            title: 'Output Format',
+            enum: ['base64', 'string'],
+            enumLabels: ['Base64', 'String (UTF-8)'],
+            default: 'base64',
+            description: 'Format of the encrypted ciphertext output, or decrypted plaintext output',
+            showWhen: { field: 'operation', value: ['encrypt', 'decrypt'] }
         }
     },
     required: ['operation']
@@ -97,11 +155,35 @@ const inputSchema = {
     properties: {
         data: {
             type: 'any',
-            description: 'Data to hash or process. Can be a string or will be JSON stringified.'
+            description: 'Data to process. For hash/hmac: string or will be JSON stringified. For encrypt: plaintext. For decrypt: ciphertext.'
         },
         secretKey: {
             type: 'string',
             description: 'Override secret key for HMAC from input'
+        },
+        encryptionKey: {
+            type: 'string',
+            description: 'Override encryption key from input (for encrypt operation)'
+        },
+        decryptionKey: {
+            type: 'string',
+            description: 'Override decryption key from input (for decrypt operation)'
+        },
+        iv: {
+            type: 'string',
+            description: 'Override IV from input (for AES decrypt)'
+        },
+        authTag: {
+            type: 'string',
+            description: 'Override auth tag from input (for AES-GCM decrypt)'
+        },
+        publicKey: {
+            type: 'string',
+            description: 'RSA public key in PEM format (for RSA encrypt)'
+        },
+        privateKey: {
+            type: 'string',
+            description: 'RSA private key in PEM format (for RSA decrypt)'
         }
     }
 };
@@ -111,7 +193,7 @@ const outputSchema = {
     properties: {
         result: {
             type: 'string',
-            description: 'The cryptographic operation result'
+            description: 'The cryptographic operation result (hash, encrypted data, or decrypted data)'
         },
         algorithm: {
             type: 'string',
@@ -136,6 +218,14 @@ const outputSchema = {
         inputLength: {
             type: 'number',
             description: 'Length of the input data in bytes'
+        },
+        iv: {
+            type: 'string',
+            description: 'Base64-encoded IV used for encryption (save this for decryption)'
+        },
+        authTag: {
+            type: 'string',
+            description: 'Base64-encoded authentication tag for AES-GCM (save this for decryption)'
         }
     }
 };
@@ -160,20 +250,16 @@ function generateRandomString(length, charset) {
     const charsetLength = charset.length;
     let result = '';
 
-    // Generate enough random bytes to select characters
-    // We need to handle modulo bias by rejecting values that would cause bias
     const bytesNeeded = length;
     const randomHex = smartbotic.crypto.randomBytes(bytesNeeded * 2);
 
     let pos = 0;
     while (result.length < length) {
         if (pos >= randomHex.length) {
-            // Need more random bytes
             const moreBytes = smartbotic.crypto.randomBytes((length - result.length) * 2);
             pos = 0;
             for (let i = 0; i < moreBytes.length && result.length < length; i += 2) {
                 const byte = parseInt(moreBytes.substr(i, 2), 16);
-                // Use rejection sampling to avoid modulo bias
                 const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength;
                 if (byte < maxUnbiased) {
                     result += charset[byte % charsetLength];
@@ -182,7 +268,6 @@ function generateRandomString(length, charset) {
         } else {
             const byte = parseInt(randomHex.substr(pos, 2), 16);
             pos += 2;
-            // Use rejection sampling to avoid modulo bias
             const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength;
             if (byte < maxUnbiased) {
                 result += charset[byte % charsetLength];
@@ -196,8 +281,12 @@ function generateRandomString(length, charset) {
 async function execute(config, input, context) {
     const operation = config.operation || 'hash';
 
-    if (operation === 'encrypt' || operation === 'decrypt') {
-        throw new Error('Encrypt and Decrypt operations are not yet implemented. Coming soon.');
+    if (operation === 'encrypt') {
+        return executeEncrypt(config, input);
+    }
+
+    if (operation === 'decrypt') {
+        return executeDecrypt(config, input);
     }
 
     if (operation === 'random') {
@@ -326,4 +415,232 @@ async function execute(config, input, context) {
     throw new Error('Unknown operation: ' + operation);
 }
 
+function executeEncrypt(config, input) {
+    const encryptionAlgorithm = config.encryptionAlgorithm || 'aes-256-gcm';
+    const inputFormat = config.inputFormat || 'string';
+
+    let inputData = input.data !== undefined ? input.data : input;
+
+    if (typeof inputData !== 'string') {
+        if (inputData === null || inputData === undefined) {
+            throw new Error('No data provided for encryption');
+        }
+        inputData = JSON.stringify(inputData);
+    }
+
+    let dataToEncrypt = inputData;
+    if (inputFormat === 'base64') {
+        dataToEncrypt = smartbotic.utils.base64Decode(inputData);
+        if (!dataToEncrypt) {
+            throw new Error('Invalid base64 input data');
+        }
+    }
+
+    if (encryptionAlgorithm === 'rsa') {
+        return executeRsaEncrypt(config, input, dataToEncrypt);
+    }
+
+    return executeAesEncrypt(config, input, dataToEncrypt, encryptionAlgorithm);
+}
+
+function executeAesEncrypt(config, input, dataToEncrypt, encryptionAlgorithm) {
+    let key = input.encryptionKey || config.encryptionKey;
+    if (key && typeof key === 'string') {
+        key = interpolate(key, input);
+    }
+
+    if (!key) {
+        throw new Error('Encryption key is required for AES encryption');
+    }
+
+    let iv = input.iv || config.iv;
+    if (iv && typeof iv === 'string') {
+        iv = interpolate(iv, input);
+    }
+
+    const usePassphrase = config.usePassphrase === true;
+    const algorithm = encryptionAlgorithm === 'aes-256-cbc' ? 'cbc' : 'gcm';
+
+    smartbotic.log.info('Encrypting ' + dataToEncrypt.length + ' bytes with AES-256-' + algorithm.toUpperCase());
+
+    const result = smartbotic.crypto.aesEncrypt({
+        data: dataToEncrypt,
+        key: key,
+        iv: iv || '',
+        algorithm: algorithm,
+        keyIsPassphrase: usePassphrase
+    });
+
+    if (result.error) {
+        throw new Error('Encryption failed: ' + result.error);
+    }
+
+    const outputResult = {
+        result: result.ciphertext,
+        operation: 'encrypt',
+        algorithm: encryptionAlgorithm,
+        iv: result.iv,
+        inputLength: dataToEncrypt.length
+    };
+
+    if (algorithm === 'gcm') {
+        outputResult.authTag = result.authTag;
+    }
+
+    return outputResult;
+}
+
+function executeRsaEncrypt(config, input, dataToEncrypt) {
+    let publicKey = input.publicKey || input.encryptionKey || config.encryptionKey;
+    if (publicKey && typeof publicKey === 'string') {
+        publicKey = interpolate(publicKey, input);
+    }
+
+    if (!publicKey) {
+        throw new Error('Public key is required for RSA encryption');
+    }
+
+    if (!publicKey.includes('-----BEGIN')) {
+        throw new Error('Public key must be in PEM format (starting with -----BEGIN PUBLIC KEY-----)');
+    }
+
+    smartbotic.log.info('Encrypting ' + dataToEncrypt.length + ' bytes with RSA');
+
+    const result = smartbotic.crypto.rsaEncrypt({
+        data: dataToEncrypt,
+        publicKey: publicKey
+    });
+
+    if (result.error) {
+        throw new Error('RSA encryption failed: ' + result.error);
+    }
+
+    return {
+        result: result.ciphertext,
+        operation: 'encrypt',
+        algorithm: 'rsa',
+        inputLength: dataToEncrypt.length
+    };
+}
+
+function executeDecrypt(config, input) {
+    const encryptionAlgorithm = config.encryptionAlgorithm || 'aes-256-gcm';
+    const outputDataFormat = config.outputDataFormat || 'base64';
+
+    let inputData = input.data !== undefined ? input.data : input;
+
+    if (typeof inputData !== 'string') {
+        if (inputData === null || inputData === undefined) {
+            throw new Error('No data provided for decryption');
+        }
+        throw new Error('Ciphertext must be a base64-encoded string');
+    }
+
+    if (encryptionAlgorithm === 'rsa') {
+        return executeRsaDecrypt(config, input, inputData, outputDataFormat);
+    }
+
+    return executeAesDecrypt(config, input, inputData, encryptionAlgorithm, outputDataFormat);
+}
+
+function executeAesDecrypt(config, input, ciphertext, encryptionAlgorithm, outputDataFormat) {
+    let key = input.decryptionKey || config.decryptionKey;
+    if (key && typeof key === 'string') {
+        key = interpolate(key, input);
+    }
+
+    if (!key) {
+        throw new Error('Decryption key is required for AES decryption');
+    }
+
+    let iv = input.iv || config.iv;
+    if (iv && typeof iv === 'string') {
+        iv = interpolate(iv, input);
+    }
+
+    if (!iv) {
+        throw new Error('IV is required for AES decryption');
+    }
+
+    const usePassphrase = config.usePassphrase === true;
+    const algorithm = encryptionAlgorithm === 'aes-256-cbc' ? 'cbc' : 'gcm';
+
+    const decryptOptions = {
+        ciphertext: ciphertext,
+        key: key,
+        iv: iv,
+        algorithm: algorithm,
+        keyIsPassphrase: usePassphrase
+    };
+
+    if (algorithm === 'gcm') {
+        let authTag = input.authTag || config.authTag;
+        if (authTag && typeof authTag === 'string') {
+            authTag = interpolate(authTag, input);
+        }
+
+        if (!authTag) {
+            throw new Error('Authentication tag is required for AES-GCM decryption');
+        }
+
+        decryptOptions.authTag = authTag;
+    }
+
+    smartbotic.log.info('Decrypting with AES-256-' + algorithm.toUpperCase());
+
+    const result = smartbotic.crypto.aesDecrypt(decryptOptions);
+
+    if (result.error) {
+        throw new Error('Decryption failed: ' + result.error);
+    }
+
+    let outputResult = result.plaintext;
+    if (outputDataFormat === 'base64') {
+        outputResult = smartbotic.utils.base64Encode(result.plaintext);
+    }
+
+    return {
+        result: outputResult,
+        operation: 'decrypt',
+        algorithm: encryptionAlgorithm
+    };
+}
+
+function executeRsaDecrypt(config, input, ciphertext, outputDataFormat) {
+    let privateKey = input.privateKey || input.decryptionKey || config.decryptionKey;
+    if (privateKey && typeof privateKey === 'string') {
+        privateKey = interpolate(privateKey, input);
+    }
+
+    if (!privateKey) {
+        throw new Error('Private key is required for RSA decryption');
+    }
+
+    if (!privateKey.includes('-----BEGIN')) {
+        throw new Error('Private key must be in PEM format (starting with -----BEGIN PRIVATE KEY----- or -----BEGIN RSA PRIVATE KEY-----)');
+    }
+
+    smartbotic.log.info('Decrypting with RSA');
+
+    const result = smartbotic.crypto.rsaDecrypt({
+        ciphertext: ciphertext,
+        privateKey: privateKey
+    });
+
+    if (result.error) {
+        throw new Error('RSA decryption failed: ' + result.error);
+    }
+
+    let outputResult = result.plaintext;
+    if (outputDataFormat === 'base64') {
+        outputResult = smartbotic.utils.base64Encode(result.plaintext);
+    }
+
+    return {
+        result: outputResult,
+        operation: 'decrypt',
+        algorithm: 'rsa'
+    };
+}
+
 module.exports = { configSchema, inputSchema, outputSchema, execute };

+ 895 - 0
src/runner/engine/script_engine.cpp

@@ -24,6 +24,10 @@
 #include <openssl/hmac.h>
 #include <openssl/md5.h>
 #include <openssl/rand.h>
+#include <openssl/pem.h>
+#include <openssl/rsa.h>
+#include <openssl/bio.h>
+#include <openssl/err.h>
 
 namespace smartbotic::runner::engine {
 
@@ -171,6 +175,596 @@ static std::string generateRandomBytes(int length) {
     return ss.str();
 }
 
+// Convert hex string to bytes
+static std::vector<unsigned char> hexToBytes(const std::string& hex) {
+    std::vector<unsigned char> bytes;
+    bytes.reserve(hex.length() / 2);
+    for (size_t i = 0; i + 1 < hex.length(); i += 2) {
+        unsigned char byte = static_cast<unsigned char>(std::stoul(hex.substr(i, 2), nullptr, 16));
+        bytes.push_back(byte);
+    }
+    return bytes;
+}
+
+// Convert bytes to hex string
+static std::string bytesToHex(const std::vector<unsigned char>& bytes) {
+    std::ostringstream ss;
+    for (const auto& byte : bytes) {
+        ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(byte);
+    }
+    return ss.str();
+}
+
+// AES encryption result structure
+struct AesEncryptResult {
+    std::string ciphertext;  // base64 encoded
+    std::string iv;          // base64 encoded
+    std::string authTag;     // base64 encoded (only for GCM)
+    std::string error;
+};
+
+// AES decryption result structure
+struct AesDecryptResult {
+    std::string plaintext;
+    std::string error;
+};
+
+// Derive key from passphrase using PBKDF2
+static std::vector<unsigned char> deriveKeyFromPassphrase(const std::string& passphrase, const std::vector<unsigned char>& salt, int iterations = 100000) {
+    std::vector<unsigned char> key(32);  // 256 bits for AES-256
+
+    if (PKCS5_PBKDF2_HMAC(
+            passphrase.c_str(),
+            static_cast<int>(passphrase.length()),
+            salt.data(),
+            static_cast<int>(salt.size()),
+            iterations,
+            EVP_sha256(),
+            static_cast<int>(key.size()),
+            key.data()) != 1) {
+        return {};
+    }
+
+    return key;
+}
+
+// AES-256-CBC encryption
+static AesEncryptResult aes256CbcEncrypt(const std::string& plaintext, const std::string& keyBase64, const std::string& ivBase64, bool keyIsPassphrase) {
+    AesEncryptResult result;
+
+    // Decode or generate IV
+    std::vector<unsigned char> iv;
+    if (ivBase64.empty()) {
+        iv.resize(16);  // 128-bit IV for AES-CBC
+        if (RAND_bytes(iv.data(), static_cast<int>(iv.size())) != 1) {
+            result.error = "Failed to generate IV";
+            return result;
+        }
+    } else {
+        std::string decoded = base64Decode(ivBase64);
+        if (decoded.empty() || decoded.size() != 16) {
+            result.error = "IV must be 16 bytes (128 bits) when base64 decoded";
+            return result;
+        }
+        iv.assign(decoded.begin(), decoded.end());
+    }
+
+    // Get key
+    std::vector<unsigned char> key;
+    if (keyIsPassphrase) {
+        key = deriveKeyFromPassphrase(keyBase64, iv);
+        if (key.empty()) {
+            result.error = "Failed to derive key from passphrase";
+            return result;
+        }
+    } else {
+        std::string decoded = base64Decode(keyBase64);
+        if (decoded.empty() || decoded.size() != 32) {
+            result.error = "Key must be 32 bytes (256 bits) when base64 decoded";
+            return result;
+        }
+        key.assign(decoded.begin(), decoded.end());
+    }
+
+    // Create cipher context
+    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
+    if (!ctx) {
+        result.error = "Failed to create cipher context";
+        return result;
+    }
+
+    // Initialize encryption
+    if (EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, key.data(), iv.data()) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to initialize encryption";
+        return result;
+    }
+
+    // Encrypt
+    std::vector<unsigned char> ciphertext(plaintext.size() + EVP_MAX_BLOCK_LENGTH);
+    int out_len = 0;
+    int total_len = 0;
+
+    if (EVP_EncryptUpdate(ctx, ciphertext.data(), &out_len,
+                          reinterpret_cast<const unsigned char*>(plaintext.data()),
+                          static_cast<int>(plaintext.size())) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Encryption failed";
+        return result;
+    }
+    total_len = out_len;
+
+    if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + total_len, &out_len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Encryption finalization failed";
+        return result;
+    }
+    total_len += out_len;
+    ciphertext.resize(total_len);
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    // Encode results
+    result.ciphertext = base64Encode(std::string(ciphertext.begin(), ciphertext.end()));
+    result.iv = base64Encode(std::string(iv.begin(), iv.end()));
+
+    return result;
+}
+
+// AES-256-CBC decryption
+static AesDecryptResult aes256CbcDecrypt(const std::string& ciphertextBase64, const std::string& keyBase64, const std::string& ivBase64, bool keyIsPassphrase) {
+    AesDecryptResult result;
+
+    // Decode ciphertext
+    std::string ciphertextStr = base64Decode(ciphertextBase64);
+    if (ciphertextStr.empty() && !ciphertextBase64.empty()) {
+        result.error = "Invalid base64 encoded ciphertext";
+        return result;
+    }
+    std::vector<unsigned char> ciphertext(ciphertextStr.begin(), ciphertextStr.end());
+
+    // Decode IV
+    std::string ivStr = base64Decode(ivBase64);
+    if (ivStr.empty() || ivStr.size() != 16) {
+        result.error = "IV must be 16 bytes (128 bits) when base64 decoded";
+        return result;
+    }
+    std::vector<unsigned char> iv(ivStr.begin(), ivStr.end());
+
+    // Get key
+    std::vector<unsigned char> key;
+    if (keyIsPassphrase) {
+        key = deriveKeyFromPassphrase(keyBase64, iv);
+        if (key.empty()) {
+            result.error = "Failed to derive key from passphrase";
+            return result;
+        }
+    } else {
+        std::string decoded = base64Decode(keyBase64);
+        if (decoded.empty() || decoded.size() != 32) {
+            result.error = "Key must be 32 bytes (256 bits) when base64 decoded";
+            return result;
+        }
+        key.assign(decoded.begin(), decoded.end());
+    }
+
+    // Create cipher context
+    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
+    if (!ctx) {
+        result.error = "Failed to create cipher context";
+        return result;
+    }
+
+    // Initialize decryption
+    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, key.data(), iv.data()) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to initialize decryption";
+        return result;
+    }
+
+    // Decrypt
+    std::vector<unsigned char> plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH);
+    int out_len = 0;
+    int total_len = 0;
+
+    if (EVP_DecryptUpdate(ctx, plaintext.data(), &out_len, ciphertext.data(), static_cast<int>(ciphertext.size())) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Decryption failed - data may be corrupted";
+        return result;
+    }
+    total_len = out_len;
+
+    if (EVP_DecryptFinal_ex(ctx, plaintext.data() + total_len, &out_len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Decryption failed - invalid key or corrupted data";
+        return result;
+    }
+    total_len += out_len;
+    plaintext.resize(total_len);
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    result.plaintext = std::string(plaintext.begin(), plaintext.end());
+    return result;
+}
+
+// AES-256-GCM encryption
+static AesEncryptResult aes256GcmEncrypt(const std::string& plaintext, const std::string& keyBase64, const std::string& ivBase64, bool keyIsPassphrase) {
+    AesEncryptResult result;
+
+    // Decode or generate IV (12 bytes for GCM)
+    std::vector<unsigned char> iv;
+    if (ivBase64.empty()) {
+        iv.resize(12);  // 96-bit IV for AES-GCM (recommended)
+        if (RAND_bytes(iv.data(), static_cast<int>(iv.size())) != 1) {
+            result.error = "Failed to generate IV";
+            return result;
+        }
+    } else {
+        std::string decoded = base64Decode(ivBase64);
+        if (decoded.empty() || decoded.size() != 12) {
+            result.error = "IV must be 12 bytes (96 bits) when base64 decoded for GCM mode";
+            return result;
+        }
+        iv.assign(decoded.begin(), decoded.end());
+    }
+
+    // Get key
+    std::vector<unsigned char> key;
+    if (keyIsPassphrase) {
+        key = deriveKeyFromPassphrase(keyBase64, iv);
+        if (key.empty()) {
+            result.error = "Failed to derive key from passphrase";
+            return result;
+        }
+    } else {
+        std::string decoded = base64Decode(keyBase64);
+        if (decoded.empty() || decoded.size() != 32) {
+            result.error = "Key must be 32 bytes (256 bits) when base64 decoded";
+            return result;
+        }
+        key.assign(decoded.begin(), decoded.end());
+    }
+
+    // Create cipher context
+    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
+    if (!ctx) {
+        result.error = "Failed to create cipher context";
+        return result;
+    }
+
+    // Initialize encryption
+    if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to initialize encryption";
+        return result;
+    }
+
+    // Set IV length
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to set IV length";
+        return result;
+    }
+
+    // Set key and IV
+    if (EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to set key and IV";
+        return result;
+    }
+
+    // Encrypt
+    std::vector<unsigned char> ciphertext(plaintext.size() + EVP_MAX_BLOCK_LENGTH);
+    int out_len = 0;
+    int total_len = 0;
+
+    if (EVP_EncryptUpdate(ctx, ciphertext.data(), &out_len,
+                          reinterpret_cast<const unsigned char*>(plaintext.data()),
+                          static_cast<int>(plaintext.size())) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Encryption failed";
+        return result;
+    }
+    total_len = out_len;
+
+    if (EVP_EncryptFinal_ex(ctx, ciphertext.data() + total_len, &out_len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Encryption finalization failed";
+        return result;
+    }
+    total_len += out_len;
+    ciphertext.resize(total_len);
+
+    // Get authentication tag
+    std::vector<unsigned char> authTag(16);
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, authTag.data()) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to get authentication tag";
+        return result;
+    }
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    // Encode results
+    result.ciphertext = base64Encode(std::string(ciphertext.begin(), ciphertext.end()));
+    result.iv = base64Encode(std::string(iv.begin(), iv.end()));
+    result.authTag = base64Encode(std::string(authTag.begin(), authTag.end()));
+
+    return result;
+}
+
+// AES-256-GCM decryption
+static AesDecryptResult aes256GcmDecrypt(const std::string& ciphertextBase64, const std::string& keyBase64, const std::string& ivBase64, const std::string& authTagBase64, bool keyIsPassphrase) {
+    AesDecryptResult result;
+
+    // Decode ciphertext
+    std::string ciphertextStr = base64Decode(ciphertextBase64);
+    if (ciphertextStr.empty() && !ciphertextBase64.empty()) {
+        result.error = "Invalid base64 encoded ciphertext";
+        return result;
+    }
+    std::vector<unsigned char> ciphertext(ciphertextStr.begin(), ciphertextStr.end());
+
+    // Decode IV
+    std::string ivStr = base64Decode(ivBase64);
+    if (ivStr.empty() || ivStr.size() != 12) {
+        result.error = "IV must be 12 bytes (96 bits) when base64 decoded for GCM mode";
+        return result;
+    }
+    std::vector<unsigned char> iv(ivStr.begin(), ivStr.end());
+
+    // Decode auth tag
+    std::string authTagStr = base64Decode(authTagBase64);
+    if (authTagStr.empty() || authTagStr.size() != 16) {
+        result.error = "Authentication tag must be 16 bytes (128 bits) when base64 decoded";
+        return result;
+    }
+    std::vector<unsigned char> authTag(authTagStr.begin(), authTagStr.end());
+
+    // Get key
+    std::vector<unsigned char> key;
+    if (keyIsPassphrase) {
+        key = deriveKeyFromPassphrase(keyBase64, iv);
+        if (key.empty()) {
+            result.error = "Failed to derive key from passphrase";
+            return result;
+        }
+    } else {
+        std::string decoded = base64Decode(keyBase64);
+        if (decoded.empty() || decoded.size() != 32) {
+            result.error = "Key must be 32 bytes (256 bits) when base64 decoded";
+            return result;
+        }
+        key.assign(decoded.begin(), decoded.end());
+    }
+
+    // Create cipher context
+    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
+    if (!ctx) {
+        result.error = "Failed to create cipher context";
+        return result;
+    }
+
+    // Initialize decryption
+    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to initialize decryption";
+        return result;
+    }
+
+    // Set IV length
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to set IV length";
+        return result;
+    }
+
+    // Set key and IV
+    if (EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data()) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to set key and IV";
+        return result;
+    }
+
+    // Decrypt
+    std::vector<unsigned char> plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH);
+    int out_len = 0;
+    int total_len = 0;
+
+    if (EVP_DecryptUpdate(ctx, plaintext.data(), &out_len, ciphertext.data(), static_cast<int>(ciphertext.size())) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Decryption failed - data may be corrupted";
+        return result;
+    }
+    total_len = out_len;
+
+    // Set expected authentication tag
+    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, authTag.data()) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Failed to set authentication tag";
+        return result;
+    }
+
+    // Finalize and verify tag
+    if (EVP_DecryptFinal_ex(ctx, plaintext.data() + total_len, &out_len) != 1) {
+        EVP_CIPHER_CTX_free(ctx);
+        result.error = "Authentication failed - data may be corrupted or tampered with";
+        return result;
+    }
+    total_len += out_len;
+    plaintext.resize(total_len);
+
+    EVP_CIPHER_CTX_free(ctx);
+
+    result.plaintext = std::string(plaintext.begin(), plaintext.end());
+    return result;
+}
+
+// RSA encryption result structure
+struct RsaEncryptResult {
+    std::string ciphertext;  // base64 encoded
+    std::string error;
+};
+
+// RSA decryption result structure
+struct RsaDecryptResult {
+    std::string plaintext;
+    std::string error;
+};
+
+// RSA encryption with public key (PEM format)
+static RsaEncryptResult rsaEncrypt(const std::string& plaintext, const std::string& publicKeyPem) {
+    RsaEncryptResult result;
+
+    // Create BIO from PEM string
+    BIO* bio = BIO_new_mem_buf(publicKeyPem.data(), static_cast<int>(publicKeyPem.size()));
+    if (!bio) {
+        result.error = "Failed to create BIO for public key";
+        return result;
+    }
+
+    // Read public key
+    EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
+    BIO_free(bio);
+
+    if (!pkey) {
+        result.error = "Failed to parse public key - ensure it is in valid PEM format";
+        return result;
+    }
+
+    // Create encryption context
+    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
+    if (!ctx) {
+        EVP_PKEY_free(pkey);
+        result.error = "Failed to create encryption context";
+        return result;
+    }
+
+    // Initialize encryption
+    if (EVP_PKEY_encrypt_init(ctx) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        EVP_PKEY_free(pkey);
+        result.error = "Failed to initialize RSA encryption";
+        return result;
+    }
+
+    // Set OAEP padding (more secure than PKCS1)
+    if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        EVP_PKEY_free(pkey);
+        result.error = "Failed to set RSA padding";
+        return result;
+    }
+
+    // Determine output size
+    size_t outlen = 0;
+    if (EVP_PKEY_encrypt(ctx, nullptr, &outlen,
+                         reinterpret_cast<const unsigned char*>(plaintext.data()),
+                         plaintext.size()) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        EVP_PKEY_free(pkey);
+        result.error = "Failed to determine encrypted data size";
+        return result;
+    }
+
+    // Encrypt
+    std::vector<unsigned char> ciphertext(outlen);
+    if (EVP_PKEY_encrypt(ctx, ciphertext.data(), &outlen,
+                         reinterpret_cast<const unsigned char*>(plaintext.data()),
+                         plaintext.size()) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        EVP_PKEY_free(pkey);
+        result.error = "RSA encryption failed - data may be too large for key size";
+        return result;
+    }
+    ciphertext.resize(outlen);
+
+    EVP_PKEY_CTX_free(ctx);
+    EVP_PKEY_free(pkey);
+
+    result.ciphertext = base64Encode(std::string(ciphertext.begin(), ciphertext.end()));
+    return result;
+}
+
+// RSA decryption with private key (PEM format)
+static RsaDecryptResult rsaDecrypt(const std::string& ciphertextBase64, const std::string& privateKeyPem) {
+    RsaDecryptResult result;
+
+    // Decode ciphertext
+    std::string ciphertextStr = base64Decode(ciphertextBase64);
+    if (ciphertextStr.empty() && !ciphertextBase64.empty()) {
+        result.error = "Invalid base64 encoded ciphertext";
+        return result;
+    }
+    std::vector<unsigned char> ciphertext(ciphertextStr.begin(), ciphertextStr.end());
+
+    // Create BIO from PEM string
+    BIO* bio = BIO_new_mem_buf(privateKeyPem.data(), static_cast<int>(privateKeyPem.size()));
+    if (!bio) {
+        result.error = "Failed to create BIO for private key";
+        return result;
+    }
+
+    // Read private key
+    EVP_PKEY* pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
+    BIO_free(bio);
+
+    if (!pkey) {
+        result.error = "Failed to parse private key - ensure it is in valid PEM format";
+        return result;
+    }
+
+    // Create decryption context
+    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(pkey, nullptr);
+    if (!ctx) {
+        EVP_PKEY_free(pkey);
+        result.error = "Failed to create decryption context";
+        return result;
+    }
+
+    // Initialize decryption
+    if (EVP_PKEY_decrypt_init(ctx) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        EVP_PKEY_free(pkey);
+        result.error = "Failed to initialize RSA decryption";
+        return result;
+    }
+
+    // Set OAEP padding
+    if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        EVP_PKEY_free(pkey);
+        result.error = "Failed to set RSA padding";
+        return result;
+    }
+
+    // Determine output size
+    size_t outlen = 0;
+    if (EVP_PKEY_decrypt(ctx, nullptr, &outlen, ciphertext.data(), ciphertext.size()) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        EVP_PKEY_free(pkey);
+        result.error = "Failed to determine decrypted data size";
+        return result;
+    }
+
+    // Decrypt
+    std::vector<unsigned char> plaintext(outlen);
+    if (EVP_PKEY_decrypt(ctx, plaintext.data(), &outlen, ciphertext.data(), ciphertext.size()) <= 0) {
+        EVP_PKEY_CTX_free(ctx);
+        EVP_PKEY_free(pkey);
+        result.error = "RSA decryption failed - invalid private key or corrupted data";
+        return result;
+    }
+    plaintext.resize(outlen);
+
+    EVP_PKEY_CTX_free(ctx);
+    EVP_PKEY_free(pkey);
+
+    result.plaintext = std::string(plaintext.begin(), plaintext.end());
+    return result;
+}
+
 // Helper structure for curl response
 struct CurlResponse {
     std::string body;
@@ -1172,6 +1766,307 @@ void ScriptEngine::setupBuiltinAPIs() {
         return JS_NewString(ctx, uuid.c_str());
     }, "randomUUID", 0));
 
+    // crypto.aesEncrypt(options) - AES-256-CBC/GCM encryption
+    // options: { data, key, iv (optional), algorithm ('cbc' or 'gcm'), keyIsPassphrase (bool) }
+    // Returns: { ciphertext, iv, authTag (GCM only), error }
+    JS_SetPropertyStr(ctx, crypto, "aesEncrypt", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1 || !JS_IsObject(argv[0])) {
+            return JS_ThrowTypeError(ctx, "crypto.aesEncrypt requires an options object");
+        }
+
+        JSValue options = argv[0];
+
+        // Get data
+        JSValue data_val = JS_GetPropertyStr(ctx, options, "data");
+        if (JS_IsUndefined(data_val)) {
+            return JS_ThrowTypeError(ctx, "data is required");
+        }
+        const char* data_str = JS_ToCString(ctx, data_val);
+        JS_FreeValue(ctx, data_val);
+        if (!data_str) {
+            return JS_ThrowTypeError(ctx, "data must be a string");
+        }
+        std::string data(data_str);
+        JS_FreeCString(ctx, data_str);
+
+        // Get key
+        JSValue key_val = JS_GetPropertyStr(ctx, options, "key");
+        if (JS_IsUndefined(key_val)) {
+            return JS_ThrowTypeError(ctx, "key is required");
+        }
+        const char* key_str = JS_ToCString(ctx, key_val);
+        JS_FreeValue(ctx, key_val);
+        if (!key_str) {
+            return JS_ThrowTypeError(ctx, "key must be a string");
+        }
+        std::string key(key_str);
+        JS_FreeCString(ctx, key_str);
+
+        // Get IV (optional)
+        std::string iv;
+        JSValue iv_val = JS_GetPropertyStr(ctx, options, "iv");
+        if (!JS_IsUndefined(iv_val) && !JS_IsNull(iv_val)) {
+            const char* iv_str = JS_ToCString(ctx, iv_val);
+            if (iv_str) {
+                iv = iv_str;
+                JS_FreeCString(ctx, iv_str);
+            }
+        }
+        JS_FreeValue(ctx, iv_val);
+
+        // Get algorithm (default: gcm)
+        std::string algorithm = "gcm";
+        JSValue algo_val = JS_GetPropertyStr(ctx, options, "algorithm");
+        if (!JS_IsUndefined(algo_val) && !JS_IsNull(algo_val)) {
+            const char* algo_str = JS_ToCString(ctx, algo_val);
+            if (algo_str) {
+                algorithm = algo_str;
+                JS_FreeCString(ctx, algo_str);
+            }
+        }
+        JS_FreeValue(ctx, algo_val);
+
+        // Get keyIsPassphrase (default: false)
+        bool keyIsPassphrase = false;
+        JSValue passphrase_val = JS_GetPropertyStr(ctx, options, "keyIsPassphrase");
+        if (!JS_IsUndefined(passphrase_val)) {
+            keyIsPassphrase = JS_ToBool(ctx, passphrase_val) == 1;
+        }
+        JS_FreeValue(ctx, passphrase_val);
+
+        // Create result object
+        JSValue result = JS_NewObject(ctx);
+
+        if (algorithm == "cbc") {
+            AesEncryptResult encResult = aes256CbcEncrypt(data, key, iv, keyIsPassphrase);
+            if (!encResult.error.empty()) {
+                JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, encResult.error.c_str()));
+            } else {
+                JS_SetPropertyStr(ctx, result, "ciphertext", JS_NewString(ctx, encResult.ciphertext.c_str()));
+                JS_SetPropertyStr(ctx, result, "iv", JS_NewString(ctx, encResult.iv.c_str()));
+            }
+        } else if (algorithm == "gcm") {
+            AesEncryptResult encResult = aes256GcmEncrypt(data, key, iv, keyIsPassphrase);
+            if (!encResult.error.empty()) {
+                JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, encResult.error.c_str()));
+            } else {
+                JS_SetPropertyStr(ctx, result, "ciphertext", JS_NewString(ctx, encResult.ciphertext.c_str()));
+                JS_SetPropertyStr(ctx, result, "iv", JS_NewString(ctx, encResult.iv.c_str()));
+                JS_SetPropertyStr(ctx, result, "authTag", JS_NewString(ctx, encResult.authTag.c_str()));
+            }
+        } else {
+            JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, "Unsupported algorithm. Use 'cbc' or 'gcm'"));
+        }
+
+        return result;
+    }, "aesEncrypt", 1));
+
+    // crypto.aesDecrypt(options) - AES-256-CBC/GCM decryption
+    // options: { ciphertext, key, iv, authTag (GCM only), algorithm ('cbc' or 'gcm'), keyIsPassphrase (bool) }
+    // Returns: { plaintext, error }
+    JS_SetPropertyStr(ctx, crypto, "aesDecrypt", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1 || !JS_IsObject(argv[0])) {
+            return JS_ThrowTypeError(ctx, "crypto.aesDecrypt requires an options object");
+        }
+
+        JSValue options = argv[0];
+
+        // Get ciphertext
+        JSValue ct_val = JS_GetPropertyStr(ctx, options, "ciphertext");
+        if (JS_IsUndefined(ct_val)) {
+            return JS_ThrowTypeError(ctx, "ciphertext is required");
+        }
+        const char* ct_str = JS_ToCString(ctx, ct_val);
+        JS_FreeValue(ctx, ct_val);
+        if (!ct_str) {
+            return JS_ThrowTypeError(ctx, "ciphertext must be a string");
+        }
+        std::string ciphertext(ct_str);
+        JS_FreeCString(ctx, ct_str);
+
+        // Get key
+        JSValue key_val = JS_GetPropertyStr(ctx, options, "key");
+        if (JS_IsUndefined(key_val)) {
+            return JS_ThrowTypeError(ctx, "key is required");
+        }
+        const char* key_str = JS_ToCString(ctx, key_val);
+        JS_FreeValue(ctx, key_val);
+        if (!key_str) {
+            return JS_ThrowTypeError(ctx, "key must be a string");
+        }
+        std::string key(key_str);
+        JS_FreeCString(ctx, key_str);
+
+        // Get IV
+        JSValue iv_val = JS_GetPropertyStr(ctx, options, "iv");
+        if (JS_IsUndefined(iv_val)) {
+            return JS_ThrowTypeError(ctx, "iv is required for decryption");
+        }
+        const char* iv_str = JS_ToCString(ctx, iv_val);
+        JS_FreeValue(ctx, iv_val);
+        if (!iv_str) {
+            return JS_ThrowTypeError(ctx, "iv must be a string");
+        }
+        std::string iv(iv_str);
+        JS_FreeCString(ctx, iv_str);
+
+        // Get algorithm (default: gcm)
+        std::string algorithm = "gcm";
+        JSValue algo_val = JS_GetPropertyStr(ctx, options, "algorithm");
+        if (!JS_IsUndefined(algo_val) && !JS_IsNull(algo_val)) {
+            const char* algo_str = JS_ToCString(ctx, algo_val);
+            if (algo_str) {
+                algorithm = algo_str;
+                JS_FreeCString(ctx, algo_str);
+            }
+        }
+        JS_FreeValue(ctx, algo_val);
+
+        // Get keyIsPassphrase (default: false)
+        bool keyIsPassphrase = false;
+        JSValue passphrase_val = JS_GetPropertyStr(ctx, options, "keyIsPassphrase");
+        if (!JS_IsUndefined(passphrase_val)) {
+            keyIsPassphrase = JS_ToBool(ctx, passphrase_val) == 1;
+        }
+        JS_FreeValue(ctx, passphrase_val);
+
+        // Create result object
+        JSValue result = JS_NewObject(ctx);
+
+        if (algorithm == "cbc") {
+            AesDecryptResult decResult = aes256CbcDecrypt(ciphertext, key, iv, keyIsPassphrase);
+            if (!decResult.error.empty()) {
+                JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, decResult.error.c_str()));
+            } else {
+                JS_SetPropertyStr(ctx, result, "plaintext", JS_NewString(ctx, decResult.plaintext.c_str()));
+            }
+        } else if (algorithm == "gcm") {
+            // Get auth tag (required for GCM)
+            JSValue tag_val = JS_GetPropertyStr(ctx, options, "authTag");
+            if (JS_IsUndefined(tag_val)) {
+                JS_FreeValue(ctx, result);
+                return JS_ThrowTypeError(ctx, "authTag is required for GCM decryption");
+            }
+            const char* tag_str = JS_ToCString(ctx, tag_val);
+            JS_FreeValue(ctx, tag_val);
+            if (!tag_str) {
+                JS_FreeValue(ctx, result);
+                return JS_ThrowTypeError(ctx, "authTag must be a string");
+            }
+            std::string authTag(tag_str);
+            JS_FreeCString(ctx, tag_str);
+
+            AesDecryptResult decResult = aes256GcmDecrypt(ciphertext, key, iv, authTag, keyIsPassphrase);
+            if (!decResult.error.empty()) {
+                JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, decResult.error.c_str()));
+            } else {
+                JS_SetPropertyStr(ctx, result, "plaintext", JS_NewString(ctx, decResult.plaintext.c_str()));
+            }
+        } else {
+            JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, "Unsupported algorithm. Use 'cbc' or 'gcm'"));
+        }
+
+        return result;
+    }, "aesDecrypt", 1));
+
+    // crypto.rsaEncrypt(options) - RSA encryption with public key
+    // options: { data, publicKey (PEM format) }
+    // Returns: { ciphertext, error }
+    JS_SetPropertyStr(ctx, crypto, "rsaEncrypt", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1 || !JS_IsObject(argv[0])) {
+            return JS_ThrowTypeError(ctx, "crypto.rsaEncrypt requires an options object");
+        }
+
+        JSValue options = argv[0];
+
+        // Get data
+        JSValue data_val = JS_GetPropertyStr(ctx, options, "data");
+        if (JS_IsUndefined(data_val)) {
+            return JS_ThrowTypeError(ctx, "data is required");
+        }
+        const char* data_str = JS_ToCString(ctx, data_val);
+        JS_FreeValue(ctx, data_val);
+        if (!data_str) {
+            return JS_ThrowTypeError(ctx, "data must be a string");
+        }
+        std::string data(data_str);
+        JS_FreeCString(ctx, data_str);
+
+        // Get public key
+        JSValue key_val = JS_GetPropertyStr(ctx, options, "publicKey");
+        if (JS_IsUndefined(key_val)) {
+            return JS_ThrowTypeError(ctx, "publicKey is required");
+        }
+        const char* key_str = JS_ToCString(ctx, key_val);
+        JS_FreeValue(ctx, key_val);
+        if (!key_str) {
+            return JS_ThrowTypeError(ctx, "publicKey must be a string");
+        }
+        std::string publicKey(key_str);
+        JS_FreeCString(ctx, key_str);
+
+        // Create result object
+        JSValue result = JS_NewObject(ctx);
+
+        RsaEncryptResult encResult = rsaEncrypt(data, publicKey);
+        if (!encResult.error.empty()) {
+            JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, encResult.error.c_str()));
+        } else {
+            JS_SetPropertyStr(ctx, result, "ciphertext", JS_NewString(ctx, encResult.ciphertext.c_str()));
+        }
+
+        return result;
+    }, "rsaEncrypt", 1));
+
+    // crypto.rsaDecrypt(options) - RSA decryption with private key
+    // options: { ciphertext, privateKey (PEM format) }
+    // Returns: { plaintext, error }
+    JS_SetPropertyStr(ctx, crypto, "rsaDecrypt", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1 || !JS_IsObject(argv[0])) {
+            return JS_ThrowTypeError(ctx, "crypto.rsaDecrypt requires an options object");
+        }
+
+        JSValue options = argv[0];
+
+        // Get ciphertext
+        JSValue ct_val = JS_GetPropertyStr(ctx, options, "ciphertext");
+        if (JS_IsUndefined(ct_val)) {
+            return JS_ThrowTypeError(ctx, "ciphertext is required");
+        }
+        const char* ct_str = JS_ToCString(ctx, ct_val);
+        JS_FreeValue(ctx, ct_val);
+        if (!ct_str) {
+            return JS_ThrowTypeError(ctx, "ciphertext must be a string");
+        }
+        std::string ciphertext(ct_str);
+        JS_FreeCString(ctx, ct_str);
+
+        // Get private key
+        JSValue key_val = JS_GetPropertyStr(ctx, options, "privateKey");
+        if (JS_IsUndefined(key_val)) {
+            return JS_ThrowTypeError(ctx, "privateKey is required");
+        }
+        const char* key_str = JS_ToCString(ctx, key_val);
+        JS_FreeValue(ctx, key_val);
+        if (!key_str) {
+            return JS_ThrowTypeError(ctx, "privateKey must be a string");
+        }
+        std::string privateKey(key_str);
+        JS_FreeCString(ctx, key_str);
+
+        // Create result object
+        JSValue result = JS_NewObject(ctx);
+
+        RsaDecryptResult decResult = rsaDecrypt(ciphertext, privateKey);
+        if (!decResult.error.empty()) {
+            JS_SetPropertyStr(ctx, result, "error", JS_NewString(ctx, decResult.error.c_str()));
+        } else {
+            JS_SetPropertyStr(ctx, result, "plaintext", JS_NewString(ctx, decResult.plaintext.c_str()));
+        }
+
+        return result;
+    }, "rsaDecrypt", 1));
+
     JS_SetPropertyStr(ctx, smartbotic, "crypto", crypto);
 
     // smartbotic.http