Download | Plain Text | Line Numbers


/*
 * Copyright (c) 2010, Manuel Mausz. 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.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - The names of the authors may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 
import static java.lang.System.err;
import static java.lang.System.out;
 
import java.util.Arrays;
import java.util.MissingResourceException;
 
import javax.crypto.*;
import java.security.*;
import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.openssl.PasswordFinder;
 
import java.net.*;
import java.io.*;
 
/*
 * Client implementation for Lab#1 of DSLab WS10
 * See angabe.pdf for details
 *
 * This code is not documented at all. This is volitional
 *
 * @author Manuel Mausz (0728348)
 */
public class Client
{
  public class ProxyConnection
    extends CommandNetwork
    implements Runnable
  {
    private final Utils.EncObjectInputStream oin;
    private final Utils.EncObjectOutputStream oout;
    private final String sharedFolder;
    private final Object mainLock;
    private final Object intLock;
 
    ProxyConnection(Utils.EncObjectInputStream oin, Utils.EncObjectOutputStream oout,
        String sharedFolder, Object mainLock, Object intLock)
      throws NoSuchMethodException
    {
      this.oin          = oin;
      this.oout         = oout;
      this.sharedFolder = sharedFolder;
      this.mainLock     = mainLock;
      this.intLock      = intLock;
 
      cmdHandler.register("!error",    this, "cmdError");
      cmdHandler.register("!output",   this, "cmdOutput");
      cmdHandler.register("!download", this, "cmdDownload");
      cmdHandler.register("!upload",   this, "cmdUpload");
      cmdHandler.register("!ok",       this, "cmdOk");
      cmdHandler.register("unknown",   this, "cmdUnknown");
    }
 
    /*------------------------------------------------------------------------*/
 
    public void notifyInteractive()
    {
      synchronized(intLock)
      {
        intLock.notify();
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdPrintOutput(PrintStream out, String cmd, String[] args)
      throws IOException
    {
      long num;
      if ((num = Utils.parseHeaderNum(args, 0)) < 0)
        return;
 
      String msg;
      for (; num > 0 && (msg = oin.readLine()) != null; --num)
        out.println(msg.substring(1));
      notifyInteractive();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdError(String cmd, String[] args)
      throws IOException
    {
      cmdPrintOutput(err, cmd, args);
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdOutput(String cmd, String[] args)
      throws IOException
    {
      cmdPrintOutput(out, cmd, args);
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdDownload(String cmd, String[] args)
    {
      if (args.length < 2 || args[1].length() <= 0)
      {
        err.println("Error: Invalid " + cmd + "-command paket from proxy. Ignoring...");
        notifyInteractive();
        return;
      }
 
      long filesize;
      if ((filesize = Utils.parseHeaderNum(args, 1)) < 0)
        return;
 
      File file = new File(sharedFolder, args[0]);
      file.delete();
      try
      {
        FileOutputStream fout = null;
        try
        {
          fout = new FileOutputStream(file);
        }
        catch(FileNotFoundException e)
        {
          err.println("Error: Unable to write to file '" + file + "': " + e.getMessage());
        }
 
        byte[] buffer = new byte[8 * 1024];
        int toread = buffer.length;
        while(filesize > 0)
        {
          if (filesize < toread)
            toread = (int) filesize;
          int count = oin.read(buffer, 0, toread);
          if (count == -1)
            throw new IOException("Connection reset by peer");
 
          if (fout != null)
          {
            /* decode that chunk */
            byte[] decbuffer = new byte[aesdecrypt.getOutputSize(count)];
            int deccount = aesdecrypt.update(buffer, 0, count, decbuffer);
            fout.write(decbuffer, 0, deccount);
          }
          filesize -= count;
        }
        if (fout != null)
        {
          /* decryption must be finalized */
          byte[] decbuffer = aesdecrypt.doFinal();
          fout.write(decbuffer);
        }
      }
      catch(IOException e)
      {
        err.println("Error: Error during file transfer: " + e.getMessage());
      }
      catch(GeneralSecurityException e)
      {
        err.println("Error: Error during decrypting file transfer: " + e.getMessage());
      }
 
      notifyInteractive();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUpload(String cmd, String[] args)
    {
      if (args.length < 1 || args[0].length() <= 0)
      {
        err.println("Error: Invalid " + cmd + "-command paket from proxy. Ignoring...");
        notifyInteractive();
        return;
      }
 
      File file = new File(sharedFolder, args[0]);
      if (!file.exists() || !file.getParent().equals(sharedFolder))
      {
        err.println("Error: File '" + args[0] + "' does not exist");
        notifyInteractive();
        return;
      }
      if (!file.isFile())
      {
        err.println("Error: File '" + args[0] + "' is not a file");
        notifyInteractive();
        return;
      }
      if (!file.canRead())
      {
        err.println("Error: File '" + args[0] + "' is not readable");
        notifyInteractive();
        return;
      }
 
      long filesize = file.length();
      try
      {
        FileInputStream fin = new FileInputStream(file);
 
        oout.writeLine("!upload2 " + file.getName() + " " + filesize);
 
        byte[] buffer = new byte[8 * 1024];
        int toread = buffer.length;
        while(filesize > 0)
        {
          if (filesize < toread)
            toread = (int) filesize;
          int count = fin.read(buffer, 0, toread);
          if (count == -1)
            throw new IOException("Error while reading from file");
 
          /* encode that chunk */
          byte[] encbuffer = new byte[aesencrypt.getOutputSize(count)];
          int enccount = aesencrypt.update(buffer, 0, count, encbuffer);
 
          oout.write(encbuffer, 0, enccount);
          filesize -= count;
        }
        /* encryption must be finalized */
        oout.write(aesencrypt.doFinal());
        oout.flush();
      }
      catch(FileNotFoundException e)
      {
        err.println("Error: File '" + args[0] + "' is not readable");
        notifyInteractive();
        return;
      }
      catch(IOException e)
      {
        err.println("Error during file transfer: " + e.getMessage());
        notifyInteractive();
        return;
      }
      catch(GeneralSecurityException e)
      {
        err.println("Error during encrypting file transfer: " + e.getMessage() + "");
        notifyInteractive();
        return;
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdOk(String cmd, String[] args)
      throws IOException
    {
      if (args.length != 4)
      {
        err.println("Error: Invalid " + cmd + "-command paket from proxy. Ignoring...");
        notifyInteractive();
        return;
      }
 
      if (!args[0].equals(mychallenge))
      {
        err.println("Error: Received invalid challenge from proxy. Ignoring...");
        notifyInteractive();
        return;
      }
 
      String secondMessage = cmd + " " + Utils.join(Arrays.asList(args), " ");
      assert secondMessage.matches("!ok [" + B64 + "]{43}= [" + B64 + "]{43}= [" + B64 + "]{43}= [" + B64 + "]{22}==") : "2nd message";
 
      String proxychallenge = args[1];
      javax.crypto.spec.SecretKeySpec seckey = new javax.crypto.spec.SecretKeySpec(Base64.decode(args[2].getBytes()), "AES");
      javax.crypto.spec.IvParameterSpec iv = new javax.crypto.spec.IvParameterSpec(Base64.decode(args[3].getBytes()));
 
      try
      {
        aesencrypt.init(Cipher.ENCRYPT_MODE, seckey, iv);
        aesdecrypt.init(Cipher.DECRYPT_MODE, seckey, iv);
      }
      catch(InvalidKeyException e)
      {
        err.println("Error: invalid AES key: " + e.getMessage());
        notifyInteractive();
        return;
      }
      catch(InvalidAlgorithmParameterException e)
      {
        err.println("Error: invalid AES parameter: " + e.getMessage());
        notifyInteractive();
        return;
      }
 
      oout.setCipher(aesencrypt);
      oin.setCipher(aesdecrypt);
 
      String thirdMessage = proxychallenge;
      assert thirdMessage.matches("[" + B64 + "]{43}=") : "3rd message"; 
      oout.writeLine(thirdMessage);
      oout.flush();
      loggedin = true;
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUnknown(String cmd, String[] args)
    {
      err.println("Error: Unknown data from proxy: " + cmd + " "
          + Utils.join(Arrays.asList(args), " "));
      notifyInteractive();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        run(oin);
      }
      catch(CommandHandler.Exception e)
      {
        err.println("Internal Error: " + e.getMessage());
      }
      catch(IOException e)
      {
        /* ignore that exception
         * thread will shutdown and unlock the main thread
         * which will shutdown the application
         */
      }
 
      notifyInteractive();
      synchronized(mainLock)
      {
        mainLock.notify();
      }
    }
  }
 
  /*==========================================================================*/
 
  public class Interactive
    extends CommandInteractive
    implements Runnable
  {
    private final InputStream sin;
    private final Utils.EncObjectInputStream oin;
    private final Utils.EncObjectOutputStream oout;
    private final Object mainLock;
    private final Object intLock;
 
    Interactive(InputStream sin, Utils.EncObjectInputStream oin,
        Utils.EncObjectOutputStream oout, Object mainLock, Object intLock)
      throws NoSuchMethodException
    {
      this.sin      = sin;
      this.mainLock = mainLock;
      this.intLock  = intLock;
      this.oin      = oin;
      this.oout     = oout;
 
      cmdHandler.register("unknown", this, "cmdUnknown");
      cmdHandler.register("!login",  this, "cmdLogin");
      cmdHandler.register("!exit",   this, "cmdExit");
    }
 
    /*------------------------------------------------------------------------*/
 
    public void waitForSocket()
    {
      synchronized(intLock)
      {
        try
        {
          intLock.wait(1000);
        }
        catch(InterruptedException e)
        {
          /* if we get interrupted -> ignore */
        }
      }
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdUnknown(String cmd, String[] args)
      throws IOException
    {
      if (!loggedin)
      {
        err.println("Not logged in");
        return;
      }
 
      oout.writeLine(cmd + " " + Utils.join(Arrays.asList(args), " "));
      oout.flush();
      waitForSocket();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdLogin(String cmd, String[] args)
      throws IOException
    {
      if (args.length != 1)
      {
        err.println("Invalid Syntax: !login <username>");
        return;
      }
      if (loggedin)
      {
        err.println("Already logged in");
        return;
      }
 
      /* read users private key */
      final String user = args[0];
      File pemfile = new File(keysDir, user + ".pem");
      if (!pemfile.isFile())
      {
        err.println("Error: Private keyfile '" + pemfile + "' doesn't exist.");
        return;
      }
      if (!pemfile.canRead())
      {
        err.println("Error: Private keyfile '" + pemfile + "' is not readable.");
        return;
      }
 
      try
      {
        PEMReader in = new PEMReader(new FileReader(pemfile), new PasswordFinder()
            {
              @Override
              public char[] getPassword()
              {
                try
                {
                  /* reads the password from standard input for decrypting the private key */
                  out.println("Enter pass phrase: ");
                  return new BufferedReader(new InputStreamReader(sin)).readLine().toCharArray();
                }
                catch(IOException e)
                {
                  char[] tmp = {};
                  return tmp;
                }
              }
            });
        KeyPair keyPair = (KeyPair) in.readObject();
        privateKey = keyPair.getPrivate();
        rsadecrypt.init(Cipher.DECRYPT_MODE, privateKey);
        oin.setCipher(rsadecrypt);
      }
      catch(FileNotFoundException e)
      {
        err.println("Error while reading private key of " + user + ". Unable to read keyfile.");
        return;
      }
      catch(IOException e)
      {
        err.println("Error while reading private key of " + user + ". Maybe wrong pass phrase.");
        return;
      }
      catch(InvalidKeyException e)
      {
        err.println("Error: invalid key file: " + e.getMessage());
        return;
      }
 
      /* generates a 32 byte secure random number */
      SecureRandom secureRandom = new SecureRandom();
      byte[] tmp = new byte[32];
      secureRandom.nextBytes(tmp);
      mychallenge = new String(Base64.encode(tmp));
 
      String firstMessage = cmd + " " + Utils.join(Arrays.asList(args), " ") + " " + mychallenge;
      assert firstMessage.matches("!login \\w+ [" + B64 + "]{43}=") : "1st message";
 
      oout.writeLine(firstMessage);
      oout.flush();
      waitForSocket();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void cmdExit(String cmd, String[] args)
    {
      stop();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void printPrompt()
    {
      out.print(">: ");
      out.flush();
    }
 
    /*------------------------------------------------------------------------*/
 
    public void shutdown()
    {
      try
      {
        oout.flush();
      }
      catch(IOException e)
      {}
    }
 
    /*------------------------------------------------------------------------*/
 
    public void run()
    {
      try
      {
        run(sin);
      }
      catch(CommandHandler.Exception e)
      {
        err.println("Internal Error: " + e.getMessage());
      }
      catch(IOException e)
      {
        /* ignore that exception
         * thread will shutdown and unlock the main thread
         * which will shutdown the application
         */
      }
 
      shutdown();
      synchronized(mainLock)
      {
        mainLock.notify();
      }
    }
  }
 
  /*==========================================================================*/
 
  private static String sharedFolder;
  private static String proxyHost;
  private static int proxyTCPPort;
  private Socket sock = null;
  private InputStream sockin = null;
  private OutputStream sockout = null;
  private Utils.EncObjectInputStream oin = null;
  private Utils.EncObjectOutputStream oout = null;
  private Thread tPConnection = null;
  private Thread tInteractive = null;
  private InputStream stdin = null;
  private final Object interactiveLock = new Object();
  private final Object mainLock = new Object();
 
  private static String keysDir;
  private static String proxyKey;
  private String mychallenge;
  private PrivateKey privateKey = null;
  private Cipher rsaencrypt, rsadecrypt;
  private Cipher aesencrypt, aesdecrypt;
  private volatile boolean loggedin = false;
  private final String B64 = "a-zA-Z0-9/+";
 
  /*--------------------------------------------------------------------------*/
 
  public Client()
  {
    try
    {
      rsaencrypt = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
      rsadecrypt = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
      aesencrypt = Cipher.getInstance("AES/CTR/NoPadding");
      aesdecrypt = Cipher.getInstance("AES/CTR/NoPadding");
    }
    catch(NoSuchAlgorithmException e)
    {
      bailout("Unable to initialize cipher: " + e.getMessage());
    }
    catch(NoSuchPaddingException e)
    {
      bailout("Unable to initialize cipher: " + e.getMessage());
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public static void usage()
    throws Utils.Shutdown
  {
    out.println("Usage: Client sharedFolder\n");
    out.println("sharedFolder\t...the directory to put downloaded files or to upload files from");
 
    // Java is some piece of crap which doesn't allow me to set exitcode w/o
    // using System.exit. Maybe someday Java will be a fully functional
    // programming language, but I wouldn't bet my money
    //System.exit(1);
    throw new Utils.Shutdown("FUCK YOU JAVA");
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void bailout(String error)
    throws Utils.Shutdown
  {
    err.println("Error: " + error);
    shutdown();
 
    // Java is some piece of crap which doesn't allow me to set exitcode w/o
    // using System.exit. Maybe someday Java will be a fully functional
    // programming language, but I wouldn't bet my money
    //System.exit(2);
    throw new Utils.Shutdown("FUCK YOU JAVA");
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void parseArgs(String[] args)
  {
    if (args.length != 1)
      usage();
 
    sharedFolder = args[0];
    File dldir = new File(sharedFolder);
    if (!dldir.isDirectory())
      bailout("sharedFolder '" + sharedFolder + "' is not a directory");
    if (!dldir.canWrite())
      bailout("sharedFolder '" + sharedFolder + "' is not writeable");
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void parseConfig()
  {
    Config config = null;
    try
    {
      config = new Config("client");
    }
    catch(MissingResourceException e)
    {
      bailout("configuration file doesn't exist or isn't readable");
    }
 
    String directive = null;
    try
    {
      directive = "proxy.host";
      proxyHost = config.getString(directive);
      if (proxyHost.length() == 0)
        bailout("configuration directive '" + directive + "' is empty or invalid");
 
      directive = "proxy.tcp.port";
      proxyTCPPort = config.getInt(directive);
      if (proxyTCPPort <= 0 || proxyTCPPort > 65536)
        bailout("configuration directive '" + directive + "' must be a valid port number (1 - 65535)");
 
      directive = "proxy.key";
      proxyKey = config.getString(directive);
      File key = new File(proxyKey);
      if (!key.isFile())
        bailout("configuration directive '" + directive + "' is not a file");
      if (!key.canRead())
        bailout("configuration directive '" + directive + "' is not readable");
      PEMReader in = new PEMReader(new FileReader(key));
      PublicKey publicKey = (PublicKey) in.readObject();
      rsaencrypt.init(Cipher.ENCRYPT_MODE, publicKey);
 
      directive = "keys.dir";
      keysDir = config.getString(directive);
      File dir = new File(keysDir);
      if (!dir.isDirectory())
        bailout("configuration directive '" + directive + "' is not a directory");
      if (!dir.canRead())
        bailout("configuration directive '" + directive + "' is not readable");
    }
    catch(FileNotFoundException e)
    {
      bailout("unable to read file of directive '" + directive + "'");
    }
    catch(IOException e)
    {
      bailout("while reading proxy public key");
    }
    catch(InvalidKeyException e)
    {
      bailout("invalid key file: " + e.getMessage());
    }
    catch(MissingResourceException e)
    {
      bailout("configuration directive '" + directive + "' is not set");
    }
    catch(NumberFormatException e)
    {
      bailout("configuration directive '" + directive + "' must be numeric");
    }
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void shutdown()
  {
    try
    {
      if (sockin != null)
        sockin.close();
    }
    catch(IOException e)
    {}
 
    try
    {
      if (sockout != null)
        sockout.close();
    }
    catch(IOException e)
    {}
 
    try
    {
      if (sock != null && !sock.isClosed())
        sock.close();
    }
    catch(IOException e)
    {}
 
    try
    {
      if (tPConnection != null)
        tPConnection.join();
    }
    catch(InterruptedException e)
    {}
 
    try
    {
      if (tInteractive != null)
      {
        tInteractive.interrupt();
        tInteractive.join();
      }
    }
    catch(InterruptedException e)
    {}
 
    try
    {
      if (stdin != null)
        stdin.close();
    }
    catch(IOException e)
    {}
  }
 
  /*--------------------------------------------------------------------------*/
 
  public void run(String[] args)
  {
    parseArgs(args);
    parseConfig();
 
    try
    {
      out.println("Connecting to " + proxyHost + ":" + proxyTCPPort + "...");
      sock = new Socket(proxyHost, proxyTCPPort);
      sockin = sock.getInputStream();
      sockout = sock.getOutputStream();
      out.println("Connected...");
    }
    catch(UnknownHostException e)
    {
      bailout("Unable to resolve hostname: " + e.getMessage());
    }
    catch(IOException e)
    {
      bailout("Unable to connect to proxy: " + e.getMessage());
    }
 
    synchronized(mainLock)
    {
      try
      {
        oin = new Utils.EncObjectInputStream(new BufferedInputStream(sockin));
        oout = new Utils.EncObjectOutputStream(sockout);
        oout.setCipher(rsaencrypt);
      }
      catch(IOException e)
      {
        bailout("Unable to create object stream: " + e.getMessage());
      }
 
      try
      {
        tPConnection = new Thread(new ProxyConnection(oin, oout,
              sharedFolder, mainLock, interactiveLock));
        tPConnection.start();
      }
      catch(NoSuchMethodException e)
      {
        bailout("Unable to setup remote command handler");
      }
 
      try
      {
        InputStream stdin = java.nio.channels.Channels.newInputStream(
            new FileInputStream(FileDescriptor.in).getChannel());
        tInteractive = new Thread(new Interactive(stdin, oin, oout,
              mainLock, interactiveLock));
        tInteractive.start();
      }
      catch(NoSuchMethodException e)
      {
        bailout("Unable to setup interactive command handler");
      }
 
      out.println("Client startup successful!");
      try
      {
        mainLock.wait();
      }
      catch(InterruptedException e)
      {
        /* if we get interrupted -> ignore */
      }
 
      try
      {
        /* let the threads shutdown */
        Thread.sleep(100);
      }
      catch(InterruptedException e)
      {}
    }
 
    if (tPConnection != null && !tPConnection.isAlive())
      bailout("Connection to proxy closed unexpected. Terminating...");
 
    shutdown();
  }
 
  /*--------------------------------------------------------------------------*/
 
  public static void main(String[] args)
  {
    try
    {
      Client cli = new Client();
      cli.run(args);
    }
    catch(Utils.Shutdown e)
    {}
  }
}