/** * @node crypto * @name Crypto * @category crypto * @version 1.1.0 * @description Hash data, generate HMACs, encrypt/decrypt data using AES or RSA, and generate random values * @icon lock */ const configSchema = { type: 'object', properties: { operation: { type: 'string', title: 'Operation', enum: ['hash', 'hmac', 'random', 'encrypt', 'decrypt'], enumLabels: ['Hash', 'HMAC', 'Random Generation', 'Encrypt', 'Decrypt'], 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' } }, randomType: { type: 'string', title: 'Random Type', enum: ['uuid', 'hex', 'alphanumeric', 'password'], enumLabels: ['UUID v4', 'Hex Token', 'Alphanumeric String', 'Secure Password'], default: 'uuid', description: 'Type of random value to generate', showWhen: { field: 'operation', value: 'random' } }, randomLength: { type: 'number', title: 'Length', default: 32, minimum: 1, maximum: 1024, description: 'Length of the generated value (1-1024). For hex, this is number of bytes (output will be 2x characters).', showWhen: { field: 'operation', value: 'random' } }, passwordUppercase: { type: 'boolean', title: 'Include Uppercase', default: true, description: 'Include uppercase letters (A-Z)', showWhen: { field: 'randomType', value: 'password' } }, passwordLowercase: { type: 'boolean', title: 'Include Lowercase', default: true, description: 'Include lowercase letters (a-z)', showWhen: { field: 'randomType', value: 'password' } }, passwordNumbers: { type: 'boolean', title: 'Include Numbers', default: true, description: 'Include numbers (0-9)', showWhen: { field: 'randomType', value: 'password' } }, passwordSymbols: { type: 'boolean', title: 'Include Symbols', 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'] }; const inputSchema = { type: 'object', properties: { data: { type: 'any', 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)' } } }; const outputSchema = { type: 'object', properties: { result: { type: 'string', description: 'The cryptographic operation result (hash, encrypted data, or decrypted data)' }, 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)' }, randomType: { type: 'string', description: 'Type of random value generated (uuid, hex, alphanumeric, password)' }, length: { type: 'number', description: 'Length of the generated random value' }, 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)' } } }; 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; }); } function generateRandomString(length, charset) { const charsetLength = charset.length; let result = ''; const bytesNeeded = length; const randomHex = smartbotic.crypto.randomBytes(bytesNeeded * 2); let pos = 0; while (result.length < length) { if (pos >= randomHex.length) { 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); const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength; if (byte < maxUnbiased) { result += charset[byte % charsetLength]; } } } else { const byte = parseInt(randomHex.substr(pos, 2), 16); pos += 2; const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength; if (byte < maxUnbiased) { result += charset[byte % charsetLength]; } } } return result; } async function execute(config, input, context) { const operation = config.operation || 'hash'; if (operation === 'encrypt') { return executeEncrypt(config, input); } if (operation === 'decrypt') { return executeDecrypt(config, input); } if (operation === 'random') { const randomType = config.randomType || 'uuid'; const length = config.randomLength || 32; if (randomType !== 'uuid' && (length < 1 || length > 1024)) { throw new Error('Length must be between 1 and 1024'); } if (randomType === 'uuid') { const uuid = smartbotic.crypto.randomUUID(); smartbotic.log.info('Generated random UUID v4'); return { result: uuid, operation: 'random', randomType: 'uuid', length: 36 }; } if (randomType === 'hex') { const randomHex = smartbotic.crypto.randomBytes(length); smartbotic.log.info('Generated ' + length + '-byte hex token (' + randomHex.length + ' characters)'); return { result: randomHex, operation: 'random', randomType: 'hex', length: randomHex.length }; } if (randomType === 'alphanumeric') { const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const result = generateRandomString(length, charset); smartbotic.log.info('Generated ' + length + '-character alphanumeric string'); return { result: result, operation: 'random', randomType: 'alphanumeric', length: length }; } if (randomType === 'password') { const includeUppercase = config.passwordUppercase !== false; const includeLowercase = config.passwordLowercase !== false; const includeNumbers = config.passwordNumbers !== false; const includeSymbols = config.passwordSymbols !== false; let charset = ''; if (includeUppercase) charset += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (includeLowercase) charset += 'abcdefghijklmnopqrstuvwxyz'; if (includeNumbers) charset += '0123456789'; if (includeSymbols) charset += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (charset.length === 0) { throw new Error('At least one character set must be enabled for password generation'); } const result = generateRandomString(length, charset); smartbotic.log.info('Generated ' + length + '-character secure password'); return { result: result, operation: 'random', randomType: 'password', length: length }; } throw new Error('Unknown random type: ' + randomType); } 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); } 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 };