entry : propertyMap.entrySet())
+ {
+ String name = entry.getKey();
+ String value = entry.getValue();
+ // Some values can be ignored
+ if (value == null || value.length() == 0)
+ {
+ continue;
+ }
+ if (value.startsWith("${") && value.endsWith("}"))
+ {
+ continue;
+ }
+ // Check the system properties
+ if (System.getProperty(name) != null)
+ {
+ // It was already there
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("\n" +
+ "Not pushing up system property: \n" +
+ " Property: " + name + "\n" +
+ " Value already present: " + System.getProperty(name) + "\n" +
+ " Value provided: " + value);
+ }
+ continue;
+ }
+ System.setProperty(name, value);
+ }
+ }
+}
diff --git a/core/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java b/core/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java
new file mode 100644
index 0000000000..cceb32c944
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encoding;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * @since 2.1
+ * @author Derek Hulley
+ */
+public abstract class AbstractCharactersetFinder implements CharactersetFinder
+{
+ private static Log logger = LogFactory.getLog(AbstractCharactersetFinder.class);
+ private static boolean isDebugEnabled = logger.isDebugEnabled();
+
+ private int bufferSize;
+
+ public AbstractCharactersetFinder()
+ {
+ this.bufferSize = 8192;
+ }
+
+ /**
+ * Set the maximum number of bytes to read ahead when attempting to determine the characterset.
+ * Most characterset detectors are efficient and can process 8K of buffered data very quickly.
+ * Some, may need to be constrained a bit.
+ *
+ * @param bufferSize the number of bytes - default 8K.
+ */
+ public void setBufferSize(int bufferSize)
+ {
+ this.bufferSize = bufferSize;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * The input stream is checked to ensure that it supports marks, after which
+ * a buffer is extracted, leaving the stream in its original state.
+ */
+ public final Charset detectCharset(InputStream is)
+ {
+ // Only support marking streams
+ if (!is.markSupported())
+ {
+ throw new IllegalArgumentException("The InputStream must support marks. Wrap the stream in a BufferedInputStream.");
+ }
+ try
+ {
+ int bufferSize = getBufferSize();
+ if (bufferSize < 0)
+ {
+ throw new RuntimeException("The required buffer size may not be negative: " + bufferSize);
+ }
+ // Mark the stream for just a few more than we actually will need
+ is.mark(bufferSize);
+ // Create a buffer to hold the data
+ byte[] buffer = new byte[bufferSize];
+ // Fill it
+ int read = is.read(buffer);
+ // Create an appropriately sized buffer
+ if (read > -1 && read < buffer.length)
+ {
+ byte[] copyBuffer = new byte[read];
+ System.arraycopy(buffer, 0, copyBuffer, 0, read);
+ buffer = copyBuffer;
+ }
+ // Detect
+ return detectCharset(buffer);
+ }
+ catch (IOException e)
+ {
+ // Attempt a reset
+ throw new AlfrescoRuntimeException("IOException while attempting to detect charset encoding.", e);
+ }
+ finally
+ {
+ try { is.reset(); } catch (Throwable ee) {}
+ }
+ }
+
+ public final Charset detectCharset(byte[] buffer)
+ {
+ try
+ {
+ Charset charset = detectCharsetImpl(buffer);
+ // Done
+ if (isDebugEnabled)
+ {
+ if (charset == null)
+ {
+ // Read a few characters for debug purposes
+ logger.debug("\n" +
+ "Failed to identify stream character set: \n" +
+ " Guessed 'chars': " + Arrays.toString(buffer));
+ }
+ else
+ {
+ // Read a few characters for debug purposes
+ logger.debug("\n" +
+ "Identified character set from stream:\n" +
+ " Charset: " + charset + "\n" +
+ " Detected chars: " + new String(buffer, charset.name()));
+ }
+ }
+ return charset;
+ }
+ catch (Throwable e)
+ {
+ logger.error("IOException while attempting to detect charset encoding.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Some implementations may only require a few bytes to do detect the stream type,
+ * whilst others may be more efficient with larger buffers. In either case, the
+ * number of bytes actually present in the buffer cannot be enforced.
+ *
+ * Only override this method if there is a very compelling reason to adjust the buffer
+ * size, and then consider handling the {@link #setBufferSize(int)} method by issuing a
+ * warning. This will prevent users from setting the buffer size when it has no effect.
+ *
+ * @return Returns the maximum desired size of the buffer passed
+ * to the {@link CharactersetFinder#detectCharset(byte[])} method.
+ *
+ * @see #setBufferSize(int)
+ */
+ protected int getBufferSize()
+ {
+ return bufferSize;
+ }
+
+ /**
+ * Worker method for implementations to override. All exceptions will be reported and
+ * absorbed and null returned.
+ *
+ * The interface contract is that the data buffer must not be altered in any way.
+ *
+ * @param buffer the buffer of data no bigger than the requested
+ * {@linkplain #getBufferSize() best buffer size}. This can,
+ * very efficiently, be turned into an InputStream using a
+ * ByteArrayInputStream.
+ * @return Returns the charset or null if an accurate conclusion
+ * is not possible
+ * @throws Exception Any exception, checked or not
+ */
+ protected abstract Charset detectCharsetImpl(byte[] buffer) throws Exception;
+}
diff --git a/core/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java b/core/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java
new file mode 100644
index 0000000000..d4908c0320
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encoding;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Byte Order Marker encoding detection.
+ *
+ * @since 2.1
+ * @author Pacific Northwest National Lab
+ * @author Derek Hulley
+ */
+public class BomCharactersetFinder extends AbstractCharactersetFinder
+{
+ private static Log logger = LogFactory.getLog(BomCharactersetFinder.class);
+
+ @Override
+ public void setBufferSize(int bufferSize)
+ {
+ logger.warn("Setting the buffersize has no effect for charset finder: " + BomCharactersetFinder.class.getName());
+ }
+
+ /**
+ * @return Returns 64
+ */
+ @Override
+ protected int getBufferSize()
+ {
+ return 64;
+ }
+
+ /**
+ * Just searches the Byte Order Marker, i.e. the first three characters for a sign of
+ * the encoding.
+ */
+ protected Charset detectCharsetImpl(byte[] buffer) throws Exception
+ {
+ Charset charset = null;
+ ByteArrayInputStream bis = null;
+ try
+ {
+ bis = new ByteArrayInputStream(buffer);
+ bis.mark(3);
+ char[] byteHeader = new char[3];
+ InputStreamReader in = new InputStreamReader(bis);
+ int bytesRead = in.read(byteHeader);
+ bis.reset();
+
+ if (bytesRead < 2)
+ {
+ // ASCII
+ charset = Charset.forName("Cp1252");
+ }
+ else if (
+ byteHeader[0] == 0xFE &&
+ byteHeader[1] == 0xFF)
+ {
+ // UCS-2 Big Endian
+ charset = Charset.forName("UTF-16BE");
+ }
+ else if (
+ byteHeader[0] == 0xFF &&
+ byteHeader[1] == 0xFE)
+ {
+ // UCS-2 Little Endian
+ charset = Charset.forName("UTF-16LE");
+ }
+ else if (
+ bytesRead >= 3 &&
+ byteHeader[0] == 0xEF &&
+ byteHeader[1] == 0xBB &&
+ byteHeader[2] == 0xBF)
+ {
+ // UTF-8
+ charset = Charset.forName("UTF-8");
+ }
+ else
+ {
+ // No idea
+ charset = null;
+ }
+ // Done
+ return charset;
+ }
+ finally
+ {
+ if (bis != null)
+ {
+ try { bis.close(); } catch (Throwable e) {}
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/org/alfresco/encoding/CharactersetFinder.java b/core/src/main/java/org/alfresco/encoding/CharactersetFinder.java
new file mode 100644
index 0000000000..365b55bcf0
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encoding/CharactersetFinder.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encoding;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+/**
+ * Interface for classes that are able to read a text-based input stream and determine
+ * the character encoding.
+ *
+ * There are quite a few libraries that do this, but none are perfect. It is therefore
+ * necessary to abstract the implementation to allow these finders to be configured in
+ * as required.
+ *
+ * Implementations should have a default constructor and be completely thread safe and
+ * stateless. This will allow them to be constructed and held indefinitely to do the
+ * decoding work.
+ *
+ * Where the encoding cannot be determined, it is left to the client to decide what to do.
+ * Some implementations may guess and encoding or use a default guess - it is up to the
+ * implementation to specify the behaviour.
+ *
+ * @since 2.1
+ * @author Derek Hulley
+ */
+public interface CharactersetFinder
+{
+ /**
+ * Attempt to detect the character set encoding for the give input stream. The input
+ * stream will not be altered or closed by this method, and must therefore support
+ * marking. If the input stream available doesn't support marking, then it can be wrapped with
+ * a {@link BufferedInputStream}.
+ *
+ * The current state of the stream will be restored before the method returns.
+ *
+ * @param is an input stream that must support marking
+ * @return Returns the encoding of the stream,
+ * or null if encoding cannot be identified
+ */
+ public Charset detectCharset(InputStream is);
+
+ /**
+ * Attempt to detect the character set encoding for the given buffer.
+ *
+ * @param buffer the first n bytes of the character stream
+ * @return Returns the encoding of the buffer,
+ * or null if encoding cannot be identified
+ */
+ public Charset detectCharset(byte[] buffer);
+}
diff --git a/core/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java b/core/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java
new file mode 100644
index 0000000000..92cd35f1c6
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encoding;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+
+import com.glaforge.i18n.io.CharsetToolkit;
+
+/**
+ * Uses the Guess Encoding
+ * library.
+ *
+ * @since 2.1
+ * @author Derek Hulley
+ */
+public class GuessEncodingCharsetFinder extends AbstractCharactersetFinder
+{
+ /** Dummy charset to detect the default guess */
+ private static final Charset DUMMY_CHARSET = new DummyCharset();
+
+ @Override
+ protected Charset detectCharsetImpl(byte[] buffer) throws Exception
+ {
+ CharsetToolkit charsetToolkit = new CharsetToolkit(buffer, DUMMY_CHARSET);
+ charsetToolkit.setEnforce8Bit(true); // Force the default instead of a guess
+ Charset charset = charsetToolkit.guessEncoding();
+ if (charset == DUMMY_CHARSET)
+ {
+ return null;
+ }
+ else
+ {
+ return charset;
+ }
+ }
+
+ /**
+ * A dummy charset to detect a default hit.
+ */
+ public static class DummyCharset extends Charset
+ {
+ DummyCharset()
+ {
+ super("dummy", new String[] {});
+ }
+
+ @Override
+ public boolean contains(Charset cs)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CharsetDecoder newDecoder()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CharsetEncoder newEncoder()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+}
diff --git a/core/src/main/java/org/alfresco/encryption/AbstractEncryptor.java b/core/src/main/java/org/alfresco/encryption/AbstractEncryptor.java
new file mode 100644
index 0000000000..865df534a4
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encryption/AbstractEncryptor.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.security.AlgorithmParameters;
+import java.security.InvalidKeyException;
+import java.security.Key;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.SealedObject;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.Pair;
+import org.alfresco.util.PropertyCheck;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Basic support for encryption engines.
+ *
+ * @since 4.0
+ */
+public abstract class AbstractEncryptor implements Encryptor
+{
+ protected static final Log logger = LogFactory.getLog(Encryptor.class);
+ protected String cipherAlgorithm;
+ protected String cipherProvider;
+
+ protected KeyProvider keyProvider;
+
+ /**
+ * Constructs with defaults
+ */
+ protected AbstractEncryptor()
+ {
+ }
+
+ /**
+ * @param keyProvider provides encryption keys based on aliases
+ */
+ public void setKeyProvider(KeyProvider keyProvider)
+ {
+ this.keyProvider = keyProvider;
+ }
+
+ public KeyProvider getKeyProvider()
+ {
+ return keyProvider;
+ }
+
+ public void init()
+ {
+ PropertyCheck.mandatory(this, "keyProvider", keyProvider);
+ }
+
+ /**
+ * Factory method to be written by implementations to construct and initialize
+ * physical ciphering objects.
+ *
+ * @param keyAlias the key alias
+ * @param params algorithm-specific parameters
+ * @param mode the cipher mode
+ * @return Cipher
+ */
+ protected abstract Cipher getCipher(String keyAlias, AlgorithmParameters params, int mode);
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pair encrypt(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ Cipher cipher = getCipher(keyAlias, params, Cipher.ENCRYPT_MODE);
+ if (cipher == null)
+ {
+ return new Pair(input, null);
+ }
+ try
+ {
+ byte[] output = cipher.doFinal(input);
+ params = cipher.getParameters();
+ return new Pair(output, params);
+ }
+ catch (Throwable e)
+ {
+// cipher.init(Cipher.ENCRYPT_MODE, key, params);
+ throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e);
+ }
+ }
+
+ protected void resetCipher()
+ {
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] decrypt(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ Cipher cipher = getCipher(keyAlias, params, Cipher.DECRYPT_MODE);
+ if (cipher == null)
+ {
+ return input;
+ }
+ try
+ {
+ return cipher.doFinal(input);
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStream decrypt(String keyAlias, AlgorithmParameters params, InputStream input)
+ {
+ Cipher cipher = getCipher(keyAlias, params, Cipher.DECRYPT_MODE);
+ if (cipher == null)
+ {
+ return input;
+ }
+
+ try
+ {
+ return new CipherInputStream(input, cipher);
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Serializes and {@link #encrypt(String, AlgorithmParameters, byte[]) encrypts} the input data.
+ */
+ @Override
+ public Pair encryptObject(String keyAlias, AlgorithmParameters params, Object input)
+ {
+ try
+ {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+ oos.writeObject(input);
+ byte[] unencrypted = bos.toByteArray();
+ return encrypt(keyAlias, params, unencrypted);
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to serialize or encrypt object", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * {@link #decrypt(String, AlgorithmParameters, byte[]) Decrypts} and deserializes the input data
+ */
+ @Override
+ public Object decryptObject(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ try
+ {
+ byte[] unencrypted = decrypt(keyAlias, params, input);
+ ByteArrayInputStream bis = new ByteArrayInputStream(unencrypted);
+ ObjectInputStream ois = new ObjectInputStream(bis);
+ Object obj = ois.readObject();
+ return obj;
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to deserialize or decrypt object", e);
+ }
+ }
+
+ @Override
+ public Serializable sealObject(String keyAlias, AlgorithmParameters params, Serializable input)
+ {
+ if (input == null)
+ {
+ return null;
+ }
+ Cipher cipher = getCipher(keyAlias, params, Cipher.ENCRYPT_MODE);
+ if (cipher == null)
+ {
+ return input;
+ }
+ try
+ {
+ return new SealedObject(input, cipher);
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to seal object", e);
+ }
+ }
+
+ @Override
+ public Serializable unsealObject(String keyAlias, Serializable input) throws InvalidKeyException
+ {
+ if (input == null)
+ {
+ return input;
+ }
+ // Don't unseal it if it is not sealed
+ if (!(input instanceof SealedObject))
+ {
+ return input;
+ }
+ // Get the Key, rather than a Cipher
+ Key key = keyProvider.getKey(keyAlias);
+ if (key == null)
+ {
+ // The client will be expecting to unseal the object
+ throw new IllegalStateException("No key matching " + keyAlias + ". Cannot unseal object.");
+ }
+ // Unseal it using the key
+ SealedObject sealedInput = (SealedObject) input;
+ try
+ {
+ Serializable output = (Serializable) sealedInput.getObject(key);
+ // Done
+ return output;
+ }
+ catch(InvalidKeyException e)
+ {
+ // let these through, can be useful to client code to know this is the cause
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to unseal object", e);
+ }
+ }
+
+ public void setCipherAlgorithm(String cipherAlgorithm)
+ {
+ this.cipherAlgorithm = cipherAlgorithm;
+ }
+
+ public String getCipherAlgorithm()
+ {
+ return this.cipherAlgorithm;
+ }
+
+ public void setCipherProvider(String cipherProvider)
+ {
+ this.cipherProvider = cipherProvider;
+ }
+
+ public String getCipherProvider()
+ {
+ return this.cipherProvider;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AlgorithmParameters decodeAlgorithmParameters(byte[] encoded)
+ {
+ try
+ {
+ AlgorithmParameters p = null;
+ String algorithm = "DESede";
+ if(getCipherProvider() != null)
+ {
+ p = AlgorithmParameters.getInstance(algorithm, getCipherProvider());
+ }
+ else
+ {
+ p = AlgorithmParameters.getInstance(algorithm);
+ }
+ p.init(encoded);
+ return p;
+ }
+ catch(Exception e)
+ {
+ throw new AlfrescoRuntimeException("", e);
+ }
+ }
+}
diff --git a/core/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java b/core/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java
new file mode 100644
index 0000000000..297f28c355
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+/**
+ * Basic support for key providers
+ *
+ * TODO: This class will provide the alias name mapping so that use-cases can be mapped
+ * to different alias names in the keystore.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public abstract class AbstractKeyProvider implements KeyProvider
+{
+ /*
+ * Not a useless class.
+ */
+}
diff --git a/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java b/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java
new file mode 100644
index 0000000000..a017ce984a
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.Key;
+import java.util.Set;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.TrustManager;
+
+/**
+ * Manages a Java Keystore for Alfresco, including caching keys where appropriate.
+ *
+ * @since 4.0
+ *
+ */
+public interface AlfrescoKeyStore
+{
+ public static final String KEY_KEYSTORE_PASSWORD = "keystore.password";
+
+ /**
+ * The name of the keystore.
+ *
+ * @return the name of the keystore.
+ */
+ public String getName();
+
+ /**
+ * Backup the keystore to the backup location. Write the keys to the backup keystore.
+ */
+ public void backup();
+
+ /**
+ * The key store parameters.
+ *
+ * @return KeyStoreParameters
+ */
+ public KeyStoreParameters getKeyStoreParameters();
+
+ /**
+ * The backup key store parameters.
+ *
+ * @return * @return
+
+ */
+ public KeyStoreParameters getBackupKeyStoreParameters();
+
+ /**
+ * Does the underlying key store exist?
+ *
+ * @return true if it exists, false otherwise
+ */
+ public boolean exists();
+
+ /**
+ * Return the key with the given key alias.
+ *
+ * @param keyAlias String
+ * @return Key
+ */
+ public Key getKey(String keyAlias);
+
+ /**
+ * Return the timestamp (in ms) of when the key was last loaded from the keystore on disk.
+ *
+ * @param keyAlias String
+ * @return long
+ */
+ public long getKeyTimestamp(String keyAlias);
+
+ /**
+ * Return the backup key with the given key alias.
+ *
+ * @param keyAlias String
+ * @return Key
+ */
+ public Key getBackupKey(String keyAlias);
+
+ /**
+ * Return all key aliases in the key store.
+ *
+ * @return Set
+ */
+ public Set getKeyAliases();
+
+ /**
+ * Create an array of key managers from keys in the key store.
+ *
+ * @return KeyManager[]
+ */
+ public KeyManager[] createKeyManagers();
+
+ /**
+ * Create an array of trust managers from certificates in the key store.
+ *
+ * @return TrustManager[]
+ */
+ public TrustManager[] createTrustManagers();
+
+ /**
+ * Create the key store if it doesn't exist.
+ * A key for each key alias will be written to the keystore on disk, either from the cached keys or, if not present, a key will be generated.
+ */
+ public void create();
+
+ /**
+ * Reload the keys from the key store.
+ */
+ public void reload() throws InvalidKeystoreException, MissingKeyException;
+
+ /**
+ * Check that the keys in the key store are valid i.e. that they match those registered.
+ */
+ public void validateKeys() throws InvalidKeystoreException, MissingKeyException;
+
+}
diff --git a/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java b/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java
new file mode 100644
index 0000000000..085bec231a
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java
@@ -0,0 +1,1100 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.DESedeKeySpec;
+import javax.management.openmbean.KeyAlreadyExistsException;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+import org.alfresco.encryption.EncryptionKeysRegistry.KEY_STATUS;
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.PropertyCheck;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * This wraps a Java Keystore and caches the encryption keys. It manages the loading and caching of the encryption keys
+ * and their registration with and validation against the encryption key registry.
+ *
+ * @since 4.0
+ *
+ */
+public class AlfrescoKeyStoreImpl implements AlfrescoKeyStore
+{
+ private static final Log logger = LogFactory.getLog(AlfrescoKeyStoreImpl.class);
+
+ protected KeyStoreParameters keyStoreParameters;
+ protected KeyStoreParameters backupKeyStoreParameters;
+ protected KeyResourceLoader keyResourceLoader;
+ protected EncryptionKeysRegistry encryptionKeysRegistry;
+
+ protected KeyMap keys;
+ protected KeyMap backupKeys;
+ protected final WriteLock writeLock;
+ protected final ReadLock readLock;
+
+ private Set keysToValidate;
+ protected boolean validateKeyChanges = false;
+
+ public AlfrescoKeyStoreImpl()
+ {
+ ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+ writeLock = lock.writeLock();
+ readLock = lock.readLock();
+ this.keys = new KeyMap();
+ this.backupKeys = new KeyMap();
+ }
+
+ public AlfrescoKeyStoreImpl(KeyStoreParameters keyStoreParameters, KeyResourceLoader keyResourceLoader)
+ {
+ this();
+
+ this.keyResourceLoader = keyResourceLoader;
+ this.keyStoreParameters = keyStoreParameters;
+
+ safeInit();
+ }
+
+ public void init()
+ {
+ writeLock.lock();
+ try
+ {
+ safeInit();
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ public void setEncryptionKeysRegistry(
+ EncryptionKeysRegistry encryptionKeysRegistry)
+ {
+ this.encryptionKeysRegistry = encryptionKeysRegistry;
+ }
+
+ public void setValidateKeyChanges(boolean validateKeyChanges)
+ {
+ this.validateKeyChanges = validateKeyChanges;
+ }
+
+ public void setKeysToValidate(Set keysToValidate)
+ {
+ this.keysToValidate = keysToValidate;
+ }
+
+ public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters)
+ {
+ this.keyStoreParameters = keyStoreParameters;
+ }
+
+ public void setBackupKeyStoreParameters(
+ KeyStoreParameters backupKeyStoreParameters)
+ {
+ this.backupKeyStoreParameters = backupKeyStoreParameters;
+ }
+
+ public void setKeyResourceLoader(KeyResourceLoader keyResourceLoader)
+ {
+ this.keyResourceLoader = keyResourceLoader;
+ }
+
+ public KeyStoreParameters getKeyStoreParameters()
+ {
+ return keyStoreParameters;
+ }
+
+ public KeyStoreParameters getBackupKeyStoreParameters()
+ {
+ return backupKeyStoreParameters;
+ }
+
+ public KeyResourceLoader getKeyResourceLoader()
+ {
+ return keyResourceLoader;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getName()
+ {
+ return keyStoreParameters.getName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void validateKeys() throws InvalidKeystoreException, MissingKeyException
+ {
+ validateKeys(keys, backupKeys);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean exists()
+ {
+ return keyStoreExists(getKeyStoreParameters().getLocation());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void reload() throws InvalidKeystoreException, MissingKeyException
+ {
+ KeyMap keys = loadKeyStore(getKeyStoreParameters());
+ KeyMap backupKeys = loadKeyStore(getBackupKeyStoreParameters());
+
+ validateKeys(keys, backupKeys);
+
+ // all ok, reload the keys
+ writeLock.lock();
+ try
+ {
+ this.keys = keys;
+ this.backupKeys = backupKeys;
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Set getKeyAliases()
+ {
+ return new HashSet(keys.getKeyAliases());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void backup()
+ {
+ writeLock.lock();
+ try
+ {
+ for(String keyAlias : keys.getKeyAliases())
+ {
+ backupKeys.setKey(keyAlias, keys.getKey(keyAlias));
+ }
+ createKeyStore(backupKeyStoreParameters, backupKeys);
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void create()
+ {
+ createKeyStore(keyStoreParameters, keys);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Key getKey(String keyAlias)
+ {
+ readLock.lock();
+ try
+ {
+ return keys.getCachedKey(keyAlias).getKey();
+ }
+ finally
+ {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getKeyTimestamp(String keyAlias)
+ {
+ readLock.lock();
+ try
+ {
+ CachedKey cachedKey = keys.getCachedKey(keyAlias);
+ return cachedKey.getTimestamp();
+ }
+ finally
+ {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Key getBackupKey(String keyAlias)
+ {
+ readLock.lock();
+ try
+ {
+ return backupKeys.getCachedKey(keyAlias).getKey();
+ }
+ finally
+ {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public KeyManager[] createKeyManagers()
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ try
+ {
+ keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation());
+ KeyStore ks = loadKeyStore(keyStoreParameters, keyInfoManager);
+
+ logger.debug("Initializing key managers");
+ KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+ String keyStorePassword = keyInfoManager.getKeyStorePassword();
+ kmfactory.init(ks, keyStorePassword != null ? keyStorePassword.toCharArray(): null);
+ return kmfactory.getKeyManagers();
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to create key manager", e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public TrustManager[] createTrustManagers()
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ try
+ {
+ keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation());
+ KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager);
+
+ logger.debug("Initializing trust managers");
+ TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmfactory.init(ks);
+ return tmfactory.getTrustManagers();
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to create key manager", e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+ }
+ }
+
+ protected String getKeyMetaDataFileLocation()
+ {
+ return keyStoreParameters.getKeyMetaDataFileLocation();
+ }
+
+ protected InputStream getKeyStoreStream(String location) throws FileNotFoundException
+ {
+ if(location == null)
+ {
+ return null;
+ }
+ return keyResourceLoader.getKeyStore(location);
+ }
+
+ protected OutputStream getKeyStoreOutStream() throws FileNotFoundException
+ {
+ return new FileOutputStream(getKeyStoreParameters().getLocation());
+ }
+
+ protected KeyInfoManager getKeyInfoManager(String metadataFileLocation) throws FileNotFoundException, IOException
+ {
+ return new KeyInfoManager(metadataFileLocation, keyResourceLoader);
+ }
+
+ protected KeyMap cacheKeys(KeyStore ks, KeyInfoManager keyInfoManager)
+ throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException
+ {
+ KeyMap keys = new KeyMap();
+
+ // load and cache the keys
+ for(Entry keyEntry : keyInfoManager.getKeyInfo().entrySet())
+ {
+ String keyAlias = keyEntry.getKey();
+
+ KeyInformation keyInfo = keyInfoManager.getKeyInformation(keyAlias);
+ String passwordStr = keyInfo != null ? keyInfo.getPassword() : null;
+
+ // Null is an acceptable value (means no key)
+ Key key = null;
+
+ // Attempt to get the key
+ key = ks.getKey(keyAlias, passwordStr == null ? null : passwordStr.toCharArray());
+ if(key != null)
+ {
+ keys.setKey(keyAlias, key);
+ }
+ // Key loaded
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Retrieved key from keystore: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType() + "\n" +
+ " Alias: " + keyAlias + "\n" +
+ " Password?: " + (passwordStr != null));
+
+ Certificate[] certs = ks.getCertificateChain(keyAlias);
+ if(certs != null)
+ {
+ logger.debug("Certificate chain '" + keyAlias + "':");
+ for(int c = 0; c < certs.length; c++)
+ {
+ if(certs[c] instanceof X509Certificate)
+ {
+ X509Certificate cert = (X509Certificate)certs[c];
+ logger.debug(" Certificate " + (c + 1) + ":");
+ logger.debug(" Subject DN: " + cert.getSubjectDN());
+ logger.debug(" Signature Algorithm: " + cert.getSigAlgName());
+ logger.debug(" Valid from: " + cert.getNotBefore() );
+ logger.debug(" Valid until: " + cert.getNotAfter());
+ logger.debug(" Issuer: " + cert.getIssuerDN());
+ }
+ }
+ }
+ }
+ }
+
+ return keys;
+ }
+
+ protected KeyStore initialiseKeyStore(String type, String provider)
+ {
+ KeyStore ks = null;
+
+ try
+ {
+ if(provider == null || provider.equals(""))
+ {
+ ks = KeyStore.getInstance(type);
+ }
+ else
+ {
+ ks = KeyStore.getInstance(type, provider);
+ }
+
+ ks.load(null, null);
+
+ return ks;
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to intialise key store", e);
+ }
+ }
+
+ protected KeyStore loadKeyStore(KeyStoreParameters keyStoreParameters, KeyInfoManager keyInfoManager)
+ {
+ String pwdKeyStore = null;
+
+ try
+ {
+ KeyStore ks = initialiseKeyStore(keyStoreParameters.getType(), keyStoreParameters.getProvider());
+
+ // Load it up
+ InputStream is = getKeyStoreStream(keyStoreParameters.getLocation());
+ if (is != null)
+ {
+ try
+ {
+ // Get the keystore password
+ pwdKeyStore = keyInfoManager.getKeyStorePassword();
+ ks.load(is, pwdKeyStore == null ? null : pwdKeyStore.toCharArray());
+ }
+ finally
+ {
+ try {is.close(); } catch (Throwable e) {}
+ }
+ }
+ else
+ {
+ // this is ok, the keystore will contain no keys.
+ logger.warn("Keystore file doesn't exist: " + keyStoreParameters.getLocation());
+ }
+
+ return ks;
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to load key store: " + keyStoreParameters.getLocation(), e);
+ }
+ finally
+ {
+ pwdKeyStore = null;
+ }
+ }
+
+ /**
+ * Initializes class
+ */
+ private void safeInit()
+ {
+ PropertyCheck.mandatory(this, "location", getKeyStoreParameters().getLocation());
+
+ // Make sure we choose the default type, if required
+ if(getKeyStoreParameters().getType() == null)
+ {
+ keyStoreParameters.setType(KeyStore.getDefaultType());
+ }
+
+ writeLock.lock();
+ try
+ {
+ keys = loadKeyStore(keyStoreParameters);
+ backupKeys = loadKeyStore(backupKeyStoreParameters);
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ private KeyMap loadKeyStore(KeyStoreParameters keyStoreParameters)
+ {
+ InputStream is = null;
+ KeyInfoManager keyInfoManager = null;
+ KeyStore ks = null;
+
+ if(keyStoreParameters == null)
+ {
+ // empty key map
+ return new KeyMap();
+ }
+
+ try
+ {
+ keyInfoManager = getKeyInfoManager(keyStoreParameters.getKeyMetaDataFileLocation());
+ ks = loadKeyStore(keyStoreParameters, keyInfoManager);
+ // Loaded
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to initialize keystore: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType(),
+ e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clearKeyStorePassword();
+ }
+
+ if (is != null)
+ {
+ try
+ {
+ is.close();
+ }
+ catch (Throwable e)
+ {
+
+ }
+ }
+ }
+
+ try
+ {
+ // cache the keys from the keystore
+ KeyMap keys = cacheKeys(ks, keyInfoManager);
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Initialized keystore: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType() + "\n" +
+ keys.numKeys() + " keys found");
+ }
+
+ return keys;
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to retrieve keys from keystore: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType() + "\n",
+ e);
+ }
+ finally
+ {
+ // Clear key information
+ keyInfoManager.clear();
+ }
+ }
+
+ protected void createKey(String keyAlias)
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ try
+ {
+ keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation());
+ Key key = getSecretKey(keyInfoManager.getKeyInformation(keyAlias));
+ encryptionKeysRegistry.registerKey(keyAlias, key);
+ keys.setKey(keyAlias, key);
+
+ KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager);
+ ks.setKeyEntry(keyAlias, key, keyInfoManager.getKeyInformation(keyAlias).getPassword().toCharArray(), null);
+ OutputStream keyStoreOutStream = getKeyStoreOutStream();
+ ks.store(keyStoreOutStream, keyInfoManager.getKeyStorePassword().toCharArray());
+ // Workaround for MNT-15005
+ keyStoreOutStream.close();
+
+ logger.info("Created key: " + keyAlias + "\n in key store: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType());
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to create key: " + keyAlias + "\n in key store: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType(),
+ e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+ }
+ }
+
+ protected void createKeyStore(KeyStoreParameters keyStoreParameters, KeyMap keys)
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ try
+ {
+ if(!keyStoreExists(keyStoreParameters.getLocation()))
+ {
+ keyInfoManager = getKeyInfoManager(keyStoreParameters.getKeyMetaDataFileLocation());
+ KeyStore ks = initialiseKeyStore(keyStoreParameters.getType(), keyStoreParameters.getProvider());
+
+ String keyStorePassword = keyInfoManager.getKeyStorePassword();
+ if(keyStorePassword == null)
+ {
+ throw new AlfrescoRuntimeException("Key store password is null for keystore at location "
+ + getKeyStoreParameters().getLocation()
+ + ", key store meta data location" + getKeyMetaDataFileLocation());
+ }
+
+ for(String keyAlias : keys.getKeyAliases())
+ {
+ KeyInformation keyInfo = keyInfoManager.getKeyInformation(keyAlias);
+
+ Key key = keys.getKey(keyAlias);
+ if(key == null)
+ {
+ logger.warn("Key with alias " + keyAlias + " is null when creating keystore at location " + keyStoreParameters.getLocation());
+ }
+ else
+ {
+ ks.setKeyEntry(keyAlias, key, keyInfo.getPassword().toCharArray(), null);
+ }
+ }
+
+// try
+// {
+// throw new Exception("Keystore creation: " + );
+// }
+// catch(Throwable e)
+// {
+// logger.debug(e.getMessage());
+// e.printStackTrace();
+// }
+
+ OutputStream keyStoreOutStream = getKeyStoreOutStream();
+ ks.store(keyStoreOutStream, keyStorePassword.toCharArray());
+ // Workaround for MNT-15005
+ keyStoreOutStream.close();
+ }
+ else
+ {
+ logger.warn("Can't create key store " + keyStoreParameters.getLocation() + ", already exists.");
+ }
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to create keystore: \n" +
+ " Location: " + keyStoreParameters.getLocation() + "\n" +
+ " Provider: " + keyStoreParameters.getProvider() + "\n" +
+ " Type: " + keyStoreParameters.getType(),
+ e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+ }
+ }
+
+ /*
+ * For testing
+ */
+// void createBackup()
+// {
+// createKeyStore(backupKeyStoreParameters, backupKeys);
+// }
+
+ private byte[] generateKeyData()
+ {
+ try
+ {
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+ byte bytes[] = new byte[DESedeKeySpec.DES_EDE_KEY_LEN];
+ random.nextBytes(bytes);
+ return bytes;
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException("Unable to generate secret key", e);
+ }
+ }
+
+ protected Key getSecretKey(KeyInformation keyInformation) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException
+ {
+ byte[] keyData = keyInformation.getKeyData();
+
+ if(keyData == null)
+ {
+ if(keyInformation.getKeyAlgorithm().equals("DESede"))
+ {
+ // no key data provided, generate key data automatically
+ keyData = generateKeyData();
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException("Unable to generate secret key: key algorithm is not DESede and no keyData provided");
+ }
+ }
+
+ DESedeKeySpec keySpec = new DESedeKeySpec(keyData);
+ SecretKeyFactory kf = SecretKeyFactory.getInstance(keyInformation.getKeyAlgorithm());
+ SecretKey secretKey = kf.generateSecret(keySpec);
+ return secretKey;
+ }
+
+ void importPrivateKey(String keyAlias, String keyPassword, InputStream fl, InputStream certstream)
+ throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, CertificateException, KeyStoreException
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ writeLock.lock();
+ try
+ {
+ keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation());
+ KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager);
+
+ // loading Key
+ byte[] keyBytes = new byte[fl.available()];
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ fl.read(keyBytes, 0, fl.available());
+ fl.close();
+ PKCS8EncodedKeySpec keysp = new PKCS8EncodedKeySpec(keyBytes);
+ PrivateKey key = kf.generatePrivate(keysp);
+
+ // loading CertificateChain
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+
+ @SuppressWarnings("rawtypes")
+ Collection c = cf.generateCertificates(certstream) ;
+ Certificate[] certs = new Certificate[c.toArray().length];
+
+ certs = (Certificate[])c.toArray(new Certificate[0]);
+
+ // storing keystore
+ ks.setKeyEntry(keyAlias, key, keyPassword.toCharArray(), certs);
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Key and certificate stored.");
+ logger.debug("Alias:"+ keyAlias);
+ }
+ OutputStream keyStoreOutStream = getKeyStoreOutStream();
+ ks.store(keyStoreOutStream, keyPassword.toCharArray());
+ // Workaround for MNT-15005
+ keyStoreOutStream.close();
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+
+ writeLock.unlock();
+ }
+ }
+
+ public boolean backupExists()
+ {
+ return keyStoreExists(getBackupKeyStoreParameters().getLocation());
+ }
+
+ protected boolean keyStoreExists(String location)
+ {
+ try
+ {
+ InputStream is = getKeyStoreStream(location);
+ if (is == null)
+ {
+ return false;
+ }
+ else
+ {
+ try { is.close(); } catch (Throwable e) {}
+ return true;
+ }
+ }
+ catch(FileNotFoundException e)
+ {
+ return false;
+ }
+ }
+
+ /*
+ * Validates the keystore keys against the key registry, throwing exceptions if the keys have been unintentionally changed.
+ *
+ * For each key to validate:
+ *
+ * (i) no main key, no backup key, the key is registered for the main keystore -> error, must re-instate the keystore
+ * (ii) no main key, no backup key, the key is not registered -> create the main key store and register the key
+ * (iii) main key exists but is not registered -> register the key
+ * (iv) main key exists, no backup key, the key is registered -> check that the key has not changed - if it has, throw an exception
+ * (v) main key exists, backup key exists, the key is registered -> check in the registry that the backup key has not changed and then re-register main key
+ */
+ protected void validateKeys(KeyMap keys, KeyMap backupKeys) throws InvalidKeystoreException, MissingKeyException
+ {
+ if(!validateKeyChanges)
+ {
+ return;
+ }
+
+ writeLock.lock();
+ try
+ {
+ // check for the existence of a key store first
+ for(String keyAlias : keysToValidate)
+ {
+ if(keys.getKey(keyAlias) == null)
+ {
+ if(backupKeys.getKey(keyAlias) == null)
+ {
+ if(encryptionKeysRegistry.isKeyRegistered(keyAlias))
+ {
+ // The key is registered and neither key nor backup key exist -> throw
+ // an exception indicating that the key is missing and the keystore should
+ // be re-instated.
+ throw new MissingKeyException(keyAlias, getKeyStoreParameters().getLocation());
+ }
+ else
+ {
+ // Neither the key nor the backup key exist, so create the key
+ createKey(keyAlias);
+ }
+ }
+ }
+ else
+ {
+ if(!encryptionKeysRegistry.isKeyRegistered(keyAlias))
+ {
+ // The key is not registered, so register it
+ encryptionKeysRegistry.registerKey(keyAlias, keys.getKey(keyAlias));
+ }
+ else if(backupKeys.getKey(keyAlias) == null && encryptionKeysRegistry.checkKey(keyAlias, keys.getKey(keyAlias)) == KEY_STATUS.CHANGED)
+ {
+ // A key has been changed, indicating that the keystore has been un-intentionally changed.
+ // Note: this will halt the application bootstrap.
+ throw new InvalidKeystoreException("The key with alias " + keyAlias + " has been changed, re-instate the previous keystore");
+ }
+ else if(backupKeys.getKey(keyAlias) != null && encryptionKeysRegistry.isKeyRegistered(keyAlias))
+ {
+ // Both key and backup key exist and the key is registered.
+ if(encryptionKeysRegistry.checkKey(keyAlias, backupKeys.getKey(keyAlias)) == KEY_STATUS.OK)
+ {
+ // The registered key is the backup key so lets re-register the key in the main key store.
+ // Unregister the existing (now backup) key and re-register the main key.
+ encryptionKeysRegistry.unregisterKey(keyAlias);
+ encryptionKeysRegistry.registerKey(keyAlias, keys.getKey(keyAlias));
+ }
+ }
+ }
+ }
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ public static class KeyInformation
+ {
+ protected String alias;
+ protected byte[] keyData;
+ protected String password;
+ protected String keyAlgorithm;
+
+ public KeyInformation(String alias, byte[] keyData, String password, String keyAlgorithm)
+ {
+ super();
+ this.alias = alias;
+ this.keyData = keyData;
+ this.password = password;
+ this.keyAlgorithm = keyAlgorithm;
+ }
+
+ public String getAlias()
+ {
+ return alias;
+ }
+
+ public byte[] getKeyData()
+ {
+ return keyData;
+ }
+
+ public String getPassword()
+ {
+ return password;
+ }
+
+ public String getKeyAlgorithm()
+ {
+ return keyAlgorithm;
+ }
+ }
+
+ /*
+ * Caches key meta data information such as password, seed.
+ *
+ */
+ public static class KeyInfoManager
+ {
+ private KeyResourceLoader keyResourceLoader;
+ private String metadataFileLocation;
+ private Properties keyProps;
+ private String keyStorePassword = null;
+ private Map keyInfo;
+
+ /**
+ * For testing.
+ *
+ * @param passwords
+ */
+ KeyInfoManager(Map passwords, KeyResourceLoader keyResourceLoader)
+ {
+ this.keyResourceLoader = keyResourceLoader;
+ keyInfo = new HashMap(2);
+ for(Map.Entry password : passwords.entrySet())
+ {
+ keyInfo.put(password.getKey(), new KeyInformation(password.getKey(), null, password.getValue(), null));
+ }
+ }
+
+ KeyInfoManager(String metadataFileLocation, KeyResourceLoader keyResourceLoader) throws IOException, FileNotFoundException
+ {
+ this.keyResourceLoader = keyResourceLoader;
+ this.metadataFileLocation = metadataFileLocation;
+ keyInfo = new HashMap(2);
+ loadKeyMetaData();
+ }
+
+ public Map getKeyInfo()
+ {
+ // TODO defensively copy
+ return keyInfo;
+ }
+
+ /**
+ * Set the map of key meta data (including passwords to access the keystore).
+ *
+ * Where required, null values must be inserted into the map to indicate the presence
+ * of a key that is not protected by a password. They entry for {@link #KEY_KEYSTORE_PASSWORD}
+ * is required if the keystore is password protected.
+ */
+ protected void loadKeyMetaData() throws IOException, FileNotFoundException
+ {
+ keyProps = keyResourceLoader.loadKeyMetaData(metadataFileLocation);
+ if(keyProps != null)
+ {
+ String aliases = keyProps.getProperty("aliases");
+ if(aliases == null)
+ {
+ throw new AlfrescoRuntimeException("Passwords file must contain an aliases key");
+ }
+
+ this.keyStorePassword = keyProps.getProperty(KEY_KEYSTORE_PASSWORD);
+
+ StringTokenizer st = new StringTokenizer(aliases, ",");
+ while(st.hasMoreTokens())
+ {
+ String keyAlias = st.nextToken();
+ keyInfo.put(keyAlias, loadKeyInformation(keyAlias));
+ }
+ }
+ else
+ {
+ // TODO
+ //throw new FileNotFoundException("Cannot find key metadata file " + getKeyMetaDataFileLocation());
+ }
+ }
+
+ public void clear()
+ {
+ this.keyStorePassword = null;
+ if(this.keyProps != null)
+ {
+ this.keyProps.clear();
+ }
+ }
+
+ public void removeKeyInformation(String keyAlias)
+ {
+ this.keyProps.remove(keyAlias);
+ }
+
+ protected KeyInformation loadKeyInformation(String keyAlias)
+ {
+ String keyPassword = keyProps.getProperty(keyAlias + ".password");
+ String keyData = keyProps.getProperty(keyAlias + ".keyData");
+ String keyAlgorithm = keyProps.getProperty(keyAlias + ".algorithm");
+
+ byte[] keyDataBytes = null;
+ if(keyData != null && !keyData.equals(""))
+ {
+ keyDataBytes = Base64.decodeBase64(keyData);
+ }
+ KeyInformation keyInfo = new KeyInformation(keyAlias, keyDataBytes, keyPassword, keyAlgorithm);
+ return keyInfo;
+ }
+
+ public String getKeyStorePassword()
+ {
+ return keyStorePassword;
+ }
+
+ public void clearKeyStorePassword()
+ {
+ this.keyStorePassword = null;
+ }
+
+ public KeyInformation getKeyInformation(String keyAlias)
+ {
+ return keyInfo.get(keyAlias);
+ }
+ }
+}
diff --git a/core/src/main/java/org/alfresco/encryption/CachedKey.java b/core/src/main/java/org/alfresco/encryption/CachedKey.java
new file mode 100644
index 0000000000..9d24ee0598
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encryption/CachedKey.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.Key;
+
+/**
+ *
+ * Represents a loaded, cached encryption key. The key can be null.
+ *
+ * @since 4.0
+ *
+ */
+public class CachedKey
+{
+ public static CachedKey NULL = new CachedKey(null, null);
+
+ private Key key;
+ private long timestamp;
+
+ CachedKey(Key key, Long timestamp)
+ {
+ this.key = key;
+ this.timestamp = (timestamp != null ? timestamp.longValue() : -1);
+ }
+
+ public CachedKey(Key key)
+ {
+ super();
+ this.key = key;
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ public Key getKey()
+ {
+ return key;
+ }
+
+ public long getTimestamp()
+ {
+ return timestamp;
+ }
+}
diff --git a/core/src/main/java/org/alfresco/encryption/DecryptingInputStream.java b/core/src/main/java/org/alfresco/encryption/DecryptingInputStream.java
new file mode 100644
index 0000000000..97a0e5bf7f
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encryption/DecryptingInputStream.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2005-2010 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.encryption;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An input stream that encrypts data produced by a {@link EncryptingOutputStream}. A lightweight yet secure hybrid
+ * encryption scheme is used. A random symmetric key is decrypted using the receiver's private key. The supplied data is
+ * then decrypted using the symmetric key and read on a streaming basis. When the end of the stream is reached or the
+ * stream is closed, a HMAC checksum of the entire stream contents is validated.
+ */
+public class DecryptingInputStream extends InputStream
+{
+
+ /** The wrapped stream. */
+ private final DataInputStream wrapped;
+
+ /** The input cipher. */
+ private final Cipher inputCipher;
+
+ /** The MAC generator. */
+ private final Mac mac;
+
+ /** Internal buffer for MAC computation. */
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
+
+ /** A DataOutputStream on top of our interal buffer. */
+ private final DataOutputStream dataStr = new DataOutputStream(this.buffer);
+
+ /** The current unencrypted data block. */
+ private byte[] currentDataBlock;
+
+ /** The next encrypted data block. (could be the HMAC checksum) */
+ private byte[] nextDataBlock;
+
+ /** Have we read to the end of the underlying stream?. */
+ private boolean isAtEnd;
+
+ /** Our current position within currentDataBlock. */
+ private int currentDataPos;
+
+ /**
+ * Constructs a DecryptingInputStream using default symmetric encryption parameters.
+ *
+ * @param wrapped
+ * the input stream to decrypt
+ * @param privKey
+ * the receiver's private key for decrypting the symmetric key
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ * @throws NoSuchAlgorithmException
+ * the no such algorithm exception
+ * @throws NoSuchPaddingException
+ * the no such padding exception
+ * @throws InvalidKeyException
+ * the invalid key exception
+ * @throws IllegalBlockSizeException
+ * the illegal block size exception
+ * @throws BadPaddingException
+ * the bad padding exception
+ * @throws InvalidAlgorithmParameterException
+ * the invalid algorithm parameter exception
+ * @throws NoSuchProviderException
+ * the no such provider exception
+ */
+ public DecryptingInputStream(final InputStream wrapped, final PrivateKey privKey) throws IOException,
+ NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
+ BadPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException
+ {
+ this(wrapped, privKey, "AES", "CBC", "PKCS5PADDING");
+ }
+
+ /**
+ * Constructs a DecryptingInputStream.
+ *
+ * @param wrapped
+ * the input stream to decrypt
+ * @param privKey
+ * the receiver's private key for decrypting the symmetric key
+ * @param algorithm
+ * encryption algorithm (e.g. "AES")
+ * @param mode
+ * encryption mode (e.g. "CBC")
+ * @param padding
+ * padding scheme (e.g. "PKCS5PADDING")
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ * @throws NoSuchAlgorithmException
+ * the no such algorithm exception
+ * @throws NoSuchPaddingException
+ * the no such padding exception
+ * @throws InvalidKeyException
+ * the invalid key exception
+ * @throws IllegalBlockSizeException
+ * the illegal block size exception
+ * @throws BadPaddingException
+ * the bad padding exception
+ * @throws InvalidAlgorithmParameterException
+ * the invalid algorithm parameter exception
+ * @throws NoSuchProviderException
+ * the no such provider exception
+ */
+ public DecryptingInputStream(final InputStream wrapped, final PrivateKey privKey, final String algorithm,
+ final String mode, final String padding) throws IOException, NoSuchAlgorithmException,
+ NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
+ InvalidAlgorithmParameterException, NoSuchProviderException
+ {
+ // Initialise a secure source of randomness
+ this.wrapped = new DataInputStream(wrapped);
+ final SecureRandom secRand = SecureRandom.getInstance("SHA1PRNG");
+
+ // Set up RSA
+ final Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING");
+ rsa.init(Cipher.DECRYPT_MODE, privKey, secRand);
+
+ // Read and decrypt the symmetric key
+ final SecretKey symKey = new SecretKeySpec(rsa.doFinal(readBlock()), algorithm);
+
+ // Read and decrypt initialisation vector
+ final byte[] keyIV = rsa.doFinal(readBlock());
+
+ // Set up cipher for decryption
+ this.inputCipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding);
+ this.inputCipher.init(Cipher.DECRYPT_MODE, symKey, new IvParameterSpec(keyIV));
+
+ // Read and decrypt the MAC key
+ final SecretKey macKey = new SecretKeySpec(this.inputCipher.doFinal(readBlock()), "HMACSHA1");
+
+ // Set up HMAC
+ this.mac = Mac.getInstance("HMACSHA1");
+ this.mac.init(macKey);
+
+ // Always read a block ahead so we can intercept the HMAC block
+ this.nextDataBlock = readBlock(false);
+ }
+
+ /**
+ * Reads the next block of data, adding it to the HMAC checksum. Strips the header recording the number of bytes in
+ * the block.
+ *
+ * @return the data block, or null
if the end of the stream has been reached
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ */
+ private byte[] readBlock() throws IOException
+ {
+ return readBlock(true);
+ }
+
+ /**
+ * Reads the next block of data, optionally adding it to the HMAC checksum. Strips the header recording the number
+ * of bytes in the block.
+ *
+ * @param updateMac
+ * should the block be added to the HMAC checksum?
+ * @return the data block, or null
if the end of the stream has been reached
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ */
+ private byte[] readBlock(final boolean updateMac) throws IOException
+ {
+ int len;
+ try
+ {
+ len = this.wrapped.readInt();
+ }
+ catch (final EOFException e)
+ {
+ return null;
+ }
+ final byte[] in = new byte[len];
+ this.wrapped.readFully(in);
+ if (updateMac)
+ {
+ macBlock(in);
+ }
+ return in;
+ }
+
+ /**
+ * Updates the HMAC checksum with the given data block.
+ *
+ * @param block
+ * the block
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ */
+ private void macBlock(final byte[] block) throws IOException
+ {
+ this.dataStr.writeInt(block.length);
+ this.dataStr.write(block);
+ // If we don't have the MAC key yet, buffer up until we do
+ if (this.mac != null)
+ {
+ this.dataStr.flush();
+ final byte[] bytes = this.buffer.toByteArray();
+ this.buffer.reset();
+ this.mac.update(bytes);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#read()
+ */
+ @Override
+ public int read() throws IOException
+ {
+ final byte[] buf = new byte[1];
+ int bytesRead;
+ while ((bytesRead = read(buf)) == 0)
+ {
+ ;
+ }
+ return bytesRead == -1 ? -1 : buf[0] & 0xFF;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#read(byte[])
+ */
+ @Override
+ public int read(final byte b[]) throws IOException
+ {
+ return read(b, 0, b.length);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#read(byte[], int, int)
+ */
+ @Override
+ public int read(final byte b[], int off, final int len) throws IOException
+ {
+ if (b == null)
+ {
+ throw new NullPointerException();
+ }
+ else if (off < 0 || off > b.length || len < 0 || off + len > b.length || off + len < 0)
+ {
+ throw new IndexOutOfBoundsException();
+ }
+ else if (len == 0)
+ {
+ return 0;
+ }
+
+ int bytesToRead = len;
+ OUTER: while (bytesToRead > 0)
+ {
+ // Fetch another block if necessary
+ while (this.currentDataBlock == null || this.currentDataPos >= this.currentDataBlock.length)
+ {
+ byte[] newDataBlock;
+ // We're right at the end of the last block so finish
+ if (this.isAtEnd)
+ {
+ this.currentDataBlock = this.nextDataBlock = null;
+ break OUTER;
+ }
+ // We've already read the last block so validate the MAC code
+ else if ((newDataBlock = readBlock(false)) == null)
+ {
+ if (!MessageDigest.isEqual(this.mac.doFinal(), this.nextDataBlock))
+ {
+ throw new IOException("Invalid HMAC");
+ }
+ // We still have what's left in the cipher to read
+ try
+ {
+ this.currentDataBlock = this.inputCipher.doFinal();
+ }
+ catch (final GeneralSecurityException e)
+ {
+ throw new RuntimeException(e);
+ }
+ this.isAtEnd = true;
+ }
+ // We have an ordinary data block to MAC and decrypt
+ else
+ {
+ macBlock(this.nextDataBlock);
+ this.currentDataBlock = this.inputCipher.update(this.nextDataBlock);
+ this.nextDataBlock = newDataBlock;
+ }
+ this.currentDataPos = 0;
+ }
+ final int bytesRead = Math.min(bytesToRead, this.currentDataBlock.length - this.currentDataPos);
+ System.arraycopy(this.currentDataBlock, this.currentDataPos, b, off, bytesRead);
+ bytesToRead -= bytesRead;
+ off += bytesRead;
+ this.currentDataPos += bytesRead;
+ }
+ return bytesToRead == len ? -1 : len - bytesToRead;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#available()
+ */
+ @Override
+ public int available() throws IOException
+ {
+ return this.currentDataBlock == null ? 0 : this.currentDataBlock.length - this.currentDataPos;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#close()
+ */
+ @Override
+ public void close() throws IOException
+ {
+ // Read right to the end, just to ensure the MAC code is valid!
+ if (this.nextDataBlock != null)
+ {
+ final byte[] skipBuff = new byte[1024];
+ while (read(skipBuff) != -1)
+ {
+ ;
+ }
+ }
+ this.wrapped.close();
+ this.dataStr.close();
+ }
+
+}
diff --git a/core/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java b/core/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java
new file mode 100644
index 0000000000..6bceef2a9d
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.AlgorithmParameters;
+import java.util.Arrays;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.alfresco.encryption.MACUtils.MACInput;
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.IPUtils;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.extensions.surf.util.Base64;
+import org.springframework.util.FileCopyUtils;
+
+/**
+ * Various encryption utility methods.
+ *
+ * @since 4.0
+ */
+public class DefaultEncryptionUtils implements EncryptionUtils
+{
+ // Logger
+ protected static Log logger = LogFactory.getLog(Encryptor.class);
+
+ protected static String HEADER_ALGORITHM_PARAMETERS = "XAlfresco-algorithmParameters";
+ protected static String HEADER_MAC = "XAlfresco-mac";
+ protected static String HEADER_TIMESTAMP = "XAlfresco-timestamp";
+
+ protected Encryptor encryptor;
+ protected MACUtils macUtils;
+ protected long messageTimeout; // ms
+ protected String remoteIP;
+ protected String localIP;
+
+ public DefaultEncryptionUtils()
+ {
+ try
+ {
+ this.localIP = InetAddress.getLocalHost().getHostAddress();
+ }
+ catch(Exception e)
+ {
+ throw new AlfrescoRuntimeException("Unable to initialise EncryptionUtils", e);
+ }
+ }
+
+ public String getRemoteIP()
+ {
+ return remoteIP;
+ }
+
+ public void setRemoteIP(String remoteIP)
+ {
+ try
+ {
+ this.remoteIP = IPUtils.getRealIPAddress(remoteIP);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new AlfrescoRuntimeException("Failed to get server IP address", e);
+ }
+ }
+
+ /**
+ * Get the local registered IP address for authentication purposes
+ *
+ * @return String
+ */
+ protected String getLocalIPAddress()
+ {
+ return localIP;
+ }
+
+ public void setMessageTimeout(long messageTimeout)
+ {
+ this.messageTimeout = messageTimeout;
+ }
+
+ public void setEncryptor(Encryptor encryptor)
+ {
+ this.encryptor = encryptor;
+ }
+
+ public void setMacUtils(MACUtils macUtils)
+ {
+ this.macUtils = macUtils;
+ }
+
+ protected void setRequestMac(HttpMethod method, byte[] mac)
+ {
+ if(mac == null)
+ {
+ throw new AlfrescoRuntimeException("Mac cannot be null");
+ }
+ method.setRequestHeader(HEADER_MAC, Base64.encodeBytes(mac));
+ }
+
+ /**
+ * Set the MAC on the HTTP response
+ *
+ * @param response HttpServletResponse
+ * @param mac byte[]
+ */
+ protected void setMac(HttpServletResponse response, byte[] mac)
+ {
+ if(mac == null)
+ {
+ throw new AlfrescoRuntimeException("Mac cannot be null");
+ }
+
+ response.setHeader(HEADER_MAC, Base64.encodeBytes(mac));
+ }
+
+ /**
+ * Get the MAC (Message Authentication Code) on the HTTP request
+ *
+ * @param req HttpServletRequest
+ * @return the MAC
+ * @throws IOException
+ */
+ protected byte[] getMac(HttpServletRequest req) throws IOException
+ {
+ String header = req.getHeader(HEADER_MAC);
+ if(header != null)
+ {
+ return Base64.decode(header);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Get the MAC (Message Authentication Code) on the HTTP response
+ *
+ * @param res HttpMethod
+ * @return the MAC
+ * @throws IOException
+ */
+ protected byte[] getResponseMac(HttpMethod res) throws IOException
+ {
+ Header header = res.getResponseHeader(HEADER_MAC);
+ if(header != null)
+ {
+ return Base64.decode(header.getValue());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Set the timestamp on the HTTP request
+ * @param method HttpMethod
+ * @param timestamp (ms, in UNIX time)
+ */
+ protected void setRequestTimestamp(HttpMethod method, long timestamp)
+ {
+ method.setRequestHeader(HEADER_TIMESTAMP, String.valueOf(timestamp));
+ }
+
+ /**
+ * Set the timestamp on the HTTP response
+ * @param res HttpServletResponse
+ * @param timestamp (ms, in UNIX time)
+ */
+ protected void setTimestamp(HttpServletResponse res, long timestamp)
+ {
+ res.setHeader(HEADER_TIMESTAMP, String.valueOf(timestamp));
+ }
+
+ /**
+ * Get the timestamp on the HTTP response
+ *
+ * @param method HttpMethod
+ * @return timestamp (ms, in UNIX time)
+ * @throws IOException
+ */
+ protected Long getResponseTimestamp(HttpMethod method) throws IOException
+ {
+ Header header = method.getResponseHeader(HEADER_TIMESTAMP);
+ if(header != null)
+ {
+ return Long.valueOf(header.getValue());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Get the timestamp on the HTTP request
+ *
+ * @param method HttpServletRequest
+ * @return timestamp (ms, in UNIX time)
+ * @throws IOException
+ */
+ protected Long getTimestamp(HttpServletRequest method) throws IOException
+ {
+ String header = method.getHeader(HEADER_TIMESTAMP);
+ if(header != null)
+ {
+ return Long.valueOf(header);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setRequestAlgorithmParameters(HttpMethod method, AlgorithmParameters params) throws IOException
+ {
+ if(params != null)
+ {
+ method.setRequestHeader(HEADER_ALGORITHM_PARAMETERS, Base64.encodeBytes(params.getEncoded()));
+ }
+ }
+
+ /**
+ * Set the algorithm parameters header on the HTTP response
+ *
+ * @param response HttpServletResponse
+ * @param params AlgorithmParameters
+ * @throws IOException
+ */
+ protected void setAlgorithmParameters(HttpServletResponse response, AlgorithmParameters params) throws IOException
+ {
+ if(params != null)
+ {
+ response.setHeader(HEADER_ALGORITHM_PARAMETERS, Base64.encodeBytes(params.getEncoded()));
+ }
+ }
+
+ /**
+ * Decode cipher algorithm parameters from the HTTP method
+ *
+ * @param method HttpMethod
+ * @return decoded algorithm parameters
+ * @throws IOException
+ */
+ protected AlgorithmParameters decodeAlgorithmParameters(HttpMethod method) throws IOException
+ {
+ Header header = method.getResponseHeader(HEADER_ALGORITHM_PARAMETERS);
+ if(header != null)
+ {
+ byte[] algorithmParams = Base64.decode(header.getValue());
+ AlgorithmParameters algorithmParameters = encryptor.decodeAlgorithmParameters(algorithmParams);
+ return algorithmParameters;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Decode cipher algorithm parameters from the HTTP method
+ *
+ * @param req
+ * @return decoded algorithm parameters
+ * @throws IOException
+ */
+ protected AlgorithmParameters decodeAlgorithmParameters(HttpServletRequest req) throws IOException
+ {
+ String header = req.getHeader(HEADER_ALGORITHM_PARAMETERS);
+ if(header != null)
+ {
+ byte[] algorithmParams = Base64.decode(header);
+ AlgorithmParameters algorithmParameters = encryptor.decodeAlgorithmParameters(algorithmParams);
+ return algorithmParameters;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] decryptResponseBody(HttpMethod method) throws IOException
+ {
+ // TODO fileoutputstream if content is especially large?
+ InputStream body = method.getResponseBodyAsStream();
+ if(body != null)
+ {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ FileCopyUtils.copy(body, out);
+
+ AlgorithmParameters params = decodeAlgorithmParameters(method);
+ if(params != null)
+ {
+ byte[] decrypted = encryptor.decrypt(KeyProvider.ALIAS_SOLR, params, out.toByteArray());
+ return decrypted;
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException("Unable to decrypt response body, missing encryption algorithm parameters");
+ }
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] decryptBody(HttpServletRequest req) throws IOException
+ {
+ if(req.getMethod().equals("POST"))
+ {
+ InputStream bodyStream = req.getInputStream();
+ if(bodyStream != null)
+ {
+ // expect algorithParameters header
+ AlgorithmParameters p = decodeAlgorithmParameters(req);
+
+ // decrypt the body
+ InputStream in = encryptor.decrypt(KeyProvider.ALIAS_SOLR, p, bodyStream);
+ return IOUtils.toByteArray(in);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean authenticateResponse(HttpMethod method, String remoteIP, byte[] decryptedBody)
+ {
+ try
+ {
+ byte[] expectedMAC = getResponseMac(method);
+ Long timestamp = getResponseTimestamp(method);
+ if(timestamp == null)
+ {
+ return false;
+ }
+ remoteIP = IPUtils.getRealIPAddress(remoteIP);
+ return authenticate(expectedMAC, new MACInput(decryptedBody, timestamp.longValue(), remoteIP));
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException("Unable to authenticate HTTP response", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean authenticate(HttpServletRequest req, byte[] decryptedBody)
+ {
+ try
+ {
+ byte[] expectedMAC = getMac(req);
+ Long timestamp = getTimestamp(req);
+ if(timestamp == null)
+ {
+ return false;
+ }
+ String ipAddress = IPUtils.getRealIPAddress(req.getRemoteAddr());
+ return authenticate(expectedMAC, new MACInput(decryptedBody, timestamp.longValue(), ipAddress));
+ }
+ catch(Exception e)
+ {
+ throw new AlfrescoRuntimeException("Unable to authenticate HTTP request", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setRequestAuthentication(HttpMethod method, byte[] message) throws IOException
+ {
+ long requestTimestamp = System.currentTimeMillis();
+
+ // add MAC header
+ byte[] mac = macUtils.generateMAC(KeyProvider.ALIAS_SOLR, new MACInput(message, requestTimestamp, getLocalIPAddress()));
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Setting MAC " + Arrays.toString(mac) + " on HTTP request " + method.getPath());
+ logger.debug("Setting timestamp " + requestTimestamp + " on HTTP request " + method.getPath());
+ }
+
+ setRequestMac(method, mac);
+
+ // prevent replays
+ setRequestTimestamp(method, requestTimestamp);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setResponseAuthentication(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
+ byte[] responseBody, AlgorithmParameters params) throws IOException
+ {
+ long responseTimestamp = System.currentTimeMillis();
+ byte[] mac = macUtils.generateMAC(KeyProvider.ALIAS_SOLR,
+ new MACInput(responseBody, responseTimestamp, getLocalIPAddress()));
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Setting MAC " + Arrays.toString(mac) + " on HTTP response to request " + httpRequest.getRequestURI());
+ logger.debug("Setting timestamp " + responseTimestamp + " on HTTP response to request " + httpRequest.getRequestURI());
+ }
+
+ setAlgorithmParameters(httpResponse, params);
+ setMac(httpResponse, mac);
+
+ // prevent replays
+ setTimestamp(httpResponse, responseTimestamp);
+ }
+
+ protected boolean authenticate(byte[] expectedMAC, MACInput macInput)
+ {
+ // check the MAC and, if valid, check that the timestamp is under the threshold and that the remote IP is
+ // the expected IP
+ boolean authorized = macUtils.validateMAC(KeyProvider.ALIAS_SOLR, expectedMAC, macInput) &&
+ validateTimestamp(macInput.getTimestamp());
+ return authorized;
+ }
+
+ protected boolean validateTimestamp(long timestamp)
+ {
+ long currentTime = System.currentTimeMillis();
+ return((currentTime - timestamp) < messageTimeout);
+ }
+
+}
diff --git a/core/src/main/java/org/alfresco/encryption/DefaultEncryptor.java b/core/src/main/java/org/alfresco/encryption/DefaultEncryptor.java
new file mode 100644
index 0000000000..630fa3d56b
--- /dev/null
+++ b/core/src/main/java/org/alfresco/encryption/DefaultEncryptor.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.PropertyCheck;
+
+/**
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class DefaultEncryptor extends AbstractEncryptor
+{
+ private boolean cacheCiphers = true;
+ private final ThreadLocal