/**
* 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);
}
}