/*
 * "@(#)GssSpNegoClient.java  1.1 05/06/15 SMI"
 *
 * Copyright 2006 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * -Redistributions of source code must retain the above copyright
 * notice, this  list of conditions and the following disclaimer.
 *
 * -Redistribution in binary form must reproduct the above copyright
 * notice, this list of conditions and the following disclaimer in
 * the documentation and/or other materials provided with the
 * distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
 * EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY
 * DAMAGES OR LIABILITIES  SUFFERED BY LICENSEE AS A RESULT OF  OR
 * RELATING TO USE, MODIFICATION OR DISTRIBUTION OF THE SOFTWARE OR
 * ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE
 * FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT,
 * SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF
 * THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that Software is not designed, licensed or
 * intended for use in the design, construction, operation or
 * maintenance of any nuclear facility.
 */

import org.ietf.jgss.*;
import java.net.Socket;
import java.io.IOException;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.security.*;
import javax.security.auth.login.LoginException;

/**
 * A sample client application that uses JGSS to do mutual authentication
 * with a server using Kerberos as the underlying mechanism. It then
 * exchanges data securely with the server.
 *
 * Every message sent to the server includes a 4-byte application-level
 * header that contains the big-endian integer value for the number
 * of bytes that will follow as part of the JGSS token.
 *
 * The protocol is:
 *    1.  Context establishment loop:
 *         a. client sends init sec context token to server
 *         b. server sends accept sec context token to client
 *         ....
 *    2. client sends a wrapped token to the server.
 *    3. server sends a wrapped token back to the client for the application
 *
 * Start GssServer first before starting GssClient.
 *
 * Usage:  java <options> GssSpNegoClient <service> <serverName>
 *
 * Example: java -Djava.security.auth.login.config=jaas-krb5.conf \
 *               GssSpNegoClient host machine.imc.org
 *
 * Add -Djava.security.krb5.conf=krb5.conf to specify application-specific
 * Kerberos configuration (different from operating system's Kerberos
 * configuration).
 */

public class GssSpNegoClient {
    private static final int PORT = 4567;
    private static final boolean verbose = false;

    public static void main(String[] args) throws Exception {

  // Obtain the command-line arguments and parse the server's principal

  if (args.length < 2) {
      System.err.println(
    "Usage: java <options> GssSpNegoClient <service> <serverName>");
      System.exit(-1);
  }

  String serverPrinc = args[0] + "@" + args[1];

  PrivilegedExceptionAction action =
      new GssClientAction(serverPrinc, args[1], PORT);

  Jaas.loginAndAction("client", action);
    }

    static class GssClientAction implements PrivilegedExceptionAction {
  private String serverPrinc;
  private String hostName;
  private int port;

  GssClientAction(String serverPrinc, String hostName, int port) {
      this.serverPrinc = serverPrinc;
      this.hostName = hostName;
      this.port = port;
  }

