= Use of Electronic Signatures to Secure VISTA Data = == Statement of Problem == The crux of the issue is the ability to store data on patients and be able to detect if the patient data has been changed intentionally or unintentionally since it was entered. Compounding this issue is the need for different people at different points in time to be able to see the data that was entered. This means that the data creator's token cannot be used as a way to secure the data against future modification. The system has to also support document retraction and reassignment, which override the data creator's authority on the document. An example is in order: how do you know if the text of a TIU note is the same as what the original user entered? == Implementation in VISTA == Various parts of VISTA accomplish this using the encryption and decryption functionality. The encryption algorithm used is a stream cipher (I am not exactly sure which one). For TIU documents, the cipher algorithm uses the document's checksum of document contents as one of the vectors in the algorithm to encrypt the signer's name and title. If the document is modified outside of TIU, the user will see gibberish for these two fields. For example, this is a typical signature: {{{ /es/ DOCTOR MCDUCK, MD ENT PHYSICIAN Signed: 02/27/2013 09:34 }}} If the document is changed outside of TIU, it may look like this (this example isn't real as I do not have access to the algorithm): {{{ /es/ lks&*(*% *(*&@#$*& A#@^*(ssDASDF Signed: 02/27/2013 09:34 }}} Other VISTA packages, notably Radiology, implement the same functionality, using other vectors for encryption. == Use in VISTA outside of the VA == The FOIA process removes sensitive security related algorithms from VISTA. As a result, those using VISTA outside of the Veteran's Administration have had to supplement this. I have surveyed all the external VISTAs and here is what I found on how they were replaced: ||Item||WorldVistA||!OpenVista||vxVista|| ||!Access/Verify Hash||Reversible Hash||None||MD5 Hash ||Electronic Signature Hash||None||Restore VA Routine||MD5 Hash ||!Encryption/Decryption||None||Restore VA Routine||Blowfish symmetric block cipher As you can see from this table, both !OpenVista and WorldVistA have done badly (it's not known whether the !OpenVista version is even legal) while vxVista has done the best. == A solution to hashing and encryption on GT.M/Unix == I would have used vxVista's solution; but it only works on Cache (both the hash and encryption). For GT.M on Unix, I used the OS pipes to do the work using the well-respected openssl library. For hashing, I use SHA512 with the user's creation date and DUZ as salt. For encryption, I use AES/Rijndael using 256-bit key in Cipher Block Chaining mode. Corresponding methods can be written for Cache on Unix. {{{ UJOSHSHP ; VEN/SMH - Encrypt Data a la XUSHSHP ;2013-02-27 9:44 AM ;;1.0;JORDAN SPECIFIC MODIFICATIONS; ; HASH ; HASH Q:'$D(X) Q:'$L(X) ; Input: X from Symbol Table. X can be any length ; Output: X hashed, as 20 characters ; ; Use sha512 with user stuff as salt ; Should use PBKDF2, bcrypt, or scrypt; but not available in openssl CLI. S X=$$UP^XLFSTR(X) ; Uppercase X; per original VA code. N SALTED S SALTED=$P(^VA(200,DUZ,1),U,7)_X_DUZ ; Salt X with User Creation Date and DUZ. N CALL S CALL="openssl dgst -sha512" ; SHA512 OS call N DEVICE S DEVICE="hashPipe" N OUTPUT N OLDIO S OLDIO=$IO OPEN DEVICE:(shell="/bin/sh":comm=CALL)::"PIPE" USE DEVICE WRITE SALTED,/EOF READ OUTPUT:1 U OLDIO CLOSE DEVICE S X=$E(OUTPUT,$L(OUTPUT)-19,$L(OUTPUT)) ; X is the return variable; only 20 characters long QUIT ; EN ; ENCRYPT Q:'$D(X)!('$D(X1))!('$D(X2)) Q:'$L(X) ; Expect X, X1, X2 ; X is the string to encrypt. X1 and X2 are salts/password ; X1 can be the DUZ ; X2 can be the checksum of the text to encrypt ; Output: X, the encrypted string. Length is the same the input. ; Maximum supported length is maximum global node length. ; Encrypting word processing fields is not supported. ; ; AES- Cipher Block Chaining with Base 64 encoding with password w no salt. N CALL S CALL="openssl enc -a -aes-256-cbc -nosalt -pass pass:"_X1_X2 ; N CALL S CALL="openssl enc -a -des-ede3-cbc -nosalt -pass pass:"_X1_X2 ; N CALL S CALL="openssl enc -bf-cbc -nosalt -pass pass:"_X1_X2 ; N CALL S CALL="openssl enc -a -rc4 -nosalt -pass pass:"_X1_X2 N DEVICE S DEVICE="encryptionPipe" N OLDIO S OLDIO=$IO OPEN DEVICE:(shell="/bin/sh":comm=CALL)::"PIPE" USE DEVICE WRITE X,/EOF ; Write the string to encrypt N I,OUTPUT ; I = counter. OUTPUT = encyrpted tems FOR I=1:1 READ OUTPUT(I) Q:$ZEOF ; Read it into Output Array U OLDIO CLOSE DEVICE ; Next two lines: concatentate the array into the root node S OUTPUT="" F I=1:1 Q:'$D(OUTPUT(I)) S OUTPUT=OUTPUT_OUTPUT(I) K OUTPUT(I) ; X is the output from the API S X=OUTPUT QUIT ; DE ; DECRYPT Q:'$D(X)!('$D(X1))!('$D(X2)) Q:'$L(X) ; Expect X, X1, X2 ; X is the string to encrypt. X1 and X2 are salts/password ; X1 can be the DUZ ; X2 can be the checksum of the text to encrypt ; Output: X, the decrypted string. Length is the same the input. ; Maximum supported length is maximum global node length. ; Encrypting word processing fields is not supported. N CALL S CALL="openssl enc -d -a -aes-256-cbc -nosalt -pass pass:"_X1_X2 ; N CALL S CALL="openssl enc -a -d -rc4 -nosalt -pass pass:"_X1_X2 N DEVICE S DEVICE="decryptionPipe" N OLDIO S OLDIO=$IO OPEN DEVICE:(shell="/bin/sh":comm=CALL)::"PIPE" USE DEVICE F W $E(X,1,64),! S X=$E(X,65,1024*1024*1024) Q:X="" ; Write Input base64 style WRITE /EOF N OUTPUT R OUTPUT:1 ; Read Output of decryption back. U OLDIO CLOSE DEVICE S X=OUTPUT ; X is output QUIT }}} In addition, with Lloyd Milligan's permission, I used the extensible framework he has written to use to call different algorithms based on configuration. The Public Entry Point, XUSHSHP, references these as follows: {{{ XUSHSHP ;SF/STAFF - HASHING ROUTINE FOR SIG BLOCK IN FILE 200 ;2013-02-26 2:32 PM ;;8.0;KERNEL;;Jul 10, 1995;Build 2 ; HASH ; PEP: HASH; Fallthrough D X^UJOXCALL("HASH/DIGEST") Q EN ; PEP: Encrypt D X^UJOXCALL("ENCRYPT STRING") Q DE ; PEP: Decrypt D X^UJOXCALL("DECRYPT STRING") Q }}} UJOXCALL is: {{{ UJOXCALL ;VEN/SMH - External APIs call wrapper ;2013-02-26 3:38 PM ;;1.0;JORDAN SPECIFIC MODIFICATIONS Q X(APINAME) ; Private Proc; Call API based on files ; Input: APINAME, by Value. External API Name in file 400000001.1 ; Output: None. Executes API. API determines Input and Output variables ; N IEN S IEN=$O(^UJO(400000001.1,"B",APINAME,"")) Q:'IEN ; IEN of API, DINUMMED in imp specific code file (400000001.3) I $L($G(^UJO(400000001.3,IEN,1))) X ^(1) QUIT ; Try Implementation specific first I $L($G(^UJO(400000001.1,IEN,2))) X ^(2) QUIT ; Otherwise, use default one }}} The extensible framework uses the file UJO SUPPORTED API (400000001.1) to decide what code to execute. Each entry in this file defines an API that can be called externally from the Mumps Database. Entries in UJO IMPLEMENTATION-SPECIFIC API (400000001.3) file are DINUMMED to this file and override the default implementation of the API specified in this file if a DINUMMED entry is present in the other file. The relevant entries in the files are: {{{ Output from what File: UJO SUPPORTED API// (4 entries) Select UJO SUPPORTED API NAME: HASH/DIGEST Another one: ENCRYPT STRING Another one: DECRYPT STRING Another one: Standard Captioned Output? Yes// (Yes) Include COMPUTED fields: (N/Y/R/B): NO// - No record number (IEN), no Computed Fields NAME: HASH/DIGEST APPLICATION GROUP: KERNEL DATE CREATED: FEB 25,2013 RESPONSIBLE PERSON: EHS/SW SUPPORTED VARIABLE: X ALWAYS DEFINED: YES BRIEF DESCRIPTION: String to hash DEFAULT XECUTE: S X=$$UP^XLFSTR(X) REMARKS: Function provides functionality of a password hash so that the original passwor d cannot be decrypted from the hash. See http://crackstation.net/hashing-security.htm NAME: ENCRYPT STRING APPLICATION GROUP: KERNEL DATE CREATED: FEB 25,2013 RESPONSIBLE PERSON: EHS/SW SUPPORTED VARIABLE: X ALWAYS DEFINED: YES BRIEF DESCRIPTION: String to be encrypted SUPPORTED VARIABLE: X1 ALWAYS DEFINED: YES BRIEF DESCRIPTION: encryption salt/password SUPPORTED VARIABLE: X2 ALWAYS DEFINED: YES BRIEF DESCRIPTION: encryption salt/password DEFAULT XECUTE: Q REMARKS: Encrypts data using a password/salt The variables X1 and X2 are values to be decided upon by the programmer calling this utility. NAME: DECRYPT STRING APPLICATION GROUP: KERNEL DATE CREATED: FEB 25,2013 RESPONSIBLE PERSON: EHS/SW SUPPORTED VARIABLE: X ALWAYS DEFINED: YES BRIEF DESCRIPTION: Encrypted string to be decrypted SUPPORTED VARIABLE: X1 ALWAYS DEFINED: YES BRIEF DESCRIPTION: Encryption salt/password SUPPORTED VARIABLE: X2 ALWAYS DEFINED: YES BRIEF DESCRIPTION: Encryption salt/password DEFAULT XECUTE: Q REMARKS: Decrypts encrypted text using X1 and X2. If X1 and X2 are different from what was used to encrypt the string, the encryption will fail. Select OPTION: INQUIRE TO FILE ENTRIES Output from what File: UJO SUPPORTED API// UJO,IM,S UJO IMPLEMENTATION-SPECIFIC API (3 entries) Select UJO IMPLEMENTATION-SPECIFIC API NAME: HASH/DIGEST Another one: ENC RYPT STRING Another one: DEC RYPT STRING Another one: Standard Captioned Output? Yes// (Yes) Include COMPUTED fields: (N/Y/R/B): NO// - No record number (IEN), no Computed Fields NAME: HASH/DIGEST XECUTE: D HASH^UJOSHSHP NAME: ENCRYPT STRING XECUTE: D EN^UJOSHSHP NAME: DECRYPT STRING XECUTE: D DE^UJOSHSHP }}} == How to implement == To implement the changes in hashing and encryption, you need to perform conversions while users are offline from the system. The number of conversions you have to perform depends on the number of packages that use these features. In the world outside of the VA, these most likely will be the TIU and !Radiology/Nuclear Medicine packages, results in two conversions in addition to that of hashing the electronic signature. As hashing is a lossy operation, it can only be done once from the plain text. To hash electronic signatures, you need to perform a loop on the new person file like this: {{{ NP ; New Person File electronic signature conversion from plain text to hashed. W !,"Hashing electronic signatures",! N DUZ ; Needed for the hashing algorithm N I S I=0 F S I=$O(^VA(200,I)) Q:'I D ; for each NP . N OLDES S OLDES=$P($G(^VA(200,I,20)),U,4) ; Get old signature . Q:'$L(OLDES) ; quit if it doesn't exist. . W I_" " . S DUZ=I ; DUZ is a hash salt . N X S X=OLDES D HASH^XUSHSHP ; hash old sig . S $P(^VA(200,I,20),U,4)=X ; store result W ! QUIT }}} PLEASE NOTE THAT IF YOU RUN THIS MORE THAN ONCE, YOU WILL NOT BE ABLE TO RETRIEVE BACK THE USER'S ELECTRONIC SIGNATURES. To convert TIU notes, perform the following: {{{ TIU ; Encrypt Signatures for TIU documents; from plain text to encrypted. W !,"Encrypting Signature Names and Titles in TIU Documents",! K ^TMP($J,"UJOFDA") ; FDA Array N I S I=0 F S I=$O(^TIU(8925,I)) Q:'I D ; for each document . N SB S SB=$G(^(I,15)) ; Get signature block **NAKED** . Q:SB="" . W I_" " . ; We get the following fields: . ; - 1503 SIGNATURE BLOCK NAME (FOX), [15;3] . ; - 1504 SIGNATURE BLOCK TITLE (FOX), [15;4] . ; - 1509 COSIGNATURE BLOCK NAME (FXO), [15;9] . ; - 1510 COSIGNATURE BLOCK TITLE (FXO), [15;10] . N F1503,F1504,F1509,F1510 ; encrypted pieces now in plain text . S F1503=$P(SB,U,3),F1504=$P(SB,U,4),F1509=$P(SB,U,9),F1510=$P(SB,U,10) ; get text values . ; . ; Store in FDA. . S:$L(F1503) ^TMP($J,"UJOFDA",8925,I_",",1503)=F1503 . S:$L(F1504) ^TMP($J,"UJOFDA",8925,I_",",1504)=F1504 . S:$L(F1509) ^TMP($J,"UJOFDA",8925,I_",",1509)=F1509 . S:$L(F1510) ^TMP($J,"UJOFDA",8925,I_",",1510)=F1510 ; D CLEAN^DILF ; clean up variables D WAIT^DICD ; Please wait a while D FILE^DIE("ET",$NA(^TMP($J,"UJOFDA"))) ; External Values, Transaction ; ZEXCEPT: DIERR ; Fileman variable cleaned in CLEAN^DILF call I $D(DIERR) W "An error occured",! S $EC=",U1," W !! QUIT }}} Please note that while the above conversion can be made rerun-safe, this is not taken into consideration here. This code can only be run once without it proving to be harmful. However, if it is ran again, the reverse operation can be performed to correct the action of the second run. I didn't write example code for !Radiology/Nuclear Medicine as I don't have test data in that package. Once the new algorithm is in use, to reuse an example presented above, here is what to expect if the document is tampered with: Original Signature: {{{ /es/ DOCTOR MCDUCK, MD ENT PHYSICIAN Signed: 02/27/2013 09:34 }}} After tampering with the document {{{ /es/ bad decrypt bad decrypt Signed: 02/27/2013 09:34 }}} == Impact on the rest of VISTA == Analysis of the impact will take two phases: That of the Hash and that of the change in encryption. === Impact of the new hashing algorithm === Replacement of the Hash algorithm presents no foreseable problems. The maximum length of the output of the new hash is identical to the maximum length of the old. You only need to ensure that at the time of installing the new hash you convert the old unencrypted electronic signatures into the hashed form so that the user's electronic signatures will continue working. === Impact of the new encryption algorithm === This area is far more problematic for two reasons: the output of the hash is random data (which Fileman cannot store, as it cannot store bytes which Mumps considers to be control characters), and with block ciphers input is padded to achieve block size. Both of these issues mean that to achieve proper encryption, you will need to base64 encode output from a block cipher so the result can be stored in Fileman. In the end, it means that the encrypted data is much longer than the original data (but not quite double the size). This means that we need to expand the Fileman fields where the encrypted data is stored. The KIDS file accompanying this article expands the 4 TIU fields that store encrypted data to 120 characters each from 60 characters. It's anticipated that other fields (especially field 10 in File 74) will need to be expanded depending on the number of packages you are using. The alternative is to change the encryption algorithm to something much weaker (even rot5) to be able to retain the current sizes. The following is an analysis of all the impacted area in VISTA: {{{ r/DVBCHLUT.m: D EN^XUSHSHP HINQ package. Unused. r/EEOEEXMT.m:LONG K EEOPT S EENOD=DECR,EEOL=$L(EENOD),EEOC="" F EEOC=0:50:250 S X=$E(EENOD,EEOC+1,EEOC+50) Q:X="" D EN^XUSHSHP D r/EEOEEXMT.m: D EN^XUSHSHP S ^TMP($J,FEE)=X r/EEOEEXMT.m: I $L(DECR)<50 S X=DECR,X2=FEE+1 D EN^XUSHSHP S ^TMP($J,FEE+1)=X,FEE=FEE+2 Q r/EEOEXMT2.m: K EEOPT S EENOD=DECR,EEOL=$L(EENOD),EEOC="" F EEOC=0:50:250 S X=$E(EENOD,EEOC+1,EEOC+50) Q:X="" D EN^XUSHSHP D r/EEOEXMT2.m: D EN^XUSHSHP S ^TMP($J,FEE)=X r/EEOEXMT2.m: I $L(DECR)<50 S X=DECR,X2=FEE+1 D EN^XUSHSHP S ^TMP($J,FEE+1)=X,FEE=FEE+2 Q EEO COMPLAINT TRACKING. Unused. r/ENTIUTL1.m: . S X=ENSTR,X1=ENDA,X2=1 D EN^XUSHSHP S ENSTRH=X ; hashed string ENGINEERING. Unused. r/MCESPRT.m: D EN^XUSHSHP MEDICINE. Probably unused. Used only by $$ENCODE^MCESPRT which is only called from SIGN^MCESEDT2 on the CLI. r/OOPSESIG.m: W " ... signed." S X=ESNAM,X1=DUZ,X2=IEN D EN^XUSHSHP S ESIG=DUZ_"^"_X r/OOPSGUIS.m: D EN^XUSHSHP r/OOPSVAL1.m: D EN^XUSHSHP Automated Safety Incident Surveillance Tracking System. Unused. r/PRCASIG.m: D EN^XUSHSHP r/PRCUESIG.m:ENCODE(X,X1,X2) D EN^XUSHSHP Q X IFCAP. Not sure if it's used. r/PRPFSIG.m:ENCODE(X,X1,X2) D EN^XUSHSHP Q X Patient Funds. Direct Global Set. No risk. r/PRSAOTX.m: S X=ESNAM,X1=DUZ,X2=DA D EN^XUSHSHP Global Set. No risk. r/PRSASC1.m: S DFN=$P(AP(1,DA),"^",1),ACT=$P(AP(1,DA),"^",2),COM=$P(AP(1,DA),"^",3),X=ESNAM,X1=DUZ,X2=DA D EN^XUSHSHP r/PRSASC1.m: S DFN=$P(AP(2,DA),"^",1),ACT=$P(AP(2,DA),"^",2),COM=$P(AP(2,DA),"^",3),X=ESNAM,X1=DUZ,X2=DA D EN^XUSHSHP r/PRSASC1.m: S DFN=$P(AP(3,DA),"^",1),ACT=$P(AP(3,DA),"^",2),COM=$P(AP(3,DA),"^",3),X=ESNAM,X1=DUZ,X2=DA D EN^XUSHSHP r/PRSASC1.m: S X=ESNAM,X1=DUZ,X2=DFN D EN^XUSHSHP All global sets. No risk. PAID. r/RACTVR4.m:X11 S X1=RASIG("PER"),X=RASIG("NAME"),X2=DA D EN^XUSHSHP S RASIGCDE=X r/RACTVR.m:X34 S X1=RASIG("PER"),X=RASIG("NAME"),X2=DA D EN^XUSHSHP S RASIGCDE=X r/RACTWR2.m:X5 S X1=RASIG("PER"),X=RASIG("NAME"),X2=DA D EN^XUSHSHP S RASIGCDE=X r/RACTWR4.m:X5 S X1=RASIG("PER"),X=RASIG("NAME"),X2=DA D EN^XUSHSHP S RASIGCDE=X r/RAHLO1.m:LOCK1 I $D(RAESIG) S X=RAESIG,X1=$G(RAVERF),X2=RARPT D EN^XUSHSHP S RAESIG=X 74,10 ELECTRONIC SIGNATURE CODE 0;10 FREE TEXT INPUT TRANSFORM: K:$L(X)>50!($L(X)<1) X OUTPUT TRANSFORM: S Y=" " ***THIS NEEDS TO CHANGE LENGTH TO 80 CHARACTERS AND RECORDS CONVERTED!*** Radiology r/RMPOBILU.m: D EN^XUSHSHP r/RMPR4LI.m: D EN^XUSHSHP Q X r/RMPRSEC.m: D EN^XUSHSHP Q X Home Oxygen Billing. Unused. r/SPNLGUCD.m: D EN^XUSHSHP Spinal code dysfunction. Unused. r/TIULC1.m: D EN^XUSHSHP TIU Already accounted for. r/VAQHSH1.m: I ('DCRYPT) D EN^XUSHSHP Q X PATIENT DATA EXHANGE. Unused. r/XUSESIG1.m: S X=ESBLK,X1=+CHKSUM,X2=1 D EN^XUSHSHP API. Not referenced anywhere. r/XUSTZ.m: S X1=IOS,X2=DT F I=1:1:XUF(.2) S X=XUF(I) D EN^XUSHSHP S XUF(I)=X Only used as hashing it seems. Global set. Throw away data. No action needed. No matches in the DD global. }}} == Future Direction == In-MUMPS encryption can be decrypted by anybody who knows Mumps well enough, since the application needs to decrypt data for ALL appropriately authorized users. This is the reason that public/private key encryption will not work smoothly here without compromising security: if you are a different user, how would you decrypt a document encrypted using another user's or a machine's private key? However, it is best to emphasize that the encryption system used in VISTA is not there to encrypt data; rather it's there to assure its integrity. = Appendix 1: How does TIU encrypt the signature blocks = TIU encrypts these fields in file TIU DOCUMENT (!#8925) {{{ 1503 SIGNATURE BLOCK NAME 1504 SIGNATURE BLOCK TITLE 1509 COSIGNATURE BLOCK NAME 1510 COSIGNATURE BLOCK TITLE }}} The encryption and decryption are done seamlessly in Fileman. Because of that, an end user and even an API programmer will not even detect that it exists. The input transform for each field is responsible for the encryption. It looks like this: {{{ S X=$$ENCRYPT^TIULC1(X,1,$$CHKSUM^TIULC("^TIU(8925,"_+DA_",""TEXT"")")) K:$L(X)>120!($L(X)<3) X }}} The output transform for each field is responsible for decryption. It looks like this: {{{ S Y=$$DECRYPT^TIULC1(Y,1,$$CHKSUM^TIULC("^TIU(8925,"_$S(+$G(DA):+DA,+$G(D0):+D0,1:"")_",""TEXT"")")) }}} The algorithm is simple: Encrypt and decrypt based on a vector of the checksum of the text. If the text changes, the originally computed encrypted string won't be decrypted successfully.