Hi Scott,

Would be great if you could post the outcome of the WRC tickey. In the near future we also would like to create a generic backend connection to Epic.

Another part of the backend integration is the check of the JWT. So the JWT should be created at IRIS side with the correct client_id, scoped, audience etc. But on IRIS side you need to create a webpage where the public part of the key is hosted so Epic can check that the JWT is valid:

Hi Pietro,

Not sure if this will help you but I written a retry mechanisme in the BO with 2 additional settings MaxRetries and RetryWaitSeconds. 

See code below:

Class BO.Rest.PostBodyHeaderAuth Extends EnsLib.REST.Operation
{

Parameter ADAPTER = "EnsLib.HTTP.OutboundAdapter";

Parameter INVOCATION = "Queue";

Property HeaderName As %String(MAXLEN = 1000) [ InitialExpression = "Authorization", Required ];

Property HeaderValue As %String(MAXLEN = 10000) [ InitialExpression = "Bearer", Required ];

Property UseSSL As %Integer [ InitialExpression = 1, Required ];

Property MaxRetries As %Integer [ InitialExpression = 3, Required ];

Property RetryWaitSeconds As %Integer [ InitialExpression = 300, Required ];

Property ContentType As %String(MAXLEN = 300) [ InitialExpression = "application/json; charset=utf-8", Required ];

Parameter SETTINGS = "HeaderName:Auth,HeaderValue:Auth,MaxRetries:Retry,RetryWaitSeconds:Retry,UseSSL:Connection,ContentType:ContentType";

Method PostBodyData(pRequest As BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthReq, Output pResponse As BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthResp) As %Status
{
      Set tSc	= $$$OK
      
      Set tCounter = 0
      
      While (tCounter <= ..MaxRetries)
      {
        //Create a new %Net.HttpRequest
        Set tHttpRequest	= ##class(%Net.HttpRequest).%New()
            
        //Get the serversetting from the Adapter
        Set tHttpRequest.Server	= ..Adapter.HTTPServer
        Set tHttpRequest.Port		= ..Adapter.HTTPPort
            
        //Set Content-Type
        Set tHttpRequest.ContentType = ..ContentType
      
        //Set Body from Request Object
        Do tHttpRequest.EntityBody.Write(pRequest.Body)
        
        // Set Https Flag
        If (..UseSSL = 1) {
            Set tHttpRequest.Https	= 1
            Set tHttpRequest.SSLConfiguration	= ..Adapter.SSLConfig
        } Else {
            Set tHttpRequest.Https	= 0
        }
          
        //Set the Header value for Authentication
        If ($LENGTH(pRequest.HeaderValue) > 0) {
            Set tSc = tHttpRequest.SetHeader(..HeaderName, pRequest.HeaderValue)
        } Else {
            Set tSc = tHttpRequest.SetHeader(..HeaderName, ..HeaderValue)  
        }
        
        //Set Custom Timeouts from Adapter
        Set tHttpRequest.WriteTimeout = ..Adapter.WriteTimeout
        Set tHttpRequest.Timeout = ..Adapter.ResponseTimeout
        Set tHttpRequest.OpenTimeout  = ..Adapter.ConnectTimeout
            
        If ($LENGTH(pRequest.Url) > 0) { 
            Set tUrl = pRequest.Url
        } Else {
            Set tUrl = ..Adapter.URL
        }
      
        //Create a trace with the complete url
        $$$TRACE(..Adapter.HTTPServer_"/"_tUrl)
            
        //Do the post call
        Set tSc = tHttpRequest.Post(tUrl)
    
        //Create the tHttpResponse object
        #dim tHttpResponse As %Net.HttpResponse
          
        //Set the response object	
        Set tHttpResponse = tHttpRequest.HttpResponse
                  
        //If an technical error has occured
        If ($$$ISERR(tSc)) 
        { 
            $$$TRACE("Error:"_$System.Status.GetErrorText(tSc))
            
        } ElseIf ('$IsObject(tHttpResponse)) 
        {
            $$$TRACE("Invalid HTTP Response Object")
            Set tSc=$$$ERROR($$$EnsErrGeneral, "Invalid HTTP Response Object.")
        } 
        Else 
        {
          ;Build Operation Response
          Set pResponse = ##class(BO.Rest.PostBodyHeaderAuth.PostBodyHeaderAuthResp).%New()
          Set pResponse.HTTPStatusCode = tHttpResponse.StatusCode
          Set pResponse.HTTPStatusLine = tHttpResponse.StatusLine
          
          ;Check the HTTPStatusCode
          Set tSuccess = ##class(Util.Functions).IsSuccess(tHttpResponse.StatusCode)
          
          If (tSuccess) {
            Set tCounter = ..MaxRetries + 1
              Do tHttpResponse.Data.Rewind()
              Set pResponse.HTTPResponseData = tHttpResponse.Data
          } Else {
            $$$TRACE("An error HTTP code has been received. HTTPSCode="_tHttpResponse.StatusCode)  
            Try {
                Do tHttpResponse.Data.Rewind()
                Set pResponse.HTTPResponseData = tHttpResponse.Data
            } Catch exception {}
          }
        }
        
        Set tCounter = tCounter+1
        If (tCounter <= ..MaxRetries) {
            $$$TRACE("Retry: "_tCounter_". Wait for "_..RetryWaitSeconds_" seconds")
            HANG ..RetryWaitSeconds	
        }	  
      }
     return tSc
}
}

@Guillaume Rongier, I've had some struggles with converting but that was nog due to your functions. 

See a snippet of working code below:

ClassMethod Decrypt(Stream As %Stream.GlobalCharacter, key As %String) As %Stream.GlobalCharacter [ Language = python ]
{
    import iris
    import os
    from cryptography.hazmat.primitives import hashes, padding
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    from cryptography.hazmat.backends import default_backend
    from base64 import b64decode

    def stream_to_string(stream)-> str:
                   string = ""
                   stream.Rewind()
                   while not stream.AtEnd:
                                   string += stream.Read(1024)
                   return string

    def string_to_stream(string:str):
                   stream = iris.cls('%Stream.GlobalCharacter')._New()
                   n = 1024
                   chunks = [string[i:i+n] for i in range(0, len(string), n)]
                   for chunk in chunks:
                                   stream.Write(chunk)
                    return stream

    # Convert the Base64 encoded key to bytes
    key_bytes = b64decode(key)

    cipher_bytes = bytes(stream_to_string(Stream),'iso-8859-1')

    # Extract the IV from the first 16 bytes of the cipher
    iv = cipher_bytes[:16]

    # Create the AES cipher object
    backend = default_backend()
    cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=backend)
    decryptor = cipher.decryptor()

    # Decrypt the data, excluding the IV
    decrypted_bytes = decryptor.update(cipher_bytes[16:]) + decryptor.finalize()

    # Remove padding
    unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
    decrypted_data = unpadder.update(decrypted_bytes) + unpadder.finalize()
    
    return string_to_stream(decrypted_data)
  }