  public Object run() throws Exception {
      Socket socket = new Socket(hostName, port);
      DataInputStream inStream =
    new DataInputStream(socket.getInputStream());
      DataOutputStream outStream =
    new DataOutputStream(socket.getOutputStream());

      System.out.println("Connected to address " +
    socket.getInetAddress());

      /*
       * This Oid is used to represent the SPNEGO GSS-API
       * mechanism. It is defined in RFC 2478. We will use this Oid
       * whenever we need to indicate to the GSS-API that it must
       * use SPNEGO for some purpose.
       */
      Oid spnegoOid = new Oid("1.3.6.1.5.5.2");

      GSSManager manager = GSSManager.getInstance();

      /*
       * Create a GSSName out of the server's name.
       */
      GSSName serverName = manager.createName(serverPrinc,
    GSSName.NT_HOSTBASED_SERVICE, spnegoOid);

      /*
       * Create a GSSContext for mutual authentication with the
       * server.
       *    - serverName is the GSSName that represents the server.
       *    - krb5Oid is the Oid that represents the mechanism to
       *      use. The client chooses the mechanism to use.
       *    - null is passed in for client credentials
       *    - DEFAULT_LIFETIME lets the mechanism decide how long the
       *      context can remain valid.
       * Note: Passing in null for the credentials asks GSS-API to
       * use the default credentials. This means that the mechanism
       * will look among the credentials stored in the current Subject
       * to find the right kind of credentials that it needs.
       */
      GSSContext context = manager.createContext(serverName,
    spnegoOid,
    null,
    GSSContext.DEFAULT_LIFETIME);

      // Set the desired optional features on the context. The client
      // chooses these options.

      context.requestMutualAuth(true);  // Mutual authentication
      context.requestConf(true);  // Will use confidentiality later
      context.requestInteg(true); // Will use integrity later

      // Do the context eastablishment loop

      byte[] token = new byte[0];

      while (!context.isEstablished()) {

    // token is ignored on the first call
    token = context.initSecContext(token, 0, token.length);

    // Send a token to the server if one was generated by
    // initSecContext
    if (token != null) {
        if (verbose) {
      System.out.println("Will send token of size " +
          token.length + " from initSecContext.");
      System.out.println("writing token = " +
          getHexBytes(token));
        }

        outStream.writeInt(token.length);
        outStream.write(token);
        outStream.flush();
    }

    // If the client is done with context establishment
    // then there will be no more tokens to read in this loop
    if (!context.isEstablished()) {
        token = new byte[inStream.readInt()];
        if (verbose) {
      System.out.println("reading token = " +
          getHexBytes(token));
      System.out.println("Will read input token of size " +
          token.length + " for processing by initSecContext");
        }
        inStream.readFully(token);
    }
      }

      System.out.println("Context Established! ");
      System.out.println("Client principal is " + context.getSrcName());
      System.out.println("Server principal is " + context.getTargName());

      /*
       * If mutual authentication did not take place, then only the
       * client was authenticated to the server. Otherwise, both
       * client and server were authenticated to each other.
       */
      if (context.getMutualAuthState())
    System.out.println("Mutual authentication took place!");

      byte[] messageBytes = "Hello There!".getBytes("UTF-8");

      /*
       * The first MessageProp argument is 0 to request
       * the default Quality-of-Protection.
       * The second argument is true to request
       * privacy (encryption of the message).
       */
      MessageProp prop =  new MessageProp(0, true);

      /*
       * Encrypt the data and send it across. Integrity protection
       * is always applied, irrespective of confidentiality
       * (i.e., encryption).
       * You can use the same token (byte array) as that used when
       * establishing the context.
       */

      System.out.println("Sending message: " +
    new String(messageBytes, "UTF-8"));
      token = context.wrap(messageBytes, 0, messageBytes.length, prop);
      outStream.writeInt(token.length);
      outStream.write(token);
      outStream.flush();

      /*
       * Now we will allow the server to decrypt the message,
       * append a time/date on it, and send then it back.
       */

      token = new byte[inStream.readInt()];
      System.out.println("Will read token of size " + token.length);
      inStream.readFully(token);
      byte[] replyBytes = context.unwrap(token, 0, token.length, prop);

      System.out.println("Received message: " +
    new String(replyBytes, "UTF-8"));

      System.out.println("Done.");
      context.dispose();
      socket.close();

      return null;
  }
    }

    private static final String getHexBytes(byte[] bytes, int pos, int len) {

  StringBuffer sb = new StringBuffer();
  for (int i = pos; i < (pos+len); i++) {

      int b1 = (bytes[i]>>4) & 0x0f;
      int b2 = bytes[i] & 0x0f;

      sb.append(Integer.toHexString(b1));
      sb.append(Integer.toHexString(b2));
      sb.append(' ');
  }
  return sb.toString();
    }

    private static final String getHexBytes(byte[] bytes) {
  return getHexBytes(bytes, 0, bytes.length);
    }
}
