/*
KeyringEditor

Copyright 2004 Markus Griessnig
Vienna University of Technology
Institute of Computer Technology

KeyringEditor is based on:
Java Keyring v0.6
Copyright 2004 Frank Taylor <[email protected]>

These programs are distributed in the hope that they 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.
*/

// Model.java

// 29.10.2004

// 31.10.2004: backup header and categories for saveData()
// 02.11.2004: class entry changed; added parameter -e; added parameter -d
// 03.11.2004: added getDateType()
// 04.11.2004: added getDataFormat()
// 06.11.2004: added debugByteArray()
// 08.11.2004: Categories-Array with 276 byte
// 11.11.2004: addes elements(), getCategoryName(), getCategories(); loadData - empty title possible
// 17.11.2004: setPassword uses char[] (security reason)
// 23.11.2004: added saveEntriesToFile()
// 24.11.2004: updated saveEntriesToFile(); updated saveData()
// 30.11.2004: printHexByteArray() added
// 01.12.2004: Keyring database format 5 support added
// 02.12.2004: toRecordFormat5() added
// 05.12.2004: convertDatabase() added
// 07.12.2004: convertDatabase() updated
// 12.01.2004: writeNewDatabase() added
// 07.09.2005: loadDatabase() ignores deleted record table entries
// 23.09.2005: added getNewUniqueId()

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

/**
* This class is used to load and save Keyring databases.
*/
public class Model {
       // ----------------------------------------------------------------
       // variables
       // ----------------------------------------------------------------

       /**
        * Field type & creator in PDB header information:
        * Used by Palm OS to determine the application for the database.
        * I am not sure if v2.0 will use the same creator-name as v1.2.2.
        * At the moment v2.0-pre1 uses a different creator-name.
        *
        * v2.0-pre4 uses the same creator-name.
        */
       private static String applcreator4 = "GkyrGtkr";
       private static String applcreator5 = "GkyrGtkr";

       public static final boolean DEBUG = false;

       // saveEntriesToFile()
       /**
        * Filename of CSV File
        */
       private static String csvFilename = "keyring.csv"; // default

       /**
        * CSV-Separator
        */
       private static char csvSeparator = ';'; // default

       // PDB header information (readPDBHeader)
       /**
        * Header of Keyring database
        */
       private byte[] pdbHeader = new byte[78];

       /**
        * Categories in Keyring database
        */
       private byte[] pdbCategories = new byte[276];

       private String pdbName;        // 32
       private int pdbFlags;          // 2 (unsigned)

       /**
        * Keyring database version
        */
       protected int pdbVersion;      // 2 (unsigned) // Keyring database format
       private long pdbModNumber;     // 4 (unsigned), modification number
       private int pdbSortInfoOffset; // 4
       private String pdbType;        // 4
       private String pdbCreator;     // 4
       private int pdbAppInfoOffset;  // 4

       /**
        * Number of records in the keyring database
        */
       private int pdbNumRecords;     // 2

       // Keyring database format 4
       private int recordZeroAttribute;
       private int recordZeroUniqueId;
       private int recordZeroLength;

       /**
        * Vector to entry objects
        */
       //private Vector<Entry> entries = new Vector<Entry>(); // Java 1.5
       private Vector entries = new Vector(); // reference to entry objects

       /**
        * Vector to category strings
        */
       //private Vector<String> categories = new Vector<String>(); // Java 1.5
       private Vector categories = new Vector(); // category labels

       /**
        * Reference to class Crypto
        */
       protected Crypto crypto; // Gui.java

       // ----------------------------------------------------------------
       // public ---------------------------------------------------------
       // ----------------------------------------------------------------

