package gnu.crypto.jce.cipher;

// --------------------------------------------------------------------------
// $Id: CipherAdapter.java,v 1.5 2005/10/06 04:24:14 rsdio Exp $
//
// Copyright (C) 2002, 2003 Free Software Foundation, Inc.
//
// This file is part of GNU Crypto.
//
// GNU Crypto 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 2 of the License, or (at
// your option) any later version.
//
// GNU Crypto 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, write to the
//
//    Free Software Foundation, Inc.,
//    51 Franklin Street, Fifth Floor,
//    Boston, MA  02110-1301
//    USA
//
// Linking this library statically or dynamically with other modules is
// making a combined work based on this library.  Thus, the terms and
// conditions of the GNU General Public License cover the whole
// combination.
//
// As a special exception, the copyright holders of this library give
// you permission to link this library with independent modules to
// produce an executable, regardless of the license terms of these
// independent modules, and to copy and distribute the resulting
// executable under terms of your choice, provided that you also meet,
// for each linked independent module, the terms and conditions of the
// license of that module.  An independent module is a module which is
// not derived from or based on this library.  If you modify this
// library, you may extend this exception to your version of the
// library, but you are not obligated to do so.  If you do not wish to
// do so, delete this exception statement from your version.
//
// --------------------------------------------------------------------------

import gnu.crypto.cipher.IBlockCipher;
import gnu.crypto.cipher.CipherFactory;
import gnu.crypto.jce.spec.BlockCipherParameterSpec;
import gnu.crypto.mode.IMode;
import gnu.crypto.mode.ModeFactory;
import gnu.crypto.pad.IPad;
import gnu.crypto.pad.PadFactory;
import gnu.crypto.pad.WrongPaddingException;

import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidParameterSpecException;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;

/**
 * <p>The implementation of a generic {@link Cipher} <i>Adapter</i> class to
 * wrap GNU Crypto cipher instances.</p>
 *
 * <p>This class defines the <i>Service Provider Interface</i> (<b>SPI</b>) for
 * the {@link Cipher} class, which provides the functionality of symmetric-key
 * block ciphers, such as the AES.<p>
 *
 * <p>This base class defines all of the abstract methods in {@link CipherSpi},
 * but does not define the (non-abstract) key wrapping functions that extended
 * the base cipher SPI, and these methods thus immediately throw an
 * {@link UnsupportedOperationException}. If a cipher implementation provides
 * this functionality, or if it in fact accepts parameters other than the key
 * and the initialization vector, the subclass should override those methods.
 * Otherwise a subclass need only call the {@link #CipherAdapter(String)}
 * constructor with the name of the cipher.</p>
 *
 * @version $Revision: 1.5 $
 */
class CipherAdapter extends CipherSpi {

   // Constants and variables.
   // -------------------------------------------------------------------------

   /** Our cipher instance. */
   protected IBlockCipher cipher;

   /** Our mode instance. */
   protected IMode mode;

   /** Our padding instance. */
   protected IPad pad;

   /** The current key size. */
   protected int keyLen;

   /** Our attributes map. */
   protected Map attributes;

   /** An incomplete block. */
   protected byte[] partBlock;

   /** The number of bytes in {@link #partBlock}. */
   protected int partLen;

   /** The length of blocks we are processing. */
   protected int blockLen;

   // Constructor(s)
   // -------------------------------------------------------------------------

   /**
    * <p>Protected constructor to be called by subclasses. The cipher name
    * argument should be the appropriate one listed in {@link gnu.crypto.Registry}.
    * The basic cipher instance is created, along with an instance of the
    * {@link gnu.crypto.mode.ECB} mode and no padding.</p>
    *
    * @param cipherName The cipher to instantiate.
    * @param blockLen The block length to use.
    */
   protected CipherAdapter(String cipherName, int blockLen) {
      cipher = CipherFactory.getInstance(cipherName);
      attributes = new HashMap();
      this.blockLen = blockLen;
      mode = ModeFactory.getInstance("ECB", cipher, blockLen);
      attributes.put(IBlockCipher.CIPHER_BLOCK_SIZE, new Integer(blockLen));
   }

   /**
    * <p>Creates a new cipher adapter with the default block size.</p>
    *
    * @param cipherName The cipher to instantiate.
    */
   protected CipherAdapter(String cipherName) {
      cipher = CipherFactory.getInstance(cipherName);
      blockLen = cipher.defaultBlockSize();
      attributes = new HashMap();
      mode = ModeFactory.getInstance("ECB", cipher, blockLen);
      attributes.put(IBlockCipher.CIPHER_BLOCK_SIZE, new Integer(blockLen));
   }

   // Instance methods implementing javax.crypto.CipherSpi.
   // -------------------------------------------------------------------------

