/*
 * Copyright (C) 2005 Luca Veltri - University of Parma - Italy
 * 
 * This file is part of MjSip (http://www.mjsip.org)
 * 
 * MjSip is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * MjSip is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with MjSip; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * Author(s):
 * Luca Veltri (luca.veltri@unipr.it)
 */

package org.zoolu.tools;


import java.io.*;
import java.util.Random;


/** OTP (One Time Pad) encryption algorithm based on MD5 hash function.
  * It uses a PRG (Pseudo-Random-Generator) function in OFB (Output Feadback) 
  * to genarate a byte-stream (the OTP) that is XORed with the plaintext. 
  *<br> The PRG is based on MD5.
  *
  * <p> The OTP is calculated starting from a key and an IV, as follows:
  *<br> h_0=hash(skey|iv)
  *<br> h_i=hash(skey|h_i-1)  
  *
  * <p> where:
  *<br> hash(.)==MD5(.)
  *<br> skey==key
  *
  * <p> while the ciphertext is calculated as follows:
  *<br> c_0=iv
  *<br> c_i=m_i XOR h_i   with i=1,2,..
  *
  * <p> Note that optionally it could modified initializing the skey as follows:
  *<br> skey==hash(key|iv)
  *<br> in order to not keep in memory the secret key for long time
  */
public class MD5OTP
{
   /** Block size in bytes */
   static int size;
   /** the OTP stream key */
   byte[] skey;
   /** pseudorandom-stream (OTP) block */
   byte[] h;
   /** index within a single block */
   int index;
   
   /** Creates a new MD5OTP */
   /*public MD5OTP(int bsize, byte[] key, byte[] iv)
   {  init(bsize,key,iv);
   }*/

   /** Creates a new MD5OTP */
   public MD5OTP(byte[] skey, byte[] iv)
   {  init(16,skey,iv);
   }

   /** Creates a new MD5OTP with IV=0 */
   public MD5OTP(byte[] skey)
   {  init(16,skey,null);
   }
   
   /** Inits the MD5OTP algorithm */
   private void init(int size, byte[] skey, byte[] iv)
   {  this.size=size;
      if (iv==null) { iv=new byte[size]; for (int i=0; i<size; i++) iv[i]=0; }
      this.skey=skey;
      //skey=hash(cat(key,iv));
      h=clone(iv);
      index=size-1;
   }

   /** Makes a clone of a byte array */
   private static byte[] clone(byte[] bb)
   {  byte[] cc=new byte[bb.length];
      for (int i=0; i<bb.length; i++) cc[i]=bb[i];
      return cc;
   }   
   
   /** Concatenates two byte arrays */
   private static byte[] cat(byte[] aa, byte[] bb)
   {  byte[] cc=new byte[aa.length+bb.length];
      for (int i=0; i<aa.length; i++) cc[i]=aa[i];
      for (int i=0; i<bb.length; i++) cc[i+aa.length]=bb[i];
      return cc;
   }   

   /** Returns a sub array */
   private static byte[] sub(byte[] bb, int begin, int end)
   {  byte[] cc=new byte[end-begin];
      for (int i=begin; i<end; i++) cc[i-begin]=bb[i];
      return cc;
   }   

   /** Return an hash of the byte array */
   private static byte[] hash(byte[] bb)
   {  return MD5.digest(bb);
   }

   /** Encrypts a block of bytes */
   public byte[] update(byte[] m)
   {  byte[] c=new byte[m.length];
      for (int i=0; i<m.length; i++)
      {  index++;
         if (index==size)
         {  // calculate new h_block
            h=hash(cat(skey,h));
            index=0;
         }
         c[i]=(byte)(m[i]^h[index]);
      }
      return c;
   }
   
   /** Encrypts a byte stream */
   public void update(InputStream in, OutputStream out)
   {  byte[] buff=new byte[2048];
      int len;
      try
      {  while ((len=in.read(buff))>0)
            out.write(update(sub(buff,0,len)));
      }
      catch (IOException e) { e.printStackTrace(); }
   }

   /** Encrypts an array of bytes. An IV is chosen and saved at top. */
   public static byte[] encrypt(byte[] m, byte[] key)
   {  // choose a random IV
      byte[] iv=MD5.digest(Long.toString((new Random()).nextLong()));
      // do encryption    
      byte[] c=(new MD5OTP(key,iv)).update(m);
      return cat(iv,c);
   }

 
   /** Decrypts an array of bytes with the IV at top. */
   public static byte[] decrypt(byte[] c, byte[] key)
   {  // read the IV
      byte[] iv=sub(c,0,16);
      byte[] buf=sub(c,16,c.length); 
      return (new MD5OTP(key,iv)).update(buf);
   } 


   /** Returns an hex representation of the byte array */
   private static String asHex(byte[] bb)
   {  StringBuffer buf=new StringBuffer(bb.length*2);
      for (int i=0; i<bb.length; i++)
      {  if (((int)bb[i] & 0xff) < 0x10) buf.append("0");
         //buf.append(Long.toString((int)bb[i] & 0xff, 16));
         buf.append(Integer.toHexString((int)bb[i] & 0xff));
      }
      return buf.toString();
   }

   public static void main(String[] args)
   {  
      if (args.length<2)
      {  System.out.println("Usage:\n\n   java MD5OTP <message> <pass_phrase> [<iv>]");
         System.exit(0);
      } 
   
      byte[] msg=args[0].getBytes();     
      byte[] key=args[1].getBytes();
      byte[] iv=null;
      if (args.length>2) iv=args[2].getBytes();
      
      System.out.println("m= "+asHex(msg)+" ("+new String(msg)+")");
      byte[] cip=(new MD5OTP(key,iv)).update(msg);
      System.out.println("c= "+asHex(cip));
      cip=(new MD5OTP(key,iv)).update(cip);
      System.out.println("m= "+asHex(cip)+" ("+new String(cip)+")");
   } 
   
}
