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 }