/*
 Copyright (C) 2002-2004 MySQL AB

 This program is free software; you can redistribute it and/or modify
 it under the terms of version 2 of the GNU General Public License as
 published by the Free Software Foundation.
 

 There are special exceptions to the terms and conditions of the GPL 
 as it is applied to this software. View the full text of the 
 exception exception in file EXCEPTIONS-CONNECTOR-J in the directory of this 
 software distribution.

 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; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

 */
package com.mysql.jdbc;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


/**
 * Methods for doing secure authentication with MySQL-4.1
 * and newer.
 *
 * @author Mark Matthews
 *
 * @version $Id: Security.java,v 1.1.4.6 2004/08/09 22:15:12 mmatthew Exp $
 */
class Security {
    private static final int SHA1_HASH_SIZE = 20;
    private static final char PVERSION41_CHAR = '*';

    /**
     * Prevent construction.
     */
    private Security() {
        super();
    }

    /*
      Convert password in salted form to binary string password and hash-salt
      For old password this involes one more hashing

      SYNOPSIS
            get_hash_and_password()
            salt         IN  Salt to convert from
            pversion     IN  Password version to use
            hash         OUT Store zero ended hash here
            bin_password OUT Store binary password here (no zero at the end)

      RETURN
            0 for pre 4.1 passwords
       !0 password version char for newer passwords
    */

    /**
     * DOCUMENT ME!
     *
     * @param salt DOCUMENT ME!
     * @param usingNewPasswords DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     *
     * @throws NoSuchAlgorithmException if the message digest 'SHA-1' is not
     * available.
     */
    static byte[] getBinaryPassword(int[] salt, boolean usingNewPasswords)
        throws NoSuchAlgorithmException {
        int val = 0;

        byte[] binaryPassword = new byte[SHA1_HASH_SIZE]; /* Binary password loop pointer */

        if (usingNewPasswords) /* New password version assumed */ {
            int pos = 0;

            for (int i = 0; i < 4; i++) /* Iterate over these elements*/ {
                val = salt[i];

                for (int t = 3; t >= 0; t--) {
                    binaryPassword[pos++] = (byte) (val & 255);
                    val >>= 8; /* Scroll 8 bits to get next part*/
                }
            }

            return binaryPassword;
        } else {
            int offset = 0;

            for (int i = 0; i < 2; i++) /* Iterate over these elements*/ {
                val = salt[i];

                for (int t = 3; t >= 0; t--) {
                    binaryPassword[t + offset] = (byte) (val % 256);
                    val >>= 8; /* Scroll 8 bits to get next part*/
                }

                offset += 4;
            }

            MessageDigest md = MessageDigest.getInstance("SHA-1");

            md.update(binaryPassword, 0, 8);

            return md.digest();
        }
    }

    /**
     * Creates key from old password to decode scramble
     * Used in 4.1 authentication with passwords stored
     * pre-4.1 hashing.
     *
     * @param passwd the password to create the key from
     *
     * @return 20 byte generated key
     *
     * @throws NoSuchAlgorithmException if the message digest 'SHA-1'
     *          is not available.
     */
    static byte[] createKeyFromOldPassword(String passwd)
        throws NoSuchAlgorithmException {
        /* At first hash password to the string stored in password */
        passwd = makeScrambledPassword(passwd);

        /* Now convert it to the salt form */
        int[] salt = getSaltFromPassword(passwd);

        /* Finally get hash and bin password from salt */
        return getBinaryPassword(salt, false);
    }

    /**
     * Creates password to be stored in user database
     * from raw string.
     *
     * Handles Pre-MySQL 4.1 passwords.
     *
     * @param password plaintext password
     *
     * @return scrambled password
     *
     * @throws NoSuchAlgorithmException if the message digest 'SHA-1' is not
     *          available.
     */
    static String makeScrambledPassword(String password)
        throws NoSuchAlgorithmException {
        long[] passwordHash = Util.newHash(password);
        StringBuffer scramble = new StringBuffer();

        scramble.append(longToHex(passwordHash[0]));
        scramble.append(longToHex(passwordHash[1]));

        return scramble.toString();
    }

    /**
     * Encrypt/Decrypt function used for password encryption in authentication
     *
     * Simple XOR is used here but it is OK as we crypt random strings
     *
     * @param from     IN     Data for encryption
     * @param to       OUT    Encrypt data to the buffer (may be the same)
     * @param password IN     Password used for encryption (same length)
     * @param length   IN     Length of data to encrypt
     */
    static void passwordCrypt(byte[] from, byte[] to, byte[] password,
        int length) {
        int pos = 0;

        while ((pos < from.length) && (pos < length)) {
            to[pos] = (byte) (from[pos] ^ password[pos]);
            pos++;
        }
    }

