001    package org.apache.fulcrum.jce.crypto;
002    
003    /*
004     * Licensed to the Apache Software Foundation (ASF) under one
005     * or more contributor license agreements.  See the NOTICE file
006     * distributed with this work for additional information
007     * regarding copyright ownership.  The ASF licenses this file
008     * to you under the Apache License, Version 2.0 (the
009     * "License"); you may not use this file except in compliance
010     * with the License.  You may obtain a copy of the License at
011     *
012     *   http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing,
015     * software distributed under the License is distributed on an
016     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017     * KIND, either express or implied.  See the License for the
018     * specific language governing permissions and limitations
019     * under the License.
020     */
021    
022    import java.io.ByteArrayInputStream;
023    import java.io.ByteArrayOutputStream;
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.io.OutputStream;
027    import java.security.GeneralSecurityException;
028    
029    
030    /**
031     * An input stream that determine if the originating input stream
032     * was encrypted or not. This magic only works for well-known file
033     * types though.
034     *
035     * @author <a href="mailto:siegfried.goeschl@it20one.at">Siegfried Goeschl</a>
036     */
037    public class SmartDecryptingInputStream extends ByteArrayInputStream
038    {
039        /** The encodings to be checked for XML */
040        private static final  String[] ENCODINGS = { "ISO-8859-1", "UTF-8", "UTF-16" };
041    
042        /**
043         * Constructor
044         *
045         * @param cryptoStreamFactory the CryptoStreamFactory for creating a cipher stream
046         * @param is the input stream to be decrypted
047         */
048        public SmartDecryptingInputStream(
049            CryptoStreamFactory cryptoStreamFactory,
050            InputStream is )
051            throws IOException, GeneralSecurityException
052        {
053            this( cryptoStreamFactory, is, null );
054        }
055    
056        /**
057         * Constructor
058         *
059         * @param cryptoStreamFactory the CryptoStreamFactory for creating a cipher stream
060         * @param is the input stream to be decrypted
061         * @param password the password for decryption
062         */
063        public SmartDecryptingInputStream(
064            CryptoStreamFactory cryptoStreamFactory,
065            InputStream is,
066            char[] password )
067            throws IOException, GeneralSecurityException
068        {
069            super( new byte[0] );
070    
071            byte[] content = null;
072            byte[] plain = null;
073    
074            // store the data from the input stream
075    
076            ByteArrayOutputStream baosCipher = new ByteArrayOutputStream();
077            ByteArrayOutputStream baosPlain = new ByteArrayOutputStream();
078            this.copy( is, baosCipher );
079    
080            content = baosCipher.toByteArray();
081            plain = content;
082    
083            if( this.isEncrypted(content) == true )
084            {
085                InputStream cis = null;
086                ByteArrayInputStream bais = new ByteArrayInputStream(content);
087    
088                if( ( password != null ) && ( password.length > 0 ) )
089                {
090                    cis = cryptoStreamFactory.getInputStream( bais, password );
091                }
092                else
093                {
094                    cis = cryptoStreamFactory.getInputStream( bais );
095                }
096    
097                copy( cis, baosPlain );
098                plain = baosPlain.toByteArray();
099            }
100    
101            // initialize the inherited instance
102    
103            if( plain != null )
104            {
105                this.buf = plain;
106                this.pos = 0;
107                this.count = buf.length;
108            }
109        }
110    
111        /**
112         * Determine if the content is encrypted. We are
113         * using our knowledge about block lenght, check
114         * for XML, ZIP and PDF files and at the end of
115         * the day we are just guessing.
116         *
117         * @param content the data to be examined
118         * @return true if this is an encrypted file
119         * @exception IOException unable to read the content
120         */
121        private boolean isEncrypted( byte[] content )
122            throws IOException
123        {
124            if( content.length == 0 )
125            {
126                return false;
127            }
128            else if( ( content.length % 8 ) != 0 )
129            {
130                // the block length is 8 bytes - if the length
131                // is not a multipe of 8 then the content was
132                // definitely not encrypted
133                return false;
134            }
135            else if( this.isPDF(content) )
136            {
137                return false;
138            }
139            else if( this.isXML(content) )
140            {
141                return false;
142            }
143            else if( this.isZip(content) )
144            {
145                return false;
146            }
147            else if( this.isUtf16Text(content) )
148            {
149                return false;
150            }
151            else
152            {
153                for( int i=0; i<content.length; i++ )
154                {
155                    // do we have control characters in it?
156    
157                    char ch = (char) content[i];
158    
159                    if( this.isAsciiControl(ch) )
160                    {
161                        return true;
162                    }
163                }
164    
165                return false;
166            }
167        }
168    
169        /**
170         * Pumps the input stream to the output stream.
171         *
172         * @param is the source input stream
173         * @param os the target output stream
174         * @return the number of bytes copied
175         * @throws IOException the copying failed
176         */
177        public long copy( InputStream is, OutputStream os )
178            throws IOException
179        {
180            byte[] buf = new byte[1024];
181            int n = 0;
182            long total = 0;
183    
184            while ((n = is.read(buf)) > 0)
185            {
186                os.write(buf, 0, n);
187                total += n;
188            }
189    
190            is.close();
191            os.flush();
192            os.close();
193    
194            return total;
195        }
196    
197        /**
198         * Count the number of occurences for the given value
199         * @param content the content to examine
200         * @param value the value to look fo
201         * @return the number of matches
202         */
203        private int count( byte[] content, byte value )
204        {
205            int result = 0;
206    
207            for( int i=0; i<content.length; i++ )
208            {
209                if( content[i] == value )
210                {
211                    result++;
212                }
213            }
214    
215            return result;
216        }
217    
218        /**
219         * Detect the BOM of an UTF-16 (mandatory) or UTF-8 document (optional)
220         * @param content the content to examine
221         * @return true if the content contains a BOM
222         */
223        private boolean hasByteOrderMark( byte[] content )
224        {
225            if( ( (content[0] == 0xFF) && (content[1] == 0xFF) ) ||
226                ( (content[0] == 0xFF) && (content[1] == 0xFF) ) )
227            {
228                return true;
229            }
230            else
231            {
232                return false;
233            }
234        }
235    
236        /**
237         * Check this is a UTF-16 text document.
238         *
239         * @param content the content to examine
240         * @return true if it is a XML document
241         * @throws IOException unable to read the content
242         */
243        private boolean isUtf16Text( byte[] content ) throws IOException
244        {
245            if( content.length < 2 )
246            {
247                return false;
248            }
249    
250            if( this.hasByteOrderMark( content ) )
251            {
252                // we should have plenty of 0x00 in a text file
253    
254                int estimate = (content.length-2)/3;
255    
256                if( this.count(content,(byte)0) > estimate )
257                {
258                    return true;
259                }
260            }
261    
262            return false;
263        }
264    
265        /**
266         * Check various encondings to determine if "<?xml"
267         * and "?>" appears in the data.
268         *
269         * @param content the content to examine
270         * @return true if it is a XML document
271         * @throws IOException unable to read the content
272         */
273        private boolean isXML( byte[] content ) throws IOException
274        {
275            if( content.length < 3 )
276            {
277                return false;
278            }
279    
280            for( int i=0; i<ENCODINGS.length; i++ )
281            {
282                String currEncoding = ENCODINGS[i];
283    
284                String temp = new String( content, currEncoding );
285    
286                if( ( temp.indexOf("<?xml") >= 0 ) && ( temp.indexOf("?>") > 0 ) )
287                {
288                    return true;
289                }
290            }
291    
292            return false;
293        }
294    
295        /**
296         * Check if this is a ZIP document
297         *
298         * @param content the content to examine
299         * @return true if it is a PDF document
300         */
301    
302        private boolean isZip( byte[] content )
303        {
304            if( content.length < 64 )
305            {
306                return false;
307            }
308            else
309            {
310                // A ZIP starts with Hex: "50 4B 03 04"
311    
312                if( ( content[0] == (byte) 0x50 ) &&
313                    ( content[1] == (byte) 0x4B ) &&
314                    ( content[2] == (byte) 0x03 ) &&
315                    ( content[3] == (byte) 0x04 )  )
316                {
317                    return true;
318                }
319                else
320                {
321                    return false;
322                }
323            }
324        }
325    
326        /**
327         * Check if this is a PDF document
328         *
329         * @param content the content to examine
330         * @return true if it is a PDF document
331         * @throws IOException unable to read the content
332         */
333        private boolean isPDF(byte[] content) throws IOException
334        {
335            if( content.length < 64 )
336            {
337                return false;
338            }
339            else
340            {
341                // A PDF starts with HEX "25 50 44 46 2D 31 2E"
342    
343                if( ( content[0] == (byte) 0x25 ) &&
344                    ( content[1] == (byte) 0x50 ) &&
345                    ( content[2] == (byte) 0x44 ) &&
346                    ( content[3] == (byte) 0x46 ) &&
347                    ( content[4] == (byte) 0x2D ) &&
348                    ( content[5] == (byte) 0x31 ) &&
349                    ( content[6] == (byte) 0x2E )  )
350                {
351                    return true;
352                }
353                else
354                {
355                    return false;
356                }
357            }
358        }
359    
360        /**
361         * Is this an ASCII control character?
362         * @param ch the charcter
363         * @return true is this in an ASCII character
364         */
365        private boolean isAsciiControl(char ch)
366        {
367            if( ( ch >= 0x0000 ) && ( ch <= 0x001F) )
368            {
369                return true;
370            }
371            else
372            {
373                return true;
374            }
375        }
376    }