/*
 * Copyright (C) 2007-2009 KenD00
 * 
 * This file is part of DumpHD.
 * 
 * DumpHD is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package dumphd.aacs;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.Semaphore;
import java.util.zip.CRC32;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import dumphd.bdplus.SubTable;
import dumphd.core.DiscSet;
import dumphd.core.KeyData;
import dumphd.core.KeyDataFile;
import dumphd.core.MultiplexedArf;
import dumphd.util.*;

/**
 * This class handles all the AACS-Decryption stuff for HD-DVDs / BluRays.
 * 
 * @author KenD00
 */
public class AACSDecrypter {

   public final static String idStringAACS = "AACS";
   public final static String idStringTKF = "DVD_HD_V_TKF";
   public final static int maxTKFVersion = 0;
   public final static long TKFFileSize = 2480;
   public final static HashMap<Integer, Integer> BD_APPLICATION_TYPES = new HashMap<Integer, Integer>();

   public final static byte[] cbc_iv = { (byte)0x0B, (byte)0xA0, (byte)0xF8, (byte)0xDD, (byte)0xFE, (byte)0xA6, (byte)0x1F, (byte)0xB3, (byte)0xD8, (byte)0xDF, (byte)0x9F, (byte)0x56, (byte)0x6A, (byte)0x05, (byte)0x0F, (byte)0x78 };
   public final static IvParameterSpec cbc_iv_spec = new IvParameterSpec(cbc_iv);
   public final static byte[] h0 = { (byte)0x2D, (byte)0xC2, (byte)0xDF, (byte)0x39, (byte)0x42, (byte)0x03, (byte)0x21, (byte)0xD0, (byte)0xCE, (byte)0xF1, (byte)0xFE, (byte)0x23, (byte)0x74, (byte)0x02, (byte)0x9D, (byte)0x95 };

   public final static int decrypterThreads = 2;
   // queueLength must be divideable by 3 or the TS Aligned Units won't fit in!!!
   public final static int queueLength = 30;

   private MessagePrinter out = null;
   private KeyDataFile kdf = null;
   private byte[] packBuffer = new byte[decrypterThreads * queueLength * PESPack.PACK_LENGTH];
   private PackDecrypter packDecrypter = null;
   private AACSKeys akl = null;

   private CRC32 crc32Calc = new CRC32();
   private Cipher aes_128d = null;
   private Cipher aes_128cbcd = null;
   private MessageDigest sha1 = null;

   private DiscSet ds = null;


   static {
      // Initialize BD_APPLICATION_TYPES
      BD_APPLICATION_TYPES.put(DiscSet.BD_MV, 0x01);
      BD_APPLICATION_TYPES.put(DiscSet.BDR_MV, 0x03);
      BD_APPLICATION_TYPES.put(DiscSet.BDR_AV, 0x02);
   }


   /**
    * Checks if the given ByteSource is an AACS-Protected ARF.
    * Resets the ByteSource position after the check to 0.
    * 
    * @param ds ByteSource to check
    * @return If true, the given ByteSource is an AACS-Protected ARF, false if not
    * @throws IOException An I/O error occurred 
    */
   public static boolean isArfEncrypted(ByteSource ds) throws IOException {
      // Better don't use Utils.buffer here, its highly possible that its already in use by the caller
      byte[] data = new byte[idStringAACS.length()];
      Arrays.fill(data, (byte)0);
      ds.read(data, 0, idStringAACS.length());
      ds.setPosition(0);
      if (new String(data, 0, idStringAACS.length(), "US-ASCII").equals(idStringAACS)) {
         return true;
      } else {
         return false;
      }
   }


   /**
    * Creates a new AACSDecrypter.
    * 
    * @param mp MessagePrinter used for textual output
    * @param kdf Key Data File to use
    * @throws AACSException
    */
   public AACSDecrypter(MessagePrinter mp, KeyDataFile kdf) throws AACSException {
      this.out = mp;
      this.kdf = kdf;
      // Create required crypo objects
      out.print("Initializing AACS... ");
      try {
         aes_128d = Cipher.getInstance("AES/ECB/NOPADDING");
         aes_128cbcd = Cipher.getInstance("AES/CBC/NOPADDING");
         sha1 = MessageDigest.getInstance("SHA-1");
         packDecrypter = new PackDecrypter(decrypterThreads, queueLength);
      }
      catch (GeneralSecurityException e) {
         out.println("FAILED");
         throw new AACSException(e.getMessage(), e);
      }
      out.println("OK");
      // Try to load the aacskeys library
      out.print("Loading aacskeys library... ");
      try {
         akl = new AACSKeys(mp);
         out.println("OK");
         out.println(akl.getVersionString());
      }
      catch (AACSException e) {
         out.println("FAILED");
         out.println(e.getMessage());
         out.println("Direct key retrieval disabled, only keys from the database will be used");
      }
      // TODO: Should this class hold the KEYDB and open it here? But when should it get closed? Should this class have a close-method?
   }

   /**
    * Initializes this AACSDecrypter to be ready to decrypt the given DiscSet.
    * 
    * @param ds The DiscSet for which this AACSDecrypter should be prepared for
    */
   public void init(DiscSet ds) throws AACSException {
      if (ds.keyData !=null) {
         this.ds = ds;
      } else throw new AACSException("DiscSet contains no key data");
   }

