//////////////////////////////////////////////////////////////////////////// 
// 
// 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;

import java.util.StringTokenizer;
import java.util.Properties;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.security.SecureRandom;

import com.dstc.security.kerberos.crypto.KerberosKey;
import com.dstc.security.kerberos.crypto.KerberosCipher;
import com.dstc.security.kerberos.crypto.CryptoException;
import com.dstc.security.kerberos.crypto.KeyMaterial;
import com.dstc.security.kerberos.creds.CredentialStore;
import com.dstc.security.kerberos.creds.CredentialInfo;
import com.dstc.security.kerberos.creds.Credential;
import com.dstc.security.kerberos.creds.KeyTab;
import com.dstc.security.kerberos.creds.KerberosTicket;
import com.dstc.security.asn1.Asn1Exception;
import com.dstc.security.asn1.Asn1;
import com.dstc.security.util.Config;

/**
 * <p>A class representing the Kerberos service.
 *
 * Application programs using the Kerberos service directly and Kerberos
 * tools should only need to use this class. However, application programmers
 * are encouraged to use higher level protocols such as GSSAPI (available in
 * the GSSAPI package) if possible.
 *
 * <p> Configuration is via a Properties file whose location is pointed
 * to by the System Property jcsi.kerberos.conf, or by default, the
 * file
 * <pre>
 *    <user.home>/.jcsi/kerberos.properties
 * </pre>
 *
 * @version 0.98, 1999/05/14
 * @author Nam Tran
 * @author Ming Yung
 */
public final class Kerberos
{
  private static final String KRB_CONF = "jcsi.kerberos.conf";
  private static final String KRB_PROPERTIES = "kerberos.properties";

  private static final String KRBTGT = "krbtgt";
  private static Kerberos instance = null;
  private static KerberosContext ctx = null;

  private String localAddress = null;
  private DatagramSocket sock = null;
  private byte[] buffer = new byte[1500];
  private static CredentialStore credStore;

  //Warning!!! Trivial seeding is being used
  private SecureRandom rand = new SecureRandom(new byte[8]);

  static
  {
    if (instance == null)
    {

      try
      {
/*
        String conf = System.getProperty("jcsi.kerberos.conf");
        if (conf == null)
          conf = ".javasecurity/krb5.properties";

        FileInputStream is = new FileInputStream(conf);
        Properties props = new Properties();
        props.load(is);
*/
        Properties props = Config.getProperties(KRB_CONF, KRB_PROPERTIES);
        ctx = new KerberosContext(props);

        instance = new Kerberos();
      }
      catch (Exception e)
      {
        e.printStackTrace();
      }
    }
  }

  /**
   * Returns an instance of Kerberos. Context information is kept in
   * a KerberosContext object, constructed from default values in the
   * configuration file. To over-ride defaults, this object can be 
   * obtained via getKerberosContext() and operated on.
   */
  public static Kerberos getDefault()
  {
    return instance;
  }

  /**
   * Default constructor
   */
  Kerberos() throws IOException
  {
    this.localAddress = InetAddress.getLocalHost().getHostAddress();
  }

  /**
   * Returns the (mutable) KerberosContext object, on which set operations 
   * can be made to over-ride defaults.
   */
  public KerberosContext getKerberosContext()
  {
    return this.ctx;
  }

  /**
   * Gets and stores an initial TGT valid for a client valid for a
   * given address. The response is decrypted with a JCE cipher
   * in the given provider. This caters for the case where the cipher
   * (and keys) are implemented in a hardware device (eg. smartcard).
   * The supplied KeyMaterial for decryption, in such a case, only
   * requires the key type to be set.
   */
  public void requestInitialTicket(KeyMaterial cred,
                                   String client, String caddr,
                                   String provider)
    throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    String serviceName = KRBTGT + "/" + ctx.getRealm();

    int nonce = rand.nextInt();

    if (caddr == null) caddr = this.localAddress;

    ASRequest asReq = new ASRequest(cred, ctx.getDefaultOptions());
    asReq.setClientRealm(ctx.getRealm());
    asReq.setClientName(client);
    asReq.setServerName(serviceName);
    asReq.addAddress(caddr);
    asReq.setNonce(nonce);
    asReq.setTicketValidity(ctx.getDefTicketLife());

    ASResponse asRep
      = (ASResponse)KerberosMessageFactory.getKDCMessage(
          sendRequest(asReq.getEncoded()));

    asRep.initDecrypt(cred, provider);
    asRep.decrypt();

    // Add to the credential cache
    Credential newCred =
      new Credential(asRep.getCredentialInfo(), asRep.getTicket());

