Skip to content

Conversation

terrypacker
Copy link
Contributor

@terrypacker terrypacker commented Aug 1, 2025

Overview

The use case for this is to allow a Mango user to write a script that can refresh an API Key on a set interval so they can continue to use an API from an Event Handler that needs to use an API with a Key Rotation requirement. We cannot know when the event handler will be activated so we must rely on some other mechanism to refresh the keys before they expire.

The traditional way of doing this would be to make the request and if it fails to authenticate just renew the token. The reason we are trying a new approach is due to this happening many times at the same time from Event Handler. So I think a global locking mechanism to reload the token could be used, but at the risk of backing up all the event handlers that are waiting on a key.

Goals

These examples attempt to accomplish 2 things:

  1. Provide a utility that allows generating and storing a secure secret in Mango with bi-directional encryption. We need the un-encrypted value back at some point in the future.
  2. Provide a utility that allows scheduling (and stopping) a task to renew the secret

Concerns

The main concerns I want reviewed are:

  1. How do we safely handle the Encryption Key, as it is currently stored in the script.
  2. There are probably some considerations about how tasks can remain alive beyond trying to stop them from the JSON Store entry.

Concept

The idea is that the script will generate a secret into the JSON Data Store and allow you to schedule a timer task to reload it using a callback so you can control how the secret is created. When you want to stop the task you update the JSON Data entry to have shouldRegenerate = false

Copy link
Collaborator

@jazdw jazdw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still pretty confused about what the use case for this is? What are you trying to actually achieve here?
All in all I think this is a pretty bad idea, the code doesn't belong in a script, the keys shouldn't be stored in a JSON store or in a script.