   protected void engineSetMode(String modeName) throws NoSuchAlgorithmException {
      if (modeName.length() >= 3
            && modeName.substring(0, 3).equalsIgnoreCase("CFB")) {
         if (modeName.length() > 3) {
            try {
               int bs = Integer.parseInt(modeName.substring(3));
               attributes.put(IMode.MODE_BLOCK_SIZE, new Integer(bs / 8));
            } catch (NumberFormatException nfe) {
               throw new NoSuchAlgorithmException(modeName);
            }
            modeName = "CFB";
         }
      } else {
         attributes.remove(IMode.MODE_BLOCK_SIZE);
      }
      mode = ModeFactory.getInstance(modeName, cipher, blockLen);
      if (mode == null) {
         throw new NoSuchAlgorithmException(modeName);
      }
   }

   protected void engineSetPadding(String padName) throws NoSuchPaddingException {
      if (padName.equalsIgnoreCase("NoPadding")) {
         pad = null;
         return;
      }
      pad = PadFactory.getInstance(padName);
      if (pad == null) {
         throw new NoSuchPaddingException(padName);
      }
   }

   protected int engineGetBlockSize() {
      if (cipher != null) {
         return blockLen;
      }
      return 0;
   }

   protected int engineGetOutputSize(int inputLen) {
      final int blockSize = mode.currentBlockSize();
      return ((inputLen + partLen) / blockSize) * blockSize;
   }

   protected byte[] engineGetIV() {
      byte[] iv = (byte[]) attributes.get(IMode.IV);
      if (iv == null) {
         return null;
      }
      return (byte[]) iv.clone();
   }

   protected AlgorithmParameters engineGetParameters() {
      BlockCipherParameterSpec spec = new BlockCipherParameterSpec(
            (byte[]) attributes.get(IMode.IV), cipher.currentBlockSize(), keyLen);
      AlgorithmParameters params;
      try {
         params = AlgorithmParameters.getInstance("BlockCipherParameters");
         params.init(spec);
      } catch (NoSuchAlgorithmException nsae) {
         return null;
      } catch (InvalidParameterSpecException ipse) {
         return null;
      }
      return params;
   }

   protected void engineInit(int opmode, Key key, SecureRandom random)
   throws InvalidKeyException {
      switch (opmode) {
      case Cipher.ENCRYPT_MODE:
            attributes.put(IMode.STATE, new Integer(IMode.ENCRYPTION)); break;
      case Cipher.DECRYPT_MODE:
            attributes.put(IMode.STATE, new Integer(IMode.DECRYPTION)); break;
      }
      if (!key.getFormat().equalsIgnoreCase("RAW")) {
         throw new InvalidKeyException("bad key format " + key.getFormat());
      }
      byte[] kb = key.getEncoded();
      if (keyLen == 0) {
         keyLen = kb.length;
      } else if (keyLen < kb.length) {
         byte[] kbb = kb;
         kb = new byte[keyLen];
         System.arraycopy(kbb, 0, kb, 0, keyLen);
      }
      attributes.put(IBlockCipher.KEY_MATERIAL, kb);
      reset ();
   }

   protected void
   engineInit(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random)
   throws InvalidKeyException, InvalidAlgorithmParameterException {
      if (params == null) {
         byte[] iv = new byte[blockLen];
         random.nextBytes(iv);
         attributes.put(IMode.IV, iv);
         blockLen = cipher.defaultBlockSize();
         attributes.put(IBlockCipher.CIPHER_BLOCK_SIZE, new Integer(blockLen));
         keyLen = 0;
      } else if (params instanceof BlockCipherParameterSpec) {
         attributes.put(IBlockCipher.CIPHER_BLOCK_SIZE,
               new Integer(((BlockCipherParameterSpec) params).getBlockSize()));
         attributes.put(IMode.IV,
               ((BlockCipherParameterSpec) params).getIV());
         keyLen = ((BlockCipherParameterSpec) params).getKeySize();
         blockLen = ((BlockCipherParameterSpec) params).getBlockSize();
      } else if (params instanceof IvParameterSpec) {
         attributes.put(IMode.IV, ((IvParameterSpec) params).getIV());
         blockLen = cipher.defaultBlockSize();
         attributes.put(IBlockCipher.CIPHER_BLOCK_SIZE, new Integer(blockLen));
         keyLen = 0;
      }
      engineInit(opmode, key, random);
   }

   protected void
   engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random)
   throws InvalidKeyException, InvalidAlgorithmParameterException
   {
      AlgorithmParameterSpec spec = null;
      try {
         if (params != null) {
            spec = params.getParameterSpec(BlockCipherParameterSpec.class);
         }
      } catch (InvalidParameterSpecException ignored) {
      }
      engineInit(opmode, key, spec, random);
   }

   protected byte[] engineUpdate(byte[] input, int off, int len) {
      final int blockSize = mode.currentBlockSize();
      final int count = (partLen + len) / blockSize;
      final byte[] out = new byte[count * blockSize];
      try {
         engineUpdate(input, off, len, out, 0);
      } catch (ShortBufferException x) { // should not happen
         x.printStackTrace(System.err);
      }
      return out;
   }

