/*
 * $Id: CryptoTerminal.java,v 1.1 2004/06/09 13:37:07 martijno Exp $
 */

package terminal;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.math.BigInteger;
import java.util.Enumeration;
import javax.swing.*;

import java.security.*;
import java.security.spec.*;
import java.security.interfaces.*;

import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;

import opencard.core.event.*;
import opencard.core.service.*;
import opencard.core.terminal.*;
import opencard.opt.terminal.*;
import opencard.opt.util.*;

/**
 * Sample OCF terminal for the Crypto applet.
 *
 * @author Martijn Oostdijk (martijno@cs.kun.nl)
 *
 * @version $Revision: 1.1 $
 */
public class CryptoTerminal extends JPanel implements ActionListener
{
   static final int BLOCKSIZE = 128;

   static final String TITLE = "Crypto Terminal";
   static final int DISPLAY_WIDTH = 30;
   static final int DISPLAY_HEIGHT = 20;

   static final String MSG_ERROR = "Error";
   static final String MSG_INVALID = "Invalid";

   static final byte[] APPLET_AID = {0x01,0x02,0x03,0x04,0x05,0x06,0x01};
   static final byte CLA_ISO = (byte)0x00;
   static final byte INS_SELECT = (byte)0xA4;
   static final short SW_NO_ERROR = (short)0x9000;
   static final short SW_APPLET_SELECT_FAILED = (short)0x6999;
   static final short SW_FILE_NOT_FOUND = (short)0x6A82;

   private static final byte CLA_CRYPTO = (byte)0xCC;
   private static final byte INS_SET_PUB_MODULUS = (byte)0x02;
   private static final byte INS_SET_PRIV_MODULUS = (byte)0x12;
   private static final byte INS_SET_PRIV_EXP = (byte)0x22;
   private static final byte INS_SET_PUB_EXP = (byte)0x32;
   private static final byte INS_ISSUE = (byte)0x40;
   private static final byte INS_ENCRYPT = (byte)0xE0;
   private static final byte INS_DECRYPT = (byte)0xD0;

   private static final int STATE_INIT = 0;;
   private static final int STATE_ISSUED = 1;

   /** GUI stuff. */
   JTextArea display;

   /** GUI stuff. */
   JButton setPubKeyButton,
           setPrivKeyButton,
           issueButton,
           encryptButton,
           decryptButton;

   /** File browser needs to remember last dir it accessed. */
   File currentDir = new File(".");

   /** The card service. */
   PassThruCardService service;

   /**
    * Constructs the terminal application.
    */
   public CryptoTerminal() {
      buildGUI();
      setEnabled(false);
      addActionListener(this);
      (new CardThread()).start();
   }

   /**
    * Builds the GUI.
    */
   void buildGUI() {
      setLayout(new BorderLayout());
      display = new JTextArea(DISPLAY_HEIGHT,DISPLAY_WIDTH);
      display.setEditable(false);
      add(new JScrollPane(display),BorderLayout.CENTER);
      JPanel buttons = new JPanel(new FlowLayout());
      setPubKeyButton = new JButton("Set Pub");
      setPrivKeyButton = new JButton("Set Priv");
      issueButton = new JButton("Issue");
      encryptButton = new JButton("Encrypt");
      decryptButton = new JButton("Decrypt");
      buttons.add(setPubKeyButton);
      buttons.add(setPrivKeyButton);
      buttons.add(issueButton);
      buttons.add(encryptButton);
      buttons.add(decryptButton);
      add(buttons,BorderLayout.SOUTH);
   }

   /**
    * Enables/disables the buttons.
    *
    * @param b boolean indicating whether to enable or disable the buttons.
    */
   public void setEnabled(boolean b) {
      super.setEnabled(b);
      setPubKeyButton.setEnabled(b);
      setPrivKeyButton.setEnabled(b);
      issueButton.setEnabled(b);
      encryptButton.setEnabled(b);
      decryptButton.setEnabled(b);
   }

   /**
    * Adds the action listener <code>l</code> to all buttons.
    *
    * @param l the action listener to add.
    */
   public void addActionListener(ActionListener l) {
      setPubKeyButton.addActionListener(l);
      setPrivKeyButton.addActionListener(l);
      issueButton.addActionListener(l);
      encryptButton.addActionListener(l);
      decryptButton.addActionListener(l);
   }

   class CardThread extends Thread implements CTListener
   {
      /** The card terminal. */
      CardTerminal terminal;

