Web Connection
JWT
Gravatar is a globally recognized avatar based on your email address. JWT
  Vincent H.
  All
  Aug 8, 2021 @ 11:59pm

Hi Rick,

For the purposes of an electronic signature (Docusign), I need to obtain authentication with JWT (JSON Web Token) - Example

This includes a header and a body, encoded in base 64, which I must concatenate and sign with my private key.

The examples all use RSA SHA-256 digital signature algorithm.

I have tried using wwEncryption.ComputeHash but the result does not seem to match the expected signature.

It seems that some are using CryptCreateHash in Advapi32.dll

Do you have a function corresponding to this signature ?

Thanks in advance

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Vincent H.
  Aug 9, 2021 @ 01:30am

In fact, you have to code in base64Url and not in Base64.

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Vincent H.
  Aug 9, 2021 @ 01:54pm

What is base64Url?

Never mind. Looks like it's this:

https://base64.guru/standards/base64url

Weird freaking format. Never heard of this before. You'll have to roll your own for that...

My recommendation would be to look into finding a .NET component that does the entire process of token creation. It's a bear rolling these types of encoding schemes yourself.

Perhaps this:

https://github.com/jwt-dotnet/jwt

but looking at that too looks a pain unless you create a small wrapper assembly for FoxPro rather than direct calls with wwDotnetBridge.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Kevin Ragsdale
  Vincent H.
  Aug 9, 2021 @ 04:48pm

This is what I use in Web Connection for JWT.

The token has a header, payload, and signature. The base64 part is easy (STRCONV(string,13)), then I just use a CHRTRAN for the base64Url part.

I use VFPEncryption.fll for the HMAC function, and in this case the key is just a #DEFINE in the program.