   /**
    * Identifies the given DiscSet. Sets the DiscID and adds the keys found in the database, if any.
    * 
    * @param ds The DiscSet to identify
    * @throws AACSException An error occurred
    */
   public void identifyDisc(DiscSet ds) throws AACSException {
      ds.selected = false;
      out.print("Identifying disc... ");
      if (ds.aacsDir != null) {
         // AACS directory present, determine ID file by disc type
         File idFile = null;
         switch (ds.contentType) {
         case DiscSet.HD_STANDARD_V:
            idFile = new File(ds.aacsDir, "VTKF.AACS");
            break;
         case DiscSet.HD_STANDARD_A:
            idFile = new File(ds.aacsDir, "ATKF.AACS");
            break;
         case DiscSet.HD_ADVANCED_V: {
            LinkedList<String> idFileList = new LinkedList<String>(); 
            Utils.scanForFilenames(ds.aacsDir, ds.aacsDir.getPath(), idFileList, false, new SimpleFilenameFilter("VTKF[0-9]{3}.AACS", false, false), true);
            if (!idFileList.isEmpty()) {
               Collections.sort(idFileList);
               idFile = new File(idFileList.getFirst());
            }
            break;
         }
         case DiscSet.HD_ADVANCED_A: {
            LinkedList<String> idFileList = new LinkedList<String>(); 
            Utils.scanForFilenames(ds.aacsDir, ds.aacsDir.getPath(), idFileList, false, new SimpleFilenameFilter("ATKF[0-9]{3}.AACS", false, false), true);
            if (!idFileList.isEmpty()) {
               Collections.sort(idFileList);
               idFile = new File(idFileList.getFirst());
            }
            break;
         }
         case DiscSet.BD_MV:
            idFile = new File(ds.aacsDir, "Unit_Key_RO.inf");
            break;
         case DiscSet.BDR_MV:
         case DiscSet.BDR_AV:
            idFile = new File(ds.aacsDir, "Unit_Key_RW.inf");
            break;
         default:
            // Unknown disc type, throw an exception
            out.println("FAILED");
         throw new AACSException("Unknown disc type: " + ds.contentType);
         }
         if (idFile.isFile()) {
            // ID file exists, calculate SHA-1 hash
            sha1.reset();
            FileSource idSource = null;
            try {
               idSource = new FileSource(idFile, FileSource.R_MODE);
               long remaining = idSource.size();
               while (remaining > 0) {
                  if (remaining > (long)Utils.buffer.length) {
                     if (idSource.read(Utils.buffer, 0, Utils.buffer.length) != Utils.buffer.length) {
                        // Not all requested data could be read
                        throw new IOException("Unexpected EOF");
                     }
                     sha1.update(Utils.buffer, 0, Utils.buffer.length);
                     remaining -= (long)Utils.buffer.length;
                  } else {
                     if (idSource.read(Utils.buffer, 0, (int)remaining) != (int)remaining) {
                        // Not all requested data could be read
                        throw new IOException("Unexpected EOF");
                     }
                     sha1.update(Utils.buffer, 0, (int)remaining);
                     remaining = 0;
                  }
               }
            }
            catch (IOException e) {
               out.println("FAILED");
               if (idSource != null) {
                  try {
                     idSource.close();
                  }
                  catch (IOException e2) {
                     // Ignore the exception
                  }
               }
               throw new AACSException(e.getMessage(), e);
            }
            byte[] discId = sha1.digest();
            out.println("OK");
            out.println("DiscID : " + Utils.toHexString(discId, 0, 20));
            out.println("Searching disc in key database...");
            try {
               ds.keyData = kdf.getKeyData(discId, 0);
               if (ds.keyData != null) {
                  out.println("Disc found in key database");
               }
            }
            catch (IOException e) {
               out.println(e.getMessage());
               ds.keyData = null;
            }
            if (ds.keyData == null) {
               ds.keyData = new KeyData(discId, 0);
               // If the aacskeys library is loaded, try to retrieve the keys from the disc
               if (akl != null) {
                  out.println("Disc not found in key database");
                  out.println("Retrieving keys from source... ");
                  try {
                     akl.getKeys(ds.srcDir.getPath(), ds.keyData);
                     out.println();
                     out.println("Keys successfully retrieved from source");
                     //TODO: Set title to non-null to allow editing of the title in the GUI, ugly, but the GUI otherwise does not know that there are no keys
                     if (ds.keyData.getTitle() == null) {
                        ds.keyData.setTitle("Movie Title");
                     }
                     // Check if the present keyData has a date and add it if not
                     if (ds.keyData.getDate() == null) {
                        try {
                           long lastModified = idFile.lastModified();
                           if (lastModified != 0L) {
                              ds.keyData.setDate(new Date(lastModified));
                           }
                        }
                        catch (SecurityException e) {
                           // Do nothing
                        }
                     }
                     out.println("Saving keys in key database...");
                     KeyData result = null;
                     try {
                        if (ds.bdplus) {
                           result = kdf.appendKeyData(ds.keyData, ds.isBluRay(), ds.isRecordable(), kdf.getSmartMode(ds.keyData) | KeyData.DATA_VIDBN);
                        } else {
                           result = kdf.appendKeyData(ds.keyData, ds.isBluRay(), ds.isRecordable());
                        }
                     }
                     catch (IOException e) {
                        out.println(e.getMessage());
                        result = null;
                     }
                     if (result != null) {
                        out.println("Keys saved in key database");
                     } else {
                        out.println("Failed saving keys in key database");
                     }
                  }
                  catch (AACSException e) {
                     out.println();
                     out.println(e.getMessage());
                     throw new AACSException("Failed retrieving keys from source");
                  }
               } else throw new AACSException("Disc not found in key database");
            } else {
               // Check if we are dealing with BD+ and the VID is present
               if (ds.bdplus && ds.keyData.getVid() == null) {
                  out.println("Disc is BD+ protected and the Volume ID is not present in the key database");
                  if (akl != null) {
                     out.println("Retrieving keys from source...");
                     try {
                        // FIXME?: aacskeys overwrites all present keys, is this OK?
                        akl.getKeys(ds.srcDir.getPath(), ds.keyData);
                        out.println();
                        out.println("Keys successfully retrieved from source");
                        //TODO: Set title to non-null to allow editing of the title in the GUI, ugly, but the GUI otherwise does not know that there are no keys
                        if (ds.keyData.getTitle() == null) {
                           ds.keyData.setTitle("Movie Title");
                        }
                        // Check if the present keyData has a date and add it if not
                        if (ds.keyData.getDate() == null) {
                           try {
                              long lastModified = idFile.lastModified();
                              if (lastModified != 0L) {
                                 ds.keyData.setDate(new Date(lastModified));
                              }
                           }
                           catch (SecurityException e) {
                              // Do nothing
                           }
                        }
                        out.println("Saving keys in key database...");
                        KeyData result = null;
                        try {
                           result = kdf.setKeyData(ds.keyData, ds.isBluRay(), ds.isRecordable(), true, kdf.getSmartMode(ds.keyData) | KeyData.DATA_VIDBN); 
                        }
                        catch (IOException e) {
                           out.println(e.getMessage());
                           result = null;
                        }
                        if (result != null) {
                           out.println("Keys saved in key database");
                        } else {
                           out.println("Failed saving keys in key database");
                        }
                     }
                     catch (AACSException e) {
                        out.println();
                        out.println(e.getMessage());
                        out.println("Warning! Failed retrieving keys from source");
                     }

                  } else {
                     out.println("Warning! Direct key retrieval disabled, cannot retrieve Volume ID");
                  }
               }
            }
            // Key data successfully retrieved, everything is OK
            ds.selected = true;
         } else {
            out.println("FAILED");
            throw new AACSException("DiscID file not found: " + idFile.getPath());
         }
      } else {
         out.println("FAILED");
         throw new AACSException("AACS directory not found");
      }
   }