       // writeNewDatabase -----------------------------------------------
       /**
        * This method dumps a minimal database with password "test".
        *
        * @param filename New database filename
        */
       public static void writeNewDatabase(String filename) {
               int[] header = {
                       0x4B, 0x65, 0x79, 0x73, 0x2D, 0x47, 0x74, 0x6B, 0x72, 0x00,
                       0x6B, 0x72, 0x5F, 0x61, 0x70, 0x70, 0x6C, 0x5F, 0x61, 0x36,
                       0x38, 0x6B, 0x00, 0x00, 0x73, 0x79, 0x73, 0x70, 0x04, 0x00,
                       0x73, 0x70, 0x00, 0x08, 0x00, 0x04, 0xBD, 0xDB, 0x65, 0x06,
                       0xBD, 0xDB, 0x65, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
                       0x00, 0x0E, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
                       0x47, 0x6B, 0x79, 0x72, 0x47, 0x74, 0x6B, 0x72, 0x00, 0xB7,
                       0x30, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
                       0x01, 0x74, 0x50, 0xB7, 0x30, 0x01, 0x00, 0x00, 0x01, 0x88,
                       0x40, 0xB7, 0x30, 0x02, 0x00, 0x00, 0x1F, 0x1F};

               int[] data = {
                       0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
                       0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x0F, 0x00, 0xB0, 0xDA,
                       0x43, 0x4A, 0x91, 0x55, 0x12, 0xEC, 0xD5, 0x96, 0xCD, 0x21,
                       0x9A, 0xFC, 0x2D, 0x01, 0x9C, 0x2F, 0xC7, 0x13, 0x61, 0x00,
                       0xF0, 0x3B, 0x16, 0xCC, 0x25, 0xCF, 0x49, 0xC0};

               int i;
               File db;
               FileOutputStream fp;
               byte[] cat = new byte[256];
               byte[] cat1 = (new String("no category")).getBytes();

               // open new database
               try {
                       db = new File(filename);
                       fp = new FileOutputStream(db);

                       // write header
                       for(i=0; i<header.length; i++) {
                               fp.write((byte)header[i]);
                       }

                       // write category-names
                       Arrays.fill(cat, (byte)0x00);
                       System.arraycopy(cat1, 0, cat, 0, cat1.length);
                       fp.write(cat, 0, 256);

                       // write password information and record 1
                       for(i=0; i<data.length; i++) {
                               fp.write((byte)data[i]);
                       }

                       fp.close();
               }
               catch(Exception e) {
                       System.err.println("Caught Exception: " + e.getMessage());
               }
       }

       // entries --------------------------------------------------------
       /**
        * This method adds an entry to the vector entries.
        *
        * @param entry Entry object
        */
       public void addEntry(Object entry) {
               entries.add((Entry)entry);
       }

       /**
        * This method removes an entry from the vector entries.
        *
        * @param entry Entry object
        */
       public void removeEntry(Object entry) {
               entries.removeElement(entry);
       }

       /**
        * This method returns the size of vector entries.
        *
        * @return Size of vector entries
        */
       public int getEntriesSize() {
               return entries.size();
       }

       /**
        * This method returns the vector entries.
        *
        * @return Vector entries
        */
       //public Vector<Entry> getEntries() { // Java 1.5
       public Vector getEntries() {
               return entries;
       }

       /**
        * This method returns the enumeration of vector entries.
        *
        * @return Enumeration of vector entries
        */
       public Enumeration getElements() {
               return entries.elements();
       }

       // categories -----------------------------------------------------
       /**
        * This method returns a category name from the vector categories.
        *
        * @param category Index of category in vector categories
        *
        * @return Category name
        */
       public String getCategoryName(int category) throws ArrayIndexOutOfBoundsException {
               return (String)categories.get(category);
       }

       /**
        * This method sets the vector categories to the specified vector.
        *
        * @param myCategories New category vector
        */
       //public void setCategories(Vector<String> myCategories) { // Java 1.5
       public void setCategories(Vector myCategories) {
               categories = myCategories;
       }

       /**
        * This method returns the vector categories.
        *
        * @return Vector categories
        */
       //public Vector<String> getCategories() { // Java 1.5
       public Vector getCategories() {
               return categories;
       }