** Build the JWT TOKEN
lcHeader = STRCONV([{"typ": "JWT","alg": "HS256"}],13)
lcPayload = STRCONV([{"id":] + ALLTRIM(TRANSFORM(m.id)) + ;
            [,"name":"] + ALLTRIM(m.name) + [","role":"] + ;
            ALLTRIM(m.role) + [","exp":] + ALLTRIM(TRANSFORM(m.exp)) + [}],13)
		
** base64URL Encoding
** Change + to -, / to _, and = to nothing
lcHeader = CHRTRAN(lcHeader,"+/=","-_")

** base64URL Encoding		
** Change + to -, / to _, and = to nothing
lcPayload = CHRTRAN(lcPayload,"+/=","-_")
		
** Sign the JWT Token to ensure subsequent requests
** have not been tampered with
lcSignature = STRCONV(HMAC(lcHeader + "." + lcPayload,HMAC_KEY,2),13)
		
** base64URL Encoding		
lcSignature = CHRTRAN(lcSignature,"+/=","-_")
		
** Add the TOKEN property to the response
ADDPROPERTY(loData,"token",lcHeader + "." + lcPayload + "." + lcSignature)

Hope this helps! Kevin

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Carl Chambers
  Kevin Ragsdale
  Aug 9, 2021 @ 08:55pm

Hey Kevin,

Thank you so much for posting this.
I've been struggling to verify the signature of a JWT from Shopify and switching to VFPEncryption.fll did the trick.

Thanks,
Carl

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Kevin Ragsdale
  Aug 9, 2021 @ 09:19pm

Nice Kevin.

You should be able to use the wwEncryption.ComputeHash function for the HMAC conversion. Doesn't HMAC need to specify which SHA bitness it's using? I imagine for the JWT token it would be HMACSHA256.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Rick Strahl
  Aug 9, 2021 @ 11:41pm

Thank you very much Rick and Kevin !

I had done the same thing, with the conversion to base64Url then with wwEncryption and .ComputeHash (cHeader + Cpoint + cBody, "HMACSHA256", Private_key)

The key is in PEM format.

But apparently I am not getting the expected signature.

I will come back to the forum as soon as I know more.

Best regards,

Vincent

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Vincent H.
  Aug 10, 2021 @ 01:28am

In the example I got, the signature is 342 bytes.

With wwEncryption (or VFPencryption71), this signature represents 43 bytes.

An idea ?

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Vincent H.
  Aug 10, 2021 @ 02:41am

It would look more like using the EncryptString() function

But this one always uses MD5 hashing

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Kevin Ragsdale
  Rick Strahl
  Aug 10, 2021 @ 03:07am

Nice catch, Rick. Yes, wwEncryption with HMACSHA256 should work fine.

Thanks!

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Rick Strahl
  Aug 10, 2021 @ 07:05am

In fact, is it possible to consider adding a 3rd parameter to this function to define an encoding different from MD5 ?

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Carl Chambers
  Rick Strahl
  Aug 10, 2021 @ 12:42pm

Hi Rick,

That's what I tried first.
After reading Kevin's post, I replaced...

lcHash = loEncrypt.ComputeHash(lcString, "HMACSHA256", SFY_SECRET)

with...

lcHash = HMAC(lcString, SFY_SECRET, 2)

and it worked.
I tried every setting I could think of but could not get a match with wwEncryption.
I'd love to see where I went wrong.

Carl

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Carl Chambers
  Aug 11, 2021 @ 06:14pm

How long is your key? The .NET code I use auto-sizes/fills the key if it's not on a key boundary, so if you need to force a result the key has to be the right size (16 or 32 characters typically for HMACSHA256). If it's anything less the key is auto-filled (by the .NET runtime library code) which may produce inconsistent results.

Also make sure you're using the latest version. There were updates somewhat recently that deal with different key lengths and algorithm variations internally.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Carl Chambers
  Rick Strahl
  Aug 11, 2021 @ 07:35pm

Hi Rick,

The secret key is 38 characters.
I'm using wwEncryption that shipped with WWWC 6.21

I'm using wwEncryption (HMACSHA256) to verify the signature of another string from Shopify (not a JWT) and it works fine using the same secret key. Unlike the JWT, this signature is in BinHex format rather than Base64.

Carl

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Vincent H.
  Aug 11, 2021 @ 09:46pm

@Vincent,

What are you talking about exactly? For the encryption bits the MD5 encoding is just the default pre-encoding mechanism if you don't provide a salt value. If you provide a salt that's used instead.

As to AES it's supported by using SetEncryptionProvider()...

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Carl Chambers
  Aug 11, 2021 @ 10:12pm

@Carl,

Are you sure you're setting the mode correctly for what is expected? You can created either base64 or binHex. The default is base64. Otherwise use SetBinHexMode().

Can't remember if that was in 6.x or not. Also as mentioned there were changes more recently in both the ComputeHash() logic (for HMAC algos specifically) and the Encrypt/Decrypt() functions.

I know you have your solution but it'd be good to know whether it actually works. I tested against online converters so pretty sure that this is correct.

DO wwEncryption

loEnc = CREATEOBJECT("wwEncryption")
loEnc.SetBinHexMode()
? loEnc.ComputeHash("Hello World", "HMACSHA256", "321456789012345678901234567890")

RETURN

Here's the output (both base64 and binHex) and the online form to match:

So I think this should work. Maybe try the latest version.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Rick Strahl
  Aug 12, 2021 @ 06:20am

@Rick

SetEncryptionProvider(): I don't have this function in my version

I am trying to use advapi32.dll and this code, but CryptImportKey failed. (Cles ("Privée") return a PEM file).

FUNCTION HmacSHA256
   LPARAMETERS tcData
   LOCAL lnStatus, lnErr, lhProv, lhHashObject, lnDataSize, lcHashValue, lnHashSize
   lhProv = 0
   lhHashObject = 0
   lnDataSize = LEN (tcData)
   lcHashValue = REPLICATE (Chr0, 16)
   lnHashSize = LEN (lcHashValue)
   pbData = Cles ("privée")
   phKey = Vierge
   TRY
      DECLARE INTEGER GetLastError ;
         IN Win32api AS GetLastError
      DECLARE INTEGER CryptAcquireContextA ;
         IN AdvApi32 AS CryptAcquireContext ;
         INTEGER @lhProvHandle, ;
         STRING  cContainer, ;
         STRING  cProvider, ;
         INTEGER nProvType, ;
         INTEGER nFlags
      * load a crypto provider
      lnStatus = CryptAcquireContext(@lhProv, 0, 0, dnPROV_RSA_FULL, dnCRYPT_VERIFYCONTEXT)
      IF lnStatus = 0
         THROW GetLastError()
      ENDIF
      * Import de la clé privée
      DECLARE INTEGER CryptImportKey ;
         IN AdvApi32 AS CryptImportKey ;
         INTEGER hProviderHandle, ;
         STRING  @pbData, ;
         INTEGER lenData, ;
         INTEGER hPubKey, ;
         INTEGER dwFlags, ;
         STRING @phKey
      lnStatus = CryptImportKey (lhProv, @pbData, LEN (pbData), 0, 0, @phKey)
      IF lnStatus = 0
         THROW GetLastError()
      ENDIF
      #DEFINE CALG_SHA_256 0x0000800c
      DECLARE INTEGER CryptCreateHash ;
         IN AdvApi32 AS CryptCreateHash ;
         INTEGER hProviderHandle, ;
         INTEGER nALG_ID, ;
         INTEGER hKeyhandle, ;
         INTEGER nFlags, ;
         INTEGER @hCryptHashHandle
      * create a hash object that uses SHA256 algorithm
      lnStatus = CryptCreateHash(lhProv, CALG_SHA_256, 0, 0, @lhHashObject)
      #UNDEFINE CALG_SHA_256
      IF lnStatus = 0
         THROW GetLastError()
      ENDIF
      DECLARE INTEGER CryptHashData ;
      IN AdvApi32 AS CryptHashData ;
      INTEGER hHashHandle, ;
      STRING  @cData, ;
      INTEGER nDataLen, ;
      INTEGER nFlags
      * add the input data to the hash object
      lnStatus = CryptHashData(lhHashObject, tcData, lnDataSize, 0)
      IF lnStatus = 0
         THROW GetLastError()
      ENDIF
      DECLARE INTEGER CryptGetHashParam ;
      IN AdvApi32 AS CryptGetHashParam ;
      INTEGER hHashHandle, ;
      INTEGER nParam, ;
      STRING  @cHashValue, ;
      INTEGER @nHashSize, ;
      INTEGER nFlags
    * retrieve the hash value, if caller did not provide enough storage (16 bytes for MD5)
    * this will fail with dnERROR_MORE_DATA and lnHashSize will contain needed storage size
    lnStatus = CryptGetHashParam(lhHashObject, dnHP_HASHVAL, @lcHashValue, @lnHashSize, 0)
    IF lnStatus = 0
      THROW GetLastError()
    ENDIF
    DECLARE INTEGER CryptDestroyHash ;
      IN AdvApi32 AS CryptDestroyHash;
      INTEGER hKeyHandle
    * free the hash object
    lnStatus = CryptDestroyHash(lhHashObject)
    IF lnStatus = 0
      THROW GetLastError()
    ENDIF
    DECLARE INTEGER CryptReleaseContext ;
      IN AdvApi32 AS CryptReleaseContext ;
      INTEGER hProvHandle, ;
      INTEGER nReserved
    * release the crypto provider
    lnStatus = CryptReleaseContext(lhProv, 0)
    IF lnStatus = 0
      THROW GetLastError()
    ENDIF
  CATCH TO lnErr
    * clean up the hash object and release provider
    IF lhHashObject != 0
      CryptDestroyHash(lhHashObject)
    ENDIF
    IF lhProv != 0
      CryptReleaseContext(lhProv, 0)
    ENDIF
    ERROR ("HmacSHA256 Failed")
  ENDTRY
  RETURN lcHashValue
ENDFUNC

Do you think I can do the same with wwEncryption (by purchasing the update) ?

Thanks in advance,

Vincent

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Carl Chambers
  Rick Strahl
  Aug 12, 2021 @ 10:19am

Hi Rick,

Please try this test.

*
*  wwEncryption / vfpEncryption comparison
*
lcString = "Hello World"
lcSecret = "NgeK9BQtPRRreTFqPAUTCIRQnbu3CDNDlQ7WyI"     && 38 chars
lcSecret = "321456789012345678901234567890"             && 30 chars

*- wwEncryption
DO wwEncryption
loEnc = CREATEOBJECT("wwEncryption")
lcBase64ww = loEnc.ComputeHash(lcString, "HMACSHA256", lcSecret)
loEnc.SetBinHexMode()
lcBinHexww = loEnc.ComputeHash(lcString, "HMACSHA256", lcSecret)
RELEASE loEnc

*- vfpEncryption
SET LIBRARY TO vfpencryption.fll ADDITIVE
lcHMAC = HMAC(lcString, lcSecret, 2)
lcBase64vfp = STRCONV(lcHMAC, 13)
lcBinHexvfp = STRCONV(lcHMAC, 15)
RELEASE LIBRARY vfpencryption.fll

CLEAR
? "Base64"
? "wwEncryption: " + lcBase64ww
? "vfpEncryption: " + lcBase64vfp
?
? "BinHex"
? "wwEncryption: " + lcBinHexww
? "vfpEncryption: " + lcBinHexvfp

When I run it using either lcSecret...
The BinHex results match.
The Base64 results are different.

Carl

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Carl Chambers
  Carl Chambers
  Aug 12, 2021 @ 12:42pm

I just found it.
In the version of wwEncryption I have, Base64 is not the default format.
By explicitly calling...

loEnc.SetBinHexMode(.F.)

...the Base64 values from wwEncryption and vfpEncryption match.

Edit...
Also realized that the results were the same for both secret keys. Then I discovered that wwEncryption sets lcSecret so I should have declared it local in this test.

Carl

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Carl Chambers
  Aug 13, 2021 @ 10:03am

base64 is the default. So unless you set SetBinHex() or SetBinHex(.T.) before (which is 'sticky') that call should not be necessary.

BinHex is the more common format for hash values actually and it's just a matter of representation of the same binary value.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Carl Chambers
  Rick Strahl
  Aug 13, 2021 @ 11:59am

base64 is the default. So unless you set SetBinHex() or SetBinHex(.T.) before (which is 'sticky') that call should not be necessary.

The documentation doesn't mention the "sticky" part.

I just now discovered that the "stickiness" outlives the wwEncryption object as releasing the wwEncryption object did not reset it back to the default.
If I run the following code immediately after starting VFP, lcBase64a and lcBinHex look fine. lcBase64b matches lcBinHex because of the last ("sticky") SetBinHexMode() setting even though loEnc was released.

DO wwEncryption
loEnc = CREATEOBJECT("wwEncryption")
lcBase64a = loEnc.ComputeHash("Hello World", "HMACSHA256", "NoneOfYourBusiness")
loEnc.SetBinHexMode(.T.)
lcBinHex = loEnc.ComputeHash("Hello World", "HMACSHA256", "NoneOfYourBusiness")
RELEASE loEnc

loEnc = CREATEOBJECT("wwEncryption")
lcBase64b = loEnc.ComputeHash("Hello World", "HMACSHA256", "NoneOfYourBusiness")
RELEASE loEnc

CLEAR
? "lcBase64a: " + lcBase64a
? "lcBinHex: " + lcBinHex
? "lcBase64b: " + lcBase64b

If I immediately run it again, the values of all results are the same - BinHex.

If I close and restart VFP and run this code again, the default Base64 setting is in force again.

So it would appear that the safest approach (at least for the version I have) is to always call SetBinHex() with the appropriate parameter before calling ComputeHash() if there is a chance that SetBinHex() was previously called.

Carl

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Carl Chambers
  Aug 13, 2021 @ 03:14pm

Yeah it's not the cleanest design. But with a name like SetBinHexMode() I thought it would be obvious that this setting is global - you're assigning a global mode switch, that doesn't change until you set it again.

The global flag is inside of the .NET code and triggers how output is generated. At the time I did this so I didn't have to modify the internal library code that I'm using which shares code with my .NET libraries.

To make this easier though I added a couple of things to the library today:

  • GetBinHexMode() which lets you retrieve the mode
  • Added a llUseBinHex parameter to ComputeHash()
************************************************************************
*  ComputeHash
****************************************
***  Function:
***      Pass:  lcText      - Text to hash, or binary data (type Q)
***             lcAlgorithm - MD5*,SHA1,SHA256, HMACSHA256  etc.
***             llBinHex    - if .T. returns binHex, base64 is default
***    Return:  Hashed value as string
************************************************************************
FUNCTION ComputeHash(lcText, lcAlgorithm, lvHashSalt, llUseBinHex)
LOCAL lcSaltType, lcResult, llOldBinHex

IF EMPTY(lcAlgorithm)
   lcAlgorithm = "MD5"
ENDIF   

lcSaltType = VARTYPE(lvHashSalt)
IF lcSaltType != "Q" AND lcSaltType != "C"
  lvHashSalt = "" 
ENDIF

llOldBinHex = this.GetBinHexMode()

IF PCOUNT() < 4 AND llOldBinhex 
   llUseBinHex = .T.
ENDIF   
this.SetBinHexMode(llUseBinHex)

lcResult = this.oBridge.InvokeStaticMethod(;
                   "Westwind.WebConnection.EncryptionUtils",;
                   "ComputeHash",lcText,lcAlgorithm,lvHashSalt)                   

this.SetBinHexMode(llOldBinHex)

RETURN lcResult
ENDFUNC
* ComputeHash


FUNCTION GetBinHexMode() 
RETURN this.oBridge.GetStaticProperty(;
                   "Westwind.WebConnection.EncryptionUtils",;
                   "UseBinHex")
ENDFUNC
*   GetBinHexMode

This way there's no ambiguity.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Rick Strahl
  Aug 13, 2021 @ 11:43pm

Thanks Rick, it's clear.

And about my last post, Is it possible to get the equivalent of the sample code using advapi32.dll ?

Vincent

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Vincent H.
  Aug 14, 2021 @ 10:15am

@Vincent,

I don't know.

The encryption in wwEncryption uses:

  • Triple DES or AES
  • ECB Ciphermode
  • Optional MD5 hashing of the key (to 16 bytes)

It's not meant to be totally generic but meant to be used for two way encryption/decryption on both ends. There are too many options to expose for a simple interface like this.

For anything more specific you can create .NET Wrapper code that does exactly what you need and call it with wwDotnetBridge (which is essentially what wwEncryption does.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Rick Strahl
  Aug 14, 2021 @ 10:19am

Thank you very much for your reply !

I'm going to try ...

Vincent

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Vincent H.
  Aug 14, 2021 @ 05:00pm

I've spent a bit of time today updating the functionality in wwEncryption to provide a more complete use case for EncryptString() and DecryptString().

You can now specify the cipher more and IV for AES and specify on the method whether to use base64 or binHex etc.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Rick Strahl
  Aug 14, 2021 @ 11:23pm

Hi Rick,
But when do you sleep ? 😉
I will watch as soon as updates are available.
Thank you

Vincent

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Vincent H.
  Aug 15, 2021 @ 02:27pm

It's updated in today's release of West Wind Client Tools...

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Rick Strahl
  Aug 15, 2021 @ 11:35pm

I think wwclient.app is missing ...

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Rick Strahl
  Vincent H.
  Aug 16, 2021 @ 11:40am

Weird. Not sure how I lost that file during packaging.

Uploaded an update that has everything in it. You can run the wwEncrytpionSample.prg in the Samples folder.

+++ Rick ---

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Vincent H.
  Rick Strahl
  Aug 17, 2021 @ 08:46am

I will wait for the release of 7.23 ... Thank you

Gravatar is a globally recognized avatar based on your email address. re: JWT
  Joel Leach
  Kevin Ragsdale
  Jan 28, 2022 @ 03:47pm

Just saying thanks! This thread was a big help to me today.

Joel

© 1996-2024