package org.openhab.binding.zwave.internal.protocol.commandclass;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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.SerialMessageType;
import org.openhab.binding.zwave.internal.protocol.ZWaveController;
import org.openhab.binding.zwave.internal.protocol.ZWaveEndpoint;
import org.openhab.binding.zwave.internal.protocol.ZWaveEventListener;
import org.openhab.binding.zwave.internal.protocol.ZWaveNode;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveEvent;
import org.openhab.binding.zwave.internal.protocol.event.ZWaveTransactionCompletedEvent;
import org.openhab.binding.zwave.internal.protocol.initialization.ZWaveNodeStageAdvancer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamOmitField;
/**
* Handles the secure pairing portion and initialization of the Security command class.
* See {@link #initialize(boolean)} for a lot of details about
* how the secure pairing process is inherently different from the other initialization process
*
*/
@XStreamAlias("securityCommandClassWithInit")
public class ZWaveSecurityCommandClassWithInitialization extends ZWaveSecurityCommandClass
implements ZWaveCommandClassInitialization, ZWaveEventListener {
private static final Logger logger = LoggerFactory.getLogger(ZWaveSecurityCommandClassWithInitialization.class);
/**
* the scheme that is used prior to any keys being negotiated
*/
private static final byte SECURITY_SCHEME_ZERO = 0x00;
/**
* Only non-null when we are including a new node
*/
@XStreamOmitField
private volatile ZWaveSecureInclusionStateTracker inclusionStateTracker = null;
/**
* The last {@link SerialMessage} that was given to {@link ZWaveNodeStageAdvancer}
* when it called {@link ZWaveSecurityCommandClass#initialize(boolean)}. Used
* in cases where we need to resend the last message (transmission failure, etc)
*/
@XStreamOmitField
private SerialMessage lastRequestSecurePairMessage = null;
private static final String SECURE_INCLUSION_FAILED_MESSAGE = "Secure Inclusion FAILED.";
/**
* Timer that tracks how long we should wait for a response. {@link ZWaveNodeStageAdvancer}
* already has a timer, but since the initialization of this class involves multiple security
* messages, we cannot rely on that to re-send the last message. So, we keep our own timer
* to know when it's time to retry a message
*/
@XStreamOmitField
private long waitForReplyTimeout = Long.MAX_VALUE;
@XStreamOmitField
private long inclusionStartedAt = Long.MIN_VALUE;
/**
* Flag so we understand that the secure pairing process was completed at some point in time
*/
protected boolean securePairingComplete = false;
public ZWaveSecurityCommandClassWithInitialization(ZWaveNode node, ZWaveController controller,
ZWaveEndpoint endpoint) {
super(node, controller, endpoint);
controller.addEventListener(this);
}
private boolean isSecureInclusionInProgress() {
return inclusionStateTracker != null;
}
/**
* There are 2 different ways we need to transmit messages:
* 1) during inclusion mode, our {@link #initialize(boolean)} method will return the next message to send (handled
* below)
* 2) during normal (non-inclusion) mode, give the message to {@link ZWaveController} (handled by the superclass)
*/
@Override
protected void transmitMessage(SerialMessage message) {
if (isSecureInclusionInProgress() && message instanceof SecurityEncapsulatedSerialMessage
&& ((SecurityEncapsulatedSerialMessage) message).getSecurityPayload() != null) {
ZWaveSecurityPayloadFrame securityPayload = ((SecurityEncapsulatedSerialMessage) message)
.getSecurityPayload();
// if the message we just created is SECURITY_NETWORK_KEY_SET, then we need to change our Network Key
// to use the real key, as the reply we will get back will be encrypted with the real Network key
if (bytesAreEqual(securityPayload.getMessageBytes()[0], ZWaveCommandClass.CommandClass.SECURITY.getKey())
&& bytesAreEqual(securityPayload.getMessageBytes()[1], SECURITY_NETWORK_KEY_SET)) {
logger.info("NODE {}: Setting Network Key to real key after SECURITY_NETWORK_KEY_SET",
this.getNode().getNodeId());
setupNetworkKey(false);
}
// We are in inclusion mode, so give the message to the tracker so it will be picked
// up when ZWaveNodeStageAdvancer calls our initialize method
inclusionStateTracker.setNextRequest(message);
} else {
// Normal (non-inclusion mode) so give the message to the controller to be transmitted
super.transmitMessage(message);
}
}
/**
* During inclusion, {@link ZWaveSecurityCommandClass#ZWaveSecurityEncapsulationThread} is not running
* so we override this logic and just have the calling thread (typically ZWaveInputThread) execute the
* security encapsulation logic
*/
@Override
protected void notifyEncapsulationThread() {
if (isSecureInclusionInProgress()) {
sendNextMessageUsingDeviceNonce();
} else {
// Normal (non-inclusion mode)
super.notifyEncapsulationThread();
}
}
/**
* {@inheritDoc}
*/
@Override
public void handleApplicationCommandRequest(SerialMessage serialMessage, int offset, int endpoint) {
byte command = (byte) serialMessage.getMessagePayloadByte(offset);
if (logger.isDebugEnabled()) {
logger.debug(String.format("NODE %s: Received Security Message 0x%02X %s ", this.getNode().getNodeId(),
command, commandToString(command)));
}
traceHex("payload bytes for incoming security message", serialMessage.getMessagePayload());
lastReceivedMessageTimestamp = System.currentTimeMillis();
if (inclusionStateTracker != null && !inclusionStateTracker.verifyAndAdvanceState(command)) {
// bad order, abort
return;
}
switch (command) {
case SECURITY_SCHEME_REPORT:
// Should be received during inclusion only
if (!wasThisNodeJustIncluded() || inclusionStateTracker == null) {
logger.error("NODE {}: Received SECURITY_SCHEME_REPORT but we are not in inclusion mode! {}",
serialMessage);
return;
}
int schemes = serialMessage.getMessagePayloadByte(offset + 1);
logger.debug("NODE {}: Received Security Scheme Report: ", this.getNode().getNodeId(), schemes);
if (schemes == SECURITY_SCHEME_ZERO) {
// Since we've agreed on a scheme for which to exchange our key, we now send our NetworkKey to the
// device
logger.debug("NODE {}: Security scheme agreed.", this.getNode().getNodeId());
// create the NetworkKey Packet
SerialMessage networkKeyMessage = new SerialMessage(this.getNode().getNodeId(),
SerialMessageClass.SendData, SerialMessageType.Request,
SerialMessageClass.ApplicationCommandHandler, SECURITY_MESSAGE_PRIORITY);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write((byte) this.getNode().getNodeId());
baos.write(18);
baos.write((byte) getCommandClass().getKey());
baos.write(SECURITY_NETWORK_KEY_SET);
try {
baos.write(realNetworkKey.getEncoded());
networkKeyMessage.setMessagePayload(baos.toByteArray());
// We can't set SECURITY_NETWORK_KEY_SET in inclusionStateTracker because we need to do a
// NONCE_GET before sending. So put this in our encrypt send queue
// and give inclusionStateTracker/ZWaveNodeStageAdvancer the NONCE_GET
queueMessageForEncapsulationAndTransmission(networkKeyMessage);
if (!inclusionStateTracker.verifyAndAdvanceState(SECURITY_NETWORK_KEY_SET)) {
return;
}
SerialMessage message = nonceGeneration.buildNonceGetIfNeeded();
// Since we are in init mode, message should always != null
if (message != null) {
// TODO: DB is this true?: logger.error("NODE {}: "+SECURE_INCLUSION_FAILED_MESSAGE+" In
// inclusion mode but buildNonceGetIfNeeded returned null, this may result in a deadlock");
inclusionStateTracker.setNextRequest(message); // Let ZWaveNodeStageAdvancer come get the
// NONCE_GET
}
} catch (IOException e) {
logger.error("NODE {}: IOException trying to write SECURITY_NETWORK_KEY_SET, aborted", e);
}
} else {
// No common security scheme. This really shouldn't happen
inclusionStateTracker.setErrorState("TODO: Security scheme " + schemes + " is not supported");
logger.error("NODE {}: " + SECURE_INCLUSION_FAILED_MESSAGE
+ " No common security scheme. The device will continue as an unsecured node. "
+ "Scheme requested was {}", this.getNode().getNodeId(), schemes);
}
return;
case SECURITY_NETWORK_KEY_VERIFY:
// Should be received during inclusion only
if (!wasThisNodeJustIncluded() || inclusionStateTracker == null) {
logger.error("NODE {}: Received SECURITY_NETWORK_KEY_VERIFY but we are not in inclusion mode! {}",
serialMessage);
return;
}
// Since we got here, it means we decrypted a packet using the key we sent in
// the SECURITY_NETWORK_KEY_SET message and the new key is in use by both sides.
// Next step is to send SECURITY_COMMANDS_SUPPORTED_GET
if (SEND_SECURITY_COMMANDS_SUPPORTED_GET_ON_STARTUP) {
securePairingComplete = true;
}
SerialMessage supportedGetMessage = new SerialMessage(this.getNode().getNodeId(),
SerialMessageClass.SendData, SerialMessageType.Request,
SerialMessageClass.ApplicationCommandHandler, SECURITY_MESSAGE_PRIORITY);
byte[] payload = { (byte) this.getNode().getNodeId(), 2, (byte) getCommandClass().getKey(),
SECURITY_COMMANDS_SUPPORTED_GET, };
supportedGetMessage.setMessagePayload(payload);
inclusionStateTracker.verifyAndAdvanceState(SECURITY_COMMANDS_SUPPORTED_GET);
SerialMessage nonceGetMessage = nonceGeneration.buildNonceGetIfNeeded();
// Since we are in init mode, message should always != null
if (nonceGetMessage == null) {
inclusionStateTracker.setErrorState(SECURE_INCLUSION_FAILED_MESSAGE
+ " In inclusion mode but buildNonceGetIfNeeded returned null,"
+ " this may result in a deadlock");
return;
}
inclusionStateTracker.setNextRequest(nonceGetMessage); // Let ZWaveNodeStageAdvancer come get it
// We can't set SECURITY_COMMANDS_SUPPORTED_GET in inclusionStateTracker because we need to do a
// NONCE_GET before sending. So put this in our encrypt send queue
// and give inclusionStateTracker/ZWaveNodeStageAdvancer the NONCE_GET
queueMessageForEncapsulationAndTransmission(supportedGetMessage);
return;
case SECURITY_COMMANDS_SUPPORTED_REPORT:
processSecurityCommandsSupportedReport(serialMessage, offset);
// This can be received during device inclusion or outside of it
if (inclusionStateTracker != null) {
// We're done with all of our NodeStage#SECURITY_REPORT stuff, set inclusionStateTracker to null
inclusionStateTracker = null;
}
return;
case SECURITY_NONCE_GET: // SECURITY_NONCE_GET is handled by superclass
case SECURITY_NONCE_REPORT: // SECURITY_NONCE_GET is handled by superclass
super.handleApplicationCommandRequest(serialMessage, offset, endpoint);
return;
case SECURITY_NETWORK_KEY_SET: // we shouldn't get a NetworkKeySet from a node if we are the controller as
// we send it out to the Devices
case SECURITY_MESSAGE_ENCAP: // SECURITY_MESSAGE_ENCAP should be caught and handled in {@link
// ApplicationCommandMessageClass}
case SECURITY_MESSAGE_ENCAP_NONCE_GET: // SECURITY_MESSAGE_ENCAP_NONCE_GET should be caught and handled in
// {@link ApplicationCommandMessageClass}
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));
}
}
// TODO: DB remove
private static boolean USE_DELAY_FOR_SCHEME_GET = false;
/**
* {@inheritDoc}
*
* This code is only executed during secure inclusion.
*
* ZWaveNodeStageAdvancer calls us for one of the following reasons:
* 1. It's checking for the next message to be sent
* 2. the ZWaveNodeStageAdvancer retry timer was triggered
* <p/>
* During node inclusion we have to exchange many message with the device to setup
* security encapsulation.
* <p/>
* Ideally, we would create all necessary messages the very first time this method
* is called and return the collection. But that is not achievable due to the following:
* 1. Some messages depend on the result of previous responses.
* 2. In order to send a security encapsulated message, we need to send a {@link #SECURITY_NONCE_GET},
* wait for the {@link #SECURITY_NONCE_REPORT} and use that data to build the message. Theoretically
* we could send many of these at once and get the replies, but they are valid for as little as 3
* seconds so they would expire before we the message that used the nonce would ever reach the device.
* <p/>
* Since we can't create all messages at once, we create a helper {@link ZWaveSecureInclusionStateTracker}
* which keeps track of where we are at in the flow and hold the next message to be sent. For security
* reasons, it's also critical to track that the steps are executing in the proper order.
* <p/>
* Adding even more complexity, this method is frequently invoked by {@link ZWaveController.ZWaveInputThread} which
* means
* that as long as the thread is here, we will not process any incoming messages such as
* {@link #SECURITY_NONCE_REPORT}.
* To avoid blocking the thread, we return an empty collection to indicate that we are still waiting for a response
* message.
* <p/>
* This method is nasty but I've already spent hours trying to refactor it into readable code but have obviously
* failed.
* <p/>
*
* @return One or more {@link SerialMessage} to be sent OR a zero length collection if we are still waiting for a
* response OR
* null if the secure pairing process has completed or failed
*
* @see {@link ZWaveNodeStageAdvancer}
*/
@Override
public Collection<SerialMessage> initialize(boolean firstIteration) {
// ZWaveNodeStageAdvancer calls us for one of the following reasons:
// 1. It's checking for the next message to be sent
// 2. the ZWaveNodeStageAdvancer retry timer was triggered
boolean wasThisNodeJustIncluded = wasThisNodeJustIncluded();
checkInit();
logger.debug(
"NODE {}: call from NodeAdvancer initialize, firstIteration={}, wasThisNodeJustIncluded={}, keyVerifyReceived={}, "
+ "lastReceivedMessage={}ms ago, lastSentMessage={}ms ago",
this.getNode().getNodeId(), firstIteration, wasThisNodeJustIncluded, securePairingComplete,
(System.currentTimeMillis() - lastReceivedMessageTimestamp),
(System.currentTimeMillis() - lastSentMessageTimestamp));
if (wasThisNodeJustIncluded) {
List<SerialMessage> inclusionMessageReturnList = null;
if (firstIteration && !securePairingComplete) {
inclusionStartedAt = System.currentTimeMillis();
// if we are adding this node, then send SECURITY_SCHEME_GET which will start the Network Key Exchange
setupNetworkKey(true);
inclusionStateTracker = new ZWaveSecureInclusionStateTracker(getNode());
inclusionStateTracker.resetWaitForReplyTimeout();
// Need to start things off by sending SECURITY_SCHEME_GET
SerialMessage message = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData,
SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler,
SECURITY_MESSAGE_PRIORITY);
byte[] payload = { (byte) this.getNode().getNodeId(), 3, (byte) getCommandClass().getKey(),
SECURITY_SCHEME_GET, 0 };
message.attempts = 1; // retry only once
message.setMessagePayload(payload);
// SchemeGet is unencrypted, hand it back or set it on inclusionStateTracker
if (USE_DELAY_FOR_SCHEME_GET) {
inclusionStateTracker.setNextRequest(message);
inclusionMessageReturnList = Collections.emptyList();
} else {
inclusionMessageReturnList = Collections.singletonList(message);
}
} else if (receivedSecurityCommandsSupportedReport) { // We're done!
securePairingComplete = true;
inclusionMessageReturnList = null; // Tell ZWaveNodeStageAdvancer to advance to the next stage
} else { // Normal inclusion flow, get the next message or wait for a response to the current one
SerialMessage nextMessage = null;
if (USE_DELAY_FOR_SCHEME_GET) {
boolean timerUp = System.currentTimeMillis() > (inclusionStartedAt + 5000);
logger.debug("NODE {}: USE_DELAY_FOR_SCHEME_GET active, timerUp={}", this.getNode().getNodeId(),
timerUp);
if (timerUp) {
nextMessage = inclusionStateTracker.getNextRequest();
}
} else {
nextMessage = inclusionStateTracker.getNextRequest();
}
logger.debug(
"NODE {}: call from NodeAdvancer initialize, inclusion flow, get the next message or wait for a response to the current one, nextMessage={}",
this.getNode().getNodeId(), nextMessage);
if (nextMessage == null) { // There is an outstanding request or a timeout error occured
if (securePairingComplete) {
inclusionStateTracker = null;
return null; // all done
} else { // !securePairingComplete
inclusionStateTracker.resetWaitForReplyTimeout();
if (inclusionStateTracker.getErrorState() != null) { // Check for errors
logger.error("NODE {}: " + SECURE_INCLUSION_FAILED_MESSAGE + " at step {}: {}",
this.getNode().getNodeId(), commandToString(inclusionStateTracker.getCurrentStep()),
inclusionStateTracker.getErrorState());
inclusionStateTracker = null;
return null; // We're done but are in a failure state
} else {
// Keep waiting for a response
inclusionMessageReturnList = Collections.emptyList();
}
} // END securePairingComplete
} else { // nextMessage != null: There is no outstanding request and we have another message to send
// Send the next request
inclusionMessageReturnList = Collections.singletonList(nextMessage);
} // END There is an outstanding request
} // END else Normal inclusion flow, get the next message or wait for a response to the current one
if (inclusionMessageReturnList != null && inclusionMessageReturnList.size() > 0) {
lastRequestSecurePairMessage = inclusionMessageReturnList.get(0);
}
logger.debug("NODE {}: call from NodeAdvancer initialize, just included, handing back message={}",
this.getNode().getNodeId(),
inclusionMessageReturnList == null ? "null" : inclusionMessageReturnList);
return inclusionMessageReturnList;
// END wasThisNodeJustIncluded
} else { // Our node was NOT just included
List<SerialMessage> returnMessageList = null;
if (!securePairingComplete) {
logger.error(
"NODE {}: Invalid state! secure inclusion has not completed and we are not in inclusion mode, aborting",
this.getNode().getNodeId());
returnMessageList = null;
} else if (firstIteration) { // request the current list of security commands as a sanity check
// The node was initialized previously and we are connecting to it after an openhab restart
if (!SEND_SECURITY_COMMANDS_SUPPORTED_GET_ON_STARTUP) {
return null; // nothing to do
}
SerialMessage message = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData,
SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler,
SECURITY_MESSAGE_PRIORITY);
byte[] payload = { (byte) this.getNode().getNodeId(), 2, (byte) getCommandClass().getKey(),
SECURITY_COMMANDS_SUPPORTED_GET, };
message.setMessagePayload(payload);
SerialMessage nonceGetMessage = nonceGeneration.buildNonceGetIfNeeded();
// We can't return SECURITY_COMMANDS_SUPPORTED_GET because we need to do a
// NONCE_GET before sending. So put this in our encrypt send queue
// and give ZWaveNodeStageAdvancer the NONCE_GET
queueMessageForEncapsulationAndTransmission(message);
returnMessageList = Collections.singletonList(nonceGetMessage);
} else if (receivedSecurityCommandsSupportedReport) {
returnMessageList = null; // Normal flow, nothing else to do, tell ZWaveNodeStageAdvancer to advance to
// the next stage
} else if (System.currentTimeMillis() > waitForReplyTimeout) {
logger.error("NODE {}: Got no response to SECURITY_COMMANDS_SUPPORTED on init, using old",
this.getNode().getNodeId());
returnMessageList = null; // Tell ZWaveNodeStageAdvancer to advance to the next stage
} else {
// the request was already sent, wait for the nonce exchange and the reply to come
returnMessageList = Collections.emptyList();
}
logger.debug("NODE {}: call from NodeAdvancer initialize, from xml, handing back message={}",
this.getNode().getNodeId(), returnMessageList);
if (returnMessageList != null && returnMessageList.size() > 0) {
lastRequestSecurePairMessage = returnMessageList.get(0);
}
return returnMessageList;
} // end if wasThisNodeJustIncluded
}
@Override
public void ZWaveIncomingEvent(ZWaveEvent event) {
if (event instanceof ZWaveTransactionCompletedEvent && event.getNodeId() == getNode().getNodeId()) {
logger.trace("NODE {}: updating lasSentMessageTimestamp", this.getNode().getNodeId());
lastSentMessageTimestamp = System.currentTimeMillis();
}
}
@Override
protected void checkInit() {
super.checkInit();
}
@Override
boolean checkRealNetworkKeyLoaded() {
if (realNetworkKey == null) {
String errorMessage = "NODE " + this.getNode()
+ ": Trying to perform secure operation but Network key is NOT set due to: ";
if (keyException != null) {
errorMessage += keyException.getMessage();
}
logger.error(errorMessage, keyException);
if (inclusionStateTracker != null) {
inclusionStateTracker.setErrorState(errorMessage);
}
return false;
}
return true;
}
public boolean wasSecureInclusionSuccessful() {
return securePairingComplete;
}
}