/**
*  KeyPass2KeyRing by David White based almost entirely on KKConvert by Hugo Haas
*    (see http://larve.net/2006/KKConvert/) and, like KKConvert, uses code from
*    KeyRing Editor (see http://www.ict.tuwien.ac.at/keyring/). We all stand on
*    the shoulders of giants - thanks to all! For support email: [email protected]
*
*  This cobbled-together program is used to convert KeePass version 2.x (see
*    http://keepass.info/information) for use in GNU KeyRing, a similar program
*    available on the Palm OS platform (see http://gnukeyring.sourceforge.net/).
*
*  Rather than directly opening a KeePass database, this program accepts input in
*    the format presented by the ListEntries command of the KPScript addon (see
*    http://keepass.info/help/v2_dev/scr_index.html) for KeePass version 2.x.
*
*   Usage: java KeePass2KeyRing (<source>|<-stdin>) <target> [password]\n");
*
*   where: <source> is the path/name of the input file created by a
*                 KPScript.exe -c:ListEntries command.
*
*     and: <-stdin> means that the input file should be read from the standard
*                 input as via a pipe. Password is REQUIRED here.
*
*     and: <target> is the path/name of the output file which typically is
*                 named Keys-Gtkr.PDB. This file MUST already exist and be
*                 a valid Keyring database file.
*
*     and: [password] is that used to decrypt the output file. This argument
*                 is REQUIRED if -stdin is specified.
*
*  NOTE: this program does not generate a KeyRing database on its own. You must
*    supply one. KeePass2KeyRing will empty the contents of that database and then
*    fills it with data from the KeePass database. All prior contents will be lost
*    so use some care.
*/

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

public class KeePass2KeyRing
{
 class CurrentEntry
 {
   int category = 0;
   boolean inComment = false;
   String title = "";
   String userName = "";
   String password = "";
   String notes = "";
   String categoryName = "";
   String url = "";

   String getNotesData()
   {
     String result = (notes == null || "".equals(notes)) ? "" : notes;
     result += (url == null || "".equals(url)) ? "" : "\n" + url;
     return result;
   }
 }

 private static boolean DEBUG = false;
 private static int MAX_CATEGORIES = 16;
 private static int MAX_CATEGORY_LENGTH = 16;
 private static String BACKUP = "Backup";
 private static String UNFILED = "Unfiled";
 private static String DATABASE = "Database";
 private static String UUID = "UUID:";
 private static String GRPN = "GRPN:";
 private static String TITLE = "S: Title =";
 private static String USER_NAME = "S: UserName =";
 private static String PASSWORD = "S: Password =";
 private static String NOTES = "S: Notes =";
 private static String GRPU = "GRPU:";
 private static String URL = "S: URL =";

 private Model model = null;

 // on newer javac the following may generate a warning. this is fine but
 // if you wish to eliminate this, swap this line
 //private Vector<String> categories;
 private Vector categories = null;

 public KeePass2KeyRing()
 {
   model = new Model();

   // on newer javac the following may generate a warning. this is fine but
   // if you wish to eliminate this, swap this line
   //categories = new Vector<String>();
   categories = new Vector();

   categories.add(UNFILED);
 }

 private void trace(String message)
 {
   if(DEBUG)
   {
     System.out.println(message);
   }
 }

