Search code examples
javascriptasp.netnode.jsbase64urlencode

Matching ouput for HttpServerUtility.UrlTokenEncode in NodeJS Javascript


I am looking at an example in dotnet which looks like the following: https://dotnetfiddle.net/t0y8yD.

The output for the HttpServerUtility.UrlTokenEncode method is:

Pn55YBwEH2S2BEM5qlNrq-LMNE8BDdHYwbWKFEHiPZo1

When I try to complete the same in NodeJS with encodeURI, encodeURIComponent or any other attempt I get the following:

Pn55YBwEH2S2BEM5qlNrq+LMNE8BDdHYwbWKFEHiPZo=

As you can see from the above the '-' should be a '+' and the last character part is different. The hash is created the same and outputs the same buffer.

  var hmac = crypto.createHmac("sha256", buf);
  hmac.update("9644873");
  var hash = hmac.digest("base64");

How can I get the two to match? One other important note is that this is one use case and I am unsure if there are other chars that do the same.

I am unsure if the dotnet variant is incorrect or the NodeJS version is. However, the comparison will be done on the dotnet side, so I need node to match that.


Solution

  • The difference of the two results is caused by the use of Base64URL encoding in the C# code vs. Base64 encoding in node.js.

    Base64URL and Base64 are almost identical, but Base64 encoding uses the characters +, / and =, which have a special meaning in URLs and thus have to be avoided. In Base64URL encoding + is replaced with -, / with _ and = (the padding character on the end) is either replaced with %20 or simply omitted.

    In your code you're calculating a HMAC-SHA256 hash, so you get a 256 bit result, which can be encoded in 32 bytes. In Base64/Base64URL every character represents 6 bits, therefore you would need 256/6 = 42,66 => 43 Base64 characters. With 43 characters you would have 2 'lonesome' bits on the end, therefore a padding char (=) is added. The question now is why HttpServerUtility.UrlTokenEncode adds a 1 as a replacement for the padding char on the end. I didn't find anything in the documentation. But you you should keep in mind that it's insignificant anyway.

    To to get the same in node.js, you can use the package base64url, or just use simple replace statements on the base64 encoded hash.

    With base64url package:

    const base64url = require('base64url');
    var hmacB64 = "Pn55YBwEH2S2BEM5qlNrq+LMNE8BDdHYwbWKFEHiPZo="
    var hmacB64url = base64url.fromBase64(hmacb64)
    
    console.log(hmacB64url)
    

    The result is:

    Pn55YBwEH2S2BEM5qlNrq-LMNE8BDdHYwbWKFEHiPZo
    

    as you can see, this library just omits the padding char.

    With replace, also replacing the padding = with 1:

    var hmacB64 = "Pn55YBwEH2S2BEM5qlNrq+LMNE8BDdHYwbWKFEHiPZo="
    console.log(hmacb64.replace(/\//g,'_').replace(/\+/g,'-').replace(/\=+$/m,'1'))
    

    The result is:

    Pn55YBwEH2S2BEM5qlNrq-LMNE8BDdHYwbWKFEHiPZo1
    

    I tried the C# code with different data and always got '1' on the end, so to replace = with 1 seems to be ok, though it doesn't seem to be conform to the RFC.

    The other alternative, if this is an option for you, is to change the C# code. Use normal base64 encoding plus string replace to get base64url output instead of using HttpServerUtility.UrlTokenEncode

    A possible solution for that is described here