diff --git a/secret-management/regenerate-secret.js b/secret-management/regenerate-secret.js new file mode 100644 index 0000000..bc9b1b3 --- /dev/null +++ b/secret-management/regenerate-secret.js @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2025 Radix IoT LLC. All rights reserved. + */ + +// Load the encryption utilities from store-encrypted-secret.js +const path = services.fileStoreService.getPathForRead('default', 'script-examples/secret-management/store-encrypted-secret.js'); +const encryptionScript = load(path); + +// Import required Java classes +const MangoRuntimeContextConfiguration = Java.type('com.infiniteautomation.mango.spring.MangoRuntimeContextConfiguration'); +const ScheduledExecutorService = Java.type('org.springframework.security.concurrent.DelegatingSecurityContextScheduledExecutorService'); +const mangoScheduledExecutorService = Common.getBean(ScheduledExecutorService, MangoRuntimeContextConfiguration.SCHEDULED_EXECUTOR_SERVICE_NAME); +const TimeUnit = Java.type('java.util.concurrent.TimeUnit'); +const Runnable = Java.type('java.lang.Runnable'); + +//Get a reference to a class to use for the Logger +const AUTO_REGENERATE_LOG_CLASS = Java.type('com.infiniteautomation.mango.util.script.MangoJavaScript'); +const AUTO_REGENERATE_LOG = LoggerFactory.getLogger(AUTO_REGENERATE_LOG_CLASS); + + + +// Import the functions we need from the loaded script +eval(encryptionScript); + +// Global variables to manage the timer +let regenerationTaskFuture = null; +let secretGeneratorStatus = { + running: false, + shouldRegenerate: false, + regenerationInterval: 0, + regenerationIntervalUnit: TimeUnit.SECONDS.toString(), + createdAt: '' +} + +/** + * Generates and stores a new encrypted secret + * @param {string} xid - JSON Data XID + * @param {string} name - JSON Data Name + * @param {string} encryptionKey - key used for encryption + * @param {number} interval - number of units to wait to regenerate secret + * @param {TimeUnit} intervalUnit - unit of time to wait + * @param {function(): string} regenerateSecretFunction - call to regenerate secret + * @param {boolean} shouldRegenerate - continue to regenerate + * + */ +function regenerateSecret(xid, name, encryptionKey, interval, + intervalUnit, regenerateSecretFunction, + shouldRegenerate) { + try { + const secretStore = getJsonStoreItem(xid); + const shouldRegenerateState = secretStore.getJsonData().get('shouldRegenerate').asBoolean() + AUTO_REGENERATE_LOG.info('Regenerating secret...'); + + // Generate new secret + const newSecret = regenerateSecretFunction(); + + // Store encrypted secret using the function from store-encrypted-secret.js + storeEncryptedSecret( + xid, + name, + newSecret, + encryptionKey, + MangoPermission.superadminOnly(), + MangoPermission.superadminOnly(), + shouldRegenerate, + interval, + intervalUnit.toString() + ); + + AUTO_REGENERATE_LOG.info('Secret regenerated successfully at: ' + new Date().toISOString()); + + // Optional: Log first few characters for verification (don't log full secret) + AUTO_REGENERATE_LOG.info('Should Regenerate: ' + shouldRegenerateState); + return shouldRegenerateState; + } catch (e) { + AUTO_REGENERATE_LOG.error('Failed to regenerate secret: ' + e.message); + return false; + } +} + +/** + * Retrieves the current secret (for testing purposes) + * @returns {string} The current decrypted secret + */ +function getCurrentSecret() { + try { + return retrieveAndDecryptSecret(SECRET_XID, ENCRYPTION_KEY); + } catch (e) { + AUTO_REGENERATE_LOG.error('Failed to retrieve current secret: ' + e.message); + return null; + } +} + +/** + * Starts the auto-regeneration timer + * @param {string} xid - JSON Data XID + * @param {string} name - JSON Data Name + * @param {string} encryptionKey - key used for encryption + * @param {number} interval - number of units to wait to regenerate secret + * @param {TimeUnit} intervalUnit - unit of time to wait + * @param {function(): string} regenerateSecretFunction - call to regenerate secret and return string secret + */ +function startAutoRegeneration(xid, name, encryptionKey, interval, + intervalUnit, regenerateSecretFunction) { + try { + // Stop existing timer if running + stopAutoRegeneration(); + + AUTO_REGENERATE_LOG.info('Starting auto-regeneration timer with interval: ' + interval + ' ' + intervalUnit.toString()); + + // Generate initial secret immediately + regenerateSecret(xid, name, encryptionKey, interval, intervalUnit, regenerateSecretFunction, true); + + // Schedule periodic regeneration + const RunnableImpl = Java.extend(Runnable, { + run: function() { + try { + let runAgain = regenerateSecret(xid, name, encryptionKey, interval, intervalUnit, regenerateSecretFunction, true); + secretGeneratorStatus = { + shouldRegenerate: runAgain, + regenerationInterval: interval, + regenerationIntervalUnit: intervalUnit.toString(), + secretXid: xid, + createdAt: new Date().toISOString() + } + if (runAgain === false) { + stopAutoRegeneration(); + } + }catch(error) { + AUTO_REGENERATE_LOG.error('Failed running regenerate secret: ' + error.message); + } + } + }); + + const regenerationTask = new RunnableImpl(); + + regenerationTaskFuture = mangoScheduledExecutorService.scheduleAtFixedRate( + regenerationTask, + interval, // Initial delay + interval, // Period + intervalUnit //Units + ); + + AUTO_REGENERATE_LOG.info('Auto-regeneration timer started successfully'); + AUTO_REGENERATE_LOG.info('Secret will be regenerated every ' + interval + ' ' + intervalUnit); + + } catch (e) { + AUTO_REGENERATE_LOG.error('Failed to start auto-regeneration: ' + e.message); + } +} + +/** + * Stops the auto-regeneration timer + */ +function stopAutoRegeneration() { + try { + if (mangoScheduledExecutorService && !mangoScheduledExecutorService.isShutdown()) { + + // Wait for graceful shutdown + if (!regenerationTaskFuture.cancel(true)) { + AUTO_REGENERATE_LOG.error('Unable to stop secret regeneration task'); + } + + AUTO_REGENERATE_LOG.info('Auto-regeneration timer stopped'); + console.log('Secret auto-regeneration stopped'); + } + } catch (e) { + AUTO_REGENERATE_LOG.error('Failed to stop auto-regeneration: ' + e.message); + } finally { + regenerationTaskFuture = null; + } +} + +/** + * Gets the status of the auto-regeneration service + * @returns {Object} Status information + */ +function getRegenerationStatus() { + secretGeneratorStatus.running = regenerationTaskFuture && !regenerationTaskFuture.isDone(); + AUTO_REGENERATE_LOG.info('Auto-regeneration status: ' + JSON.stringify(secretGeneratorStatus, null, 2)) + return secretGeneratorStatus; +} + +// Cleanup function to ensure proper shutdown +function cleanup() { + AUTO_REGENERATE_LOG.info('Cleaning up auto-regeneration service...'); + stopAutoRegeneration(); +} diff --git a/secret-management/store-encrypted-secret.js b/secret-management/store-encrypted-secret.js new file mode 100644 index 0000000..74de090 --- /dev/null +++ b/secret-management/store-encrypted-secret.js @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2025 Radix IoT LLC. All rights reserved. + */ + +/** + * Mango Script: Secure String Encryption and JSON Store Management + * This script encrypts strings using AES encryption and stores them securely in the JSON store + * it is intended to be used as a tool via evaluating it, see test-store-secret.js + */ + +// Import required Java classes for encryption +const Cipher = Java.type('javax.crypto.Cipher'); +const KeyGenerator = Java.type('javax.crypto.KeyGenerator'); +const SecretKeySpec = Java.type('javax.crypto.spec.SecretKeySpec'); +const Base64 = Java.type('java.util.Base64'); +const StandardCharsets = Java.type('java.nio.charset.StandardCharsets'); +const Common = Java.type('com.serotonin.m2m2.Common'); +const JsonDataVO = Java.type('com.serotonin.m2m2.vo.json.JsonDataVO'); +const MangoPermission = Java.type('com.infiniteautomation.mango.permission.MangoPermission'); +const Role = Java.type('com.serotonin.m2m2.vo.role.Role'); +const ObjectMapper = Java.type('com.fasterxml.jackson.databind.ObjectMapper'); +const JsonNode = Java.type('com.fasterxml.jackson.databind.JsonNode'); + +// Mango services and logging +const LoggerFactory = Java.type('org.slf4j.LoggerFactory'); +//Get a reference to a class to use for the Logger +const STORE_SECRET_LOG_CLASS = Java.type('com.infiniteautomation.mango.util.script.MangoJavaScript'); +const STORE_SECRET_LOG = LoggerFactory.getLogger(STORE_SECRET_LOG_CLASS); + +// Create ObjectMapper instance for JSON operations +const mapper = new ObjectMapper(); + +/** + * Creates a JsonNode from a JavaScript object + * @param {Object} jsObject - JavaScript object to convert + * @returns {JsonNode} Jackson JsonNode + */ +function createJsonNode(jsObject) { + try { + // Convert JavaScript object to JSON string, then parse as JsonNode + const jsonString = JSON.stringify(jsObject); + return mapper.readTree(jsonString); + } catch (e) { + STORE_SECRET_LOG.error('Failed to create JsonNode: ' + e.message); + throw e; + } +} + + +/** + * Encrypts a string using AES encryption + * @param {string} plainText - The text to encrypt + * @param {string} secretKey - The encryption key (must be 16, 24, or 32 bytes) + * @returns {string} Base64 encoded encrypted string + */ +function encryptString(plainText, secretKey) { + try { + // Convert string to bytes properly + let keyBytes = Java.to(secretKey.split('').map(c => c.charCodeAt(0)), 'byte[]'); + + // Ensure key is proper length (pad or truncate to 32 bytes for AES-256) + if (keyBytes.length < 32) { + const paddedKey = Java.to(new Array(32).fill(0), 'byte[]'); + java.lang.System.arraycopy(keyBytes, 0, paddedKey, 0, keyBytes.length); + keyBytes = paddedKey; + } else if (keyBytes.length > 32) { + const truncatedKey = Java.to(new Array(32).fill(0), 'byte[]'); + java.lang.System.arraycopy(keyBytes, 0, truncatedKey, 0, 32); + keyBytes = truncatedKey; + } + + const secretKeySpec = new SecretKeySpec(keyBytes, 'AES'); + const cipher = Cipher.getInstance('AES/ECB/PKCS5Padding'); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); + + const plainTextBytes = Java.to(plainText.split('').map(c => c.charCodeAt(0)), 'byte[]'); + const encryptedBytes = cipher.doFinal(plainTextBytes); + + return Base64.getEncoder().encodeToString(encryptedBytes); + } catch (e) { + STORE_SECRET_LOG.error('Encryption failed: ' + e.message); + throw e; + } +} + +/** + * Decrypts a Base64 encoded encrypted string + * @param {string} encryptedText - Base64 encoded encrypted text + * @param {string} secretKey - The decryption key + * @returns {string} Decrypted plain text + */ +function decryptString(encryptedText, secretKey) { + try { + // Convert string to bytes properly + let keyBytes = Java.to(secretKey.split('').map(c => c.charCodeAt(0)), 'byte[]'); + + // Ensure key is proper length + if (keyBytes.length < 32) { + const paddedKey = Java.to(new Array(32).fill(0), 'byte[]'); + java.lang.System.arraycopy(keyBytes, 0, paddedKey, 0, keyBytes.length); + keyBytes = paddedKey; + } else if (keyBytes.length > 32) { + const truncatedKey = Java.to(new Array(32).fill(0), 'byte[]'); + java.lang.System.arraycopy(keyBytes, 0, truncatedKey, 0, 32); + keyBytes = truncatedKey; + } + + const secretKeySpec = new SecretKeySpec(keyBytes, 'AES'); + const cipher = Cipher.getInstance('AES/ECB/PKCS5Padding'); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + + const encryptedBytes = Base64.getDecoder().decode(encryptedText); + const decryptedBytes = cipher.doFinal(encryptedBytes); + + // Convert bytes back to string + let result = ''; + for (let i = 0; i < decryptedBytes.length; i++) { + result += String.fromCharCode(decryptedBytes[i] & 0xFF); + } + + return result; + } catch (e) { + STORE_SECRET_LOG.error('Decryption failed: ' + e.message); + throw e; + } +} + +/** + * Stores an encrypted string in the JSON store using Mango's REST endpoint + * @param {string} xid - Unique identifier for the JSON store item + * @param {string} name - Human readable name + * @param {string} plainTextSecret - The secret to encrypt and store + * @param {string} encryptionKey - The key to use for encryption + * @param {Array} readPermission - Permission to read this item + * @param {Array} editPermission - Permission to edit this item + * @param {boolean} shouldRegenerate - Should we regenerate the secret + * @param {number} regenerationInterval - interval used to generate the secret + * @param {string} regenerationIntervalUnit - Unit of interval used to regenerate + */ +function storeEncryptedSecret(xid, name, plainTextSecret, encryptionKey, + readPermission, + editPermission, + shouldRegenerate, regenerationInterval, regenerationIntervalUnit) { + readPermission = readPermission || MangoPermission.superadminOnly(); + editPermission = editPermission || MangoPermission.superadminOnly(); + + try { + const encryptedSecret = encryptString(plainTextSecret, encryptionKey); + + const jsonData = { + encryptedValue: encryptedSecret, + algorithm: 'AES/ECB/PKCS5Padding', + createdAt: new Date().toISOString(), + description: 'Encrypted secret stored securely', + shouldRegenerate: shouldRegenerate, + regenerationInterval: regenerationInterval, + regenerationIntervalUnit: regenerationIntervalUnit, + }; + + // Create JSON store item structure + const jsonStoreItem = new JsonDataVO(); + jsonStoreItem.setXid(xid); + jsonStoreItem.setName(name); + jsonStoreItem.setJsonData(createJsonNode(jsonData)); + jsonStoreItem.setReadPermission(readPermission); + jsonStoreItem.setEditPermission(editPermission) + + // Use Mango's JSON store service + try { + const existing = services.jsonDataService.get(xid); + const result = services.jsonDataService.update(xid, jsonStoreItem); + STORE_SECRET_LOG.info('Successfully updated encrypted secret with XID: ' + xid); + return result; + }catch (e) { + const result = services.jsonDataService.insert(jsonStoreItem); + STORE_SECRET_LOG.info('Successfully inserted encrypted secret with XID: ' + xid); + return result; + } + } catch (e) { + STORE_SECRET_LOG.error('Failed to store encrypted secret: ' + e.message); + throw e; + } +} + +/** + * Get the JSON Store Item + * @param xid + * @returns {*} + */ +function getJsonStoreItem(xid) { + let jsonStoreItem; + + // Try to use Mango's JSON store service + jsonStoreItem = services.jsonDataService.get(xid); + + if (!jsonStoreItem) { + throw new Error('JSON store item not found: ' + xid); + } + return jsonStoreItem; +} +/** + * Retrieves and decrypts a secret from the JSON store + * @param {string} xid - The XID of the JSON store item + * @param {string} decryptionKey - The key to decrypt with + * @returns {string} The decrypted secret + */ +function retrieveAndDecryptSecret(xid, decryptionKey) { + try { + let jsonStoreItem = getJsonStoreItem(xid); + const encryptedValue = jsonStoreItem.getJsonData().get('encryptedValue').textValue(); + if (!encryptedValue) { + throw new Error('No encrypted value found in JSON store item: ' + xid); + } + + const decryptedSecret = decryptString(encryptedValue, decryptionKey); + STORE_SECRET_LOG.info('Successfully retrieved and decrypted secret with XID: ' + xid); + + return decryptedSecret; + } catch (e) { + STORE_SECRET_LOG.error('Failed to retrieve and decrypt secret: ' + e.message); + throw e; + } +} + +STORE_SECRET_LOG.info('Secure String Encryption Script loaded successfully'); \ No newline at end of file diff --git a/secret-management/test-regenerate-secret.js b/secret-management/test-regenerate-secret.js new file mode 100644 index 0000000..1e5de2b --- /dev/null +++ b/secret-management/test-regenerate-secret.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2025 Radix IoT LLC. All rights reserved. + */ + +// Load the encryption utilities from store-encrypted-secret.js +const regenerateSecretScriptPath = services.fileStoreService.getPathForRead('default', 'script-examples/secret-management/regenerate-secret.js'); + +const regenerateSecretScript = load(regenerateSecretScriptPath); + +// Import the functions we need from the loaded script +eval(regenerateSecretScript); + +// Configuration +const SECRET_XID = 'api-key-secret'; +const SECRET_NAME = 'External API Key'; +const ENCRYPTION_KEY = 'MySecureEncryptionKey123456789012'; // 32 characters for AES-256 +const REGENERATION_INTERVAL = 30; +const REGENERATION_INTERVAL_UNIT = TimeUnit.SECONDS; + +//Get a reference to a class to use for the Logger +const LOG_CLASS = Java.type('com.infiniteautomation.mango.util.script.MangoJavaScript'); +const LOG = LoggerFactory.getLogger(LOG_CLASS); + +/** + * Generates a new secure random string + * @returns {string} Random secure string + */ +function generateSecureSecret() { + const length = 64; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'; + let result = ''; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + +// Example usage and testing +try { + LOG.info('Starting Auto-Regenerate Secret Script...'); + + // Start the auto-regeneration service + startAutoRegeneration(SECRET_XID, + SECRET_NAME, + ENCRYPTION_KEY, + REGENERATION_INTERVAL, + REGENERATION_INTERVAL_UNIT, + generateSecureSecret); + + console.log('Auto-regeneration service started!'); + console.log('Status:', JSON.stringify(getRegenerationStatus(), null, 2)); + + // Example: How to retrieve the current secret + setTimeout(function() { + try { + const current = getCurrentSecret(); + if (current) { + console.log('Current secret retrieved successfully (length: ' + current.length + ')'); + } + console.log('Status:', JSON.stringify(getRegenerationStatus(), null, 2)); + } catch (e) { + console.log('Failed to retrieve current secret: ' + e.message); + } + }, 2000); // Wait 2 seconds before testing retrieval + +} catch (e) { + LOG.error('Script initialization failed: ' + e.message); +} diff --git a/secret-management/test-store-secret.js b/secret-management/test-store-secret.js new file mode 100644 index 0000000..5c32950 --- /dev/null +++ b/secret-management/test-store-secret.js @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2025 Radix IoT LLC. All rights reserved. + */ + +// Load the encryption utilities from store-encrypted-secret.js +const path = services.fileStoreService.getPathForRead('default', 'script-examples/secret-management/store-encrypted-secret.js'); +const encryptionScript = load(path); + +// Import the functions we need from the loaded script +eval(encryptionScript); + +const LOG = LoggerFactory.getLogger('TestStoreSecret'); + +// Example usage +try { + LOG.info('Starting secure string encryption example...'); + + const secretKey = 'MySecureEncryptionKey123456789012'; // 32 characters for AES-256 + const secretToStore = 'my-api-key-12345'; + + // Store encrypted secret + storeEncryptedSecret( + 'api-key-secret', // XID + 'External API Key', // Name + secretToStore, // Secret to encrypt + secretKey, // Encryption key + MangoPermission.superadminOnly(), // Read permission + MangoPermission.superadminOnly(), // Edit permission + false, //Should regenerate + 10, //Regenerate interval + "SECONDS" //Regenerate Unit + ); + + // Retrieve and decrypt secret + const retrievedSecret = retrieveAndDecryptSecret('api-key-secret', secretKey); + console.log('Original secret: ' + secretToStore); + console.log('Retrieved secret: ' + retrievedSecret); + console.log('Secrets match: ' + (secretToStore === retrievedSecret)); + +} catch (e) { + LOG.error('Script execution failed: ' + e.message); +} \ No newline at end of file