= 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 ; RUNTESTS S IO=$P S DIQUIET=1 D DT^DICRW D:$L($T(EN^XTMUNIT)) EN^XTMUNIT($T(+0),1) QUIT THASH ; @TEST - Test Hashing N DUZ S DUZ=1 N X S X="HELLO WORLD" D HASH N RESULT1 S RESULT1=X S DUZ=1 N X S X="HELLO WORLD" D HASH N RESULT2 S RESULT2=X D CHKEQ^XTMUNIT(RESULT1,RESULT2,"Hash results are not equal") ; S DUZ=1 N X S X="HELLO WORLD" D HASH N RESULT1 S RESULT1=X S DUZ=2 N X S X="HELLO WORLD" D HASH N RESULT2 S RESULT2=X D CHKTF^XTMUNIT(RESULT1'=RESULT2,"Hash results are not supposed to be equal") ; S DUZ=1 N X S X="Hello World" D HASH N RESULT1 S RESULT1=X N X S X="HELLO WORLD" D HASH N RESULT2 S RESULT2=X D CHKEQ^XTMUNIT(RESULT1,RESULT2,"Hash results of different cases aren't equal") QUIT ; THASH2 ; @TEST - Test Hashing using main calling routine XUSHSHP N DUZ S DUZ=1 N X S X="HELLO WORLD" D HASH^XUSHSHP N RESULT1 S RESULT1=X S DUZ=1 N X S X="HELLO WORLD" D HASH^XUSHSHP N RESULT2 S RESULT2=X D CHKEQ^XTMUNIT(RESULT1,RESULT2,"Hash results are not equal") ; S DUZ=1 N X S X="HELLO WORLD" D HASH^XUSHSHP N RESULT1 S RESULT1=X S DUZ=2 N X S X="HELLO WORLD" D HASH^XUSHSHP N RESULT2 S RESULT2=X D CHKTF^XTMUNIT(RESULT1'=RESULT2,"Hash results are not supposed to be equal") ; S DUZ=1 N X S X="Hello World" D HASH^XUSHSHP N RESULT1 S RESULT1=X N X S X="HELLO WORLD" D HASH^XUSHSHP N RESULT2 S RESULT2=X D CHKEQ^XTMUNIT(RESULT1,RESULT2,"Hash results of different cases aren't equal") QUIT ; TENC ; @TEST - Test Encryption N VALUE S VALUE="Mary has a little lamb" S X=VALUE,X1=1,X2=234234234 D EN,DE ; encrypt, decrypt X D CHKEQ^XTMUNIT(VALUE,X,"Encryption and decryption didn't happen properly") ; N VALUE2 S VALUE2="Mary has a little lamb" S X=VALUE2,X1=88,X2=234234234 D EN ; encrypt N ENCSTR1 S ENCSTR1=X S X=VALUE2,X1=1,X2=234234234 ; Different X1 D EN ; encrypt N ENCSTR2 S ENCSTR2=X D CHKTF^XTMUNIT(ENCSTR1'=ENCSTR2,"Encrypted strings with different passwords are not supposed to be equal") QUIT ; TENC2 ; @TEST - Test Encryption by calling XUSHSHP N VALUE S VALUE="Mary has a little lamb" S X=VALUE,X1=1,X2=234234234 D EN^XUSHSHP,DE^XUSHSHP ; encrypt, decrypt X D CHKEQ^XTMUNIT(VALUE,X,"Encryption and decryption didn't happen properly") ; N VALUE2 S VALUE2="Mary has a little lamb" S X=VALUE2,X1=88,X2=234234234 D EN^XUSHSHP ; encrypt N ENCSTR1 S ENCSTR1=X S X=VALUE2,X1=1,X2=234234234 ; Different X1 D EN^XUSHSHP ; encrypt N ENCSTR2 S ENCSTR2=X D CHKTF^XTMUNIT(ENCSTR1'=ENCSTR2,"Encrypted strings with different passwords are not supposed to be equal") QUIT LENGTH ; @TEST - Test Lengths N X,X1,X2,I S (X1,X2)=982734987234 W ! F I=1:1:60 K X S $P(X,"A",I)="A" W $L(X) D EN W ?30,$L(X),! 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 ; ; ; RUNTESTS I $L($T(EN^XTMUNIT)) S IO=$P,DIQUIET=1 D DT^DICRW,EN^XTMUNIT($T(+0),1) QUIT TESTAPI ; @TEST - Test calling X(APINAME) with existent and non-existent entries. N X,X1,X2 D X("UNIT TEST ENTRY") D CHKEQ^XTMUNIT(X,"HELLO WORLD","API wasn't called") ; N STR S STR="HELLO WORLD2" S X=STR,X1="LKJSDF",X2=23432 D X("ENCRYPT STRING") D CHKTF^XTMUNIT(X'="HELLO WORLD2","Encrypt failed") D X("DECRYPT STRING") D CHKEQ^XTMUNIT(X,"HELLO WORLD2","Encrypt/Decrypt failed") ; D X("LKSJDKLFDF") ; Make sure there is no crash QUIT }}} 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 }}}