/*
 * GNU.FREE 2002
 *
 * Copyright (c) 1999, 2000, 2001, 2002
 * The Free Software Foundation (www.fsf.org)
 *
 * GNU.FREE Co-ordinator: Jason Kitcat <jeep@free-project.org>
 *
 * GNU site: http://www.gnu.org/software/free/
 * 
 * FREE e-democracy site: http://www.free-project.org
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it 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.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program (COPYING); if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */
  
package ERServer;

import java.io.*;
import java.sql.*;
import java.util.*;
//#ifdef INSTALL
import org.hsql.*;

import cryptix.provider.key.RawSecretKey;
import cryptix.provider.padding.OneAndZeroes;
import cryptix.provider.cipher.Blowfish;
import cryptix.util.core.Hex;

import xjava.security.Cipher;
import xjava.security.FeedbackCipher;
import xjava.security.SecretKey;

import java.security.*;

import Free.util.*;
import Free.DBPool.*;

/**
 * DBase creates databases, tables and interacts with these through JDBC and SQL.
 *
 * Implementation of the databases is with 
 * <a href="http://hsql.oron.ch">Hypersonic SQL</a> which is written
 * totally in Java.
 *
 * This implementation was created to support the demoonstration security
 * authorisation system. To implement a new (e.g. biometric) auth system
 * then you *WILL* need to change some SQL. It won't hurt.
 *
 * @version 1.8  28 August 2001
 * @author Jason Kitcat
 */
public class DBase {

//#ifdef INSTALL
	private static final String username = "sa";
//#ifdef INSTALL
	private static final String password = "";
	// USER SUPPLIED SESSION WORD
	protected static String cryptword = "";
	

	/**
	 * initialises the Hypersonic SQL database.
	 *
	 */
	protected static void init() {
	
		try {

//#ifdef INSTALL
			Class.forName("org.hsql.jdbcDriver"); // Load the Hypersonic SQL JDBC driver

			/* Prepare the pool with currently 10 connections */
//#ifdef INSTALL
			new JDCConnectionDriver("org.hsql.jdbcDriver","jdbc:HypersonicSQL:erserver",username, password);

			/* Get a connection from the pool */
			Connection freeConn = getConnection();
			Statement freeStat = freeConn.createStatement();
			
			/* make table for Electoral Roll */
			try {
				ResultSet r = freeStat.executeQuery("SELECT name FROM Roll"); //check for the table
			} catch (SQLException sqle) {
				if(ERServer.DEV.isDebugEnabled()) {
					ERServer.DEV.debug("Roll table didn't exist so creating it");
				}
				freeStat.execute("CREATE TABLE Roll(name VARCHAR(255), code VARCHAR(255), pword VARCHAR(255), voted CHAR(1), vote_where CHAR(1), info_sent CHAR(1), other VARCHAR(255))");
			}

			DatabaseMetaData dbMetaData = freeConn.getMetaData();

			freeStat.close();
			
			ERServer.NORM.info("Database intialised");
			ERServer.DEV.debug("DB Using: "  + dbMetaData.getDatabaseProductName() + " " + dbMetaData.getDatabaseProductVersion());

		} catch(Exception e) {
			ERServer.NORM.error("Database error: " + e.toString());
		}
			
	} //EOF init()


