diff --git a/source/java/org/alfresco/heartbeat/HeartBeat.java b/source/java/org/alfresco/heartbeat/HeartBeat.java new file mode 100644 index 0000000000..a39b1aec57 --- /dev/null +++ b/source/java/org/alfresco/heartbeat/HeartBeat.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2005-2008 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.heartbeat; + +import java.beans.XMLEncoder; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Date; +import java.util.Enumeration; +import java.util.Map; +import java.util.TreeMap; +import java.util.zip.GZIPOutputStream; + +import javax.sql.DataSource; + +import org.alfresco.repo.descriptor.DescriptorDAO; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.descriptor.Descriptor; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Base64; +import org.alfresco.util.security.EncryptingOutputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.Scheduler; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; +import org.springframework.context.ApplicationContext; + +import de.schlichtherle.util.ObfuscatedString; + +/** + * This class communicates some very basic repository statistics to Alfresco on a regular basis. + * + * @author dward + */ +public class HeartBeat +{ + + /** The logger. */ + private static final Log logger = LogFactory.getLog(HeartBeat.class); + + /** The relative path to the public keystore resource. */ + static final String PUBLIC_STORE = "heartbeatpublic.keystore"; + + /** The password protecting this store. */ + static final char[] PUBLIC_STORE_PWD = new ObfuscatedString(new long[] + { + 0x7D47AC5E71B3B560L, 0xD6F1405DC20AE70AL + }).toString().toCharArray(); + + /** + * Are we running in test mode? If so we send data to local port 9999 rather than an alfresco server. We also use a + * special test encryption certificate and ping on a more frequent basis. + */ + private final boolean testMode; + + /** The transaction service. */ + private final TransactionService transactionService; + + /** DAO for current repository descriptor. */ + private final DescriptorDAO currentRepoDescriptorDAO; + + /** The person service. */ + private final PersonService personService; + + /** The data source. */ + private final DataSource dataSource; + + /** + * The parameters that we expect to remain static throughout the lifetime of the repository. There is no need to + * continuously update these. + */ + private final Map staticParameters; + + /** A secure source of random numbers used for encryption. */ + private final SecureRandom random; + + /** The public key used for encryption. */ + private final PublicKey publicKey; + + /** + * Initialises the heart beat service. Note that dependencies are intentionally 'pulled' rather than injected + * because we don't want these to be reconfigured. + * + * @param context + * the context + */ + public HeartBeat(final ApplicationContext context) + { + this(context, false); + } + + /** + * Initialises the heart beat service, potentially in test mode. Note that dependencies are intentionally 'pulled' + * rather than injected because we don't want these to be reconfigured. + * + * @param context + * the context + * @param testMode + * are we running in test mode? If so we send data to local port 9999 rather than an alfresco server. We + * also use a special test encryption certificate and ping on a more frequent basis. + */ + public HeartBeat(final ApplicationContext context, final boolean testMode) + { + this.testMode = testMode; + this.transactionService = (TransactionService) context.getBean("transactionService"); + this.currentRepoDescriptorDAO = (DescriptorDAO) context.getBean("currentRepoDescriptorDAO"); + this.personService = (PersonService) context.getBean("personService"); + this.dataSource = (DataSource) context.getBean("dataSource"); + this.staticParameters = new TreeMap(); + try + { + // Load up the static parameters + final String ip = getLocalIps(); + this.staticParameters.put("ip", ip); + final String uid; + final Descriptor currentRepoDescriptor = this.currentRepoDescriptorDAO.getDescriptor(); + if (currentRepoDescriptor != null) + { + uid = currentRepoDescriptor.getId(); + this.staticParameters.put("uid", uid); + } + else + { + uid = "Unknown"; + } + final Descriptor serverDescriptor = ((DescriptorDAO) context.getBean("serverDescriptorDAO")) + .getDescriptor(); + this.staticParameters.put("edition", serverDescriptor.getEdition()); + this.staticParameters.put("versionMajor", serverDescriptor.getVersionMajor()); + this.staticParameters.put("versionMinor", serverDescriptor.getVersionMinor()); + this.staticParameters.put("schema", String.valueOf(serverDescriptor.getSchema())); + + // Use some of the unique parameters to seed the random number generator used for encryption + this.random = SecureRandom.getInstance("SHA1PRNG"); + this.random.setSeed((uid + ip + System.currentTimeMillis()).getBytes("UTF-8")); + + // Load the public key from the key store (use the trial one if this is a unit test) + final KeyStore keyStore = KeyStore.getInstance("JKS"); + final InputStream in = getClass().getResourceAsStream(HeartBeat.PUBLIC_STORE); + keyStore.load(in, HeartBeat.PUBLIC_STORE_PWD); + in.close(); + final String jobName = testMode ? "test" : "heartbeat"; + final Certificate cert = keyStore.getCertificate(jobName); + this.publicKey = cert.getPublicKey(); + + // Schedule the heart beat to run regularly + final Scheduler scheduler = (Scheduler) context.getBean("schedulerFactory"); + final JobDetail jobDetail = new JobDetail(jobName, Scheduler.DEFAULT_GROUP, HeartBeatJob.class); + jobDetail.getJobDataMap().put("heartBeat", this); + final Trigger trigger = new SimpleTrigger(jobName + "Trigger", Scheduler.DEFAULT_GROUP, new Date(), null, + SimpleTrigger.REPEAT_INDEFINITELY, testMode ? 1000 : 4 * 60 * 60 * 1000); + scheduler.scheduleJob(jobDetail, trigger); + } + catch (final RuntimeException e) + { + throw e; + } + catch (final Exception e) + { + throw new RuntimeException(e); + } + } + + /** + * Sends encrypted data over HTTP. + * + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws GeneralSecurityException + * an encryption related exception + */ + public void sendData() throws IOException, GeneralSecurityException + { + final HttpURLConnection req = (HttpURLConnection) new URL(this.testMode ? "http://localhost:9999/heartbeat/" + : "http://DAVIDW01.activiti.local:8080/heartbeat/" /*"http://heartbeat.alfresco.com/heartbeat/"*/).openConnection(); + try + { + req.setRequestMethod("POST"); + req.setRequestProperty("Content-Type", "application/octet-stream"); + req.setChunkedStreamingMode(1024); + req.setConnectTimeout(2000); + req.setDoOutput(true); + req.connect(); + sendData(req.getOutputStream()); + if (req.getResponseCode() != HttpURLConnection.HTTP_OK) + { + throw new IOException(req.getResponseMessage()); + } + } + finally + { + try + { + req.disconnect(); + } + catch (final Exception e) + { + } + + } + } + + /** + * Writes the heartbeat data to a given output stream. Parameters are serialized in XML format for maximum forward + * compatibility. + * + * @param dest + * the stream to write to + * @throws IOException + * Signals that an I/O exception has occurred. + * @throws GeneralSecurityException + * an encryption related exception + */ + public void sendData(final OutputStream dest) throws IOException, GeneralSecurityException + { + // Complement the static parameters with some dynamic ones + final Map params = this.transactionService.getRetryingTransactionHelper().doInTransaction( + new RetryingTransactionCallback>() + { + public Map execute() + { + final Map params = new TreeMap(HeartBeat.this.staticParameters); + params.put("numUsers", String.valueOf(HeartBeat.this.personService.getAllPeople().size())); + params.put("maxNodeId", String.valueOf(getMaxNodeId())); + final byte[] licenseKey = HeartBeat.this.currentRepoDescriptorDAO.getLicenseKey(); + if (licenseKey != null) + { + params.put("licenseKey", Base64.encodeBytes(licenseKey, Base64.DONT_BREAK_LINES)); + } + return params; + } + }, true /* readOnly */, false /* requiresNew */); + + // Compress and encrypt the output stream + OutputStream out = new GZIPOutputStream(new EncryptingOutputStream(dest, this.publicKey, this.random), 1024); + + // Encode the parameters to XML + XMLEncoder encoder = null; + try + { + encoder = new XMLEncoder(out); + encoder.writeObject(params); + } + finally + { + if (encoder != null) + { + try + { + encoder.close(); + out = null; + } + catch (final Exception e) + { + } + } + if (out != null) + { + try + { + out.close(); + } + catch (final Exception e) + { + } + } + } + } + + /** + * The scheduler job responsible for triggering a heartbeat on a regular basis. + */ + public static class HeartBeatJob implements Job + { + /* + * (non-Javadoc) + * @see org.quartz.Job#execute(org.quartz.JobExecutionContext) + */ + public void execute(final JobExecutionContext jobexecutioncontext) throws JobExecutionException + { + final JobDataMap dataMap = jobexecutioncontext.getJobDetail().getJobDataMap(); + final HeartBeat heartBeat = (HeartBeat) dataMap.get("heartBeat"); + try + { + heartBeat.sendData(); + } + catch (final Exception e) + { + // Heartbeat errors are non-fatal and will show as single line warnings + HeartBeat.logger.warn(e.toString()); + throw new JobExecutionException(e); + } + } + } + + /** + * Attempts to get all the local IP addresses of this machine in order to distinguish it from other nodes in the + * same network. The machine may use a static IP address in conjunction with a loopback adapter (e.g. to support + * Oracle on Windows), so the IP of the default network interface may not be enough to uniquely identify this + * machine. + * + * @return the local IP addresses, separated by the '/' character + */ + private String getLocalIps() + { + final StringBuilder ip = new StringBuilder(1024); + boolean first = true; + try + { + final Enumeration i = NetworkInterface.getNetworkInterfaces(); + while (i.hasMoreElements()) + { + final NetworkInterface n = i.nextElement(); + final Enumeration j = n.getInetAddresses(); + while (j.hasMoreElements()) + { + InetAddress a = j.nextElement(); + if (a.isLoopbackAddress()) + { + continue; + } + if (first) + { + first = false; + } + else + { + ip.append('/'); + } + ip.append(a.getHostAddress()); + } + } + } + catch (final Exception e) + { + // Ignore + } + return first ? "127.0.0.1" : ip.toString(); + } + + /** + * Gets the maximum repository node id. Note that this isn't the best indication of size, because on oracle, all + * unique IDs are generated from the same sequence. A count(*) would result in an index scan. + * + * @return the max node id + */ + private int getMaxNodeId() + { + Connection connection = null; + Statement stmt = null; + try + { + connection = this.dataSource.getConnection(); + connection.setAutoCommit(true); + stmt = connection.createStatement(); + final ResultSet rs = stmt.executeQuery("select max(id) from alf_node"); + if (!rs.next()) + { + return 0; + } + return rs.getInt(1); + } + catch (final SQLException e) + { + return 0; + } + finally + { + if (stmt != null) + { + try + { + stmt.close(); + } + catch (final Exception e) + { + } + } + if (connection != null) + { + try + { + connection.close(); + } + catch (final Exception e) + { + } + } + } + } +} diff --git a/source/java/org/alfresco/heartbeat/HeartBeatTest.java b/source/java/org/alfresco/heartbeat/HeartBeatTest.java new file mode 100644 index 0000000000..7d9a21a3ae --- /dev/null +++ b/source/java/org/alfresco/heartbeat/HeartBeatTest.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2005-2008 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.heartbeat; + +import java.beans.XMLDecoder; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import org.alfresco.util.BaseSpringTest; +import org.alfresco.util.security.DecryptingInputStream; + +/** + * An integration test for the heartbeat service. Fakes an HTTP endpoint with a server socket in order to sure the + * service is functioning correctly. + * + * @author dward + */ +public class HeartBeatTest extends BaseSpringTest +{ + + /** + * Test heart beat. + * + * @throws Exception + * the exception + */ + @SuppressWarnings("unchecked") + public void testHeartBeat() throws Exception + { + // Load the private key from the trial key store + PrivateKey privateKey; + { + final KeyStore keyStore = KeyStore.getInstance("JKS"); + final InputStream in = getClass().getResourceAsStream(HeartBeat.PUBLIC_STORE); + keyStore.load(in, HeartBeat.PUBLIC_STORE_PWD); + in.close(); + privateKey = (PrivateKey) keyStore.getKey("test", HeartBeat.PUBLIC_STORE_PWD); + } + + // Construct a heartbeat instance in test mode (beats every second using test public key) + new HeartBeat(getApplicationContext(), true); + ServerSocket serverSocket = new ServerSocket(9999); + + // Now attempt to parse 4 of the 'beats' + for (int i = 0; i < 4; i++) + { + Socket clientSocket = serverSocket.accept(); + XMLDecoder decoder = null; + InputStream in = null; + OutputStream out = null; + try + { + in = new GZIPInputStream(new DecryptingInputStream(new HttpChunkedInputStream(clientSocket + .getInputStream()), privateKey), 1024); + out = clientSocket.getOutputStream(); + decoder = new XMLDecoder(in); + Map params = (Map) decoder.readObject(); + out.write("HTTP/1.1 200 OK\r\n\r\n".getBytes("ASCII")); + System.out.println(params); + } + finally + { + if (decoder != null) + { + try + { + decoder.close(); + in = null; + } + catch (final Exception e) + { + } + } + if (in != null) + { + try + { + in.close(); + } + catch (final Exception e) + { + } + } + if (out != null) + { + try + { + out.close(); + } + catch (final Exception e) + { + } + } + try + { + clientSocket.close(); + } + catch (Exception e) + { + } + } + + } + serverSocket.close(); + + } + + /** + * Wraps a raw byte stream in a chunked HTTP request to look like a regular input stream. Skips headers and parses + * chunk sizes. + */ + public static class HttpChunkedInputStream extends InputStream + { + /** The raw input stream. */ + private final InputStream socketIn; + + /** A buffer for parsing headers. */ + private StringBuilder headerBuff = new StringBuilder(100); + + /** The current chunk size. */ + private int chunkSize; + + /** The current position in the chunk. */ + private int chunkPosition; + + /** Have we got to the end of the last chunk? */ + private boolean isAtEnd; + + /** + * Instantiates a new http chunked input stream. + * + * @param socketIn + * raw input stream from an HTTP request + * @throws IOException + * Signals that an I/O exception has occurred. + */ + public HttpChunkedInputStream(InputStream socketIn) throws IOException + { + this.socketIn = socketIn; + for (;;) + { + if (getNextHeader().length() == 0) + { + break; + } + } + setNextChunkSize(); + } + + /** + * Gets the next header. + * + * @return the next header + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private String getNextHeader() throws IOException + { + int b; + while ((b = socketIn.read()) != '\n') + { + if (b == -1) + { + throw new EOFException(); + } + headerBuff.append((char) b); // cast to char acceptable because this is ASCII + } + String header = headerBuff.toString().trim(); + headerBuff.setLength(0); + return header; + } + + /** + * Sets the next chunk size by parsing a chunk header. May detect an end of file condition and set isAtEnd = + * true. + * + * @return the next chunk size + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private int setNextChunkSize() throws IOException + { + String chunkHeader = getNextHeader(); + int sepIndex = chunkHeader.indexOf(';'); + if (sepIndex != -1) + { + chunkHeader = chunkHeader.substring(0, sepIndex).trim(); + } + this.chunkSize = Integer.parseInt(chunkHeader, 16); + this.chunkPosition = 0; + if (this.chunkSize == 0) + { + this.isAtEnd = true; + } + return this.chunkSize; + } + + /* + * (non-Javadoc) + * @see java.io.InputStream#close() + */ + @Override + public void close() throws IOException + { + // We intentionally avoid closing the socket input stream here, as that seems to close the entire socket, + // and stops us from being able to write a response! + // this.socketIn.close(); + } + + /* + * (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[], int, int) + */ + @Override + public int read(byte[] b, int off, int len) throws IOException + { + if (len == 0) + { + return 0; + } + if (this.isAtEnd) + { + return -1; + } + int bytesToRead = len; + while (bytesToRead > 0) + { + if (this.chunkPosition >= this.chunkSize) + { + // Skip the \r\n after this chunk + String eol = getNextHeader(); + if (eol.length() > 0) + { + throw new IOException("Bad chunk format"); + } + // Read the new chunk header + setNextChunkSize(); + if (this.isAtEnd) + { + // Skip past the trailers. We have to do this in case the same connection is recycled for the + // next request + for (;;) + { + if (getNextHeader().length() == 0) + { + break; + } + } + break; + } + + } + int bytesRead = Math.min(bytesToRead, this.chunkSize - this.chunkPosition); + bytesRead = this.socketIn.read(b, off, bytesRead); + if (bytesRead == -1) + { + break; + } + bytesToRead -= bytesRead; + off += bytesRead; + this.chunkPosition += bytesRead; + } + return bytesToRead == len ? -1 : len - bytesToRead; + } + } +} diff --git a/source/java/org/alfresco/heartbeat/heartbeatpublic.keystore b/source/java/org/alfresco/heartbeat/heartbeatpublic.keystore new file mode 100644 index 0000000000..26706f8ef2 Binary files /dev/null and b/source/java/org/alfresco/heartbeat/heartbeatpublic.keystore differ diff --git a/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java b/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java index 368b5858bb..3060e75cec 100644 --- a/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java +++ b/source/java/org/alfresco/repo/descriptor/DescriptorServiceImpl.java @@ -50,7 +50,6 @@ import org.springframework.context.ApplicationEvent; */ public class DescriptorServiceImpl extends AbstractLifecycleBean implements DescriptorService, InitializingBean { - /** The server descriptor DAO. */ private DescriptorDAO serverDescriptorDAO; @@ -64,8 +63,12 @@ public class DescriptorServiceImpl extends AbstractLifecycleBean implements Desc private TransactionService transactionService; /** The license service. */ - private LicenseService licenseService = null; + private LicenseService licenseService; + /** The heart beat service. */ + @SuppressWarnings("unused") + private Object heartBeat; + /** The server descriptor. */ private Descriptor serverDescriptor; @@ -166,13 +169,38 @@ public class DescriptorServiceImpl extends AbstractLifecycleBean implements Desc // note: this requires that the repository schema has already been initialised final RetryingTransactionCallback createDescriptorWork = new RetryingTransactionCallback() { - public Descriptor execute() + public Descriptor execute() throws ClassNotFoundException { + boolean initialiseHeartBeat = false; + // initialise license service (if installed) - initialiseLicenseService(); + try + { + DescriptorServiceImpl.this.licenseService = (LicenseService) constructSpecialService("org.alfresco.license.LicenseComponent"); + } + catch (ClassNotFoundException e) + { + DescriptorServiceImpl.this.licenseService = new NOOPLicenseService(); + initialiseHeartBeat = true; + } // verify license, but only if license component is installed - licenseService.verifyLicense(); + try + { + licenseService.verifyLicense(); + LicenseDescriptor l = licenseService.getLicense(); + // Initialise the heartbeat unless it is disabled by the license + if (initialiseHeartBeat || l == null || !l.isHeartBeatDisabled()) + { + DescriptorServiceImpl.this.heartBeat = constructSpecialService("org.alfresco.heartbeat.HeartBeat"); + } + } + catch (LicenseException e) + { + // Initialise heart beat anyway + DescriptorServiceImpl.this.heartBeat = constructSpecialService("org.alfresco.heartbeat.HeartBeat"); + throw e; + } // persist the server descriptor values currentRepoDescriptor = DescriptorServiceImpl.this.currentRepoDescriptorDAO @@ -221,51 +249,40 @@ public class DescriptorServiceImpl extends AbstractLifecycleBean implements Desc } /** - * Initialise License Service. + * Constructs a special service whose dependencies cannot or should not be injected declaratively. Examples include + * the license component and heartbeat service that are intentionally left unconfigurable. + * + * @param className + * the class name + * @return the object + * @throws ClassNotFoundException + * the class not found exception */ - private void initialiseLicenseService() + private Object constructSpecialService(String className) throws ClassNotFoundException { try { - // NOTE: We could tie in the License Component via Spring configuration, but then it could - // be declaratively taken out in an installed environment. - Class licenseComponentClass = Class.forName("org.alfresco.license.LicenseComponent"); - Constructor constructor = licenseComponentClass.getConstructor(new Class[] + Class componentClass = Class.forName(className); + Constructor constructor = componentClass.getConstructor(new Class[] { ApplicationContext.class }); - licenseService = (LicenseService) constructor.newInstance(new Object[] + return constructor.newInstance(new Object[] { getApplicationContext() }); } catch (ClassNotFoundException e) { - licenseService = new NOOPLicenseService(); + throw e; } - catch (SecurityException e) + catch (RuntimeException e) { - throw new AlfrescoRuntimeException("Failed to initialise license service", e); + throw e; } - catch (IllegalArgumentException e) + catch (Exception e) { - throw new AlfrescoRuntimeException("Failed to initialise license service", e); - } - catch (NoSuchMethodException e) - { - throw new AlfrescoRuntimeException("Failed to initialise license service", e); - } - catch (InvocationTargetException e) - { - throw new AlfrescoRuntimeException("Failed to initialise license service", e); - } - catch (InstantiationException e) - { - throw new AlfrescoRuntimeException("Failed to initialise license service", e); - } - catch (IllegalAccessException e) - { - throw new AlfrescoRuntimeException("Failed to initialise license service", e); + throw new AlfrescoRuntimeException("Failed to initialise " + className, e); } } diff --git a/source/java/org/alfresco/service/license/LicenseDescriptor.java b/source/java/org/alfresco/service/license/LicenseDescriptor.java index 4a4fb6402b..60e5024a02 100644 --- a/source/java/org/alfresco/service/license/LicenseDescriptor.java +++ b/source/java/org/alfresco/service/license/LicenseDescriptor.java @@ -27,7 +27,6 @@ package org.alfresco.service.license; import java.security.Principal; import java.util.Date; - /** * Provides access to License information. * @@ -39,50 +38,56 @@ public interface LicenseDescriptor /** * Gets the date license was issued * - * @return issue date + * @return issue date */ public Date getIssued(); - + /** * Gets the date license is valid till * - * @return valid until date (or null, if no time limit) + * @return valid until date (or null, if no time limit) */ public Date getValidUntil(); /** * Gets the length (in days) of license validity - * - * @return length in days of license validity (or null, if no time limit) + * + * @return length in days of license validity (or null, if no time limit) */ public Integer getDays(); - + /** * Ges the number of remaining days left on license * - * @return remaining days (or null, if no time limit) + * @return remaining days (or null, if no time limit) */ public Integer getRemainingDays(); /** * Gets the subject of the license * - * @return the subject + * @return the subject */ public String getSubject(); - + /** * Gets the holder of the license * - * @return the holder + * @return the holder */ public Principal getHolder(); - + /** * Gets the issuer of the license * - * @return the issuer + * @return the issuer */ public Principal getIssuer(); - + + /** + * Does this license allow the heartbeat to be disabled? + * + * @return true if this license allow the heartbeat to be disabled + */ + public boolean isHeartBeatDisabled(); } diff --git a/source/java/org/alfresco/util/security/DecryptingInputStream.java b/source/java/org/alfresco/util/security/DecryptingInputStream.java new file mode 100644 index 0000000000..07abbde9a5 --- /dev/null +++ b/source/java/org/alfresco/util/security/DecryptingInputStream.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2005-2008 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.util.security; + +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/source/java/org/alfresco/util/security/EncryptingOutputStream.java b/source/java/org/alfresco/util/security/EncryptingOutputStream.java new file mode 100644 index 0000000000..49db8231bb --- /dev/null +++ b/source/java/org/alfresco/util/security/EncryptingOutputStream.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2005-2008 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.util.security; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +/** + * An output stream that encrypts data to another output stream. A lightweight yet secure hybrid encryption scheme is + * used. A random symmetric key is generated and encrypted using the receiver's public key. The supplied data is then + * encrypted using the symmetric key and sent to the underlying stream on a streaming basis. An HMAC checksum is also + * computed on an ongoing basis and appended to the output when the stream is closed. This class can be used in + * conjunction with {@link DecryptingInputStream} to transport data securely. + */ +public class EncryptingOutputStream extends OutputStream +{ + /** The wrapped stream. */ + private final OutputStream wrapped; + + /** The output cipher. */ + private final Cipher outputCipher; + + /** 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); + + /** + * Constructs an EncryptingOutputStream using default symmetric encryption parameters. + * + * @param wrapped + * outputstream to store the encrypted data + * @param receiverKey + * the receiver's public key for encrypting the symmetric key + * @param rand + * a secure source of randomness + * @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 BadPaddingException + * the bad padding exception + * @throws IllegalBlockSizeException + * the illegal block size exception + */ + public EncryptingOutputStream(final OutputStream wrapped, final PublicKey receiverKey, final SecureRandom rand) + throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException + { + this(wrapped, receiverKey, "AES", rand, 128, "CBC", "PKCS5PADDING"); + } + + /** + * Constructs an EncryptingOutputStream. + * + * @param wrapped + * outputstream to store the encrypted data + * @param receiverKey + * the receiver's public key for encrypting the symmetric key + * @param algorithm + * symmetric encryption algorithm (e.g. "AES") + * @param rand + * a secure source of randomness + * @param strength + * the key size in bits (e.g. 128) + * @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 BadPaddingException + * the bad padding exception + * @throws IllegalBlockSizeException + * the illegal block size exception + */ + public EncryptingOutputStream(final OutputStream wrapped, final PublicKey receiverKey, final String algorithm, + final SecureRandom rand, final int strength, final String mode, final String padding) throws IOException, + NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, + BadPaddingException + { + // Initialise + this.wrapped = wrapped; + + // Generate a random symmetric key + final KeyGenerator keyGen = KeyGenerator.getInstance(algorithm); + keyGen.init(strength, rand); + final Key symKey = keyGen.generateKey(); + + // Instantiate Symmetric cipher for encryption. + this.outputCipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding); + this.outputCipher.init(Cipher.ENCRYPT_MODE, symKey, rand); + + // Set up HMAC + this.mac = Mac.getInstance("HMACSHA1"); + final byte[] macKeyBytes = new byte[20]; + rand.nextBytes(macKeyBytes); + final Key macKey = new SecretKeySpec(macKeyBytes, "HMACSHA1"); + this.mac.init(macKey); + + // Set up RSA to encrypt symmetric key + final Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING"); + rsa.init(Cipher.ENCRYPT_MODE, receiverKey, rand); + + // Write the header + + // Write out an RSA-encrypted block for the key of the cipher. + writeBlock(rsa.doFinal(symKey.getEncoded())); + + // Write out RSA-encrypted Initialisation Vector block + writeBlock(rsa.doFinal(this.outputCipher.getIV())); + + // Write out key for HMAC. + writeBlock(this.outputCipher.doFinal(macKey.getEncoded())); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#write(int) + */ + @Override + public void write(final int b) throws IOException + { + write(new byte[] + { + (byte) b + }, 0, 1); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#write(byte[]) + */ + @Override + public void write(final byte b[]) throws IOException + { + write(b, 0, b.length); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#write(byte[], int, int) + */ + @Override + public void write(final byte b[], final 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; + } + final byte[] out = this.outputCipher.update(b, off, len); // Encrypt data. + if (out != null && out.length > 0) + { + writeBlock(out); + } + } + + /** + * Writes a block of data, preceded by its length, and adds it to the HMAC checksum. + * + * @param out + * the data to be written. + * @throws IOException + * Signals that an I/O exception has occurred. + */ + private void writeBlock(final byte[] out) throws IOException + { + this.dataStr.writeInt(out.length); // Write length. + this.dataStr.write(out); // Write encrypted data. + this.dataStr.flush(); + final byte[] block = this.buffer.toByteArray(); + this.buffer.reset(); + this.mac.update(block); + this.wrapped.write(block); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#flush() + */ + @Override + public void flush() throws IOException + { + this.wrapped.flush(); + } + + /* + * (non-Javadoc) + * @see java.io.OutputStream#close() + */ + @Override + public void close() throws IOException + { + try + { + // Write the last block + writeBlock(this.outputCipher.doFinal()); + } + catch (final GeneralSecurityException e) + { + throw new RuntimeException(e); + } + // Write the MAC code + writeBlock(this.mac.doFinal()); + this.wrapped.close(); + this.dataStr.close(); + } + +} diff --git a/source/java/org/alfresco/util/security/EncryptingOutputStreamTest.java b/source/java/org/alfresco/util/security/EncryptingOutputStreamTest.java new file mode 100644 index 0000000000..122ca63231 --- /dev/null +++ b/source/java/org/alfresco/util/security/EncryptingOutputStreamTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2005-2008 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.util.security; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.util.Arrays; + +import junit.framework.TestCase; + +/** + * Tests that the EncryptingOutputStream and EncryptingInputStream classes work correctly. + */ +public class EncryptingOutputStreamTest extends TestCase +{ + + /** + * Tests encryption / decryption by attempting to encrypt and decrypt the bytes forming this class definition and + * comparing it with the unencrypted bytes. + * + * @throws Exception + * an exception + */ + public void testEncrypt() throws Exception + { + final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + final SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + final byte[] seed = getClass().getName().getBytes("UTF-8"); + random.setSeed(seed); + keyGen.initialize(1024, random); + final KeyPair pair = keyGen.generateKeyPair(); + + final ByteArrayOutputStream buff = new ByteArrayOutputStream(2048); + final OutputStream encrypting = new EncryptingOutputStream(buff, pair.getPublic(), random); + final ByteArrayOutputStream plainText1 = new ByteArrayOutputStream(2048); + + final InputStream in = getClass().getResourceAsStream(getClass().getSimpleName() + ".class"); + final byte[] inbuff = new byte[17]; + int bytesRead; + while ((bytesRead = in.read(inbuff)) != -1) + { + encrypting.write(inbuff, 0, bytesRead); + plainText1.write(inbuff, 0, bytesRead); + } + in.close(); + encrypting.close(); + plainText1.close(); + final InputStream decrypting = new DecryptingInputStream(new ByteArrayInputStream(buff.toByteArray()), pair + .getPrivate()); + final ByteArrayOutputStream plainText2 = new ByteArrayOutputStream(2048); + while ((bytesRead = decrypting.read(inbuff)) != -1) + { + plainText2.write(inbuff, 0, bytesRead); + } + + assertTrue(Arrays.equals(plainText1.toByteArray(), plainText2.toByteArray())); + + } +}