X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;f=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Fsecurity%2FJcrKeyring.java;h=c23db194dbca0e13e275cbaf7c060c1fec37ac85;hb=3c7803ca05e2b0276d635e64046d924d3f1884c9;hp=1ccb81731e0770b5101bcfee802b665f5ae561d9;hpb=77a5498dd5d10d2442127022efd6501a7dbddbae;p=lgpl%2Fargeo-commons.git 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 1ccb81731..c23db194d 100644 --- a/org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java +++ b/org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java @@ -1,50 +1,46 @@ -/* - * 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.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.NoSuchAlgorithmException; import java.security.Provider; import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; 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.argeo.jcr.ArgeoJcrException; +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.jcr.JcrException; import org.argeo.jcr.JcrUtils; -import org.argeo.node.ArgeoNames; -import org.argeo.node.ArgeoTypes; -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"; @@ -52,107 +48,152 @@ 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 Session session; + private final Repository repository; + // TODO remove thread local session ; open a session each time + private ThreadLocal sessionThreadLocal = new ThreadLocal() { + @Override + protected Session initialValue() { + return login(); + } + + }; + + // 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() { + // private ThreadLocal notYetSavedKeyring = new ThreadLocal() { + // + // @Override + // protected Node initialValue() { + // return null; + // } + // }; + + public JcrKeyring(Repository repository) { + this.repository = repository; + } - @Override - protected Node initialValue() { - return null; + private Session session() { + Session session = this.sessionThreadLocal.get(); + if (!session.isLive()) { + session = login(); + sessionThreadLocal.set(session); } - }; + return session; + } - @Override - protected Boolean isSetup() { + private Session login() { try { - if (notYetSavedKeyring.get() != null) - return true; + return repository.login(NodeConstants.HOME_WORKSPACE); + } catch (RepositoryException e) { + throw new JcrException("Cannot login key ring session", e); + } + } + @Override + protected synchronized Boolean isSetup() { + Session session = null; + try { + // 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); + throw new JcrException("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 { - Node userHome = NodeUtils.getUserHome(session); - if (userHome.hasNode(ARGEO_KEYRING)) - throw new ArgeoJcrException("Keyring already setup"); - Node keyring = userHome.addNode(ARGEO_KEYRING); + session().refresh(true); + Node userHome = NodeUtils.getUserHome(session()); + Node keyring; + if (userHome.hasNode(ARGEO_KEYRING)) { + throw new IllegalArgumentException("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(); + 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); - keyring.setProperty(ARGEO_SALT, binary); + try (InputStream in = new ByteArrayInputStream(salt);) { + binary = session().getValueFactory().createBinary(in); + keyring.setProperty(ARGEO_SALT, binary); + } catch (IOException e) { + throw new RuntimeException("Cannot set keyring salt", e); + } Integer iterationCount = username.length() * iterationCountFactor; 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, 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); // JcrUtils.closeQuietly(binary); // byte[] btPass = hash(password, salt, iterationCount); // in = new ByteArrayInputStream(btPass); - // binary = session.getValueFactory().createBinary(in); + // binary = session().getValueFactory().createBinary(in); // keyring.setProperty(ARGEO_PASSWORD, binary); - notYetSavedKeyring.set(keyring); - } catch (Exception e) { - throw new ArgeoJcrException("Cannot setup keyring", e); + // notYetSavedKeyring.set(keyring); + } catch (RepositoryException e) { + throw new JcrException("Cannot setup keyring", e); } finally { JcrUtils.closeQuietly(binary); - IOUtils.closeQuietly(in); - // JcrUtils.discardQuietly(session); + // IOUtils.closeQuietly(in); + // JcrUtils.discardQuietly(session()); } } @Override - protected void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) { + protected synchronized void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) { + Session session = null; try { + 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"); + throw new IllegalStateException("Keyring not setup"); pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY).getString(), JcrUtils.getBinaryAsBytes(keyring.getProperty(ARGEO_SALT)), @@ -160,10 +201,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); + throw new JcrException("Cannot handle key spec callback", e); + } finally { + JcrUtils.logoutQuietly(session); } } @@ -171,22 +214,53 @@ 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)) { + if (!session().nodeExists(path)) { String parentPath = JcrUtils.parentPath(path); - if (!session.nodeExists(parentPath)) - throw new ArgeoJcrException("No parent node of " + path); - Node parentNode = session.getNode(parentPath); + if (!session().nodeExists(parentPath)) + throw new IllegalStateException("No parent node of " + path); + Node parentNode = session().getNode(parentPath); node = parentNode.addNode(JcrUtils.nodeNameFromPath(path)); } else { - node = session.getNode(path); + 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 JcrException("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]; @@ -194,61 +268,66 @@ 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(); - } catch (Exception e) { - throw new ArgeoJcrException("Cannot encrypt", e); - } finally { - IOUtils.closeQuietly(unencrypted); - IOUtils.closeQuietly(in); - JcrUtils.closeQuietly(binary); + 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 (RepositoryException e) { + throw new JcrException("Cannot encrypt", e); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Cannot encrypt", e); } } @Override protected synchronized InputStream decrypt(String path) { Binary binary = null; - InputStream encrypted = null; - Reader reader = null; try { - if (!session.nodeExists(path)) { + 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); + Node node = session().getNode(path); + return decrypt(secretKey, cipher, node); } - } catch (Exception e) { - throw new ArgeoJcrException("Cannot decrypt", e); + } catch (RepositoryException e) { + throw new JcrException("Cannot decrypt", e); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Cannot decrypt", e); } finally { - 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); + Node userHome = NodeUtils.getUserHome(session()); if (!userHome.hasNode(ARGEO_KEYRING)) - throw new ArgeoJcrException("Keyring not setup"); + throw new IllegalArgumentException("Keyring not setup"); Node keyring = userHome.getNode(ARGEO_KEYRING); String cipherName = keyring.getProperty(ARGEO_CIPHER).getString(); Provider securityProvider = getSecurityProvider(); @@ -258,33 +337,57 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { else cipher = Cipher.getInstance(cipherName, securityProvider); return cipher; - } catch (Exception e) { - throw new ArgeoJcrException("Cannot get cipher", e); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("Cannot get cipher", e); + } catch (RepositoryException e) { + throw new JcrException("Cannot get cipher", e); + } finally { + } } 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 (GeneralSecurityException e) { + throw new RuntimeException("Cannot change JCR keyring password", e); + } catch (RepositoryException e) { + throw new JcrException("Cannot change JCR keyring password", e); + } finally { + JcrUtils.logoutQuietly(session); + } } - public synchronized void setSession(Session session) { - this.session = session; - } + // public synchronized void setSession(Session session) { + // this.session = session; + // } public void setIterationCountFactor(Integer iterationCountFactor) { 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) {