Search code examples
phphashcoldfusioncoldfusion-10sha512

Hash Function that works identically on ColdFusion 10+ and PHP 7.x?


I am currently working on a new PHP site for a site currently utilizing ColdFusion 10. When the new site is ready the ColdFusion site will be decommissioned and I won't have access to anything related to ColdFusion. I don't want to have to reset all the previous passwords so need to be able to duplicate the one-way SHA-512 hash that is utilized in ColdFusion in PHP.

This question on Stack Overflow is very relevant to this problem: hash function that works identically on ColdFusion MX7 and PHP 5.x?

The difference is they manually looped in 1024 iterations in ColdFusion. The ColdFusion site I am translating uses the built in iterations feature. I have tried what they did in the above question, plus a few variations including XOR in the end but ultimately I can't find documentation on what ColdFusion is doing during those iterations.

ColdFusion:

<cfset hpswd = Hash(FORM.npswd & salt, "SHA-512", "UTF-8", 1000) >

PHP (without iterations logic):

$hpswd = strtoupper(hash("sha512", $npswd.$salt));

Given this password: q7+Z6Wp@&#hQ

With this salt: F4DD573A-EC09-0A78-61B5DA6CBDB39F36

ColdFusion gives this Hash (with 1000 iterations): 1FA341B135918B61CB165AA67B33D024CC8243C679F20967A690C159D1A48FACFA4C57C33DDDE3D64539BF4211C44C8D1B18C787917CD779B2777856438E4D21

Even with making sure to strtoupper with PHP I have not managed to duplicate the iterations step so the question, what operands is ColdFusion 10+ doing during the iterations step?


Solution

  • Regardless of language, a SHA-512 hashing function should return the same output given the same inputs. Here, it looks like you may just need to ensure that your inputs are the same. This includes the encoding of the text you are inputting. Then you'll hash over it the same total number of times.

    As of today, the CFDocs documentation of ColdFusion hash() is incorrect, but I have submitted a correction for that. See my comments above about why I believe Adobe lists their defaults this way. A Default of 1 Iteration is correct for Lucee CFML, but not for Adobe CF. You are correct that the ACF default is 0. CF2018 clarifies this parameter.

    Now, to your issue, your original code in ACF10 is:

    <cfset hpswd = Hash(FORM.npswd & salt, "SHA-512", "UTF-8", 1000) >
    

    This says that you are hashing with the SHA-512 algorithm, using UTF-8 encoding, and repeating an additional 1000 times. This means that your hash() function is actually being called 1001 times for your final output.

    So:

    <cfset npswd="q7+Z6Wp@&##hQ">
    <cfset salt = "F4DD573A-EC09-0A78-61B5DA6CBDB39F36">
    <cfset hpswd = Hash(npswd & salt, "SHA-512","UTF-8",1000) >
    
    <cfoutput>#hpswd#</cfoutput>
    

    Gives us 1FA341B135918B61CB165AA67B33D024CC8243C679F20967A690C159D1A48FACFA4C57C33DDDE3D64539BF4211C44C8D1B18C787917CD779B2777856438E4D21.

    https://trycf.com/gist/7212b3ee118664c5a7f1fb744b30212d/acf?theme=monokai

    One thing to note is that the ColdFusion hash() function returns a HEXIDECIMAL string of the hashed input, but when it uses it's iteration argument, it iterates over the BINARY output of the hashed value. This will make a big difference in the final output.

    https://trycf.com/gist/c879e9e900e8fd0aa23e766bc308e072/acf?theme=monokai

    To do this in PHP, we'd do something like this:

    NOTE: I am not a PHP developer, so this is probably not the best way to do this. Don't judge me, please. :-)

    <?php
    mb_internal_encoding("UTF-8");
    $npswd="q7+Z6Wp@&#hQ";
     $salt = "F4DD573A-EC09-0A78-61B5DA6CBDB39F36";
    $hpswd = $npswd.$salt ;
    for($i=1; $i<=1001; $i++){
       $hpswd = hash("SHA512",$hpswd,true); // raw_output=true argument >> raw binary data. 
            // > https://www.php.net/manual/en/function.hash.php
    }
    
    echo(strtoupper(bin2hex($hpswd)));
    ?> 
    

    The first thing I do is ensure that the encoding we are using is UTF-8. Then I iterate over the given input string 1+1000 times. Using the raw_output argument of PHP hash() gives us binary representations each loop, which will give us the same final output. Afterwards, we use bin2hex() to convert the final binary value to a hexidecimal value, and then strtoupper() to uppercase it. Giving us 1FA341B135918B61CB165AA67B33D024CC8243C679F20967A690C159D1A48FACFA4C57C33DDDE3D64539BF4211C44C8D1B18C787917CD779B2777856438E4D21, matching the CF-hashed value.

    Also note that CF returns an uppercase value whereas PHP is lowercase.

    And final note: There are better methods for storing and using hashed passwords in PHP. This will help convert between CF and PHP hashes, but it would probably be better to ultimately convert all stored hashes into the PHP equivalents. https://www.php.net/manual/en/faq.passwords.php

    =============================================================

    A point of clarification:

    Both Adobe and Lucee changed the name of this parameter to clarify their intent, however they behave differently.

    Lucee named the parameter numIterations with default 1. This is the total times that hash() will run.

    In CF2018, with the introduction of Named Parameters, Adobe renamed the parameter additionalIterations from the original (and still documented) iterations. The original improper parameter name didn't matter prior to CF2018 because you couldn't use named params anyway. On their hash() documentation page, their verbiage is "Hence, this parameter is the number of iterations + 1. The default number of additional iterations is 0." (emphasis mine) The behavior has always (since CF10) matched this description, but there is clearly some confusion about its actual meaning, especially since there is a difference with Lucee's behavior and with Adobe's incorrect initial name of the parameter.

    The parameter name iterations is incorrect and doesn't work with either Adobe CF 2018 or Lucee 4.5 or 5.x. And this is a function that is not currently compatible as-is between Lucee and Adobe ColdFusion.

    The important thing to remember, especially if working with both Adobe and Lucee code, is that this function with the ???Iterations named param specified will produce two different outputs if the same-ish code is run. Adobe will run a hash() one additional time vs Lucee. The good news is that since the param names aren't the same, then if they are used, an error will be thrown instead of silently producing different hashes.

    hash("Stack Overflow","md5","UTF-8",42) ;
    // Lucee: C0F20A4219490E4BF9F03ED51A546F27
    // Adobe: 42C57ECBF9FF2B4BEC61010B7807165A
    
    hash(input="Stack Overflow", algorithm="MD5", encoding="UTF-8", numIterations=42) ;
    // Lucee: C0F20A4219490E4BF9F03ED51A546F27
    // Adobe: Error: Parameter validation error
    
    hash(string="Stack Overflow", algorithm="MD5", encoding="UTF-8", additionalIterations=42) ;
    // Lucee: Error: argument [ADDITIONALITERATIONS] is not allowed
    // Adobe: 42C57ECBF9FF2B4BEC61010B7807165A
    

    https://helpx.adobe.com/coldfusion/cfml-reference/coldfusion-functions/functions-h-im/hash.html https://docs.lucee.org/reference/functions/hash.html