       // loadData -------------------------------------------------------
       /**
        * This method loads a Keyring database and generates entry objects for each account.
        *
        * @param filename Keyring database
        */
       public void loadData(String filename) throws Exception {
               File db;
               FileInputStream fp;
               byte[] data;
               byte[] encrypted = null;
               int bufferSize = 100*1024;
               int entryLength;
               int pdbLength;
               int emptyTitle = 0;
               int start = 0;
               int len;
               int reallen;
               byte[] iv = null;
               String title = null;

               // record entry descriptors
               int pdbOffset[];    // 4
               int pdbAttribute[]; // 1
               int pdbUniqueId[];  // 3

               // initialisation
               entries.clear();
               categories.clear();

               // read database
               db = new File(filename);
               fp = new FileInputStream(db);

               data = new byte[bufferSize];

               pdbLength = fp.read(data);

               if(pdbLength == bufferSize) {
                       throw new Exception("File too large.");
               }

               fp.close();

               if(DEBUG) {
                       System.out.println("\n========== loadData() ==========\n");
               }

               // read header
               pdbHeader = sliceBytes(data, 0, 78);
               pdbName = sliceString(data, 0, 32);
               pdbFlags = (int)sliceNumber(data, 32, 2);
               pdbVersion = (int)sliceNumber(data, 34, 2); // 12 byte time information
               pdbModNumber = sliceNumber(data, 48, 4);
               pdbAppInfoOffset = (int)sliceNumber(data, 52, 4);
               pdbSortInfoOffset = (int)sliceNumber(data, 56, 4);
               pdbType = sliceString(data, 60, 4);
               pdbCreator = new String(data, 64, 4); // 8 byte unknown
               pdbNumRecords = (int)sliceNumber(data, 76, 2);

               // check Keyring database format
               if(!(pdbVersion == 4 || pdbVersion == 5)) {
                       throw new Exception("Wrong Keyring database format.");
               }

               // offsets
               pdbOffset = new int[pdbNumRecords];
               pdbAttribute = new int[pdbNumRecords];
               pdbUniqueId = new int[pdbNumRecords];

               for(int i=0; i<pdbNumRecords; i++) {
                       pdbOffset[i] = (int)sliceNumber(data, 78 + (i*8), 4);
                       pdbAttribute[i] = (int)(sliceNumber(data, 78 + 4 + (i*8), 1));
                       pdbUniqueId[i] = (int)sliceNumber(data, 78 + 4 + 1 + (i*8), 3);

                       if(DEBUG) {
                               System.out.println(i + ": " + pdbOffset[i]+ " / " + pdbAttribute[i] + " / " + pdbUniqueId[i]);
                       }
               }

               if(DEBUG) {
                       printPDBHeader();
               }

               pdbCategories = sliceBytes(data, pdbAppInfoOffset, 276);

               // determine the category list
               for(int i=0; i<16; i++) {
                       String categoryName = sliceString(data, pdbAppInfoOffset + 2 + (16 * i), 16);

                       if (!categoryName.equals("")) {
                               categories.add(categoryName);
                       }
               }

               if(pdbVersion == 5) {
                       if(pdbNumRecords <= 0) {
                               throw new Exception("No real data.");
                       }

                       byte[] salt = sliceBytes(data, pdbAppInfoOffset + 276, 8);
                       int iter = (int)sliceNumber(data, pdbAppInfoOffset + 276 + 8, 2);
                       int cipher = (int)sliceNumber(data, pdbAppInfoOffset + 276 + 8 + 2, 2);
                       byte[] hash = sliceBytes(data, pdbAppInfoOffset + 276 + 8 + 2 + 2, 8);

                       // initialize crypto Object
                       crypto = new Crypto(null, 5, salt, hash, iter, cipher);

                       start = 0; // start with first record

                       switch(cipher) {
                               case 1: break; // triple des
                               case 2: break; // aes 128 bit
                               case 3: break; // aes 256 bit
                               default: throw new Exception("No cipher not supported.");
                       }
               }

               if(pdbVersion == 4) {
                       if(pdbNumRecords <= 1) { // only password information
                               throw new Exception("No real data.");
                       }

                       recordZeroAttribute = pdbAttribute[0];
                       recordZeroUniqueId = pdbUniqueId[0];
                       recordZeroLength = pdbOffset[1] - pdbOffset[0];

                       // load up password information (entry 0)
                       crypto = new Crypto(sliceBytes(data, pdbOffset[0], pdbOffset[1] - pdbOffset[0]), 4);

                       start = 1; // start with second record
               }

               // example (Keyring database format 4):
               // numberOfEntries = 4
               // entry 0 = password information
               // entry 1
               // entry 2
               // entry 3

               for(int i=start; i<pdbNumRecords; i++) {

                       // check record attribute
                       if((pdbAttribute[i] & 0xF0) == 0x40) {
               // determine entry length
               if(i == pdbNumRecords - 1) {
                   entryLength = pdbLength - pdbOffset[i];
               }
               else {
                   entryLength = pdbOffset[i+1] - pdbOffset[i];
               }

               //if(DEBUG) {
               //    System.out.println("i=" + i + ": " + pdbOffset[i] + " / " + entryLength);
               //}

               if(pdbVersion == 4) { // Keyring database format 4
                   // title + \0 + encrypted data
                   title = sliceString(data, pdbOffset[i], -1);
                   iv = null;
                   encrypted = sliceBytes(data, pdbOffset[i] + title.length() + 1, entryLength - title.length() - 1);
               }

               if(pdbVersion == 5) {
                   // get length of field
                   len = (int)sliceNumber(data, pdbOffset[i], 2);
                   reallen = (len + 1) & ~1; // padding for next even address

                   title = sliceString(data, pdbOffset[i] + 4, len);

                   int ivlen = 8; // tripledes
                   if(crypto.type == 2 || crypto.type == 3) ivlen = 16; // aes

                   iv = sliceBytes(data, pdbOffset[i] + reallen + 4, ivlen);
                   encrypted = sliceBytes(data, pdbOffset[i] + reallen + 4 + ivlen, entryLength - (reallen + 4 + ivlen));
               }

               // Keyring: empty title possible
               if(title.equals("")) {
                   title = "#" + (emptyTitle++);
               }

               // generate entry object
               Entry myEntry = new Entry(
                   i,
                   title,
                   pdbAttribute[i] & 15,
                   encrypted,
                   crypto,
                   pdbAttribute[i],
                   pdbUniqueId[i],
                   entryLength,
                   iv);

               //entries.add(myEntry); // Java 1.5
               entries.add((Object)myEntry);
           }
               }
       }

       // saveData -------------------------------------------------------
       /**
        * This method calls the saveData method according to database version (pdbVersion).
        *
        * @param filename Keyring database
        */
       public void saveData(String filename) throws Exception {
               if(DEBUG) {
                        System.out.println("saveData");
               }

               switch(pdbVersion) {
                       case 4: saveData_4(filename); break;
                       case 5: saveData_5(filename); break;
               }
       }

