/**
 * Class: userVoiceUtil
 * Author: Patrick Brinksma
 * Purpose: Create an encrypted SSO Token for Salesforce.com - UserVoice integration 
 * Example: 
 * userVoiceUtil.createSSOToken('en', 'userGuid', 'deny', true, false, new List<Integer>{10001}, new List<Integer>{10002,10003});
 * Log:
 * 26 Mar 2014	Patrick Brinksma	Created
 */
public with sharing class userVoiceUtil {

    // Account key for Uservoice
    private static final String ACCOUNTKEY = '<Your Uservoice Account Key>';
    // API key for Uservoice
    private static final String APIKEY = '<Your Uservoice API Key>';
    // Initialization Vector for encryption
    private static final String INITVECTOR = 'OpenSSL for Ruby'; 
    // Host paramater for UserVoice
    private static final String HOST = '<Your Uservoice Host>';
    
    /**
     * Method to create encrypted UserVoice SSO token
     * @param userLocale - language of the user
     * @param guid - unique identifier of user
     * @param adminAccess - Grant the user account admin access (deny, grant)
     * @param receiveUpdates - Whether the user will receive updates on suggestions (on create only)
     * @param commentUpdates - Whether the user will receive updates on suggestion comments (on create only)
     * @param allowedForums - Exclusive list of Forum ids user has access to 
     * @param deniedForums - List of Forum ids user does not have access to 
     * @return - encrypted SSO Token
     */ 
    public static String createSSOToken( String userLocale, String guid, String adminAccess, Boolean receiveUpdates, Boolean commentUpdates, List<Integer> allowedForums, List<Integer> deniedForums ) {

        // Construct Map for Token encryption
        Map<String, Object> mapOfToken = new Map<String, Object>();

        // Token is valid for 1 hour
        mapOfToken.put( 'expires', System.now().addHours(1).formatGmt( 'yyyy-MM-dd HH:mm:ss' ) );
        
        // User details
        mapOfToken.put( 'guid', guid );
        mapOfToken.put( 'email', UserInfo.getUserEmail() );

        // Construct display name
        String displayName = UserInfo.getFirstName();
        if ( displayName == null ) {
            displayName = '';
        } else {
            displayName += ' ';
        }
        mapOfToken.put( 'display_name', displayName + UserInfo.getLastName() );
        mapOfToken.put( 'locale', userLocale );
        mapOfToken.put( 'admin', adminAccess );

        // Forums
        if ( !allowedForums.isEmpty() ) {
            mapOfToken.put( 'allow_forums', allowedForums );
        }

        if ( !deniedForums.isEmpty() ) {
            mapOfToken.put( 'deny_forums', deniedForums );
        }

        // Updates
        mapOfToken.put( 'updates', receiveUpdates );
        mapOfToken.put( 'comment_updates', commentUpdates );

        // Return encrypted JSON
        return encrypt( JSON.serialize( mapOfToken ) );
    }
    
    /**
     * Method to encrypt string
     * @param strData - data to be encrypted
     * @return - encrypted data
     */ 
    private static String encrypt( String strData ) {

        // Generate salted hash and private key
        String strSalted       = APIKEY + ACCOUNTKEY;
        Blob blobPrivateKey    = getFirst16Bytes( Crypto.generateDigest( 'SHA1', Blob.valueOf( strSalted ) ) ); 

        // XOR first 16 bytes of data with first 16 bytes of IV
        strData = xorFirst16Bytes( strData );

        // Encrypt data
        Blob blobData = Blob.valueOf( strData );
        Blob blobIV = Blob.valueOf( INITVECTOR );
        Blob encryptedData = Crypto.encrypt( 'AES128', blobPrivateKey, blobIV, blobData );

        // return ASCII url encoded base64 encoded
        return EncodingUtil.urlEncode( EncodingUtil.base64Encode( encryptedData ), 'ASCII' );
    }

    /**
     * Method to decrypt string (added to be used in Test class for assertion)
     * @param strData - data to be decrypted
     * @return - decrypted data
     */ 
    public static String decrypt( String strData ) {

        // Generate salted hash and private key
        String strSalted       = APIKEY + ACCOUNTKEY;
        Blob blobPrivateKey    = getFirst16Bytes( Crypto.generateDigest( 'SHA1', Blob.valueOf( strSalted ) ) ); 

        // Decrypt data
        Blob blobData = EncodingUtil.base64Decode( EncodingUtil.urlDecode( strData , 'ASCII' ) );
        Blob blobIV = Blob.valueOf( INITVECTOR );
        Blob decryptedData = Crypto.decrypt( 'AES128', blobPrivateKey, blobIV, blobData );

        // XOR first 16 bytes of data with first 16 bytes of IV
        strData = xorFirst16Bytes( decryptedData.toString() );        
        
        return strData;
    }    
    
    /**
     * Method to XOR first 16 bytes of input with first 16 bytes of IV
     * @param dataIn - data to be XOR'd
     * @return - XOR'd data
     */     
    private static String xorFirst16Bytes( String dataIn ) {

        // Get first 16 bytes from Initialization Vector in Hex
        Blob blobIV = Blob.valueOf( INITVECTOR );
        String strIVHexFromBlob = EncodingUtil.convertToHex( blobIV );
        String strIVFirst16Bytes = strIVHexFromBlob.left( 32 );     

        // Get first 16 bytes from input string in Hex
        Blob blobData = Blob.valueOf( dataIn );
        String strDataHexFromBlob = EncodingUtil.convertToHex( blobData );
        String strDataFirst16Bytes = strDataHexFromBlob.left( 32 );     

        // For every Hex value, get the ASCII value for both IV and Data
        // and XOR the result, convert it back to String, and concatenate.
        String xorResult = '';
        for ( Integer i=0; i<32; i+=2 ) {
            Integer iIV = parseInt( strIVFirst16Bytes.substring( i, i + 2 ) );
            Integer iData = parseInt( strDataFirst16Bytes.substring( i, i + 2 ) );
            Integer iXor = iData ^ iIV;
            // ASCII value back to character and concatenate
            xorResult += String.fromCharArray(new List<Integer>{iXor});
        }

        // Convert result into HEX to replace first 16 bytes of input data
        Blob blobXorResult = Blob.valueOf(xorResult);
        String hexXorResult = EncodingUtil.convertToHex(blobXorResult);
        String hexFinal = hexXorResult + strDataHexFromBlob.substring(32);

        // Convert resulting hex back to String
        String dataOut = '';
        for ( Integer i=0; i < hexFinal.length(); i+=2 ) {
            Integer iChar = parseInt( hexFinal.substring( i, i + 2) );
            dataOut += String.fromCharArray( new List<Integer>{iChar} );
        }

        return dataOut;
    }

    /**
     * Convert HEX value (example '2A') to Integer value
     * Code copied and adapted from the apex-lang library which 
     * can be found at: http://code.google.com/p/apex-lang/
     * @param str - String (hex) to be converted 
     * @return - ASCII Integere
     */     
    private static Integer parseInt( String str ) {
        str = str.toUpperCase();
        String validChars = '0123456789ABCDEF';
        final Integer LENGTH = str.length();
        Long returnValue = 0;
        for( Integer i = 0; i < LENGTH; i++ ) {
            returnValue +=
                ( validChars.indexOf( str.substring( LENGTH-i-1, LENGTH-i ) )
                * Math.pow( 16,i ) ).longValue();
        }
        return returnValue.intValue();
    }   

    /**
     * Method to get first 16 bytes of a Blob
     * @param blobInput - blob to get first 16bytes
     * @return - first 16 bytes in Blob
     */     
/*
// The method EncodingUtil.ConvertFromHex will only work 
// from Salesforce.com API v30.0 and above
    private static Blob getFirst16Bytes( Blob blobInput ) {

        String strHexFromBlob = EncodingUtil.convertToHex( blobInput );
        String strFirst16Bytes = strHexFromBlob.left( 32 );

        return EncodingUtil.convertFromHex( strFirst16Bytes );
    }
*/     
    
    /**
     * Return first 16 bytes from given Blob
     * @param blobInput - Blob value 
     * @return - First 16 bytes of Blob value
     */     
    private static Blob getFirst16Bytes(Blob blobInput) {

        // Maps for converting String to Integer and back for Base64
        Map<String, Integer> mapBase64str2int = new Map<String, Integer>{ 'A'=>0,'B'=>1,'C'=>2,'D'=>3,'E'=>4,'F'=>5,'G'=>6,'H'=>7,'I'=>8,'J'=>9,'K'=>10,'L'=>11,'M'=>12,'N'=>13,'O'=>14,'P'=>15,'Q'=>16,'R'=>17,'S'=>18,'T'=>19,'U'=>20,'V'=>21,'W'=>22,'X'=>23,'Y'=>24,'Z'=>25,'a'=>26,'b'=>27,'c'=>28,'d'=>29,'e'=>30,'f'=>31,'g'=>32,'h'=>33,'i'=>34,'j'=>35,'k'=>36,'l'=>37,'m'=>38,'n'=>39,'o'=>40,'p'=>41,'q'=>42,'r'=>43,'s'=>44,'t'=>45,'u'=>46,'v'=>47,'w'=>48,'x'=>49,'y'=>50,'z'=>51,'0'=>52,'1'=>53,'2'=>54,'3'=>55,'4'=>56,'5'=>57,'6'=>58,'7'=>59,'8'=>60,'9'=>61,'+'=>62,'/'=>63 };
        Map<Integer, String> mapBase64int2str = new Map<Integer, String>{ 0=>'A',16=>'Q',32=>'g',48=>'w' };     
    
        String strBase64 = EncodingUtil.base64encode(blobInput);
        //last 2bit byte 16 + 4bit of byte 17
        String strLastByte = strBase64.substring(21,22);
        //first 21 characters will never change, byte 1-15 + 6 bits of byte 16 
        strBase64 = strBase64.substring(0,21); 

        Integer intLastByte = mapBase64str2int.get(strLastByte);
        intLastByte = intLastByte & 48; // mask lower 4 bit (those of byte 17)

        strBase64 = strBase64 + mapBase64int2str.get(intLastByte) + '=='; // get right value + add ending == for a 16byte base64 string
        Blob blReturn = EncodingUtil.base64decode(strBase64);

        return blReturn;
    }   

}