   /**
    * Initializes the given DiscSet. Decrypts the required keys, if possible.
    * 
    *  TODO: Skip key decryption if TK's / UK's are present from the database, or prevent them from being overwritten
    *  
    * @param ds The DiscSet to initialize
    * @throws AACSException An error occurred
    */
   public void initDisc(DiscSet ds) throws AACSException {
      out.print("Processing disc AACS data... ");
      if (ds.keyData != null) {
         // This is just to determine if a newline should be printed if VUK / PAK's are present
         boolean calculatedPak = false;
         if (ds.keyData.pakCount() < 1) {
            calculatedPak = true;
            // No VUK / PAK's present, check if we can calculate them
            out.print("\nNo Volume Unique Key / Protected Area Keys present, calculating them... ");
            byte[] mek = ds.keyData.getMek();
            if (mek != null && ds.keyData.bnCount() > 0) {
               // Calculate all VUK / PAK's
               try {
                  SecretKey mekKey = new SecretKeySpec(mek, "AES");
                  Iterator<Integer> it = ds.keyData.bnIdx().iterator();
                  while (it.hasNext()) {
                     int index = it.next();
                     byte[] bn = ds.keyData.getBn(index);
                     System.arraycopy(bn, 0, Utils.buffer, 0, bn.length);
                     aes_128d.init(Cipher.DECRYPT_MODE, mekKey);
                     aes_128d.doFinal(Utils.buffer, 0, mek.length, Utils.buffer, 0);
                     // Do AES-G
                     for (int i = 0; i < mek.length; i++) {
                        Utils.buffer[i] = (byte)((byte)Utils.buffer[i] ^ (byte)bn[i]);
                     }
                     ds.keyData.setPak(index, Utils.buffer, 0);
                  }
                  out.println("OK");
               }
               catch (GeneralSecurityException e) {
                  out.println("FAILED");
                  out.println(e.getMessage());
               }
            } else {
               out.println("FAILED");
               out.println("Not enough data present to calculate a Volume Unique Key / Protected Area Keys");
            }
         }
         if (ds.keyData.pakCount() > 0) {
            // VUK / PAK's found, decrypt Title / CPS Unit Keys
            if (!calculatedPak) {
               out.println("\nVolume Unique Key / Protected Area Keys present, decrypting Title Keys / CPS Unit Keys... ");
            }
            out.println("Searching Title Key / CPS Unit Key Files... ");
            File keyFile = null;
            LinkedList<String> keyFilenames = new LinkedList<String>();
            switch (ds.contentType) {
            case DiscSet.HD_STANDARD_V:
               keyFile = new File(ds.aacsDir, "VTKF.AACS");
               if (keyFile.isFile()) {
                  keyFilenames.add(keyFile.getPath());
               }
               break;
            case DiscSet.HD_STANDARD_A:
               keyFile = new File(ds.aacsDir, "ATKF.AACS");
               if (keyFile.isFile()) {
                  keyFilenames.add(keyFile.getPath());
               }
               break;
            case DiscSet.HD_ADVANCED_V:
               Utils.scanForFilenames(ds.aacsDir, ds.aacsDir.getPath(), keyFilenames, false, new SimpleFilenameFilter("VTKF[0-9]{3}.AACS", false, false), false);
               if (!keyFilenames.isEmpty()) {
                  Collections.sort(keyFilenames);
               }
               break;
            case DiscSet.HD_ADVANCED_A:
               Utils.scanForFilenames(ds.aacsDir, ds.aacsDir.getPath(), keyFilenames, false, new SimpleFilenameFilter("ATKF[0-9]{3}.AACS", false, false), false);
               if (!keyFilenames.isEmpty()) {
                  Collections.sort(keyFilenames);
               }
               break;
            case DiscSet.BD_MV:
               keyFile = new File(ds.aacsDir, "Unit_Key_RO.inf");
               if (keyFile.isFile()) {
                  keyFilenames.add(keyFile.getPath());
               }
               break;
            case DiscSet.BDR_MV:
            case DiscSet.BDR_AV:
               keyFile = new File(ds.aacsDir, "Unit_Key_RW.inf");
               if (keyFile.isFile()) {
                  keyFilenames.add(keyFile.getPath());
               }
               break;
            default:
               // Unknown disc type, throw an exception
               out.println("FAILED");
            throw new AACSException("Unknown disc type: " + ds.contentType);
            }
            if (!keyFilenames.isEmpty()) {
               FileSource keySource = null;
               // Actually the VUK is the same as the first PAK, this way this code works for BD recordables too,
               // but it won't with HD-DVD recordables
               byte[] vuk = ds.keyData.getVuk();
               SecretKey vukKey = new SecretKeySpec(vuk, "AES");
               switch (ds.contentType) {
               case DiscSet.HD_STANDARD_V:
               case DiscSet.HD_STANDARD_A:
               case DiscSet.HD_ADVANCED_V:
               case DiscSet.HD_ADVANCED_A:
                  // Title Key Files found, decrypt them
                  Iterator<String> it = keyFilenames.iterator();
                  while (it.hasNext()) {
                     String fileName = it.next();
                     out.print("Decrypting " + fileName + "... ");
                     try {
                        keySource = new FileSource(new File(fileName), FileSource.R_MODE);
                        if (keySource.size() >= TKFFileSize) {
                           if (keySource.read(Utils.buffer, 0, (int)TKFFileSize) != (int)TKFFileSize) {
                              throw new IOException("Unexpected EOF");
                           }
                           if (new String(Utils.buffer, 0, idStringTKF.length(), "US-ASCII").equals(idStringTKF)) {
                              if (ByteArray.getVarLong(Utils.buffer, idStringTKF.length(), 4) == TKFFileSize) {
                                 if (ByteArray.getUShort(Utils.buffer, 32) <= maxTKFVersion) {
                                    int pos = 128;
                                    for (int i = 1; i <= 64; i++) {
                                       if ((ByteArray.getUByte(Utils.buffer, pos) & 0x80) == 0x80) {
                                          // Title Key valid, decrypt it
                                          aes_128d.init(Cipher.DECRYPT_MODE, vukKey);
                                          aes_128d.doFinal(Utils.buffer, pos + 4, vuk.length, Utils.buffer, 0);
                                          // TODO: Set only if not already present?
                                          ds.keyData.setTuk(i, Utils.buffer, 0);
                                       }
                                       pos += 36;
                                    }
                                    out.println("OK");
                                 } else throw new AACSException("Unsupported version");
                              } else throw new AACSException("Wrong file size stated in HD_VTKF_SIZE"); 
                           } else throw new AACSException("Wrong TKF_ID");
                        } else throw new AACSException("File too small to be a Title Key File");
                     }
                     catch (IOException e) {
                        out.println("FAILED");
                        out.println(e.getMessage());
                     }
                     catch (AACSException e2) {
                        out.println("FAILED");
                        out.println(e2.getMessage());
                     }
                     catch (GeneralSecurityException e3) {
                        out.println("FAILED");
                        out.println(e3.getMessage());
                     }
                     finally {
                        try {
                           keySource.close();
                        }
                        catch (IOException e2) {
                           // Ignore the exception
                        }
                     }
                  }
                  if (ds.keyData.tukCount() == 0) {
                     throw new AACSException("Could not decrypt any Title Key, no decrypted Title Keys present");
                  } else {
                     out.println("Updated disc data:");
                     out.println(ds.keyData.toString());
                  }
                  out.print("Searching Sequence Key Block File... ");
                  if (new File(ds.aacsDir, "SKB.AACS").isFile()) {
                     out.println("ERROR");
                     out.println("Sequence Key Block File found! Cannot process Sequence Keys, dumping may fail!");
                  } else {
                     out.println("OK");
                     out.println("Sequence Key Block File not found (this is good)");
                  }
                  break;
               case DiscSet.BD_MV:
               case DiscSet.BDR_MV:
               case DiscSet.BDR_AV:
                  long keyBlockOffset = 0;
                  boolean bdDirectoryWarning = false;
                  boolean skbWarning = false;
                  try {
                     out.println("Decrypting " + keyFilenames.getFirst() + "... ");
                     keySource = new FileSource(new File(keyFilenames.getFirst()), FileSource.R_MODE);
                     if (keySource.read(Utils.buffer, 0, 20) != 20) {
                        throw new IOException("Unexpected EOF");
                     }
                     keyBlockOffset = ByteArray.getVarLong(Utils.buffer, 0, 4); 
                     // Parse Unit Key File Header
                     if (ByteArray.getUByte(Utils.buffer, 16) == AACSDecrypter.BD_APPLICATION_TYPES.get(ds.contentType)) {
                        int bdDirectories = ByteArray.getUByte(Utils.buffer, 17);
                        if (bdDirectories >= 1) {
                           if ((ds.contentType == DiscSet.BD_MV) && ((ByteArray.getUByte(Utils.buffer, 18) >>> 7) == 0x01)) {
                              skbWarning = true;
                           }
                           // For non-BDR_AV Content Types limit bdDirectories to one and issue a warning
                           if (ds.contentType != DiscSet.BDR_AV && bdDirectories > 1) {
                              bdDirectories = 1;
                              bdDirectoryWarning = true;
                           }
                           int tcs = 0;
                           for (int i = 0; i < bdDirectories; i++) {
                              if (keySource.read(Utils.buffer, 0, 6) != 6) {
                                 throw new IOException("Unexpected EOF");
                              }
                              // BDR_MV does not have this head elements
                              if (ds.contentType != DiscSet.BDR_MV) {
                                 int tmp = ByteArray.getUShort(Utils.buffer, 0);
                                 if (tmp != 0) {
                                    ds.keyData.setHeadUnit(i, 0, tmp);
                                 }
                                 tmp = ByteArray.getUShort(Utils.buffer, 2);
                                 if (tmp != 0) {
                                    ds.keyData.setHeadUnit(i, 1, tmp);
                                 }
                              }
                              tcs = ByteArray.getUShort(Utils.buffer, 4);
                              for (int j = 1; j <= tcs; j++) {
                                 if (keySource.read(Utils.buffer, 0, 4) != 4) {
                                    throw new IOException("Unexpected EOF");
                                 }
                                 if (ds.contentType != DiscSet.BD_MV) {
                                    // Only recordables have the Clip_ID#
                                    ds.keyData.setTcUnit(i, ByteArray.getUShort(Utils.buffer, 0), ByteArray.getUShort(Utils.buffer, 2));
                                 } else {
                                    // Prerecorded ones use the Title#
                                    ds.keyData.setTcUnit(i, j, ByteArray.getUShort(Utils.buffer, 2));
                                 }
                              }
                           }

                           // Parse Unit Key Block
                           keySource.setPosition(keyBlockOffset);
                           if (keySource.read(Utils.buffer, 0, 16) != 16) {
                              throw new IOException("Unexpected EOF");
                           }
                           int cpsUnits = ByteArray.getUShort(Utils.buffer, 0);
                           // Define some variables used by recordables here to avoid recreating them in every loop 
                           // Intermediate CPS Unit Key before XORing it with the AES-H value
                           byte[] ukey = new byte[16];
                           // Buffer for storing the AES-H values
                           byte[] hbuf = new byte[AACSDecrypter.h0.length];
                           for (int unit = 1; unit <= cpsUnits; unit++) {
                              if (keySource.read(Utils.buffer, 0, 48) != 48) {
                                 throw new IOException("Unexpected EOF");
                              }
                              aes_128d.init(Cipher.DECRYPT_MODE, vukKey);
                              aes_128d.doFinal(Utils.buffer, 32, vuk.length, Utils.buffer, 0);
                              if (ds.contentType != DiscSet.BD_MV) {
                                 // Copy the intermediate key to have Utils.buffer available again
                                 System.arraycopy(Utils.buffer, 0, ukey, 0, ukey.length);
                                 // Initialize the AES-H buffer with h0
                                 System.arraycopy(AACSDecrypter.h0, 0, hbuf, 0, AACSDecrypter.h0.length);
                                 try {
                                    FileSource ur = new FileSource(new File(ds.aacsDir, String.format("CPSUnit%1$05d.cci", unit)), FileSource.R_MODE);
                                    // TODO: This simple approach works because the size of a CPS Unit Usage Rules file is always a multiple of 128 bits
                                    //       If this wouldn't be the case we must pad according to the spec if necessary
                                    while (ur.read(Utils.buffer, 0, hbuf.length) != -1) {
                                       SecretKeySpec aeskey = new SecretKeySpec(Utils.buffer, 0, hbuf.length, "AES");
                                       aes_128d.init(Cipher.DECRYPT_MODE, aeskey);
                                       // Use Utils.buffer to store the intermediate result
                                       aes_128d.doFinal(hbuf, 0, hbuf.length, Utils.buffer, 0);
                                       // Do AES-G directly into hbuf so that it is setup for the next step
                                       for (int i = 0; i < hbuf.length; i++) {
                                          hbuf[i] = (byte)((byte)Utils.buffer[i] ^ (byte)hbuf[i]);
                                       }
                                    }
                                    // Now hbuf contains the final AES-H value, XOR the key with it
                                    for (int i = 0; i < ukey.length; i++) {
                                       ukey[i] = (byte)((byte)ukey[i] ^ (byte)hbuf[i]);
                                    }
                                    ds.keyData.setTuk(unit, ukey, 0);
                                 }
                                 catch (IOException e1) {
                                    out.println("Failed decrypting CPS Unit Key: " + unit);
                                    out.println(e1.getMessage());
                                 }
                                 catch (GeneralSecurityException e2) {
                                    out.println("Failed decrypting CPS Unit Key: " + unit);
                                    out.println(e2.getMessage());
                                 }
                              } else {
                                 ds.keyData.setTuk(unit, Utils.buffer, 0);
                              }
                           }
                           out.println("Finished decrypting CPS Unit Keys");
                        } else throw new AACSException("Invalid number of BD Directories: " + bdDirectories);
                     } else throw new AACSException("Wrong Application Type, expected: " + AACSDecrypter.BD_APPLICATION_TYPES.get(ds.contentType) + ", got: " + ByteArray.getUByte(Utils.buffer, 16));
                  }
                  catch (IOException e1) {
                     out.println("Failed decrypting CPS Unit Keys");
                     out.println(e1.getMessage());
                  }
                  catch (AACSException e2) {
                     out.println("Failed decrypting CPS Unit Keys");
                     out.println(e2.getMessage());
                  }
                  catch (GeneralSecurityException e3) {
                     out.println("Failed decrypting CPS Unit Keys");
                     out.println(e3.getMessage());
                  }
                  finally {
                     try {
                        keySource.close();
                     }
                     catch (IOException e2) {
                        // Ignore the exception
                     }
                  }
                  if (bdDirectoryWarning) {
                     out.println("WARNING! More than one BD Directory specified, ignored the following ones");
                  }
                  if (ds.keyData.tukCount() == 0) {
                     throw new AACSException("Could not decrypt any CPS Unit Key, no decrypted CPS Unit Keys present");
                  } else {
                     out.println("Updated disc data:");
                     out.println(ds.keyData.toString());
                  }
                  if (skbWarning) {
                     out.println("ERROR! Sequence Key Block found! Cannot process Sequence Keys, dumping may fail!");
                  } else {
                     out.println("Sequence Key Block not found (this is good)");
                  }
                  break;
               default:
                  // Unknown disc type, throw an exception
                  out.println("FAILED");
                  throw new AACSException("Unknown disc type: " + ds.contentType);
               }
               out.println("AACS data processed");
            } else {
               // No Title Key / CPS Unit Key File found
               out.println("No Title Key / CPS Unit Key Files Found");
               if (ds.keyData.tukCount() == 0) {
                  throw new AACSException("Could not decrypt Title Keys / CPS Unit Keys, no decrypted Title Keys / CPS Unit Keys present");
               }
            }
         } else {
            // No VUK / PAK's present, check if any Title Key / CPS Unit Key is present
            if (ds.keyData.tukCount() == 0) {
               out.println("FAILED");
               throw new AACSException("No Volume Unique Key / Protected Area Keys present, no decrypted Title Keys / CPS Unit Keys present");
            } else {
               out.println("OK");
            }
         }
      } else {
         // No Key Data present 
         out.println("FAILED");
         throw new AACSException("No Key Data present");
      }
   }

