<property name="authenticationManager" ref="authenticationManager" />
</bean>
- <bean id="keyringLoginModule" class="org.argeo.security.equinox.KeyringLoginModule"
+ <bean id="keyringLoginModule" class="org.argeo.crypto.KeyringLoginModule"
scope="prototype">
</bean>
</beans>
<instructions>
<Import-Package>*,
org.springframework.core,
- org.argeo.eclipse.spring
+ org.argeo.eclipse.spring,
+ org.argeo.util.crypto
</Import-Package>
</instructions>
</configuration>
+++ /dev/null
-package org.argeo.security.equinox;
-
-import java.util.Map;
-import java.util.Set;
-
-import javax.security.auth.Subject;
-import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.CallbackHandler;
-import javax.security.auth.callback.PasswordCallback;
-import javax.security.auth.login.LoginException;
-import javax.security.auth.spi.LoginModule;
-
-import org.apache.commons.logging.LogConfigurationException;
-import org.argeo.util.crypto.PasswordBasedEncryption;
-
-public class KeyringLoginModule implements LoginModule {
- private Subject subject;
- private CallbackHandler callbackHandler;
- private PasswordBasedEncryption passwordBasedEncryption;
-
- public void initialize(Subject subject, CallbackHandler callbackHandler,
- Map<String, ?> sharedState, Map<String, ?> options) {
- this.subject = subject;
- this.callbackHandler = callbackHandler;
- }
-
- public boolean login() throws LoginException {
- Set<PasswordBasedEncryption> pbes = subject
- .getPrivateCredentials(PasswordBasedEncryption.class);
- if (pbes.size() > 0)
- return true;
- PasswordCallback pc = new PasswordCallback("Master password", false);
- Callback[] callbacks = { pc };
- try {
- callbackHandler.handle(callbacks);
- passwordBasedEncryption = new PasswordBasedEncryption(
- pc.getPassword());
- } catch (Exception e) {
- throw new LogConfigurationException(e);
- }
- return true;
- }
-
- public boolean commit() throws LoginException {
- if (passwordBasedEncryption != null)
- subject.getPrivateCredentials(PasswordBasedEncryption.class).add(
- passwordBasedEncryption);
- return true;
- }
-
- public boolean abort() throws LoginException {
- return true;
- }
-
- public boolean logout() throws LoginException {
- Set<PasswordBasedEncryption> pbes = subject
- .getPrivateCredentials(PasswordBasedEncryption.class);
- pbes.clear();
- return true;
- }
-
-}
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;
+};
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 {
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) {
point="org.eclipse.equinox.security.callbackHandlerMapping">
<callbackHandlerMapping
callbackHandlerId="org.argeo.security.ui.defaultLoginDialog"
- configName="UNIX">
+ configName="NIX">
</callbackHandlerMapping>
</extension>
<extension
package org.argeo.security.ui;
+import java.io.IOException;
+
+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.jface.resource.ImageDescriptor;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
/**
* The activator class controls the plug-in life cycle
// The plug-in ID
public static final String PLUGIN_ID = "org.argeo.security.ui"; //$NON-NLS-1$
- // The shared instance
- private static SecurityUiPlugin plugin;
+ public final static String CONTEXT_KEYRING = "KEYRING";
- /**
- * The constructor
- */
- public SecurityUiPlugin() {
- }
+ private CallbackHandler defaultCallbackHandler;
+ private ServiceRegistration defaultCallbackHandlerReg;
+
+ private static SecurityUiPlugin plugin;
- /*
- * (non-Javadoc)
- *
- * @see
- * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext
- * )
- */
public void start(BundleContext context) throws Exception {
super.start(context);
plugin = this;
+
+ defaultCallbackHandler = new CallbackHandler() {
+ public void handle(Callback[] callbacks) throws IOException,
+ UnsupportedCallbackException {
+ DefaultLoginDialog dialog = new DefaultLoginDialog();
+ dialog.handle(callbacks);
+ }
+ };
+ defaultCallbackHandlerReg = context.registerService(
+ CallbackHandler.class.getName(), defaultCallbackHandler, null);
}
- /*
- * (non-Javadoc)
- *
- * @see
- * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext
- * )
- */
public void stop(BundleContext context) throws Exception {
plugin = null;
+ defaultCallbackHandlerReg.unregister();
super.stop(context);
}
return new Point(300, 180);
}
+ @Override
+ protected Control createContents(Composite parent) {
+ Control control = super.createContents(parent);
+ parent.pack();
+ return control;
+ }
+
protected Control createDialogArea(Composite parent) {
Composite dialogarea = (Composite) super.createDialogArea(parent);
Composite composite = new Composite(dialogarea, SWT.NONE);
composite.setLayout(new GridLayout(2, false));
composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
createCallbackHandlers(composite);
- parent.pack();
+ // parent.pack();
return composite;
}
for (int i = 0; i < callbacks.length; i++) {
Callback callback = callbacks[i];
if (callback instanceof TextOutputCallback) {
- createTextoutputHandler(composite,
+ createLabelTextoutputHandler(composite,
(TextOutputCallback) callback);
} else if (callback instanceof NameCallback) {
createNameHandler(composite, (NameCallback) callback);
passwordText.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent event) {
+ // FIXME use getTextChars() in Eclipse 3.7
callback.setPassword(passwordText.getText().toCharArray());
}
});
});
}
- private void createTextoutputHandler(Composite composite,
- TextOutputCallback callback) {
+ private void createLabelTextoutputHandler(Composite composite,
+ final TextOutputCallback callback) {
+ Label label = new Label(composite, SWT.NONE);
+ label.setText(callback.getMessage());
+ GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
+ data.horizontalSpan = 2;
+ label.setLayoutData(data);
// TODO: find a way to pass this information
// int messageType = callback.getMessageType();
// int dialogMessageType = IMessageProvider.NONE;
import org.springframework.security.Authentication;
import org.springframework.security.GrantedAuthority;
+/**
+ * Retrieves information about the current user. Not an API, can change without
+ * notice.
+ */
public class CurrentUser {
public final static String getUsername() {
Subject subject = getSubject();
}
public final static Subject getSubject() {
-
Subject subject = Subject.getSubject(AccessController.getContext());
if (subject == null)
throw new ArgeoException("Not authenticated.");
return subject;
-
}
}
--- /dev/null
+package org.argeo.security.ui.keyring;
+
+import java.security.AccessController;
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import org.apache.commons.logging.LogConfigurationException;
+import org.argeo.util.crypto.PasswordBasedEncryption;
+
+public class KeyringLoginModule implements LoginModule {
+ private Subject subject;
+ private CallbackHandler callbackHandler;
+ private PasswordBasedEncryption passwordBasedEncryption;
+
+ public void initialize(Subject subject, CallbackHandler callbackHandler,
+ Map<String, ?> sharedState, Map<String, ?> options) {
+ this.subject = subject;
+ if (subject == null) {
+ subject = Subject.getSubject(AccessController.getContext());
+ }
+ this.callbackHandler = callbackHandler;
+ }
+
+ public boolean login() throws LoginException {
+ Set<PasswordBasedEncryption> pbes = subject
+ .getPrivateCredentials(PasswordBasedEncryption.class);
+ if (pbes.size() > 0)
+ return true;
+ PasswordCallback pc = new PasswordCallback("Master password", false);
+ Callback[] callbacks = { pc };
+ try {
+ callbackHandler.handle(callbacks);
+ passwordBasedEncryption = new PasswordBasedEncryption(
+ pc.getPassword());
+ } catch (Exception e) {
+ throw new LogConfigurationException(e);
+ }
+ return true;
+ }
+
+ public boolean commit() throws LoginException {
+ if (passwordBasedEncryption != null)
+ subject.getPrivateCredentials(PasswordBasedEncryption.class).add(
+ passwordBasedEncryption);
+ return true;
+ }
+
+ public boolean abort() throws LoginException {
+ return true;
+ }
+
+ public boolean logout() throws LoginException {
+ Set<PasswordBasedEncryption> pbes = subject
+ .getPrivateCredentials(PasswordBasedEncryption.class);
+ pbes.clear();
+ return true;
+ }
+
+}
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";
// 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";
}
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";
+
}
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;
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;
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
NodeIterator ni = node.getNodes();
while (ni.hasNext())
curNodeSize += getNodeApproxSize(ni.nextNode());
+ log.debug(node + ": " + curNodeSize);
return curNodeSize;
} catch (RepositoryException re) {
throw new ArgeoException(
--- /dev/null
+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<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
+
+ @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;
+ }
+
+}
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) *
- 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)
+