From: Mathieu Baudier Date: Mon, 5 Feb 2018 13:42:55 +0000 (+0100) Subject: Implement keyring change password X-Git-Tag: argeo-commons-2.1.71~15 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=6d206b9052689ffa880cd4593bfefa704dc0dd46;p=lgpl%2Fargeo-commons.git Implement keyring change password --- diff --git a/org.argeo.cms.ui.workbench.rap/META-INF/spring/commands.xml b/org.argeo.cms.ui.workbench.rap/META-INF/spring/commands.xml index d9c72b419..2bfa179a4 100644 --- a/org.argeo.cms.ui.workbench.rap/META-INF/spring/commands.xml +++ b/org.argeo.cms.ui.workbench.rap/META-INF/spring/commands.xml @@ -9,6 +9,7 @@ scope="prototype"> + diff --git a/org.argeo.cms.ui.workbench.rap/META-INF/spring/osgi.xml b/org.argeo.cms.ui.workbench.rap/META-INF/spring/osgi.xml index 6e0f3b332..231b7d883 100644 --- a/org.argeo.cms.ui.workbench.rap/META-INF/spring/osgi.xml +++ b/org.argeo.cms.ui.workbench.rap/META-INF/spring/osgi.xml @@ -10,4 +10,5 @@ + diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenChangePasswordDialog.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenChangePasswordDialog.java index 64f4ff9b8..30836b948 100644 --- a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenChangePasswordDialog.java +++ b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenChangePasswordDialog.java @@ -35,6 +35,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; import org.argeo.eclipse.ui.dialogs.ErrorFeedback; +import org.argeo.node.security.CryptoKeyring; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; @@ -59,6 +60,7 @@ public class OpenChangePasswordDialog extends AbstractHandler { private final static Log log = LogFactory.getLog(OpenChangePasswordDialog.class); private UserAdmin userAdmin; private UserTransaction userTransaction; + private CryptoKeyring keyring = null; public Object execute(ExecutionEvent event) throws ExecutionException { ChangePasswordDialog dialog = new ChangePasswordDialog(HandlerUtil.getActiveShell(event), userAdmin); @@ -87,6 +89,10 @@ public class OpenChangePasswordDialog extends AbstractHandler { try { userTransaction.begin(); user.getCredentials().put(null, newPassword); + if (keyring != null) { + keyring.changePassword(oldPassword, newPassword); + // TODO change secret keys in the CMS session + } userTransaction.commit(); } catch (Exception e) { try { @@ -162,4 +168,9 @@ public class OpenChangePasswordDialog extends AbstractHandler { public void setUserTransaction(UserTransaction userTransaction) { this.userTransaction = userTransaction; } + + public void setKeyring(CryptoKeyring keyring) { + this.keyring = keyring; + } + } diff --git a/org.argeo.cms/ext/test/org/argeo/cms/security/PasswordBasedEncryptionTest.java b/org.argeo.cms/ext/test/org/argeo/cms/security/PasswordBasedEncryptionTest.java index 49319f15a..5c43e34f3 100644 --- a/org.argeo.cms/ext/test/org/argeo/cms/security/PasswordBasedEncryptionTest.java +++ b/org.argeo.cms/ext/test/org/argeo/cms/security/PasswordBasedEncryptionTest.java @@ -35,7 +35,7 @@ import junit.framework.TestCase; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.argeo.cms.security.PasswordBasedEncryption; +import org.argeo.util.PasswordEncryption; public class PasswordBasedEncryptionTest extends TestCase { private final static Log log = LogFactory @@ -43,7 +43,7 @@ public class PasswordBasedEncryptionTest extends TestCase { public void testEncryptDecrypt() { final String password = "test long password since they are safer"; - PasswordBasedEncryption pbeEnc = new PasswordBasedEncryption( + PasswordEncryption pbeEnc = new PasswordEncryption( password.toCharArray()); String message = "Hello World!"; log.info("Password:\t'" + password + "'"); @@ -51,7 +51,7 @@ public class PasswordBasedEncryptionTest extends TestCase { byte[] encrypted = pbeEnc.encryptString(message); log.info("Encrypted:\t'" + DatatypeConverter.printBase64Binary(encrypted) + "'"); - PasswordBasedEncryption pbeDec = new PasswordBasedEncryption( + PasswordEncryption pbeDec = new PasswordEncryption( password.toCharArray()); InputStream in = null; in = new ByteArrayInputStream(encrypted); diff --git a/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java index eca28e8b9..19c0d60ed 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java @@ -55,6 +55,7 @@ public class AnonymousLoginModule implements LoginModule { if (request != null) locale = request.getLocale(); CmsAuthUtils.addAuthorization(subject, authorization, locale, request); + CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale); if (log.isTraceEnabled()) log.trace("Anonymous logged in to CMS: " + subject); return true; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java index 661cc6905..aa313ee0a 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java @@ -93,7 +93,7 @@ class CmsAuthUtils { throw new CmsException("Cannot commit", e); } - registerSessionAuthorization(request, subject, authorization, locale); + // registerSessionAuthorization(request, subject, authorization, locale); } private static void checkSubjectEmpty(Subject subject) { @@ -121,7 +121,7 @@ class CmsAuthUtils { // subject.getPrincipals().removeAll(subject.getPrincipals(AnonymousPrincipal.class)); } - private synchronized static void registerSessionAuthorization(HttpServletRequest request, Subject subject, + synchronized static void registerSessionAuthorization(HttpServletRequest request, Subject subject, Authorization authorization, Locale locale) { // synchronized in order to avoid multiple registrations // TODO move it to a service in order to avoid static synchronization @@ -144,6 +144,8 @@ class CmsAuthUtils { throw new CmsException("Inconsistent user " + authorization.getName() + " for existing CMS session " + cmsSession); } + // keyring + subject.getPrivateCredentials().addAll(cmsSession.getSecretKeys()); } else {// anonymous if (cmsSession.getAuthorization().getName() != null) { cmsSession.close(); @@ -151,10 +153,9 @@ class CmsAuthUtils { cmsSession = null; } } - } - - if (cmsSession == null) + } else if (cmsSession == null) { cmsSession = new WebCmsSessionImpl(subject, authorization, locale, request); + } // request.setAttribute(CmsSession.class.getName(), cmsSession); CmsSessionId nodeSessionId = new CmsSessionId(cmsSession.getUuid()); if (subject.getPrivateCredentials(CmsSessionId.class).size() == 0) @@ -165,7 +166,9 @@ class CmsAuthUtils { throw new CmsException( "Subject already logged with session " + storedSessionId + " (not " + nodeSessionId + ")"); } - } else { + } else + + { // TODO desktop, CLI } } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java index ccd02b5b2..81ca5baf3 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.security.cert.X509Certificate; import java.util.Base64; import java.util.Collection; +import java.util.Locale; import java.util.Map; import java.util.StringTokenizer; @@ -119,7 +120,9 @@ public class HttpSessionLoginModule implements LoginModule { } if (authorization != null) { - CmsAuthUtils.addAuthorization(subject, authorization, request.getLocale(), request); + Locale locale = request.getLocale(); + CmsAuthUtils.addAuthorization(subject, authorization,locale , request); + CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale); cleanUp(); return true; } else { diff --git a/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java index 2c4958254..09fece03a 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java @@ -30,8 +30,8 @@ import javax.security.auth.callback.PasswordCallback; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; -import org.argeo.cms.security.PasswordBasedEncryption; import org.argeo.node.security.PBEKeySpecCallback; +import org.argeo.util.PasswordEncryption; /** Adds a secret key to the private credentials */ public class KeyringLoginModule implements LoginModule { @@ -39,8 +39,8 @@ public class KeyringLoginModule implements LoginModule { private CallbackHandler callbackHandler; private SecretKey secretKey; - public void initialize(Subject subject, CallbackHandler callbackHandler, - Map sharedState, Map options) { + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map options) { this.subject = subject; if (subject == null) { subject = Subject.getSubject(AccessController.getContext()); @@ -49,9 +49,9 @@ public class KeyringLoginModule implements LoginModule { } public boolean login() throws LoginException { - Set pbes = subject.getPrivateCredentials(SecretKey.class); - if (pbes.size() > 0) - return true; +// Set pbes = subject.getPrivateCredentials(SecretKey.class); +// if (pbes.size() > 0) +// return true; PasswordCallback pc = new PasswordCallback("Master password", false); PBEKeySpecCallback pbeCb = new PBEKeySpecCallback(); Callback[] callbacks = { pc, pbeCb }; @@ -59,21 +59,17 @@ public class KeyringLoginModule implements LoginModule { callbackHandler.handle(callbacks); char[] password = pc.getPassword(); - SecretKeyFactory keyFac = SecretKeyFactory.getInstance(pbeCb - .getSecretKeyFactory()); + SecretKeyFactory keyFac = SecretKeyFactory.getInstance(pbeCb.getSecretKeyFactory()); PBEKeySpec keySpec; if (pbeCb.getKeyLength() != null) - keySpec = new PBEKeySpec(password, pbeCb.getSalt(), - pbeCb.getIterationCount(), pbeCb.getKeyLength()); + keySpec = new PBEKeySpec(password, pbeCb.getSalt(), pbeCb.getIterationCount(), pbeCb.getKeyLength()); else - keySpec = new PBEKeySpec(password, pbeCb.getSalt(), - pbeCb.getIterationCount()); + keySpec = new PBEKeySpec(password, pbeCb.getSalt(), pbeCb.getIterationCount()); String secKeyEncryption = pbeCb.getSecretKeyEncryption(); if (secKeyEncryption != null) { SecretKey tmp = keyFac.generateSecret(keySpec); - secretKey = new SecretKeySpec(tmp.getEncoded(), - secKeyEncryption); + secretKey = new SecretKeySpec(tmp.getEncoded(), secKeyEncryption); } else { secretKey = keyFac.generateSecret(keySpec); } @@ -86,8 +82,10 @@ public class KeyringLoginModule implements LoginModule { } public boolean commit() throws LoginException { - if (secretKey != null) + if (secretKey != null) { + subject.getPrivateCredentials().removeAll(subject.getPrivateCredentials(SecretKey.class)); subject.getPrivateCredentials().add(secretKey); + } return true; } @@ -96,8 +94,7 @@ public class KeyringLoginModule implements LoginModule { } public boolean logout() throws LoginException { - Set pbes = subject - .getPrivateCredentials(PasswordBasedEncryption.class); + Set pbes = subject.getPrivateCredentials(PasswordEncryption.class); pbes.clear(); return true; } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java index e39918e40..683d13b21 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -29,11 +29,13 @@ import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; import org.argeo.cms.internal.kernel.Activator; import org.argeo.naming.LdapAttrs; +import org.argeo.node.security.CryptoKeyring; import org.argeo.osgi.useradmin.AuthenticatingUser; import org.argeo.osgi.useradmin.IpaUtils; import org.argeo.osgi.useradmin.OsUserUtils; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; import org.osgi.service.useradmin.Authorization; import org.osgi.service.useradmin.User; import org.osgi.service.useradmin.UserAdmin; @@ -122,6 +124,8 @@ public class UserAdminLoginModule implements LoginModule { password = passwordCallback.getPassword(); else throw new CredentialNotFoundException("No credentials provided"); + sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, username); + sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, password); } User user = searchForUser(userAdmin, username); if (user == null) @@ -204,9 +208,38 @@ public class UserAdminLoginModule implements LoginModule { throw new LoginException( "User admin found no authorization for authenticated user " + authenticatingUser.getName()); } + // Log and monitor new login - CmsAuthUtils.addAuthorization(subject, authorization, locale, - (HttpServletRequest) sharedState.get(CmsAuthUtils.SHARED_STATE_HTTP_REQUEST)); + HttpServletRequest request = (HttpServletRequest) sharedState.get(CmsAuthUtils.SHARED_STATE_HTTP_REQUEST); + CmsAuthUtils.addAuthorization(subject, authorization, locale, request); + + // Unlock keyring (underlying login to the JCR repository) + char[] password = (char[]) sharedState.get(CmsAuthUtils.SHARED_STATE_PWD); + if (password != null) { + ServiceReference keyringSr = bc.getServiceReference(CryptoKeyring.class); + if (keyringSr != null) { + CryptoKeyring keyring = bc.getService(keyringSr); + Subject.doAs(subject, new PrivilegedAction() { + + @Override + public Void run() { + try { + keyring.unlock(password); + } catch (Exception e) { + e.printStackTrace(); + log.warn("Could not unlock keyring with the password provided by " + authorization.getName() + + ": " + e.getMessage()); + } + return null; + } + + }); + } + } + + // Register CmsSession with initial subject + CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale); + if (log.isDebugEnabled()) log.debug("Logged in to CMS: " + subject); return true; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java index 9b667717b..863f7c202 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import javax.crypto.SecretKey; import javax.jcr.Repository; import javax.jcr.Session; import javax.naming.InvalidNameException; @@ -122,6 +123,10 @@ public class CmsSessionImpl implements CmsSession { private Subject getSubject() { return Subject.getSubject(initialContext); } + + public Set getSecretKeys() { + return getSubject().getPrivateCredentials(SecretKey.class); + } public synchronized Session getDataSession(String cn, String workspace, Repository repository) { // FIXME make it more robust diff --git a/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java b/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java index 7f4e960cd..779406a1d 100644 --- a/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java +++ b/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java @@ -25,7 +25,6 @@ import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.security.AccessController; -import java.security.MessageDigest; import java.security.Provider; import java.security.Security; import java.util.Arrays; @@ -43,15 +42,16 @@ import javax.security.auth.login.LoginException; import org.apache.commons.io.IOUtils; import org.argeo.cms.CmsException; +import org.argeo.node.NodeConstants; import org.argeo.node.security.CryptoKeyring; import org.argeo.node.security.Keyring; import org.argeo.node.security.PBEKeySpecCallback; /** username / password based keyring. TODO internationalize */ public abstract class AbstractKeyring implements Keyring, CryptoKeyring { - public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING"; + // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING"; - private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT; + // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT; private CallbackHandler defaultCallbackHandler; private String charset = "UTF-8"; @@ -82,16 +82,18 @@ public abstract class AbstractKeyring implements Keyring, CryptoKeyring { protected abstract InputStream decrypt(String path); /** Triggers lazy initialization */ - protected SecretKey getSecretKey() { + protected SecretKey getSecretKey(char[] password) { Subject subject = Subject.getSubject(AccessController.getContext()); // we assume only one secrete key is available Iterator iterator = subject.getPrivateCredentials(SecretKey.class).iterator(); - if (!iterator.hasNext()) {// not initialized - CallbackHandler callbackHandler = new KeyringCallbackHandler(); + if (!iterator.hasNext() || password!=null) {// not initialized + CallbackHandler callbackHandler = password == null ? new KeyringCallbackHandler() + : new PasswordProvidedCallBackHandler(password); ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); try { - LoginContext loginContext = new LoginContext(loginContextName, subject, callbackHandler); + LoginContext loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_KEYRING, subject, + callbackHandler); loginContext.login(); // FIXME will login even if password is wrong iterator = subject.getPrivateCredentials(SecretKey.class).iterator(); @@ -119,48 +121,55 @@ public abstract class AbstractKeyring implements Keyring, CryptoKeyring { } public char[] getAsChars(String path) { - InputStream in = getAsStream(path); - CharArrayWriter writer = null; - Reader reader = null; - try { - writer = new CharArrayWriter(); - reader = new InputStreamReader(in, charset); + // InputStream in = getAsStream(path); + // CharArrayWriter writer = null; + // Reader reader = null; + try (InputStream in = getAsStream(path); + CharArrayWriter writer = new CharArrayWriter(); + Reader reader = new InputStreamReader(in, charset);) { IOUtils.copy(reader, writer); return writer.toCharArray(); } catch (IOException e) { throw new CmsException("Cannot decrypt to char array", e); } finally { - IOUtils.closeQuietly(reader); - IOUtils.closeQuietly(in); - IOUtils.closeQuietly(writer); + // IOUtils.closeQuietly(reader); + // IOUtils.closeQuietly(in); + // IOUtils.closeQuietly(writer); } } public void set(String path, char[] arr) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ByteArrayInputStream in = null; - Writer writer = null; - try { - writer = new OutputStreamWriter(out, charset); + // ByteArrayOutputStream out = new ByteArrayOutputStream(); + // ByteArrayInputStream in = null; + // Writer writer = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, charset);) { + // writer = new OutputStreamWriter(out, charset); writer.write(arr); writer.flush(); - in = new ByteArrayInputStream(out.toByteArray()); - set(path, in); + // in = new ByteArrayInputStream(out.toByteArray()); + try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());) { + set(path, in); + } } catch (IOException e) { throw new CmsException("Cannot encrypt to char array", e); } finally { - IOUtils.closeQuietly(writer); - IOUtils.closeQuietly(out); - IOUtils.closeQuietly(in); + // IOUtils.closeQuietly(writer); + // IOUtils.closeQuietly(out); + // IOUtils.closeQuietly(in); } } - protected Provider getSecurityProvider() { - return Security.getProvider(securityProviderName); + public void unlock(char[] password) { + if (!isSetup()) + setup(password); + SecretKey secretKey = getSecretKey(password); + if (secretKey == null) + throw new CmsException("Could not unlock keyring"); } - public void setLoginContextName(String loginContextName) { - this.loginContextName = loginContextName; + protected Provider getSecurityProvider() { + return Security.getProvider(securityProviderName); } public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) { @@ -175,31 +184,32 @@ public abstract class AbstractKeyring implements Keyring, CryptoKeyring { this.securityProviderName = securityProviderName; } - @Deprecated - protected static byte[] hash(char[] password, byte[] salt, Integer iterationCount) { - ByteArrayOutputStream out = null; - OutputStreamWriter writer = null; - try { - out = new ByteArrayOutputStream(); - writer = new OutputStreamWriter(out, "UTF-8"); - writer.write(password); - MessageDigest pwDigest = MessageDigest.getInstance("SHA-256"); - pwDigest.reset(); - pwDigest.update(salt); - byte[] btPass = pwDigest.digest(out.toByteArray()); - for (int i = 0; i < iterationCount; i++) { - pwDigest.reset(); - btPass = pwDigest.digest(btPass); - } - return btPass; - } catch (Exception e) { - throw new CmsException("Cannot hash", e); - } finally { - IOUtils.closeQuietly(out); - IOUtils.closeQuietly(writer); - } - - } + // @Deprecated + // protected static byte[] hash(char[] password, byte[] salt, Integer + // iterationCount) { + // ByteArrayOutputStream out = null; + // OutputStreamWriter writer = null; + // try { + // out = new ByteArrayOutputStream(); + // writer = new OutputStreamWriter(out, "UTF-8"); + // writer.write(password); + // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256"); + // pwDigest.reset(); + // pwDigest.update(salt); + // byte[] btPass = pwDigest.digest(out.toByteArray()); + // for (int i = 0; i < iterationCount; i++) { + // pwDigest.reset(); + // btPass = pwDigest.digest(btPass); + // } + // return btPass; + // } catch (Exception e) { + // throw new CmsException("Cannot hash", e); + // } finally { + // IOUtils.closeQuietly(out); + // IOUtils.closeQuietly(writer); + // } + // + // } /** * Convenience method using the underlying callback to ask for a password @@ -223,7 +233,7 @@ public abstract class AbstractKeyring implements Keyring, CryptoKeyring { // checks if (callbacks.length != 2) throw new IllegalArgumentException( - "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}"); + "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}"); if (!(callbacks[0] instanceof PasswordCallback)) throw new UnsupportedCallbackException(callbacks[0]); if (!(callbacks[1] instanceof PBEKeySpecCallback)) @@ -266,4 +276,30 @@ public abstract class AbstractKeyring implements Keyring, CryptoKeyring { } } + + class PasswordProvidedCallBackHandler implements CallbackHandler { + private final char[] password; + + public PasswordProvidedCallBackHandler(char[] password) { + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + // checks + if (callbacks.length != 2) + throw new IllegalArgumentException( + "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}"); + if (!(callbacks[0] instanceof PasswordCallback)) + throw new UnsupportedCallbackException(callbacks[0]); + if (!(callbacks[1] instanceof PBEKeySpecCallback)) + throw new UnsupportedCallbackException(callbacks[0]); + + PasswordCallback passwordCb = (PasswordCallback) callbacks[0]; + passwordCb.setPassword(password); + PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1]; + handleKeySpecCallback(pbeCb); + } + + } } diff --git a/org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java b/org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java index 43eab4b3c..04e3eb9db 100644 --- a/org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java +++ b/org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java @@ -17,8 +17,11 @@ package org.argeo.cms.security; import java.io.ByteArrayInputStream; import java.io.CharArrayReader; +import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.security.Provider; import java.security.SecureRandom; @@ -28,12 +31,16 @@ import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.jcr.Binary; import javax.jcr.Node; +import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; +import javax.jcr.query.Query; import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.argeo.cms.ArgeoNames; import org.argeo.cms.ArgeoTypes; import org.argeo.cms.CmsException; @@ -44,9 +51,9 @@ import org.argeo.node.security.PBEKeySpecCallback; /** JCR based implementation of a keyring */ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { + private final static Log log = LogFactory.getLog(JcrKeyring.class); /** - * Stronger with 256, but causes problem with Oracle JVM, force 128 in this - * case + * Stronger with 256, but causes problem with Oracle JVM, force 128 in this case */ public final static Long DEFAULT_SECRETE_KEY_LENGTH = 256l; public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; @@ -54,9 +61,9 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding"; private Integer iterationCountFactor = 200; - private Long secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH; - private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY; - private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION; + private Long secretKeyLength = DEFAULT_SECRETE_KEY_LENGTH; + private String secretKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY; + private String secretKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION; private String cipherName = DEFAULT_CIPHER_NAME; private final Repository repository; @@ -71,19 +78,19 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { // FIXME is it really still needed? /** - * 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. + * 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; - } - }; +// private ThreadLocal notYetSavedKeyring = new ThreadLocal() { +// +// @Override +// protected Node initialValue() { +// return null; +// } +// }; public JcrKeyring(Repository repository) { this.repository = repository; @@ -107,11 +114,12 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { } @Override - protected Boolean isSetup() { + protected synchronized Boolean isSetup() { try { - if (notYetSavedKeyring.get() != null) - return true; +// if (notYetSavedKeyring.get() != null) +// return true; + session().refresh(true); Node userHome = NodeUtils.getUserHome(session()); return userHome.hasNode(ARGEO_KEYRING); } catch (RepositoryException e) { @@ -120,28 +128,33 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { } @Override - protected void setup(char[] password) { + protected synchronized void setup(char[] password) { Binary binary = null; - InputStream in = null; + // InputStream in = null; try { + session().refresh(true); Node userHome = NodeUtils.getUserHome(session()); - if (userHome.hasNode(ARGEO_KEYRING)) - throw new ArgeoJcrException("Keyring already setup"); - Node keyring = userHome.addNode(ARGEO_KEYRING); + Node keyring; + if (userHome.hasNode(ARGEO_KEYRING)) { + throw new CmsException("Keyring already set up"); + } else { + 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(); + byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8); 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); + try (InputStream in = new ByteArrayInputStream(salt);) { + binary = session().getValueFactory().createBinary(in); + } keyring.setProperty(ARGEO_SALT, binary); Integer iterationCount = username.length() * iterationCountFactor; @@ -149,12 +162,12 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { // default algo // TODO check if algo and key length are available, use DES if not - keyring.setProperty(ARGEO_SECRET_KEY_FACTORY, secreteKeyFactoryName); - keyring.setProperty(ARGEO_KEY_LENGTH, secreteKeyLength); - keyring.setProperty(ARGEO_SECRET_KEY_ENCRYPTION, secreteKeyEncryption); + keyring.setProperty(ARGEO_SECRET_KEY_FACTORY, secretKeyFactoryName); + keyring.setProperty(ARGEO_KEY_LENGTH, secretKeyLength); + keyring.setProperty(ARGEO_SECRET_KEY_ENCRYPTION, secretKeyEncryption); keyring.setProperty(ARGEO_CIPHER, cipherName); - // keyring.getSession().save(); + keyring.getSession().save(); // encrypted password hash // IOUtils.closeQuietly(in); @@ -164,25 +177,26 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { // binary = session().getValueFactory().createBinary(in); // keyring.setProperty(ARGEO_PASSWORD, binary); - notYetSavedKeyring.set(keyring); +// notYetSavedKeyring.set(keyring); } catch (Exception e) { throw new ArgeoJcrException("Cannot setup keyring", e); } finally { JcrUtils.closeQuietly(binary); - IOUtils.closeQuietly(in); + // IOUtils.closeQuietly(in); // JcrUtils.discardQuietly(session()); } } @Override - protected void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) { + protected synchronized void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) { try { + session().refresh(true); Node userHome = NodeUtils.getUserHome(session()); Node keyring; if (userHome.hasNode(ARGEO_KEYRING)) keyring = userHome.getNode(ARGEO_KEYRING); - else if (notYetSavedKeyring.get() != null) - keyring = notYetSavedKeyring.get(); +// else if (notYetSavedKeyring.get() != null) +// keyring = notYetSavedKeyring.get(); else throw new ArgeoJcrException("Keyring not setup"); @@ -192,8 +206,8 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { (int) keyring.getProperty(ARGEO_KEY_LENGTH).getLong(), keyring.getProperty(ARGEO_SECRET_KEY_ENCRYPTION).getString()); - if (notYetSavedKeyring.get() != null) - notYetSavedKeyring.remove(); +// if (notYetSavedKeyring.get() != null) +// notYetSavedKeyring.remove(); } catch (RepositoryException e) { throw new ArgeoJcrException("Cannot handle key spec callback", e); } @@ -203,12 +217,13 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { @Override protected synchronized void encrypt(String path, InputStream unencrypted) { // should be called first for lazy initialization - SecretKey secretKey = getSecretKey(); + SecretKey secretKey = getSecretKey(null); + Cipher cipher = createCipher(); - Binary binary = null; - InputStream in = null; + // Binary binary = null; + // InputStream in = null; try { - Cipher cipher = createCipher(); + session().refresh(true); Node node; if (!session().nodeExists(path)) { String parentPath = JcrUtils.parentPath(path); @@ -219,6 +234,36 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { } else { node = session().getNode(path); } + encrypt(secretKey, cipher, node, unencrypted); + // node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED); + // SecureRandom random = new SecureRandom(); + // byte[] iv = new byte[16]; + // random.nextBytes(iv); + // cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); + // JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv); + // + // try (InputStream in = new CipherInputStream(unencrypted, cipher);) { + // binary = session().getValueFactory().createBinary(in); + // node.setProperty(Property.JCR_DATA, binary); + // session().save(); + // } + } catch (RepositoryException e) { + throw new ArgeoJcrException("Cannot encrypt", e); + } finally { + try { + unencrypted.close(); + } catch (IOException e) { + // silent + } + // IOUtils.closeQuietly(unencrypted); + // IOUtils.closeQuietly(in); + // JcrUtils.closeQuietly(binary); + JcrUtils.logoutQuietly(session()); + } + } + + protected synchronized void encrypt(SecretKey secretKey, Cipher cipher, Node node, InputStream unencrypted) { + try { node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED); SecureRandom random = new SecureRandom(); byte[] iv = new byte[16]; @@ -226,58 +271,70 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv); - in = new CipherInputStream(unencrypted, cipher); - binary = session().getValueFactory().createBinary(in); - node.setProperty(Property.JCR_DATA, binary); - session().save(); + Binary binary = null; + try (InputStream in = new CipherInputStream(unencrypted, cipher);) { + binary = session().getValueFactory().createBinary(in); + node.setProperty(Property.JCR_DATA, binary); + session().save(); + } finally { + JcrUtils.closeQuietly(binary); + } } catch (Exception e) { throw new ArgeoJcrException("Cannot encrypt", e); } finally { - IOUtils.closeQuietly(unencrypted); - IOUtils.closeQuietly(in); - JcrUtils.closeQuietly(binary); - JcrUtils.logoutQuietly(session()); + try { + unencrypted.close(); + } catch (IOException e) { + // silent + } + // IOUtils.closeQuietly(unencrypted); + // IOUtils.closeQuietly(in); + // JcrUtils.closeQuietly(binary); + // JcrUtils.logoutQuietly(session()); } } @Override protected synchronized InputStream decrypt(String path) { Binary binary = null; - InputStream encrypted = null; - Reader reader = null; + // InputStream encrypted = null; try { + session().refresh(true); if (!session().nodeExists(path)) { char[] password = ask(); - reader = new CharArrayReader(password); - return new ByteArrayInputStream(IOUtils.toByteArray(reader)); + Reader reader = new CharArrayReader(password); + return new ByteArrayInputStream(IOUtils.toByteArray(reader, StandardCharsets.UTF_8)); } else { // should be called first for lazy initialisation - SecretKey secretKey = getSecretKey(); - + SecretKey secretKey = getSecretKey(null); Cipher cipher = createCipher(); - 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); + return decrypt(secretKey, cipher, node); } } catch (Exception e) { throw new ArgeoJcrException("Cannot decrypt", e); } finally { - IOUtils.closeQuietly(encrypted); - IOUtils.closeQuietly(reader); + // IOUtils.closeQuietly(encrypted); + // IOUtils.closeQuietly(reader); JcrUtils.closeQuietly(binary); JcrUtils.logoutQuietly(session()); } } + protected synchronized InputStream decrypt(SecretKey secretKey, Cipher cipher, Node node) + throws RepositoryException, GeneralSecurityException { + 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 binary = node.getProperty(Property.JCR_DATA).getBinary(); + InputStream encrypted = binary.getStream(); + return new CipherInputStream(encrypted, cipher); + } + protected Cipher createCipher() { try { Node userHome = NodeUtils.getUserHome(session()); @@ -298,7 +355,25 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { } public synchronized void changePassword(char[] oldPassword, char[] newPassword) { - // TODO decrypt with old pw / encrypt with new pw all argeo:encrypted + // TODO make it XA compatible + SecretKey oldSecretKey = getSecretKey(oldPassword); + SecretKey newSecretKey = getSecretKey(newPassword); + Session session = session(); + try { + NodeIterator encryptedNodes = session.getWorkspace().getQueryManager() + .createQuery("select * from [argeo:encrypted]", Query.JCR_SQL2).execute().getNodes(); + while (encryptedNodes.hasNext()) { + Node node = encryptedNodes.nextNode(); + InputStream in = decrypt(oldSecretKey, createCipher(), node); + encrypt(newSecretKey, createCipher(), node, in); + if (log.isDebugEnabled()) + log.debug("Converted keyring encrypted value of " + node.getPath()); + } + } catch (RepositoryException | GeneralSecurityException e) { + throw new CmsException("Cannot change JCR keyring password", e); + } finally { + JcrUtils.logoutQuietly(session); + } } // public synchronized void setSession(Session session) { @@ -309,16 +384,16 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { this.iterationCountFactor = iterationCountFactor; } - public void setSecreteKeyLength(Long keyLength) { - this.secreteKeyLength = keyLength; + public void setSecretKeyLength(Long keyLength) { + this.secretKeyLength = keyLength; } - public void setSecreteKeyFactoryName(String secreteKeyFactoryName) { - this.secreteKeyFactoryName = secreteKeyFactoryName; + public void setSecretKeyFactoryName(String secreteKeyFactoryName) { + this.secretKeyFactoryName = secreteKeyFactoryName; } - public void setSecreteKeyEncryption(String secreteKeyEncryption) { - this.secreteKeyEncryption = secreteKeyEncryption; + public void setSecretKeyEncryption(String secreteKeyEncryption) { + this.secretKeyEncryption = secreteKeyEncryption; } public void setCipherName(String cipherName) { diff --git a/org.argeo.cms/src/org/argeo/cms/security/PasswordBasedEncryption.java b/org.argeo.cms/src/org/argeo/cms/security/PasswordBasedEncryption.java deleted file mode 100644 index a74cb592e..000000000 --- a/org.argeo.cms/src/org/argeo/cms/security/PasswordBasedEncryption.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2007-2012 Argeo GmbH - * - * Licensed 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 org.argeo.cms.security; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.Key; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -import org.apache.commons.io.IOUtils; -import org.argeo.cms.CmsException; - -/** Simple password based encryption / decryption */ -public class PasswordBasedEncryption { - public final static Integer DEFAULT_ITERATION_COUNT = 1024; - /** Stronger with 256, but causes problem with Oracle JVM */ - public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256; - public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128; - public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; - public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES"; - public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding"; - public final static String DEFAULT_CHARSET = "UTF-8"; - - private Integer iterationCount = DEFAULT_ITERATION_COUNT; - private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH; - private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY; - private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION; - private String cipherName = DEFAULT_CIPHER_NAME; - - private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, - (byte) 0xC8, (byte) 0x32, (byte) 0x56, (byte) 0x35, (byte) 0xE3, - (byte) 0x03 }; - private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, - (byte) 0xC8, (byte) 0x32, (byte) 0x56, (byte) 0x35, (byte) 0xE3, - (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, - (byte) 0x56, (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; - - private Key key; - private Cipher ecipher; - private Cipher dcipher; - - private String securityProviderName = null; - - /** - * This is up to the caller to clear the passed array. Neither copy of nor - * reference to the passed array is kept - */ - public PasswordBasedEncryption(char[] password) { - this(password, DEFAULT_SALT_8, DEFAULT_IV_16); - } - - /** - * This is up to the caller to clear the passed array. Neither copies of nor - * references to the passed arrays are kept - */ - public PasswordBasedEncryption(char[] password, byte[] passwordSalt, - byte[] initializationVector) { - try { - initKeyAndCiphers(password, passwordSalt, initializationVector); - } catch (InvalidKeyException e) { - Integer previousSecreteKeyLength = secreteKeyLength; - secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED; - System.err.println("'" + e.getMessage() + "', will use " - + secreteKeyLength + " secrete key length instead of " - + previousSecreteKeyLength); - try { - initKeyAndCiphers(password, passwordSalt, initializationVector); - } catch (Exception e1) { - throw new CmsException( - "Cannot get secret key (with restricted length)", e1); - } - } catch (Exception e) { - throw new CmsException("Cannot get secret key", e); - } - } - - protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, - byte[] initializationVector) throws GeneralSecurityException { - byte[] salt = new byte[8]; - System.arraycopy(passwordSalt, 0, salt, 0, salt.length); - // for (int i = 0; i < password.length && i < salt.length; i++) - // salt[i] = (byte) password[i]; - byte[] iv = new byte[16]; - System.arraycopy(initializationVector, 0, iv, 0, iv.length); - - SecretKeyFactory keyFac = SecretKeyFactory - .getInstance(getSecretKeyFactoryName()); - PBEKeySpec keySpec = new PBEKeySpec(password, salt, - getIterationCount(), getKeyLength()); - String secKeyEncryption = getSecretKeyEncryption(); - if (secKeyEncryption != null) { - SecretKey tmp = keyFac.generateSecret(keySpec); - key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption()); - } else { - key = keyFac.generateSecret(keySpec); - } - if (securityProviderName != null) - ecipher = Cipher.getInstance(getCipherName(), securityProviderName); - else - ecipher = Cipher.getInstance(getCipherName()); - ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); - dcipher = Cipher.getInstance(getCipherName()); - dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); - } - - public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) - throws IOException { - try { - CipherOutputStream out = new CipherOutputStream(encryptedOut, - ecipher); - IOUtils.copy(decryptedIn, out); - IOUtils.closeQuietly(out); - } catch (IOException e) { - throw e; - } catch (Exception e) { - throw new CmsException("Cannot encrypt", e); - } finally { - IOUtils.closeQuietly(decryptedIn); - } - } - - public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) - throws IOException { - try { - CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, - dcipher); - IOUtils.copy(decryptedIn, decryptedOut); - } catch (IOException e) { - throw e; - } catch (Exception e) { - throw new CmsException("Cannot decrypt", e); - } finally { - IOUtils.closeQuietly(encryptedIn); - } - } - - public byte[] encryptString(String str) { - ByteArrayOutputStream out = null; - ByteArrayInputStream in = null; - try { - out = new ByteArrayOutputStream(); - in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); - encrypt(in, out); - return out.toByteArray(); - } catch (Exception e) { - throw new CmsException("Cannot encrypt", e); - } finally { - IOUtils.closeQuietly(out); - } - } - - /** Closes the input stream */ - public String decryptAsString(InputStream in) { - ByteArrayOutputStream out = null; - try { - out = new ByteArrayOutputStream(); - decrypt(in, out); - return new String(out.toByteArray(), DEFAULT_CHARSET); - } catch (Exception e) { - throw new CmsException("Cannot decrypt", e); - } finally { - IOUtils.closeQuietly(out); - } - } - - protected Key getKey() { - return key; - } - - protected Cipher getEcipher() { - return ecipher; - } - - protected Cipher getDcipher() { - return dcipher; - } - - protected Integer getIterationCount() { - return iterationCount; - } - - protected Integer getKeyLength() { - return secreteKeyLength; - } - - protected String getSecretKeyFactoryName() { - return secreteKeyFactoryName; - } - - protected String getSecretKeyEncryption() { - return secreteKeyEncryption; - } - - protected String getCipherName() { - return cipherName; - } - - public void setIterationCount(Integer iterationCount) { - this.iterationCount = iterationCount; - } - - public void setSecreteKeyLength(Integer keyLength) { - this.secreteKeyLength = keyLength; - } - - public void setSecreteKeyFactoryName(String secreteKeyFactoryName) { - this.secreteKeyFactoryName = secreteKeyFactoryName; - } - - public void setSecreteKeyEncryption(String secreteKeyEncryption) { - this.secreteKeyEncryption = secreteKeyEncryption; - } - - public void setCipherName(String cipherName) { - this.cipherName = cipherName; - } - - public void setSecurityProviderName(String securityProviderName) { - this.securityProviderName = securityProviderName; - } -} diff --git a/org.argeo.node.api/src/org/argeo/node/NodeConstants.java b/org.argeo.node.api/src/org/argeo/node/NodeConstants.java index 2b4c284f6..22afe0065 100644 --- a/org.argeo.node.api/src/org/argeo/node/NodeConstants.java +++ b/org.argeo.node.api/src/org/argeo/node/NodeConstants.java @@ -1,22 +1,6 @@ package org.argeo.node; public interface NodeConstants { - /* - * PIDs - */ - String NODE_STATE_PID = "org.argeo.node.state"; - String NODE_DEPLOYMENT_PID = "org.argeo.node.deployment"; - String NODE_INSTANCE_PID = "org.argeo.node.instance"; - - String NODE_KEYRING_PID = "org.argeo.node.keyring"; - String NODE_FS_PROVIDER_PID = "org.argeo.node.fsProvider"; - - /* - * FACTORY PIDs - */ - String NODE_REPOS_FACTORY_PID = "org.argeo.node.repos"; - String NODE_USER_ADMIN_PID = "org.argeo.node.userAdmin"; - /* * DN ATTRIBUTES (RFC 4514) */ @@ -73,6 +57,7 @@ public interface NodeConstants { String LOGIN_CONTEXT_ANONYMOUS = "ANONYMOUS"; String LOGIN_CONTEXT_DATA_ADMIN = "DATA_ADMIN"; String LOGIN_CONTEXT_SINGLE_USER = "SINGLE_USER"; + String LOGIN_CONTEXT_KEYRING = "KEYRING"; /* * PATHS @@ -108,4 +93,20 @@ public interface NodeConstants { // HTTP String HTTP_PORT = "org.osgi.service.http.port"; String HTTP_PORT_SECURE = "org.osgi.service.http.port.secure"; + + /* + * PIDs + */ + String NODE_STATE_PID = "org.argeo.node.state"; + String NODE_DEPLOYMENT_PID = "org.argeo.node.deployment"; + String NODE_INSTANCE_PID = "org.argeo.node.instance"; + + String NODE_KEYRING_PID = "org.argeo.node.keyring"; + String NODE_FS_PROVIDER_PID = "org.argeo.node.fsProvider"; + + /* + * FACTORY PIDs + */ + String NODE_REPOS_FACTORY_PID = "org.argeo.node.repos"; + String NODE_USER_ADMIN_PID = "org.argeo.node.userAdmin"; } diff --git a/org.argeo.node.api/src/org/argeo/node/security/CryptoKeyring.java b/org.argeo.node.api/src/org/argeo/node/security/CryptoKeyring.java index 026fcb06d..dd3402277 100644 --- a/org.argeo.node.api/src/org/argeo/node/security/CryptoKeyring.java +++ b/org.argeo.node.api/src/org/argeo/node/security/CryptoKeyring.java @@ -19,5 +19,7 @@ package org.argeo.node.security; * Marker interface for an advanced keyring based on cryptography. */ public interface CryptoKeyring extends Keyring { + public void changePassword(char[] oldPassword, char[] newPassword); + public void unlock(char[] password); } diff --git a/org.argeo.node.api/src/org/argeo/node/security/Keyring.java b/org.argeo.node.api/src/org/argeo/node/security/Keyring.java index 467d9a8aa..fe054c3cc 100644 --- a/org.argeo.node.api/src/org/argeo/node/security/Keyring.java +++ b/org.argeo.node.api/src/org/argeo/node/security/Keyring.java @@ -23,8 +23,6 @@ import java.io.InputStream; * change. */ public interface Keyring { - public void changePassword(char[] oldPassword, char[] newPassword); - /** * Returns the confidential information as chars. Must ask for it if it is * not stored. diff --git a/org.argeo.util/src/org/argeo/util/PasswordEncryption.java b/org.argeo.util/src/org/argeo/util/PasswordEncryption.java new file mode 100644 index 000000000..7269689bc --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/PasswordEncryption.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2007-2012 Argeo GmbH + * + * Licensed 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 org.argeo.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public class PasswordEncryption { + public final static Integer DEFAULT_ITERATION_COUNT = 1024; + /** Stronger with 256, but causes problem with Oracle JVM */ + public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256; + public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128; + public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; + public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES"; + public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding"; + public final static String DEFAULT_CHARSET = "UTF-8"; + + private Integer iterationCount = DEFAULT_ITERATION_COUNT; + private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH; + private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY; + private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION; + private String cipherName = DEFAULT_CIPHER_NAME; + + private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; + private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; + + private Key key; + private Cipher ecipher; + private Cipher dcipher; + + private String securityProviderName = null; + + /** + * This is up to the caller to clear the passed array. Neither copy of nor + * reference to the passed array is kept + */ + public PasswordEncryption(char[] password) { + this(password, DEFAULT_SALT_8, DEFAULT_IV_16); + } + + /** + * This is up to the caller to clear the passed array. Neither copies of nor + * references to the passed arrays are kept + */ + public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) { + try { + initKeyAndCiphers(password, passwordSalt, initializationVector); + } catch (InvalidKeyException e) { + Integer previousSecreteKeyLength = secreteKeyLength; + secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED; + System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength + + " secrete key length instead of " + previousSecreteKeyLength); + try { + initKeyAndCiphers(password, passwordSalt, initializationVector); + } catch (Exception e1) { + throw new UtilsException("Cannot get secret key (with restricted length)", e1); + } + } catch (Exception e) { + throw new UtilsException("Cannot get secret key", e); + } + } + + protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector) + throws GeneralSecurityException { + byte[] salt = new byte[8]; + System.arraycopy(passwordSalt, 0, salt, 0, salt.length); + // for (int i = 0; i < password.length && i < salt.length; i++) + // salt[i] = (byte) password[i]; + byte[] iv = new byte[16]; + System.arraycopy(initializationVector, 0, iv, 0, iv.length); + + SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName()); + PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength()); + String secKeyEncryption = getSecretKeyEncryption(); + if (secKeyEncryption != null) { + SecretKey tmp = keyFac.generateSecret(keySpec); + key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption()); + } else { + key = keyFac.generateSecret(keySpec); + } + if (securityProviderName != null) + ecipher = Cipher.getInstance(getCipherName(), securityProviderName); + else + ecipher = Cipher.getInstance(getCipherName()); + ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); + dcipher = Cipher.getInstance(getCipherName()); + dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + } + + public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException { + try { + CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher); + StreamUtils.copy(decryptedIn, out); + StreamUtils.closeQuietly(out); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new UtilsException("Cannot encrypt", e); + } finally { + StreamUtils.closeQuietly(decryptedIn); + } + } + + public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException { + try { + CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher); + StreamUtils.copy(decryptedIn, decryptedOut); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new UtilsException("Cannot decrypt", e); + } finally { + StreamUtils.closeQuietly(encryptedIn); + } + } + + public byte[] encryptString(String str) { + ByteArrayOutputStream out = null; + ByteArrayInputStream in = null; + try { + out = new ByteArrayOutputStream(); + in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); + encrypt(in, out); + return out.toByteArray(); + } catch (Exception e) { + throw new UtilsException("Cannot encrypt", e); + } finally { + StreamUtils.closeQuietly(out); + } + } + + /** Closes the input stream */ + public String decryptAsString(InputStream in) { + ByteArrayOutputStream out = null; + try { + out = new ByteArrayOutputStream(); + decrypt(in, out); + return new String(out.toByteArray(), DEFAULT_CHARSET); + } catch (Exception e) { + throw new UtilsException("Cannot decrypt", e); + } finally { + StreamUtils.closeQuietly(out); + } + } + + protected Key getKey() { + return key; + } + + protected Cipher getEcipher() { + return ecipher; + } + + protected Cipher getDcipher() { + return dcipher; + } + + protected Integer getIterationCount() { + return iterationCount; + } + + protected Integer getKeyLength() { + return secreteKeyLength; + } + + protected String getSecretKeyFactoryName() { + return secreteKeyFactoryName; + } + + protected String getSecretKeyEncryption() { + return secreteKeyEncryption; + } + + protected String getCipherName() { + return cipherName; + } + + public void setIterationCount(Integer iterationCount) { + this.iterationCount = iterationCount; + } + + public void setSecreteKeyLength(Integer keyLength) { + this.secreteKeyLength = keyLength; + } + + public void setSecreteKeyFactoryName(String secreteKeyFactoryName) { + this.secreteKeyFactoryName = secreteKeyFactoryName; + } + + public void setSecreteKeyEncryption(String secreteKeyEncryption) { + this.secreteKeyEncryption = secreteKeyEncryption; + } + + public void setCipherName(String cipherName) { + this.cipherName = cipherName; + } + + public void setSecurityProviderName(String securityProviderName) { + this.securityProviderName = securityProviderName; + } +}