   /**
    * Decrypts an EVOB.
    * 
    * @param input Input EVOB
    * @param output Output EVOB
    * @throws AACSException A cryptographic error occurred
    * @throws IOException An I/O error occurred
    */
   public Collection<MultiplexedArf> decryptEvob(ByteSource input, ByteSource output) throws AACSException, IOException {
      if (ds == null) {
         throw new AACSException("AACSDecrypter not initialized");
      }

      // PESPack used to parse the current present pack data
      PESPack pack = new PESPack();
      // Actual position of the currently processed pack in the input ByteSource
      long position = 0;
      // The start position of the packBuffer in the input ByteSource
      long baseAddress = 0;

      // KEY_VF field from the CPI field
      int key_vf = 0;
      // Key pointer of the current parsed pack, can be a title key pointer or a segment key pointer
      int packKeyPtr = 0;
      // Currently used Key pointer, can be a title key pointer, a segment key pointer or a magic value 
      int currentKeyPtr = 0;
      // Currently used Key, can be a title key, segment key, or null for no key
      byte[] npk = null;

      // True, if this is the first time that a read was done, false otherwise
      boolean firstRead = true;
      // Number of bytes read during the last read
      int read = 0;
      // Offset into the packBuffer to write the read result to, is also used during parsing section as pointer to the current pack
      int readOffset = 0;
      // Number of bytes to read, initial value is the complete packBuffer length, after the first read always the queueLength
      int readAmount = packBuffer.length;
      // Offset into the packGuard, used in the reading section
      int packGuardReadOffset = 0;

      // Offset into the packBuffer to start writing from
      int writeOffset = 0;
      // Number of bytes to write, is in writing section always queueLength * PESPack.PACK_LENGTH, except the first read returned less data, then its read
      int writeAmount = queueLength * PESPack.PACK_LENGTH;
      // Number of packs to write, is queueLength, except the first read returned less data, then its read / PESPack.PACK_LENGTH
      int packGuardCheckAmount = queueLength;
      // Offset into the packGuard, used in the writing section
      int packGuardWriteOffset = 0;

      // Each element in the packGuard represents one pack in the packBuffer, processing a pack unlocks it, writing it locks it again
      // Used to mark when a pack is ready to be written
      Semaphore[] packGuard = new Semaphore[packBuffer.length / PESPack.PACK_LENGTH];
      for (int i = 0; i < packGuard.length; i++) {
         packGuard[i] = new Semaphore(0, false);
      }

      // References to current open ARFs, index is the second byte of the header (ID?)
      MultiplexedArf[] arfs = new MultiplexedArf[256];
      for (int i = 0; i < arfs.length; i++) {
         arfs[i] = null;
      }
      // The current used ARF
      MultiplexedArf currentArf = null;

      // List of finished ARFs, this one is returned
      LinkedList<MultiplexedArf> arfList = new LinkedList<MultiplexedArf>();

      packDecrypter.init(packBuffer, packGuard, null);
      packDecrypter.updateBaseAddress(baseAddress);

      while ((read = input.read(packBuffer, readOffset, readAmount)) != -1) {
         // ***********************
         // *** Reading section ***
         // ***********************
         // Parse the read in data, increment readOffset automatically
         for (int endOffset = readOffset + read; readOffset < endOffset; readOffset += PESPack.PACK_LENGTH) {
            try {
               pack.parse(packBuffer, readOffset, false);
               // TODO: To reduce if's, check first scramble state
               if (pack.isNavPck()) {
                  // NAV_PCK, update current crypto-settings
                  // TODO: Fixed offset, better use dynamic discovery?
                  key_vf = packBuffer[readOffset + 60] & 0xC0; 
                  if (key_vf == 0x80) {
                     // Title Key Pointer valid
                     packKeyPtr = packBuffer[readOffset + 62] & 0xFF;
                     if (packKeyPtr != currentKeyPtr) {
                        // Key has changed
                        if (packKeyPtr > 0 && packKeyPtr <= 64) {
                           // Valid Title Key Pointer
                           out.println(String.format("0x%1$010X Key change, new key: Title Key #%2$d", position, packKeyPtr));
                           currentKeyPtr = packKeyPtr;
                           npk = ds.keyData.getTuk(currentKeyPtr);
                           if (npk == null) {
                              // Required Title Key NOT present!
                              out.println(String.format("0x%1$010X ERROR! Required Title Key not present: #%2$d", position, currentKeyPtr));
                           } else {
                              packDecrypter.updateKey(npk, false);
                           }
                        } else {
                           // Invalid Title Key Pointer!
                           out.println(String.format("0x%1$010X ERROR! Key change, new Title Key pointer invalid, ignoring #%2$d", position, packKeyPtr));
                        }
                     }
                  } else {
                     if (key_vf == 0x40) {
                        // Segment Key Pointer valid
                        packKeyPtr = packBuffer[readOffset + 63] & 0xFF;
                        if (packKeyPtr != currentKeyPtr) {
                           // Key has changed
                           if (packKeyPtr > 0 && packKeyPtr <= 192) {
                              // Valid Sequence Key Pointer
                              out.println(String.format("0x%1$010X Key change, new key: Sequence Key #%2$d", position, packKeyPtr));
                              currentKeyPtr = packKeyPtr;
                              out.println("### !ERROR! Sequence Keys not supported, skipping decryption !ERROR! ###");
                              npk = null;
                           } else {
                              // Invalid Sequence Key Pointer
                              out.println(String.format("0x%1$010X ERROR! Key change, new Sequence Key pointer invalid, ignoring #%2$d", position, packKeyPtr));
                           }
                        }
                     } else {
                        if (key_vf == 0) {
                           // No Key valid
                           if (currentKeyPtr != 0) {
                              out.println(String.format("0x%1$010X Warning! No Key valid, disabling decryption", position));
                              currentKeyPtr = 0;
                              npk = null;
                           }
                        } else {
                           // Reserved value!
                           if (currentKeyPtr != Integer.MAX_VALUE) {
                              out.println(String.format("0x%1$010X Warning! Reserved Key Validity Flag, disabling decryption", position));
                              currentKeyPtr = Integer.MAX_VALUE;
                              npk = null;
                           }
                        }
                     }
                  }
                  // Updating CPI-Field
                  packDecrypter.updateCpi(readOffset);
                  // TODO: Which values should be preserved?
                  // Clearing CPI-Field, CPI starts at 60
                  // Clear Key Management Information
                  ByteArray.setInt(packBuffer, readOffset + 60, 0);
                  // Clear Usage Rule Management Information
                  ByteArray.setShort(packBuffer, readOffset + 68, 0x7FFF);
                  // Set all CCI fields to valid and allow everything
                  ByteArray.setInt(packBuffer, readOffset + 70, 0xF000);
                  // Mark this pack as ready to write
                  packGuard[packGuardReadOffset].release();
               } else {
                  if (pack.isScrambled()) {
                     // Scrambled pack, decrypt it
                     // Decrypt only when a key is present
                     if (npk != null) {
                        // TODO: Not nice, should the thread set the scramble state?
                        pack.setScrambled(false);
                        packDecrypter.decryptPack(readOffset);
                     } else {
                        // Mark this pack as ready to write
                        packGuard[packGuardReadOffset].release();
                     }
                  } else {
                     if (pack.isAdvPck()) {
                        // ADV_PKT require special handling
                        int payloadOffset = pack.getPacket(0).payloadOffset();
                        int payloadLength = pack.getPacket(0).getPayloadLength();
                        // The payload must be at least 3 bytes long, 2 bytes header, 1 byte 0x00 for INTERMEDIATE_PACKET and END_PACKET,
                        // at least 1 char filename for START_PACKET
                        if (payloadLength >= 3) {
                           int packetType = ByteArray.getUByte(packBuffer, payloadOffset); 
                           int packetId = ByteArray.getUByte(packBuffer, payloadOffset + 1);
                           //System.out.println("ARF found, position: " + position + ", payloadOffset: " + (payloadOffset - readOffset) + ", payloadLength: " + payloadLength + ", packetType: " + packetType + ", packetId: " + packetId);
                           if (packetType == MultiplexedArf.INTERMEDIATE_PACKET) {
                              // INTERMEDIATE_PACKET found
                              currentArf = arfs[packetId];
                              if (currentArf != null) {
                                 // Add payload
                                 // Skip header plus 1 byte, make the payloadOffset relative to the pack start!
                                 currentArf.addData(position, payloadOffset + 3 - readOffset, payloadLength - 3);
                              } else {
                                 // Error, ARF not open
                                 out.println(String.format("0x%1$010X Error! INTERMEDIATE_PACKET for not opened ARF ID? 0x%2$02X found, ignoring", position, packetId));
                              }
                           } else {
                              if (packetType == MultiplexedArf.START_PACKET) {
                                 // START_PACKET found
                                 if (arfs[packetId] == null) {
                                    // The payload must be at least 258 bytes long, 2 bytes header, 256 bytes filename field
                                    if (payloadLength >= 258) {
                                       // Locate end of filename
                                       int nameStringEndOffset = payloadOffset + 2;
                                       for (int maxNameStringOffset = payloadOffset + 258; nameStringEndOffset < maxNameStringOffset; nameStringEndOffset++) {
                                          if (packBuffer[nameStringEndOffset] == 0x00) {
                                             break;
                                          }
                                       }
                                       //System.out.println("Name end offset: " + (nameStringEndOffset - readOffset));
                                       try {
                                          String arfName = new String(packBuffer, payloadOffset + 2, nameStringEndOffset - (payloadOffset + 2), "US-ASCII");
                                          out.println(String.format("0x%1$010X Multiplexed ARF found, ID?: 0x%2$02X, filename: %3$s", position, packetId, arfName));
                                          // Create new MultiplexedArf, non demux mode
                                          currentArf = new MultiplexedArf(arfName, output, false);
                                          // Skip header plus filename field, make the payloadOffset relative to the pack start!
                                          currentArf.addData(position, payloadOffset + 258 - readOffset, payloadLength - 258);
                                          arfs[packetId] = currentArf;
                                       }
                                       catch (UnsupportedEncodingException e) {
                                          // This is not allowed to happen!
                                          out.println(String.format("0x%1$010X ERROR! Could not decode filename for ARF ID? 0x%2$02X, ignoring", position, packetId));
                                       }
                                    } else {
                                       // ARF too small for header to fit in
                                       out.println(String.format("0x%1$010X Warning! Broken START_PACKET packet found for ARF ID? 0x%2$02X, payload too small for filename field, ignoring", position, packetId));
                                    }
                                 } else {
                                    // Error, ARF is open
                                    out.println(String.format("0x%1$010X ERROR! START_PACKET for already opened ARF ID? 0x%2$02X found, ignoring", position, packetId));
                                 }
                              } else {
                                 if (packetType == MultiplexedArf.END_PACKET) {
                                    // END_PACKET found
                                    currentArf = arfs[packetId];
                                    if (currentArf != null) {
                                       // Add payload and close ARF
                                       // Skip header plus 1 byte, make the payloadOffset relative to the pack start!
                                       currentArf.addData(position, payloadOffset + 3 - readOffset, payloadLength - 3);
                                       currentArf.markComplete();
                                       arfList.add(currentArf);
                                       arfs[packetId] = null;
                                    } else {
                                       // Error, ARF not open
                                       //TODO: Maybe this is allowed if the ARF is only one packet big?
                                       out.println(String.format("0x%1$010X ERROR! END_PACKET for not opened ARF ID? 0x%2$02X found, ignoring", position, packetId));
                                    }
                                 } else {
                                    // Unknown packet type found
                                    out.println(String.format("0x%1$010X Warning! Unknown ARF packet type 0x%2$02X for ARF ID? 0x%3$02X found, ignoring", position, packetType, packetId));
                                 }
                              }
                           }
                        } else {
                           // ARF too small for header data to fit in
                           out.println(String.format("0x%1$010X Warning! Broken ARF packet found, payload too small, ignoring", position));
                        }
                        // Mark this pack as ready to write
                        //System.out.println("Releasing: " + packGuardReadOffset);
                        packGuard[packGuardReadOffset].release();
                     } else {
                        // An unencrypted pack, just copy it
                        // Mark this pack as ready to write
                        packGuard[packGuardReadOffset].release();
                     }
                  }
               }
            }
            catch (PESParserException e) {
               out.println(String.format("0x%1$010X ERROR! PES parser exception: %2$s", position, e.getMessage()));
               // In case of an exception the pack must be marked as ready to write, or the write section will wait forever
               packGuard[packGuardReadOffset].release();
            }
            packGuardReadOffset += 1;
            if (packGuardReadOffset == packGuard.length) {
               packGuardReadOffset = 0;
            }
            position += PESPack.PACK_LENGTH;
         } // End-for parsing read in data
         // If readOffset hit the buffer end, reset it to 0
         // Note: If read != packBuffer.length / 2 && read != packBuffer.length we won't get on the end and there will be less space left than half of packBuffer
         //       But this can only happen when we reached the EOF, so this was the last time this loop gets executed
         if (readOffset == packBuffer.length) {
            readOffset = 0;
            baseAddress += packBuffer.length;
            packDecrypter.updateBaseAddress(baseAddress);
         }
         // ***********************
         // *** Writing section ***
         // ***********************
         // If the first read completely filled the buffer set the new readAmount to read only queueLength packs
         // If the first read did not fill the buffer completely, then the file is smaller than the buffer, process the complete buffer at once
         if (firstRead) {
            firstRead = false;
            if (read == packBuffer.length) {
               readAmount = queueLength * PESPack.PACK_LENGTH;
            } else {
               // The first read did not fill the buffer completely, adjust values to process the whole buffer
               writeAmount = read;
               packGuardCheckAmount = read / PESPack.PACK_LENGTH;
            }
         }
         // Lock all packs that we want to write
         for (int endOffset = packGuardWriteOffset + packGuardCheckAmount; packGuardWriteOffset < endOffset; packGuardWriteOffset++) {
            try {
               //System.out.println("Aquiring forced: " + packGuardWriteOffset);
               packGuard[packGuardWriteOffset].acquire();
            }
            catch (InterruptedException e) {
               // TODO: Do we get interrupted?
            }
         }
         if (packGuardWriteOffset == packGuard.length) {
            packGuardWriteOffset = 0;
         }
         // TODO: Check write result?
         output.write(packBuffer, writeOffset, writeAmount);
         writeOffset += writeAmount;
         if (writeOffset == packBuffer.length) {
            writeOffset = 0;
         }
      } // End-while reading input file
      // *******************************
      // *** Buffer flushing section ***
      // *******************************
      // Wait until the decrypter threads finish and write the remaining data, if any 
      packDecrypter.waitForDecryption();
      writeAmount = 0;
      // This can never overrun the buffer because the offsets are correctly set before we reach this section and there is never more than queueLength bytes left
      while (packGuard[packGuardWriteOffset].tryAcquire()) {
         writeAmount += PESPack.PACK_LENGTH;
         packGuardWriteOffset += 1;
         // If the last queueLength part of the buffer is flushed we could overrun, so break out in that case
         if (packGuardWriteOffset == packGuard.length) {
            break;
         }
      }
      // TODO: Check write result?
      output.write(packBuffer, writeOffset, writeAmount);
      // ****************************
      // *** ARF checking section ***
      // ****************************
      // Check for open ARFs and close them
      for (int i = 0; i < arfs.length; i++) {
         currentArf = arfs[i];
         // If there is something != null then this ARF is open, because closed ones get removed from this array
         if (currentArf != null) {
            arfs[i] = null;
            out.println(String.format("Warning! Remaining open ARF with ID? 0x%1$02X found, enforcing close", i));
            currentArf.markComplete();
            arfList.add(currentArf);
         }
      }
      return arfList;
   }