	/**
	 * Provides a database connection from the pool.
	 *
	 * @returns  A working connection
	 */
	private static Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:jdc:jdcpool");
    }
    

	/**
	 * checks String input to make sure it only contains safe characters.
	 * It's not pretty but regular expression packages tried all were far
	 * too slow.
	 *
	 * @param input  The String to check
	 * @returns  True if the string is ok
	 */
	 protected static boolean isSafe(String input) throws Exception{
	 
	 	boolean ok = true;
	 	 
		for(int i = 0; (i<input.length())&&(ok==true); i++) {
	 
	 		switch (input.charAt(i)) {
	 			case 'a': ok = true; break;
	 			case 'b': ok = true; break;
	 			case 'c': ok = true; break;
	 			case 'd': ok = true; break;
	 			case 'e': ok = true; break;
	 			case 'f': ok = true; break;
	 			case 'g': ok = true; break;
	 			case 'h': ok = true; break;
	 			case 'i': ok = true; break;
	 			case 'j': ok = true; break;
	 			case 'k': ok = true; break;
	 			case 'l': ok = true; break;
	 			case 'm': ok = true; break;
	 			case 'n': ok = true; break;
	 			case 'o': ok = true; break;
	 			case 'p': ok = true; break;
	 			case 'q': ok = true; break;
	 			case 'r': ok = true; break;
	 			case 's': ok = true; break;
	 			case 't': ok = true; break;
	 			case 'u': ok = true; break;
	 			case 'v': ok = true; break;
	 			case 'w': ok = true; break;
	 			case 'x': ok = true; break;
	 			case 'y': ok = true; break;
	 			case 'z': ok = true; break;
	 			case 'A': ok = true; break;
	 			case 'B': ok = true; break;
	 			case 'C': ok = true; break;
	 			case 'D': ok = true; break;
	 			case 'E': ok = true; break;
	 			case 'F': ok = true; break;
	 			case 'G': ok = true; break;
	 			case 'H': ok = true; break;
	 			case 'I': ok = true; break;
	 			case 'J': ok = true; break;
	 			case 'K': ok = true; break;
	 			case 'L': ok = true; break;
	 			case 'M': ok = true; break;
	 			case 'N': ok = true; break;
	 			case 'O': ok = true; break;
	 			case 'P': ok = true; break;
	 			case 'Q': ok = true; break;
	 			case 'R': ok = true; break;
	 			case 'S': ok = true; break;
	 			case 'T': ok = true; break;
	 			case 'U': ok = true; break;
	 			case 'V': ok = true; break;
	 			case 'W': ok = true; break;
	 			case 'X': ok = true; break;
	 			case 'Y': ok = true; break;
	 			case 'Z': ok = true; break;
	 			case '1': ok = true; break;
	 			case '2': ok = true; break;
	 			case '3': ok = true; break;
	 			case '4': ok = true; break;
	 			case '5': ok = true; break;
	 			case '6': ok = true; break;
	 			case '7': ok = true; break;
	 			case '8': ok = true; break;
	 			case '9': ok = true; break;
	 			case '0': ok = true; break;
	 			case ';': ok = true; break;
	 			case ')': ok = true; break;
	 			case '(': ok = true; break;
	 			case '*': ok = true; break;
	 			case '\'': ok = true; break;
	 			case ' ': ok = true; break;
	 			case '=': ok = true; break;
	 			case '-': ok = true; break;
	 			case '_': ok = true; break;
	 			case '+': ok = true; break;
	 			case '@': ok = true; break;
	 			case ',': ok = true; break;
	 			case '.': ok = true; break;
	 			case '<': ok = true; break;
	 			case '>': ok = true; break;
	 			case '?': ok = true; break;
	 			default:  ok = false; break;
	 		} //eof case
	 		
		 } //eof for
	 
	 	return ok;
	 
	 } //eof isSafe


	/**
	 * Encrypts string data before being inserted into database
	 *
	 * @param data  The string to encrypt
	 * @returns  Encrypted string
	 */
	 private static String encrypt(String data) throws Exception {
	 
	 	RawSecretKey key;
	 	byte[] ect;
		Cipher blowalg = Cipher.getInstance(new Blowfish(),null,new OneAndZeroes());
	 	
		/* build a Blowfish (16-round) key using ECB */
		key = new RawSecretKey("Blowfish", Hex.fromString(cryptword));
        
		blowalg.initEncrypt(key);
		ect = blowalg.crypt(StringByteTools.asciiGetBytes(data));

		return Hex.toString(ect);

	} //eof encrypt()


	/**
	 * Decrypts data pulled from the database returning normal strings.
	 *
	 * @param data  The encrypted string
	 * @returns  Decrypted string
	 */
	protected static String decrypt(String data) throws Exception {

		RawSecretKey key;
	 	byte[] dect;
		Cipher blowalg = Cipher.getInstance(new Blowfish(),null,new OneAndZeroes());
	 	
		/* build a Blowfish (16-round) key using ECB */
		key = new RawSecretKey("Blowfish", Hex.fromString(cryptword));
        
		blowalg.initDecrypt(key);
		dect = blowalg.crypt(Hex.fromString(data));

		return new String(dect);

	} //eof decrypt()


	/**
	 * Counts how many people have voted.
	 *
	 * @returns  An integer of how many people voted
	 */
	protected static int usersVoted() throws Exception {
	
		ResultSet result;
		boolean res;
		int count;
	
		/* Get a connection from the pool */
		Connection freeConn = getConnection();
		Statement freeStat = freeConn.createStatement();
		
		/* search for the data */
		res = freeStat.execute("SELECT COUNT(voted) FROM Roll WHERE voted='" + encrypt("T") + "'");
		result = freeStat.getResultSet();
		
		/* check answer */
		res = result.next();
		if (!res) {
			count = 0;
		} else {
			count = new Integer(result.getString(1)).intValue();
		}
		
		freeStat.close();
					
		return count;

	} //EOF usersVoted()


	/**
	 * checks Electoral Roll information against that in the database for FreeClient.
	 *
	 * @param name  String representing the voter's name
	 * @param code  String representing the voter's vote-specific code
	 * @param pword String representing the voter's password
	 * @returns    A boolean, true if the details match with one in the database
	 */
	protected static boolean checkER(String name, String code, String pword) throws Exception {
	
		ResultSet result;
		boolean temp, res;
	
		/* boundary check the data first */
		// only if the data is alphanumeric, punctuation or spaces do we carry on
		if (isSafe(name)&&isSafe(code)&&isSafe(pword)) {

			/* Get a connection from the pool */
			Connection freeConn = getConnection();
			Statement freeStat = freeConn.createStatement();
			
			/* search for the data */
			res = freeStat.execute("SELECT voted, vote_where FROM Roll WHERE name = '" + encrypt(name) + "' AND code = '" + encrypt(code) + "' AND pword = '" + encrypt(pword) + "'");
			result = freeStat.getResultSet();
		
			/* check answer */
			res = result.next();
			if (!res) {
				temp = false;
			} else {
				if (decrypt(result.getString(2)).equals("I")) { // user authorised to vote online
					if (decrypt(result.getString(1)).equals("F")) {  // they haven't already voted
						temp = true;
						freeStat.execute("UPDATE Roll SET voted = '" + encrypt("C") + "' WHERE name = '" + encrypt(name) + "' AND code = '" + encrypt(code) + "' AND pword = '" + encrypt(pword) + "'");
					} else if (decrypt(result.getString(1)).equals("C")) {  // their vote hasn't been registered but they have logged in before
						temp = true;
						// no need to change database but write to log just in case
						ERServer.NORM.info("DBase.checkER(): User already tried to vote but not confirmed");
					} else {  // otherwise they've already voted
						temp = false;
					}
				} else {
					ERServer.NORM.info("DBase.checkER(): User not authorised to vote online.");
					temp = false;
				}
			}
		
			freeStat.close();
			
		} else {
		
			temp = false;
			throw new Exception("DBase.checkER() - invalid character(s) used");
			
		}
		
		return temp;

	} //EOF checkER()

	
	/**
	 * checks Electoral Roll information against that in the database for PollManager.
	 *
	 * @param name  String representing the voter's name
	 * @param code  String representing the voter's vote-specific code
	 * @param pword String representing the voter's password
	 * @returns    An array of Strings which return: voted status and vote_where
	 */
	protected static String[] PMcheckER(String name, String code, String pword) throws Exception {
	
		ResultSet result;
		boolean temp, res;
		String[] results = new String[] {"", ""};
	
		/* boundary check the data first */
		// only if the data is alphanumeric, punctuation or spaces do we carry on
		if (isSafe(name)&&isSafe(code)&&isSafe(pword)) {

			/* Get a connection from the pool */
			Connection freeConn = getConnection();
			Statement freeStat = freeConn.createStatement();
			
			/* search for the data */
			res = freeStat.execute("SELECT voted, vote_where FROM Roll WHERE name = '" + encrypt(name) + "' AND code = '" + encrypt(code) + "' AND pword = '" + encrypt(pword) + "'");
			result = freeStat.getResultSet();
		
			/* check answer */
			res = result.next();
			if (!res) {
				temp = false;
			} else {
				// make sure we only set as logged in if not already voted
				if (decrypt(result.getString(1)).equals("F")) {  // they haven't already voted
					freeStat.execute("UPDATE Roll SET voted = '" + encrypt("C") + "' WHERE name = '" + encrypt(name) + "' AND code = '" + encrypt(code) + "' AND pword = '" + encrypt(pword) + "'");
				} else if (decrypt(result.getString(1)).equals("C")) {  // their vote hasn't been registered but they have logged in before
					// no need to change database but write to log just in case
					ERServer.NORM.info("DBase.PMcheckER(): User already tried to vote but not confirmed");
				} else {  // otherwise they've already voted
				}
				results[0] = decrypt(result.getString(1));
				results[1] = decrypt(result.getString(2));
			}
		
			freeStat.close();
			
		} else {
		
			results[0] = "ERROR";
			results[1] = "ERROR";
			throw new Exception("DBase.PMcheckER() - invalid character(s) used");
			
		}
		
		return results;

	} //EOF PMcheckER()


	/**
	 * A little processor intensive and might need tuning but this method finds the user
	 * entry in the Electoral Roll that matches the received key and sets it has having
	 * voted.
	 *
	 * @param akey String representing the voter's AuthKey
	 * @returns    A boolean, true if the details match with one in the database
	 */
	protected static boolean confirmVoted(String akey) throws Exception {
	
		ResultSet results;
		int count;
		boolean carryon = true;
		boolean res, retVal;
	
		/* Get a connection from the pool */
		Connection freeConn = getConnection();
		Statement freeStat = freeConn.createStatement();
			
		/* search for the data */
		// only select data which has been through stage one authentication
		res = freeStat.execute("SELECT * FROM Roll WHERE voted = '" + encrypt("C") + "'");
		
		/* crunch to find one that matches */
		count = freeStat.getUpdateCount();
		results = freeStat.getResultSet();
		if (!res) {  // if no result data records
			carryon=true;
		} else {  // otherwise calculate and find
			while(results.next()&&carryon) {
				if (akey.equals(AuthKey.build(decrypt(results.getString(1)),decrypt(results.getString(2)),decrypt(results.getString(3)),1))) {
					// update db to set user as having voted
					freeStat.execute("UPDATE Roll SET voted = '" + encrypt("T") + "' WHERE name = '" + results.getString(1) + "' AND code = '" + results.getString(2) + "' AND pword = '" + results.getString(3) + "'");
					carryon = false;
				}
			}
		}
		
		// if we didn't find a match something is wrong
		if (carryon) {
			retVal=false;
		} else {
			retVal=true;
		}
				
		freeStat.close();
		
		return retVal;
		
	} //EOF confirmVoted()


	/**
	 * Using data from the PollManager sets a user as having voted
	 *
	 * @param name  String representing the voter's name
	 * @param code  String representing the voter's vote-specific code
	 * @param pword String representing the voter's password
	 * @returns    A boolean, true if the details match with one in the database
	 */
	protected static void PMconfirmVoted(String name, String code, String pword) throws Exception {
	
		/* Get a connection from the pool */
		Connection freeConn = getConnection();
		Statement freeStat = freeConn.createStatement();
			
		freeStat.execute("UPDATE Roll SET voted = '" + encrypt("T") + "' WHERE name = '" + encrypt(name) + "' AND code = '" + encrypt(code) + "' AND pword = '" + encrypt(pword) + "'");

		freeStat.close();
		
	} //EOF PMconfirmVoted()


	/**
	 * Goes through all Electoral Roll data to create keys made with key 1 and writes them
	 * to a file for export.
	 *
	 */
	protected static void makeAllKeys() throws Exception {
	
		ERServer.NORM.info("Building all keys...");
		
		File keyFile = new File("rtserver.keys");
		File testFile = new File("rtserver.test.keys"); //for test ballots
		FileWriter out = new FileWriter(keyFile);
		FileWriter t_out = new FileWriter(testFile);
		ResultSet results;
		int count;
		boolean res, res2;
		boolean test=false;
		String temp;
	
		/* Get a connection from the pool */
		Connection freeConn = getConnection();
		Statement freeStat = freeConn.createStatement();
			
		/* search for the data */
		// only select data which has been through stage one authentication
		res = freeStat.execute("SELECT * FROM Roll");
		
		/* crunch to build each key */
		count = freeStat.getUpdateCount();
		results = freeStat.getResultSet();
		if (!res) {  // if no result data records
			throw new Exception("No data to make keys with");
		} else {  // otherwise do work
			while(results.next()) {
				// build + encrypt output
				if (decrypt(results.getString(4)).equals("B")) {
					/* test ballot */
					temp = AuthKey.build(decrypt(results.getString(1)),decrypt(results.getString(2)),decrypt(results.getString(3)),1);
					temp = AuthKey.encrypt(temp,1);
					t_out.write(temp + "\r\n");
					// erase all sign that it was a test ballot
					Connection freeConn2 = getConnection();
					Statement freeStat2 = freeConn2.createStatement();
					res2 = freeStat2.execute("UPDATE Roll SET voted = '" + encrypt("F") + "' WHERE name = '" + results.getString(1) + "' AND code = '" + results.getString(2) + "' AND pword = '" + results.getString(3) + "'");
					freeStat2.close();
					test=true;
				} else {
					/* normal user */
					temp = AuthKey.build(decrypt(results.getString(1)),decrypt(results.getString(2)),decrypt(results.getString(3)),1);
					temp = AuthKey.encrypt(temp,1);
					out.write(temp + "\r\n");
				}
			}
		}
						
		out.close();
		t_out.close();
		freeStat.close();

		ERServer.NORM.info("Done! Wrote rtserver.keys for export.");
		if (test) {
			ERServer.NORM.info("Wrote rtserver.test.keys for export.");
		}
		
	} //EOF makeAllKeys()


	/**
	 * Imports users from a CSV file and inserts them into ERServer database.
	 *
	 */
	 protected static void importUsers() throws Exception {
		int normal=0;
		int test=0;
		int line=0;
		ResultSet result;
		boolean res;
		String inData = "";

		ERServer.NORM.info("Importing users from erserver.users");
	
		BufferedReader in = new BufferedReader(new FileReader("erserver.users"));
		
		inData = in.readLine();

		while (inData!=null) {
			line++;
			/* boundary check the data first */
			// only if the data is alphanumeric, punctuation or spaces do we carry on
			if (isSafe(inData)) {
				// if a blank line then skip
				if ((inData.equals(""))||(inData.equals(" "))||(inData.equals(null))) {
					// do nothing
					ERServer.NORM.info("importUsers - blank line skipped on line " + line);
				} else {
				
					// parse to get seperate data fields
					int i = 0;
					String[] pp = new String[] {"", "", "", "", "", "", "", "", "", ""};
	
					StringTokenizer splitter = new StringTokenizer(inData, ",", false);

					while (splitter.hasMoreTokens()) {
						pp[i] = splitter.nextToken();
						i++;
					}
					
					String name = pp[0];
					String code = pp[1];
					String pword = pp[2];
					String status = pp[3];
					String vote_where = pp[4];

					// validity checks
					switch(status.charAt(0)) {
						case 'F': break; // normal voter
						case 'B': break; // test ballot
						default : throw new Exception("importUsers - bad voter status in record, only F or B allowed. On line " + line);
					}

					switch(vote_where.charAt(0)) {
						case 'I': break;  // internt
						case 'M': break;  // mail (postal)
						case 'P': break;  // polling station
						case 'O': break;  // other
						default : throw new Exception("importUsers - bad vote_where in record, only I,M,P or O allowed. On line " + line);
					}
					
					/* search DB for duplicates */
					
					// Get a connection from the pool
					Connection freeConn = getConnection();
					Statement freeStat = freeConn.createStatement();
					res = freeStat.execute("SELECT voted FROM Roll WHERE name = '" + encrypt(name) + "' AND code = '" + encrypt(AuthSys.makeDigest(code)) + "' AND pword = '" + encrypt(AuthSys.makeDigest(pword)) + "'");
					result = freeStat.getResultSet();
		
					// check answer 
					res = result.next();
					if (!res) {
						doSQL("INSERT INTO Roll VALUES ('" + encrypt(name) + "', '" + encrypt(AuthSys.makeDigest(code)) + "', '" + encrypt(AuthSys.makeDigest(pword)) + "', '" + encrypt(status) + "', '" + encrypt(vote_where) + "','','');");
					} else {
						// we have a duplicate so inform and skip
						ERServer.NORM.info("importUsers - record on line " + line + " already exists in DB.");
					}

					freeStat.close();

					/* ratio count */
					if (status.equals("B")) {
						// Test Ballot
						test++;
					} else {
						// Normal user
						normal++;
					}
				}
					inData = in.readLine();
			} else {
				throw new Exception("importUsers - invalid character(s) used on line " + line);
			}
		}
	 
	 	float proportions = new Float(test).floatValue() / (new Float(test).floatValue() + new Float(normal).floatValue());
	 	if (proportions>0.29) {
	 		ERServer.NORM.warn("Proportion of Test Ballots is rather high: " + new Float(proportions * 100).toString() + "%");
	 	}
		ERServer.NORM.info("Import complete.");
		
	 } //eof importUsers


	/**
	 * executes the SQL command sent as a parameter and returns the result to DB Console.
	 *
	 * @param  sqlCommand  A string containing SQL
	 */
	protected static void doSQL(String sqlCommand) {
	
		ResultSet results;
		boolean res;
		int count;
		
		try {
			/* Is it a custom command? Denoted by a '~' */
			if (sqlCommand.charAt(0) == '~') {
				// parse into SQL
				String data, name, code, pword;
				int i=0;
				int t=0;

				data = sqlCommand.substring(2);
									
				// parse to get seperate data fields
				while (data.charAt(i)!='-') {
					i++;
				}
				name = data.substring(0,i);

				t = i+2;
					
				while (data.charAt(t)!='-') {
					t++;
				}
			
				code = data.substring((i+1),t);									
				pword = data.substring(t+1);

				sqlCommand = "INSERT INTO Roll VALUES ('" + encrypt(name) + "', '" + encrypt(AuthSys.makeDigest(code)) + "', '" + encrypt(AuthSys.makeDigest(pword)) + "', '" + encrypt("F") + "', '" + encrypt("I") + "','','');";
			}			
		
			/* boundary check the data first */
			// only if the data is alphanumeric, punctuation or spaces do we carry on
			if (isSafe(sqlCommand)) {
							
				ERServer.DEV.info("Executing: " + sqlCommand);
		
				/* Get a connection from the pool */
				Connection freeConn = getConnection();
				Statement freeStat = freeConn.createStatement();
			
				/* execute SQL */
				res = freeStat.execute(sqlCommand);
										
				/* display results */
				count = freeStat.getUpdateCount();
				results = freeStat.getResultSet();
				if (!res) {  // if no result data records
					ERServer.frame2.showInfo(count + " record(s) affected.");
					ERServer.frame2.showInfo("-DONE-");
				} else {  // otherwise show results
					while(results.next()) {
						ERServer.frame2.showInfo(decrypt(results.getString(1)) + "  " + decrypt(results.getString(2)) + "  " + decrypt(results.getString(3)) + "  " + decrypt(results.getString(4)) + "  " + decrypt(results.getString(5)) + "  " + decrypt(results.getString(6)) + "  " + decrypt(results.getString(7)));
					}  //FIXME: Improve flexibility as assumes seven columns of results
					ERServer.frame2.showInfo("-DONE-");
				}
      
				freeStat.close();
				
			} else {
			
				throw new Exception("doSQL - invalid character(s) used");
				
			}
			
		} catch(Exception sqle) {
			ERServer.frame2.showError("Database error: " + sqle.toString());
			ERServer.NORM.error("Database error: " + sqle.toString());
		}
		
	} //EOF doSQL()
	


} //EOF Class