Преглед изворни кода

feat: US-007 - Crypto Node - Hashing & HMAC

Add crypto node for cryptographic operations in workflows:
- Hash operation: MD5, SHA-1, SHA-256, SHA-384, SHA-512
- HMAC operation with configurable secret key
- Random generation (hex, base64, UUID)
- Output format options: hex and base64
- Encrypt/Decrypt placeholders for future implementation

Add smartbotic.crypto namespace to QuickJS runtime:
- crypto.hash(data, algorithm, outputFormat)
- crypto.hmac(data, key, algorithm, outputFormat)
- crypto.randomBytes(length)
- crypto.randomUUID()
fszontagh пре 6 месеци
родитељ
комит
2081b4b61f
2 измењених фајлова са 452 додато и 0 уклоњено
  1. 222 0
      nodes/crypto/crypto.js
  2. 230 0
      src/runner/engine/script_engine.cpp

+ 222 - 0
nodes/crypto/crypto.js

@@ -0,0 +1,222 @@
+/**
+ * @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
+ * @icon lock
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        operation: {
+            type: 'string',
+            title: 'Operation',
+            enum: ['hash', 'hmac', 'random', 'encrypt', 'decrypt'],
+            enumLabels: ['Hash', 'HMAC', 'Random Generation', 'Encrypt (Coming Soon)', 'Decrypt (Coming Soon)'],
+            default: 'hash',
+            description: 'Cryptographic operation to perform'
+        },
+        algorithm: {
+            type: 'string',
+            title: 'Algorithm',
+            enum: ['md5', 'sha1', 'sha256', 'sha384', 'sha512'],
+            enumLabels: ['MD5', 'SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'],
+            default: 'sha256',
+            description: 'Hash algorithm to use',
+            showWhen: { field: 'operation', value: ['hash', 'hmac'] }
+        },
+        outputFormat: {
+            type: 'string',
+            title: 'Output Format',
+            enum: ['hex', 'base64'],
+            enumLabels: ['Hexadecimal', 'Base64'],
+            default: 'hex',
+            description: 'Format of the output hash or HMAC',
+            showWhen: { field: 'operation', value: ['hash', 'hmac'] }
+        },
+        secretKey: {
+            type: 'string',
+            title: 'Secret Key',
+            description: 'Secret key for HMAC generation. Supports {{variable}} interpolation.',
+            showWhen: { field: 'operation', value: 'hmac' }
+        },
+        randomLength: {
+            type: 'number',
+            title: 'Random Bytes Length',
+            default: 32,
+            description: 'Number of random bytes to generate (1-1024)',
+            showWhen: { field: 'operation', value: 'random' }
+        },
+        randomFormat: {
+            type: 'string',
+            title: 'Random Output Format',
+            enum: ['hex', 'base64', 'uuid'],
+            enumLabels: ['Hexadecimal', 'Base64', 'UUID v4'],
+            default: 'hex',
+            description: 'Format of the random output',
+            showWhen: { field: 'operation', value: 'random' }
+        }
+    },
+    required: ['operation']
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        data: {
+            type: 'any',
+            description: 'Data to hash or process. Can be a string or will be JSON stringified.'
+        },
+        secretKey: {
+            type: 'string',
+            description: 'Override secret key for HMAC from input'
+        }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        result: {
+            type: 'string',
+            description: 'The cryptographic operation result'
+        },
+        algorithm: {
+            type: 'string',
+            description: 'Algorithm used for the operation'
+        },
+        operation: {
+            type: 'string',
+            description: 'Operation that was performed'
+        },
+        format: {
+            type: 'string',
+            description: 'Output format (hex or base64)'
+        },
+        inputLength: {
+            type: 'number',
+            description: 'Length of the input data in bytes'
+        }
+    }
+};
+
+function interpolate(template, data) {
+    if (typeof template !== 'string') return template;
+    return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
+        const keys = key.trim().split('.');
+        let value = data;
+        for (const k of keys) {
+            if (value && typeof value === 'object' && k in value) {
+                value = value[k];
+            } else {
+                return match;
+            }
+        }
+        return value !== undefined ? String(value) : match;
+    });
+}
+
+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 === 'random') {
+        const format = config.randomFormat || 'hex';
+
+        if (format === 'uuid') {
+            const uuid = smartbotic.crypto.randomUUID();
+            smartbotic.log.info('Generated random UUID');
+            return {
+                result: uuid,
+                operation: 'random',
+                format: 'uuid'
+            };
+        }
+
+        const length = config.randomLength || 32;
+        if (length < 1 || length > 1024) {
+            throw new Error('Random length must be between 1 and 1024');
+        }
+
+        const randomHex = smartbotic.crypto.randomBytes(length);
+        let result = randomHex;
+
+        if (format === 'base64') {
+            let binary = '';
+            for (let i = 0; i < randomHex.length; i += 2) {
+                binary += String.fromCharCode(parseInt(randomHex.substr(i, 2), 16));
+            }
+            result = smartbotic.utils.base64Encode(binary);
+        }
+
+        smartbotic.log.info('Generated ' + length + ' random bytes in ' + format + ' format');
+
+        return {
+            result: result,
+            operation: 'random',
+            format: format,
+            inputLength: length
+        };
+    }
+
+    const inputData = input.data !== undefined ? input.data : input;
+
+    let dataStr;
+    if (typeof inputData === 'string') {
+        dataStr = inputData;
+    } else if (inputData === null || inputData === undefined) {
+        throw new Error('No data provided for ' + operation + ' operation');
+    } else {
+        dataStr = JSON.stringify(inputData);
+    }
+
+    const algorithm = config.algorithm || 'sha256';
+    const outputFormat = config.outputFormat || 'hex';
+
+    if (operation === 'hash') {
+        smartbotic.log.info('Hashing ' + dataStr.length + ' bytes with ' + algorithm.toUpperCase());
+
+        const result = smartbotic.crypto.hash(dataStr, algorithm, outputFormat);
+
+        return {
+            result: result,
+            algorithm: algorithm,
+            operation: 'hash',
+            format: outputFormat,
+            inputLength: dataStr.length
+        };
+    }
+
+    if (operation === 'hmac') {
+        let secretKey = input.secretKey || config.secretKey;
+
+        if (secretKey && typeof secretKey === 'string') {
+            secretKey = interpolate(secretKey, input);
+        }
+
+        if (!secretKey) {
+            throw new Error('Secret key is required for HMAC operation');
+        }
+
+        smartbotic.log.info('Generating HMAC-' + algorithm.toUpperCase() + ' for ' + dataStr.length + ' bytes');
+
+        const result = smartbotic.crypto.hmac(dataStr, secretKey, algorithm, outputFormat);
+
+        return {
+            result: result,
+            algorithm: algorithm,
+            operation: 'hmac',
+            format: outputFormat,
+            inputLength: dataStr.length
+        };
+    }
+
+    throw new Error('Unknown operation: ' + operation);
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, execute };

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

@@ -21,6 +21,9 @@
 #include <openssl/sha.h>
 #include <openssl/evp.h>
 #include <openssl/buffer.h>
+#include <openssl/hmac.h>
+#include <openssl/md5.h>
+#include <openssl/rand.h>
 
 namespace smartbotic::runner::engine {
 
@@ -85,6 +88,89 @@ static std::string sha256Hash(const std::string& input) {
     return ss.str();
 }
 
+// Generic hash function using EVP interface
+static std::string hashData(const std::string& input, const std::string& algorithm) {
+    const EVP_MD* md = nullptr;
+
+    if (algorithm == "md5") {
+        md = EVP_md5();
+    } else if (algorithm == "sha1") {
+        md = EVP_sha1();
+    } else if (algorithm == "sha256") {
+        md = EVP_sha256();
+    } else if (algorithm == "sha384") {
+        md = EVP_sha384();
+    } else if (algorithm == "sha512") {
+        md = EVP_sha512();
+    } else {
+        return "";
+    }
+
+    unsigned char hash[EVP_MAX_MD_SIZE];
+    unsigned int hash_len = 0;
+
+    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
+    EVP_DigestInit_ex(ctx, md, nullptr);
+    EVP_DigestUpdate(ctx, input.data(), input.size());
+    EVP_DigestFinal_ex(ctx, hash, &hash_len);
+    EVP_MD_CTX_free(ctx);
+
+    std::ostringstream ss;
+    for (unsigned int i = 0; i < hash_len; ++i) {
+        ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(hash[i]);
+    }
+
+    return ss.str();
+}
+
+// HMAC function using EVP interface
+static std::string hmacData(const std::string& input, const std::string& key, const std::string& algorithm) {
+    const EVP_MD* md = nullptr;
+
+    if (algorithm == "md5") {
+        md = EVP_md5();
+    } else if (algorithm == "sha1") {
+        md = EVP_sha1();
+    } else if (algorithm == "sha256") {
+        md = EVP_sha256();
+    } else if (algorithm == "sha384") {
+        md = EVP_sha384();
+    } else if (algorithm == "sha512") {
+        md = EVP_sha512();
+    } else {
+        return "";
+    }
+
+    unsigned char hmac_result[EVP_MAX_MD_SIZE];
+    unsigned int hmac_len = 0;
+
+    HMAC(md, key.data(), static_cast<int>(key.size()),
+         reinterpret_cast<const unsigned char*>(input.data()), input.size(),
+         hmac_result, &hmac_len);
+
+    std::ostringstream ss;
+    for (unsigned int i = 0; i < hmac_len; ++i) {
+        ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(hmac_result[i]);
+    }
+
+    return ss.str();
+}
+
+// Generate random bytes
+static std::string generateRandomBytes(int length) {
+    std::vector<unsigned char> buffer(length);
+    if (RAND_bytes(buffer.data(), length) != 1) {
+        return "";
+    }
+
+    std::ostringstream ss;
+    for (int i = 0; i < length; ++i) {
+        ss << std::hex << std::setfill('0') << std::setw(2) << static_cast<int>(buffer[i]);
+    }
+
+    return ss.str();
+}
+
 // Helper structure for curl response
 struct CurlResponse {
     std::string body;
@@ -944,6 +1030,150 @@ void ScriptEngine::setupBuiltinAPIs() {
 
     JS_SetPropertyStr(ctx, smartbotic, "utils", utils);
 
+    // smartbotic.crypto - Cryptographic operations
+    JSValue crypto = JS_NewObject(ctx);
+
+    // crypto.hash(data, algorithm, outputFormat) - Hash data using specified algorithm
+    // algorithm: 'md5', 'sha1', 'sha256', 'sha384', 'sha512'
+    // outputFormat: 'hex' (default) or 'base64'
+    JS_SetPropertyStr(ctx, crypto, "hash", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 2) {
+            return JS_ThrowTypeError(ctx, "crypto.hash requires data and algorithm arguments");
+        }
+
+        const char* data_str = JS_ToCString(ctx, argv[0]);
+        if (!data_str) {
+            return JS_ThrowTypeError(ctx, "data must be a string");
+        }
+        std::string data(data_str);
+        JS_FreeCString(ctx, data_str);
+
+        const char* algo_str = JS_ToCString(ctx, argv[1]);
+        if (!algo_str) {
+            return JS_ThrowTypeError(ctx, "algorithm must be a string");
+        }
+        std::string algorithm(algo_str);
+        JS_FreeCString(ctx, algo_str);
+
+        std::string outputFormat = "hex";
+        if (argc > 2 && !JS_IsUndefined(argv[2])) {
+            const char* format_str = JS_ToCString(ctx, argv[2]);
+            if (format_str) {
+                outputFormat = format_str;
+                JS_FreeCString(ctx, format_str);
+            }
+        }
+
+        std::string hash_result = hashData(data, algorithm);
+        if (hash_result.empty()) {
+            return JS_ThrowTypeError(ctx, "Unsupported hash algorithm");
+        }
+
+        if (outputFormat == "base64") {
+            // Convert hex to binary, then to base64
+            std::string binary;
+            binary.reserve(hash_result.length() / 2);
+            for (size_t i = 0; i < hash_result.length(); i += 2) {
+                unsigned char byte = static_cast<unsigned char>(std::stoul(hash_result.substr(i, 2), nullptr, 16));
+                binary.push_back(static_cast<char>(byte));
+            }
+            std::string base64_result = base64Encode(binary);
+            return JS_NewString(ctx, base64_result.c_str());
+        }
+
+        return JS_NewString(ctx, hash_result.c_str());
+    }, "hash", 3));
+
+    // crypto.hmac(data, key, algorithm, outputFormat) - Generate HMAC
+    // algorithm: 'md5', 'sha1', 'sha256', 'sha384', 'sha512'
+    // outputFormat: 'hex' (default) or 'base64'
+    JS_SetPropertyStr(ctx, crypto, "hmac", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 3) {
+            return JS_ThrowTypeError(ctx, "crypto.hmac requires data, key, and algorithm arguments");
+        }
+
+        const char* data_str = JS_ToCString(ctx, argv[0]);
+        if (!data_str) {
+            return JS_ThrowTypeError(ctx, "data must be a string");
+        }
+        std::string data(data_str);
+        JS_FreeCString(ctx, data_str);
+
+        const char* key_str = JS_ToCString(ctx, argv[1]);
+        if (!key_str) {
+            return JS_ThrowTypeError(ctx, "key must be a string");
+        }
+        std::string key(key_str);
+        JS_FreeCString(ctx, key_str);
+
+        const char* algo_str = JS_ToCString(ctx, argv[2]);
+        if (!algo_str) {
+            return JS_ThrowTypeError(ctx, "algorithm must be a string");
+        }
+        std::string algorithm(algo_str);
+        JS_FreeCString(ctx, algo_str);
+
+        std::string outputFormat = "hex";
+        if (argc > 3 && !JS_IsUndefined(argv[3])) {
+            const char* format_str = JS_ToCString(ctx, argv[3]);
+            if (format_str) {
+                outputFormat = format_str;
+                JS_FreeCString(ctx, format_str);
+            }
+        }
+
+        std::string hmac_result = hmacData(data, key, algorithm);
+        if (hmac_result.empty()) {
+            return JS_ThrowTypeError(ctx, "Unsupported HMAC algorithm");
+        }
+
+        if (outputFormat == "base64") {
+            // Convert hex to binary, then to base64
+            std::string binary;
+            binary.reserve(hmac_result.length() / 2);
+            for (size_t i = 0; i < hmac_result.length(); i += 2) {
+                unsigned char byte = static_cast<unsigned char>(std::stoul(hmac_result.substr(i, 2), nullptr, 16));
+                binary.push_back(static_cast<char>(byte));
+            }
+            std::string base64_result = base64Encode(binary);
+            return JS_NewString(ctx, base64_result.c_str());
+        }
+
+        return JS_NewString(ctx, hmac_result.c_str());
+    }, "hmac", 4));
+
+    // crypto.randomBytes(length) - Generate cryptographically secure random bytes
+    // Returns hex string of random bytes
+    JS_SetPropertyStr(ctx, crypto, "randomBytes", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc < 1) {
+            return JS_ThrowTypeError(ctx, "crypto.randomBytes requires a length argument");
+        }
+
+        int32_t length;
+        if (JS_ToInt32(ctx, &length, argv[0]) != 0) {
+            return JS_ThrowTypeError(ctx, "length must be a number");
+        }
+
+        if (length <= 0 || length > 1024) {
+            return JS_ThrowRangeError(ctx, "length must be between 1 and 1024");
+        }
+
+        std::string result = generateRandomBytes(length);
+        if (result.empty()) {
+            return JS_ThrowInternalError(ctx, "Failed to generate random bytes");
+        }
+
+        return JS_NewString(ctx, result.c_str());
+    }, "randomBytes", 1));
+
+    // crypto.randomUUID() - Generate a random UUID v4
+    JS_SetPropertyStr(ctx, crypto, "randomUUID", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        std::string uuid = common::UUID::generate();
+        return JS_NewString(ctx, uuid.c_str());
+    }, "randomUUID", 0));
+
+    JS_SetPropertyStr(ctx, smartbotic, "crypto", crypto);
+
     // smartbotic.http
     JSValue http = JS_NewObject(ctx);
     JS_SetPropertyStr(ctx, http, "request", JS_NewCFunction(ctx, js_http_request, "request", 1));