   /**
    * Decrypts an ARF. If the input is not an encrypted ARF, it is just copied.
    * 
    * @param input Input ARF
    * @param output Output ARF
    * @return Number of written bytes to the output and the crc32 of the output
    * @throws AACSException A cryptographic error occurred
    * @throws IOException An I/O error occurred
    */
   public CopyResult decryptArf(ByteSource input, ByteSource output) throws AACSException, IOException {
      if (ds == null) {
         throw new AACSException("AACSDecrypter not initialized");
      }

      // Buffer used for decryption
      byte[] data = Utils.buffer;
      // ID string of the file
      String fileId = null;
      // Type of AACS protection
      int protectionType = 0;
      // Title Key Pointer to use for decryption
      int titleKeyPtr = 0;
      // Resource File Size field of the AACS header
      long resourceFileSize = 0;
      // Read the AACS header
      if (input.read(data, 0, 11) == 11) {
         fileId = new String(data, 0, idStringAACS.length(), "US-ASCII");
         // Check if the ACCS id is present
         if (fileId.equals(idStringAACS)) {
            protectionType = ByteArray.getUByte(data, 4);
            titleKeyPtr = ByteArray.getUByte(data, 6);
            resourceFileSize = ByteArray.getVarLong(data, 7, 4);
            //System.out.println("fileId: " + fileId + ", pType: " + protectionType + ", tkp: " + titleKeyPtr + ", rfs: " + resourceFileSize);
            // Check if at least the resource data fits in
            // Note: some protection types make the file bigger than resourceFileSize + 272!
            if (resourceFileSize + 272 <= input.size()) {
               // Read in the Resource Filename Field. Attention, in two cases this field is encrypted, it MUST be decrypted too!
               input.read(data, 0, 272);
               switch (protectionType) {
               // Ignoring the hash values both formats are handled the same
               case 0x01:
                  // Encapsulation for Encryption
               case 0x11:
                  // Encapsulation for Encryption and Hash
                  // Read as much data fits into the buffer, be careful that bufferSize % 16 > 0 is NOT encrypted
                  if (titleKeyPtr > 0 && titleKeyPtr <= 64) {
                     long written = 0;
                     byte[] tk = ds.keyData.getTuk(titleKeyPtr);
                     if (tk != null) {
                        // Title Key found
                        SecretKey tkKey = new SecretKeySpec(tk, "AES");
                        try {
                           aes_128cbcd.init(Cipher.DECRYPT_MODE, tkKey, cbc_iv_spec);
                           // Decrypt the Resource Filename Field!
                           aes_128cbcd.update(data, 0, 272, data, 0);
                           crc32Calc.reset();
                           int readResult = 0;
                           int writeResult = 0;
                           while (written < resourceFileSize) {
                              long writeAmount = resourceFileSize - written;
                              if (writeAmount < data.length) {
                                 // All remaining data fits into the buffer
                                 readResult = input.read(data, 0, (int)writeAmount);
                                 if (readResult != (int)writeAmount) {
                                    throw new IOException("Could not read the requested number of bytes");
                                 }
                                 // Check for unencrypted residual data
                                 int residual = ((int)writeAmount) % 16;
                                 if (residual > 0) {
                                    aes_128cbcd.doFinal(data, 0, (int)writeAmount - residual, data, 0);
                                 } else {
                                    aes_128cbcd.doFinal(data, 0, (int)writeAmount, data, 0);
                                 }
                                 writeResult = output.write(data, 0, (int)writeAmount);
                                 if (writeResult != (int)writeAmount) {
                                    throw new IOException("Could not write the requested number of bytes");
                                 }
                                 written += (int)writeAmount;
                                 crc32Calc.update(data, 0, (int)writeAmount);
                              } else {
                                 // More data than buffer space
                                 // The buffersize % 16 MUST be 0!!!!
                                 readResult = input.read(data, 0, data.length);
                                 if (readResult != data.length) {
                                    throw new IOException("Could not read the requested number of bytes");
                                 }
                                 aes_128cbcd.update(data, 0, data.length, data, 0);
                                 writeResult = output.write(data, 0, data.length);
                                 if (writeResult != data.length) {
                                    throw new IOException("Could not write the requested number of bytes");
                                 }
                                 written += data.length;
                                 crc32Calc.update(data, 0, data.length);
                              }
                           }
                        }
                        catch (GeneralSecurityException e) {
                           throw new AACSException(e.getMessage(), e);
                        }
                     } else throw new AACSException("Title Key #" + titleKeyPtr + " not found");
                     return new CopyResult(written, crc32Calc.getValue());
                  } else throw new AACSException("Invalid Title Key Pointer #" + titleKeyPtr);
                  // Ignoring the MAC, Hash and Filename check, these formats are handled the same
               case 0x02:
                  // Encapsulation for MAC
               case 0x12:
                  // Encapsulation for Hash
               case 0x21:
                  // Encapsulation for Non-Protected Advanced Element
                  // Just copying the file
                  return Utils.copyBs(input, output, resourceFileSize);
               default:
                  // Unknown protection type
                  throw new AACSException("Unknown protection type");
               }
            } else throw new AACSException("Physical filesize smaller than AACS filesize");
         } // End-if ID-check
      } // End-if Header-check
      // This section MAY ONLY be reached if the input should be copied to output
      // Reset position because there were already parts of the input read
      input.setPosition(0);
      // TODO: Check if complete file got copied?
      return Utils.copyBs(input, output, input.size());
   }

