[java-support] branch master updated: JSPT-87 - Scriptable DataSealerKeyStrategy

Scott Cantor cantor.2 at osu.edu
Mon Jun 10 09:55:24 EDT 2019


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch master
in repository java-support.

View the commit online:
http://git.shibboleth.net/view/?p=java-support.git;a=commit;h=bbb32f7fb68437546e3bf257694e0d28b9dd29ef

The following commit(s) were added to refs/heads/master by this push:
       new  bbb32f7   JSPT-87 - Scriptable DataSealerKeyStrategy
bbb32f7 is described below

commit bbb32f7fb68437546e3bf257694e0d28b9dd29ef
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Jun 10 09:55:22 2019 -0400

    JSPT-87 - Scriptable DataSealerKeyStrategy
    
    https://issues.shibboleth.net/jira/browse/JSPT-87
---
 .../support/security/impl/ScriptedKeyStrategy.java | 303 +++++++++++++++++++++
 .../security/impl/ScriptedKeyStrategyTest.java     |  98 +++++++
 .../java/support/security/keyStrategyScript.js     |   7 +
 3 files changed, 408 insertions(+)

diff --git a/src/main/java/net/shibboleth/utilities/java/support/security/impl/ScriptedKeyStrategy.java b/src/main/java/net/shibboleth/utilities/java/support/security/impl/ScriptedKeyStrategy.java
new file mode 100644
index 0000000..099d956
--- /dev/null
+++ b/src/main/java/net/shibboleth/utilities/java/support/security/impl/ScriptedKeyStrategy.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.utilities.java.support.security.impl;
+
+import java.security.KeyException;
+import java.time.Duration;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
+import javax.script.ScriptContext;
+import javax.script.ScriptException;
+import javax.script.SimpleScriptContext;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.TimerSupport;
+import net.shibboleth.utilities.java.support.scripting.EvaluableScript;
+import net.shibboleth.utilities.java.support.security.DataSealerKeyStrategy;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Implements a strategy for access to versioned symmetric keys using scripts.
+ * 
+ * <p>Suitable for integrating with external key services.</p>
+ */
+public class ScriptedKeyStrategy extends AbstractInitializableComponent implements DataSealerKeyStrategy {
+    
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ScriptedKeyStrategy.class);
+
+    /** Script to obtain keys. */
+    @NonnullAfterInit private EvaluableScript keyScript;
+    
+    /** Custom object for script. */
+    @Nullable private Object customObject;
+    
+    /** Current key alias loaded. */
+    @NonnullAfterInit private String currentAlias;
+
+    /** Current default key loaded. */
+    @NonnullAfterInit private SecretKey defaultKey;
+    
+    /** Cache of keys. */
+    @Nonnull private final LinkedHashMap<String,SecretKey> keyCache;
+    
+    /** Time between key update checks. Default value: (PT15M). */
+    @Nonnull private Duration updateInterval;
+
+    /** Timer used to schedule update tasks. */
+    @Nullable private Timer updateTaskTimer;
+
+    /** Timer used to schedule update tasks if no external one set. */
+    @Nullable private Timer internalTaskTimer;
+
+    /** Task that checks for updated key version. */
+    @Nullable private TimerTask updateTask;
+    
+    /** Constructor. */
+    public ScriptedKeyStrategy() {
+        keyCache = new LinkedHashMap<>(10);
+        updateInterval = Duration.ofMinutes(15);
+    }
+
+    /**
+     * Set the script to run to access keys.
+     * 
+     * @param script script to run
+     */
+    public void setKeyScript(@Nonnull final EvaluableScript script) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        keyScript = Constraint.isNotNull(script, "Script cannot be null");
+    }
+    
+
+    /**
+     * Set the custom (externally provided) object.
+     * 
+     * @param object the custom object
+     */
+    public void setCustomObject(@Nullable final Object object) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        customObject = object;
+    }
+
+    /**
+     * Set the time between key update checks. A value of 0 indicates that no updates will be
+     * performed.
+     * 
+     * This setting cannot be changed after the service has been initialized.
+     * 
+     * @param interval time between key update checks
+     */
+    public void setUpdateInterval(@Nonnull final Duration interval) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        Constraint.isNotNull(interval, "Interval cannot be null");
+        Constraint.isFalse(interval.isNegative(), "Interval cannot be negative");
+
+        updateInterval = interval;
+    }
+
+    /**
+     * Set the timer used to schedule update tasks.
+     * 
+     * This setting cannot be changed after the service has been initialized.
+     * 
+     * @param timer timer used to schedule update tasks
+     */
+    public void setUpdateTaskTimer(@Nullable final Timer timer) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        updateTaskTimer = timer;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (keyScript == null) {
+            throw new ComponentInitializationException("Script cannot be null");
+        }
+        
+        try {
+            updateDefaultKey();
+    
+        } catch (final KeyException e) {
+            log.error("Error loading default key", e);
+            throw new ComponentInitializationException("Exception loading the default key", e);
+        }
+
+        if (!updateInterval.isZero()) {
+            updateTask = new TimerTask() {
+                public void run() {
+                    try {
+                        updateDefaultKey();
+                    } catch (final KeyException e) {
+                        
+                    }
+                }
+            };
+            if (updateTaskTimer == null) {
+                internalTaskTimer = new Timer(TimerSupport.getTimerName(this), true);
+            } else {
+                internalTaskTimer = updateTaskTimer;
+            }
+            internalTaskTimer.schedule(updateTask, updateInterval.toMillis(), updateInterval.toMillis());
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doDestroy() {
+        if (updateTask != null) {
+            updateTask.cancel();
+            updateTask = null;
+            if (updateTaskTimer == null) {
+                internalTaskTimer.cancel();
+            }
+            internalTaskTimer = null;
+        }
+        super.doDestroy();
+    }
+
+    /** {@inheritDoc} */
+    @Nonnull public Pair<String,SecretKey> getDefaultKey() throws KeyException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        
+        synchronized(this) {
+            if (defaultKey != null) {
+                return new Pair<>(currentAlias, defaultKey);
+            } else {
+                throw new KeyException("Default key unavailable");
+            }
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Nonnull public SecretKey getKey(@Nonnull @NotEmpty final String name) throws KeyException {
+        synchronized(this) {
+            if (defaultKey != null && name.equals(currentAlias)) {
+                return defaultKey;
+            } else if (keyCache.containsKey(name)) {
+                return keyCache.get(name);
+            }
+        }
+
+        try {
+            final SimpleScriptContext scriptContext = new SimpleScriptContext();
+            scriptContext.setAttribute("custom", customObject, ScriptContext.ENGINE_SCOPE);
+            scriptContext.setAttribute("name", name, ScriptContext.ENGINE_SCOPE);
+            
+            final Object result = keyScript.eval(scriptContext);
+            
+            if (result instanceof SecretKey) {
+                synchronized(this) {
+                    keyCache.put(name, (SecretKey) result);
+                }
+                log.debug("Loaded key '{}' from external script", name);
+                return (SecretKey) result;
+            } else if (result instanceof Pair && ((Pair) result).getSecond() instanceof SecretKey) {
+                synchronized(this) {
+                    keyCache.put(name, (SecretKey) ((Pair<String,SecretKey>) result).getSecond());
+                }
+                log.debug("Loaded key '{}' from external script", name);
+                return ((Pair<String,SecretKey>) result).getSecond();
+            } else {
+                throw new KeyException("Script did not return SecretKey or Pair<String,SecretKey> result.");
+            }
+        } catch (final ScriptException e) {
+            throw new KeyException(e);
+        }
+    }
+
+    /**
+     * Update the loaded copy of the default key based on the current key version if it's out of date
+     * (loading key version from scratch if need be).
+     * 
+     * <p>Also purge cache to limit size.</p>
+     * 
+     * @throws KeyException if the key cannot be updated
+     */
+    private void updateDefaultKey() throws KeyException {
+        
+        synchronized(this) {
+            int size = keyCache.size();
+            if (size > 30) {
+                final Iterator<String> iter = keyCache.keySet().iterator();
+                while (size > 30) {
+                    iter.next();
+                    iter.remove();
+                    size--;
+                }
+            }
+        }
+            
+        try {
+            final SimpleScriptContext scriptContext = new SimpleScriptContext();
+            scriptContext.setAttribute("custom", customObject, ScriptContext.ENGINE_SCOPE);
+            
+            final Object result = keyScript.eval(scriptContext);
+            
+            if (result instanceof Pair) {
+                final Pair p = (Pair) result;
+                if (p.getFirst() instanceof String && p.getSecond() instanceof SecretKey) {
+                    synchronized(this) {
+                        if (currentAlias == null) {
+                            log.info("Loaded initial default key: {}", p.getFirst());
+                        } else if (!currentAlias.equals(p.getFirst())) {
+                            log.info("Updated default key from {} to {}", currentAlias, p.getFirst());
+                        } else {
+                            log.debug("Default key version has not changed, still {}", currentAlias);
+                            return;
+                        }
+                        
+                        currentAlias = (String) p.getFirst();
+                        defaultKey = (SecretKey) p.getSecond();
+                        keyCache.put((String) p.getFirst(), (SecretKey) p.getSecond());
+                    }
+                } else {
+                    throw new KeyException("Script did not return Pair<String,SecretKey> result.");
+                }
+            } else {
+                throw new KeyException("Script did not return Pair<String,SecretKey> result.");
+            }
+        } catch (final ScriptException e) {
+            throw new KeyException(e);
+        }
+    }
+    
+}
\ No newline at end of file
diff --git a/src/test/java/net/shibboleth/utilities/java/support/security/impl/ScriptedKeyStrategyTest.java b/src/test/java/net/shibboleth/utilities/java/support/security/impl/ScriptedKeyStrategyTest.java
new file mode 100644
index 0000000..06040d0
--- /dev/null
+++ b/src/test/java/net/shibboleth/utilities/java/support/security/impl/ScriptedKeyStrategyTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.utilities.java.support.security.impl;
+
+import java.io.File;
+import java.security.KeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.KeyGenerator;
+import javax.script.ScriptException;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.scripting.EvaluableScript;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/**
+ * Test for {@link ScriptedKeyStrategy}.
+ */
+public class ScriptedKeyStrategyTest {
+
+    final String scriptPath = "src/test/resources/net/shibboleth/utilities/java/support/security/keyStrategyScript.js";
+    
+    private Map<String,Object> customMap;
+    
+    private KeyGenerator keyGenerator;
+    
+    private ScriptedKeyStrategy strategy;
+    
+    @BeforeMethod public void setUp() throws ComponentInitializationException, NoSuchAlgorithmException, ScriptException {
+
+        final SecureRandom random = new SecureRandom(); 
+        keyGenerator = KeyGenerator.getInstance("AES");
+        keyGenerator.init(random); 
+        customMap = new HashMap<>();
+        customMap.put("secret1", keyGenerator.generateKey());
+        customMap.put("default", "secret1");
+        
+        strategy = new ScriptedKeyStrategy();
+        strategy.setUpdateInterval(Duration.ofSeconds(1));
+        strategy.setKeyScript(new EvaluableScript("javascript", new File(scriptPath)));
+        strategy.setCustomObject(customMap);
+        strategy.initialize();
+    }
+    
+    @Test(expectedExceptions=ComponentInitializationException.class)
+    public void testNoScript() throws ComponentInitializationException {
+        final ScriptedKeyStrategy strategy = new ScriptedKeyStrategy();
+        strategy.initialize();
+    }
+    
+    @Test(expectedExceptions=ComponentInitializationException.class)
+    public void testScriptFailure() throws ComponentInitializationException, ScriptException {
+        final ScriptedKeyStrategy strategy = new ScriptedKeyStrategy();
+        strategy.setKeyScript(new EvaluableScript("null"));
+        strategy.initialize();
+    }
+    
+    @Test public void testScriptedKeystoreKeyStrategy() throws Exception {
+
+        
+        Assert.assertEquals(strategy.getDefaultKey().getFirst(), "secret1");
+        try {
+            strategy.getKey("secret2");
+            Assert.fail("secret2 should not exist");
+        } catch (final KeyException e) {
+
+        }
+
+        customMap.put("secret2", keyGenerator.generateKey());
+        customMap.put("default", "secret2");
+        Thread.sleep(5000);
+        Assert.assertEquals(strategy.getDefaultKey().getFirst(), "secret2");
+        Assert.assertNotNull(strategy.getKey("secret1"));
+    }
+    
+}
\ No newline at end of file
diff --git a/src/test/resources/net/shibboleth/utilities/java/support/security/keyStrategyScript.js b/src/test/resources/net/shibboleth/utilities/java/support/security/keyStrategyScript.js
new file mode 100644
index 0000000..22c64f6
--- /dev/null
+++ b/src/test/resources/net/shibboleth/utilities/java/support/security/keyStrategyScript.js
@@ -0,0 +1,7 @@
+var Pair = Java.type("net.shibboleth.utilities.java.support.collection.Pair");
+
+if (typeof name != "undefined" && name != null) {
+    custom.get(name);
+} else {
+    new Pair(custom.get("default"), custom.get(custom.get("default")));
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list