/*
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 <keyring@lieder.me.uk>

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++;
		}
	}
}

