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 }