SmartDecryptingInputStream.java
package org.apache.fulcrum.jce.crypto;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
/**
* An input stream that determine if the originating input stream
* was encrypted or not. This magic only works for well-known file
* types though.
*
* @author <a href="mailto:siegfried.goeschl@it20one.at">Siegfried Goeschl</a>
*/
public class SmartDecryptingInputStream extends ByteArrayInputStream
{
/** The encodings to be checked for XML */
private static final String[] ENCODINGS = { "ISO-8859-1", "UTF-8", "UTF-16" };
/**
* Constructor
*
* @param cryptoStreamFactory the CryptoStreamFactory for creating a cipher stream
* @param is the input stream to be decrypted
* @throws IOException if file not found
* @throws GeneralSecurityException if security check fails
*/
public SmartDecryptingInputStream(
CryptoStreamFactory cryptoStreamFactory,
InputStream is )
throws IOException, GeneralSecurityException
{
this( cryptoStreamFactory, is, null );
}
/**
* Constructor
*
* @param cryptoStreamFactory the CryptoStreamFactory for creating a cipher stream
* @param is the input stream to be decrypted
* @param password the password for decryption
*
* @throws IOException if file not found
* @throws GeneralSecurityException if security check fails
*/
public SmartDecryptingInputStream(
CryptoStreamFactory cryptoStreamFactory,
InputStream is,
char[] password )
throws IOException, GeneralSecurityException
{
super( new byte[0] );
byte[] content = null;
byte[] plain = null;
// store the data from the input stream
ByteArrayOutputStream baosCipher = new ByteArrayOutputStream();
ByteArrayOutputStream baosPlain = new ByteArrayOutputStream();
this.copy( is, baosCipher );
content = baosCipher.toByteArray();
plain = content;
if( this.isEncrypted(content) == true )
{
InputStream cis = null;
ByteArrayInputStream bais = new ByteArrayInputStream(content);
if( ( password != null ) && ( password.length > 0 ) )
{
cis = cryptoStreamFactory.getInputStream( bais, password );
}
else
{
cis = cryptoStreamFactory.getInputStream( bais );
}
copy( cis, baosPlain );
plain = baosPlain.toByteArray();
}
// initialize the inherited instance
if( plain != null )
{
this.buf = plain;
this.pos = 0;
this.count = buf.length;
}
}
/**
* Determine if the content is encrypted. We are
* using our knowledge about block length, check
* for XML, ZIP and PDF files and at the end of
* the day we are just guessing.
*
* @param content the data to be examined
* @return true if this is an encrypted file
* @throws IOException unable to read the content
*/
private boolean isEncrypted( byte[] content )
throws IOException
{
if( content.length == 0 )
{
return false;
}
else if( ( content.length % 8 ) != 0 )
{
// the block length is 8 bytes - if the length
// is not a multipe of 8 then the content was
// definitely not encrypted
return false;
}
else if( this.isPDF(content) )
{
return false;
}
else if( this.isXML(content) )
{
return false;
}
else if( this.isZip(content) )
{
return false;
}
else if( this.isUtf16Text(content) )
{
return false;
}
else
{
for( int i=0; i<content.length; i++ )
{
// do we have control characters in it?
char ch = (char) content[i];
if( this.isAsciiControl(ch) )
{
return true;
}
}
return false;
}
}
/**
* Pumps the input stream to the output stream.
*
* @param is the source input stream
* @param os the target output stream
* @return the number of bytes copied
* @throws IOException the copying failed
*/
public long copy( InputStream is, OutputStream os )
throws IOException
{
byte[] buf = new byte[1024];
int n = 0;
long total = 0;
while ((n = is.read(buf)) > 0)
{
os.write(buf, 0, n);
total += n;
}
is.close();
os.flush();
os.close();
return total;
}
/**
* Count the number of occurences for the given value
* @param content the content to examine
* @param value the value to look fo
* @return the number of matches
*/
private int count( byte[] content, byte value )
{
int result = 0;
for( int i=0; i<content.length; i++ )
{
if( content[i] == value )
{
result++;
}
}
return result;
}
/**
* Detect the BOM of an UTF-16 (mandatory) or UTF-8 document (optional)
* @param content the content to examine
* @return true if the content contains a BOM
*/
private boolean hasByteOrderMark( byte[] content )
{
// bytes ar always signed in java, ff is 255
// removes signed parts
int firstUnsigned = content[0] & 0xFF;
int second = content[1] & 0xFF;
if( ((firstUnsigned == 0xFF) && (second == 0xFE)) ||
((firstUnsigned == 0xFE) && (second == 0xFF)))
{
return true;
}
else
{
return false;
}
}
/**
* Check this is a UTF-16 text document.
*
* @param content the content to examine
* @return true if it is a XML document
* @throws IOException unable to read the content
*/
private boolean isUtf16Text( byte[] content ) throws IOException
{
if( content.length < 2 )
{
return false;
}
if( this.hasByteOrderMark( content ) )
{
// we should have plenty of 0x00 in a text file
int estimate = (content.length-2)/3;
if( this.count(content,(byte)0) > estimate )
{
return true;
}
}
return false;
}
/**
* Check various encondings to determine if "<?xml"
* and "?>" appears in the data.
*
* @param content the content to examine
* @return true if it is a XML document
* @throws IOException unable to read the content
*/
private boolean isXML( byte[] content ) throws IOException
{
if( content.length < 3 )
{
return false;
}
for( int i=0; i<ENCODINGS.length; i++ )
{
String currEncoding = ENCODINGS[i];
String temp = new String( content, currEncoding );
if( ( temp.indexOf("<?xml") >= 0 ) && ( temp.indexOf("?>") > 0 ) )
{
return true;
}
}
return false;
}
/**
* Check if this is a ZIP document
*
* @param content the content to examine
* @return true if it is a PDF document
*/
private boolean isZip( byte[] content )
{
if( content.length < 64 )
{
return false;
}
else
{
// A ZIP starts with Hex: "50 4B 03 04"
if( ( content[0] == (byte) 0x50 ) &&
( content[1] == (byte) 0x4B ) &&
( content[2] == (byte) 0x03 ) &&
( content[3] == (byte) 0x04 ) )
{
return true;
}
else
{
return false;
}
}
}
/**
* Check if this is a PDF document
*
* @param content the content to examine
* @return true if it is a PDF document
* @throws IOException unable to read the content
*/
private boolean isPDF(byte[] content) throws IOException
{
if( content.length < 64 )
{
return false;
}
else
{
// A PDF starts with HEX "25 50 44 46 2D 31 2E"
if( ( content[0] == (byte) 0x25 ) &&
( content[1] == (byte) 0x50 ) &&
( content[2] == (byte) 0x44 ) &&
( content[3] == (byte) 0x46 ) &&
( content[4] == (byte) 0x2D ) &&
( content[5] == (byte) 0x31 ) &&
( content[6] == (byte) 0x2E ) )
{
return true;
}
else
{
return false;
}
}
}
/**
* Is this an ASCII control character?
* @param ch the charcter
* @return true is this in an ASCII character
*/
private boolean isAsciiControl(char ch)
{
if( ( ch >= 0x0000 ) && ( ch <= 0x001F) )
{
return true;
}
else
{
return true;
}
}
}