C#SSO哈希迁移到PHP

I'm working on an SSO implementation in PHP that authenticates to a system written in C#. Here's some pseudo code to demonstrate:

$token = "MqsXexqpYRUNAHR_lHkPRic1g1BYhH6bFNVPagEkuaL8Mf80l_tOirhThQYIbfWYErgu4bDwl-7brVhXTWnJNQ2";
$id = "bob@company.com";
$ssokey = "7MpszrQpO95p7H";
$idAndKey = $id . $ssokey;
$salt = base64_decode(substr($token, 0, -1));
$hashed = hash_pbkdf2("sha256", $idAndKey, mb_convert_encoding($salt, 'UTF-16LE'), 1000, 24, false);
$data = base64_encode($hashed);

This outputs: NWZiMTBhZmNhNTlmYzMxMTEzMThhZmVl

Here's the C# version from the system with which I'm integrating:

var token = "MqsXexqpYRUNAHR_lHkPRic1g1BYhH6bFNVPagEkuaL8Mf80l_tOirhThQYIbfWYErgu4bDwl-7brVhXTWnJNQ2";
var id = "bob@company.com";
var ssokey = "7MpszrQpO95p7H";
string idAndKey = id + ssokey;
var salt = HttpServerUtility.UrlTokenDecode(token);
var pbkdf2 = new Rfc2898DeriveBytes(idAndKey, salt) {IterationCount = 1000};
var key = HttpServerUtility.UrlTokenEncode(pbkdf2.GetBytes(24)); 
Console.WriteLine(key.ToString());

This outputs: aE1k9-djZ66WbUATqdHbWyJzskMI5ABS0

I cannot figure out how to get my PHP code to do the same thing. I have a feeling it is in the salt generation.

I've tried to translate the C# HttpServerUtility.UrlTokenDecode function to PHP like so:

function UrlTokenDecode($token) {
    $numPadChars = substr($token, -1);

    // add the padded count to the end
    $salt = substr($token, 0, -1) . $numPadChars;

    // Transform the "-" to "+", and "*" to "/"
    $salt = str_replace('-', '+', str_replace('*', '/', $salt));

    // base64_decode
    $salt = base64_decode($salt);

    return $salt;
}

That didn't get me to where I needed to go. Halp!

This is for Absorb LMS. Documentation of their methods are here: https://support.absorblms.com/hc/en-us/articles/222446647-Incoming-Absorb-Single-Sign-On#Methods

Thanks!

I don't know php at all, but still can help I think. First, as stated in my comment, Rfc2898DeriveBytes in C# uses SHA1 as hash function, not SHA256, doesn't matter what your documentation says.

Next, UrlTokenDecode (and Encode) is quite strange thing I rarely seen in practice. It converts regular base64 to "url safe" version as follows:

  • replaces '+' with '-'
  • replaces '/' with '_'
  • removes padding ('==' at the end) and appends length of removed padding as a number as last character (if there were no padding - it still appends "0"). This step doesn't make any sense to me, but it's how it works.

So to replicate you need to base64_encode, replace, remove padding, and then add padding length as character. So if your base64 string ended with == - you remove that and add "2" at the end. If there was no padding - you add "0".

So to decode that string you need to make back replacement, then remove last character and add that much '=' to the end as indicated by that character.

So string

MqsXexqpYRUNAHR_lHkPRic1g1BYhH6bFNVPagEkuaL8Mf80l_tOirhThQYIbfWYErgu4bDwl-7brVhXTWnJNQ2

In normal base64 is MqsXexqpYRUNAHR/lHkPRic1g1BYhH6bFNVPagEkuaL8Mf80l/tOirhThQYIbfWYErgu4bDwl+7brVhXTWnJNQ==

Then, I have no idea why you do that

mb_convert_encoding($salt, 'UTF-16LE')

Just remove it (though as I don't know php - there might be some reason you are doing that, but I just cannot imagine which, so take care).

Then as other answer states - the last argument to hash_pbkdf2() should be true.

After making this changes your code will work (I used token already converted to normal base64 string):

$token = "MqsXexqpYRUNAHR/lHkPRic1g1BYhH6bFNVPagEkuaL8Mf80l/tOirhThQYIbfWYErgu4bDwl+7brVhXTWnJNQ==";
$id = "bob@company.com";
$ssokey = "7MpszrQpO95p7H";
$idAndKey = $id . $ssokey;
$salt = base64_decode($token);
$hashed = hash_pbkdf2("sha1", $idAndKey, $salt, 1000, 24, true);
$data = base64_encode($hashed);
echo $data;

produces expected answer (in normal base64 - you need to "url encode" it to get exact match).

I've already burned through more time than I should have on this, but while it's not a complete answer, the few major problems that I found were:

  1. The hashing algorithm is SHA1, not SHA256. [as @Evk already noted]
  2. HttpServerUtility.UrlToken(De|En)code() use a url-safe variant of base64 that needs to be replicated.

    function base64url_encode($bin) {
        return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($bin));
    }
    
    function base64url_decode($str) {
        return base64_decode(str_replace(['-', '_'], ['+', '/'], $str));
    }
    
  3. When you decode the token the result is a binary string, and trying to run that through mb_convert_encoding to change the endian-ness [I found that awful blog post too] won't do what you think. You can try the following, but the token has an odd number of bytes which is problematic no matter which way you look at it. [edit: is there just a bare \x0d carriage return at the end?]

    function swapEndian16($in) {
        $out = '';
        foreach(str_split($in, 2) as $chunk) {
            $out .= $chunk[1] . $chunk[0];
        }
        return $out;
    }
    
  4. The last argument to hash_pbkdf2() should be true, otherwise you're getting a hex-encoded hash rather than the raw bytes.

Really what I'd suggest is asking your vendor if they have any insight on accomplishing this. Chances are that someone's already had and solved this problem with their integrations.

Edit: With the new info from @Evk's answer, here are some sassily-named functions for compatibility with C#'s brilliant base64 URL encoding:

function dumb_base64url_encode($bin) {
    return preg_replace_callback(
        '/(=*)$/',
        function($matches){
            return strlen($matches[0]);
        },
        str_replace(
            ['+', '/'],
            ['-', '_'],
            base64_encode($bin)
        ),
        1
    );
}

function dumb_base64url_decode($str) {
    return base64_decode(
        str_replace(
            ['-', '_'],
            ['+', '/'],
            substr($str, 0, -1)
        )
    );
}

So now, with the un-"corrected" token:

$token = "MqsXexqpYRUNAHR_lHkPRic1g1BYhH6bFNVPagEkuaL8Mf80l_tOirhThQYIbfWYErgu4bDwl-7brVhXTWnJNQ2";
$id = "bob@company.com";
$ssokey = "7MpszrQpO95p7H";
$idAndKey = $id . $ssokey;
$salt = dumb_base64url_decode($token);
$hashed = hash_pbkdf2("sha1", $idAndKey, $salt, 1000, 24, true);
$data = dumb_base64url_encode($hashed);
echo $data; // output: aE1k9-djZ66WbUATqdHbWyJzskMI5ABS0

And don't sweat whose answer to mark correct, I think @Evk's got the most important bits sorted.