Clarify naming.
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / security / JcrKeyring.java
index 43eab4b3cb467c69666bb7a5a26a00afd01ebee1..9ba607587e31211f53950c2186faa1c66c59cf57 100644 (file)
@@ -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,25 +31,30 @@ 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.api.NodeConstants;
+import org.argeo.api.NodeUtils;
+import org.argeo.api.security.PBEKeySpecCallback;
 import org.argeo.cms.ArgeoNames;
 import org.argeo.cms.ArgeoTypes;
 import org.argeo.cms.CmsException;
 import org.argeo.jcr.ArgeoJcrException;
 import org.argeo.jcr.JcrUtils;
-import org.argeo.node.NodeUtils;
-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,12 +62,13 @@ 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;
+       // TODO remove thread local session ; open a session each time
        private ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<Session>() {
 
                @Override
@@ -71,19 +80,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<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
-
-               @Override
-               protected Node initialValue() {
-                       return null;
-               }
-       };
+       // private ThreadLocal<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
+       //
+       // @Override
+       // protected Node initialValue() {
+       // return null;
+       // }
+       // };
 
        public JcrKeyring(Repository repository) {
                this.repository = repository;
@@ -100,48 +109,57 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames {
 
        private Session login() {
                try {
-                       return repository.login();
+                       return repository.login(NodeConstants.HOME_WORKSPACE);
                } catch (RepositoryException e) {
                        throw new CmsException("Cannot login key ring session", e);
                }
        }
 
        @Override
-       protected Boolean isSetup() {
+       protected synchronized Boolean isSetup() {
+               Session session = null;
                try {
-                       if (notYetSavedKeyring.get() != null)
-                               return true;
-
-                       Node userHome = NodeUtils.getUserHome(session());
+                       // if (notYetSavedKeyring.get() != null)
+                       // return true;
+                       session = session();
+                       session.refresh(true);
+                       Node userHome = NodeUtils.getUserHome(session);
                        return userHome.hasNode(ARGEO_KEYRING);
                } catch (RepositoryException e) {
                        throw new ArgeoJcrException("Cannot check whether keyring is setup", e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
                }
        }
 
        @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 +167,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 +182,28 @@ 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) {
+               Session session = null;
                try {
-                       Node userHome = NodeUtils.getUserHome(session());
+                       session = session();
+                       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,10 +213,12 @@ 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);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
                }
        }
 
@@ -203,12 +226,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 +243,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 +280,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 +364,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 +393,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) {