From 78469faf92044c7ad7b3a829ae2b5e4f943748b7 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Thu, 29 Sep 2011 16:56:14 +0000 Subject: [PATCH] JCR Keyring git-svn-id: https://svn.argeo.org/commons/trunk@4765 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- .../META-INF/spring/loginModules.xml | 2 +- .../org.argeo.security.equinox/pom.xml | 3 +- .../META-INF/jaas_default.txt | 9 + .../ui/rcp/SecureApplicationActivator.java | 21 -- .../plugins/org.argeo.security.ui/plugin.xml | 2 +- .../argeo/security/ui/SecurityUiPlugin.java | 45 ++-- .../ui/dialogs/DefaultLoginDialog.java | 21 +- .../security/ui/internal/CurrentUser.java | 6 +- .../ui/keyring}/KeyringLoginModule.java | 6 +- .../main/java/org/argeo/jcr/ArgeoNames.java | 13 ++ .../main/java/org/argeo/jcr/ArgeoTypes.java | 6 + .../src/main/java/org/argeo/jcr/JcrUtils.java | 42 ++++ .../org/argeo/jcr/security/JcrKeyring.java | 218 ++++++++++++++++++ .../main/resources/org/argeo/jcr/argeo.cnd | 26 +++ 14 files changed, 368 insertions(+), 52 deletions(-) rename security/plugins/{org.argeo.security.equinox/src/main/java/org/argeo/security/equinox => org.argeo.security.ui/src/main/java/org/argeo/security/ui/keyring}/KeyringLoginModule.java (91%) create mode 100644 server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/JcrKeyring.java diff --git a/security/plugins/org.argeo.security.equinox/META-INF/spring/loginModules.xml b/security/plugins/org.argeo.security.equinox/META-INF/spring/loginModules.xml index d661b5f56..f409ecd99 100644 --- a/security/plugins/org.argeo.security.equinox/META-INF/spring/loginModules.xml +++ b/security/plugins/org.argeo.security.equinox/META-INF/spring/loginModules.xml @@ -14,7 +14,7 @@ - diff --git a/security/plugins/org.argeo.security.equinox/pom.xml b/security/plugins/org.argeo.security.equinox/pom.xml index d3528a980..31cd1da14 100644 --- a/security/plugins/org.argeo.security.equinox/pom.xml +++ b/security/plugins/org.argeo.security.equinox/pom.xml @@ -43,7 +43,8 @@ *, org.springframework.core, - org.argeo.eclipse.spring + org.argeo.eclipse.spring, + org.argeo.util.crypto diff --git a/security/plugins/org.argeo.security.ui.rcp/META-INF/jaas_default.txt b/security/plugins/org.argeo.security.ui.rcp/META-INF/jaas_default.txt index 96747d3ea..98e39b54f 100644 --- a/security/plugins/org.argeo.security.ui.rcp/META-INF/jaas_default.txt +++ b/security/plugins/org.argeo.security.ui.rcp/META-INF/jaas_default.txt @@ -16,3 +16,12 @@ WINDOWS { org.eclipse.equinox.security.auth.module.ExtensionLoginModule required extensionId="org.argeo.security.equinox.osSpringLoginModule"; }; + +KEYRING_OLD { + org.eclipse.equinox.security.auth.module.ExtensionLoginModule required + extensionId="org.argeo.security.equinox.keyringLoginModule"; +}; + +KEYRING { + org.argeo.util.crypto.KeyringLoginModule required; +}; diff --git a/security/plugins/org.argeo.security.ui.rcp/src/main/java/org/argeo/security/ui/rcp/SecureApplicationActivator.java b/security/plugins/org.argeo.security.ui.rcp/src/main/java/org/argeo/security/ui/rcp/SecureApplicationActivator.java index d5617d773..1c8bd7c25 100644 --- a/security/plugins/org.argeo.security.ui.rcp/src/main/java/org/argeo/security/ui/rcp/SecureApplicationActivator.java +++ b/security/plugins/org.argeo.security.ui.rcp/src/main/java/org/argeo/security/ui/rcp/SecureApplicationActivator.java @@ -1,19 +1,11 @@ package org.argeo.security.ui.rcp; -import java.io.IOException; import java.net.URL; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.UnsupportedCallbackException; - -import org.argeo.security.ui.dialogs.DefaultLoginDialog; import org.eclipse.equinox.security.auth.ILoginContext; import org.eclipse.equinox.security.auth.LoginContextFactory; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.ServiceRegistration; /** Activator able to create {@link ILoginContext} */ public class SecureApplicationActivator implements BundleActivator { @@ -23,25 +15,12 @@ public class SecureApplicationActivator implements BundleActivator { private static final String JAAS_CONFIG_FILE = "/META-INF/jaas_default.txt"; private static BundleContext bundleContext; - private ServiceRegistration callbackHandlerRegistration; public void start(BundleContext bundleContext) throws Exception { SecureApplicationActivator.bundleContext = bundleContext; - - CallbackHandler callbackHandler = new CallbackHandler() { - - public void handle(Callback[] callbacks) throws IOException, - UnsupportedCallbackException { - DefaultLoginDialog dialog = new DefaultLoginDialog(); - dialog.handle(callbacks); - } - }; - callbackHandlerRegistration = bundleContext.registerService( - CallbackHandler.class.getName(), callbackHandler, null); } public void stop(BundleContext context) throws Exception { - callbackHandlerRegistration.unregister(); } static ILoginContext createLoginContext(String context) { diff --git a/security/plugins/org.argeo.security.ui/plugin.xml b/security/plugins/org.argeo.security.ui/plugin.xml index a9ad7d848..59ccd3bee 100644 --- a/security/plugins/org.argeo.security.ui/plugin.xml +++ b/security/plugins/org.argeo.security.ui/plugin.xml @@ -21,7 +21,7 @@ point="org.eclipse.equinox.security.callbackHandlerMapping"> + configName="NIX"> sharedState, Map options) { this.subject = subject; + if (subject == null) { + subject = Subject.getSubject(AccessController.getContext()); + } this.callbackHandler = callbackHandler; } diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java index 86a909483..2dc0c2eb4 100644 --- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java +++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoNames.java @@ -7,8 +7,12 @@ public interface ArgeoNames { public final static String ARGEO_URI = "argeo:uri"; public final static String ARGEO_USER_ID = "argeo:userID"; + public final static String ARGEO_PREFERENCES = "argeo:preferences"; public final static String ARGEO_DATA_MODEL_VERSION = "argeo:dataModelVersion"; + public final static String ARGEO_REMOTE = "argeo:remote"; + public final static String ARGEO_PASSWORD = "argeo:password"; + // user profile public final static String ARGEO_PROFILE = "argeo:profile"; public final static String ARGEO_FIRST_NAME = "argeo:firstName"; @@ -19,4 +23,13 @@ public interface ArgeoNames { // tabular public final static String ARGEO_IS_KEY = "argeo:isKey"; + // crypto + public final static String ARGEO_IV = "argeo:iv"; + public final static String ARGEO_SECRET_KEY_FACTORY = "argeo:secretKeyFactory"; + public final static String ARGEO_SALT = "argeo:salt"; + public final static String ARGEO_ITERATION_COUNT = "argeo:iterationCount"; + public final static String ARGEO_KEY_LENGTH = "argeo:keyLength"; + public final static String ARGEO_SECRET_KEY_ENCRYPTION = "argeo:secretKeyEncryption"; + public final static String ARGEO_CIPHER = "argeo:cipher"; + public final static String ARGEO_KEYRING = "argeo:keyring"; } diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoTypes.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoTypes.java index 8dfab71da..7416759b8 100644 --- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoTypes.java +++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/ArgeoTypes.java @@ -5,9 +5,15 @@ public interface ArgeoTypes { public final static String ARGEO_LINK = "argeo:link"; public final static String ARGEO_USER_HOME = "argeo:userHome"; public final static String ARGEO_USER_PROFILE = "argeo:userProfile"; + public final static String ARGEO_REMOTE_REPOSITORY = "argeo:remoteRepository"; // tabular public final static String ARGEO_TABLE = "argeo:table"; public final static String ARGEO_COLUMN = "argeo:column"; public final static String ARGEO_CSV = "argeo:csv"; + + // crypto + public final static String ARGEO_ENCRYPTED = "argeo:encrypted"; + public final static String ARGEO_PBE_SPEC = "argeo:pbeSpec"; + } diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java index 6dfb4a017..760e66009 100644 --- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java +++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java @@ -16,6 +16,9 @@ package org.argeo.jcr; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.text.DateFormat; @@ -52,6 +55,7 @@ import javax.jcr.query.qom.QueryObjectModelFactory; import javax.jcr.query.qom.Selector; import javax.jcr.query.qom.StaticOperand; +import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.ArgeoException; @@ -660,6 +664,43 @@ public class JcrUtils implements ArgeoJcrConstants { binary.dispose(); } + /** Retrieve a {@link Binary} as a byte array */ + public static byte[] getBinaryAsBytes(Property property) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InputStream in = null; + Binary binary = null; + try { + binary = property.getBinary(); + in = binary.getStream(); + IOUtils.copy(in, out); + return out.toByteArray(); + } catch (Exception e) { + throw new ArgeoException("Cannot read binary " + property + + " as bytes", e); + } finally { + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(in); + closeQuietly(binary); + } + } + + /** Writes a {@link Binary} from a byte array */ + public static void setBinaryAsBytes(Node node, String property, byte[] bytes) { + InputStream in = null; + Binary binary = null; + try { + in = new ByteArrayInputStream(bytes); + binary = node.getSession().getValueFactory().createBinary(in); + node.setProperty(property, binary); + } catch (Exception e) { + throw new ArgeoException("Cannot read binary " + property + + " as bytes", e); + } finally { + IOUtils.closeQuietly(in); + closeQuietly(binary); + } + } + /** * Creates depth from a string (typically a username) by adding levels based * on its first characters: "aBcD",2 => a/aB @@ -995,6 +1036,7 @@ public class JcrUtils implements ArgeoJcrConstants { NodeIterator ni = node.getNodes(); while (ni.hasNext()) curNodeSize += getNodeApproxSize(ni.nextNode()); + log.debug(node + ": " + curNodeSize); return curNodeSize; } catch (RepositoryException re) { throw new ArgeoException( diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/JcrKeyring.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/JcrKeyring.java new file mode 100644 index 000000000..7d9baad95 --- /dev/null +++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/JcrKeyring.java @@ -0,0 +1,218 @@ +package org.argeo.jcr.security; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.argeo.ArgeoException; +import org.argeo.jcr.ArgeoNames; +import org.argeo.jcr.ArgeoTypes; +import org.argeo.jcr.JcrUtils; +import org.argeo.util.crypto.AbstractKeyring; +import org.argeo.util.crypto.PBEKeySpecCallback; + +/** JCR based implementation of a keyring */ +public class JcrKeyring extends AbstractKeyring implements ArgeoNames { + private Session session; + + /** + * When setup is called the session has not yet been saved and we don't want + * to save it since there maybe other data which would be inconsistent. So + * we keep a reference to this node which will then be used (an reset to + * null) when handling the PBE callback. We keep one per thread in case + * multiple users are accessing the same instance of a keyring. + */ + private ThreadLocal notYetSavedKeyring = new ThreadLocal() { + + @Override + protected Node initialValue() { + return null; + } + }; + + @Override + protected Boolean isSetup() { + try { + if (notYetSavedKeyring.get() != null) + return true; + + Node userHome = JcrUtils.getUserHome(session); + return userHome.hasNode(ARGEO_KEYRING); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot check whether keyring is setup", e); + } + } + + @Override + protected void setup() { + Binary binary = null; + InputStream in = null; + try { + Node userHome = JcrUtils.getUserHome(session); + if (userHome.hasNode(ARGEO_KEYRING)) + throw new ArgeoException("Keyring already setup"); + Node keyring = userHome.addNode(ARGEO_KEYRING); + keyring.addMixin(ArgeoTypes.ARGEO_PBE_SPEC); + + // deterministic salt and iteration count based on username + String username = session.getUserID(); + byte[] salt = new byte[8]; + byte[] usernameBytes = username.getBytes(); + for (int i = 0; i < salt.length; i++) { + if (i < usernameBytes.length) + salt[i] = usernameBytes[i]; + else + salt[i] = 0; + } + in = new ByteArrayInputStream(salt); + binary = session.getValueFactory().createBinary(in); + keyring.setProperty(ARGEO_SALT, binary); + + Long iterationCount = username.length() * 200l; + keyring.setProperty(ARGEO_ITERATION_COUNT, iterationCount); + + // default algo + // TODO check if algo and key length are available, use DES if not + keyring.setProperty(ARGEO_SECRET_KEY_FACTORY, "PBKDF2WithHmacSHA1"); + keyring.setProperty(ARGEO_KEY_LENGTH, 256l); + keyring.setProperty(ARGEO_SECRET_KEY_ENCRYPTION, "AES"); + keyring.setProperty(ARGEO_CIPHER, "AES/CBC/PKCS5Padding"); + + notYetSavedKeyring.set(keyring); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot setup keyring", e); + } finally { + JcrUtils.closeQuietly(binary); + IOUtils.closeQuietly(in); + // JcrUtils.discardQuietly(session); + } + } + + @Override + protected void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) { + try { + Node userHome = JcrUtils.getUserHome(session); + Node keyring; + if (userHome.hasNode(ARGEO_KEYRING)) + keyring = userHome.getNode(ARGEO_KEYRING); + else if (notYetSavedKeyring.get() != null) + keyring = notYetSavedKeyring.get(); + else + throw new ArgeoException("Keyring not setup"); + pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY) + .getString(), JcrUtils.getBinaryAsBytes(keyring + .getProperty(ARGEO_SALT)), + (int) keyring.getProperty(ARGEO_ITERATION_COUNT).getLong(), + (int) keyring.getProperty(ARGEO_KEY_LENGTH).getLong(), + keyring.getProperty(ARGEO_SECRET_KEY_ENCRYPTION) + .getString()); + + if (notYetSavedKeyring.get() != null) + notYetSavedKeyring.remove(); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot handle key spec callback", e); + } + } + + /** The node must already exist at this path */ + @Override + protected void encrypt(String path, InputStream unencrypted) { + // should be called first for lazy initialization + SecretKey secretKey = getSecretKey(); + + Binary binary = null; + InputStream in = null; + + try { + Cipher cipher = createCipher(); + if (!session.nodeExists(path)) + throw new ArgeoException("No node at " + path); + Node node = session.getNode(path); + node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + byte[] iv = cipher.getIV(); + if (iv != null) { + JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv); + } + in = new CipherInputStream(unencrypted, cipher); + binary = session.getValueFactory().createBinary(in); + node.setProperty(Property.JCR_DATA, binary); + } catch (Exception e) { + throw new ArgeoException("Cannot encrypt", e); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(unencrypted); + JcrUtils.closeQuietly(binary); + } + } + + @Override + protected InputStream decrypt(String path) { + // should be called first for lazy initialization + SecretKey secretKey = getSecretKey(); + + Binary binary = null; + InputStream encrypted = null; + + try { + Cipher cipher = createCipher(); + if (!session.nodeExists(path)) + throw new ArgeoException("No node at " + path); + Node node = session.getNode(path); + if (node.hasProperty(ARGEO_IV)) { + byte[] iv = JcrUtils.getBinaryAsBytes(node + .getProperty(ARGEO_IV)); + cipher.init(Cipher.DECRYPT_MODE, secretKey, + new IvParameterSpec(iv)); + } else { + cipher.init(Cipher.DECRYPT_MODE, secretKey); + } + binary = node.getProperty(Property.JCR_DATA).getBinary(); + encrypted = binary.getStream(); + return new CipherInputStream(encrypted, cipher); + } catch (Exception e) { + throw new ArgeoException("Cannot decrypt", e); + } finally { + // IOUtils.closeQuietly(encrypted); + JcrUtils.closeQuietly(binary); + } + } + + protected Cipher createCipher() { + try { + Node userHome = JcrUtils.getUserHome(session); + if (!userHome.hasNode(ARGEO_KEYRING)) + throw new ArgeoException("Keyring not setup"); + Node keyring = userHome.getNode(ARGEO_KEYRING); + Cipher cipher = Cipher.getInstance(keyring + .getProperty(ARGEO_CIPHER).getString()); + return cipher; + } catch (Exception e) { + throw new ArgeoException("Cannot get cipher", e); + } + } + + public void changePassword(char[] oldPassword, char[] newPassword) { + // TODO Auto-generated method stub + + } + + public Session getSession() { + return session; + } + + public void setSession(Session session) { + this.session = session; + } + +} diff --git a/server/runtime/org.argeo.server.jcr/src/main/resources/org/argeo/jcr/argeo.cnd b/server/runtime/org.argeo.server.jcr/src/main/resources/org/argeo/jcr/argeo.cnd index 8fa59aceb..315e2c244 100644 --- a/server/runtime/org.argeo.server.jcr/src/main/resources/org/argeo/jcr/argeo.cnd +++ b/server/runtime/org.argeo.server.jcr/src/main/resources/org/argeo/jcr/argeo.cnd @@ -12,11 +12,18 @@ mixin mixin - argeo:userID (STRING) m + argeo:profile (argeo:userProfile) ++ argeo:keyring (argeo:pbeSpec) ++ argeo:preferences (nt:unstructured) [argeo:userProfile] > mix:created, mix:lastModified, mix:title, mix:versionable mixin - argeo:userID (STRING) m +[argeo:remoteRepository] > nt:unstructured +- argeo:uri (STRING) +- argeo:userID (STRING) ++ argeo:password (argeo:encrypted) + // TABULAR CONTENT [argeo:table] > nt:file + * (argeo:column) * @@ -25,3 +32,22 @@ mixin - jcr:requiredType (STRING) = 'STRING' [argeo:csv] > nt:resource + +// CRYPTO +[argeo:encrypted] > nt:base +mixin +// initialization vector used by some algorithms +- argeo:iv (BINARY) + +[argeo:pbeKeySpec] > nt:base +mixin +- argeo:secretKeyFactory (STRING) +- argeo:salt (BINARY) +- argeo:iterationCount (LONG) +- argeo:keyLength (LONG) +- argeo:secretKeyEncryption (STRING) + +[argeo:pbeSpec] > argeo:pbeKeySpec +mixin +- argeo:cipher (STRING) + -- 2.39.2