package nl.fontys.linux.utils;

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

public abstract class MD5Crypt {
    
    // Character set allowed for the salt string
    private static final String SALTCHARS
            = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    
    // Character set of the encrypted password: A-Za-z0-9./  
    private static final String ITOA64
            = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    
    private static final String MD5_SALT_PREFIX = "$1$";
    
    /**
     * Function to return a string from the set: A-Za-z0-9./
     * @return A string of size (size) from the set A-Za-z0-9./
     * @param size Length of the string
     * @param v value to be converted
     */
    private static final String to64( long v, int size ) {
        StringBuffer result = new StringBuffer();
        
        while ( --size >= 0 ) {
            result.append( ITOA64.charAt( ( int )( v & 0x3f ) ) );
            v >>>= 6;
        }
        
        return result.toString();
    }
    
    private static void clearbits( byte bits[] ) {
        for ( int i = 0; i < bits.length; i++ ) {
            bits[i] = 0;
        }
    }
    
    /**
     * convert an encoded unsigned byte value
     * into a int with the unsigned value.
     */
    private static int bytes2u( byte inp ) {
        return ( int )inp & 0xff;
    }
    
    /**
     * Linux/BSD MD5Crypt function
     * @throws java.lang.Exception
     * @return The encrypted password as an MD5 hash
     * @param salt 8 byte permutation string
     * @param password user password
     */
    public static final String crypt( String password, String salt) {
        
        byte finalState[];
        long l;
        
        /**
         * Two MD5 hashes are used
         */
        MessageDigest ctx, ctx1;
        
        try {
            ctx = MessageDigest.getInstance( "md5" );
            ctx1 = MessageDigest.getInstance( "md5" );
        } catch (NoSuchAlgorithmException ex) {
            System.err.println(ex);
            return null;
        }
        
        /* Refine the Salt first */
        /* If it starts with the magic string, then skip that */
        
        if ( salt.startsWith( MD5_SALT_PREFIX ) ) {
            salt = salt.substring( MD5_SALT_PREFIX.length() );
        }
        
        /* It stops at the first '$', max 8 chars */
        
        if ( salt.indexOf( '$' ) != -1 ) {
            salt = salt.substring( 0, salt.indexOf( '$' ) );
        }
        
        if ( salt.length() > 8 ) {
            salt = salt.substring( 0, 8 );
        }
        
        /**
         * Transformation set #1:
         * The password first, since that is what is most unknown
         * Magic string
         * Raw salt
         */
        ctx.update( password.getBytes() );
        ctx.update( MD5_SALT_PREFIX.getBytes() );
        ctx.update( salt.getBytes() );
        
        
        /* Then just as many characters of the MD5(pw,salt,pw) */
        
        ctx1.update( password.getBytes() );
        ctx1.update( salt.getBytes() );
        ctx1.update( password.getBytes() );
        finalState = ctx1.digest(); // ctx1.Final();
        
        for ( int pl = password.length(); pl > 0; pl -= 16 ) {
            ctx.update( finalState, 0, pl > 16 ? 16 : pl );
        }
        
        clearbits( finalState );
        
        /* Then something really weird... */
        
        for ( int i = password.length(); i != 0; i >>>= 1 ) {
            if ( ( i & 1 ) != 0 ) {
                ctx.update( finalState, 0, 1 );
            } else {
                ctx.update( password.getBytes(), 0, 1 );
            }
        }
        
        finalState = ctx.digest();
        
        /** and now, just to make sure things don't run too fast
         * On a 60 Mhz Pentium this takes 34 msec, so you would
         * need 30 seconds to build a 1000 entry dictionary...
         * (The above timings from the C version)
         */
        
        for ( int i = 0; i < 1000; i++ ) {
            try {
                ctx1 = MessageDigest.getInstance( "md5" );
            } catch (NoSuchAlgorithmException e0 ) {
                return null;
            }
            
            if ( ( i & 1 ) != 0 ) {
                ctx1.update( password.getBytes() );
            } else {
                ctx1.update( finalState, 0, 16 );
            }
            
            if ( ( i % 3 ) != 0 ) {
                ctx1.update( salt.getBytes() );
            }
            
            if ( ( i % 7 ) != 0 ) {
                ctx1.update( password.getBytes() );
            }
            
            if ( ( i & 1 ) != 0 ) {
                ctx1.update( finalState, 0, 16 );
            } else {
                ctx1.update( password.getBytes() );
            }
            
            finalState = ctx1.digest(); // Final();
        }
        
        /* Now make the output string */
        
        StringBuffer result = new StringBuffer();
        
        result.append( MD5_SALT_PREFIX );
        result.append( salt );
        result.append( "$" );
        
        /**
         * Build a 22 byte output string from the set: A-Za-z0-9./
         */
        l = ( bytes2u( finalState[0] ) << 16 )
        | ( bytes2u( finalState[6] ) << 8 ) | bytes2u( finalState[12] );
        result.append( to64( l, 4 ) );
        
        l = ( bytes2u( finalState[1] ) << 16 )
        | ( bytes2u( finalState[7] ) << 8 ) | bytes2u( finalState[13] );
        result.append( to64( l, 4 ) );
        
        l = ( bytes2u( finalState[2] ) << 16 )
        | ( bytes2u( finalState[8] ) << 8 ) | bytes2u( finalState[14] );
        result.append( to64( l, 4 ) );
        
        l = ( bytes2u( finalState[3] ) << 16 )
        | ( bytes2u( finalState[9] ) << 8 ) | bytes2u( finalState[15] );
        result.append( to64( l, 4 ) );
        
        l = ( bytes2u( finalState[4] ) << 16 )
        | ( bytes2u( finalState[10] ) << 8 ) | bytes2u( finalState[5] );
        result.append( to64( l, 4 ) );
        
        l = bytes2u( finalState[11] );
        result.append( to64( l, 2 ) );
        
        // Don't leave anything usable around in the vm
        clearbits( finalState );
        
        return result.toString();
    }
    
}