 private boolean readPasswords(BufferedReader reader)
 {
   try
   {
           String line;
     CurrentEntry currentEntry = null;

     while(true)
     {
       line = reader.readLine();
       if (line == null)
       {
         if(null != currentEntry)
         {
           // post the current entry to the model
           trace("Adding final entry with: " + currentEntry.title + "," + currentEntry.category + "," + currentEntry.userName + "," + currentEntry.password + "," + currentEntry.getNotesData());
           addEntry(currentEntry.title, currentEntry.category, currentEntry.userName, currentEntry.password, currentEntry.getNotesData());
         }

         break;
       }

       if (line.startsWith(UUID))
       {
         if((null != currentEntry) && (!BACKUP.equals(currentEntry.categoryName)))
         {
           // post the current entry to the model
           trace("Adding new entry with: " + currentEntry.title + "," + currentEntry.category + "," + currentEntry.userName + "," + currentEntry.password + "," + currentEntry.getNotesData());
           addEntry(currentEntry.title, currentEntry.category, currentEntry.userName, currentEntry.password, currentEntry.getNotesData());
         }

         // start a new entry
         currentEntry = new CurrentEntry();
         continue;
       }

       if (line.startsWith(GRPN))
       {
         currentEntry.inComment = false;
         currentEntry.categoryName = extractData(line, GRPN);

         // keyring doesn't like category names > 16 characters
         if(currentEntry.categoryName.length() > MAX_CATEGORY_LENGTH)
         {
           currentEntry.categoryName = currentEntry.categoryName.substring(0, MAX_CATEGORY_LENGTH);
         }

         // prevent creating a Backup category
         if(BACKUP.equals(currentEntry.categoryName))
         {
           trace("An entry in the Backup group is being ignored");
           continue;
         }

         // entries in the root are given the group "database" but I
         // consider these to be "unfiled" on the Palm
         if(DATABASE.equals(currentEntry.categoryName))
         {
           trace("An entry from the Database group is being placed into Unfiled");
           currentEntry.categoryName = UNFILED;
         }

         if (!categories.contains(currentEntry.categoryName))
         {
           // keyring supports only 16 categories so if we encounter any more
           // than 16, put the new ones in the Unfiled category
           if(categories.size() < MAX_CATEGORIES)
           {
             categories.add(currentEntry.categoryName);
           }
           else
           {
             trace("More than 16 categories are in use, an entry is being placed into Unfiled");
             currentEntry.categoryName = UNFILED;
           }

           currentEntry.category = categories.indexOf(currentEntry.categoryName);
           trace("Added category: " + currentEntry.categoryName + " at #" + currentEntry.category);
         }
         else
         {
           currentEntry.category = categories.indexOf(currentEntry.categoryName);
           trace("Using existing category: " + currentEntry.categoryName + " at #" + currentEntry.category);
         }

         continue;
       }

       if (line.startsWith(TITLE))
       {
         currentEntry.inComment = false;
         currentEntry.title = extractData(line, TITLE);
         trace("Added title: " + currentEntry.title);
         continue;
       }

       if (line.startsWith(USER_NAME))
       {
         currentEntry.inComment = false;
         currentEntry.userName = extractData(line, USER_NAME);
         trace("Added user name: " + currentEntry.userName);
         continue;
       }

       if (line.startsWith(PASSWORD))
       {
         currentEntry.inComment = false;
         currentEntry.password = extractData(line, PASSWORD);
         trace("Added password: " + currentEntry.password);
         continue;
       }

       if (line.startsWith(NOTES))
       {
         currentEntry.notes = extractData(line, NOTES);
         trace("Starting note with: " + currentEntry.notes);
         currentEntry.inComment = true;
         continue;
       }

       if(line.startsWith(GRPU))
       {
         currentEntry.inComment = false;
         continue;
       }

       if(line.startsWith(URL))
       {
         currentEntry.inComment = false;
         currentEntry.url = extractData(line, URL);
         trace("Added url: " + currentEntry.url);
         continue;
       }

       if(null != currentEntry && currentEntry.inComment)
       {
         currentEntry.notes += "\n" + line;
         trace("Continue note with: " + line);
         continue;
       }
     }
 }
 catch (Exception e)
 {
   trace("Exception: " + e.toString());
   e.printStackTrace();
   return false;
       }

 return true;
}

 private String extractData(String data, String key)
 {
   if(!data.startsWith(key))
   {
     throw new RuntimeException("Data does not begin with the expected key");
   }

   if(data.length() <= key.length())
   {
     return "";
   }

   return data.substring(key.length() + 1);
 }

