13

As per this question's related answer, I'm attempting to put together a pack/unpack solution resembling this PHP process, however in Nodejs (Javascript) using md5 and bufferpack

Here's the PHP approach (adapted from DaloRADIUS:

  $challenge = 'c731395aca5dcf45446c0ae83db5319e';
  $uamsecret = 'secret';
  $password = 'password';

  $hexchal = pack ("H32", $challenge);
  $newchal = pack ("H*", md5($hexchal . $uamsecret));
  $response = md5("\0" . $password . $newchal);
  $newpwd = pack("a32", $password);
  $pappassword = implode ("", unpack("H32", ($newpwd ^ $newchal)));

  echo "Response: ---> ", $response, "\n";
  echo "New Password: ---> ", $newpwd, "\n";
  echo "Pap Password: ---> ", $pappassword, "\n";

The above echos these:

Responses PHP

Above in plaintext:

Response: ---> 2d4bd27184f5eb032641137f728c6043
New Password: ---> password
Pap Password: ---> 356a1fb08f909fc400dfe448fc483ce3

In Javascript, here's what I'm doing now:

  var challenge = 'c731395aca5dcf45446c0ae83db5319e';
  var uamsecret = 'secret';
  var password = 'password';

  var hexchal = pack.pack("H32", challenge);
  var newchal = pack.pack("H*", md5(hexchal + uamsecret));
  var response = md5("\0" + password + newchal);
  var newpwd = pack.pack("a32", password);
  var pappassword = pack.unpack("H32", (newpwd ^ newchal)).join("");

  console.log("Response: --> ", response);
  console.log("New Password: -->", newpwd);
  console.log("Pap Password: --->", pappassword);

Which gives the result:

Response Javascript

In JSON:

Response Javascript JSON format

In plaintext:

Response: -->  e8a54a55cbcd81dbc2bdfd9b197d62af
New Password: --> <Buffer >
Pap Password: ---> NaN

All the above snippets are available here: RadiusNES

My understanding in this whole process isn't the best, and will appreciate insights and where I'm going wrong.

Why is there a mismatch?

Community
  • 1
  • 1
KhoPhi
  • 9,660
  • 17
  • 77
  • 128
  • 1
    Whenever possible please try and post plain-text as formatted code in the body of the question. These screenshots can be very hard if not impossible for some people to read: They're dependent on screen readers or translation tools. – tadman Jan 14 '17 at 22:45
  • @tadman Okay, will update soon. – KhoPhi Jan 14 '17 at 22:46
  • It's not a big deal, but trying to reproduce your results by typing in long hashes is not going to be fun for anyone. I think you nee to look more closely at the precursor components you're using to assemble your response, `hexchal` and `newchal` to make sure they're the same on both sides. – tadman Jan 14 '17 at 22:48
  • @tadman I've updated question, and will take a closer look at the `hexchal` and `newchal` – KhoPhi Jan 14 '17 at 22:53
  • @tadman only the `hexchal` happens to match. From `newchal`, then none matches anymore. – KhoPhi Jan 18 '17 at 22:30
  • 1
    Check the MD5 values of those first. I think PHP might 0-byte (`NULL`) pad your string, JavaScript may truncate. – tadman Jan 18 '17 at 23:05

1 Answers1

15

The translation does not work because the PHP Pack function uses different format strings and returns strings, whilst the Javascript bufferpack module returns arrays. Also you cannot xor strings in Javascript.

Whilst there may be modules to do what you want, I have my own functions for parsing hex strings. Also I like modifying prototypes which not everyone agrees with, but these could be converted to regular functions.

String.prototype.pad = function( length ,padding ) {

    var padding = typeof padding === 'string' && padding.length > 0 ? padding[0] : '\x00'
        ,length = isNaN( length ) ? 0 : ~~length;

    return this.length < length ? this + Array( length - this.length + 1 ).join( padding ) : this;

}

String.prototype.packHex = function() {

    var source = this.length % 2 ? this + '0' : this
        ,result = '';

    for( var i = 0; i < source.length; i = i + 2 ) {
        result += String.fromCharCode( parseInt( source.substr( i , 2 ) ,16 ) );
    }

    return result;

}

var challenge = 'c731395aca5dcf45446c0ae83db5319e'
    ,uamsecret = 'secret'
    ,password = 'password';

var hexchal = challenge.packHex();
var newchal = md5( hexchal + uamsecret ).packHex();
var response = md5( '\0' + password + newchal );
var newpwd = password.pad( 32 );
var pappassword = '';
for( var i = 0; i < newchal.length; i++ ) {
    pappassword += ( newpwd.charCodeAt( i ) ^ newchal.charCodeAt( i ) ).toString( 16 );
}

console.log("Response: --> ", response);
console.log("New Password: -->", newpwd);
console.log("Pap Password: --->", pappassword);

Two functions are defined in the String prototype to replace the use of the pack function:

.pad( 32, string ) is used to pad out a string with nulls to give the same results as pack( 'a32', string ). Although not needed here it also takes a second parameter if wanting to pad the string ith a character other than nulls.

.packHex is the equivalent of pack( 'H*' ,string ) and translating the code of each pair of hex characters into a character. The function ideally needs more validation to test the string is a valid hex one if is to be used.

After the inputs have been defined, the next four lines instead set variables using these functions rather than pack.