       /**
        * This method saves all entries in the specified database (Database format 4).
        *
        * @param filename Keyring database
        */
       public void saveData_4(String filename) throws Exception {
               File db;
               FileOutputStream fp;
               int offset = 0;

               // open new database
               db = new File(filename);
               fp = new FileOutputStream(db);

               pdbAppInfoOffset = 78 + 8 * entries.size() + 2 + 8; // + 8 for recordZero
               pdbNumRecords = entries.size() + 1;
               offset = pdbAppInfoOffset + 276;

               // write header
               fp.write(pdbHeader, 0, 52);
               fp.write(numberToByte(pdbAppInfoOffset, 4), 0, 4);
               fp.write(pdbHeader, 56, 20);
               fp.write(numberToByte(pdbNumRecords, 2), 0, 2); // + 1 for recordZero

               // write offset recordZero
               fp.write(numberToByte(offset, 4), 0, 4);
               fp.write(numberToByte(recordZeroAttribute, 1), 0, 1);
               fp.write(numberToByte(recordZeroUniqueId, 3), 0, 3);

               offset += recordZeroLength;

               // write offsets
               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       fp.write(numberToByte(offset, 4), 0, 4);
                       fp.write(numberToByte(entry.attribute, 1), 0, 1); // category
                       fp.write(numberToByte(entry.uniqueId, 3), 0, 3);

                       if(DEBUG) {
                               System.out.println("saveData4: " + offset + ", " + entry.attribute + ", " + entry.uniqueId);
                       }

                       offset += entry.recordLength;
               }

               fp.write((int)0x0000);
               fp.write((int)0x0000);

               // write categories
               updateCategories(); // Categories in Gui.java are editable
               fp.write(pdbCategories, 0, 276);

               // write password information
               fp.write(crypto.recordZero);

