package edu.harvard.i2b2.crc.loader.util.security;
import java.io.FileNotFoundException;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Arrays;
import java.util.Date;
import java.util.Hashtable;
import java.sql.*;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import sun.misc.BASE64Decoder;
import edu.harvard.i2b2.common.exception.I2B2Exception;
/**
* A class that recursively scans a directory and its sub_dirs,
* The class implements Runnable
* to allow to use this within a Thread in a larger program.
*/
public class HighEncryption {
private static Log log = LogFactory.getLog(HighEncryption.class.getName());
private Hashtable ht = new Hashtable();
private String msFile = null;
private RijndaelAlgorithm cipher; // Cipher for mrnrs
// private Callback m_callBackHandle=null;
private boolean m_wfcui=false;
// medical record number encryption parameters
private String m_sBWH_IdentifyingFirstCharacter = "B";
private String m_sMGH_IdentifyingFirstCharacter = "M";
private String m_sEMPI_IdentifyingFirstCharacter = "Z";
private String m_sENCYPT_IdentifyingFirstCharacter = ")";
private int m_iBWH_standard_length = 11;
private int m_iMGH_standard_length = 11;
private int m_iEMPI_standard_length = 12;
private String m_sTheFillCharacter = "X";
private String m_sEncryptionErrorValue = "";
// medical record number decryption parameters
private int m_iBWH_de_standard_length = 8;
private int m_iMGH_de_standard_length = 7;
private int m_iEMPI_de_standard_length = 9;
private String m_sTheDeFillCharacter = "0";
public HighEncryption(String sFileName) throws Exception {
this(sFileName, null);
}
public HighEncryption(String inFileName, String keys, Connection sConn) throws Exception {
String values = null;
cipher = new RijndaelAlgorithm(keys, 128); //, "AES");
Statement stmt = sConn.createStatement ();
ResultSet rs = stmt.executeQuery ("SELECT description FROM key WHERE NAME='" + inFileName + "'");
while (rs.next ()) {
ht.put(inFileName, cipher.decrypt(rs.getString(1)));
//ConvertBaseNToDecDecrypt(rs.getString(1));
}
rs.close();
}
public HighEncryption(String inFileName, Hashtable keys) throws Exception {
String key = null;
// makearray64(); // create the array regardless of how key will be acquired AHA@20011016
if ((keys!=null)&&(keys.size()>0))
key = keys.get(inFileName).toString();
if ((key==null)||(key.length()==0)){
msFile = inFileName;
byte baBuffer[] = new byte[2056];
try {
FileInputStream oFileIn = new FileInputStream(inFileName);
int iBytes = oFileIn.read(baBuffer,0,2056);
String sString = new String(baBuffer);
//new String(baBuffer,0,0,iBytes);
key = sString;
}
catch (FileNotFoundException fnfe) {
log.fatal("HighEncryption initialization file-not-found error");
throw new Exception("HighEncryption initialization error");
}
catch (Exception e) {
log.fatal("HighEncryption initialization error");
throw new Exception("HighEncryption initialization error");
}
}
try {
key = key.trim();
cipher = new RijndaelAlgorithm(key, 128); //, "AES"); //long version for dates
}
catch (Exception ex) {
//Lib.TError("HighEncryption initialization error");
ex.printStackTrace();
throw new Exception("HighEncryption initialization error");
}
}
public boolean setDatabaseKey(Connection sConn, String masterKey, String sFilename, String newKey, String token, String username) throws Exception
{
if (token == null)
token = "";
String eNewKey = cipher.encrypt(newKey);
if (!newKey.equals(cipher.decrypt(eNewKey)))
throw new Exception ("Error verifying decrpyt is eqal to encrypt");
PreparedStatement setKey = sConn.prepareStatement("INSERT INTO key (name, description, token, userinserted, dateinserted) VALUES "
+ "(?, ?, ?, ?, ?)");
setKey.setString(1, sFilename);
setKey.setString(2, eNewKey);
setKey.setString(3, token);
setKey.setString(4, username);
setKey.setDate(5, new java.sql.Date(new java.util.Date().getTime()));
int count = setKey.executeUpdate();
if (count == 1)
return true;
else
return false;
}
/**
* Turns array of bytes into string
*
* @param buf Array of bytes to convert to hex string
* @return Generated hex string
*/
public static String asHex (byte buf[]) {
StringBuffer strbuf = new StringBuffer(buf.length * 2);
int i;
for (i = 0; i < buf.length; i++) {
if (((int) buf[i] & 0xff) < 0x10)
strbuf.append("0");
strbuf.append(Long.toString((int) buf[i] & 0xff, 16));
}
return strbuf.toString();
}
/**
* mrn_encrypt makes the encrypted medical record number.
*
* All empi numbers are 9 digits long, in the form 100xxxxxx. Therefore, no
* padding needs to be added to the front of the number. The output empi
* number is an octal of length 11, as set by the variable: empi_stardard_length.
*
* All bwh numbers are 8 digits long, but sometimes when they start with
* zero's they are shorter (as though they were true numbers). This routine
* does not care, it always removes leading zero's. The output bwh LMRN is
* an octal of length set by the variable m_iBWH_stardard_length.
*
* All mgh numbers are 7 digits long, but sometimes when they start with
* zero's they are shorter (as though they were true numbers). This routine
* does not care, it always removes leading zero's, however note that the
* encrypt routine will add them back again as part of the standard
* incryption method. The output mgh LMRN is an octal of length set
* by the variable m_iMGH_stardard_length.
*
* @param theInput String is the cleartext bwh LMRN number.
* @param standardLength Boolean if TRUE pads a return octal out with X's to
* make the total length what m_iXXX_stardard_length is set to.
* @param theSite String is the name of the owner of the medical
* record number.
* @returns the encrypted medical record number.
* @exception the program returns a String as defined by the
* variable m_sEncryptionErrorValue.
*/
public String mrn_encrypt(String theInput, boolean standardLength, String theSite) {
if ((theInput==null)||(theInput.length()==0)) return m_sEncryptionErrorValue;
int standard_length;
StringBuffer theOutput = new StringBuffer(16);
if (theSite.equalsIgnoreCase("BWH")) {
theOutput.append(m_sBWH_IdentifyingFirstCharacter);
standard_length = m_iBWH_standard_length - 1;
}
else if (theSite.equalsIgnoreCase("MGH")) {
theOutput.append(m_sMGH_IdentifyingFirstCharacter);
standard_length = m_iMGH_standard_length - 1;
}
else if (theSite.equalsIgnoreCase("EMP") || theSite.equalsIgnoreCase("EMPI")) {
theOutput.append(m_sEMPI_IdentifyingFirstCharacter);
standard_length = m_iEMPI_standard_length - 1;
}
else {
//Lib.TError("A valid site was not passed to mrn_encrypt function");
return m_sEncryptionErrorValue;
}
String theLong=null;
try {
//Strip off extra 0 in front
theLong = ConvertDecToBaseNEncrypt(Integer.toString(Integer.parseInt(theInput)),32);
//theLong = cipher.encrypt(theInput);
}
catch(Exception e) {
//Lib.TError("Parsing error in mrn_encrypt, message was: "+e.getMessage());
return m_sEncryptionErrorValue;
}
theOutput.append(theLong);
return theOutput.toString();
// RAJ:Following lines commented because exception is thrown
// in length comparison
//
/*
if (!standardLength) return (theOutput.append(theLong).toString());
try {
int addXes = standard_length - theLong.length();
if (addXes<0) {
//Lib.TError("A number to encrypt exceeded the maximum allowable length, the number was: '"+theLong+"'.");
return m_sEncryptionErrorValue;
}
else {
theOutput.append(theLong);
for (int i=0; i<addXes; i++) {
theOutput.append(m_sTheFillCharacter);
}
return theOutput.toString();
}
}
catch (Exception e) {
//Lib.TError("Unhandled error in mrn_encrypt: "+e.getMessage());
return m_sEncryptionErrorValue;
}
*/
}
public String bwh_encnum_encrypt(String theInput)
{
if ((theInput == null) || (theInput.length() == 0))
return m_sEncryptionErrorValue;
try
{
// If AES just encrypt the whole input string, do not split of up and attach the
// remaining at the end.
return m_sENCYPT_IdentifyingFirstCharacter +
m_sBWH_IdentifyingFirstCharacter +
ConvertDecToBaseNEncrypt(theInput,32);
}
catch (Exception e)
{
log.fatal("Unhandled error in bwh_encnum_encrypt: "+e.getMessage());
return m_sEncryptionErrorValue;
}
}
public String bwh_encnum_decrypt(String theInput)
{
if ((theInput == null) || (theInput.length() == 0))
return m_sEncryptionErrorValue;
try
{
return "TSI-BWH-" + ConvertBaseNToDecDecrypt(theInput.substring(2),32);
}
catch (Exception e)
{
log.fatal("Unhandled error in bwh_encnum_decrypt: "+e.getMessage());
return m_sEncryptionErrorValue;
}
}
public String mgh_encnum_encrypt(String theInput)
{
if ((theInput == null) || (theInput.length() == 0))
return m_sEncryptionErrorValue;
try
{
// If AES just encrypt the whole input string, do not split of up and attach the
// remaining at the end.
return m_sENCYPT_IdentifyingFirstCharacter +
m_sMGH_IdentifyingFirstCharacter +
ConvertDecToBaseNEncrypt(theInput,32);
}
catch (Exception e)
{
log.fatal("Unhandled error in mgh_encnum_encrypt: "+e.getMessage());
return m_sEncryptionErrorValue;
}
}
public String mgh_encnum_decrypt(String theInput)
{
if ((theInput == null) || (theInput.length() == 0))
return m_sEncryptionErrorValue;
try
{
return "TSI-MGH-" + ConvertBaseNToDecDecrypt(theInput.substring(2),32);
}
catch (Exception e)
{
log.fatal("Unhandled error in mgh_encnum_decrypt: "+e.getMessage());
return m_sEncryptionErrorValue;
}
}
public String generic_encnum_encrypt(String theInput) throws I2B2Exception
{
return generic_encrypt(theInput);
}
public String generic_encrypt(String theInput) throws I2B2Exception
{
if ((theInput == null) || (theInput.length() == 0))
{
log.warn("Empty input to generic encryption: ");
return m_sEncryptionErrorValue;
}
try
{
String encryptedAccession = ConvertDecToBaseNEncrypt(theInput, 32);
return encryptedAccession;
}
catch (Exception e)
{
log.fatal("Unhandled error in generic_encrypt: "+e.getMessage());
throw new I2B2Exception(e.getMessage(),e);
}
//
// return m_sEncryptionErrorValue;
//}
}
public String generic_encnum_derypt(String theInput) throws I2B2Exception
{
return generic_decrypt(theInput);
}
public String generic_decrypt(String theInput) throws I2B2Exception
{
if ((theInput == null) || (theInput.length() == 0))
{
log.warn("Empty input to generic decryption: ");
return m_sEncryptionErrorValue;
}
String accession64 = theInput;
try
{
accession64 = ConvertBaseNToDecDecrypt(accession64,32);
return accession64;
}
catch (Exception e)
{
log.fatal("Unhandled error in generic_decrypt: "+e.getMessage());
throw new I2B2Exception(e.getMessage(),e);
// return m_sEncryptionErrorValue;
}
}
public String mrn_decrypt(String theInput, boolean standardLength) {
if ((theInput==null)||(theInput.length()==0)) return m_sEncryptionErrorValue;
int standard_length;
StringBuffer theOutput = new StringBuffer(16);
if (theInput.startsWith(m_sMGH_IdentifyingFirstCharacter)) {
standard_length = m_iMGH_de_standard_length;
}
else if (theInput.startsWith(m_sBWH_IdentifyingFirstCharacter)) {
standard_length = m_iBWH_de_standard_length;
}
else if (theInput.startsWith(m_sEMPI_IdentifyingFirstCharacter)) {
standard_length = m_iEMPI_de_standard_length;
}
else {
//Lib.TError("A valid site was not passed to mrn_decrypt function");
return m_sEncryptionErrorValue;
}
String theLong=null;
String sTempNumber = theInput.substring(1);
sTempNumber = sTempNumber.replaceAll(m_sTheFillCharacter,""); //Lib.StrFindAndReplace(m_sTheFillCharacter,"",sTempNumber);
try {
theLong = ConvertBaseNToDecDecrypt(sTempNumber, 32);
theLong = leftPad(theLong, standard_length, m_sTheDeFillCharacter);
}
catch(Exception e) {
log.fatal("Parsing error in mrn_decrypt, message was: "+e.getMessage());
return m_sEncryptionErrorValue;
}
return theLong;
}
public static String leftPad (String stringToPad, int size, String padder)
{
if (padder.length() == 0)
{
return stringToPad;
}
StringBuffer strb = new StringBuffer(size);
StringCharacterIterator sci = new StringCharacterIterator(padder);
while (strb.length() < (size - stringToPad.length()))
{
for (char ch = sci.first(); ch != CharacterIterator.DONE ; ch = sci.next())
{
if (strb.length() < size - stringToPad.length())
{
strb.insert( strb.length(),String.valueOf(ch));
}
}
}
return strb.append(stringToPad).toString();
}
public String rightPad(String s, int length, char pad) {
StringBuffer buffer = new StringBuffer(s);
int curLen=s.length();
if (curLen < length)
{
for (int i=0; i<length; i++)
{
buffer.append(pad);
}
}
return buffer.toString();
}
//* B220
// Samples: Y229033779X3XXXXXXXX, Y96289732X10003XXXXX
public String encnum_decrypt(String sEncNumber) {
if ((sEncNumber==null)||(sEncNumber.length()==0)) {
// log.fatal(("No IDX main number to decrypt");
return m_sEncryptionErrorValue;
}
try {
//int theLength = sEncNumber.length();
int theGroupNumberStartsOn = sEncNumber.indexOf('X');
// get the encounter number part, start after the 'Y'
String sIdxEncNumber = sEncNumber.substring(1,theGroupNumberStartsOn);
// decrypt the encounter number
String sDecryptIdxEncNumber = cipher.decrypt(sIdxEncNumber);
// get the group number part, start after the 'X'
String sIdxGroupNumber = sEncNumber.substring(theGroupNumberStartsOn+1, sEncNumber.length());
// take off the trailing 'X's
char sTheFillCharacter = m_sTheFillCharacter.charAt(0);
int iLastX = sIdxGroupNumber.length();
for (int i=0; i<sIdxGroupNumber.length(); i++) {
if (sIdxGroupNumber.charAt(i) == sTheFillCharacter) {
iLastX = i;
break;
}
}
sIdxGroupNumber = sIdxGroupNumber.substring(0,iLastX);
// make the encounter number in the form:
// ... IDX-<group number>-<encounter number>
String sFinalNumber = "IDX-MGH-" + sIdxGroupNumber + "-" + sDecryptIdxEncNumber;
return sFinalNumber;
}
catch (Exception e) {
log.fatal("Error in idxmgh_encnum_decrypt: "+e.getMessage());
return m_sEncryptionErrorValue;
}
}
//* 3231
public String encnum_encrypt(String sExtraNumber, String sMainNumber) {
int standard_length = 20;
StringBuffer theOutput = new StringBuffer(standard_length);
if ((sMainNumber==null)||(sMainNumber.length()==0)) {
// log.fatal("No IDX main number to encrypt");
return m_sEncryptionErrorValue;
}
if ((sExtraNumber==null)||(sExtraNumber.length()==0)) {
log.error("No IDX extra number with main number " + sMainNumber);
return m_sEncryptionErrorValue;
}
String sEncMainNumber = null;
try {
sEncMainNumber = cipher.encrypt(sMainNumber);
}
catch(Exception e) {
log.fatal("Parsing error in idxmgh_encnum_encrypt, message was: "+e.getMessage());
return m_sEncryptionErrorValue;
}
String theLong = sEncMainNumber + "X" + sExtraNumber;
try {
int addXes = standard_length - 1 - theLong.length();
if (addXes<0) {
log.error("A number to encrypt exceeded the maximum allowable length, the number was: '"+theLong+"'.");
return m_sEncryptionErrorValue;
}
else {
theOutput.append("Z");
theOutput.append(theLong);
for (int i=0; i<addXes; i++) {
theOutput.append(m_sTheFillCharacter);
}
return theOutput.toString();
}
}
catch (Exception e) {
log.fatal("Unhandled error in mrn_encrypt: "+e.getMessage());
return m_sEncryptionErrorValue;
}
}
/*
* ConvertDecToBaseNEncrypt makes the encrypted string in base N
*
* The string is first encrypted into AES than converted to base N
*
* @param bVale String is the cleartext string of any length.
* @param byBase Int is the base to use, between 2 and 36
* @returns the ciphertext string in base N
*/
public String ConvertDecToBaseNEncrypt(String bValue, int byBase ) throws Exception
{
String sValue = ConvertDecToBaseNClear(cipher.bencrypt(bValue), byBase);
if (!bValue.equals(ConvertBaseNToDecDecrypt(sValue, byBase)))
throw new Exception("Encryption check failed for Base N: " + bValue);
//String sValue = cipher.bencrypt(bValue).toString();
return sValue;
}
/*
* ConvertDecToBaseNClear makes the string in base N
*
* The reason for calling Clear is because to does not call any encryption
* routines. The input string is converted into multiple 8 byte arrays and
* converted into int64 which is than converted into the selected base.
* The routine will add '!' for padding if the returned string is to small
* so that all the strings are the same length.
*
* @param bVale Byte[] is the byte array of any length.
* @param byBase Int is the base to use, between 2 and 36
* @returns the base N string
*/
private String ConvertDecToBaseNClear(byte[] bValue, int byBase ) throws Exception
{
double bValueLen = bValue.length;
double divValue = bValueLen / 8;
double celing = Math.ceil(divValue);
int rounds = (int) celing;
StringBuilder dValue = new StringBuilder(26);
int count = 0;
for (int i=0; i < rounds; i++)
{
int endLen = 8;
if ((count+8) > bValue.length)
endLen = bValue.length - count;
byte[] b = new byte[8];
System.arraycopy(bValue,count,b,0,endLen);
// this is slow
UInt64 myGuidInt = BitConverter(b);
StringBuilder sValue = new StringBuilder();
sValue.append(ConvertDecToBaseNClear(myGuidInt,32));
// Deal with short strings
if (sValue.length() < 13)
{
int addXes = 13 - sValue.length();
for (int j = 0; j < addXes; j++)
{
sValue.append("!");
}
}
dValue.append(sValue);
count=count+8;
}
return dValue.toString();
}
/*
* ConvertDecToBaseNClear makes the int in base N
*
* The reason for calling Clear is because to does not call any encryption
* routines. This is the actual routine that does the conversation into
* the base N. The reason for using unsigned is because the conversation
* using AES would have negative inte values which would get lost when converting
* into the base representation.
*
* @param dValue uint64 is a valid unsigned integer
* @param byBase Int is the base to use, between 2 and 36
* @returns the base N string
*/
private String ConvertDecToBaseNClear( UInt64 dValue, long byBase ) throws Exception
{
String BaseNums = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
//String sResult = "";
StringBuffer sResult = new StringBuffer();
UInt64 dRemainder;
//sResult = "";
if ( (byBase >= 2) && (byBase <= 36) )
{
while (dValue.compareTo((Object) 0) > 0) // x > 0
{
dRemainder = dValue.divideAndRemainder(byBase);
//sResult = BaseNums.substring(dRemainder.intValue(), dRemainder.intValue()+1) + sResult;
sResult.insert(0, BaseNums.substring(dRemainder.intValue(), dRemainder.intValue()+1));
}
return sResult.toString();
}
else
{
throw new Exception("Base should be between 2 and 36.");
}
}
/*
* ConvertBaseNToDecClear makes the string from base N
*
* The reason for calling Clear is because to does not call any encryption
* routines. This is the actual routine that does the conversation from
* the base N. The reason for using unsigned is because the conversation
* using AES would have negative inte values which would get lost when converting
* into the base representation. The routine will remove any '!' used for padding
*
* @param dValue string is a valid base N string of length of a valid int64 base
* @param byBase Int is the base to use, between 2 and 36
* @returns the string
*/
private UInt64 ConvertBaseNToDecClear( int byBase, String origValue) throws Exception
{
// Deal with short strings
String dValue = origValue;
while (dValue.endsWith("!"))
dValue = dValue.substring(0,dValue.length()-1);
String BaseNums = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
UInt64 lReturn = new UInt64(0);
int n;
if ( (byBase >= 2) && (byBase <= 36) )
{
n = 0;
while (n != dValue.length() )
{
//lReturn = (long) ((BaseNums.indexOf(dValue.substring(((dValue.length() - n) - 1), dValue.length() - 1)))
// * (Math.pow(byBase,n))) + lReturn;
//Half working
//lReturn.add(
// new java.math.BigInteger(Long.toString((BaseNums.indexOf(
// dValue.substring(((dValue.length() - n) - 1), dValue.length() - n)))
// * (long) (Math.pow(byBase,n)))));
UInt64 a = new UInt64(Long.toString((BaseNums.indexOf(
dValue.substring(((dValue.length() - n) - 1), dValue.length() - n)))));
a.multiply((long) (Math.pow(byBase,n)));
lReturn.add(a.bigIntValue());
n++;
}
return lReturn;
}
else
{
throw new Exception("Base should be between 2 and 36.");
}
}
/*
* ConvertBaseNToDecDecrypt makes the string from base N into the original string
*
* The input string is split into multiple chuncks. Each chunck is split into 2
* sections. Both are converted back into the original string and appeneded together
* The new string is decrypted and returned.
*
* @param dValue string is a valid base N string of any length
* @param byBase Int is the base to use, between 2 and 36
* @returns the cleartext string in a string.
*/
public String ConvertBaseNToDecDecrypt(String dValue, int byBase ) throws Exception
{
int rounds = dValue.length() / 13;
StringBuffer sValue = new StringBuffer();
byte[] cipherB = new byte[16];
for (int i=0; i < rounds; i=i+2)
{
UInt64 myInt = ConvertBaseNToDecClear(32,dValue.substring(i*13,(i+1)*13));
byte[] b = BitConverter(myInt);
System.arraycopy(b,0,cipherB,0,8);
myInt = ConvertBaseNToDecClear(32,dValue.substring((i+1)*13,(i+2)*13));
b = BitConverter(myInt);
System.arraycopy(b,0,cipherB,8,8);
//String a = new String( cipher.decrypt(cipherB));
//a = a.substring(0, a.indexOf(0));
byte[] ciph = cipher.decrypt(cipherB);
int len = (int) ciph[15];
if ((len < 0) || (len > 16))
throw new Exception ("Invalid key");
sValue.append(new String( ciph, 0, len )); //.trim();
}
return sValue.toString();
}
// Converts a double into an array of bytes with length
// eight.
private byte[] BitConverter(UInt64 a)
{
UInt64 value = new UInt64(a.bigIntValue());
byte[] w = new byte[8];
w[0] = (byte) value.byteValue();
value.shiftRight(8);
w[1] = (byte) (value.byteValue());
value = new UInt64(a.bigIntValue());
value.shiftRight(16);
w[2] = (byte) (value.byteValue());
value = new UInt64(a.bigIntValue());
value.shiftRight(24);
w[3] = (byte) (value.byteValue());
value = new UInt64(a.bigIntValue());
value.shiftRight(32);
w[4] = (byte) (value.byteValue());
value = new UInt64(a.bigIntValue());
value.shiftRight(40);
w[5] = (byte) (value.byteValue());
value = new UInt64(a.bigIntValue());
value.shiftRight(48);
w[6] = (byte) (value.byteValue());
value = new UInt64(a.bigIntValue());
value.shiftRight(56);
w[7] = (byte) (value.byteValue());
return w;
}
private UInt64 BitConverter(byte[] a)
{
UInt64 accum = new UInt64("0");
long fff = 0;
for ( int shiftBy=0; shiftBy<64; shiftBy+=8 )
{
long b = a[shiftBy/8] & 0xff;
b = b << shiftBy;
fff = fff | b;
}
if (fff < 0)
{
accum = new UInt64(Long.toString(Long.MAX_VALUE));
accum.add(new java.math.BigInteger(Long.toString(fff+2)));
accum.add(new java.math.BigInteger(Long.toString(Long.MAX_VALUE)));
}
else
{
accum = new UInt64(Long.toString(fff));
}
return accum;
}
public static void main(String[] args) {
}
}