   /**
    * Decrypts a m2ts file.
    * 
    * TODO: Do i need a way to prevent key brute forcing with a null key? BDAV code calls this with key == null and usually
    *       does not want to search for a key
    * 
    * @param input Input m2ts
    * @param output Output m2ts
    * @param key The CPS Unit key to use, if null it is tried do determine it by brute forcing
    * @param subTable A Conversion Table SubTable to use for BD+ removal, set to null if BD+ isn't present
    * @throws AACSException A cryptographic error occurred
    * @throws IOException An I/O error occurred
    */
   public void decryptTs(ByteSource input, ByteSource output, byte[] key, SubTable subTable) throws AACSException, IOException {
      if (ds == null) {
         throw new AACSException("AACSDecrypter not initialized");
      }

      // TSAlignedUnit to parse the current present aligned unit data
      TSAlignedUnit unit = new TSAlignedUnit();
      // Actual position of the currently processed aligned unit in the input ByteSource
      long position = 0;
      // The start position of the packBuffer in the input ByteSource
      long baseAddress = 0;

      // Current state of the decryption, -1: has not been set yet, 0: decryption disabled, 1: decryption enabled
      int decryptionState = -1;

      // True, if this is the first time that a read was done, false otherwise
      boolean firstRead = true;
      // Number of bytes read during the last read
      int read = 0;
      // Offset into the packBuffer to write the read result to, is also used during parsing section as pointer to the current pack
      int readOffset = 0;
      // Number of bytes to read, initial value is the complete packBuffer length, after the first read always the queueLength
      int readAmount = packBuffer.length;
      // Offset into the packGuard, used in the reading section
      int packGuardReadOffset = 0;

      // Offset into the packBuffer to start writing from
      int writeOffset = 0;
      // Number of bytes to write, is in writing section always queueLength * PESPack.PACK_LENGTH, except the first read returned less data, then its read
      int writeAmount = queueLength * TSAlignedUnit.UNIT_LENGTH / 3;
      // Number of packs to write, is queueLength, except the first read returned less data, then its read / PESPack.PACK_LENGTH
      int packGuardCheckAmount = queueLength / 3;
      // Offset into the packGuard, used in the writing section
      int packGuardWriteOffset = 0;

      // Each element in the packGuard represents one pack in the packBuffer, processing a pack unlocks it, writing it locks it again
      // Used to mark when a pack is ready to be written
      Semaphore[] packGuard = new Semaphore[packBuffer.length / TSAlignedUnit.UNIT_LENGTH];
      for (int i = 0; i < packGuard.length; i++) {
         packGuard[i] = new Semaphore(0, false);
      }

      packDecrypter.init(packBuffer, packGuard, subTable);
      packDecrypter.updateBaseAddress(baseAddress);
      
      // Search for the CPS Unit Key if necessary
      if (key == null) {
         out.print("Searching CPS Unit Key... ");
         int index = findTsKey(input);
         if (index < 0) {
            out.println("ERROR");
            if (index == -2) {
               out.println("Failed to determine CPS Unit Key!");
            } else {
               out.println("An error occured while trying to determine the CPS Unit Key!");
            }
         } else if (index == 0) {
            out.println("Not encrypted");
         } else {
            out.println(String.format("#%1$d", index));
            key = ds.keyData.getTuk(index);
         }
      }
      // Set the CPS Unit Key if present
      if (key != null) {
         packDecrypter.updateKey(key, true);
      }
      
      while ((read = input.read(packBuffer, readOffset, readAmount)) != -1) {
         // ***********************
         // *** Reading section ***
         // ***********************
         // Parse the read in data, increment readOffset automatically
         for (int endOffset = readOffset + read; readOffset < endOffset; readOffset += TSAlignedUnit.UNIT_LENGTH) {
            try {
               unit.parse(packBuffer, readOffset);
               // Check if the aligned unit is encrypted
               if (unit.isEncrypted()) {
                  // Aligned unit is encrypted
                  if (decryptionState == 0 || decryptionState == -1) {
                     // Aligned unit is encrypted
                     if (decryptionState == 0) {
                        // Aligned unit is encrypted but decryption has been disabled, this is not allowed.
                        if (ds.contentType == DiscSet.BD_MV) {
                           out.println(String.format("0x%1$010X Warning! Decryption is disabled, enabling decryption", position));
                        } else {
                           out.println(String.format("0x%1$010X Info! Enabling decryption", position));
                        }
                     }
                     decryptionState = 1;
                     // Check here if the required key is present just to show an error message if not
                     if (key != null) {
                        out.println(String.format("0x%1$010X Decryption enabled", position));
                     } else {
                        out.println(String.format("0x%1$010X ERROR! Required CPS Unit Key / Sequence Key not present", position));
                     }
                  }
                  // Actually perform the decryption
                  // TODO: If in case of a missing key no decryption is performed that can result into false alarams that the conversion table
                  //       is broken, put the pack into the packDecrypter and let it perfrom BD+ removal?
                  if (key != null) {
                     // Key is present, add a job to the packDecrypter
                     packDecrypter.decryptAlignedUnit(readOffset, false);
                  } else {
                     // Key is not present, mark the unit as ready to write
                     packGuard[packGuardReadOffset].release();
                  }
               } else {
                  // Aligned unit is not encrypted
                  if (decryptionState == -1) {
                     // The aligned unit is not encrypted and the state has not been set yet, disable decryption
                     decryptionState = 0;
                     out.println(String.format("0x%1$010X Decryption disabled", position));
                  } else {
                     if (decryptionState == 1) {
                        // Aligned unit is not encrypted but decryption has been enabled, disable it again. This is not allowed to happen.
                        decryptionState = 0;
                        if (ds.contentType == DiscSet.BD_MV) {
                           out.println(String.format("0x%1$010X Warning! Decryption is enabled, disabling decryption", position));
                        } else {
                           out.println(String.format("0x%1$010X Info! Disabling decryption", position));
                        }
                     }
                  }
                  // Check if BD+ should be removed
                  // TODO: This check isn't really necessary because the packDecrypter checks that himself, but maybe its faster to not
                  //       put the pack into the packDecrypter?
                  if (subTable != null) {
                     packDecrypter.decryptAlignedUnit(readOffset, true);
                  } else {
                     // Just mark the aligned unit to be finished
                     packGuard[packGuardReadOffset].release();
                  }
               }
            }
            catch (TSParserException e) {
               out.println(String.format("0x%1$010X ERROR! TS parser exception: %2$s", position, e.getMessage()));
               // In case of an exception the pack must be marked as ready to write, or the write section will wait forever
               packGuard[packGuardReadOffset].release();
            }
            packGuardReadOffset += 1;
            if (packGuardReadOffset == packGuard.length) {
               packGuardReadOffset = 0;
            }
            position += TSAlignedUnit.UNIT_LENGTH;
         } // End-for parsing read in data
         // If readOffset hit the buffer end, reset it to 0
         // Note: If read != packBuffer.length / 2 && read != packBuffer.length we won't get on the end and there will be less space left than half of packBuffer
         //       But this can only happen when we reached the EOF, so this was the last time this loop gets executed
         if (readOffset == packBuffer.length) {
            readOffset = 0;
            baseAddress += packBuffer.length;
            packDecrypter.updateBaseAddress(baseAddress);
         }
         // ***********************
         // *** Writing section ***
         // ***********************
         // If the first read completely filled the buffer set the new readAmount to read only queueLength packs
         // If the first read did not fill the buffer completely, then the file is smaller than the buffer, process the complete buffer at once
         if (firstRead) {
            firstRead = false;
            if (read == packBuffer.length) {
               readAmount = queueLength * TSAlignedUnit.UNIT_LENGTH / 3;
            } else {
               // The first read did not fill the buffer completely, adjust values to process the whole buffer
               writeAmount = read;
               packGuardCheckAmount = read / TSAlignedUnit.UNIT_LENGTH;
            }
         }
         // Lock all packs that we want to write
         for (int endOffset = packGuardWriteOffset + packGuardCheckAmount; packGuardWriteOffset < endOffset; packGuardWriteOffset++) {
            try {
               //System.out.println("Aquiring forced: " + packGuardWriteOffset);
               packGuard[packGuardWriteOffset].acquire();
            }
            catch (InterruptedException e) {
               // TODO: Do we get interrupted?
            }
         }
         if (packGuardWriteOffset == packGuard.length) {
            packGuardWriteOffset = 0;
         }
         // TODO: Check write result?
         output.write(packBuffer, writeOffset, writeAmount);
         writeOffset += writeAmount;
         if (writeOffset == packBuffer.length) {
            writeOffset = 0;
         }
      } // End-while reading input file
      // *******************************
      // *** Buffer flushing section ***
      // *******************************
      // Wait until the decrypter threads finish and write the remaining data, if any 
      packDecrypter.waitForDecryption();
      writeAmount = 0;
      // This can never overrun the buffer because the offsets are correctly set before we reach this section and there is never more than queueLength bytes left
      while (packGuard[packGuardWriteOffset].tryAcquire()) {
         writeAmount += TSAlignedUnit.UNIT_LENGTH;
         packGuardWriteOffset += 1;
         // If the last queueLength part of the buffer is flushed we could overrun, so break out in that case
         if (packGuardWriteOffset == packGuard.length) {
            break;
         }
      }
      // TODO: Check write result?
      output.write(packBuffer, writeOffset, writeAmount);
   }