      /**
       * Runs the card thread.
       */
      public void run() {
         try {
            SmartCard.start();
            CardTerminalRegistry terminalRegistry =
               CardTerminalRegistry.getRegistry();
            Enumeration terminals = terminalRegistry.getCardTerminals();
            terminal = (CardTerminal)terminals.nextElement();
            EventGenerator eventGenerator = EventGenerator.getGenerator();
            eventGenerator.addCTListener(this);
            cardSession(CardRequest.ANYCARD);
            while (true) {
               cardSession(CardRequest.NEWCARD);
            }
         } catch (Exception e) {
           setEnabled(false);
           log(MSG_ERROR);
           System.out.println("ERROR: " + e.getMessage());
           e.printStackTrace();
         }
      }

      /**
       * Waits for card, selects applet, enables GUI if applet was found.
       *
       * @param requestType indicates whether to wait for newly inserted
       *                    card or already inserted card.
       */
      void cardSession(int requestType) {
         try {
            CardRequest request =
               new CardRequest(requestType,terminal,PassThruCardService.class);
            SmartCard card = SmartCard.waitForCard(request);
            service =
               (PassThruCardService)card.getCardService(PassThruCardService.class,true);
            if ((short)selectApplet(APPLET_AID).sw() == SW_NO_ERROR) {
               setEnabled(true);
            } else {
               log(MSG_INVALID);
               setEnabled(false);
            }
         } catch (Exception e) {
            setEnabled(false);
            log(MSG_ERROR);
            System.out.println("ERROR: " + e.getMessage());
            e.printStackTrace();
         }
      }

      /**
       * Handles card insertion events.
       *
       * @param cte the event indicating a card insertion.
       */
      public void cardInserted(CardTerminalEvent cte) {
      }

      /**
       * Handles card removal events. Disables the GUI.
       *
       * @param cte the event indicating a card insertion.
       */
      public void cardRemoved(CardTerminalEvent cte) {
         setEnabled(false);
      }
   }

   /**
    * Handles button events.
    *
    * @param ae event indicating a button was pressed.
    */
   public void actionPerformed(ActionEvent ae) {
      try {
         Object src = ae.getSource();
         if (src instanceof JButton) {
            JButton button = (JButton)src;
            if (button.equals(setPubKeyButton)) {
               setPubKey();
            } else if (button.equals(setPrivKeyButton)) {
               setPrivKey();
            } else if (button.equals(issueButton)) {
               issue();
            } else if (button.equals(encryptButton)) {
               encrypt();
            } else if (button.equals(decryptButton)) {
               decrypt();
            }
         }
      } catch (Exception e) {
         log(MSG_ERROR);
         System.out.println("ERROR: " + e.getMessage());
      }
   }

   /**
    * Handles 'set pub key' button event.
    *
    * @throws CardServiceException if something goes wrong.
    */
   void setPubKey() throws CardServiceException {
      try {
         byte[] data = readFile();
         X509EncodedKeySpec spec = new X509EncodedKeySpec(data);
         KeyFactory factory = KeyFactory.getInstance("RSA");
         RSAPublicKey key = (RSAPublicKey)factory.generatePublic(spec);

         byte[] modulus = getBytes(key.getModulus());

         CommandAPDU capdu;
         capdu = new ISOCommandAPDU(CLA_CRYPTO,INS_SET_PUB_MODULUS,
                                    (byte)0,(byte)0,modulus);
         sendCommandAPDU(capdu);

         byte[] exponent = getBytes(key.getPublicExponent());
         capdu = new ISOCommandAPDU(CLA_CRYPTO,INS_SET_PUB_EXP,
                                    (byte)0,(byte)0,exponent);
         sendCommandAPDU(capdu);
      } catch (Exception e) {
         throw new CardServiceException(e.getMessage());
      }
   }

   /**
    * Handles 'set priv key' button event.
    *
    * @throws CardServiceException if something goes wrong.
    */
   void setPrivKey() throws CardServiceException {
      try {
         byte[] data = readFile();
         PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(data);
         KeyFactory factory = KeyFactory.getInstance("RSA");
         RSAPrivateKey key = (RSAPrivateKey)factory.generatePrivate(spec);

         byte[] modulus = getBytes(key.getModulus());
         CommandAPDU capdu; 
         capdu = new ISOCommandAPDU(CLA_CRYPTO,INS_SET_PRIV_MODULUS,
                                    (byte)0,(byte)0,modulus);
         sendCommandAPDU(capdu);

         byte[] exponent = getBytes(key.getPrivateExponent());
         capdu = new ISOCommandAPDU(CLA_CRYPTO,INS_SET_PRIV_EXP,
                                    (byte)0,(byte)0,exponent);
         sendCommandAPDU(capdu);

      } catch (Exception e) {
         throw new CardServiceException(e.getMessage());
      }
   }