Because Javascript cannot natively xor strings, you then need to use a loop to extract each character, convert it to a numeric, xor those values, then convert the result back into a character to create the pappassword string.

That will return, for me:

Response: -->  – "fbfd42ffde05fcf8dbdd02b7e8ae2d90"
New Password: --> – "password������������������������"
Pap Password: ---> – "dcbdacb03f5d38ca33c128b931c272a"

Which is a result, but unfortunately a different on from the PHP code.

This is because my installation of PHP is configured to use ISO-8859-1 encoding internally, whilst Javascript natively uses UTF-16.

This is not a problem in normal use, but it means the respective md5 functions will be seeing different values and therefore return a different hash.

Assuming you are writing an authentication routine using a PHP backend you will obviously need consistent results. There may be modules available to convert the encoding of the Javscript values for compatibility, but it is much easier to make changes to the PHP code.

Because we know the hex strings will be one byte, Javascript is effectively using UTF-8, so PHP can do the same by using the utf8_encode() function to convert the packed hex strings before md5ing them.

Originally I thought that Javascript was internally converting the encoded hex characters into their unicode equivalents because of this, but this was not the case. Instead it was the md5 module being used in Javascript that was performing a UTF-8 conversion on the input.

This leaves two possible options.


1. Use UTF-8 PHP

If possible you can reconfigure your PHP server to use UTF-8 encoding. Or you can change your script to use the utf8_encode() function to mirror the same process as is happening in the Javascript, and convert the hex packed strings to UTF-8 before passing them to md5()

$challenge = 'c731395aca5dcf45446c0ae83db5319e';
$uamsecret = 'secret';
$password = 'password';

$hexchal = pack ("H32", $challenge);
$newchal = pack ("H*", md5(utf8_encode($hexchal) . $uamsecret));
$response = md5("\0" . $password . utf8_encode($newchal));
$newpwd = pack("a32", $password);
$pappassword = implode ("", unpack("H32", ($newpwd ^ $newchal)));

echo "Response: ---> ", $response, "\n";
echo "New Password: ---> ", $newpwd, "\n";
echo "Pap Password: ---> ", $pappassword, "\n";

This then returns the same results as the Javscript:

Response: ---> fbfd42ffde05fcf8dbdd02b7e8ae2d90
New Password: ---> password
Pap Password: ---> dcbdacb03f5d38ca33c128b9310c272a



2. Change the md5 module in Javascript

I am assuming you are using the bluimp JavaScript-MD5 module, as this is what it used by DaloRADIUS routine you linked. You can patch this to bypass the UTF-8 conversion.

There are various ways you can patch this, but on line 259 is the definition of the md5() function itself. This is simply a set of if statements to parse the input options and call the appropriate internal function.

However, the functions called by this block simply provide the UTF-8 input conversion, through a function called str2rstrUTF8() before then call the appropriate functions to provide the actual hashing. You may therefore want to patch the md5() to accept a third parameter to indicate whether the UTF-8 conversion should be applied and then call other functions as appropriate.

However to simply remove the conversion completely the easier way is to change str2rstrUTF8() to return the input unchanged. This function can be found on line 239, changing it to just read as follows will stop the conversion:

function str2rstrUTF8 (input) {
  return input
}

Alternatively to remove the redundant function call you can instead just remove the references to it. Change the function starting on line 246 to read as follows:

function rawMD5 (s) {
  return rstrMD5(s)
}

The rawHMACMD5() function on line 252 also includes calls to the str2rstrUTF8() function which you may also want to patch for consistency but this is not required for the above routine. That function is called instead when a second parameter is passed to provide a key hash, a feature not available in the native PHP md5() function.

After making either of those changes the Javascript routine now returns the same output as your original (ISO-8859-1 using) PHP code:

Response: -->  – "2d4bd27184f5eb032641137f728c6043"
New Password: --> – "password������������������������"
Pap Password: ---> – "356a1fb08f909fc40dfe448fc483ce3"
Rebecka
  • 1,213
  • 8
  • 14
  • Is there a way to configure my Javascript to use ISO-8859-1 encoding instead of changing the PHP to use the UTF? The pappassword is sent to a Network Access Server (Router) running chillispot software, which expects the exact response from the un-altered PHP. Thus, is there a way to change the encoding on the Javascript to match the exact response? – KhoPhi Feb 02 '17 at 20:45
  • Yes! I do not have time to update the answer now, but will do it in the morning. Basically though I thought it was Javascript converting the packed hex strings into UTF, but it was actually leaving the characters codes alone and instead it was the md5 module converting its input strings to UTF. That is very easy to patch to avoid that step, and after doing so it returns the same results as your original PHP example. If you want to look for yourself it is the `str2rstrUTF8` function in md5.js that needs skipping, otherwise I will post the full details tomorrow anyway. – Rebecka Feb 03 '17 at 23:16
  • I'll take a look at the `str2rstrUTF8`. Thanks for pointing out. Will also appreciate you include in the answer when you get time. – KhoPhi Feb 04 '17 at 10:07
  • 2
    Updated, I hope the answer makes sense. The patching is very simple, but there are various ways of applying it that it comes down to personal preference. – Rebecka Feb 04 '17 at 13:28
  • @Michael this is just great, I've open another bounty for you. But I can reward this after 23 hours.. – Reigel Gallarde Mar 06 '18 at 07:15