   /**
    * Decrypts a Thumbnail Data File found on BDAV discs.
    * 
    * @param input Input m2ts
    * @param output Output m2ts
    * @param key The CPS Unit key to use
    * @throws AACSException A cryptographic error occurred
    * @throws IOException An I/O error occurred
    */
   public void decryptTdf(ByteSource input, ByteSource output, byte[] key) throws AACSException, IOException {
      // This is not really needed here, just for consistency
      if (ds == null) {
         throw new AACSException("AACSDecrypter not initialized");
      }
      if (key == null) {
         throw new AACSException("Required CPS Unit Key not present");
      }
      // The CPS Unit Key as KeySpec
      SecretKey cpsKey = new SecretKeySpec(key, "AES");
      // The calculated Block Key
      byte[] bk = new byte[16];
      // The Block Key as KeySpec
      SecretKeySpec bkKey = null;

      // This very simple approach can be used because the file is always a multiple of 2048 (says the spec...)
      // TODO: This can be done multi-threaded
      while (input.read(Utils.buffer, 0, 2048) != -1) {
         try {
            // Calculate the Block Key
            // Note: The key gets ENcrypted!
            aes_128d.init(Cipher.ENCRYPT_MODE, cpsKey);
            aes_128d.doFinal(Utils.buffer, 0, bk.length, bk, 0);
            for (int i = 0; i < bk.length; i++) {
               bk[i] = (byte)((byte)bk[i] ^ (byte)Utils.buffer[i]);
            }
            bkKey = new SecretKeySpec(bk, "AES");
            // Decrypt the Block
            aes_128cbcd.init(Cipher.DECRYPT_MODE, bkKey, AACSDecrypter.cbc_iv_spec);
            aes_128cbcd.doFinal(Utils.buffer, 16, 2032, Utils.buffer, 16);
         }
         catch (GeneralSecurityException e) {
            throw new AACSException(e.getMessage(), e);
         }
         // Write the decrypted data
         // TODO: Check write result?
         output.write(Utils.buffer, 0, 2048);
      }
   }
   
