Omaha #3226 encryption support for total lightning

Change-Id: I87af2d986830681d258c065304e3ebfb2d2af8db

Former-commit-id: fe199cb32b3fe33ac48f039888bd43d4fedd9820
This commit is contained in:
Brian Clements 2014-06-09 10:40:34 -05:00
parent 5348dfaa9f
commit d4c55244a1
5 changed files with 524 additions and 233 deletions

View file

@ -26,7 +26,6 @@ import gov.noaa.nws.ost.edex.plugin.binlightning.EncryptedBinLightningCipher;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@ -86,6 +85,7 @@ import com.raytheon.uf.edex.decodertools.core.IBinDataSource;
* removed TimeTools usage, removed constructDataURI() call
* added decodeBinLightningData() and decodeBitShiftedBinLightningData() from BinLightningDecoderUtil
* Jun 05, 2014 3226 bclement LightningStikePoint refactor, added extractPData()
* Jun 09, 2014 3226 bclement moved data array decrypt prep to EncryptedBinLightingCipher
*
* </pre>
*
@ -102,6 +102,8 @@ public class BinLightningDecoder extends AbstractDecoder {
private static final IUFStatusHandler logger = UFStatus
.getHandler(BinLightningDecoder.class);
public static final String BINLIGHTNING_KEYSTORE_PREFIX = "binlightning";
/**
* Default lightning strike type for FLASH messages. RT_FLASH documents
* indicate no default, but D2D code defaults to STRIKE_CG also.
@ -343,40 +345,11 @@ public class BinLightningDecoder extends AbstractDecoder {
if (needDecrypt) {
try {
/*
* NOTE: 11/14/2013 WZ:
* encrypted test data on TNCF (got from Melissa Porricelli)
* seems to have extra 4 bytes (0x0d 0x0d 0x0a 0x03) at the end,
* making the data size not a multiple of 16. However, original
* test data do not have this trailing bytes. while NCEP test
* data has extra 8 trailing bytes.
* Brain Rapp's email on 11/13/2013 confirms that Unidata LDM
* software used by AWIPS II will strips off all SBN protocol
* headers
* that precede the WMO header and adds its own 11 byte header
* like this: "soh cr cr nl 2 5 4 sp cr cr nl". It
* also adds a four byte trailer consisting of "cr cr nl etx"
* (0x0d 0x0d 0x0a 0x03)
* So, it seems necessary to trim trailing bytes if it is not
* multiple of 16, warning messages will be logged though
*/
int dataLengthToBeDecrypted = pdata.length;
if (pdata.length % 16 != 0) {
dataLengthToBeDecrypted = pdata.length
- (pdata.length % 16);
logger.warn(traceId + " - Data length from file " + traceId
+ " is " + pdata.length + " bytes, trailing "
+ (pdata.length - dataLengthToBeDecrypted)
+ " bytes has been trimmed to "
+ dataLengthToBeDecrypted
+ " bytes for decryption.");
}
byte[] encryptedData = new byte[dataLengthToBeDecrypted];
encryptedData = Arrays.copyOfRange(pdata, 0,
dataLengthToBeDecrypted);
byte[] encryptedData = EncryptedBinLightningCipher
.prepDataForDecryption(pdata, traceId);
byte[] decryptedData = cipher.decryptData(encryptedData,
dataDate);
dataDate, BINLIGHTNING_KEYSTORE_PREFIX);
// decrypt ok, then decode, first check if keep-alive record
if (BinLightningDecoderUtil.isKeepAliveRecord(decryptedData)) {
logger.info(traceId

View file

@ -32,7 +32,8 @@ import com.raytheon.uf.common.numeric.UnsignedNumbers;
*
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* Jun 3, 2014 3226 bclement Initial creation
* Jun 03, 2014 3226 bclement Initial creation
* Jun 09, 2014 3226 bclement Added ByteBuffer constructor
*
* </pre>
*
@ -62,6 +63,13 @@ public class ChecksumByteBuffer {
this.buff = ByteBuffer.wrap(data);
}
/**
* @param buff
*/
public ChecksumByteBuffer(ByteBuffer buff) {
this.buff = buff;
}
/**
* get the sum of the next numberOfBytes worth of data
*

View file

@ -19,6 +19,9 @@
**/
package com.raytheon.edex.plugin.binlightning.total;
import gov.noaa.nws.ost.edex.plugin.binlightning.EncryptedBinLightningCipher;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
@ -40,6 +43,7 @@ import com.raytheon.uf.common.status.IUFStatusHandler;
import com.raytheon.uf.common.status.UFStatus;
import com.raytheon.uf.common.time.util.TimeUtil;
import com.raytheon.uf.common.wmo.WMOHeader;
import com.raytheon.uf.common.wmo.WMOTimeParser;
/**
* Decoder for Earth Networks Total Lightning data
@ -51,6 +55,7 @@ import com.raytheon.uf.common.wmo.WMOHeader;
* Date Ticket# Engineer Description
* ------------ ---------- ----------- --------------------------
* May 30, 2014 3226 bclement Initial creation
* Jun 09, 2014 3226 bclement added encryption support
*
* </pre>
*
@ -81,9 +86,19 @@ public class TotalLightningDecoder {
// constant metadata
public static final String DATA_SOURCE = "ENTLN";
/* in bytes, header is total size of flash and pulses */
private static final int COMBINATION_PACKET_HEADER_SIZE = 2;
/* in bytes, doesn't include checksum */
private static final int FLASH_PACKET_SIZE = 25;
private static final IUFStatusHandler log = UFStatus
.getHandler(TotalLightningDecoder.class);
private static final EncryptedBinLightningCipher CIPHER = new EncryptedBinLightningCipher();
public static final String TOTAL_LIGHTNING_KEYSTORE_PREFIX = "total.lightning";
/**
* Parse total lightning data into BinLightningRecords
*
@ -99,7 +114,7 @@ public class TotalLightningDecoder {
byte[] pdata = BinLightningDecoder.extractPData(wmoHdr, data);
if (pdata != null) {
try {
rval = decodeInternal(fileName, pdata);
rval = decodeInternal(wmoHdr, fileName, pdata);
} catch (Exception e) {
error(e, headers, wmoHdr);
rval = new PluginDataObject[0];
@ -115,6 +130,27 @@ public class TotalLightningDecoder {
return rval;
}
/**
* @param data
* @param startIndex
* starting index of flash packet (not combination packet)
* @return true if there if a valid flash packet in data starting at index
*/
private static boolean validFlashPacket(byte[] data, int startIndex) {
/* plus one to include packet checksum */
final int packetWithChecksum = FLASH_PACKET_SIZE + 1;
if (data.length < startIndex + packetWithChecksum) {
return false;
}
ChecksumByteBuffer buff = new ChecksumByteBuffer(ByteBuffer.wrap(data,
startIndex, packetWithChecksum));
for (int i = 0; i < FLASH_PACKET_SIZE; ++i) {
/* build up sum in buffer */
buff.get();
}
return passesCheckSum(buff, false);
}
/**
* Display warning message with file and header names
*
@ -141,21 +177,51 @@ public class TotalLightningDecoder {
/**
* @param wmoHdr
* @param fileName
* @param pdata
* data after WMO header is removed
* @return
* @throws DecoderException
*/
private PluginDataObject[] decodeInternal(String fileName, byte[] pdata)
private PluginDataObject[] decodeInternal(WMOHeader wmoHdr,
String fileName, byte[] pdata)
throws DecoderException {
List<LightningStrikePoint> decodeStrikes = decodeStrikes(fileName,
pdata);
BinLightningRecord record = new BinLightningRecord(decodeStrikes);
if (!validFlashPacket(pdata, COMBINATION_PACKET_HEADER_SIZE)) {
/* assume data is encrypted if we can't understand it */
pdata = decrypt(wmoHdr, fileName, pdata);
}
List<LightningStrikePoint> strikes = decodeStrikes(fileName, pdata);
if (!strikes.isEmpty()) {
BinLightningRecord record = new BinLightningRecord(strikes);
return new PluginDataObject[] { record };
} else {
return new PluginDataObject[0];
}
}
/**
* @param wmoHdr
* @param fileName
* @param pdata
* @return
* @throws DecoderException
*/
private byte[] decrypt(WMOHeader wmoHdr, String fileName, byte[] pdata)
throws DecoderException {
Calendar baseTime = WMOTimeParser.findDataTime(wmoHdr.getYYGGgg(),
fileName);
pdata = EncryptedBinLightningCipher.prepDataForDecryption(pdata,
fileName);
try {
return CIPHER.decryptData(pdata, baseTime.getTime(),
TOTAL_LIGHTNING_KEYSTORE_PREFIX);
} catch (Exception e) {
throw new DecoderException("Problem decrypting total lightning", e);
}
}
/**
* Extract strike data from raw binary
*
@ -169,10 +235,17 @@ public class TotalLightningDecoder {
List<LightningStrikePoint> rval = new ArrayList<LightningStrikePoint>();
ChecksumByteBuffer buff = new ChecksumByteBuffer(pdata);
while (buff.position() < buff.size()) {
int startingPostion = buff.position();
int totalBytes = UnsignedNumbers.ushortToInt(buff.getShort());
if (totalBytes > (buff.size() - buff.position())) {
if (totalBytes > (buff.size() - startingPostion)) {
if (validFlashPacket(pdata, buff.position())) {
log.error("Truncated total lightning packet in file: "
+ fileName);
} else {
int extra = buff.size() - startingPostion;
log.warn("Extra data at end of lightning packets: " + extra
+ " bytes");
}
break;
}
/* start flash packet */
@ -189,7 +262,7 @@ public class TotalLightningDecoder {
int pulseCount = UnsignedNumbers.ubyteToShort(buff.get());
strike.setPulseCount(pulseCount);
checkSum(buff, false);
ensureCheckSum(buff, false);
List<LightningPulsePoint> pulses = new ArrayList<LightningPulsePoint>(
pulseCount);
@ -202,13 +275,15 @@ public class TotalLightningDecoder {
decodeCommonFields(pulse, buff);
/* discard pulse count (already set in strike) */
buff.get();
checkSum(buff, false);
ensureCheckSum(buff, false);
pulses.add(pulse);
}
strike.setPulses(pulses);
checkSum(buff, true);
ensureCheckSum(buff, true);
if (strike.getType() != LtgStrikeType.KEEP_ALIVE) {
rval.add(strike);
}
}
return rval;
}
@ -244,6 +319,19 @@ public class TotalLightningDecoder {
return TimeUtil.newGmtCalendar(new Date(totalMillis));
}
/**
* @see #passesCheckSum(ChecksumByteBuffer, boolean)
* @param buff
* @param total
* @throws DecoderException
*/
private static void ensureCheckSum(ChecksumByteBuffer buff, boolean total)
throws DecoderException {
if (!passesCheckSum(buff, total)) {
throw new DecoderException("Checksum failed");
}
}
/**
* Ensure data integrity, resets appropriate sum(s) in buffer after check
*
@ -251,11 +339,9 @@ public class TotalLightningDecoder {
* @param total
* true if total sum should be checked, otherwise checks packet
* sum
* @throws DecoderException
* if check fails
* @return true if checksum passes
*/
private static void checkSum(ChecksumByteBuffer buff, boolean total)
throws DecoderException {
private static boolean passesCheckSum(ChecksumByteBuffer buff, boolean total) {
long rawsum = total ? buff.getTotalSum() : buff.getPacketSum();
/* convert to overflowed unsigned byte */
rawsum &= 0xFF;
@ -263,8 +349,10 @@ public class TotalLightningDecoder {
long mungedSum = (256 - rawsum) & 0xFF;
/* get expected after sum so it is not reflected in sum */
long expected = UnsignedNumbers.ubyteToShort(buff.get());
if (mungedSum != expected) {
throw new DecoderException("Checksum failed: expected " + expected
boolean rval = mungedSum == expected;
if (!rval) {
log.debug("Checksum failed: expected " + expected
+ " got " + mungedSum);
}
if (total) {
@ -272,6 +360,7 @@ public class TotalLightningDecoder {
} else {
buff.resetPacketSum();
}
return rval;
}
/**

View file

@ -21,9 +21,11 @@ import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -44,6 +46,7 @@ import com.raytheon.uf.common.status.UFStatus;
* ------------ ---------- ----------- --------------------------
* 20130503 DCS 112 Wufeng Zhou To handle both the new encrypted data and legacy bit-shifted data
* Jun 03, 2014 3226 bclement moved from com.raytheon.edex.plugin.binlightning to gov.noaa.nws.ost.edex.plugin.binlightning
* Jun 09, 2014 3226 bclement refactored to support multiple stores for different data types
*
* </pre>
*
@ -57,45 +60,101 @@ public class BinLightningAESKey {
/** System property name that can used to specify configuration property file, which will overwrite the default keystore location */
public static final String SYS_PROP_FOR_CONF_FILE = "binlightning.aeskeypropfile";
public static final String KEYSTORE_PROP = "binlightning.AESKeystore";
public static final String KEYSTORE_PASS_PROP = "binlightning.AESKeystorePassword";
public static final String KEYSTORE_PROP_SUFFIX = ".AESKeystore";
public static final String KEYSTORE_PASS_PROP_SUFFIX = ".AESKeystorePassword";
public static final String CIPHER_ALGORITHM_SUFFIX = ".cipherAlgorithm";
public static final String DEFAULT_CIPHER_ALGORITHM = "AES";
private static final String CONF_PROPERTIES_FILE = "BinLightningAESKey.properties";
public static final String KEY_ALIAS_PREFIX = "^\\d{4}-\\d{2}-\\d{2}";
private static final Pattern KEY_ALIAS_PREFIX_PATTERN = Pattern.compile(KEY_ALIAS_PREFIX);
private static final SimpleDateFormat KEY_ALIAS_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
private static IUFStatusHandler logger = UFStatus
private static final IUFStatusHandler logger = UFStatus
.getHandler(BinLightningAESKey.class);
private static Properties props = new Properties();
private static final Properties props = new Properties();
private static volatile boolean propsLoaded = false;
private static KeyStore keystore;
private static BinLightningAESKey[] keys = null;
private static final Map<String, BinLightningAESKey[]> keyCache = new ConcurrentHashMap<String, BinLightningAESKey[]>(
2);
/**
* Helper method to selectively get all the bin lightning related AES encryption keys, ordered by key issue date in descending order.
* Keys will be ignored when its alias is not starting with yyyy-MM-dd prefix or key algorithm is not "AES"
* Helper method to selectively get all the bin lightning related AES
* encryption keys for a lightning type, ordered by key issue date in
* descending order. Keys will be ignored when its alias is not starting
* with yyyy-MM-dd prefix or key algorithm is not "AES"
*
* If properties file is specified through system property binlightning.aeskeypropfile, then use it to load the properties.
* Otherwise, load the default property that is at the same place as this class, and overwrite properties
* if the property is specified through system property.
* So, binlightning.aeskeypropfile has higher priority, if it is specified, other properties specified in system property will be ignored
* If properties file is specified through system property
* binlightning.aeskeypropfile, then use it to load the properties.
* Otherwise, load the default property that is at the same place as this
* class, and overwrite properties if the property is specified through
* system property. So, binlightning.aeskeypropfile has higher priority, if
* it is specified, other properties specified in system property will be
* ignored
*
* Assumption: Valid key imported/stored to the keystore will have yyyy-MM-dd prefix in its alias.
* Assumption: Valid key imported/stored to the keystore will have
* yyyy-MM-dd prefix in its alias.
*
* @return valid bin lightning AES keys (with aliases) in descending order of key issue date
* or null when no valid keys found
* @param propertyPrefix
* prefix for properties associated with a particular lightning
* data type
* @return valid bin lightning AES keys (with aliases) in descending order
* of key issue date or null when no valid keys found
*/
public static BinLightningAESKey[] getBinLightningAESKeys() {
if (keys != null) return keys;
public static BinLightningAESKey[] getBinLightningAESKeys(
String propertyPrefix) {
BinLightningAESKey[] rval = keyCache.get(propertyPrefix);
if (rval == null) {
if (!propsLoaded) {
synchronized (props) {
if (!propsLoaded) {
loadProperties();
}
}
}
synchronized (keyCache) {
rval = keyCache.get(propertyPrefix);
if (rval == null) {
rval = createAESKeys(propertyPrefix);
if (rval != null) {
keyCache.put(propertyPrefix, rval);
}
}
}
}
return rval;
}
// if properties file is specified through system property binlightning.aeskeypropfile, then use it to load the properties
// otherwise, use default property file and overwrite with available system properties
/**
* load properties files. If properties file is specified through system
* property binlightning.aeskeypropfile, then use it to load the properties.
* Otherwise, load the default property that is at the same place as this
* class, and overwrite properties if the property is specified through
* system property. So, binlightning.aeskeypropfile has higher priority, if
* it is specified, other properties specified in system property will be
* ignored
*/
private static void loadProperties() {
/*
* if properties file is specified through system property
* binlightning.aeskeypropfile, then use it to load the properties
* otherwise, use default property file and overwrite with available
* system properties
*/
try {
if (System.getProperty(SYS_PROP_FOR_CONF_FILE, "").equals("") == false) {
File file = new File(System.getProperty(SYS_PROP_FOR_CONF_FILE));
String confFileName = System.getProperty(SYS_PROP_FOR_CONF_FILE);
if (confFileName != null && !confFileName.isEmpty()) {
File file = new File(confFileName);
if (file.exists() == false) {
logger.error("System specified property file " + file.getAbsolutePath() + " does not exist.");
logger.error("System specified property file "
+ file.getAbsolutePath() + " does not exist.");
} else {
FileInputStream fis = new FileInputStream(file);
props.load(fis);
@ -106,7 +165,8 @@ public class BinLightningAESKey {
Properties defProps = new Properties();
File file = new File(DEFAULT_KEYSTORE_LOC, CONF_PROPERTIES_FILE);
if (file.exists() == false) {
logger.error("Default properties file " + file.getAbsolutePath() + " does not exist.");
logger.error("Default properties file "
+ file.getAbsolutePath() + " does not exist.");
} else {
FileInputStream fis = new FileInputStream(file);
defProps.load(fis);
@ -114,34 +174,63 @@ public class BinLightningAESKey {
}
props.putAll(defProps);
// now check if the properties should be overwritten, if it is specified in system properties
/*
* now check if the properties should be overwritten, if it is
* specified in system properties
*/
Iterator<?> iter = defProps.keySet().iterator();
while (iter.hasNext()) {
String key = (String)iter.next();
String key = (String) iter.next();
if (System.getProperty(key, "").equals("") == false) {
props.setProperty(key, System.getProperty(key));
}
}
}
propsLoaded = true;
} catch (IOException ioe) {
logger.error("Fail to load BinLightningAESCipher configuration from file or system properties.", ioe);
logger.error(
"Fail to load BinLightningAESCipher configuration from file or system properties.",
ioe);
propsLoaded = false;
}
}
/**
* Read all keys in keystore associated with datatype, ordered by key issue
* date in descending order. Keys will be ignored when its alias is not
* starting with yyyy-MM-dd prefix or key algorithm is not "AES"
*
* @param propertyPrefix
* prefix for properties associated with a particular lightning
* data type
* @return all keys sorted by
*/
private static BinLightningAESKey[] createAESKeys(String propertyPrefix) {
// load keystore
String keystoreProp = propertyPrefix + KEYSTORE_PROP_SUFFIX;
String keystorePassProp = propertyPrefix + KEYSTORE_PASS_PROP_SUFFIX;
BinLightningAESKey[] rval = null;
try {
if (props.getProperty(KEYSTORE_PROP, "").equals("") == false) {
File ksFile = new File(props.getProperty(KEYSTORE_PROP));
keystore = KeyStore.getInstance("JCEKS"); // type JCEKS can store AES symmetric secret key, while default JKS store can't
String ksFileName = props.getProperty(keystoreProp);
if (ksFileName != null && !ksFileName.isEmpty()) {
File ksFile = new File(ksFileName);
/*
* type JCEKS can store AES symmetric secret key, while default
* JKS store can't
*/
keystore = KeyStore.getInstance("JCEKS");
FileInputStream fis = null;
char[] keystorePassword = null;
try {
fis = new FileInputStream(ksFile);
char[] keystorePassword = null;
if (props.getProperty(KEYSTORE_PASS_PROP) != null) {
keystorePassword = props.getProperty(KEYSTORE_PASS_PROP).toCharArray();
if (props.getProperty(keystorePassProp) != null) {
keystorePassword = props.getProperty(keystorePassProp)
.toCharArray();
}
keystore.load(fis, keystorePassword);
} finally {
if (fis != null) fis.close();
if (fis != null)
fis.close();
}
Enumeration<String> enu = keystore.aliases();
@ -149,8 +238,9 @@ public class BinLightningAESKey {
while (enu.hasMoreElements()) {
String alias = enu.nextElement();
Matcher matcher = KEY_ALIAS_PREFIX_PATTERN.matcher(alias);
if (matcher.lookingAt()) { // alias starts with yyyy-MM-dd pattern
Key key = keystore.getKey(alias, props.getProperty(KEYSTORE_PASS_PROP).toCharArray());
/* alias starts with yyyy-MM-dd pattern */
if (matcher.lookingAt()) {
Key key = keystore.getKey(alias, keystorePassword);
if (key.getAlgorithm().equals("AES")) {
// valid AES key for bin lightning decryption
treeMap.put(alias, key);
@ -158,43 +248,70 @@ public class BinLightningAESKey {
}
}
List<BinLightningAESKey> keyListSortedByAliasDesc = new ArrayList<BinLightningAESKey>();
for (Entry<String, Key> entry = treeMap.pollLastEntry(); entry != null; entry = treeMap.pollLastEntry()) {
Date keyDate = KEY_ALIAS_DATE_FORMAT.parse(entry.getKey().substring(0, 10));
BinLightningAESKey blkey = new BinLightningAESKey(entry.getKey(), entry.getValue(), keyDate);
for (Entry<String, Key> entry = treeMap.pollLastEntry(); entry != null; entry = treeMap
.pollLastEntry()) {
Date keyDate = KEY_ALIAS_DATE_FORMAT.parse(entry.getKey()
.substring(0, 10));
BinLightningAESKey blkey = new BinLightningAESKey(
entry.getKey(), entry.getValue(), keyDate);
keyListSortedByAliasDesc.add(blkey);
}
keys = keyListSortedByAliasDesc.toArray(new BinLightningAESKey[] {});
return keys;
rval = keyListSortedByAliasDesc
.toArray(new BinLightningAESKey[] {});
} else {
logger.error("binlightning.AESKeystore property not set.");
}
} catch (KeyStoreException kse) {
logger.error("Fail to getInstance of JCEKS keystore.", kse);
} catch (FileNotFoundException fnfe) {
logger.error("Fail to find the keystore file configured: " + props.getProperty(KEYSTORE_PROP), fnfe);
logger.error(
"Fail to find the keystore file configured: "
+ props.getProperty(keystoreProp), fnfe);
} catch (NoSuchAlgorithmException e) {
logger.error("NoSuchAlgorithmException in loading keystore from file: " + props.getProperty(KEYSTORE_PROP), e);
logger.error(
"NoSuchAlgorithmException in loading keystore from file: "
+ props.getProperty(keystoreProp), e);
} catch (CertificateException e) {
logger.error("CertificateException in loading keystore from file: " + props.getProperty(KEYSTORE_PROP), e);
logger.error("CertificateException in loading keystore from file: "
+ props.getProperty(keystoreProp), e);
} catch (IOException e) {
logger.error("IOException in loading keystore from file: " + props.getProperty(KEYSTORE_PROP), e);
logger.error(
"IOException in loading keystore from file: "
+ props.getProperty(keystoreProp), e);
} catch (UnrecoverableKeyException e) {
logger.error("UnrecoverableKeyException in loading keystore from file: " + props.getProperty(KEYSTORE_PROP), e);
logger.error(
"UnrecoverableKeyException in loading keystore from file: "
+ props.getProperty(keystoreProp), e);
} catch (ParseException e) {
logger.error("ParseException in parsing alias for key date: " + props.getProperty(KEYSTORE_PROP), e);
logger.error("ParseException in parsing alias for key date: "
+ props.getProperty(keystoreProp), e);
}
return null;
return rval;
}
/**
* force to reload keys, useful for testing
* @return
*/
public static BinLightningAESKey[] reloadBinLightningAESKeys() {
if (keys != null) keys = null;
return getBinLightningAESKeys();
public static BinLightningAESKey[] reloadBinLightningAESKeys(
String propertyPrefix) {
keyCache.clear();
propsLoaded = false;
return getBinLightningAESKeys(propertyPrefix);
}
/**
* Get cipher algorithm name for data type
*
* @param propertyPrefix
* prefix for properties associated with a particular lightning
* data type
* @return default algorithm if no property is found for prefix
*/
public static String getCipherAlgorithm(String propertyPrefix) {
return props.getProperty(propertyPrefix + CIPHER_ALGORITHM_SUFFIX,
DEFAULT_CIPHER_ALGORITHM);
}
private String alias;
private Key key;

View file

@ -10,6 +10,8 @@ import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@ -34,6 +36,7 @@ import com.raytheon.uf.common.status.UFStatus;
* 20130503 DCS 112 Wufeng Zhou To handle both the new encrypted data and legacy bit-shifted data
* Jun 03, 2014 3226 bclement moved from com.raytheon.edex.plugin.binlightning to gov.noaa.nws.ost.edex.plugin.binlightning
* handled null return from BinLightningAESKey.getBinLightningAESKeys()
* Jun 09, 2014 3226 bclement refactored to support multiple stores for different data types
*
* </pre>
*
@ -41,38 +44,68 @@ import com.raytheon.uf.common.status.UFStatus;
*
*/
public class EncryptedBinLightningCipher {
private static final String BINLIGHTNING_CIPHER_TYPE = "AES";
/** Maximum size of the encrypted block, determined by 3 byte length field in the header */
private static final int MAX_SIZE_ENCRYPTED_BLOCK = 0xffffff;
/**
* Cipher creation is a relatively expensive operation and would be better to reuse it in the same thread.
**/
private static final ThreadLocal<HashMap<String, Cipher>> decryptCipherMap = new ThreadLocal<HashMap<String, Cipher>>() {
private static final Map<String, Map<String, Cipher>> decryptCipherMapCache = new ConcurrentHashMap<String, Map<String, Cipher>>(
2);
@Override
protected HashMap<String, Cipher> initialValue() {
// get AES keys from keystore and create encryption and decryption ciphers from them
BinLightningAESKey[] keys = BinLightningAESKey.getBinLightningAESKeys();
/**
* Get cipher map using cache
*
* @param propertyPrefix
* datatype properties file prefix
* @return
*/
private static Map<String, Cipher> getCachedCipherMap(String propertyPrefix) {
Map<String, Cipher> rval = decryptCipherMapCache.get(propertyPrefix);
if (rval == null) {
synchronized (decryptCipherMapCache) {
if (rval == null) {
rval = createCipherMap(propertyPrefix);
decryptCipherMapCache.put(propertyPrefix, rval);
}
}
}
return rval;
}
/**
* Create a mapping key aliases to keys for datatype
*
* @param propertyPrefix
* datatype properties file prefix
* @return
*/
private static Map<String, Cipher> createCipherMap(String propertyPrefix) {
/*
* get AES keys from keystore and create encryption and decryption
* ciphers from them
*/
BinLightningAESKey[] keys = BinLightningAESKey
.getBinLightningAESKeys(propertyPrefix);
if (keys == null) {
keys = new BinLightningAESKey[0];
}
HashMap<String, Cipher> cipherMap = new HashMap<String, Cipher>();
for (BinLightningAESKey key : keys) {
try {
SecretKeySpec skeySpec = (SecretKeySpec)key.getKey();
Cipher cipher = Cipher.getInstance(BINLIGHTNING_CIPHER_TYPE);
SecretKeySpec skeySpec = (SecretKeySpec) key.getKey();
String algorithm = BinLightningAESKey
.getCipherAlgorithm(propertyPrefix);
Cipher cipher = Cipher.getInstance(algorithm);
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
cipherMap.put(key.getAlias(), cipher);
} catch (Exception e) {
logger.error("Fail to create decrypt Cipher from key " + key.getAlias(), e);
logger.error(
"Fail to create decrypt Cipher from key "
+ key.getAlias(), e);
}
}
return cipherMap;
}
};
private static IUFStatusHandler logger = UFStatus
.getHandler(EncryptedBinLightningCipher.class);
@ -85,24 +118,33 @@ public class EncryptedBinLightningCipher {
* decrypt data with AES keys
*
* @param data
* @param propertyPrefix
* prefix for lightning type configuration
* @return
* @throws IllegalBlockSizeException
* @throws BadPaddingException
*/
public byte[] decryptData(byte[] data) throws IllegalBlockSizeException, BadPaddingException, BinLightningDataDecryptionException {
return decryptData(data, null);
public byte[] decryptData(byte[] data, String propertyPrefix)
throws IllegalBlockSizeException, BadPaddingException,
BinLightningDataDecryptionException {
return decryptData(data, null, propertyPrefix);
}
/**
* decrypt data with AES keys, using data observation date as a hint to find the best suitable key to try first
* decrypt data with AES keys, using data observation date as a hint to find
* the best suitable key to try first
*
* @param data
* @param dataDate
* @param propertyPrefix
* prefix for lightning type configuration
* @return
* @throws IllegalBlockSizeException
* @throws BadPaddingException
*/
public byte[] decryptData(byte[] data, Date dataDate) throws IllegalBlockSizeException, BadPaddingException, BinLightningDataDecryptionException {
public byte[] decryptData(byte[] data, Date dataDate, String propertyPrefix)
throws IllegalBlockSizeException, BadPaddingException,
BinLightningDataDecryptionException {
if (data == null) {
throw new IllegalBlockSizeException("Data is null");
}
@ -110,12 +152,14 @@ public class EncryptedBinLightningCipher {
throw new IllegalBlockSizeException("Data is empty");
}
if (data.length > MAX_SIZE_ENCRYPTED_BLOCK) {
throw new IllegalBlockSizeException("Block size exceeds maxinum expected.");
throw new IllegalBlockSizeException(
"Block size exceeds maximum expected.");
}
HashMap<String, Cipher> cipherMap = EncryptedBinLightningCipher.decryptCipherMap.get();
Map<String, Cipher> cipherMap = getCachedCipherMap(propertyPrefix);
// find the preferred key order to try decryption based on data date
List<BinLightningAESKey> preferredKeyList = findPreferredKeyOrderForData(dataDate);
List<BinLightningAESKey> preferredKeyList = findPreferredKeyOrderForData(
dataDate, propertyPrefix);
if (preferredKeyList == null || preferredKeyList.size() == 0) {
throw new BinLightningDataDecryptionException("No AES key found to decrypt data. Please make sure keystore is properly configured with key(s).");
@ -124,7 +168,12 @@ public class EncryptedBinLightningCipher {
// try to decrypt the data using ciphers in the list until successful
byte[] decryptedData = null;
for (int i = 0; i < preferredKeyList.size(); i++) {
Cipher cipher = cipherMap.get(preferredKeyList.get(i).getAlias());
String alias = preferredKeyList.get(i).getAlias();
Cipher cipher = cipherMap.get(alias);
if (cipher == null) {
logger.warn("No cipher found for alias: " + alias);
continue;
}
try {
decryptedData = cipher.doFinal(data, 0, data.length);
@ -133,45 +182,63 @@ public class EncryptedBinLightningCipher {
if ( BinLightningDecoderUtil.isKeepAliveRecord(decryptedData) == false && BinLightningDecoderUtil.isLightningDataRecords(decryptedData) == false) {
//if (BinLigntningDecoderUtil.isValidMixedRecordData(decryptedData) == false) { // use this only if keep-alive record could be mixed with lightning records
throw new BinLightningDataDecryptionException("Decrypted data (" + decryptedData.length + " bytes) with key "
+ preferredKeyList.get(i).getAlias() + " is not valid keep-alive or binLightning records.", decryptedData);
+ alias
+ " is not valid keep-alive or binLightning records.",
decryptedData);
}
logger.info("Data (" + data.length + " bytes) decrypted to " + decryptedData.length + " bytes with key: " + preferredKeyList.get(i).getAlias());
logger.info("Data (" + data.length + " bytes) decrypted to "
+ decryptedData.length + " bytes with key: " + alias);
break; // decrypt ok, break out
} catch (IllegalBlockSizeException e) {
// ignore exception if not the last, and try next cipher
logger.info("Fail to decrypt data (" + data.length + " bytes) with key: " + preferredKeyList.get(i).getAlias() + " - " + e.getMessage() + ", will try other available key");
logger.info("Fail to decrypt data (" + data.length
+ " bytes) with key: " + alias + " - " + e.getMessage()
+ ", will try other available key");
if (i == (preferredKeyList.size() - 1)) {
logger.error("Fail to decrypt with all known keys, either data is not encrypted or is invalid: " + e.getMessage());
throw e;
}
} catch (BadPaddingException e) {
// ignore exception if not the last, and try next cipher
logger.info("Fail to decrypt data (" + data.length + " bytes) with key: " + preferredKeyList.get(i).getAlias() + " - " + e.getMessage() + ", will try other available key");
logger.info("Fail to decrypt data (" + data.length
+ " bytes) with key: " + alias + " - " + e.getMessage()
+ ", will try other available key");
if (i == (preferredKeyList.size() - 1)) {
logger.error("Fail to decrypt with all known keys, either data is not encrypted or is invalid: " + e.getMessage());
throw e;
}
} catch (BinLightningDataDecryptionException e) {
// ignore exception if not the last, and try next cipher
logger.info("Fail to decrypt data (" + data.length + " bytes) with key: " + preferredKeyList.get(i).getAlias() + " - " + e.getMessage() + ", will try other available key");
logger.info("Fail to decrypt data (" + data.length
+ " bytes) with key: " + alias + " - " + e.getMessage()
+ ", will try other available key");
if (i == (preferredKeyList.size() - 1)) {
logger.error("Fail to decrypt with all known keys, either data is not encrypted or is invalid: " + e.getMessage());
throw e;
}
}
}
if (decryptedData == null) {
throw new BinLightningDataDecryptionException(
"No ciphers found for data type: " + propertyPrefix);
}
return decryptedData;
}
/**
* Assuming the best keys to decrypt data should be issued before the data observation date, so
* if there were many keys issued, this hopefully will reduce the unnecessary decryption tries
* Assuming the best keys to decrypt data should be issued before the data
* observation date, so if there were many keys issued, this hopefully will
* reduce the unnecessary decryption tries
*
* @param dataDate
* @param propertyPrefix
* prefix for lightning type configuration
* @return preferred key list order
*/
private List<BinLightningAESKey> findPreferredKeyOrderForData(Date dataDate) {
BinLightningAESKey[] binLightningAESKeys = BinLightningAESKey.getBinLightningAESKeys();
private List<BinLightningAESKey> findPreferredKeyOrderForData(
Date dataDate, String propertyPrefix) {
BinLightningAESKey[] binLightningAESKeys = BinLightningAESKey
.getBinLightningAESKeys(propertyPrefix);
if (binLightningAESKeys == null || binLightningAESKeys.length < 1) {
return Collections.emptyList();
}
@ -198,4 +265,41 @@ public class EncryptedBinLightningCipher {
}
}
/**
* ensures that the data is the appropriate length for AES decryption.
* Copies data to new array.
*
* @param pdata
* @param traceId
* @return
*/
public static byte[] prepDataForDecryption(byte[] pdata, String traceId) {
/*
* NOTE: 11/14/2013 WZ:
* encrypted test data on TNCF (got from Melissa Porricelli)
* seems to have extra 4 bytes (0x0d 0x0d 0x0a 0x03) at the end,
* making the data size not a multiple of 16. However, original
* test data do not have this trailing bytes. while NCEP test
* data has extra 8 trailing bytes.
* Brain Rapp's email on 11/13/2013 confirms that Unidata LDM
* software used by AWIPS II will strips off all SBN protocol
* headers
* that precede the WMO header and adds its own 11 byte header
* like this: "soh cr cr nl 2 5 4 sp cr cr nl". It
* also adds a four byte trailer consisting of "cr cr nl etx"
* (0x0d 0x0d 0x0a 0x03)
* So, it seems necessary to trim trailing bytes if it is not
* multiple of 16, warning messages will be logged though
*/
int dataLengthToBeDecrypted = pdata.length;
if (pdata.length % 16 != 0) {
dataLengthToBeDecrypted = pdata.length - (pdata.length % 16);
logger.warn(traceId + " - Data length from file " + traceId
+ " is " + pdata.length + " bytes, trailing "
+ (pdata.length - dataLengthToBeDecrypted)
+ " bytes has been trimmed to " + dataLengthToBeDecrypted
+ " bytes for decryption.");
}
return Arrays.copyOfRange(pdata, 0, dataLengthToBeDecrypted);
}
}