/* * Copyright (c) 2010-2015, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.zwave.internal.protocol.commandclass; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.openhab.binding.zwave.internal.protocol.SecurityEncapsulatedSerialMessage; 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.ZWaveEndpoint; import org.openhab.binding.zwave.internal.protocol.ZWaveNode; import org.openhab.binding.zwave.internal.protocol.commandclass.ZWaveSecureNonceTracker.Nonce; import org.openhab.binding.zwave.internal.protocol.event.ZWaveInclusionEvent; import org.openhab.binding.zwave.internal.protocol.event.ZWaveInclusionEvent.Type; import org.openhab.binding.zwave.internal.protocol.initialization.ZWaveNodeSerializer; import org.openhab.binding.zwave.internal.protocol.serialmessage.ApplicationCommandMessageClass; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.thoughtworks.xstream.annotations.XStreamAlias; import com.thoughtworks.xstream.annotations.XStreamOmitField; /** * For code readability and maintainability, the logic for the secure command class is split into 2 classes: this one * and {@link ZWaveSecurityCommandClassWithInitialization}. {@link ZWaveSecurityCommandClassWithInitialization} * will always be created, as this class is abstract * * @see {@link ZWaveSecurityCommandClassWithInitialization} * @author Dave Badia * @since TODO */ @XStreamAlias("securityCommandClass") public abstract class ZWaveSecurityCommandClass extends ZWaveCommandClass { private static final Logger logger = LoggerFactory.getLogger(ZWaveSecurityCommandClass.class); /** * Per the z-wave spec, this is the AES key used to derive {@link #encryptKey} from {@link #networkKey} */ private static final byte[] DERIVE_ENCRYPT_KEY = { (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA, (byte) 0xAA }; /** * Per the z-wave spec, this is the AES key used to derive {@link #authKey} from {@link #networkKey} */ private static final byte[] DERIVE_AUTH_KEY = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }; private static final String AES = "AES"; private static final int MAC_LENGTH = 8; private static final int IV_LENGTH = 16; static final int HALF_OF_IV = IV_LENGTH / 2; /** * Marks the end of the list of supported command classes. The remaining classes are those that can be controlled by * the device. These classes are created without values. Messages received cause notification events instead. */ public static final byte COMMAND_CLASS_MARK = (byte) 0xef; /** * Request which commands the device supports using * security encapsulation (encryption) */ static final byte SECURITY_COMMANDS_SUPPORTED_GET = 0x02; /** * Response from the device which indicates which commands * the device supports using security encapsulation (encryption) */ static final byte SECURITY_COMMANDS_SUPPORTED_REPORT = 0x03; /** * Request which security initialization schemes the * device supports */ static final byte SECURITY_SCHEME_GET = 0x04; /** * Response from the device of which security initialization * schemes the device supports */ static final byte SECURITY_SCHEME_REPORT = 0x05; /** * The controller is sending the device the network key to * be used for all secure transmissions */ static final byte SECURITY_NETWORK_KEY_SET = 0x06; /** * Response from the device after getting SECURITY_NETWORK_KEY_SET * that was encapsulated using the new key */ static final byte SECURITY_NETWORK_KEY_VERIFY = 0x07; /** * Not supported since we are always the master */ private static final byte SECURITY_SCHEME_INHERIT = 0x08; /** * Request to generate a nonce to be used in message encapsulation */ public static final byte SECURITY_NONCE_GET = 0x40; /** * Response with the generated nonce to be used in message encapsulation */ public static final byte SECURITY_NONCE_REPORT = (byte) 0x80; /** * Indicates this message has been encapsulated and must be decrypted * to reveal the actual message * public so {@link ApplicationCommandMessageClass} can check for this and invoke * {@link #decryptMessage(byte[], int)} as needed */ public static final byte SECURITY_MESSAGE_ENCAP = (byte) 0x81; /** * Indicates this message has been encapsulated and must be decrypted * to reveal the actual message and that there are more messages to * send so another nonce is needed. * public so {@link ApplicationCommandMessageClass} can check for this and invoke * {@link #decryptMessage(byte[], int)} as needed */ public static final byte SECURITY_MESSAGE_ENCAP_NONCE_GET = (byte) 0xc1; private static final Map<Byte, String> COMMAND_LOOKUP_TABLE = new ConcurrentHashMap<Byte, String>(); private static final List<Byte> REQUIRED_ENCAPSULATION_LIST = Arrays .asList(new Byte[] { SECURITY_NETWORK_KEY_SET, SECURITY_NETWORK_KEY_VERIFY, SECURITY_SCHEME_INHERIT, SECURITY_COMMANDS_SUPPORTED_GET, SECURITY_COMMANDS_SUPPORTED_REPORT }); /** * Should be set to true to ensure all incoming security encapsulated messages adhere to * zwave security mac standards * * Package-protected visible for test case use */ static boolean DROP_PACKETS_ON_MAC_FAILURE = true; /** * Should be set to true unless we find a good reason not to since we * also have {@link #disableEncapNonceGet} which will disable the * use of {@link #SECURITY_MESSAGE_ENCAP_NONCE_GET} on a per device basis * if it's not working * * OZW code comments say that {@link #SECURITY_MESSAGE_ENCAP_NONCE_GET} * doesn't work so keep a flag to enable/disable. * * Package-protected visible for test case use * * @see {@link #disableEncapNonceGet} * TODO: DB NONE, NORMAL, AGGRESSIVE, VERY_AGRESSIVE */ private static final boolean USE_SECURITY_MESSAGE_ENCAP_NONCE_GET = true; /** * OZW code sets different transmit option flags for some security * messages. * * Package-protected visible for test case use */ static final boolean OVERRIDE_DEFAULT_TRANSMIT_OPTIONS = true; /** * Should be set to false as sending outside of the inclusion flow has been know to cause issues * with the lock becoming unresponsive per OZW team * * When false, SECURITY_COMMANDS_SUPPORTED_GET is only sent during secure inclusion. * When true, SECURITY_COMMANDS_SUPPORTED_GET is sent every time OpenHAB starts up. * * Package-protected visible for test case use */ protected static final boolean SEND_SECURITY_COMMANDS_SUPPORTED_GET_ON_STARTUP = false; /** * Security messages are time sensitive so mark them as high priority */ public static final SerialMessagePriority SECURITY_MESSAGE_PRIORITY = SerialMessagePriority.High; /** * Header is made up of 10 bytes: * command class byte * message type byte * 8 bytes for the device's nonce */ private static final int ENCAPSULATED_HEADER_LENGTH = 10; /** * Footer consists of the nonce ID (1 byte) and the MAC (8 bytes) */ private static final int ENCAPSULATED_FOOTER_LENGTH = 9; /** * Security encapsulated messages have much higher overhead than normal messages so we must use care to ensure we do * not bombard devices with messages or they can stop responding. To avoid bombardment, a similar message check is * performed so that if someone hits lock, then unlock, only the unlock command is sent. We only apply this logic to * specific command classes, as specified in this list */ private static final List<CommandClass> SIMILAR_FRAME_COMMAND_CLASS_LIST = Arrays .asList(new CommandClass[] { CommandClass.DOOR_LOCK, CommandClass.BATTERY }); /** * Queue of {@link ZWaveSecurityPayloadFrame} that are waiting for nonces * so they can be encapsulated and set. * * Note the reference is ConcurrentLinkedQueue and not Queue. This is done * as a safety check we have to instantiate this in multiple places. This * will ensure we always create it as the correct type. */ @XStreamOmitField private ConcurrentLinkedQueue<ZWaveSecurityPayloadFrame> payloadEncapsulationQueue = new ConcurrentLinkedQueue<ZWaveSecurityPayloadFrame>(); /** * The network key as configured in the openhab.cfg -> zwave:networkey */ @XStreamOmitField protected static SecretKey realNetworkKey; /** * The network key currently in use. My be {@link #realNetworkKey} or a scheme network key */ @XStreamOmitField private SecretKey networkKey; /** * The encryption key currently in use which is derived from {@link #networkKey} */ @XStreamOmitField private SecretKey encryptKey; /** * The auth key currently in use which is derived from {@link #networkKey} */ @XStreamOmitField private SecretKey authKey; /** * The error that occurred when trying to load the encryption key from openhab.cfg -> zwave:networkey * Will be null if the load succeeded */ @XStreamOmitField protected static Exception keyException; /** * Flag so we understand that we received the {@link #SECURITY_COMMANDS_SUPPORTED_REPORT} reply * This occurs during inclusion and optionally during normal startup depending on the * {@value #SEND_SECURITY_COMMANDS_SUPPORTED_GET_ON_STARTUP} flag */ @XStreamOmitField protected boolean receivedSecurityCommandsSupportedReport = false; /** * The last time we sent a security message to the node */ @XStreamOmitField protected long lastSentMessageTimestamp = 0; /** * The last time we received any security message from the node */ @XStreamOmitField protected long lastReceivedMessageTimestamp = 0; /** * The last time we received a {@link #SECURITY_NONCE_GET} message from the node */ @XStreamOmitField private long lastNonceGetReceivedAt = 0L; @XStreamOmitField private volatile SecurityEncapsulatedSerialMessage lastEncapsulatedRequstMessage = null; @XStreamOmitField protected ZWaveSecureNonceTracker nonceGeneration = new ZWaveSecureNonceTracker(getNode()); @XStreamOmitField private Object threadLock = new Object(); @XStreamOmitField private ZWaveSecurityEncapsulationThread encapsulationThread; @XStreamOmitField private long lastDeviceNonceReceivedAt = 0L; // TODO: DB serialize /** * Flag to disable the use of {@link #SECURITY_MESSAGE_ENCAP_NONCE_GET} * on a per device basis if it's not working */ private boolean disableEncapNonceGet = false; static { // Initialize the COMMAND_LOOKUP_TABLE COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_COMMANDS_SUPPORTED_GET), "SECURITY_COMMANDS_SUPPORTED_GET"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_COMMANDS_SUPPORTED_REPORT), "SECURITY_COMMANDS_SUPPORTED_REPORT"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_SCHEME_GET), "SECURITY_SCHEME_GET"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_SCHEME_REPORT), "SECURITY_SCHEME_REPORT"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_NETWORK_KEY_SET), "SECURITY_NETWORK_KEY_SET"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_NETWORK_KEY_VERIFY), "SECURITY_NETWORK_KEY_VERIFY"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_SCHEME_INHERIT), "SECURITY_SCHEME_INHERIT"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_NONCE_GET), "SECURITY_NONCE_GET"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_NONCE_REPORT), "SECURITY_NONCE_REPORT"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_MESSAGE_ENCAP), "SECURITY_MESSAGE_ENCAP"); COMMAND_LOOKUP_TABLE.put(Byte.valueOf(SECURITY_MESSAGE_ENCAP_NONCE_GET), "SECURITY_MESSAGE_ENCAP_NONCE_GET"); } abstract boolean checkRealNetworkKeyLoaded(); protected void transmitMessage(SerialMessage serialMessage) { // Normal (non-inclusion mode) so give the message to the controller to be transmitted this.getController().sendData(serialMessage); } /** * Creates a new instance of the ZWaveSecurityCommandClass class. This is package * protected as {@link ZWaveSecurityCommandClassWithInitialization} will typically be invoked * * @param node * the node this command class belongs to * @param controller * the controller to use * @param endpoint * the endpoint this Command class belongs to */ protected ZWaveSecurityCommandClass(ZWaveNode node, ZWaveController controller, ZWaveEndpoint endpoint) { super(node, controller, endpoint); if (!checkRealNetworkKeyLoaded()) { throw new IllegalStateException( "NODE " + getNode().getNodeId() + ": node wants to use security but key is not set"); } setupNetworkKey(false); } /** * {@inheritDoc} */ @Override public CommandClass getCommandClass() { return getSecurityCommandClass(); } protected static CommandClass getSecurityCommandClass() { return CommandClass.SECURITY; } /** * {@inheritDoc} */ @Override public int getMaxVersion() { return 1; } /** * The Security command class is unique in that only some commands require encryption * (for all others, the security encapsulation requirement applies to the entire command class.) */ public static boolean doesCommandRequireSecurityEncapsulation(Byte commandByte) { return REQUIRED_ENCAPSULATION_LIST.contains(commandByte); } /** * {@inheritDoc} */ @Override public void handleApplicationCommandRequest(SerialMessage serialMessage, int offset, int endpoint) { byte command = (byte) serialMessage.getMessagePayloadByte(offset); traceHex("payload bytes for incoming security message", serialMessage.getMessagePayload()); lastReceivedMessageTimestamp = System.currentTimeMillis(); switch (command) { case SECURITY_NONCE_GET: // the Device wants to send us a Encrypted Packet, so we need to generate a nonce and send it lastNonceGetReceivedAt = System.currentTimeMillis(); sendNonceReport(); break; case SECURITY_NONCE_REPORT: lastDeviceNonceReceivedAt = System.currentTimeMillis(); // we received a NONCE from a device in response to our SECURITY_NONCE_GET or // SECURITY_MESSAGE_ENCAP_NONCE_GET // Nonce is messageBuf without the first offset +1 bytes byte[] messageBuf = serialMessage.getMessagePayload(); int startAt = offset + 1; int copyCount = messageBuf.length - startAt; byte[] nonceBytes = new byte[copyCount]; System.arraycopy(messageBuf, startAt, nonceBytes, 0, copyCount); debugHex("Received device nonce", nonceBytes); nonceGeneration.receivedNonceFromDevice(nonceBytes); // Notify the ZWaveSecurityEncapsulationThread since we received a nonce notifyEncapsulationThread(); return; case SECURITY_MESSAGE_ENCAP: // SECURITY_MESSAGE_ENCAP should be trapped and handled in {@link // ApplicationCommandMessageClass} case SECURITY_MESSAGE_ENCAP_NONCE_GET: // SECURITY_MESSAGE_ENCAP_NONCE_GET should be trapped and handled in // {@link ApplicationCommandMessageClass} case SECURITY_COMMANDS_SUPPORTED_REPORT:// Handled by ZWaveSecurityCommandClassInitialization case SECURITY_SCHEME_REPORT: // Handled by ZWaveSecurityCommandClassInitialization and is only received // during secure inclusion case SECURITY_NETWORK_KEY_VERIFY: // Handled by ZWaveSecurityCommandClassInitialization and is only received // during secure inclusion case SECURITY_NETWORK_KEY_SET: // Should NEVER be received since we are the controller case SECURITY_SCHEME_INHERIT: // Should NEVER be received as this is only used in a controller replication // environment (unsupported). logger.info("NODE {}: Received {} from node but we shouldn't have gotten it.", this.getNode().getNodeId(), commandToString(command)); return; default: logger.warn(String.format( "NODE %s: Unsupported Command 0x%02X for command class %s (0x%02X) for message %s.", this.getNode().getNodeId(), command, this.getCommandClass().getLabel(), this.getCommandClass().getKey(), serialMessage)); } } protected void processSecurityCommandsSupportedReport(SerialMessage serialMessage, int offset) { // This can be received during device inclusion or outside of it depending on // SEND_SECURITY_COMMANDS_SUPPORTED_GET_ON_STARTUP byte[] messagePayload = serialMessage.getMessagePayload(); int ourOffset = offset + 1; int size = messagePayload.length - ourOffset; byte[] secureClassBytes = new byte[size]; System.arraycopy(messagePayload, ourOffset, secureClassBytes, 0, size); traceHex("Supported Security Classes", secureClassBytes); getNode().setSecuredClasses(secureClassBytes); receivedSecurityCommandsSupportedReport = true; } public void sendNonceReport() { SerialMessage nonceReportMessage = nonceGeneration.generateAndBuildNonceReport(); if (nonceReportMessage == null) { logger.error("NODE {}: generateAndBuildNonceReport returned null"); } else { transmitMessage(nonceReportMessage); } } /** * Decrypts a security encapsulated message from the Z-Wave network. Ideally this would return * a {@link SerialMessage} but we don't have enough data to do so. So we just return the * decrypted payload bytes * * @param offset the offset at which the command byte exists * @param endpoint * @param messagePayload * @return the decrypted payload bytes. 0=command class, 1=command, 2+=payload */ public byte[] decryptMessage(byte[] data, int offset) { if (!checkRealNetworkKeyLoaded()) { return null; } traceHex("in decryptMessage starting at offset, buffer is", data, offset); ByteArrayInputStream bais = new ByteArrayInputStream(data); // check for minimum size here so we can ignore the return value of bais.read() below int minimumSize = offset + ENCAPSULATED_HEADER_LENGTH + ENCAPSULATED_FOOTER_LENGTH; if (data.length < minimumSize) { logger.error("NODE {}: Dropping security encapsulated packet which is too small: min={}, actual={}", this.getNode().getNodeId(), minimumSize, data.length); return null; } try { // advance to the command byte bais.read(new byte[offset]); byte command = (byte) bais.read(); byte[] initializationVector = new byte[IV_LENGTH]; // the next 8 bytes of packet are the nonce generated by the device for the IV bais.read(initializationVector, 0, HALF_OF_IV); traceHex("device nonce", initializationVector, 0, HALF_OF_IV); int ciphertextSize = data.length - offset - ENCAPSULATED_HEADER_LENGTH - ENCAPSULATED_FOOTER_LENGTH + 1; // Next are the ciphertext bytes byte[] ciphertextBytes = new byte[ciphertextSize]; bais.read(ciphertextBytes); logger.trace("NODE {}: Encrypted Packet Sizes: total={}, encrypted={}", this.getNode().getNodeId(), data.length, ciphertextSize); traceHex("ciphertextBytes", ciphertextBytes); // We stored the nonce that we sent to the device, retrieve it by the id so we can use it in the IV byte nonceId = (byte) bais.read(); Nonce nonceWeSentToDevice = nonceGeneration.getNonceWeGeneratedById(nonceId); if (nonceWeSentToDevice == null) { // probably expired // Error message logged in ZWaveSecureNonceTracker, just return return null; } System.arraycopy(nonceWeSentToDevice.getNonceBytes(), 0, initializationVector, HALF_OF_IV, HALF_OF_IV); traceHex("IV", initializationVector); byte[] macFromPacket = new byte[MAC_LENGTH]; bais.read(macFromPacket); Cipher cipher = Cipher.getInstance("AES/OFB/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, encryptKey, new IvParameterSpec(initializationVector)); byte[] plaintextBytes = cipher.doFinal(ciphertextBytes); traceHex("plaintextBytes", plaintextBytes); byte driverNodeId = (byte) this.getController().getOwnNodeId(); byte[] mac = generateMAC(command, ciphertextBytes, (byte) this.getNode().getNodeId(), driverNodeId, initializationVector); if (Arrays.equals(mac, macFromPacket)) { logger.trace("NODE {}: MAC Authentication of packet verified OK", this.getNode().getNodeId()); } else { logger.error("NODE {}: MAC Authentication of packet failed. dropping", this.getNode().getNodeId()); traceHex("full packet", data); traceHex("package mac", macFromPacket); traceHex("our mac", mac); if (DROP_PACKETS_ON_MAC_FAILURE) { return null; } else { logger.error("NODE {}: Just kidding, ignored failed MAC Authentication of packet", this.getNode().getNodeId()); } } byte sequenceDataByte = plaintextBytes[0]; if (sequenceDataByte != ZWaveSecurityPayloadFrame.SEQUENCE_BYTE_FOR_SINGLE_FRAME_MESSAGE) { // This is a multi frame message which is not yet supported logger.error( "NODE {}: Received multi frmae message which is not supported. Please post this to the OpenHab" + "mailing list so it can be fixed! bytes=", this.getNode().getNodeId(), SerialMessage.bb2hex(plaintextBytes)); return null; } // so we know if we got something that's not supported logger.debug("NODE {}: decrypted bytes {}", getNode().getNodeId(), SerialMessage.bb2hex(plaintextBytes)); if (lastEncapsulatedRequstMessage != null) { lastEncapsulatedRequstMessage.securityReponseReceived(plaintextBytes); } notifyEncapsulationThread(); return plaintextBytes; } catch (Exception e) { logger.error("NODE {}: Error decrypting packet", getNode().getNodeId(), e); return null; } } /** * Queues the given message for security encapsulation and transmission. * * Note that, per the z-wave spec, we don't just encrypt the message and send it. We need to first request a nonce * from the node, wait for that response, then encrypt and send. Therefore this message will be split into one or * more security frames, placed into a queue until the next nonce is received. Only then will it be encrypted and * sent. * * @param message * the unencrypted message to be transmitted */ public void queueMessageForEncapsulationAndTransmission(SerialMessage serialMessage) { checkInit(); if (serialMessage.getMessageBuffer().length < 7) { logger.error("NODE {}: Message too short for encapsulation, dropping message {}", this.getController().getNode(serialMessage.getMessageNode()).getNodeId(), serialMessage); return; } if (serialMessage.getMessageClass() != SerialMessageClass.SendData) { logger.error(String.format("NODE %d: Invalid message class %s (0x%02X) for sendData for message %s", getNode().getNodeId(), serialMessage.getMessageClass().getLabel(), serialMessage.getMessageClass().getKey(), serialMessage.toString())); } List<ZWaveSecurityPayloadFrame> securityPayloadFrameList = ZWaveSecurityPayloadFrame .convertToSecurityPayload(getNode(), serialMessage); logger.debug("NODE {}: Converted serial message {} to securityPayload(s): {}", getNode().getNodeId(), serialMessage, securityPayloadFrameList); if (!payloadEncapsulationQueue.isEmpty()) { // Clean up expired items and check for duplicate requests. This is necessary as // bombarding a device with messages will typically cause issues and it will stop // responding (seen during testing with Kwikset locks) Iterator<ZWaveSecurityPayloadFrame> iter = payloadEncapsulationQueue.iterator(); while (iter.hasNext()) { ZWaveSecurityPayloadFrame aFrameFromQueue = iter.next(); boolean shouldRemove = false; byte[] newMessageTwoBytes = new byte[2]; System.arraycopy(securityPayloadFrameList.get(0).getMessageBytes(), 0, newMessageTwoBytes, 0, 2); CommandClass newMessageCommandClass = CommandClass.getCommandClass(newMessageTwoBytes[0] & 0xff); // Expired frame check if (System.currentTimeMillis() > aFrameFromQueue.getExpirationTime()) { shouldRemove = true; logger.warn("NODE {}: Expired from payloadEncapsulationQueue: {}", getNode().getNodeId(), aFrameFromQueue); } else if (SIMILAR_FRAME_COMMAND_CLASS_LIST.contains(newMessageCommandClass)) { // Duplicate message check - if the queue already contains a message like this one, replace it // Compare the first 2 bytes (command class and operation) to do so byte[] aFrameFromQueueTwoBytes = new byte[2]; System.arraycopy(aFrameFromQueue.getMessageBytes(), 0, aFrameFromQueueTwoBytes, 0, 2); shouldRemove = Arrays.equals(newMessageTwoBytes, aFrameFromQueueTwoBytes); logger.debug("NODE {}: payloadEncapsulationQueue simliar frame check, shouldRemove={}: {} vs {}", getNode().getNodeId(), shouldRemove, SerialMessage.bb2hex(aFrameFromQueue.getMessageBytes()), SerialMessage.bb2hex(securityPayloadFrameList.get(0).getMessageBytes())); } if (shouldRemove) { removeFromEncapsulationQueue(aFrameFromQueue, iter, "Newer request received"); } } } // Finally, since we've cleanup duplicates and removed old entries, we can add the new frame{s} to our queue payloadEncapsulationQueue.addAll(securityPayloadFrameList); // Wake up the {@link ZWaveSecurityEncapsulationThread} so it can do what it needs to notifyEncapsulationThread(); } /** * Deletes the give frame using iter.remove(). Will automatically delete subsequent frames as needed * for multi-part messages * * @param frameToRemove the frame to remove, this is used only to check for multiple parts * @param iter the iterator which <b>must</b> point to frameToRemove * @param reason the reason it is being removed, will be put in the logs */ private void removeFromEncapsulationQueue(ZWaveSecurityPayloadFrame frameToRemove, Iterator<ZWaveSecurityPayloadFrame> iter, String reason) { logger.info("NODE {}: {} removing from payloadEncapsulationQueue: {}", getNode().getNodeId(), reason, frameToRemove); boolean hasMultipleParts = frameToRemove.getTotalParts() > 0; iter.remove(); if (hasMultipleParts) { if (!iter.hasNext()) { logger.warn("NODE {}: security payload frame was marked as having 2 parts, " + "but only found 1 in payloadEncapsulationQueue: {}", frameToRemove); } else { ZWaveSecurityPayloadFrame secondFrame = iter.next(); // Go to the 2nd part logger.info("NODE {}: Removing 2nd part from payloadEncapsulationQueue: {}", getNode().getNodeId(), secondFrame); iter.remove(); } } } /** * Gets the next message from {@link #payloadEncapsulationQueue}, encapsulates (encrypts and MACs) it, then * transmits * Invoked by {@link ZWaveSecurityEncapsulationThread}. This method must only be called by * {@link ZWaveSecurityEncapsulationThread} */ protected void sendNextMessageUsingDeviceNonce() { checkInit(); if (!checkRealNetworkKeyLoaded()) { return; } if (encryptKey == null) { // when loaded from xml, encrypt key will be null so we load it here setupNetworkKey(false); } if (payloadEncapsulationQueue.isEmpty()) { logger.warn("NODE {}: payloadQueue was empty, returning", this.getNode().getNodeId()); return; } Nonce deviceNonce = nonceGeneration.getUseableDeviceNonce(); if (deviceNonce == null) { SerialMessage nonceGetMessage = nonceGeneration.buildNonceGetIfNeeded(); if (nonceGetMessage == null) { // Nothing to do, we are already waiting for a nonce from the device } else { transmitMessage(nonceGetMessage); } return; } // Fetch the next payload from the queue and encapsulate it ZWaveSecurityPayloadFrame securityPayload = payloadEncapsulationQueue.poll(); if (securityPayload == null) { logger.warn("NODE {}: payloadQueue was empty, returning", this.getNode().getNodeId()); return; } // Encapsulate the message fragment traceHex("SecurityPayloadBytes", securityPayload.getMessageBytes()); // Note that we set the expected reply to that of the original message, as it can vary SecurityEncapsulatedSerialMessage message = new SecurityEncapsulatedSerialMessage(SerialMessageClass.SendData, SerialMessageType.Request, securityPayload.getOriginalMessage()); message.setDeviceNonceId(deviceNonce.getNonceBytes()[0]); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write((byte) this.getNode().getNodeId()); baos.write(securityPayload.getLength() + 20); baos.write(this.getCommandClass().getKey()); byte commandByte = SECURITY_MESSAGE_ENCAP; if (USE_SECURITY_MESSAGE_ENCAP_NONCE_GET && !disableEncapNonceGet) { boolean useNonceGetMessage = false; if (payloadEncapsulationQueue.size() > 0) { useNonceGetMessage = true; logger.debug("NODE {}: using SECURITY_MESSAGE_ENCAP_NONCE_GET with queue size of {}", this.getNode().getNodeId(), payloadEncapsulationQueue.size()); } else if (false) { // Check for messages that we know will have a follow-up request that is secure TODO: DB // change flag to AGGRESSIVE, etc? or just remove.. useNonceGetMessage = bytesAreEqual(securityPayload.getMessageBytes()[0], ZWaveCommandClass.CommandClass.DOOR_LOCK.getKey()) && bytesAreEqual(securityPayload.getMessageBytes()[1], ZWaveDoorLockCommandClass.DOORLOCK_SET); if (useNonceGetMessage) { logger.debug( "NODE {}: using SECURITY_MESSAGE_ENCAP_NONCE_GET since there will be a followup command", this.getNode().getNodeId()); } } if (useNonceGetMessage) { commandByte = SECURITY_MESSAGE_ENCAP_NONCE_GET; nonceGeneration.sendingEncapNonceGet(message); } } logger.trace("NODE {}: Used nonce to form {} ({}).", this.getNode().getNodeId(), commandToString(commandByte), securityPayload.getLogMessage()); baos.write(commandByte); // create the iv byte[] initializationVector = new byte[16]; // Generate a new nonce and fill the first half of the IV buffer with it byte[] nonceBytes = nonceGeneration.generateNonceForEncapsulationMessage(); System.arraycopy(nonceBytes, 0, initializationVector, 0, HALF_OF_IV); // the 2nd half of the IV is the nonce provided by the device System.arraycopy(deviceNonce.getNonceBytes(), 0, initializationVector, HALF_OF_IV, HALF_OF_IV); try { // Append the first 8 bytes of the IV (our nonce) to the message baos.write(initializationVector, 0, HALF_OF_IV); int totalParts = securityPayload.getTotalParts(); if (totalParts < 1 || totalParts > 2) { logger.error("NODE {}: securityPayload had invalid number of parts: {} Send aborted.", this.getNode().getNodeId(), totalParts); return; } // at most, the payload will be securityPayload length + 1 byte for the sequence byte byte[] plaintextMessageBytes = new byte[1 + securityPayload.getLength()]; plaintextMessageBytes[0] = securityPayload.getSequenceByte(); System.arraycopy(securityPayload.getMessageBytes(), 0, plaintextMessageBytes, 1, securityPayload.getLength()); // Append the message payload after encrypting it with AES-OFB traceHex("Input frame for encryption:", plaintextMessageBytes); traceHex("IV:", initializationVector); // This will use hardware AES acceleration when possible (default in JDK 8) Cipher encryptCipher = Cipher.getInstance("AES/OFB/NoPadding"); encryptCipher.init(Cipher.ENCRYPT_MODE, encryptKey, new IvParameterSpec(initializationVector)); byte[] ciphertextBytes = encryptCipher.doFinal(plaintextMessageBytes); traceHex("Encrypted Output", ciphertextBytes); baos.write(ciphertextBytes); // Append the nonce identifier which is the first byte of the device nonce baos.write(deviceNonce.getNonceBytes()[0]); int commandClassByteOffset = 2; int toMacLength = baos.toByteArray().length - commandClassByteOffset; // Start at command class byte byte[] toMac = new byte[toMacLength]; System.arraycopy(baos.toByteArray(), commandClassByteOffset, toMac, 0, toMacLength); // Generate the MAC byte sendingNode = (byte) this.getController().getOwnNodeId(); byte[] mac = generateMAC(commandByte, ciphertextBytes, sendingNode, (byte) getNode().getNodeId(), initializationVector); traceHex("Auth mac", mac); baos.write(mac); byte[] payload = baos.toByteArray(); debugHex( String.format("Outgoing encrypted message (device nonce=%02X): ", initializationVector[HALF_OF_IV]), payload); message.setMessagePayload(payload); message.setSecurityPayload(securityPayload); lastEncapsulatedRequstMessage = message; transmitMessage(message); } catch (GeneralSecurityException e) { logger.error("NODE {}: Error in sendNextMessageWithNonce, message not sent", e); } catch (IOException e) { logger.error("NODE {}: Error in sendNextMessageWithNonce, message not sent", e); } } /** * Checks the fields which are marked with XStreamOmitField as they will be null * upon deserialization from a file */ protected synchronized void checkInit() { if (nonceGeneration == null) { nonceGeneration = new ZWaveSecureNonceTracker(getNode()); } if (threadLock == null) { threadLock = new Object(); } if (payloadEncapsulationQueue == null) { payloadEncapsulationQueue = new ConcurrentLinkedQueue<ZWaveSecurityPayloadFrame>(); } } public void startSecurityEncapsulationThread() { if (encapsulationThread == null) { encapsulationThread = new ZWaveSecurityEncapsulationThread(); encapsulationThread.start(); } } // package visible for junit void setupNetworkKey(boolean useSchemeZero) { logger.info("NODE {}: setupNetworkKey useSchemeZero={}", this.getNode().getNodeId(), useSchemeZero); if (useSchemeZero) { logger.info("NODE {}: Using Scheme0 Network Key for Key Exchange since we are in inclusion mode.)", this.getNode().getNodeId()); // Scheme0 network key is a key of all zeros networkKey = new SecretKeySpec(new byte[16], AES); } else { if (!checkRealNetworkKeyLoaded()) { return; // Nothing we can do } // Use the real key logger.trace("NODE {}: Using Real Network Key.", this.getNode().getNodeId()); networkKey = realNetworkKey; } try { // Derived the message encryption key from the network key Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, networkKey); encryptKey = new SecretKeySpec(cipher.doFinal(DERIVE_ENCRYPT_KEY), AES); // Derived the message auth key from the network key cipher.init(Cipher.ENCRYPT_MODE, networkKey); authKey = new SecretKeySpec(cipher.doFinal(DERIVE_AUTH_KEY), AES); } catch (GeneralSecurityException e) { logger.error("NODE " + this.getNode().getNodeId() + ": Error building derived keys", e); keyException = e; } } /** * @return true if we are in the process of adding this node, ie the controller * and device are performing a secure pair */ protected boolean wasThisNodeJustIncluded() { ZWaveInclusionEvent lastInclusionEvent = getNode().getController().getLastIncludeSlaveFoundEvent(); boolean result = false; if (lastInclusionEvent != null && lastInclusionEvent.getEvent() == Type.IncludeSlaveFound && getNode().getNodeId() == lastInclusionEvent.getNodeId()) { // Check that this node was included very recently long twoMinutesAgoMs = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(2); result = lastInclusionEvent.getIncludedAt().getTime() > twoMinutesAgoMs; } logger.trace("NODE {}: lastInclusionEvent={} returning={}", this.getNode().getNodeId(), lastInclusionEvent, result); return result; } /** * Generate the MAC (message authentication code) from a security-encrypted message * * @throws GeneralSecurityException */ byte[] generateMAC(byte commandClass, byte[] ciphertext, byte sendingNode, byte receivingNode, byte[] iv) throws GeneralSecurityException { traceHex("generateMAC ciphertext", ciphertext); traceHex("generateMAC iv", iv); // Build a buffer containing a 4-byte header and the encrypted message data, padded with zeros to a 16-byte // boundary. int bufferSize = ciphertext.length + 4; // +4 to account for the header byte[] buffer = new byte[bufferSize]; byte[] tempAuth = new byte[16]; buffer[0] = commandClass; buffer[1] = sendingNode; buffer[2] = receivingNode; buffer[3] = (byte) ciphertext.length; System.arraycopy(ciphertext, 0, buffer, 4, ciphertext.length); traceHex("generateMAC NetworkKey", networkKey.getEncoded()); traceHex("generateMAC Raw Auth (minus IV)", buffer); // Encrypt the IV with ECB Cipher encryptCipher = Cipher.getInstance("AES/ECB/NoPadding"); encryptCipher.init(Cipher.ENCRYPT_MODE, authKey); tempAuth = encryptCipher.doFinal(iv); traceHex("generateMAC tmp1", tempAuth); // our temporary holding byte[] encpck = new byte[16]; int block = 0; // now xor the buffer with our encrypted IV for (int i = 0; i < bufferSize; i++) { encpck[block] = buffer[i]; block++; // if we hit a blocksize, then xor and encrypt if (block == 16) { for (int j = 0; j < 16; j++) { // here we do our xor tempAuth[j] = (byte) (encpck[j] ^ tempAuth[j]); encpck[j] = 0; } // reset encpck for good measure Arrays.fill(encpck, (byte) 0); // reset our block counter back to 0 block = 0; encryptCipher.init(Cipher.ENCRYPT_MODE, authKey); tempAuth = encryptCipher.doFinal(tempAuth); } } // any left over data that isn't a full block size if (block > 0) { for (int i = 0; i < 16; i++) { // encpck from block to 16 is already guaranteed to be 0 so its safe to xor it with out tempAuth tempAuth[i] = (byte) (encpck[i] ^ tempAuth[i]); } encryptCipher.init(Cipher.ENCRYPT_MODE, authKey); tempAuth = encryptCipher.doFinal(tempAuth); } // we only care about the first 8 bytes of tempAuth as the mac traceHex("generateMAC Computed Auth", tempAuth); byte[] mac = new byte[8]; System.arraycopy(tempAuth, 0, mac, 0, 8); return mac; } /** * Complex as in hard to understand what's going on * * @deprecated use {@link #generateMAC(byte, byte[], byte, byte, byte[]) instead */ @Deprecated byte[] generateMACComplex(byte[] data, int length, byte sendingNode, byte receivingNode, byte[] iv) throws GeneralSecurityException { traceHex("data", data); traceHex("iv", iv); // Build a buffer containing a 4-byte header and the encrypted message data, padded with zeros to a 16-byte // boundary. byte[] buffer = new byte[256]; byte[] tempAuth = new byte[16]; buffer[0] = data[0]; // Security command class command buffer[1] = sendingNode; buffer[2] = receivingNode; byte copyLength = (byte) (length - 19); // Subtract 19 to account for the 9 security command class bytes that // come before and after the encrypted data buffer[3] = copyLength; System.arraycopy(data, 9, buffer, 4, copyLength); // Copy the cipher bytes over int bufferSize = copyLength + 4; // +4 to account for the header above traceHex("Raw Auth (minus IV)", buffer); // Encrypt the IV with ECB Cipher encryptCipher = Cipher.getInstance("AES/ECB/NoPadding"); encryptCipher.init(Cipher.ENCRYPT_MODE, authKey); tempAuth = encryptCipher.doFinal(iv); // our temporary holding byte[] encpck = new byte[16]; int block = 0; // now xor the buffer with our encrypted IV for (int i = 0; i < bufferSize; i++) { encpck[block] = buffer[i]; block++; // if we hit a blocksize, then encrypt if (block == 16) { for (int j = 0; j < 16; j++) { // here we do our xor tempAuth[j] = (byte) (encpck[j] ^ tempAuth[j]); encpck[j] = 0; } // reset encpck for good measure Arrays.fill(encpck, (byte) 0); // reset our block counter back to 0 block = 0; encryptCipher.init(Cipher.ENCRYPT_MODE, authKey); tempAuth = encryptCipher.doFinal(tempAuth); traceHex("BAD tmp2", tempAuth); } } // any left over data that isn't a full block size if (block > 0) { for (int i = 0; i < 16; i++) { // encpck from block to 16 is already guaranteed to be 0 so its safe to xor it with out tmpmac tempAuth[i] = (byte) (encpck[i] ^ tempAuth[i]); } encryptCipher.init(Cipher.ENCRYPT_MODE, authKey); tempAuth = encryptCipher.doFinal(tempAuth); } /* we only care about the first 8 bytes of tmpauth as the mac */ traceHex("Computed Auth", tempAuth); byte[] mac = new byte[8]; System.arraycopy(tempAuth, 0, mac, 0, 8); return mac; } /** * Utility method to do unsigned byte comparison. This is necessary since in java all primitives are signed but * zwave we often represent values in hex (which is unsigned). * * @param aByte * a byte * @param anotherByte * an int * @return true if they are equal */ public static boolean bytesAreEqual(byte aByte, int anotherByte) { return aByte == ((byte) (anotherByte & 0xff)); } /** * Used to set the security key from the config file * * @param hexString a comma separated hex string, for example: (please DO NOT use this as your key!) * 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 */ public static void setRealNetworkKey(String hexString) { try { byte[] keyBytes = hexStringToByteArray(hexString); ZWaveSecurityCommandClass.realNetworkKey = new SecretKeySpec(keyBytes, "AES"); logger.info("Update networkKey"); ZWaveSecurityCommandClass.keyException = null; // we have a valid key } catch (IllegalArgumentException e) { logger.error("Error parsing zwave:networkKey", e); ZWaveSecurityCommandClass.keyException = e; } } protected void notifyEncapsulationThread() { long start = System.currentTimeMillis(); synchronized (threadLock) { threadLock.notify(); } long elapsed = System.currentTimeMillis() - start; if (elapsed > 500) { logger.warn("NODE {}: Took {}ms to get threadLock for notify", getNode().getNodeId(), elapsed); } } public static String commandToString(int command) { Byte theByte = Byte.valueOf((byte) (command & 0xff)); String result = COMMAND_LOOKUP_TABLE.get(theByte); if (result == null) { return "unknown"; } return result; } /** * Utility method to dump a byte array as hex. Will only print the data if debug * mode is debug logging is actually enabled. We don't use {@link SerialMessage#bb2hex(byte[])} * because we need our debug format to match that of OZW * * @param description * a human readable description of the data being logged * @param bytes * the bytes to convert to hex and log * @param offset * where to start from; zero means log the full byte array */ private void traceHex(String description, byte[] bytesParam, int offset, int length) { if (!logger.isTraceEnabled()) { return; } byte[] bytes = bytesParam; if (length < bytes.length) { bytes = new byte[length]; System.arraycopy(bytesParam, offset, bytes, 0, length); } logger.trace("NODE {}: {}={}", getNode().getNodeId(), description, SerialMessage.bb2hex(bytes)); } private void traceHex(String description, byte[] bytes, int offset) { traceHex(description, bytes, offset, bytes.length - offset); } private void debugHex(String description, byte[] bytes, int offset, int length) { if (!logger.isDebugEnabled()) { return; } StringBuilder buf = new StringBuilder(); for (int i = offset; i < offset + length; i++) { buf.append(String.format("%02x ", (bytes[i] & 0xff))); } String byteString = buf.toString().toUpperCase(); logger.debug("NODE {}: {}={}", getNode().getNodeId(), description, byteString); } private void debugHex(String description, byte[] bytes) { int offset = 0; debugHex(description, bytes, offset, bytes.length - offset); } /** * Utility method to dump a byte array as hex. Will only print the data if debug mode is debug logging is actually * enabled * * @param description * a human readable description of the data being logged * @param bytes * the bytes to convert to hex and log */ protected void traceHex(String description, byte[] messagePayload) { traceHex(description, messagePayload, 0, messagePayload.length); } public static byte[] hexStringToByteArray(String hexStringParam) { String hexString = hexStringParam.replace("0x", ""); hexString = hexString.replace(",", ""); hexString = hexString.replace(" ", ""); // from https://stackoverflow.com/questions/23354999/hex-string-to-byte-array-conversion if ((hexString.length() % 2) != 0) { throw new IllegalArgumentException("Input string must contain an even number of characters"); } byte result[] = new byte[hexString.length() / 2]; char enc[] = hexString.toCharArray(); for (int i = 0; i < enc.length; i += 2) { StringBuilder curr = new StringBuilder(2); curr.append(enc[i]).append(enc[i + 1]); result[i / 2] = (byte) Integer.parseInt(curr.toString(), 16); } return result; } public static boolean isSecurityNonceReportMessage(SerialMessage serialMessage) { if (serialMessage == null) { return false; } if (serialMessage.getMessagePayload() == null || serialMessage.getMessagePayload().length < 3) { return false; } byte[] payloadBytes = serialMessage.getMessagePayload(); boolean result = (payloadBytes[2] == ((byte) CommandClass.SECURITY.getKey()) && payloadBytes[3] == SECURITY_NONCE_REPORT); if (result) { logger.trace("NODE {}: found Security NonceReportMessage={} payloadBytes={}", serialMessage.getMessageNode(), result, SerialMessage.bb2hex(payloadBytes)); } return result; } /** * Security encapsulation thread. This waits for 1) a device nonce to arrive * and 2) the last transaction to be completed. It will then use the device * nonce to to security encapsulate the next message in {@link ZWaveSecurityCommandClass#payloadEncapsulationQueue} * and give it to the controller for sending * * @author Dave Badia * @since TODO */ private class ZWaveSecurityEncapsulationThread extends Thread { /** * The default time we will wait to receive a response (or if no response will be sent at all) */ private static final long DEFAULT_WAIT_FOR_RESPONSE = 10000; /** * If we get a {@link ZWaveSecurityCommandClass#SECURITY_NONCE_GET}, then we know the node * is going to send us a security encapsulated response message. Wait additional time to receive that */ private static final long NONCE_GET_ADDON = 20000; private ZWaveSecurityEncapsulationThread() { super("ZWaveSecurityEncapsulationThreadForNode" + getNode().getNodeId()); } @Override public void run() { logger.debug("NODE {}: Starting Z-Wave thread: security encapsulation", getNode().getNodeId()); while (true) { try { boolean transmitNext = lastEncapsulatedRequstMessage == null; if (!transmitNext && lastEncapsulatedRequstMessage.hasBeenTransmitted()) { // Recompute the timeout each time long timeOutAt = lastEncapsulatedRequstMessage.getTransmittedAt() + DEFAULT_WAIT_FOR_RESPONSE; boolean expectingResponseMessage = lastNonceGetReceivedAt > lastEncapsulatedRequstMessage .getTransmittedAt(); if (expectingResponseMessage) { timeOutAt += NONCE_GET_ADDON; } // See if we have reached the timeout yet if (System.currentTimeMillis() > timeOutAt) { if (expectingResponseMessage) { logger.error("NODE {}: Timed out waiting on response for encapsulated message {}", getNode().getNodeId(), lastEncapsulatedRequstMessage); } else { logger.debug("NODE {}: no response expected for security transaction {}", getNode().getNodeId(), lastEncapsulatedRequstMessage); } // SECURITY_MESSAGE_ENCAP_NONCE_GET doesn't always work. If it's not working with a device, // we disable it. Check here to see if it's working logger.debug("NODE {}: TODO DB: remove checking for SECURITY_MESSAGE_ENCAP_NONCE_GET", getNode().getNodeId()); if (bytesAreEqual(lastEncapsulatedRequstMessage.getMessagePayload()[3], SECURITY_MESSAGE_ENCAP_NONCE_GET)) { logger.debug("NODE {}: SECURITY_MESSAGE_ENCAP_NONCE_GET are equal", getNode().getNodeId()); if (lastDeviceNonceReceivedAt < lastEncapsulatedRequstMessage.getTransmittedAt()) { // The last NONCE_REPORT was received before we sent // SECURITY_MESSAGE_ENCAP_NONCE_GET // so SECURITY_MESSAGE_ENCAP_NONCE_GET isn't working, disable it disableEncapNonceGet = true; logger.error("NODE {}: SECURITY_MESSAGE_ENCAP_NONCE_GET disabled", getNode().getNodeId()); // Save the setting so we remember new ZWaveNodeSerializer().SerializeNode(getNode()); } } lastEncapsulatedRequstMessage = null; transmitNext = true; } } if (transmitNext && !payloadEncapsulationQueue.isEmpty()) { sendNextMessageUsingDeviceNonce(); } synchronized (threadLock) { threadLock.wait(1000); } } catch (InterruptedException e) { continue; } catch (Exception e) { logger.error("NODE {}: Exception during Z-Wave thread: security encapsulation", getNode().getNodeId(), e); } } } } }