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}