////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) DSTC Pty Ltd (ACN 052 372 577) 1993, 1994, 1995.
// Unpublished work.  All Rights Reserved.
//
// The software contained on this media is the property of the
// DSTC Pty Ltd.  Use of this software is strictly in accordance
// with the license agreement in the accompanying LICENSE.DOC
// file. If your distribution of this software does not contain
// a LICENSE.DOC file then you have no rights to use this
// software in any manner and should contact DSTC at the address
// below to determine an appropriate licensing arrangement.
//
//      DSTC Pty Ltd
//      Level 7, GP South
//      University of Queensland
//      St Lucia, 4072
//      Australia
//      Tel: +61 7 3365 4310
//      Fax: +61 7 3365 4311
//      Email: jcsi@dstc.qut.edu.au
//
// This software is being provided "AS IS" without warranty of
// any kind.  In no event shall DSTC Pty Ltd be liable for
// damage of any kind arising out of or in connection with
// the use or performance of this software.
//
////////////////////////////////////////////////////////////////////////////

package com.dstc.security.kerberos.crypto;

import java.util.Arrays;
import java.util.Properties;
import java.security.SecureRandom;
import java.security.InvalidKeyException;
import java.security.InvalidAlgorithmParameterException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;

import com.dstc.security.kerberos.v5.crypto.EncryptedData;

import com.dstc.security.asn1.Asn1;

/**
 * <p>A cipher class for Kerberos encryption/decryption.
 *
 * @version 0.98, 98/07/01
 * @author Ming Yung
 */
public class KerberosCipher
{
  //Encryption types
  public static final int DES_CBC_CRC = 0x01;
  public static final int DES_CBC_MD5 = 0x03;

  public static int CONFOUNDER_LEN = 8; //CRC or MD5
  public static int MD5_CKSUM_LEN = 16;
  public static int CRC_CKSUM_LEN = 4;

  private int beginData;
  private int cksumLength;

  private Cipher desCipher;
  private KerberosMac mac;
  private int encType;
  private byte[] keyBytes;
  private SecureRandom rand;

  private KerberosCipher(String algName, int encType, String provider)
  {
    try
    {
      if (provider == null)
        this.desCipher = Cipher.getInstance(algName);
      else
        this.desCipher = Cipher.getInstance(algName, provider);
      this.encType = encType;
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
  
    switch(this.encType)
    {
      case (DES_CBC_CRC):
        cksumLength = CRC_CKSUM_LEN;
        mac = KerberosMac.getInstance(KerberosMac.CRC32);
        break;

      case (DES_CBC_MD5):
        cksumLength = MD5_CKSUM_LEN;
        mac = KerberosMac.getInstance(KerberosMac.MD5);
        break;
    }

    this.beginData = CONFOUNDER_LEN + cksumLength;
  }

  public KerberosCipher(KeyMaterial km, SecureRandom rand, String provider)
  {
    this("DES/CBC/Zeroes", km.getKeyType(), provider);
    this.keyBytes = km.getKeyBytes();
    this.rand = rand;
  }

  public void initDecrypt() throws CryptoException
  {
    init(Cipher.DECRYPT_MODE, new SecretKeySpec(this.keyBytes, "DES"));
  }

  public void initEncrypt() throws CryptoException
  {
    init(Cipher.ENCRYPT_MODE, new SecretKeySpec(this.keyBytes, "DES"));
  }

  /**
   * Initializes this KerberosCipher with a KerberosKey and an IV
   * = key if DES_CBC_CRC or IV = zero if otherwise
   */
  private void init(int mode, SecretKey key)
    throws CryptoException
  {
    byte zero[] = new byte[8];

    IvParameterSpec ivSpec;
    if (this.encType == DES_CBC_CRC)
      ivSpec = new IvParameterSpec(key.getEncoded());
    else
      ivSpec = new IvParameterSpec(zero);

    try
    {
      this.desCipher.init(mode, key, ivSpec);
    }
    catch (Exception e)
    {
      throw new CryptoException(e.getMessage());
    }
  }

  /**
   * Encrypts the supplied plainText and returns it
   */
  public EncryptedData encrypt(byte[] data) throws CryptoException
  {
    //Make confounder
    byte confounder[] = new byte[CONFOUNDER_LEN];
    rand.nextBytes(confounder);

    //Make toBeEncrypted from confounder, data and padding
    int sum = beginData + data.length;
    int extra = 8 - (sum % 8);

    byte[] toBeEncrypted = new byte[sum + (extra == 8 ? 0 : extra)];
    System.arraycopy(confounder, 0, toBeEncrypted, 0, CONFOUNDER_LEN);
    System.arraycopy(data, 0, toBeEncrypted, beginData, data.length);

    mac.update(toBeEncrypted);
    byte checksum[] = mac.doFinal();
    System.arraycopy(checksum, 0, toBeEncrypted, CONFOUNDER_LEN, cksumLength);

    //Encrypt
    try
    {
      byte encrypted[] = this.desCipher.doFinal(toBeEncrypted);
      return new EncryptedData(this.encType, encrypted);
    }
    catch (Exception e)
    {
      throw new CryptoException(e.toString());
    }
  }

  /**
   * Decrypts the cipherText and returns it
   */
  public byte[] decrypt(EncryptedData encData) throws CryptoException
  {
    if (encData.getEncryptionType() != this.encType)
      throw new CryptoException("Encryption type mismatch");

    try
    {
      //call cipher method to decrypt
      byte decrypted[] = desCipher.doFinal(encData.getCipherText());

      //Extract checksum
      byte orig[] = new byte[cksumLength];
      System.arraycopy(decrypted, CONFOUNDER_LEN, orig, 0, cksumLength);

      //Zero out check
      for (int i=CONFOUNDER_LEN; i<beginData; i++)
        decrypted[i] = 0;

      byte retval[] = new byte[decrypted.length - beginData];
      System.arraycopy(decrypted, beginData, retval, 0, retval.length);

      //Compute checksum
      mac.update(decrypted);
      byte computed[] = mac.doFinal();
      if(!Arrays.equals(orig, computed))
        throw new CryptoException("Integrity check failure");

      return retval;
    }
    catch (Exception e)
    {
      throw new CryptoException(e.toString());
    }
  }

  protected static void debug(String msg, byte bytes[])
  {
    System.out.print(msg);
    for (int i=0; i<bytes.length; i++)
    {
      System.out.print(java.lang.Integer.toHexString(bytes[i] & 0xff) + " ");
    }
    System.out.println(" ");
  }
}
