[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