    /**
     * Stage one password hashing, used in MySQL 4.1 password handling
     *
     * @param password plaintext password
     *
     * @return stage one hash of password
     *
     * @throws NoSuchAlgorithmException if the message digest 'SHA-1' is not
     *          available.
     */
    static byte[] passwordHashStage1(String password)
        throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        StringBuffer cleansedPassword = new StringBuffer();

        int passwordLength = password.length();

        for (int i = 0; i < passwordLength; i++) {
            char c = password.charAt(i);

            if ((c == ' ') || (c == '\t')) {
                continue; /* skip space in password */
            }

            cleansedPassword.append(c);
        }

        return md.digest(cleansedPassword.toString().getBytes());
    }

    /**
     * Stage two password hashing used in MySQL 4.1
     * password handling
     *
     * @param hash from passwordHashStage1
     * @param salt salt used for stage two hashing
     *
     * @return result of stage two password hash
     *
     * @throws NoSuchAlgorithmException if the message digest 'SHA-1' is not
     * available.
     */
    static byte[] passwordHashStage2(byte[] hashedPassword, byte[] salt)
        throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");

        // hash 4 bytes of salt
        md.update(salt, 0, 4);

        md.update(hashedPassword, 0, SHA1_HASH_SIZE);

        return md.digest();
    }

    private static int[] getSaltFromPassword(String password) {
        int[] result = new int[6];

        if ((password == null) || (password.length() == 0)) {
            return result;
        }

        if (password.charAt(0) == PVERSION41_CHAR) {
            // new password
            String saltInHex = password.substring(1, 5);

            int val = 0;

            for (int i = 0; i < 4; i++) {
                val = (val << 4) + charVal(saltInHex.charAt(i));
            }

            return result;
        } else {
            int resultPos = 0;
            int pos = 0;
            int length = password.length();

            while (pos < length) {
                int val = 0;

                for (int i = 0; i < 8; i++) {
                    val = (val << 4) + charVal(password.charAt(pos++));
                }

                result[resultPos++] = val;
            }

            return result;
        }
    }

    /**
     * Returns hex value for given char
     */
    private static int charVal(char c) {
        return (int) (((c >= '0') && (c <= '9')) ? (c - '0')
                                                 : (((c >= 'A') && (c <= 'Z'))
        ? (c - 'A' + 10) : (c - 'a' + 10)));
    }

    private static String longToHex(long val) {
        String longHex = Long.toHexString(val);

        int length = longHex.length();

        if (length < 8) {
            int padding = 8 - length;
            StringBuffer buf = new StringBuffer();

            for (int i = 0; i < padding; i++) {
                buf.append("0");
            }

            buf.append(longHex);

            return buf.toString();
        } else {
            return longHex.substring(0, 8);
        }
    }

	//	SERVER:  public_seed=create_random_string()
		//			 send(public_seed)
		//
		//	CLIENT:  recv(public_seed)
		//			 hash_stage1=sha1("password")
		//			 hash_stage2=sha1(hash_stage1)
		//			 reply=xor(hash_stage1, sha1(public_seed,hash_stage2)
		//
		//			 // this three steps are done in scramble()
		//
		//			 send(reply)
		//
		//
		//	SERVER:  recv(reply)
		//			 hash_stage1=xor(reply, sha1(public_seed,hash_stage2))
		//			 candidate_hash2=sha1(hash_stage1)
		//			 check(candidate_hash2==hash_stage2)
		
	static byte[] scramble411(String password, String seed) throws NoSuchAlgorithmException {
		MessageDigest md = MessageDigest.getInstance("SHA-1");
		
		byte[] passwordHashStage1 = md.digest(password.getBytes());
		md.reset();
		byte[] passwordHashStage2 = md.digest(passwordHashStage1);
		md.reset();
		byte[] seedAsBytes = seed.getBytes(); // for debugging
		md.update(seedAsBytes);
		md.update(passwordHashStage2);
		
		byte[] toBeXord = md.digest();
		
		int numToXor = toBeXord.length;
		
		for (int i = 0; i < numToXor; i++) {
			toBeXord[i] = (byte)(toBeXord[i] ^ passwordHashStage1[i]);
		}
		
		return toBeXord;
		
	}
}