//   protected int
//   engineUpdate(byte[] in, int inOff, int inLen, byte[] out, int outOff)
//   throws ShortBufferException
//   {
//      int blockSize = mode.currentBlockSize();
//      int count = (partLen + inLen) / blockSize;
//      if (count * blockSize > out.length - outOff) {
//         throw new ShortBufferException();
//      }
//      byte[] buf;
//      if (partLen > 0 && count > 0) {
//         buf = new byte[partLen + inLen];
//         System.arraycopy(partBlock, 0, buf, 0, partLen);
//         if (in != null && inLen > 0) {
//            System.arraycopy(in, inOff, buf, partLen, inLen);
//         }
//         partLen = 0;
//         inOff = 0;
//      } else {
//         buf = in;
//      }
//      for (int i = 0; i < count; i++) {
//         mode.update(buf, i * blockSize + inOff, out, i * blockSize + outOff);
//      }
//      if (inOff + inLen > count * blockSize) {
//         partLen = (inOff + inLen) - (count * blockSize);
//         System.arraycopy(in, count * blockSize, partBlock, 0, partLen);
//      }
//      return count * blockSize;
//   }

   protected int
   engineUpdate(byte[] in, int inOff, int inLen, byte[] out, int outOff)
   throws ShortBufferException {
      if (inLen == 0) { // nothing to process
         return 0;
      }
      final int blockSize = mode.currentBlockSize();
      final int blockCount = (partLen + inLen) / blockSize;
      final int result = blockCount * blockSize;
      if (result > out.length - outOff) {
         throw new ShortBufferException();
      }
      if (blockCount == 0) { // not enough bytes for even 1 block
         System.arraycopy(in, inOff, partBlock, partLen, inLen);
         partLen += inLen;
         return 0;
      }
      final byte[] buf;
      // we have enough bytes for at least 1 block
      if (partLen == 0) { // if no cached bytes use input
         buf = in;
      } else { // prefix input with cached bytes
         buf = new byte[partLen + inLen];
         System.arraycopy(partBlock, 0, buf, 0, partLen);
         if (in != null && inLen > 0) {
            System.arraycopy(in, inOff, buf, partLen, inLen);
         }
         inOff = 0;
      }
      for (int i = 0; i < blockCount; i++) { // update blockCount * blockSize
         mode.update(buf, inOff, out, outOff);
         inOff += blockSize;
         outOff += blockSize;
      }
      partLen += inLen - result;
      if (partLen > 0) { // cache remaining bytes from buf
         System.arraycopy(buf, inOff, partBlock, 0, partLen);
      }
      return result;
   }

   protected byte[] engineDoFinal(byte[] input, int off, int len)
   throws IllegalBlockSizeException, BadPaddingException {
      final byte[] result;
      final byte[] buf = engineUpdate(input, off, len);
      if (pad != null) {
         switch (((Integer) attributes.get(IMode.STATE)).intValue()) {
         case IMode.ENCRYPTION:
            byte[] padding = pad.pad(partBlock, 0, partLen);
            byte[] buf2 = engineUpdate(padding, 0, padding.length);
            result = new byte[buf.length + buf2.length];
            System.arraycopy(buf, 0, result, 0, buf.length);
            System.arraycopy(buf2, 0, result, buf.length, buf2.length);
            break;
         case IMode.DECRYPTION:
            int padLen;
            try {
               padLen = pad.unpad(buf, 0, buf.length);
            } catch (WrongPaddingException wpe) {
               throw new BadPaddingException(wpe.getMessage());
            }
            result = new byte[buf.length - padLen];
            System.arraycopy(buf, 0, result, 0, result.length);
            break;
         default:
            throw new IllegalStateException();
         }
      } else {
         if (partLen > 0) {
            throw new IllegalBlockSizeException(partLen + " trailing bytes");
         }
         result = buf;
      }

      try
        {
          reset ();
        }
      catch (InvalidKeyException ike)
        {
          // Should not happen; if we initialized it with the current
          // parameters before, we should be able to do it again.
          throw new Error (ike);
        }
      return result;
   }

   protected int
   engineDoFinal(byte[] in, int inOff, int inLen, byte[] out, int outOff)
   throws BadPaddingException, IllegalBlockSizeException, ShortBufferException
   {
      byte[] buf = engineDoFinal(in, inOff, inLen);
      if (out.length + outOff < buf.length) {
         throw new ShortBufferException();
      }
      System.arraycopy(buf, 0, out, outOff, buf.length);
      return buf.length;
   }

   private void reset () throws InvalidKeyException
   {
      mode.reset();
      mode.init(attributes);
      if (pad != null) {
         pad.reset();
         pad.init(blockLen);
      }
      partBlock = new byte[blockLen];
      partLen = 0;
   }
}