    if (credStore == null)
      credStore = CredentialStore.getDefault();
    credStore.put(newCred);
  }

  /**
   * Gets and stores an initial TGT valid for a given address, for the
   * given client. The response is decrypted with a JCE cipher currently
   * configured. The supplied KeyMaterial should contain the key and
   * key type for doing the decryption.
   */
  public void requestInitialTicket(KeyMaterial cred,
                                   String client, String caddr)
    throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    requestInitialTicket(cred, client, caddr, null);
  }

  /**
   * Gets and stores a ticket from the TGS using an existing TGT
   * for a given service, valid for the given address. The Credential
   * passed in is used for encrypting the request and decrypting the
   * response.
   */
  public void requestServiceTicket(Credential cred, String serviceName,
                                   String caddr)
    throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    int nonce = rand.nextInt();

    if (caddr == null) caddr = this.localAddress;

    TGSRequest serviceReq
      = new TGSRequest(cred, ctx.getDefaultOptions(), this.rand);
    serviceReq.setClientRealm(ctx.getRealm());
    //serviceReq.setClientName(ctx.getUsername());
    serviceReq.setServerName(serviceName);
    serviceReq.addAddress(caddr);
    serviceReq.setNonce(nonce);
    serviceReq.setTicketValidity(ctx.getDefTicketLife());
    serviceReq.encrypt();

    TGSResponse tgsRep
      = (TGSResponse)KerberosMessageFactory.getKDCMessage(
          sendRequest(serviceReq.getEncoded()));

    tgsRep.initDecrypt(cred);
    tgsRep.decrypt();

    // Add to the credential cache
    Credential newCred =
      new Credential(tgsRep.getCredentialInfo(), tgsRep.getTicket());

    if (credStore == null)
      credStore = CredentialStore.getDefault();
    credStore.put(newCred);
  }

  /**
   * Gets and stores a service ticket using the default existing TGT
   *
   * @param serviceName A principal name for which to get a ticket
   */
  public void requestServiceTicket(String serviceName)
      throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    Credential cred = getTGTCredential();
    requestServiceTicket(cred, serviceName, null);
  }

  private byte[] sendRequest(byte[] msg) throws IOException
  {
    this.sock = new DatagramSocket();
    DatagramPacket outPack
      = new DatagramPacket(msg, msg.length, ctx.getKDCAddress(),
         ctx.getKDCPort());
    this.sock.send(outPack);

    DatagramPacket inPack = new DatagramPacket(buffer, 1500);
    this.sock.receive(inPack);

    return inPack.getData();
  }

  /**
   * Convenience method which gets and stores an initial TGT valid for
   * the local address, for a user using a password.
   */
  public void requestTGTWithPassword(String user, String password)
      throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    requestTGTWithPassword(user, password, null);
  }

  /**
   * Convenience method which gets and stores an initial TGT valid for the
   * local address, for a user using a JavaCard, for which a JCE Cipher 
   * implementation is available in the given provider, and in which the
   * key bytes are already stored.
   */
  public void requestTGTWithJavaCard(String user, String provider)
      throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    KerberosKey contextKey = new KerberosKey(3, new byte[8]);

    requestInitialTicket(contextKey, user, null, provider);
  }

  /**
   * Convenience method which gets and stores an initial TGT for a user, 
   * valid for a given address using a password.
   */
  public void requestTGTWithPassword(String user, String password,
                                     String address)
    throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    // Construct the Kerberos key
    StringBuffer sb = new StringBuffer(password);
    sb.append(ctx.getRealm());
    StringTokenizer tknz = new StringTokenizer(user, "/");
    while (tknz.hasMoreTokens())
        sb.append(tknz.nextToken());

    //TODO: remove hard-coding of key type

    KerberosKey contextKey = new KerberosKey(1, sb.toString());

    requestInitialTicket(contextKey, user, address);
  }

  /**
   * Convenience method which gets and stores an initial TGT for a user 
   * using a keytab.
   */
  public void requestTGTWithKeytab(String user, String keytab)
      throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    requestTGTWithKeytab(user, keytab, null);
  }

  /**
   * Convenience method which stores an initial TGT for a user, valid for 
   * a given address using a keytab.
   */
  public void requestTGTWithKeytab(String user, String keytab, String address)
      throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    ctx.setKeyTabName(keytab);
    KeyTab kt = KeyTab.getDefault();
    requestInitialTicket(kt.getKeyTabEntry(user), user, address);
  }

  /**
   * Returns the service ticket for a given service, valid for the given 
   * address. The Credential passed in must be a valid TGT Credential. 
   */
  public Credential getServiceTicket(Credential cred, String serviceName,
                                     String addr)
    throws KerberosException, IOException, CryptoException, Asn1Exception
  {
    String serverName = serviceName.substring(0, serviceName.indexOf('@'));

    requestServiceTicket(cred, serverName, addr);

    return credStore.getCredential(serviceName);
  }

  /**
   * Returns the service ticket for a given service, valid for the local
   * address. The Credential passed in must be a valid TGT Credential.
   */
  public Credential getServiceTicket(Credential cred, String serviceName)
    throws KerberosException, IOException, CryptoException, Asn1Exception
  {
    return getServiceTicket(cred, serviceName, null);
  }

  /**
   * Returns the TGT credential from the cache
   */
  public Credential getTGTCredential() throws IOException
  {
    //Should check for CredentialCache
    if (this.credStore == null)
      this.credStore = CredentialStore.getDefault();

    String realmName = ctx.getRealm();
    return credStore.getCredential(KRBTGT + "/" + realmName + "@" + realmName);
  }

  /**
   * Gets and returns a TGT suitable for used in delegation. The default TGT
   * is used to get this TGT and must be FORWARDABLE or PROXIABLE. The
   * returned TGT will have its flags set to FORWARDED or PROXY accordingly.
   *
   * @param destAddr Address of the destination host for delegation.
   * @returns A Credential suitable for delegation or null.
   */
  public Credential getDelegationCredential(String destAddr)
    throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    Credential cred = getTGTCredential();

    return getDelegationCredential(cred, destAddr);
  }

  /**
   * Gets and returns a TGT suitable for used in delegation. The given TGT
   * is used to get this TGT and must be FORWARDABLE or PROXIABLE. The
   * returned TGT will have its flags set to FORWARDED or PROXY accordingly.
   *
   * @param tgtCred The TGT used to request the new TGT
   * @param destAddr Address of the destination host for delegation.
   * @returns A Credential suitable for delegation or null.
   */
  public Credential getDelegationCredential(Credential tgtCred,
                                             String destAddr)
     throws IOException, KerberosException, CryptoException, Asn1Exception
  {
    // Check credential flags for FORWARDABLE and PROXIABLE and construct new
    // property appropriately
    CredentialInfo credInfo = tgtCred.getCredentialInfo();
    boolean forwardable = credInfo.getTicketFlags().isOn("FORWARDABLE");
    boolean proxiable = credInfo.getTicketFlags().isOn("PROXIABLE");

    //TODO: set flags in context before making new request

    String tgsName = KRBTGT + "/" + ctx.getRealm() + "@" + ctx.getRealm();
    return getServiceTicket(tgtCred, tgsName, destAddr);
  }

  ////////////////////////////////////////////////////////////
  //   Main
  ////////////////////////////////////////////////////////////
  
  /**
   * The main method to function as jinit.
   */
  public static void main(String args[])
  {
    boolean useKeytab = false, useCard = false, error = false;
    String user = null, password, keytab = null, ccache = null;
    String provider = null;

    // parse command line arguments
    for (int i = 0; i < args.length; i++)
    {
      if ("-j".equalsIgnoreCase(args[i]))
      {
        if (useCard)
        {
          error = true;
          System.err.println("Error: duplicate -j options");
          break;
        }
        if (useKeytab)
        {
          error = true;
          System.err.println("Error: conflicting -j and -k options");
          break;
        }
        useCard = true;
        provider = args[++i];
      }
      else if ("-k".equalsIgnoreCase(args[i]))
      {
        if (useKeytab)
        {
          error = true;
          System.err.println("Error: duplicate -k options");
          break;
        }
        if (useCard)
        {
          error = true;
          System.err.println("Error: conflicting -c and -k options");
          break;
        }
        if (i == args.length - 1)
        {
          error = true;
          System.err.println("Error: no keytab specified");
          break;
        }
        useKeytab = true;
        keytab = args[++i];
      }
      else if ("-c".equalsIgnoreCase(args[i]))
      {
        if (i == args.length - 1)
        {
          error = true;
          System.err.println("Error: no cache file specified");
          break;
        }
        ccache = args[++i];
      }
      else if (i == args.length -1) // the last can be the user name
        user = args[i];
      else        // tolerate invalid argument
        System.err.println("Ignoring invalid argument " + args[i]);
    }

    // determine user
    if (!error && (user == null))
    {
      // try the system properties first
      user = ctx.getUsername();

      // then complain if not successful
      if (user == null)
      {
        error = true;
        System.err.println("Error: I don't know who the user is");
      }
    }
    else
      ctx.setUsername(user);

    // can't continue if there was an error
    if (error)
    {
      System.err.println("Usage jinit [-j | -k keytab] [-c cache] [user]");
      System.err.println("  -j provider Use a javacard with provider " +
                                                             "specified");
      System.err.println("  -k keytab   Use the keytab specified");
      System.err.println("  -c cache    Use the credential cache specified");
      System.err.println("  user        Principal name");
      System.exit(1);
    }

    if (ccache != null)
      ctx.setCCName(ccache);

    // now try to get the TGT
    try
    {
      Kerberos krb = Kerberos.getDefault();

      if (useCard)
        krb.requestTGTWithJavaCard(user, provider);
      else if (useKeytab)
        krb.requestTGTWithKeytab(user, keytab);
      else
      {
        String prompt = "Enter password for " + user + "@" +
            krb.getKerberosContext().getRealm() + ": ";
        System.out.print(prompt);
        InputStreamReader isr = new InputStreamReader(System.in);
        BufferedReader in = new BufferedReader(isr);

        password = in.readLine();
        if (password == null || password.equals(""))
        {
          System.err.println("Invalid password");
          System.exit(1);
        }
        krb.requestTGTWithPassword(user, password);
      }
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
  }
}
