X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;f=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Fsecurity%2FJcrKeyring.java;h=c23db194dbca0e13e275cbaf7c060c1fec37ac85;hb=d169026cb1939009fd90ac46a11f480cb3d803c0;hp=43eab4b3cb467c69666bb7a5a26a00afd01ebee1;hpb=6decc943ce5bca1b57ef407b7e9c6bb6ad6f3c97;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 43eab4b3c..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,52 +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.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.JcrException; 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 +48,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 sessionThreadLocal = new ThreadLocal() { @Override @@ -71,19 +66,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; @@ -100,61 +95,72 @@ 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); + throw new JcrException("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); + 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 { + 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 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(); 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); @@ -164,27 +170,30 @@ public class JcrKeyring extends AbstractKeyring implements ArgeoNames { // 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); + // 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"); + throw new IllegalStateException("Keyring not setup"); pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY).getString(), JcrUtils.getBinaryAsBytes(keyring.getProperty(ARGEO_SALT)), @@ -192,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); } } @@ -203,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)) { String parentPath = JcrUtils.parentPath(path); if (!session().nodeExists(parentPath)) - throw new ArgeoJcrException("No parent node of " + path); + throw new IllegalStateException("No parent node of " + path); Node parentNode = session().getNode(parentPath); node = parentNode.addNode(JcrUtils.nodeNameFromPath(path)); } 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 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]; @@ -226,63 +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); - JcrUtils.logoutQuietly(session()); + 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 { + 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); + } 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()); 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(); @@ -292,13 +337,37 @@ 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) { @@ -309,16 +378,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) {