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
In fact, you have to code in base64Url and not in Base64.
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 ---
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
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
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 ---
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
In the example I got, the signature is 342 bytes.
With wwEncryption (or VFPencryption71), this signature represents 43 bytes.
An idea ?
It would look more like using the EncryptString()
function
But this one always uses MD5 hashing
Nice catch, Rick. Yes, wwEncryption with HMACSHA256 should work fine.
Thanks!
In fact, is it possible to consider adding a 3rd parameter to this function to define an encoding different from MD5 ?
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
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 ---
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
@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 ---
@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 ---
@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
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
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
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 ---
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
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 toComputeHash()
************************************************************************
* 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 ---
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
@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 ---
Thank you very much for your reply !
I'm going to try ...
Vincent
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 ---
Hi Rick,
But when do you sleep ? 😉
I will watch as soon as updates are available.
Thank you
Vincent
It's updated in today's release of West Wind Client Tools...
+++ Rick ---
I think wwclient.app is missing ...
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 ---
I will wait for the release of 7.23 ... Thank you