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;
        }
    }
}