001/* 002 * Copyright 2008 Vócali Sistemas Inteligentes 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package es.vocali.util; 017 018import java.io.BufferedInputStream; 019import java.io.BufferedOutputStream; 020import java.io.File; 021import java.io.FileInputStream; 022import java.io.FileOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.io.UnsupportedEncodingException; 027import java.net.NetworkInterface; 028import java.security.GeneralSecurityException; 029import java.security.InvalidKeyException; 030import java.security.MessageDigest; 031import java.security.SecureRandom; 032import java.util.Arrays; 033import java.util.Enumeration; 034 035import javax.crypto.Cipher; 036import javax.crypto.Mac; 037import javax.crypto.spec.IvParameterSpec; 038import javax.crypto.spec.SecretKeySpec; 039 040/** 041 * This class provides methods to encrypt and decrypt files using 042 * <a href="http://www.aescrypt.com/aes_file_format.html">aescrypt file format</a>, 043 * version 1 or 2. 044 * <p> 045 * Requires Java 6 and <a href="http://java.sun.com/javase/downloads/index.jsp">Java 046 * Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files</a>. 047 * <p> 048 * Thread-safety and sharing: this class is not thread-safe.<br> 049 * <tt>AESCrypt</tt> objects can be used as Commands (create, use once and dispose), 050 * or reused to perform multiple operations (not concurrently though). 051 * 052 * @author Vócali Sistemas Inteligentes 053 */ 054public class AESCrypt { 055 private static final String JCE_EXCEPTION_MESSAGE = "Please make sure " 056 + "\"Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files\" " 057 + "(http://java.sun.com/javase/downloads/index.jsp) is installed on your JRE."; 058 private static final String RANDOM_ALG = "SHA1PRNG"; 059 public static final String DIGEST_ALG_256 = "SHA-256"; // SHA-512 060 public static final String DIGEST_ALG_512 = "SHA-512"; // SHA-512 061 public static final String DIGEST_ALG_1024 = "SHA-1024"; // SHA-512 062 private static final String HMAC_ALG = "HmacSHA256"; 063 private static final String CRYPT_ALG = "AES"; 064 private static final String CRYPT_TRANS = "AES/CBC/NoPadding"; //AES/CBC/PKCS7 065 private static final byte[] DEFAULT_MAC = 066 {0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xab, (byte) 0xcd, (byte) 0xef}; 067 private static final int KEY_SIZE = 32; 068 private static final int BLOCK_SIZE = 16; 069 private static final int SHA_SIZE = 32; 070 071 private final boolean DEBUG; 072 private byte[] password; 073 private Cipher cipher; 074 private Mac hmac; 075 private SecureRandom random; 076 private MessageDigest digest; 077 private IvParameterSpec ivSpec1; 078 private SecretKeySpec aesKey1; 079 private IvParameterSpec ivSpec2; 080 private SecretKeySpec aesKey2; 081 private String disgestAlg = DIGEST_ALG_256; 082 083 084 /******************* 085 * PRIVATE METHODS * 086 *******************/ 087 088 089 /** 090 * Prints a debug message on standard output if DEBUG mode is turned on. 091 */ 092 protected void debug(String message) { 093 if (DEBUG) { 094 System.out.println("[DEBUG] " + message); 095 } 096 } 097 098 099 /** 100 * Prints a debug message on standard output if DEBUG mode is turned on. 101 */ 102 protected void debug(String message, byte[] bytes) { 103 if (DEBUG) { 104 StringBuilder buffer = new StringBuilder("[DEBUG] "); 105 buffer.append(message); 106 buffer.append("["); 107 for (int i = 0; i < bytes.length; i++) { 108 buffer.append(bytes[i]); 109 buffer.append(i < bytes.length - 1 ? ", " : "]"); 110 } 111 System.out.println(buffer.toString()); 112 } 113 } 114 115 116 /** 117 * Generates a pseudo-random byte array. 118 * @return pseudo-random byte array of <tt>len</tt> bytes. 119 */ 120 protected byte[] generateRandomBytes(int len) { 121 byte[] bytes = new byte[len]; 122 random.nextBytes(bytes); 123 return bytes; 124 } 125 126 127 /** 128 * SHA256 digest over given byte array and random bytes.<br> 129 * <tt>bytes.length</tt> * <tt>num</tt> random bytes are added to the digest. 130 * <p> 131 * The generated hash is saved back to the original byte array.<br> 132 * Maximum array size is {@link #SHA_SIZE} bytes. 133 */ 134 protected void digestRandomBytes(byte[] bytes, int num) { 135 assert bytes.length <= SHA_SIZE; 136 137 digest.reset(); 138 digest.update(bytes); 139 for (int i = 0; i < num; i++) { 140 random.nextBytes(bytes); 141 digest.update(bytes); 142 } 143 System.arraycopy(digest.digest(), 0, bytes, 0, bytes.length); 144 } 145 146 147 /** 148 * Generates a pseudo-random IV based on time and this computer's MAC. 149 * <p> 150 * This IV is used to crypt IV 2 and AES key 2 in the file. 151 * @return IV. 152 */ 153 protected byte[] generateIv1() { 154 byte[] iv = new byte[BLOCK_SIZE]; 155 long time = System.currentTimeMillis(); 156 byte[] mac = null; 157 try { 158 Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces(); 159 while (mac == null && ifaces.hasMoreElements()) { 160 mac = ifaces.nextElement().getHardwareAddress(); 161 } 162 } catch (Exception e) { 163 // Ignore. 164 } 165 if (mac == null) { 166 mac = DEFAULT_MAC; 167 } 168 169 for (int i = 0; i < 8; i++) { 170 iv[i] = (byte) (time >> (i * 8)); 171 } 172 System.arraycopy(mac, 0, iv, 8, mac.length); 173 digestRandomBytes(iv, 256); 174 return iv; 175 } 176 177 178 /** 179 * Generates an AES key starting with an IV and applying the supplied user password. 180 * <p> 181 * This AES key is used to crypt IV 2 and AES key 2. 182 * @return AES key of {@link #KEY_SIZE} bytes. 183 */ 184 protected byte[] generateAESKey1(byte[] iv, byte[] password) { 185 byte[] aesKey = new byte[KEY_SIZE]; 186 System.arraycopy(iv, 0, aesKey, 0, iv.length); 187 for (int i = 0; i < 8192; i++) { 188 digest.reset(); 189 digest.update(aesKey); 190 digest.update(password); 191 aesKey = digest.digest(); 192 } 193 return aesKey; 194 } 195 196 197 /** 198 * Generates the random IV used to crypt file contents. 199 * @return IV 2. 200 */ 201 protected byte[] generateIV2() { 202 byte[] iv = generateRandomBytes(BLOCK_SIZE); 203 digestRandomBytes(iv, 256); 204 return iv; 205 } 206 207 208 /** 209 * Generates the random AES key used to crypt file contents. 210 * @return AES key of {@link #KEY_SIZE} bytes. 211 */ 212 protected byte[] generateAESKey2() { 213 byte[] aesKey = generateRandomBytes(KEY_SIZE); 214 digestRandomBytes(aesKey, 32); 215 return aesKey; 216 } 217 218 219 /** 220 * Utility method to read bytes from a stream until the given array is fully filled. 221 * @throws IOException if the array can't be filled. 222 */ 223 protected void readBytes(InputStream in, byte[] bytes) throws IOException { 224 if (in.read(bytes) != bytes.length) { 225 throw new IOException("Unexpected end of file"); 226 } 227 } 228 229 230 /************** 231 * PUBLIC API * 232 **************/ 233 234 235 /** 236 * Builds an object to encrypt or decrypt files with the given password. 237 * @throws GeneralSecurityException if the platform does not support the required cryptographic methods. 238 * @throws UnsupportedEncodingException if UTF-16 encoding is not supported. 239 */ 240 public AESCrypt(String password) throws GeneralSecurityException, UnsupportedEncodingException { 241 this(false, password); 242 } 243 244 245 /** 246 * Builds an object to encrypt or decrypt files with the given password. 247 * @throws GeneralSecurityException if the platform does not support the required cryptographic methods. 248 * @throws UnsupportedEncodingException if UTF-16 encoding is not supported. 249 */ 250 public AESCrypt(boolean debug, String password) throws GeneralSecurityException, UnsupportedEncodingException { 251 this(false, password, DIGEST_ALG_256); 252 } 253 254 255 /** 256 * Builds an object to encrypt or decrypt files with the given password. 257 * @throws GeneralSecurityException if the platform does not support the required cryptographic methods. 258 * @throws UnsupportedEncodingException if UTF-16 encoding is not supported. 259 */ 260 public AESCrypt(boolean debug, String password, String digestSize) throws GeneralSecurityException, UnsupportedEncodingException { 261 disgestAlg = digestSize; 262 try { 263 DEBUG = debug; 264 setPassword(password); 265 random = SecureRandom.getInstance(RANDOM_ALG); 266 digest = MessageDigest.getInstance(disgestAlg); 267 cipher = Cipher.getInstance(CRYPT_TRANS); 268 hmac = Mac.getInstance(HMAC_ALG); 269 } catch (GeneralSecurityException e) { 270 throw new GeneralSecurityException(JCE_EXCEPTION_MESSAGE, e); 271 } 272 } 273 274 275 /** 276 * Changes the password this object uses to encrypt and decrypt. 277 * @throws UnsupportedEncodingException if UTF-16 encoding is not supported. 278 */ 279 public void setPassword(String password) throws UnsupportedEncodingException { 280 this.password = password.getBytes("UTF-16LE"); 281 debug("Using password: ", this.password); 282 } 283 284 285 /** 286 * The file at <tt>fromPath</tt> is encrypted and saved at <tt>toPath</tt> location. 287 * <p> 288 * <tt>version</tt> can be either 1 or 2. 289 * @throws IOException when there are I/O errors. 290 * @throws GeneralSecurityException if the platform does not support the required cryptographic methods. 291 */ 292 public void encrypt(int version, String fromPath, String toPath) 293 throws IOException, GeneralSecurityException { 294 InputStream in = null; 295 OutputStream out = null; 296 try { 297 in = new BufferedInputStream(new FileInputStream(fromPath)); 298 debug("Opened for reading: " + fromPath); 299 out = new BufferedOutputStream(new FileOutputStream(toPath)); 300 debug("Opened for writing: " + toPath); 301 302 encrypt(version, in, out); 303 } finally { 304 if (in != null) { 305 in.close(); 306 } 307 if (out != null) { 308 out.close(); 309 } 310 } 311 } 312 313 /** 314 * The input stream is encrypted and saved to the output stream. 315 * <p> 316 * <tt>version</tt> can be either 1 or 2.<br> 317 * None of the streams are closed. 318 * @throws IOException when there are I/O errors. 319 * @throws GeneralSecurityException if the platform does not support the required cryptographic methods. 320 */ 321 public void encrypt(int version, InputStream in, OutputStream out) 322 throws IOException, GeneralSecurityException { 323 try { 324 byte[] text = null; 325 326 ivSpec1 = new IvParameterSpec(generateIv1()); 327 aesKey1 = new SecretKeySpec(generateAESKey1(ivSpec1.getIV(), password), CRYPT_ALG); 328 ivSpec2 = new IvParameterSpec(generateIV2()); 329 aesKey2 = new SecretKeySpec(generateAESKey2(), CRYPT_ALG); 330 debug("IV1: ", ivSpec1.getIV()); 331 debug("AES1: ", aesKey1.getEncoded()); 332 debug("IV2: ", ivSpec2.getIV()); 333 debug("AES2: ", aesKey2.getEncoded()); 334 335 out.write("AES".getBytes("UTF-8")); // Heading. 336 out.write(version); // Version. 337 out.write(0); // Reserved. 338 if (version == 2) { // No extensions. 339 out.write(0); 340 out.write(0); 341 } 342 out.write(ivSpec1.getIV()); // Initialization Vector. 343 344 text = new byte[BLOCK_SIZE + KEY_SIZE]; 345 cipher.init(Cipher.ENCRYPT_MODE, aesKey1, ivSpec1); 346 cipher.update(ivSpec2.getIV(), 0, BLOCK_SIZE, text); 347 cipher.doFinal(aesKey2.getEncoded(), 0, KEY_SIZE, text, BLOCK_SIZE); 348 out.write(text); // Crypted IV and key. 349 debug("IV2 + AES2 ciphertext: ", text); 350 351 hmac.init(new SecretKeySpec(aesKey1.getEncoded(), HMAC_ALG)); 352 text = hmac.doFinal(text); 353 out.write(text); // HMAC from previous cyphertext. 354 debug("HMAC1: ", text); 355 356 cipher.init(Cipher.ENCRYPT_MODE, aesKey2, ivSpec2); 357 hmac.init(new SecretKeySpec(aesKey2.getEncoded(), HMAC_ALG)); 358 text = new byte[BLOCK_SIZE]; 359 int len, last = 0; 360 while ((len = in.read(text)) > 0) { 361 cipher.update(text, 0, BLOCK_SIZE, text); 362 hmac.update(text); 363 out.write(text); // Crypted file data block. 364 last = len; 365 } 366 last &= 0x0f; 367 out.write(last); // Last block size mod 16. 368 debug("Last block size mod 16: " + last); 369 370 text = hmac.doFinal(); 371 out.write(text); // HMAC from previous cyphertext. 372 debug("HMAC2: ", text); 373 } catch (InvalidKeyException e) { 374 throw new GeneralSecurityException(JCE_EXCEPTION_MESSAGE, e); 375 } 376 } 377 378 379 /** 380 * The file at <tt>fromPath</tt> is decrypted and saved at <tt>toPath</tt> location. 381 * <p> 382 * The input file can be encrypted using version 1 or 2 of aescrypt.<br> 383 * @throws IOException when there are I/O errors. 384 * @throws GeneralSecurityException if the platform does not support the required cryptographic methods. 385 */ 386 public void decrypt(String fromPath, String toPath) 387 throws IOException, GeneralSecurityException { 388 InputStream in = null; 389 OutputStream out = null; 390 try { 391 in = new BufferedInputStream(new FileInputStream(fromPath)); 392 debug("Opened for reading: " + fromPath); 393 out = new BufferedOutputStream(new FileOutputStream(toPath)); 394 debug("Opened for writing: " + toPath); 395 396 decrypt(new File(fromPath).length(), in, out); 397 } finally { 398 if (in != null) { 399 in.close(); 400 } 401 if (out != null) { 402 out.close(); 403 } 404 } 405 } 406 407 408 /** 409 * The input stream is decrypted and saved to the output stream. 410 * <p> 411 * The input file size is needed in advance.<br> 412 * The input stream can be encrypted using version 1 or 2 of aescrypt.<br> 413 * None of the streams are closed. 414 * @throws IOException when there are I/O errors. 415 * @throws GeneralSecurityException if the platform does not support the required cryptographic methods. 416 */ 417 public void decrypt(long inSize, InputStream in, OutputStream out) 418 throws IOException, GeneralSecurityException { 419 try { 420 byte[] text = null, backup = null; 421 long total = 3 + 1 + 1 + BLOCK_SIZE + BLOCK_SIZE + KEY_SIZE + SHA_SIZE + 1 + SHA_SIZE; 422 int version; 423 424 text = new byte[3]; 425 readBytes(in, text); // Heading. 426 if (!new String(text, "UTF-8").equals("AES")) { 427 throw new IOException("Invalid file header"); 428 } 429 430 version = in.read(); // Version. 431 if (version < 1 || version > 2) { 432 throw new IOException("Unsupported version number: " + version); 433 } 434 debug("Version: " + version); 435 436 in.read(); // Reserved. 437 438 if (version == 2) { // Extensions. 439 text = new byte[2]; 440 int len; 441 do { 442 readBytes(in, text); 443 len = ((0xff & (int) text[0]) << 8) | (0xff & (int) text[1]); 444 if (in.skip(len) != len) { 445 throw new IOException("Unexpected end of extension"); 446 } 447 total += 2 + len; 448 debug("Skipped extension sized: " + len); 449 } while (len != 0); 450 } 451 452 text = new byte[BLOCK_SIZE]; 453 readBytes(in, text); // Initialization Vector. 454 ivSpec1 = new IvParameterSpec(text); 455 aesKey1 = new SecretKeySpec(generateAESKey1(ivSpec1.getIV(), password), CRYPT_ALG); 456 debug("IV1: ", ivSpec1.getIV()); 457 debug("AES1: ", aesKey1.getEncoded()); 458 459 cipher.init(Cipher.DECRYPT_MODE, aesKey1, ivSpec1); 460 backup = new byte[BLOCK_SIZE + KEY_SIZE]; 461 readBytes(in, backup); // IV and key to decrypt file contents. 462 debug("IV2 + AES2 ciphertext: ", backup); 463 text = cipher.doFinal(backup); 464 ivSpec2 = new IvParameterSpec(text, 0, BLOCK_SIZE); 465 aesKey2 = new SecretKeySpec(text, BLOCK_SIZE, KEY_SIZE, CRYPT_ALG); 466 debug("IV2: ", ivSpec2.getIV()); 467 debug("AES2: ", aesKey2.getEncoded()); 468 469 hmac.init(new SecretKeySpec(aesKey1.getEncoded(), HMAC_ALG)); 470 backup = hmac.doFinal(backup); 471 text = new byte[SHA_SIZE]; 472 readBytes(in, text); // HMAC and authenticity test. 473 if (!Arrays.equals(backup, text)) { 474 throw new IOException("Message has been altered or password incorrect"); 475 } 476 debug("HMAC1: ", text); 477 478 total = inSize - total; // Payload size. 479 if (total % BLOCK_SIZE != 0) { 480 throw new IOException("Input file is corrupt"); 481 } 482 if (total == 0) { // Hack: empty files won't enter block-processing for-loop below. 483 in.read(); // Skip last block size mod 16. 484 } 485 debug("Payload size: " + total); 486 487 cipher.init(Cipher.DECRYPT_MODE, aesKey2, ivSpec2); 488 hmac.init(new SecretKeySpec(aesKey2.getEncoded(), HMAC_ALG)); 489 backup = new byte[BLOCK_SIZE]; 490 text = new byte[BLOCK_SIZE]; 491 for (int block = (int) (total / BLOCK_SIZE); block > 0; block--) { 492 int len = BLOCK_SIZE; 493 if (in.read(backup, 0, len) != len) { // Cyphertext block. 494 throw new IOException("Unexpected end of file contents"); 495 } 496 cipher.update(backup, 0, len, text); 497 hmac.update(backup, 0, len); 498 if (block == 1) { 499 int last = in.read(); // Last block size mod 16. 500 debug("Last block size mod 16: " + last); 501 len = (last > 0 ? last : BLOCK_SIZE); 502 } 503 out.write(text, 0, len); 504 } 505 out.write(cipher.doFinal()); 506 507 backup = hmac.doFinal(); 508 text = new byte[SHA_SIZE]; 509 readBytes(in, text); // HMAC and authenticity test. 510 if (!Arrays.equals(backup, text)) { 511 throw new IOException("Message has been altered or password incorrect"); 512 } 513 debug("HMAC2: ", text); 514 } catch (InvalidKeyException e) { 515 throw new GeneralSecurityException(JCE_EXCEPTION_MESSAGE, e); 516 } 517 } 518 519 520 public static void main(String[] args) { 521 try { 522 if (args.length < 4) { 523 System.out.println("AESCrypt e|d password fromPath toPath"); 524 return; 525 } 526 AESCrypt aes = new AESCrypt(true, args[1]); 527 switch (args[0].charAt(0)) { 528 case 'e': 529 aes.encrypt(2, args[2], args[3]); 530 break; 531 case 'd': 532 aes.decrypt(args[2], args[3]); 533 break; 534 default: 535 System.out.println("Invalid operation: must be (e)ncrypt or (d)ecrypt."); 536 return; 537 } 538 } catch (Exception e) { 539 e.printStackTrace(); 540 } 541 } 542}