/* * A CCNx library test. * * Copyright (C) 2008-2012 Palo Alto Research Center, Inc. * * This work is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License version 2 as published by the * Free Software Foundation. * This work 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. */ package org.ccnx.ccn.test.io; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.util.Random; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.impl.security.crypto.ContentKeys; import org.ccnx.ccn.impl.security.crypto.StaticContentKeys; import org.ccnx.ccn.impl.security.crypto.UnbufferedCipherInputStream; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.io.CCNFileInputStream; import org.ccnx.ccn.io.CCNFileOutputStream; import org.ccnx.ccn.io.CCNInputStream; import org.ccnx.ccn.io.CCNOutputStream; import org.ccnx.ccn.io.CCNVersionedInputStream; import org.ccnx.ccn.io.CCNVersionedOutputStream; import org.ccnx.ccn.io.content.ContentEncodingException; import org.ccnx.ccn.profiles.SegmentationProfile; import org.ccnx.ccn.protocol.ContentName; import org.ccnx.ccn.protocol.ContentObject; import org.ccnx.ccn.protocol.PublisherPublicKeyDigest; import org.ccnx.ccn.protocol.SignedInfo; import org.ccnx.ccn.protocol.SignedInfo.ContentType; import org.ccnx.ccn.test.CCNTestHelper; import org.ccnx.ccn.test.Flosser; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; /** * Test for stream encryption/decryption. */ public class CCNSecureInputStreamTest { static protected abstract class StreamFactory { ContentName name; ContentKeys keys; int encrLength; byte [] encrData; public StreamFactory(String file_name, int length) throws NoSuchAlgorithmException, IOException, InterruptedException { name = new ContentName(testHelper.getClassNamespace(), file_name); encrLength = length; flosser.handleNamespace(name); try { keys = StaticContentKeys.generateRandomKeys(); } catch (NoSuchPaddingException e) { Log.severe(Log.FAC_TEST, "NoSuchPaddingExcption creating algorithm we have used before! {0}", e.getMessage()); return; } writeFile(encrLength); flosser.stopMonitoringNamespace(name); } public abstract CCNInputStream makeInputStream() throws IOException; public abstract OutputStream makeOutputStream() throws IOException; public void writeFile(int fileLength) throws IOException, NoSuchAlgorithmException, InterruptedException { Random randBytes = new Random(0); // always same sequence, to aid debugging OutputStream os = makeOutputStream(); ByteArrayOutputStream data = new ByteArrayOutputStream(); byte [] bytes = new byte[BUF_SIZE]; int elapsed = 0; int nextBufSize = 0; final double probFlush = .3; while (elapsed < fileLength) { nextBufSize = ((fileLength - elapsed) > BUF_SIZE) ? BUF_SIZE : (fileLength - elapsed); randBytes.nextBytes(bytes); os.write(bytes, 0, nextBufSize); data.write(bytes, 0, nextBufSize); elapsed += nextBufSize; if (randBytes.nextDouble() < probFlush) { Log.info(Log.FAC_TEST, "Flushing buffers."); os.flush(); } } os.close(); encrData = data.toByteArray(); } public void streamEncryptDecrypt() throws IOException { // check we get identical data back out CCNInputStream vfirst = makeInputStream(); byte [] read_data = readFile(vfirst, encrLength); Assert.assertArrayEquals(encrData, read_data); // check things fail if we use different keys ContentKeys keys2 = keys; CCNInputStream v2 = null; try { keys = StaticContentKeys.generateRandomKeys(); v2 = makeInputStream(); } catch (NoSuchAlgorithmException e) { Log.severe(Log.FAC_TEST, "Unexpected NoSuchAlgorithmException using default algorithm! " + keys.getBaseAlgorithm()); Assert.fail("Unexpected NoSuchAlgorithmException using default algorithm! " + keys.getBaseAlgorithm()); } catch (NoSuchPaddingException e) { Log.severe(Log.FAC_TEST, "Unexpected NoSuchPaddingException using default algorithm! " + keys.getBaseAlgorithm()); Assert.fail("Unexpected NoSuchPaddingException using default algorithm! " + keys.getBaseAlgorithm()); } finally { keys = keys2; } read_data = readFile(v2, encrLength); Assert.assertFalse(encrData.equals(read_data)); } public void seekZero() throws IOException, NoSuchAlgorithmException { CCNInputStream i = makeInputStream(); i.seek(0); } public void seeking() throws IOException, NoSuchAlgorithmException { // check really small seeks/reads (smaller than 1 Cipher block) doSeeking(10); // check small seeks (but bigger than 1 Cipher block) doSeeking(600); // check large seeks (multiple ContentObjects) doSeeking(4096*5+350); } private void doSeeking(int length) throws IOException, NoSuchAlgorithmException { CCNInputStream i = makeInputStream(); // make sure we start mid ContentObject and past the first Cipher block int start = ((int) (encrLength*0.3) % 4096) +600; i.seek(start); readAndCheck(i, start, length); i.seek(start); readAndCheck(i, start, length); } public void markReset() throws IOException, NoSuchAlgorithmException { // check really small seeks/reads (smaller than 1 Cipher block) doMarkReset(10); // check small seeks (but bigger than 1 Cipher block) doMarkReset(600); // check large seeks (multiple ContentObjects) doMarkReset(4096*2+350); } private void doMarkReset(int length) throws IOException, NoSuchAlgorithmException { CCNInputStream i = makeInputStream(); i.skip(length); i.reset(); readAndCheck(i, 0, length); i.skip(1024); i.mark(length); readAndCheck(i, length+1024, length); i.reset(); readAndCheck(i, length+1024, length); } private void readAndCheck(CCNInputStream i, int start, int length) throws IOException, NoSuchAlgorithmException { byte [] origData = new byte[length]; System.arraycopy(encrData, start, origData, 0, length); byte [] readData = new byte[length]; i.read(readData); Assert.assertArrayEquals(origData, readData); } public void skipping() throws IOException, NoSuchAlgorithmException { // read some data, skip some data, read some more data CCNInputStream inStream = makeInputStream(); int start = (int) (encrLength*0.3); // check first part reads correctly readAndCheck(inStream, 0, start); // skip a short bit (less than 1 cipher block) inStream.skip(10); start += 10; // check second part reads correctly readAndCheck(inStream, start, 100); start += 100; // skip a medium bit (more than than 1 cipher block) inStream.skip(600); start += 600; // check third part reads correctly readAndCheck(inStream, start, 600); start += 600; // skip a bug bit (more than than 1 Content object) inStream.skip(600+4096*2); start += 600+4096*2; // check fourth part reads correctly readAndCheck(inStream, start, 600); } } /** * Handle naming for the test */ static CCNTestHelper testHelper = new CCNTestHelper(CCNSecureInputStreamTest.class); static CCNHandle outputLibrary; static CCNHandle inputLibrary; static Flosser flosser; static final int BUF_SIZE = 4096; static StreamFactory basic; static StreamFactory versioned; static StreamFactory file; static StreamFactory emptyFile; @BeforeClass public static void setUpBeforeClass() throws Exception { outputLibrary = CCNHandle.open(); inputLibrary = CCNHandle.open(); flosser = new Flosser(); basic = new StreamFactory("basic.txt", 25*1024+301){ public CCNInputStream makeInputStream() throws IOException { return new CCNInputStream(name, null, null, keys, inputLibrary); } public OutputStream makeOutputStream() throws IOException { return new CCNOutputStream(name, null, null, null, keys, outputLibrary); } }; versioned = new StreamFactory("versioned.txt", 25*1024+302){ public CCNInputStream makeInputStream() throws IOException { return new CCNVersionedInputStream(name, 0L, null, keys, inputLibrary); } public OutputStream makeOutputStream() throws IOException { return new CCNVersionedOutputStream(name, null, null, keys, outputLibrary); } }; file = new StreamFactory("file.txt", 25*1024+303){ public CCNInputStream makeInputStream() throws IOException { return new CCNFileInputStream(name, null, null, keys, inputLibrary); } public OutputStream makeOutputStream() throws IOException { return new CCNFileOutputStream(name, keys, outputLibrary); } }; emptyFile = new StreamFactory("emptyFile.txt", 0){ public CCNInputStream makeInputStream() throws IOException { return new CCNFileInputStream(name, null, null, keys, inputLibrary); } public OutputStream makeOutputStream() throws IOException { return new CCNFileOutputStream(name, keys, outputLibrary); } }; flosser.stop(); } @AfterClass public static void cleanupAfterClass() { outputLibrary.close(); inputLibrary.close(); } public static byte [] readFile(InputStream inputStream, int fileLength) throws IOException { ByteArrayOutputStream bos = null; bos = new ByteArrayOutputStream(); int elapsed = 0; int read = 0; byte [] bytes = new byte[BUF_SIZE]; while (elapsed < fileLength) { read = inputStream.read(bytes); bos.write(bytes, 0, read); if (read < 0) { break; } else if (read == 0) { try { Thread.sleep(10); } catch (InterruptedException e) { } } elapsed += read; } return bos.toByteArray(); } /** * Test cipher encryption & decryption work * @throws ContentEncodingException */ @Test public void cipherEncryptDecrypt() throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, ContentEncodingException { Log.info(Log.FAC_TEST, "Starting cipherEncryptDecrypt"); Cipher c = basic.keys.getSegmentEncryptionCipher(basic.name, outputLibrary.getDefaultPublisher(), 0); byte [] d = c.doFinal(basic.encrData); c = basic.keys.getSegmentDecryptionCipher(basic.name, outputLibrary.getDefaultPublisher(), 0); d = c.doFinal(d); // check we get identical data back out Assert.assertArrayEquals(basic.encrData, d); Log.info(Log.FAC_TEST, "Completed cipherEncryptDecrypt"); } /** * Test cipher stream encryption & decryption work * @throws IOException */ @Test public void cipherStreamEncryptDecrypt() throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException { Log.info(Log.FAC_TEST, "Starting cipherStreamEncryptDecrypt"); Cipher c = basic.keys.getSegmentEncryptionCipher(basic.name, outputLibrary.getDefaultPublisher(),0); InputStream is = new ByteArrayInputStream(basic.encrData, 0, basic.encrData.length); is = new UnbufferedCipherInputStream(is, c); byte [] cipherText = new byte[4096]; int total, res; for(total = 0, res = 0; res >= 0 && total < 4096; total+=(res > 0) ? res : 0) res = is.read(cipherText,total,4096-total); c = basic.keys.getSegmentDecryptionCipher(basic.name, outputLibrary.getDefaultPublisher(), 0); is = new ByteArrayInputStream(cipherText, 0, total); is = new UnbufferedCipherInputStream(is, c); byte [] buf = new byte[4096]; for(total = 0, res = 0; res >= 0 && total < 4096; total+=(res > 0) ? res : 0) res = is.read(buf,total,4096-total); // check we get identical data back out byte [] input = new byte[Math.min(4096, basic.encrLength)]; byte [] output = new byte[Math.min(4096, total)]; System.arraycopy(basic.encrData, 0, input, 0, input.length); System.arraycopy(buf, 0, output, 0, output.length); Assert.assertArrayEquals(input, output); Log.info(Log.FAC_TEST, "Completed cipherStreamEncryptDecrypt"); } /** * Test content encryption & decryption work * @throws IOException */ @Test public void contentEncryptDecrypt() throws InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException { Log.info(Log.FAC_TEST, "Starting contentEncryptDecrypt"); // create an encrypted content block PublisherPublicKeyDigest publisher = outputLibrary.getDefaultPublisher(); Cipher c = basic.keys.getSegmentEncryptionCipher(basic.name, publisher, 0); InputStream is = new ByteArrayInputStream(basic.encrData, 0, basic.encrData.length); is = new UnbufferedCipherInputStream(is, c); ContentName rootName = SegmentationProfile.segmentRoot(basic.name); Key signingKey = outputLibrary.keyManager().getSigningKey(publisher); byte [] finalBlockID = SegmentationProfile.getSegmentNumberNameComponent(1); int coLength = Math.min(4096, basic.encrData.length); ContentObject co = new ContentObject(SegmentationProfile.segmentName(rootName, 0), new SignedInfo(publisher, null, ContentType.ENCR, outputLibrary.keyManager().getKeyLocator(signingKey), new Integer(300), finalBlockID), is, coLength); // attempt to decrypt the data c = basic.keys.getSegmentDecryptionCipher(basic.name, publisher, 0); is = new UnbufferedCipherInputStream(new ByteArrayInputStream(co.content()), c); byte [] output = new byte[co.contentLength()]; for(int total = 0, res = 0; res >= 0 && total < output.length; total+=res) res = is.read(output, total, output.length-total); // check we get identical data back out byte [] input = new byte[coLength]; System.arraycopy(basic.encrData, 0, input, 0, input.length); Assert.assertArrayEquals(input, output); Log.info(Log.FAC_TEST, "Completed contentEncryptDecrypt"); } /** * Test stream encryption & decryption work, and that using different keys for decryption fails */ @Test public void basicStreamEncryptDecrypt() throws IOException { Log.info(Log.FAC_TEST, "Starting streamEncryptDecrypt"); basic.streamEncryptDecrypt(); Log.info(Log.FAC_TEST, "Completed streamEncryptDecrypt"); } @Test public void versionedStreamEncryptDecrypt() throws IOException { Log.info(Log.FAC_TEST, "Starting versionedStreamEncryptDecrypt"); versioned.streamEncryptDecrypt(); Log.info(Log.FAC_TEST, "Completed versionedStreamEncryptDecrypt"); } @Test public void fileStreamEncryptDecrypt() throws IOException { Log.info(Log.FAC_TEST, "Starting fileStreamEncryptDecrypt"); file.streamEncryptDecrypt(); Log.info(Log.FAC_TEST, "Completed fileStreamEncryptDecrypt"); } @Test public void emptyFileStreamEncryptDecrypt() throws IOException { Log.info(Log.FAC_TEST, "Starting emptyFileStreamEncryptDecrypt"); emptyFile.streamEncryptDecrypt(); Log.info(Log.FAC_TEST, "Completed emptyFileStreamEncryptDecrypt"); } /** * seek forward, read, seek back, read and check the results * do it for different size parts of the data */ @Test public void basicSeekZero() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting basicSeekZero"); basic.seekZero(); Log.info(Log.FAC_TEST, "Completed basicSeekZero"); } @Test public void versionedSeekZero() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting versionedSeekZero"); versioned.seekZero(); Log.info(Log.FAC_TEST, "Completed versionedSeekZero"); } @Test public void fileSeekZero() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting fileSeekZero"); file.seekZero(); Log.info(Log.FAC_TEST, "Completed fileSeekZero"); } @Test public void emptyFileSeekZero() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting emptyFileSeekZero"); emptyFile.seekZero(); Log.info(Log.FAC_TEST, "Completed emptyFileSeekZero"); } @Test public void basicSeeking() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting basicSeeking"); basic.seeking(); Log.info(Log.FAC_TEST, "Completed basicSeeking"); } @Test public void versionedSeeking() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting versionedSeeking"); versioned.seeking(); Log.info(Log.FAC_TEST, "Completed versionedSeeking"); } @Test public void fileSeeking() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting fileSeeking"); file.seeking(); Log.info(Log.FAC_TEST, "Completed fileSeeking"); } /** * Test that skipping while reading an encrypted stream works * Tries small/medium/large skips */ @Test public void basicSkipping() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting basicSkipping"); basic.skipping(); Log.info(Log.FAC_TEST, "Completed basicSkipping"); } @Test public void versionedSkipping() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting versionedSkipping"); versioned.skipping(); Log.info(Log.FAC_TEST, "Completed versionedSkipping"); } @Test public void fileSkipping() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting fileSkipping"); file.skipping(); Log.info(Log.FAC_TEST, "Completed fileSkipping"); } /** * Test that mark and reset on an encrypted stream works * Tries small/medium/large jumps */ @Test public void basicMarkReset() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting basicMarkReset"); basic.markReset(); Log.info(Log.FAC_TEST, "Completed basicMarkReset"); } @Test public void versionedMarkReset() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting versionedMarkReset"); versioned.markReset(); Log.info(Log.FAC_TEST, "Completed versionedMarkReset"); } @Test public void fileMarkReset() throws IOException, NoSuchAlgorithmException { Log.info(Log.FAC_TEST, "Starting versionedMarkReset"); file.markReset(); Log.info(Log.FAC_TEST, "Completed versionedMarkReset"); } }