   /**
    * Tries to find the CPS Unit Key which decrypts the given input.
    * 
    * This is done using a brute force approach using every present CPS Unit Key to decrypt the first AU
    * and check if it seems to be decrypted.
    * 
    * TODO: Contains duplicated code from PackDecrypter
    * 
    * @param input TS to find the CPS Unit Key for.
    * @return The CPS Unit Key number which decrypts the input. 0 if the input is not encrypted,
    *         -1 if an error occurred, -2 if no key found
    * @throws IOException An I/O error occurred
    */
   private int findTsKey(ByteSource input) throws IOException {
      // Save the current position of the input file
      long currentOffset = input.getPosition();
      // The currently used CPS Unit Key Number
      int currentIndex = 0;
      // The currently used CPS Unit Key
      byte[] currentKey = null;
      
      // Used to parse the Aligned Unit
      TSAlignedUnit unit = new TSAlignedUnit();
      // The CPS Unit Key as SecretKey
      SecretKey npkKey = null;
      // The Block Key
      byte[] ck = new byte[16];
      // The Block Key as SecretKey
      SecretKey ckKey = null;
      
      // Seek to position 0, read one Aligned Unit and copy the unencrypted part behind the read part
      // The encrypted AU will be decrypted behind its position to avoid rereading it in every run
      input.setPosition(0);
      if (input.read(Utils.buffer, 0, TSAlignedUnit.UNIT_LENGTH) != TSAlignedUnit.UNIT_LENGTH) {
         // Source too small to be AACS encrypted
         input.setPosition(currentOffset);
         return 0;
      }
      // Reset to previous position, input isn't touched anymore
      input.setPosition(currentOffset);
      // Check if the AU is encrypted
      try {
         unit.parse(Utils.buffer, 0);
         if (unit.isEncrypted()) {
            System.arraycopy(Utils.buffer, 0, Utils.buffer, TSAlignedUnit.UNIT_LENGTH, 16);
         } else {
            return 0;
         }
      }
      catch (TSParserException ex) {
         return -1;
      }
      
      try {
         // Try all CPS Unit Keys, abort if a suitable key was found
         Iterator<Integer> it = ds.keyData.tukIdx().iterator();
         while (it.hasNext()) {
            currentIndex = it.next();
            // Calculate the Block Key
            currentKey = ds.keyData.getTuk(currentIndex);
            npkKey = new SecretKeySpec(currentKey, "AES");
            aes_128d.init(Cipher.ENCRYPT_MODE, npkKey);
            aes_128d.doFinal(Utils.buffer, 0, 16, ck);
            for (int i = 0; i < 16; i++) {
               ck[i] = (byte)((byte)ck[i] ^ (byte)Utils.buffer[i]);
            }
            ckKey = new SecretKeySpec(ck, "AES");
            
            // Decrypt the AU, store the decrypted content behind the encrypted source
            aes_128cbcd.init(Cipher.DECRYPT_MODE, ckKey, AACSDecrypter.cbc_iv_spec);
            // Encrypted part starts at 16
            aes_128cbcd.doFinal(Utils.buffer, 16, TSAlignedUnit.UNIT_LENGTH - 16, Utils.buffer, TSAlignedUnit.UNIT_LENGTH + 16);
            
            // Check if enough sync bytes are present. Accept and return in that case, otherwise test another key
            // Due to BD+ protection it might be possible that some sync bytes are corrupted, we don't apply BD+ here so
            // allow some damaged sync bytes
            // TODO: Can this be abused? Can BD+ damage all sync bytes by intent?
            int found = 0;
            // There are 32 TS Packets inside an Aligned Unit 
            for (int i = 0; i < 32; i++) {
               if (Utils.buffer[TSAlignedUnit.UNIT_LENGTH + i * TSAlignedUnit.PACKET_LENGTH + 4] == 0x47) {
                  found++;
                  if (found > 24) {
                     return currentIndex;
                  }
               }
            }
         }
      }
      catch (GeneralSecurityException e) {
         return -1;
      }
      
      return -2;
   }

}