try {
LOG.info('Starting secure string encryption example...');

const secretKey = 'MySecureEncryptionKey123456789012'; // 32 characters for AES-256
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I know the key can't go here but I figured we could discuss where to put it and if there is somewhere safe to do this.

@terrypacker
Copy link
Contributor Author

I've updated the description with the reasoning why I put any time into this. I also wasn't originally sure if this was going to be possible safely so instead of taking up a bunch of everyones time I code up a few examples quickly using Junie and put them out here for us to discuss.

I orignally wanted to use the Mango keystore for this but I'm not sure how to store random strings in it.

@terrypacker
Copy link
Contributor Author

terrypacker commented Aug 4, 2025

Upon further investigation I could probably do something like this using a KeyStore.SecretKeyEntry if we wanted to use the keystore, in this I would ideally load the keystore using mango.properties

    //Load the keystore (Ideally we have Mango APIs for this)
    KeyStore ks = KeyStore.getInstance("JCEKS"); // Or "PKCS12", "JKS", etc.

    //Password for keystore, could load from Mango Properties
    char[] ksPassword = "keystorePassword".toCharArray();

    // To load an existing keystore:
    // FileInputStream fis = new FileInputStream("myKeystore.jks");
    // ks.load(fis, ksPassword);
    // fis.close();

    // To create a new keystore:
    ks.load(null, ksPassword); // Load with null stream for a new keystore

    //Store the Secret
    String secretString = "mySensitiveData123";
    byte[] secretBytes = secretString.getBytes("UTF-8"); // Specify encoding
    SecretKey secretKey = new SecretKeySpec(secretBytes, "AES"); // Use a suitable algorithm

    KeyStore.SecretKeyEntry secretEntry = new KeyStore.SecretKeyEntry(secretKey);
    
    String alias = "mySecretAlias";
   
    //Add a password to the entry (Not sure if this is any use as there is no safe place to store this password)
    char[] entryPassword = "entryPassword".toCharArray();
    KeyStore.ProtectionParameter protectionParameter = new KeyStore.PasswordProtection(entryPassword);

    ks.setEntry(alias, secretEntry, protectionParameter);
   
    //Update the keystore
    FileOutputStream fos = new FileOutputStream("myKeystore.jks");
    ks.store(fos, ksPassword);
    fos.close();

@terrypacker
Copy link
Contributor Author

Ha ha, I just found this class when looking at our script tools in Mango: com.infiniteautomation.mango.spring.script.bindings.TimeoutAndInterval. this lets you schedule tasks for one time or repeated use.

I was looking at a way to create a Global lock, which can easily be done by adding another Binding in the Java Code that contains code to handling the locking so that in the scripts one could do this:

// Works in both engines
var resourceName = "database-connection";

// Simple exclusive lock
GlobalScriptContext.withWriteLock(resourceName, function() {
    print("Exclusive access to " + resourceName);
    
    // Store some global state
    var currentValue = GlobalScriptContext.getGlobalData("counter") || 0;
    GlobalScriptContext.setGlobalData("counter", currentValue + 1);
    
    print("Counter is now: " + (currentValue + 1));
});

// Read lock for concurrent reads
GlobalScriptContext.withReadLock(resourceName, function() {
    var counter = GlobalScriptContext.getGlobalData("counter");
    print("Current counter value: " + counter);
});

But this is plenty of rope to hang yourself with, and I'm pretty against putting a way to lock code into the script environment.

@terrypacker
Copy link
Contributor Author

However I would be for adding a global map storage that we can use to persist data between scripts. Then if someone was tricky enough they could create and store a lock there... This seems like a better idea. I don't think we currently support that, if we did it would look something like this to allow us to add bindings at runtime:

@Component
public class RuntimeBindingsDefinition extends ScriptBindingsDefinition {
    
    @Override
    public MangoPermission requiredPermission() {
        return MangoPermission.requireAnyRole(BuiltinRoles.SUPERADMIN);
    }
    
    @Override
    public void addBindings(MangoScript script, Bindings engineBindings, 
                           Object synchronizationObject, ScriptEngineDefinition engineDefinition) {
        
        // Expose a function to add bindings during execution
        engineBindings.put("addBinding", new BiConsumer<String, Object>() {
            @Override
            public void accept(String key, Object value) {
                synchronized (synchronizationObject) {
                    engineBindings.put(key, value);
                }
            }
        });
        
        // Expose a function to remove bindings during execution
        engineBindings.put("removeBinding", new Consumer<String>() {
            @Override
            public void accept(String key) {
                synchronized (synchronizationObject) {
                    engineBindings.remove(key);
                }
            }
        });
        
        // Expose a function to check if binding exists
        engineBindings.put("hasBinding", new Function<String, Boolean>() {
            @Override
            public Boolean apply(String key) {
                synchronized (synchronizationObject) {
                    return engineBindings.containsKey(key);
                }
            }
        });
        
        // Expose the bindings map itself (read-only access)
        engineBindings.put("getAllBindings", new Supplier<Map<String, Object>>() {
            @Override
            public Map<String, Object> get() {
                synchronized (synchronizationObject) {
                    return new HashMap<>(engineBindings);
                }
            }
        });
    }
}

Usage:

// Add a lock during script execution
var lockName = "myDynamicLock-" + Math.random();
var newLock = new java.util.concurrent.locks.ReentrantLock();
addBinding(lockName, newLock);

// Use the newly created lock
var createdLock = eval(lockName); // Access the binding by name
try {
    createdLock.lock();
    print("Using dynamically created lock: " + lockName);
    
    // Create more bindings during execution
    addBinding("timestamp", new Date().getTime());
    addBinding("processId", java.lang.Thread.currentThread().getId());
    
} finally {
    createdLock.unlock();
}

// Check if binding exists and use it
if (hasBinding("timestamp")) {
    print("Timestamp was: " + timestamp);
}

// Create a shared counter that other scripts can use
if (!hasBinding("globalCounter")) {
    addBinding("globalCounter", new java.util.concurrent.atomic.AtomicInteger(0));
}
var counter = globalCounter.incrementAndGet();
print("Execution count: " + counter);

@terrypacker
Copy link
Contributor Author

After discussion we have come up with some alternative ways to accomplish this:

  1. Setup the token to be stored into a file on disk
  2. Refresh that token using an Advanced Schedule and handler to write the new token to a file
  3. The event handler's that need the token can then read it from disk and use it.

The token doesn't need be stored in an encrypted state, just protected from other user's viewing.

This use-case is probably a little extreme for a Script in Mango.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants