/*
 * @(#)MD5.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */

package com.jscape.inet.util;


/**
 * Implements basic MD5 hash functionality based on algorithm described in RFC 1321.
 * <p/>
 * Example Usage:
 * <p/>
 * <pre>
 * String data = "secret";
 * MD5 hash = new MD5(data);
 * String hashValue = hash.getHash();
 * </pre>
 * <br>Example Usage for keyed hashing:<P>
 * <pre>
 * String data = "secret";
 * String key = "confidential";
 * MD5 hash = new MD5(data,key);
 * String hashValue = hash.getHash();
 * </pre>
 */

public class MD5 {

    int aA;
    int aB;
    int aC;
    int aD;
    int[] d;
    int numwords;
    byte ipad = 0x36; //ipad value
    byte opad = 0x5c; //opad value
    byte[] keyBytearray; // key_bytearray is to store keyvalue bytes
    byte[] valueBytearray; // value_bytearray is to store value bytes
    byte[] keyXorIpad;
    byte[] byteHashKeyXorIpad;
    byte[] keyXorOpad;

    /**
     * Constructs new MD5 instance.
     *
     * @param value the <code>String</code> value to hash
     */
    public MD5(String value) {
        byte[] abyte0 = new byte[value.length()];

        for (int i = 0; i < value.length(); i++) {
            abyte0[i] = (byte) (value.charAt(i) & 0xff);
        }
        initialize(abyte0);
        seed();
    }

    /**
     * Constructs new MD5 instance.
     *
     * @param bytes a <code>byte[]</code> to hash
     */
    public MD5(byte[] bytes) {
        initialize(bytes);
        seed();
    }


    /**
     * Constructs new MD5 instance.
     *
     * @param value    the <code>String</code> to hash
     * @param keyvalue the <code>String</code> used as secret key
     */
    public MD5(String value, String keyvalue) {
        valueBytearray = new byte[value.length()];

        keyXorIpad = new byte[64 + value.length()]; //this is to store xor value of key and ipad and also the appended data

        //
        // check if key is null then do simple hashing on value
        //

        for (int i = 0; i < value.length(); i++) { //convert value string into bytes
            valueBytearray[i] = (byte) (value.charAt(i) & 0xff);
        }

        if (keyvalue == null) { //do simple hashing
            initialize(valueBytearray);
            seed();
            return;
        }


        //
        //step 1. compress key if it is > 64 bytes
        //

        keyBytearray = new byte[keyvalue.length() > 64 ? keyvalue.length() : 64]; // to store bytes of keyvalue
        byte[] temp;   //temporary array of bytes to store compressed keyvalue

        if (keyvalue.length() > 64) {
            //if key is long compress it using hash to 16 bytes
            for (int i = 0; i < keyvalue.length() - 1; i++) {
                keyBytearray[i] = (byte) (keyvalue.charAt(i) & 0xff);
            }

            //perform the hashing on key
            initialize(keyBytearray);
            seed();
            temp = fromHexString(getHash()); // take the hash value and convert it into array of bytes

            for (int i = 0; i < temp.length; i++) {
                //copy compressed key into key_bytearray
                keyBytearray[i] = temp[i];
            }
        } else {   //If key size is < 64 then no need to compress it.
            for (int i = 0; i < keyvalue.length(); i++) {
                keyBytearray[i] = (byte) (keyvalue.charAt(i) & 0xff);
            }
        }

        //
        //step 2. append 0x00 to key byte array
        //

        for (int i = keyvalue.length() > 64 ? 16 : keyvalue.length(); i < 64; i++) {
            keyBytearray[i] = 0x00;
        }

        //
        //step 3. do xor of keyvalue and ipad
        //

        for (int i = 0; i < 64; i++) { // do the bitwise xor between key and ipad.
            keyXorIpad[i] = (byte) (keyBytearray[i] ^ ipad); //xor ipad and key
        }

        //
        // step 4. append data to ans of step 3
        //

        for (int i = 64, j = 0; j < valueBytearray.length; i++, j++) { // appended data is stored after 64 bytes
            keyXorIpad[i] = valueBytearray[j];
        }

        //
        // step 5. claculate hash on ans of stpe 4
        //

        initialize(keyXorIpad);
        seed();

        byteHashKeyXorIpad = new byte[16];
        byteHashKeyXorIpad = fromHexString(getHash()); //convert hex string of hash into array of bytes.

        //
        //step 6. xor key with opad
        //

        keyXorOpad = new byte[80]; // 64 for xor value and 16 for append ans of step 5
        for (int i = 0; i < 64; i++) { // xor key and opad.
            keyXorOpad[i] = (byte) (keyBytearray[i] ^ opad);
        }

        //
        //step 7. append hash of step 5 with o/p of step 6 after 64 bytes
        //

        for (int i = 64, j = 0; i < 80; i++, j++) { // append o/p of step 5 to xor value
            keyXorOpad[i] = byteHashKeyXorIpad[j];
        }

        //
        //step 8. calculate final hash
        //

        initialize(keyXorOpad);
        seed();
    }


    /**
     * Constructs new MD5 instance.
     *
     * @param value    a <code>byte[]</code> to hash
     * @param keyvalue a <code>byte[]</code> to use for secret key
     */

    public MD5(byte[] value, byte[] keyvalue) {
        valueBytearray = new byte[value.length];
        keyXorIpad = new byte[64 + value.length]; //to store xor value and append of data

        //
        // check if keyvalue is null then do simple hashing on value
        //

        if (keyvalue == null) {
            initialize(value);
            seed();
            return;
        }

        if (keyvalue.length == 0) { //do simple hashing
            initialize(value);
            seed();
            return;
        }

        //
        //step 1. compress key if it is > 64 bytes
        //

        keyBytearray = new byte[keyvalue.length > 64 ? keyvalue.length : 64];
        byte[] temp; //take temporary array of bytes to store hash of key

        if (keyvalue.length > 64) { // if key is long compress it using hash to 16 bytes.
            initialize(keyvalue);
            seed();
            temp = fromHexString(getHash()); // get the hash and store it in form of bytes.

            for (int i = 0; i < temp.length; i++) {
                keyBytearray[i] = temp[i];
            }

        } else { //no compression is needed.
            for (int i = 0; i < keyvalue.length; i++) {
                keyBytearray[i] = (byte) (keyvalue[i] & 0xff);
            }
        }

        //
        //step 2. append 0x00 to key byte array
        //

        for (int i = keyvalue.length > 64 ? 16 : keyvalue.length; i < 64; i++) {
            keyBytearray[i] = 0x00;
        }

        //
        //step 3. xor the keyvalue with ipad
        //

        for (int i = 0; i < 64; i++) { // initialize ipad and opad and key_xor_ipad
            keyXorIpad[i] = (byte) (keyBytearray[i] ^ ipad);
        }

        //
        ///step 4. append data to ans of xor
        //

        for (int i = 64, j = 0; j < value.length; i++, j++) {
            keyXorIpad[i] = value[j];
        }

        //
        //step 5. claculate hash on ans of stpe 4
        //

        initialize(keyXorIpad);
        seed();
        byteHashKeyXorIpad = new byte[16]; // because hash is of 16 bytes
        byteHashKeyXorIpad = fromHexString(getHash()); //convert hex string of hash  into array of bytes.

        //
        //step 6. xor key with opad
        //

        keyXorOpad = new byte[80]; //64 for xor and 16 to append result of step 5

        for (int i = 0; i < 64; i++) { // xor the key and opad
            keyXorOpad[i] = (byte) (keyBytearray[i] ^ opad);
        }

        //
        //step 7. append hash of step 5 with o/p of step 6
        //

        for (int i = 64, j = 0; i < 80; i++, j++) { // append result of step 5
            keyXorOpad[i] = byteHashKeyXorIpad[j];
        }

        //
        //step 8. calculate final hash
        //

        initialize(keyXorOpad);
        seed();

    }

    /**
     * fromHexString() function is to convert hex string to array of bytes.
     *
     * @param s - Hex characters
     *
     * @return value a <code>byte[]</code>of hex string
     */

    public byte[] fromHexString(String s) {
        int stringLength = s.length();
        if ((stringLength & 0x1) != 0) {  //check for even number of characters
            throw new IllegalArgumentException("fromHexString requires an even number of hex characters");
        }
        byte[] b = new byte[stringLength / 2];

        for (int i = 0, j = 0; i < stringLength; i += 2, j++) {
            int high = charToNibble(s.charAt(i));
            int low = charToNibble(s.charAt(i + 1));
            b[j] = (byte) ((high << 4) | low); //do
        }
        return b;
    }

    /**
     * charToNibble() converts char to integer for hex
     *
     * @return value a <code>integer<code>nibble value
     */

    private static int charToNibble(char c) {
        if ('0' <= c && c <= '9') {
            //if it is between 0 to 9 retrun simple integer value
            return c - '0';
        } else if ('a' <= c && c <= 'f') {
            //if it is between a and f convert them into int
            return c - 'a' + 0xa;
        } else if ('A' <= c && c <= 'F') {
            //if it is between A and F convert them into int
            return c - 'A' + 0xa;
        } else {
            throw new IllegalArgumentException("Invalid hex character: " + c);
        }
    }

    private static int f(int i, int j, int k) {
        return i & j | ~i & k;
    }

    private static int g(int i, int j, int k) {
        return i & k | j & ~k;
    }

    private static int h(int i, int j, int k) {
        return i ^ j ^ k;
    }

    private static int i(int i, int j, int k) {
        return j ^ (i | ~k);
    }

    private void round1(int i) {
        aA = rotintlft(aA + f(aB, aC, aD) + d[16 * i] + 0xd76aa478, 7) + aB;
        aD = rotintlft(aD + f(aA, aB, aC) + d[1 + 16 * i] + 0xe8c7b756, 12) + aA;
        aC = rotintlft(aC + f(aD, aA, aB) + d[2 + 16 * i] + 0x242070db, 17) + aD;
        aB = rotintlft(aB + f(aC, aD, aA) + d[3 + 16 * i] + 0xc1bdceee, 22) + aC;
        aA = rotintlft(aA + f(aB, aC, aD) + d[4 + 16 * i] + 0xf57c0faf, 7) + aB;
        aD = rotintlft(aD + f(aA, aB, aC) + d[5 + 16 * i] + 0x4787c62a, 12) + aA;
        aC = rotintlft(aC + f(aD, aA, aB) + d[6 + 16 * i] + 0xa8304613, 17) + aD;
        aB = rotintlft(aB + f(aC, aD, aA) + d[7 + 16 * i] + 0xfd469501, 22) + aC;
        aA = rotintlft(aA + f(aB, aC, aD) + d[8 + 16 * i] + 0x698098d8, 7) + aB;
        aD = rotintlft(aD + f(aA, aB, aC) + d[9 + 16 * i] + 0x8b44f7af, 12) + aA;
        aC = rotintlft(aC + f(aD, aA, aB) + d[10 + 16 * i] + -42063, 17) + aD;
        aB = rotintlft(aB + f(aC, aD, aA) + d[11 + 16 * i] + 0x895cd7be, 22) + aC;
        aA = rotintlft(aA + f(aB, aC, aD) + d[12 + 16 * i] + 0x6b901122, 7) + aB;
        aD = rotintlft(aD + f(aA, aB, aC) + d[13 + 16 * i] + 0xfd987193, 12) + aA;
        aC = rotintlft(aC + f(aD, aA, aB) + d[14 + 16 * i] + 0xa679438e, 17) + aD;
        aB = rotintlft(aB + f(aC, aD, aA) + d[15 + 16 * i] + 0x49b40821, 22) + aC;
    }

    private void round2(int i) {
        aA = rotintlft(aA + g(aB, aC, aD) + d[1 + 16 * i] + 0xf61e2562, 5) + aB;
        aD = rotintlft(aD + g(aA, aB, aC) + d[6 + 16 * i] + 0xc040b340, 9) + aA;
        aC = rotintlft(aC + g(aD, aA, aB) + d[11 + 16 * i] + 0x265e5a51, 14) + aD;
        aB = rotintlft(aB + g(aC, aD, aA) + d[16 * i] + 0xe9b6c7aa, 20) + aC;
        aA = rotintlft(aA + g(aB, aC, aD) + d[5 + 16 * i] + 0xd62f105d, 5) + aB;
        aD = rotintlft(aD + g(aA, aB, aC) + d[10 + 16 * i] + 0x2441453, 9) + aA;
        aC = rotintlft(aC + g(aD, aA, aB) + d[15 + 16 * i] + 0xd8a1e681, 14) + aD;
        aB = rotintlft(aB + g(aC, aD, aA) + d[4 + 16 * i] + 0xe7d3fbc8, 20) + aC;
        aA = rotintlft(aA + g(aB, aC, aD) + d[9 + 16 * i] + 0x21e1cde6, 5) + aB;
        aD = rotintlft(aD + g(aA, aB, aC) + d[14 + 16 * i] + 0xc33707d6, 9) + aA;
        aC = rotintlft(aC + g(aD, aA, aB) + d[3 + 16 * i] + 0xf4d50d87, 14) + aD;
        aB = rotintlft(aB + g(aC, aD, aA) + d[8 + 16 * i] + 0x455a14ed, 20) + aC;
        aA = rotintlft(aA + g(aB, aC, aD) + d[13 + 16 * i] + 0xa9e3e905, 5) + aB;
        aD = rotintlft(aD + g(aA, aB, aC) + d[2 + 16 * i] + 0xfcefa3f8, 9) + aA;
        aC = rotintlft(aC + g(aD, aA, aB) + d[7 + 16 * i] + 0x676f02d9, 14) + aD;
        aB = rotintlft(aB + g(aC, aD, aA) + d[12 + 16 * i] + 0x8d2a4c8a, 20) + aC;
    }

    private void round3(int i) {
        aA = rotintlft(aA + h(aB, aC, aD) + d[5 + 16 * i] + 0xfffa3942, 4) + aB;
        aD = rotintlft(aD + h(aA, aB, aC) + d[8 + 16 * i] + 0x8771f681, 11) + aA;
        aC = rotintlft(aC + h(aD, aA, aB) + d[11 + 16 * i] + 0x6d9d6122, 16) + aD;
        aB = rotintlft(aB + h(aC, aD, aA) + d[14 + 16 * i] + 0xfde5380c, 23) + aC;
        aA = rotintlft(aA + h(aB, aC, aD) + d[1 + 16 * i] + 0xa4beea44, 4) + aB;
        aD = rotintlft(aD + h(aA, aB, aC) + d[4 + 16 * i] + 0x4bdecfa9, 11) + aA;
        aC = rotintlft(aC + h(aD, aA, aB) + d[7 + 16 * i] + 0xf6bb4b60, 16) + aD;
        aB = rotintlft(aB + h(aC, aD, aA) + d[10 + 16 * i] + 0xbebfbc70, 23) + aC;
        aA = rotintlft(aA + h(aB, aC, aD) + d[13 + 16 * i] + 0x289b7ec6, 4) + aB;
        aD = rotintlft(aD + h(aA, aB, aC) + d[16 * i] + 0xeaa127fa, 11) + aA;
        aC = rotintlft(aC + h(aD, aA, aB) + d[3 + 16 * i] + 0xd4ef3085, 16) + aD;
        aB = rotintlft(aB + h(aC, aD, aA) + d[6 + 16 * i] + 0x4881d05, 23) + aC;
        aA = rotintlft(aA + h(aB, aC, aD) + d[9 + 16 * i] + 0xd9d4d039, 4) + aB;
        aD = rotintlft(aD + h(aA, aB, aC) + d[12 + 16 * i] + 0xe6db99e5, 11) + aA;
        aC = rotintlft(aC + h(aD, aA, aB) + d[15 + 16 * i] + 0x1fa27cf8, 16) + aD;
        aB = rotintlft(aB + h(aC, aD, aA) + d[2 + 16 * i] + 0xc4ac5665, 23) + aC;
    }

    private void round4(int i) {
        aA = rotintlft(aA + i(aB, aC, aD) + d[16 * i] + 0xf4292244, 6) + aB;
        aD = rotintlft(aD + i(aA, aB, aC) + d[7 + 16 * i] + 0x432aff97, 10) + aA;
        aC = rotintlft(aC + i(aD, aA, aB) + d[14 + 16 * i] + 0xab9423a7, 15) + aD;
        aB = rotintlft(aB + i(aC, aD, aA) + d[5 + 16 * i] + 0xfc93a039, 21) + aC;
        aA = rotintlft(aA + i(aB, aC, aD) + d[12 + 16 * i] + 0x655b59c3, 6) + aB;
        aD = rotintlft(aD + i(aA, aB, aC) + d[3 + 16 * i] + 0x8f0ccc92, 10) + aA;
        aC = rotintlft(aC + i(aD, aA, aB) + d[10 + 16 * i] + 0xffeff47d, 15) + aD;
        aB = rotintlft(aB + i(aC, aD, aA) + d[1 + 16 * i] + 0x85845dd1, 21) + aC;
        aA = rotintlft(aA + i(aB, aC, aD) + d[8 + 16 * i] + 0x6fa87e4f, 6) + aB;
        aD = rotintlft(aD + i(aA, aB, aC) + d[15 + 16 * i] + 0xfe2ce6e0, 10) + aA;
        aC = rotintlft(aC + i(aD, aA, aB) + d[6 + 16 * i] + 0xa3014314, 15) + aD;
        aB = rotintlft(aB + i(aC, aD, aA) + d[13 + 16 * i] + 0x4e0811a1, 21) + aC;
        aA = rotintlft(aA + i(aB, aC, aD) + d[4 + 16 * i] + 0xf7537e82, 6) + aB;
        aD = rotintlft(aD + i(aA, aB, aC) + d[11 + 16 * i] + 0xbd3af235, 10) + aA;
        aC = rotintlft(aC + i(aD, aA, aB) + d[2 + 16 * i] + 0x2ad7d2bb, 15) + aD;
        aB = rotintlft(aB + i(aC, aD, aA) + d[9 + 16 * i] + 0xeb86d391, 21) + aC;
    }

    // initialize hash
    private void initialize(byte[] abyte0) {
        long l1 = abyte0.length * 8;
        int j = abyte0.length % 64;
        int k;

        if (j < 56) {
            k = 64 - j;
        } else {
            k = (64 - j) + 64;
        }
        int i = abyte0.length + k;
        byte[] abyte1 = new byte[i];

        for (int l = 0; l < abyte0.length; l++) {
            abyte1[l] = abyte0[l];
        }

        abyte1[abyte0.length] = -128;
        for (int i1 = abyte1.length + 1; i1 < i - 8; i1++) {
            abyte1[i1] = 0;
        }

        for (int j1 = 0; j1 < 8; j1++) {
            abyte1[(i - 8) + j1] = (byte) (int) (l1 & 255L);
            l1 >>= 8;
        }
        aA = 0x67452301;
        aB = 0xefcdab89;
        aC = 0x98badcfe;
        aD = 0x10325476;
        numwords = i / 4;
        d = new int[numwords];
        for (int k1 = 0; k1 < i; k1 += 4) {
            d[k1 / 4] = (abyte1[k1] & 0xff) + ((abyte1[k1 + 1] & 0xff) << 8) + ((abyte1[k1 + 2] & 0xff) << 16) + ((abyte1[k1 + 3] & 0xff) << 24);
        }
    }

    /**
     * Gets hash representation of this MD5.
     * Same as invoking getHash method.
     *
     * @return hash
     */
    public String toString() {
        return tohex(aA) + tohex(aB) + tohex(aC) + tohex(aD);
    }

    /**
     * Gets hash representation of this MD5.
     *
     * @return hash value
     */
    public String getHash() {
        return toString();
    }

    // seed MD5
    private void seed() {
        for (int i1 = 0; i1 < numwords / 16; i1++) {
            int i = aA;
            int j = aB;
            int k = aC;
            int l = aD;

            round1(i1);
            round2(i1);
            round3(i1);
            round4(i1);
            aA += i;
            aB += j;
            aC += k;
            aD += l;
        }
    }

    // rotate left
    private static int rotintlft(int i, int j) {
        return i << j | i >>> 32 - j;
    }

    // convert to hex
    private static String tohex(int i) {
        String s = "";

        for (int j = 0; j < 4; j++) {
            s = s + Integer.toString(i >> 4 & 0xf, 16) + Integer.toString(i & 0xf, 16);
            i >>= 8;
        }
        return s;
    }

}