For development VS Code is great. Indent is a lot better and also the syntax checking. One small functionality I'm missing is to open classes fast with a shortcut. We're using HealthConnect and at an item the Class Name is shown in the management portal:

I can copy the class name, go to studio press Ctrl-O and paste the class name:

In VS code I can open also with Ctrl-O but then I need to fill in /HS/FHIRServer/Interop.HTTPOperation.cls

Could be a small improvement?

Hello Marc,

I saw the same, but after adjusting I get the same error when decrypting (Padding is invalid and cannot be removed)

Latest Objectscript code:

Class TEST.ENCRYPT
{

// Symmetric Keys sample to encrypt
ClassMethod DoAESCBCEncrypt() As %Status
{
	Set key="pZR8qfrz7t47G+dboyJCH4NnJRrF+dJbvxq37y/cLUo="
	Write "Key="_key,!
	Set keyBase64=$SYSTEM.Encryption.Base64Encode(key,1)
	Write "KeyBase64="_keyBase64,!
	
	Set iv=$system.Encryption.GenCryptRand(16)
	Write "IV="_iv,!
	Set ivBase64 = $SYSTEM.Encryption.Base64Encode(iv,1)
	Write "IVBase64="_ivBase64,!
	
	Set text="This is just an encryption test with AES256, blocksize 128, padding PKCS7, mode, CBC with an IV of 16 bytes"
	Write "Plain Text: "_text,!
	
	Set encrypted=$SYSTEM.Encryption.AESCBCEncrypt($zcvt(text,"O","UTF8"),keyBase64,iv)
	Set EncryptedBase64=$SYSTEM.Encryption.Base64Encode(encrypted,1)
	Write "EncryptedBase64: "_EncryptedBase64,!
	
	Set encryptedComplete = ivBase64_EncryptedBase64
	Write "EncryptedBase64WithIV: "_encryptedComplete,!
	
	Set ciphertext = $$$URLENCODE(encryptedComplete)

	write "URL Encoded:"_ciphertext,!
	return $$$OK
}

}

Hi Julius,

Thank you again. I agree with you that $system.Encryption.GenCryptRand(length) should be a nicer option to generate the random IV.

About the IV, this a random IV to make every message unique. The sender will generate the IV and the receiver gets the first 24 characters and this is the IV and will be used in the decryption.

//get IV
string ivString = deEscape.Substring(0, 24);
Console.WriteLine("IV String: " + ivString);

The IV is used in the decryption. The part after the IV is the part thats need to be decrypted:

string toDecryptWithoutIV = deEscape.Substring(24);
Console.WriteLine("To Decrypt without IV: " + toDecryptWithoutIV);

I can't find what's the problem in Objectscript. So i've switched to Python and in Python it works right away. See example code below:

import base64
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import urllib.parse

keyBase64="pZR8qfrz7t47G+dboyJCH4NnJRrF+dJbvxq37y/cLUo="
key = base64.b64decode(keyBase64)

toEncrypt = "This is just an encryption test with AES256, blocksize 128, padding PKCS7, mode, CBC with an IV of 16 bytes"
toEncryptBytes = str.encode(toEncrypt)

cipher = AES.new(key, AES.MODE_CBC)
ct_bytes = cipher.encrypt(pad(toEncryptBytes, AES.block_size))
iv = b64encode(cipher.iv).decode('utf-8')
ct = b64encode(ct_bytes).decode('utf-8')
complete=iv+ct
complete = urllib.parse.quote(complete)
print(complete)

In our used version of IRIS,  Python is embedded so I think this will be the first functionality where we're going to use Python.

Hi Julius,

Thank you for your response. I've tried your suggestions. See modified code below:

Class TEST.ENCRYPT
{

// Symmetric Keys sample to encrypt
ClassMethod DoAESCBCEncrypt() As %Status
{
	set key="pZR8qfrz7t47G+dboyJCH4NnJRrF+dJbvxq37y/cLUo="
	set iv=##class(%PopulateUtils).StringMin(16,16)	
	Write "Key="_key,!
	Write "IV="_iv,!
	Set ivBase64 = $SYSTEM.Encryption.Base64Encode(iv,1)
	Write "IVBase64="_ivBase64,!
	
	set text="This is just an encryption test with AES256, blocksize 128, padding PKCS7, mode, CBC with an IV of 16 bytes"
	Write "Plain Text: "_text,!
	
	Set encrypted=$SYSTEM.Encryption.AESCBCEncrypt($zcvt(text,"O","UTF8"),key,iv)
	Set EncryptedBase64=$SYSTEM.Encryption.Base64Encode(encrypted,1)
	Write "EncryptedBase64: "_EncryptedBase64,!
	
	Set encryptedComplete = ivBase64_EncryptedBase64
	Write "EncryptedBase64WithIV: "_encryptedComplete,!
	
	Set ciphertext = $$$URLENCODE(encryptedComplete)

	write "URL Encoded:"_ciphertext,!
	return $$$OK
}

}

Unfortunately when decrypted with the C# code I'm getting the error ''padding is invalid and cannot be removed". Is this about the PaddingMode.PKCS7?

Hi All,

The  problem is in HS.FHIRServer.RestHandler

 Class HS.FHIRServer.HC.FHIRInteropAdapter Extends HS.FHIRServer.RestHandler
{ Parameter isInteropAdapter As %Boolean = 1; Parameter ServiceConfigName As %String = "InteropService"; }

For some reason its not allowed to send the bearer token with the unauthenticated application:

 // Access token present on unsecure CSP request is invalid. Otherwise, if access
// token found on secure CSP request then add to FHIR request AdditionalInfo for
// later evaluation by the FHIR service.
If '%request.Secure {
If ($ZConvert($Piece(%request.GetCgiEnv("HTTP_AUTHORIZATION")," ",1),"U") = "BEARER") || ($Get(%request.Data("access_token",1)) '= "") {
Set %response.Status = ..#HTTP401UNAUTHORIZED
Return $$$OK
}
Set accessToken = ""
Else {
// InterSystems FHIRServer policy is to NOT allow passing access token in the
// request URL or form encoded body (either can be found in %request.Data).
If $Get(%request.Data("access_token",1)) '= "" {
Set %response.Status = ..#HTTP401UNAUTHORIZED
Return $$$OK
}
Set accessToken = ##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.tSC)
$$$ThrowOnError(tSC)
If accessToken '= "" {
Do tRequest.AdditionalInfo.SetAt(accessToken, "USER:OAuthToken")
Do tRequest.AdditionalInfo.SetAt(hsrestconfig.OAuthClientName, "USER:OAuthClient")
}
}

We're gonna discuss the issue with intersystems but it looks like we need custom programming to make this situation work in the new HC version.

Thank you @Alister Pino and @Julius Kavay for your help.

In this scenario see the final solution below:

ClassMethod MergeDynamicObjects(ObjComplete As %Library.DynamicObject, ObjAddendum As %Library.DynamicObject) [ Final ]
{
  Set obj1 = ObjComplete.%Get("OptOuts")
  Set obj2 = ObjAddendum.%Get("OptOuts")     
  Set OptOuts2Iter = obj2.%GetIterator()
  While OptOuts2Iter.%GetNext(.key , .value ) {
    Do obj1.%Push(value)
  }
  return ObjComplete
}