               // write records
               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       fp.write(entry.getTitle().getBytes());
                       fp.write(0x00);
                       fp.write(entry.encrypted);
               }

               fp.close();
       }

       /**
        * This method saves all entries in the specified database (Database format 5).
        *
        * @param filename Keyring database
        */
       public void saveData_5(String filename) throws Exception {
               File db;
               FileOutputStream fp;
               int offset = 0;

               // open new database
               db = new File(filename);
               fp = new FileOutputStream(db);

               pdbAppInfoOffset = 78 + 8 * entries.size() + 2;
               pdbNumRecords = entries.size();
               offset = pdbAppInfoOffset + 276 + 20; // salt hash type

               // write header
               fp.write(pdbHeader, 0, 52);
               fp.write(numberToByte(pdbAppInfoOffset, 4), 0, 4);
               fp.write(pdbHeader, 56, 20);
               fp.write(numberToByte(pdbNumRecords, 2), 0, 2); // + 1 for recordZero

               // write offsets
               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       fp.write(numberToByte(offset, 4), 0, 4);
                       fp.write(numberToByte(entry.attribute, 1), 0, 1); // category
                       fp.write(numberToByte(entry.uniqueId, 3), 0, 3);

                       offset += entry.recordLength;
               }

               fp.write((int)0x0000);
               fp.write((int)0x0000);

               // write categories
               updateCategories(); // Categories in Gui.java are editable
               fp.write(pdbCategories, 0, 276);

               // write SALT HASH TYPE (db_format.txt)
               fp.write(crypto.salt);
               fp.write(numberToByte(crypto.iter, 2));
               fp.write(numberToByte(crypto.type, 2));
               fp.write(crypto.hash);

               // write records
               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       fp.write(convertStringToField(entry.getTitle(), 0));
                       fp.write(entry.iv);
                       fp.write(entry.encrypted);
               }

               fp.close();
       }

       // convertDatabase ------------------------------------------------
       /**
        * This method calls convertTo method according to database format.
        *
        * @param from Database format of loaded database
        * @param to Convert to database format
        * @param filename New keyring database
        * @param pw Password of new database
        * @param type Cipher type (for database format 5)
        * @param iter Iterations (for database format 5)
        */
       public void convertDatabase(int from, int to, String filename, char[] pw, int type, int iter) throws Exception {
               switch(to) {
                       case 4: convertTo_4(from, filename, pw); break;
                       case 5: convertTo_5(from, filename, pw, type, iter); break;
                       default: return;
               }
       }

       /**
        * This method converts all entries to database format 4 and saves to specified database.
        *
        * @param from Database format of loaded database
        * @param filename New keyring database
        * @param pw Password of new database
        */
       public void convertTo_4(int from, String filename, char[] pw) throws Exception {
       // Keyring database format 4
               File db;
               FileOutputStream fp;
               int i;
               int offset = 0;
               byte[] recordzero = new byte[20];
               byte[] pass = new byte[pw.length];
               byte[] salt = new byte[4];
               byte[] record = null;
               byte[] ciphertext = null;
               Crypto converted = null;

               // open new database
               db = new File(filename);
               fp = new FileOutputStream(db);

               pdbAppInfoOffset = 78 + 8 * entries.size() + 2 + 8; // + 8 for recordZero
               pdbNumRecords = entries.size() + 1;
               offset = pdbAppInfoOffset + 276;

               // create record zero
               Arrays.fill(recordzero, (byte)0);

               // Keyring supports passwords of up to 40 characters
               if(pw.length > 40) {
                       throw new Exception("Password too long.");
               }

               // convert password from char to byte
               for(i=0;i<pw.length;i++) {
                       pass[i] = (byte)(0xff & pw[i]);
               }

               // get salt
               switch(from) {
                       case 4: // convert from 4 to 4 (changing password)
                               for(i=0;i<4;i++) {
                                       salt[i] = crypto.recordZero[i]; // get old salt
                                       recordzero[i] = crypto.recordZero[i];
                               }

                               break;

                       case 5:
                               // take first 4 bytes from format 5 salt
                               for(i=0;i<4;i++) {
                                       salt[i] = crypto.salt[i];
                                       recordzero[i] = crypto.salt[i];
                               }

                               break;
               }

               // get hash from password
               byte[] hash = crypto.checkPasswordHash_4(salt, pass);

               // fill recordzero
               for(i=0; i<16; i++) {
                       recordzero[i+4] = hash[i];
               }

               // new crypto object
               converted = new Crypto(recordzero, 4);
               converted.setPassword(pw);

               Arrays.fill(pw, (char)0);
               Arrays.fill(pass, (byte)0);

               // write header
               fp.write(pdbHeader, 0, 34);
               fp.write(numberToByte(4, 2), 0, 2); // write new version
               fp.write(pdbHeader, 36, 16);
               fp.write(numberToByte(pdbAppInfoOffset, 4), 0, 4);
               //fp.write(pdbHeader, 56, 20);
               fp.write(pdbHeader, 56, 4); // sort info offset
               fp.write(applcreator4.getBytes()); // type, creator
               fp.write(pdbHeader, 68, 8); // sort info offset
               fp.write(numberToByte(pdbNumRecords, 2), 0, 2); // + 1 for recordZero

               // write offset recordZero
               fp.write(numberToByte(offset, 4), 0, 4);
               fp.write(numberToByte(80, 1), 0, 1);
               fp.write(numberToByte(0, 3), 0, 3);
               offset += 20;

               // write offsets
               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       fp.write(numberToByte(offset, 4), 0, 4);
                       fp.write(numberToByte(entry.attribute, 1), 0, 1); // category
                       fp.write(numberToByte(entry.uniqueId, 3), 0, 3);

                       // decrypt and encrypt records
                       record = Model.toRecordFormat4(
                               entry.getAccount() + "\0" +
                               entry.getPassword() + "\0" +
                               entry.getNotes() + "\0");

                       ciphertext = converted.encrypt(record);

                       entry.encrypted = sliceBytes(ciphertext, 16, ciphertext.length - 16); // 16 byte iv ignored

                       offset += entry.getTitle().length() + 1 + entry.encrypted.length;
               }

               fp.write((int)0x0000);
               fp.write((int)0x0000);

               // write categories
               updateCategories();
               fp.write(pdbCategories, 0, 276);

               // write password information
               fp.write(recordzero);

               // write records
               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       fp.write(entry.getTitle().getBytes());
                       fp.write(0x00);
                       fp.write(entry.encrypted);
               }

               converted = null;

               fp.close();
       }

       /**
        * This method converts all entries to database format 5 and saves to specified database.
        *
        * @param from Database format of loaded database
        * @param filename New keyring database
        * @param pw Password of new database
        * @param type Cipher type (for database format 5)
        * @param iter Iterations (for database format 5)
        */
       public void convertTo_5(int from, String filename, char[] pw, int type, int iter) throws Exception {
       // Keyring database format 5
               File db;
               FileOutputStream fp;
               int i;
               int offset = 0;
               byte[] record = null;
               byte[] ciphertext = null;
               byte[] salthashtype = new byte[20];
               Crypto converted = null;
               int[] cipherlen = {0, 24, 16, 32}; // keylength in byte
               byte[] pass = new byte[pw.length];
               byte[] salt = new byte[8];
               int index;

               for(i=0;i<pw.length;i++) {
                       pass[i] = (byte)(0xFF & pw[i]);
               }

               // open new database
               db = new File(filename);
               fp = new FileOutputStream(db);

               pdbAppInfoOffset = 78 + 8 * entries.size() + 2;
               pdbNumRecords = entries.size();
               offset = pdbAppInfoOffset + 276 + 20; // salt hash type

               switch(from) {
                       case 4:
                               for(i=0; i<4; i++) {
                                       salt[i] = crypto.recordZero[i];
                                       salt[i+4] = crypto.recordZero[i];
                               }
                               break;

                       case 5:
                               for(i=0; i<8; i++) {
                                       salt[i] = crypto.salt[i];
                               }
                               break;
               }

               // PKCS#5 PBKDF2
               // Key Derivation function
               byte[] deskey = crypto.pbkdf2(pass, salt, iter, cipherlen[type]);

               // set odd parity
               if(type == 1) { // TripleDES
                       for(i=0; i<24; i++) {
                               index = (int)(0xff & deskey[i]);
                               deskey[i] = (byte)crypto.odd_parity[index];
                       }
               }

               // SHA1
               byte[] digest = crypto.getMessageDigest(deskey, salt);

               byte[] hash = Model.sliceBytes(digest, 0, 8);

               converted = new Crypto(null, 5, salt, hash, iter, type);
               converted.setPassword(pw);

               Arrays.fill(pw, (char)0);
               Arrays.fill(pass, (byte)0);

               // write header
               fp.write(pdbHeader, 0, 34);
               fp.write(numberToByte(5, 2), 0, 2); // write new version
               fp.write(pdbHeader, 36, 16);
               fp.write(numberToByte(pdbAppInfoOffset, 4), 0, 4); // application info offset
               //fp.write(pdbHeader, 56, 20);
               fp.write(pdbHeader, 56, 4); // sort info offset
               fp.write(applcreator5.getBytes()); // type, creator
               fp.write(pdbHeader, 68, 8); // sort info offset

               fp.write(numberToByte(pdbNumRecords, 2), 0, 2); // + 1 for recordZero

               // write offsets
               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       fp.write(numberToByte(offset, 4), 0, 4);
                       fp.write(numberToByte(entry.attribute, 1), 0, 1); // category
                       fp.write(numberToByte(entry.uniqueId, 3), 0, 3);

                       // decrypt and encrypt records
                       record = Model.toRecordFormat5(entry.getAccount(), entry.getPassword(), entry.getNotes());

                       ciphertext = converted.encrypt(record);

                       // extract iv
                       int ivlen = 8;
                       if(type != 1) { // TripleDES
                               ivlen = 16; // AES128, AES256
                       }

                       entry.iv = sliceBytes(ciphertext, 0, ivlen);
                       entry.encrypted = Model.sliceBytes(ciphertext, 16, ciphertext.length - 16);

                       offset += (Model.convertStringToField(entry.getTitle(), 0)).length + ivlen + entry.encrypted.length;
               }

               fp.write((int)0x0000);
               fp.write((int)0x0000);

               // write categories
               updateCategories();
               fp.write(pdbCategories, 0, 276);

               // write SALT HASH TYPE (db_format.txt)
               fp.write(salt);
               fp.write(numberToByte(iter, 2));
               fp.write(numberToByte(type, 2));
               fp.write(hash);

               // write records
               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       fp.write(convertStringToField(entry.getTitle(), 0));
                       fp.write(entry.iv);
                       fp.write(entry.encrypted);
               }

               converted = null;

               fp.close();
       }

       // toRecordFormat4 ------------------------------------------------
       /**
        * This method adds todays date (DateType format) to decrypted data (for database format 4).
        *
        * @param data Example: Account + \0 + Password + \0 + Notes + \0
        *
        * @return data + todays date in datetype format
        */
       public static byte[] toRecordFormat4(String data) {
               byte[] today = getDateType();
               byte[] buffer = data.getBytes();
               byte[] result = new byte[buffer.length + 2];

               System.arraycopy(buffer, 0, result, 0, buffer.length);
               result[buffer.length] = today[1];
               result[buffer.length + 1] = today[0];

               return result;
       }

       // toRecordFormat5 ------------------------------------------------
       /**
        * This method adds todays date (DateType format) to decrypted data (for database format 5).
        *
        * @param account Entry account
        * @param password Entry password
        * @param notes Entry notes
        *
        * @return decrypted data in database format 5
        */
       public static byte[] toRecordFormat5(String account, String password, String notes) {
               // Format:
               // field (account)
               // field (password)
               // field (notes)
               // field (datetype)
               // 0xff
               // 0xff
               // random padding to multiple of 8 bytes
               byte[] datetype = {0x00, 0x02, 0x03, 0x00, 0x00, 0x00};

               byte[] field1 = account.getBytes();
               byte[] field2 = password.getBytes();
               byte[] field3 = notes.getBytes();

               int lenField1 = field1.length;
               int lenField2 = field2.length;
               int lenField3 = field3.length;

               if(lenField1 != 0) {
                       field1 = convertStringToField(account, 1);
                       lenField1 = field1.length;
               }

               if(lenField2 != 0) {
                       field2 = convertStringToField(password, 2);
                       lenField2 = field2.length;
               }

               if(lenField3 != 0) {
                       field3 = convertStringToField(notes, 255);
                       lenField3 = field3.length;
               }

               byte[] now = getDateType();
               datetype[4] = now[1];
               datetype[5] = now[0];

               int padding = (lenField1 + lenField2 + lenField3 + 6 + 2) % 8;
               byte[] result = new byte[lenField1 + lenField2 + lenField3 + 6 + 2 + padding];
               Arrays.fill(result, (byte)0xff);

               if(lenField1 != 0) {
                       System.arraycopy(field1, 0, result, 0, lenField1);
               }

               if(lenField2 != 0) {
                       System.arraycopy(field2, 0, result, lenField1, lenField2);
               }

               if(lenField3 != 0) {
                       System.arraycopy(field3, 0, result, lenField1 + lenField2, lenField3);
               }

               System.arraycopy(datetype, 0, result, lenField1 + lenField2 + lenField3, 6);

               return result;
       }

       /**
        * This method converts a string in the format used by database format 5 (Field).
        *
        * @param field Text
        * @param label Label information (account=1, password=2, notes=255)
        *
        * @return Field
        */
       public static byte[] convertStringToField(String field, int label) {
               // Format:
               // 2 byte length of field
               // 1 byte label
               // 1 byte 0x00
               // data
               // 0/1 padding for next even address
               byte[] buffer = field.getBytes();
               int padding = 0;
               int len = buffer.length;

               if((len % 2) == 1) {
                       padding = 1;
               }

               byte[] result = new byte[4 + len + padding];
               Arrays.fill(result, (byte)0);

               System.arraycopy(numberToByte(len,2), 0, result, 0, 2);
               System.arraycopy(numberToByte(label,1), 0, result, 2, 1);
               result[3] = (byte)0x00;
               System.arraycopy(buffer, 0, result, 4, len);

               return result;
       }


       // getNewUniqueId -------------------------------------------------
       /**
        * This method searches the entries for the highest id.
        *
        * @return New unique id
        */
       public int getNewUniqueId() {
               int id = 0;

               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {

                       Entry entry = (Entry)e.nextElement();

                       if(entry.getUniqueId() > id) {
                               id = entry.getUniqueId();
                       }
               }

               id = id + 1;

               return(id);
       }

       // saveEntriesToFile ----------------------------------------------
       /**
        * This method saves all entries to a csv file.
        *
        * @param filename CSV file
        */
       public void saveEntriesToFile(String filename) throws Exception{
               csvFilename = filename;

               File outputFile = new File(csvFilename);
       FileWriter out = new FileWriter(outputFile);

               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       String buffer =
                               "" + '"' + entry.getEntryId() + '"' + csvSeparator +
                               '"' + categories.elementAt(entry.getCategory()) + '"' + csvSeparator +
                               '"' + entry.getTitle() + '"' + csvSeparator +
                               '"' + entry.getAccount() + '"' + csvSeparator +
                               '"' + entry.getPassword() + '"' + csvSeparator +
                               '"' + entry.getDate() + '"' + "\n";

                       out.write(buffer.toCharArray());
               }

               out.close();
       }

       public void setCsvSeparator(char sep) {
               this.csvSeparator = sep;
       }

       public void setCsvFilename(String filename) {
               this.csvFilename = filename;
       }

       public String getCsvFilename() {
               return csvFilename;
       }

       // tools ----------------------------------------------------------
       /**
        * This method converts a long into a byte array.
        *
        * @param number Number
        * @param len Size of byte array
        *
        * @return Byte array representation of number
        */
       public static byte[] numberToByte(long number, int len) {
               int i, shift;
               byte[] buffer = new byte[len];

               for(i=0, shift=((len-1) * 8); i<len; i++, shift -= 8) {
                       buffer[i] = (byte)(0xFF & (number >> shift));
               }

               return buffer;
       }

       /**
        * This method converts a byte to int.
        *
        * @param b Byte
        *
        * @return Int representation of Byte
        */
       public static int unsignedByteToInt(byte b) {
               return (int)(b & 0xFF);
       }

       /**
        * This method slices a byte array from an byte array.
        *
        * @param data Byte array
        * @param start Index to start from
        * @param length Length of byte array to slice out
        *
        * @return Byte array
        */
       public static byte[] sliceBytes(byte[] data, int start, int length) {
               byte[] bytes = new byte[length];

               for(int i=0; i<length; i++) {
                       bytes[i] = data[start + i];
               }

               return bytes;
       }

       /**
        * This method slices a byte array from an byte array and converts it to a long.
        *
        * @param data Byte array
        * @param start Index to start from
        * @param length Length of byte array to slice out
        *
        * @return Long representation of the byte array
        */
       public static long sliceNumber(byte[] data, int start, int length) {
               long value = 0, factor = 1;

               for(int i=0; i<length; i++) {
                       value += (long)(unsignedByteToInt(data[start + length - (i + 1)]) * factor);
                       factor *= 256;
               }

               return value;
       }

       /**
        * This method slices a byte array from an byte array and converts it to a string.
        *
        * @param data Byte array
        * @param start Index to start from
        * @param length Length of byte array to slice out
        *
        * @return String representation of the byte array
        */
       public static String sliceString(byte[] data, int start, int length) {
               int realLength = 0;

               if(length == -1) {
                       // no specific max length (make it to the end of the array)
                       length = data.length - start;
               }

               while(realLength < length && data[start + realLength] != 0) {
                       realLength++;
               }

               return new String((byte[])data, start, realLength);
       }

       /**
        * not used
        */
       public static void printByteArray(String info, byte[] buffer) {
               System.out.print("printByteArray " + info + " (" + buffer.length + "): ");
               for(int i=0;i<buffer.length;i++) {
                       System.out.print((int)(buffer[i] & 0xFF) + " ");
               }
               System.out.println();
       }

       /**
        * not used
        */
       public static void printHexByteArray(String info, byte[] buffer) {
               char[] hexNumbers = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
               int zahl, rest;

               System.out.println("printHexByteArray " + info + " (" + buffer.length + "): ");

               for(int i=0;i<buffer.length;i++) {

                       zahl = (int)(buffer[i] & 0xFF) / 16;
                       rest = (int)(buffer[i] & 0xFF) % 16;

                       System.out.print("" + hexNumbers[zahl] + "" + hexNumbers[rest] + " ");
               }

               System.out.println();
       }

       // ----------------------------------------------------------------
       // private --------------------------------------------------------
       // ----------------------------------------------------------------

       /*
       private static void printUsage() {
               System.err.println("Usage:");
               System.err.println("View entries: java Model database.pdb password");
               System.err.println("   Add entry: ... -n title account password");
               System.err.println("  Edit entry: ... -e id title account passwort");
               System.err.println("Delete entry: ... -d id");
       }
       */

       /**
        * not used
        */
       private void printPDBHeader() {
               System.out.println("PDB Name: " + pdbName);
               System.out.println("PDB Flags: " + pdbFlags);
               System.out.println("PDB Version: " + pdbVersion);
               System.out.println("PDB Modification Number: " + pdbModNumber);
               System.out.println("PDB AppInfoOffset: " + pdbAppInfoOffset);
               System.out.println("PDB SortInfoOffset: " + pdbSortInfoOffset);
               System.out.println("PDB Type: " + pdbType);
               System.out.println("PDB Creator: " + pdbCreator);
               System.out.println("PDB NumberOfRecords: " + pdbNumRecords + "\n");
       }

       /**
        * not used
        */
       private void printEntries() {
               int i=0;

               for(Enumeration c = categories.elements(); c.hasMoreElements(); ) {
                       String help = (String)c.nextElement();

                       System.out.println("Category " + (i++) + ": " + help);
               }
               System.out.println();

               for(Enumeration e = entries.elements(); e.hasMoreElements(); ) {
                       Entry entry = (Entry)e.nextElement();

                       System.out.println(entry.getInfo());
               }
               System.out.println();
       }

       // DateType -------------------------------------------------------
       /**
        * This method return todays date in DateType format.
        *
        * @return Todays date in DateType format (byte[2])
        */
       private static byte[] getDateType() {
               int day, month, year;
               int[] intResult = new int[2];
               byte[] byteResult = new byte[2];

               Calendar rightNow = new GregorianCalendar();

               day = rightNow.get(Calendar.DAY_OF_MONTH);
               month = rightNow.get(Calendar.MONTH) + 1; // Calender month from 0 to 11
               year = rightNow.get(Calendar.YEAR) - 1904; // DateType year since 1904

               day = (day & 0x1F); // 5 bit
               month = (month & 0x0F); // 4 bit
               year = (year & 0x7F); // 7 bit

               // DateType (2 bytes): 7 bit year, 4 bit month, 5 bit day
               intResult[0] = day | ((month & 0x07) << 5);
               intResult[1] = (year << 1) | ((month & 0x08) >> 3);

               // System.out.println(intResult[1] + " " + intResult[0]);
               byteResult[0] = (byte)intResult[0];
               byteResult[1] = (byte)intResult[1];

               return byteResult;
       }

       // updateCategories - saveData()
       /**
        * This method updates the categories in variable pdbCategories according to vector categories.
        */
       private void updateCategories() {
               byte[] cat = new byte[16];
               int index = 0;

               for(Enumeration c = categories.elements(); c.hasMoreElements(); ) {
                       String strCategory = (String)c.nextElement();
                       byte[] temp = strCategory.getBytes();

                       // resize to 16 byte
                       for(int i=0; i<16; i++) {
                               if(i < temp.length)
                                       cat[i] = temp[i];
                               else
                                       cat[i] = 0x00;
                       }

                       // overwrite old categories
                       System.arraycopy(cat, 0, pdbCategories, 2 + (index * 16), 16);
                       index++;
               }
       }
}