/** * 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: whitedavidp@fastmail.us * * 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 (|<-stdin>) [password]\n"); * * where: 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: 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 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(); 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 (|<-stdin>) [password]\n"); System.err.println (" where: 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: 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); } }