View Javadoc
1   package org.apache.fulcrum.jce.crypto;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.security.GeneralSecurityException;
28  
29  
30  /**
31   * An input stream that determine if the originating input stream
32   * was encrypted or not. This magic only works for well-known file
33   * types though.
34   *
35   * @author <a href="mailto:siegfried.goeschl@it20one.at">Siegfried Goeschl</a>
36   */
37  public class SmartDecryptingInputStream extends ByteArrayInputStream
38  {
39      /** The encodings to be checked for XML */
40      private static final  String[] ENCODINGS = { "ISO-8859-1", "UTF-8", "UTF-16" };
41  
42      /**
43       * Constructor
44       *
45       * @param cryptoStreamFactory the CryptoStreamFactory for creating a cipher stream
46       * @param is the input stream to be decrypted
47       * @throws IOException if file not found
48       * @throws GeneralSecurityException if security check fails
49       */
50      public SmartDecryptingInputStream(
51          CryptoStreamFactory cryptoStreamFactory,
52          InputStream is )
53          throws IOException, GeneralSecurityException
54      {
55          this( cryptoStreamFactory, is, null );
56      }
57  
58      /**
59       * Constructor
60       *
61       * @param cryptoStreamFactory the CryptoStreamFactory for creating a cipher stream
62       * @param is the input stream to be decrypted
63       * @param password the password for decryption
64       * 
65       * @throws IOException if file not found
66       * @throws GeneralSecurityException if security check fails 
67       */
68      public SmartDecryptingInputStream(
69          CryptoStreamFactory cryptoStreamFactory,
70          InputStream is,
71          char[] password )
72          throws IOException, GeneralSecurityException
73      {
74          super( new byte[0] );
75  
76          byte[] content = null;
77          byte[] plain = null;
78  
79          // store the data from the input stream
80  
81          ByteArrayOutputStream baosCipher = new ByteArrayOutputStream();
82          ByteArrayOutputStream baosPlain = new ByteArrayOutputStream();
83          this.copy( is, baosCipher );
84  
85          content = baosCipher.toByteArray();
86          plain = content;
87  
88          if( this.isEncrypted(content) == true )
89          {
90              InputStream cis = null;
91              ByteArrayInputStream bais = new ByteArrayInputStream(content);
92  
93              if( ( password != null ) && ( password.length > 0 ) )
94              {
95                  cis = cryptoStreamFactory.getInputStream( bais, password );
96              }
97              else
98              {
99                  cis = cryptoStreamFactory.getInputStream( bais );
100             }
101 
102             copy( cis, baosPlain );
103             plain = baosPlain.toByteArray();
104         }
105 
106         // initialize the inherited instance
107 
108         if( plain != null )
109         {
110             this.buf = plain;
111             this.pos = 0;
112             this.count = buf.length;
113         }
114     }
115 
116     /**
117      * Determine if the content is encrypted. We are
118      * using our knowledge about block length, check
119      * for XML, ZIP and PDF files and at the end of
120      * the day we are just guessing.
121      *
122      * @param content the data to be examined
123      * @return true if this is an encrypted file
124      * @throws IOException unable to read the content
125      */
126     private boolean isEncrypted( byte[] content )
127         throws IOException
128     {
129         if( content.length == 0 )
130         {
131             return false;
132         }
133         else if( ( content.length % 8 ) != 0 )
134         {
135             // the block length is 8 bytes - if the length
136             // is not a multipe of 8 then the content was
137             // definitely not encrypted
138             return false;
139         }
140         else if( this.isPDF(content) )
141         {
142             return false;
143         }
144         else if( this.isXML(content) )
145         {
146             return false;
147         }
148         else if( this.isZip(content) )
149         {
150             return false;
151         }
152         else if( this.isUtf16Text(content) )
153         {
154             return false;
155         }
156         else
157         {
158             for( int i=0; i<content.length; i++ )
159             {
160                 // do we have control characters in it?
161 
162                 char ch = (char) content[i];
163 
164                 if( this.isAsciiControl(ch) )
165                 {
166                     return true;
167                 }
168             }
169 
170             return false;
171         }
172     }
173 
174     /**
175      * Pumps the input stream to the output stream.
176      *
177      * @param is the source input stream
178      * @param os the target output stream
179      * @return the number of bytes copied
180      * @throws IOException the copying failed
181      */
182     public long copy( InputStream is, OutputStream os )
183         throws IOException
184     {
185         byte[] buf = new byte[1024];
186         int n = 0;
187         long total = 0;
188 
189         while ((n = is.read(buf)) > 0)
190         {
191             os.write(buf, 0, n);
192             total += n;
193         }
194 
195         is.close();
196         os.flush();
197         os.close();
198 
199         return total;
200     }
201 
202     /**
203      * Count the number of occurences for the given value
204      * @param content the content to examine
205      * @param value the value to look fo
206      * @return the number of matches
207      */
208     private int count( byte[] content, byte value )
209     {
210         int result = 0;
211 
212         for( int i=0; i<content.length; i++ )
213         {
214             if( content[i] == value )
215             {
216                 result++;
217             }
218         }
219 
220         return result;
221     }
222 
223     /**
224      * Detect the BOM of an UTF-16 (mandatory) or UTF-8 document (optional)
225      * @param content the content to examine
226      * @return true if the content contains a BOM
227      */
228     private boolean hasByteOrderMark( byte[] content )
229     {
230         // bytes ar always signed in java, ff is 255
231         // removes signed parts
232         int firstUnsigned = content[0] & 0xFF;
233         int second = content[1] & 0xFF;
234         if( ((firstUnsigned == 0xFF) && (second == 0xFE)) ||
235                 ((firstUnsigned == 0xFE) && (second == 0xFF)))
236         {
237             return true;
238         }
239         else
240         {
241             return false;
242         }
243     }
244 
245     /**
246      * Check this is a UTF-16 text document.
247      *
248      * @param content the content to examine
249      * @return true if it is a XML document
250      * @throws IOException unable to read the content
251      */
252     private boolean isUtf16Text( byte[] content ) throws IOException
253     {
254         if( content.length < 2 )
255         {
256             return false;
257         }
258 
259         if( this.hasByteOrderMark( content ) )
260         {
261             // we should have plenty of 0x00 in a text file
262 
263             int estimate = (content.length-2)/3;
264 
265             if( this.count(content,(byte)0) > estimate )
266             {
267                 return true;
268             }
269         }
270 
271         return false;
272     }
273 
274     /**
275      * Check various encondings to determine if "<?xml"
276      * and "?>" appears in the data.
277      *
278      * @param content the content to examine
279      * @return true if it is a XML document
280      * @throws IOException unable to read the content
281      */
282     private boolean isXML( byte[] content ) throws IOException
283     {
284         if( content.length < 3 )
285         {
286             return false;
287         }
288 
289         for( int i=0; i<ENCODINGS.length; i++ )
290         {
291             String currEncoding = ENCODINGS[i];
292 
293             String temp = new String( content, currEncoding );
294 
295             if( ( temp.indexOf("<?xml") >= 0 ) && ( temp.indexOf("?>") > 0 ) )
296             {
297                 return true;
298             }
299         }
300 
301         return false;
302     }
303 
304     /**
305      * Check if this is a ZIP document
306      *
307      * @param content the content to examine
308      * @return true if it is a PDF document
309      */
310 
311     private boolean isZip( byte[] content )
312     {
313         if( content.length < 64 )
314         {
315             return false;
316         }
317         else
318         {
319             // A ZIP starts with Hex: "50 4B 03 04"
320 
321             if( ( content[0] == (byte) 0x50 ) &&
322                 ( content[1] == (byte) 0x4B ) &&
323                 ( content[2] == (byte) 0x03 ) &&
324                 ( content[3] == (byte) 0x04 )  )
325             {
326                 return true;
327             }
328             else
329             {
330                 return false;
331             }
332         }
333     }
334 
335     /**
336      * Check if this is a PDF document
337      *
338      * @param content the content to examine
339      * @return true if it is a PDF document
340      * @throws IOException unable to read the content
341      */
342     private boolean isPDF(byte[] content) throws IOException
343     {
344         if( content.length < 64 )
345         {
346             return false;
347         }
348         else
349         {
350             // A PDF starts with HEX "25 50 44 46 2D 31 2E"
351 
352             if( ( content[0] == (byte) 0x25 ) &&
353                 ( content[1] == (byte) 0x50 ) &&
354                 ( content[2] == (byte) 0x44 ) &&
355                 ( content[3] == (byte) 0x46 ) &&
356                 ( content[4] == (byte) 0x2D ) &&
357                 ( content[5] == (byte) 0x31 ) &&
358                 ( content[6] == (byte) 0x2E )  )
359             {
360                 return true;
361             }
362             else
363             {
364                 return false;
365             }
366         }
367     }
368 
369     /**
370      * Is this an ASCII control character?
371      * @param ch the charcter
372      * @return true is this in an ASCII character
373      */
374     private boolean isAsciiControl(char ch)
375     {
376         if( ( ch >= 0x0000 ) && ( ch <= 0x001F) )
377         {
378             return true;
379         }
380         else
381         {
382             return true;
383         }
384     }
385 }