 private void addEntry(String title, int category, String account, String password, String notes) throws Exception
 {
   byte[] ciphertext = null;

   int entryId = model.getEntriesSize() + 1;
   int uniqueId = model.getNewUniqueId();
   byte[] record = Model.toRecordFormat4(account + "\0" + password + "\0" + notes + "\0");

   try
   {
           ciphertext = model.crypto.encrypt(record);
   }
   catch (Exception e)
   {
     System.err.println("Error encrypting entry.");
     throw e;
   }

   int len = title.length() + ciphertext.length - 16 + 1;

   Entry entry = new Entry(entryId,
                           title,
                           category,
                           Model.sliceBytes(ciphertext, 16, ciphertext.length - 16),
                           model.crypto,
                           category | 0x40, // ???
                           uniqueId,
                           len,
                           null);
   model.addEntry(entry);
 }

 public static void main(String[] args)
 {
   boolean readFromStdin = false;
   BufferedReader reader = null;

   if (args.length < 2 || args.length > 3)
   {
     displayUsage();
   }

   String source = args[0];
   String target = args[1];
   String password = null;

   if(args.length == 3)
   {
     password = args[2];
   }

   if("-stdin".equals(source))
   {
     if(null == password)
     {
       displayUsage();
     }

     readFromStdin = true;
     reader = new BufferedReader(new InputStreamReader(System.in));
   }
   else
   {
     if(password == null)
     {
       BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
       try
       {
         System.out.print("Enter your KeyRing password: ");
         password = stdin.readLine();
       }
       catch (Exception e)
       {
         System.err.println("You need to enter a password!");
         System.exit(1);
       }
     }
   }

   KeePass2KeyRing converter = new KeePass2KeyRing();
   converter.model = new Model();
   try
   {
     converter.model.loadData(target);
           char[] p = new char [password.length()];
           password.getChars(0, password.length(), p, 0);
     converter.model.crypto.setPassword(p);
   }
   catch (Exception e)
   {
           System.err.println("Error generating database:");
           e.printStackTrace();
     System.exit(1);
   }

   Vector v = converter.model.getEntries();
   Object[] o = v.toArray();
   for(int i = 0; i < o.length; i++)
   {
     converter.model.removeEntry(o[i]);
   }

   if(!readFromStdin)
   {
     FileReader fileReader = null;

     try
     {
       fileReader = new FileReader(source);
     }
     catch (Exception e)
     {
       System.err.println("Error opening password file: " + e.getMessage());
       System.exit(1);
     }

     reader = new BufferedReader(fileReader);
   }

   if(!converter.readPasswords(reader))
   {
     System.err.println("Error processing password file");
     System.exit(1);
   }

   converter.trace("Categories are:");
   for(int i = 0; i < converter.categories.size(); i++)
   {
     converter.trace(((String)converter.categories.elementAt(i)) + " at #" + i);
   }

   converter.model.setCategories(converter.categories);

   try
   {
     converter.model.saveData(target);
   }
   catch (Exception e)
   {
           System.err.println("Error writing database:");
           e.printStackTrace();
           System.exit(4);
   }

   System.exit(0);
 }

 private static void displayUsage()
 {
   System.err.println ("\nUsage: java KeePass2KeyRing (<source>|<-stdin>) <target> [password]\n");
   System.err.println ("  where: <source> is the path/name of the input file created by a");
   System.err.println ("                KPScript.exe -c:ListEntries command.\n");
   System.err.println ("    and: <-stdin> means that the input file should be read from the standard");
   System.err.println ("                input as via a pipe. Password is REQUIRED here.\n");
   System.err.println ("    and: <target> is the path/name of the output file which typically is");
   System.err.println ("                named Keys-Gtkr.PDB. This file MUST already exist and be");
   System.err.println ("                a valid Keyring database file.\n");
   System.err.println ("    and: [password] is that used to decrypt the output file. This argument");
   System.err.println ("                is REQUIRED if -stdin is specified.\n");
   System.exit(1);
 }
}