   /**
    * Handles 'issue' button event.
    *
    * @throws CardServiceException if something goes wrong.
    */
   void issue() throws CardServiceException {
      try {
         CommandAPDU capdu =
            new ISOCommandAPDU(CLA_CRYPTO,INS_ISSUE,(byte)0,(byte)0);
         sendCommandAPDU(capdu);
      } catch (Exception e) {
         throw new CardServiceException(e.getMessage());
      }
   }

   /**
    * Handles 'encrypt' button event.
    *
    * @throws CardServiceException if something goes wrong.
    */
   void encrypt() throws CardServiceException {
      try {
         byte[] data = readFile();
         if (data.length > BLOCKSIZE) {
            throw new CardServiceException("File too large.");
         }
         CommandAPDU capdu =
            new ISOCommandAPDU(CLA_CRYPTO,INS_ENCRYPT,
                               (byte)0,(byte)0,data,BLOCKSIZE);
         sendCommandAPDU(capdu);
      } catch (IOException e) {
         throw new CardServiceException(e.getMessage());
      }
   }

   /**
    * Handles 'decrypt' button event.
    *
    * @throws CardServiceException if something goes wrong.
    */
   void decrypt() throws CardServiceException {
      try {
         byte[] data = readFile();
         if (data.length > BLOCKSIZE) {
            throw new CardServiceException("File too large.");
         }
         CommandAPDU capdu =
            new ISOCommandAPDU(CLA_CRYPTO,INS_DECRYPT,
                               (byte)0,(byte)0,data,BLOCKSIZE);
         sendCommandAPDU(capdu);
      } catch (IOException e) {
         throw new CardServiceException(e.getMessage());
      }
   }

   /**
    * Selects the applet with AID <code>aid</code>.
    *
    * @param aid the AID to select.
    *
    * @throws CardTerminalException if something goes wrong.
    */
   ResponseAPDU selectApplet(byte[] aid) throws CardTerminalException {
      CommandAPDU capdu =
         new ISOCommandAPDU(CLA_ISO,INS_SELECT,(byte)0x04,(byte)0x00,aid,0);
      return sendCommandAPDU(capdu);
   }

   /**
    * Sends a command to the card.
    *
    * @param capdu the command to send.
    *
    * @return the response from the card.
    *
    * @throws CardTerminalException if something goes wrong.
    */
   ResponseAPDU sendCommandAPDU(CommandAPDU capdu)
   throws CardTerminalException {
      log(capdu);
      ResponseAPDU rapdu = service.sendCommandAPDU(capdu);
      log(rapdu);
      return rapdu;
   }

   /**
    * Writes <code>obj</code> to the log.
    *
    * @param obj the message to write to the log.
    */
   void log(Object obj) {
      display.append(obj.toString() + "\n");
      System.out.println(obj.toString());
   }

   /**
    * Pops up dialog to ask user to select file and reads the file.
    *
    * @return byte array with contents of the file.
    *
    * @throws IOException if file could not be read.
    */
   byte[] readFile() throws IOException {
      JFileChooser chooser = new JFileChooser();
      chooser.setCurrentDirectory(currentDir);
      int n = chooser.showOpenDialog(this);
      if (n != JFileChooser.APPROVE_OPTION) {
         throw new IOException("No file selected");
      }
      File file = chooser.getSelectedFile();
      FileInputStream in = new FileInputStream(file);
      int length = in.available();
      byte[] data = new byte[length];
      in.read(data);
      in.close();
      currentDir = file.getParentFile();
      return data;
   }

   /**
    * Gets an unsigned byte array representation of <code>big</code>.
    * A leading zero (present only to hold sign bit) is stripped.
    *
    * @param big a big integer.
    *
    * @return a byte array containing a representation of <code>big</code>.
    */
   byte[] getBytes(BigInteger big) {
      byte[] data = big.toByteArray();
      if (data[0] == 0) {
         byte[] tmp = data;
         data = new byte[tmp.length - 1];
         System.arraycopy(tmp,1,data,0,tmp.length - 1);
      }
      return data;
   }

   /**
    * Creates an instance of this class and puts it inside a frame.
    *
    * @param arg command line arguments.
    */
   public static void main(String[] arg) {
      JFrame frame = new JFrame(TITLE);
      Container c = frame.getContentPane();
      CryptoTerminal panel = new CryptoTerminal();
      c.add(panel);
      frame.addWindowListener(new CloseEventListener());
      frame.pack();
      frame.setVisible(true);
   }
}

/**
 * Class to close window.
 */
class CloseEventListener extends WindowAdapter
{
   /**
    * What to do when user closes window.
    *
    * @param we window event.
    */
   public void windowClosing(WindowEvent we) {
      try {
         SmartCard.shutdown();
      } catch (Exception e) {
      } finally {
         System.exit(0);
      }
   }
}

