package org.openhab.binding.zwave.internal.protocol.commandclass; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.TimeUnit; import org.openhab.binding.zwave.internal.protocol.SerialMessage; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageClass; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessagePriority; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageType; import org.openhab.binding.zwave.internal.protocol.ZWaveController; import org.openhab.binding.zwave.internal.protocol.ZWaveEventListener; import org.openhab.binding.zwave.internal.protocol.ZWaveNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Nonces (one time use tokens) are used quite heavily in zwave secure encapsulated messages. * This class is in charge of storing: * 1) The nonces we generate and send to the device so it can encapsulate incoming messages * 2) The nonces we receive from the device to encapsulate outgoig messages * * Temporary storage is required in both cases. Each nonce also has suggested timeouts per the * zwave spec, those timeouts are also tracked in this class * * @author Dave Badia * @since TODO * */ public class ZWaveSecureNonceTracker { private static final Logger logger = LoggerFactory.getLogger(ZWaveSecureNonceTracker.class); /** * Should be set to true * * The code from which this was based included numerous bad security practices (hardcoded IVs, seeding of PRNG * with timestamp). * * It is unknown as to whether that logic was necessary to work around device defects or if it was just by mistake. * * Setting this to false will use the bad security practices from the original code. true will use accepted security * best practices * * Package-protected visible for test case use */ static boolean USE_SECURE_CRYPTO_PRACTICES = true; /** * It's a security best practice to periodically re-seed our random number * generator * http://www.cigital.com/justice-league-blog/2009/08/14/proper-use-of-javas-securerandom/ */ private static final long SECURE_RANDOM_RESEED_INTERVAL_MILLIS = TimeUnit.DAYS.toMillis(1); private final ZWaveNode node; /** * Nonces generated by us and sent to the device in a {@link ZWaveSecurityCommandClass#SECURITY_NONCE_REPORT} * message * Used in the decryption process to process incoming SECURITY messages */ private NonceTable ourNonceTable = new NonceTable(); /** * Nonces generated by the device and sent to us in a {@link ZWaveSecurityCommandClass#SECURITY_NONCE_REPORT} * message. * Used in the encryption process for outgoing SECURITY messages */ private DeviceNonceTable deviceNonceTable = new DeviceNonceTable(); /** * Timer to track time elapsed between sending {@link ZWaveSecurityCommandClass#SECURITY_NONCE_GET} and * receiving {@link #SECURITY_NONCE_REPORT}. If too * much time elapses we should request a new nonce. This timer is optional * but recommended */ private NonceTimer requestNonceTimer = null; /** * The last nonce request message sent to the device. We implement {@link ZWaveEventListener} since * the time from which we generate the nonce request message to the time we actually send it can * be a significant delay, especially during secure inclusion */ private SerialMessage requestNonceMessage = null; // TODO: DB what was the point of this? private SecureRandom secureRandom = null; private long reseedAt = 0L; ZWaveSecureNonceTracker(ZWaveNode node) { this.node = node; } /** * @return a useable {@link Nonce} or null if none are available */ synchronized Nonce getUseableDeviceNonce() { Nonce nonce = deviceNonceTable.getDeviceNonceToEncryptMessage(); logger.debug("NODE {}: getUseableDeviceNonce returning {}", node.getNodeId(), nonce); return nonce; } /** * @return true if a nonce has been requested from the node and a reply is pending */ private synchronized boolean hasNonceBeenRequested() { logger.debug("NODE {}: getUseableDeviceNonce() requestNonceTimer={}", node.getNodeId(), requestNonceTimer); if (requestNonceTimer != null && !requestNonceTimer.isExpired()) { return true; } else { requestNonceTimer = null; return false; } } synchronized SerialMessage buildNonceGetIfNeeded() { if (hasNonceBeenRequested()) { logger.debug("NODE {}: already waiting for nonce", node.getNodeId()); return null; } logger.debug("NODE {}: requesting nonce", node.getNodeId()); SerialMessage message = new SerialMessage(node.getNodeId(), SerialMessageClass.SendData, SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler, ZWaveSecurityCommandClass.SECURITY_MESSAGE_PRIORITY); byte[] payload = { (byte) node.getNodeId(), 2, (byte) ZWaveSecurityCommandClass.getSecurityCommandClass().getKey(), ZWaveSecurityCommandClass.SECURITY_NONCE_GET, }; if (ZWaveSecurityCommandClass.OVERRIDE_DEFAULT_TRANSMIT_OPTIONS) { logger.trace("NODE {}: Using custom transmit options", node.getNodeId()); message.setTransmitOptions( ZWaveController.TRANSMIT_OPTION_ACK | ZWaveController.TRANSMIT_OPTION_AUTO_ROUTE); } // We only try once as strange things happen with NONCE_GET requests TODO: DB add more detail as to what we are // trying to fix here message.setMessagePayload(payload); if (requestNonceTimer != null) { logger.warn("NODE {}: requestNonceTimer != null but generating a new request", node.getNodeId()); } requestNonceTimer = new NonceTimer(NonceTimerType.REQUESTED, node); requestNonceMessage = message; return message; } /** * Generate a new nonce, then build a SECURITY_NONCE_REPORT */ SerialMessage generateAndBuildNonceReport() { Nonce nonce = ourNonceTable.generateNewUniqueNonceForDevice(); // SECURITY_NONCE_REPORT gets immediate priority SerialMessage message = new SerialMessage(node.getNodeId(), SerialMessageClass.SendData, SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler, SerialMessagePriority.Immediate); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write((byte) node.getNodeId()); baos.write((byte) 10); baos.write((byte) ZWaveSecurityCommandClass.getSecurityCommandClass().getKey()); baos.write(ZWaveSecurityCommandClass.SECURITY_NONCE_REPORT); try { baos.write(nonce.getNonceBytes()); message.setMessagePayload(baos.toByteArray()); if (ZWaveSecurityCommandClass.OVERRIDE_DEFAULT_TRANSMIT_OPTIONS) { logger.trace("NODE {}: Using custom transmit options", node.getNodeId()); message.setTransmitOptions( ZWaveController.TRANSMIT_OPTION_ACK | ZWaveController.TRANSMIT_OPTION_AUTO_ROUTE); } } catch (IOException e) { logger.error("NODE {}: Error during Security sendNonceReport.", node.getNodeId(), e); return null; } return message; } /** * Called by {@link ZWaveSecurityCommandClass} so the nonce tracker is aware that * a nonce request is being sent via SECURITY_MESSAGE_ENCAP_NONCE_GET and our timer should be started */ synchronized void sendingEncapNonceGet(SerialMessage message) { // No requestNonceTimer != null check since this will be called multiple times for teh same requestNonceTimer = new NonceTimer(NonceTimerType.REQUESTED, node); requestNonceMessage = message; } synchronized void receivedNonceFromDevice(byte[] nonceBytes) { if (requestNonceTimer == null) { logger.warn("NODE {}: nonce was received, but we have no requestNonceTimer", node.getNodeId()); } else if (requestNonceTimer.isExpired()) { // The nonce was not received within the alloted time of us sending the nonce request. Send it again logger.warn("NODE {}: nonce was not received within {}ms, a new one will be requested.", node.getNodeId(), NonceTimerType.REQUESTED.validityInMillis); // The ZWaveSecurityEncapsulationThread will request a new one for us return; } logger.debug("NODE {}: receivedNonceFromDevice nonce received setting requestNonceTimer to null", node.getNodeId()); requestNonceTimer = null; deviceNonceTable.addNonceFromDevice(nonceBytes); } Nonce getNonceWeGeneratedById(byte nonceId) { Nonce nonce = ourNonceTable.getNonceById(nonceId); return nonce; } /** * Generates a nonce that isn't stored anywhere */ byte[] generateNonceForEncapsulationMessage() { return generateNonceBytes(); } private byte[] generateNonceBytes() { if (!USE_SECURE_CRYPTO_PRACTICES) { return Nonce.INSECURE_NONCE_BYTES; } if (System.currentTimeMillis() > reseedAt) { secureRandom = createNewSecureRandom(); reseedAt = System.currentTimeMillis() + SECURE_RANDOM_RESEED_INTERVAL_MILLIS; } byte[] nonceBytes = new byte[8]; secureRandom.nextBytes(nonceBytes); return nonceBytes; } private static SecureRandom createNewSecureRandom() { SecureRandom secureRandom = null; // SecureRandom advice taken from // http://www.cigital.com/justice-league-blog/2009/08/14/proper-use-of-javas-securerandom/ try { secureRandom = SecureRandom.getInstance("SHA1PRNG", "SUN"); } catch (GeneralSecurityException e) { secureRandom = new SecureRandom(); } // force an internal seeding secureRandom.nextBoolean(); // Add some entropy of our own to the seed secureRandom.setSeed(Runtime.getRuntime().freeMemory()); for (File root : File.listRoots()) { secureRandom.setSeed(root.getUsableSpace()); } return secureRandom; } /* -------------- Begin inner classes -------------- */ /** * The type of Nonce */ private static enum NonceTimerType { /** * Optional but recommended, so we implement it. * Is triggered when we send a {@link ZWaveSecurityCommandClass#SECURITY_NONCE_GET} */ REQUESTED(TimeUnit.SECONDS.toMillis(20)), // 20 seconds since this is optional anyway /** * Required and is triggered when we generate a nonce to send to the device * via a {@link ZWaveSecurityCommandClass#SECURITY_NONCE_REPORT}. * Represents how long the device has to use the nonce we sent from the time we * generated it (NOT the time we received the ack). min=3, recommended=10, max=20 */ GENERATED(TimeUnit.SECONDS.toMillis(10)), /** * Is used to estimate if a nonce we received from a device is still * useful. We have no way of knowing for sure, as nonces can be valid * for as little as 3 but as many as 20 seconds. Also, the devices * timer starts when it sends the nonce, not when we get it. So slow * transmission time can also cause the nonce to be unusable. * */ // TODO: DB track if nonce used are from ENCAP_GET_NONCE, and if those keep failing, disable the use // of them since the device has a short timer RECEIVED(TimeUnit.SECONDS.toMillis(5)), // 5 seconds is our best guess /** * No timer required. Typically used when we generate a nonce to include in a * {@link ZWaveSecurityCommandClass#SECURITY_MESSAGE_ENCAP} or * {@link ZWaveSecurityCommandClass#SECURITY_MESSAGE_ENCAP_NONCE_GET} message */ NONE(Long.MAX_VALUE); private final long generatedAt = System.currentTimeMillis(); private final long validityInMillis; private NonceTimerType(long validityInMillis) { this.validityInMillis = validityInMillis; } private long computeExpiresAt() { return System.currentTimeMillis() + validityInMillis; } } /** * per the spec we must track how long it has been since we * sent a nonce and only allow it's use within a specified * time period. */ static class NonceTimer { private NonceTimerType type; private long expiresAt; private int nodeId; NonceTimer(NonceTimerType type, ZWaveNode node) { this.type = type; this.nodeId = node.getNodeId(); reset(); } void reset() { expiresAt = type.computeExpiresAt(); } private long getExpiresAt() { return expiresAt; } /** * @return ms left before this nonce expires, or a negative number if * it has already expired */ private long getTimeLeft() { return expiresAt - System.currentTimeMillis(); } private boolean isExpired() { long now = System.currentTimeMillis(); boolean expired = getTimeLeft() < 0; if (logger.isTraceEnabled()) { DateFormat dateFormatter = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); logger.trace("NODE {}: expiresAt={} now={}, expired={}", nodeId, dateFormatter.format(expiresAt), dateFormatter.format(now), expired); } return expired; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("NonceTimer [type=").append(type).append(" expired=").append(isExpired()) .append(" getTimeLeft=").append(getTimeLeft()).append("]"); return builder.toString(); } } /** * Class to hold the nonce itself and the it's related data */ static class Nonce { private static final byte[] INSECURE_NONCE_BYTES = new byte[] { (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, }; private byte[] nonceBytes; private NonceTimer timer; private byte nonceId; /** * Generates a nonce to be sent to a device in * a {@link ZWaveSecurityCommandClass#SECURITY_NONCE_REPORT} message * * @param nonceBytes * @param timer the timer should be used, can be null */ protected Nonce(byte[] nonceBytes, NonceTimer timer) { super(); if (nonceBytes == null || nonceBytes.length != 8) { throw new IllegalArgumentException("Invalid nonce length for " + Arrays.toString(nonceBytes)); } this.nonceBytes = nonceBytes; this.nonceId = nonceBytes[0]; this.timer = timer; } byte[] getNonceBytes() { return nonceBytes; } /** * @return the timer or null if none was used */ private NonceTimer getTimer() { return timer; } private byte getNonceId() { return nonceId; } @Override public String toString() { StringBuilder buf = new StringBuilder("Nonce "); if (timer != null) { buf.append(timer.type).append(" "); } buf.append(SerialMessage.bb2hex(nonceBytes)); if (timer != null) { buf.append("; time left=").append(timer.getTimeLeft()); } return buf.toString(); } @Override public int hashCode() { int prime = 31; int result = 1; result = prime * result + Arrays.hashCode(nonceBytes); result = prime * result + nonceId; result = prime * result + ((timer == null) ? 0 : timer.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Nonce other = (Nonce) obj; if (!Arrays.equals(nonceBytes, other.nonceBytes)) { return false; } return true; } } /** * Data store to hold the nonces we have generated and * provide a method to cleanup old nonces * */ private class NonceTable { /** * Store nonces that we generated but have not been retreived yet here */ private Map<Byte, Nonce> table = new ConcurrentHashMap<Byte, Nonce>(); /** * Once a nonce is used (that is, we get a {@link ZWaveSecurityCommandClass#SECURITY_MESSAGE_ENCAP} * with the nonce id of a nonce) it is removed from {@link #table}. But the nonce's ID (1st byte) * is stored here */ private SizeLimitedQueue<Byte> usedNonceIdList = new SizeLimitedQueue(10); /** * When {@link #cleanup()} finds an expired nonce, it's remove from {@link #table} and * it's nonce id gets stored here */ private SizeLimitedQueue<Byte> expiredNonceIdList = new SizeLimitedQueue(10); private NonceTable() { super(); } private Nonce generateNewUniqueNonceForDevice() { byte[] nonceBytes = generateNonceBytes(); boolean unique = false; while (!unique) { // Collision, try again nonceBytes = generateNonceBytes(); // Make sure the id is unique for all nonces in storage // Can't have duplicate 1st bytes since that is the nonce ID unique = ourNonceTable.getNonceById(nonceBytes[0]) == null && !usedNonceIdList.contains(nonceBytes[0]) && !expiredNonceIdList.contains(nonceBytes[0]); } Nonce nonce = new Nonce(nonceBytes, new NonceTimer(NonceTimerType.GENERATED, node)); logger.debug(String.format("NODE %s: Generated new nonce for device: %s", node.getNodeId(), SerialMessage.bb2hex(nonce.getNonceBytes()))); table.put(nonce.getNonceId(), nonce); return nonce; } private Nonce getNonceById(byte id) { // Nonces can only be used once so remove it Nonce nonce = table.remove(id); if (nonce != null) { usedNonceIdList.add(nonce.getNonceId()); logger.debug(String.format( "NODE %s: Device message contained nonce id of id=0x%02X, found matching nonce of: %s", node.getNodeId(), id, nonce)); } else if (expiredNonceIdList.contains(id)) { logger.error(String.format("NODE %s: Device message contained expired nonce id=0x%02X", node.getNodeId(), id)); } else if (usedNonceIdList.contains(id)) { logger.error( String.format("NODE %s: Device message contained nonce that was previously used, id=0x%02X", node.getNodeId(), id)); } else { logger.error(String.format( "NODE %s: Device message contained nonce that is unknown to us, id=0x%02X. table=%s, expiredList=%s, usedList=%s", node.getNodeId(), id, table, expiredNonceIdList, usedNonceIdList)); } cleanup(); return nonce; } /** * Remove any expired nonces from our table */ private void cleanup() { Iterator<Entry<Byte, Nonce>> iter = table.entrySet().iterator(); while (iter.hasNext()) { Nonce nonce = iter.next().getValue(); if (nonce.getTimer() != null) { // Wait an extra 10 seconds after we send the nonce to the device for // it to come back and be used long removeAt = nonce.getTimer().getExpiresAt() + 10000; if (System.currentTimeMillis() > removeAt) { logger.warn(String.format("NODE %s: Expiring nonce with id=0x%02X", node.getNodeId(), nonce.getNonceId())); iter.remove(); expiredNonceIdList.add(nonce.getNonceId()); } } } } @Override public String toString() { StringBuilder buf = new StringBuilder("NonceTable: ["); for (Nonce nonce : table.values()) { buf.append(nonce.toString()).append(" "); } return buf.toString(); } } /** * Data store to hold nonces generated by the device and sent to us * in a {@link ZWaveSecurityCommandClass#SECURITY_NONCE_REPORT} message. * Used in the encryption process for outgoing SECURITY messages */ private class DeviceNonceTable { private Map<Byte, Nonce> table = new ConcurrentHashMap<Byte, Nonce>(); private ConcurrentSkipListMap<Long, Nonce> timeToNonceMap = new ConcurrentSkipListMap<Long, Nonce>(); private DeviceNonceTable() { super(); } private void addNonceFromDevice(byte[] nonceBytes) { Nonce deviceNonce = new Nonce(nonceBytes, new NonceTimer(NonceTimerType.RECEIVED, node)); table.put(deviceNonce.getNonceId(), deviceNonce); timeToNonceMap.put(deviceNonce.getTimer().getExpiresAt(), deviceNonce); } private Nonce getDeviceNonceToEncryptMessage() { logger.trace("NODE {}: getDeviceNonceToEncryptMessage start deviceNonceTable={}, timeToNonceMap={}", node.getNodeId(), deviceNonceTable, timeToNonceMap); cleanup(); logger.trace("NODE {}: getDeviceNonceToEncryptMessage post cleanup deviceNonceTable={}, timeToNonceMap={}", node.getNodeId(), deviceNonceTable, timeToNonceMap); Iterator<Nonce> iter = timeToNonceMap.values().iterator(); if (iter.hasNext()) { Nonce nonce = iter.next(); logger.trace("NODE {}: getDeviceNonceToEncryptMessage returning DeviceNonce={}", node.getNodeId(), nonce); iter.remove(); // Remove it since we are using it return nonce; } else { return null; } } /** * Remove any expired nonces from our table */ private void cleanup() { Iterator<Entry<Byte, Nonce>> iter = table.entrySet().iterator(); while (iter.hasNext()) { Nonce nonce = iter.next().getValue(); if (nonce.getTimer() != null && nonce.getTimer().isExpired()) { logger.warn(String.format("NODE %s: Expiring nonce with id=%02X", node.getNodeId(), nonce.getNonceId())); iter.remove(); // Remove the nonce from timeToNonceIdMap byte nonceId = nonce.getNonceId(); Iterator<Map.Entry<Long, Nonce>> iter2 = timeToNonceMap.entrySet().iterator(); while (iter2.hasNext()) { Map.Entry<Long, Nonce> entry = iter2.next(); if (nonceId == entry.getValue().getNonceId()) { iter2.remove(); } } } } } @Override public String toString() { StringBuilder buf = new StringBuilder("NonceTable: ["); for (Nonce nonce : table.values()) { buf.append(nonce.toString()).append(" "); } return buf.toString(); } } /** * To ease nonce error condition tracking, we keep nonce values after they expire or are used. * Limit the number we keep to avoid a memory leak * * code from https://stackoverflow.com/questions/5498865/size-limited-queue-that-holds-last-n-elements-in-java * * @author Dave Badia */ private class SizeLimitedQueue<E> extends LinkedList<E> { private int limit; public SizeLimitedQueue(int limit) { this.limit = limit; } @Override public boolean add(E o) { super.add(o); while (size() > limit) { super.remove(); } return true; } } }