/** * Copyright (c) 2010-2016 by the respective copyright holders. * * 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.satel.internal.protocol; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.util.Random; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Represents Satel ETHM-1 module. Implements method required to connect and * communicate with that module over TCP/IP protocol. The module must have * integration protocol enable in DLOADX configuration options. * * @author Krzysztof Goworek * @since 1.7.0 */ public class Ethm1Module extends SatelModule { private static final Logger logger = LoggerFactory.getLogger(Ethm1Module.class); private String host; private int port; private String encryptionKey; /** * Creates new instance with host, port, timeout and encryption key set to * specified values. * * @param host * host name or IP of ETHM-1 module * @param port * TCP port the module listens on * @param timeout * timeout value in milliseconds for connect/read/write * operations * @param encryptionKey * encryption key for encrypted communication */ public Ethm1Module(String host, int port, int timeout, String encryptionKey) { super(timeout); this.host = host; this.port = port; this.encryptionKey = encryptionKey; } @Override protected CommunicationChannel connect() { logger.info("Connecting to ETHM-1 module at {}:{}", this.host, this.port); try { Socket socket = new Socket(); socket.connect(new InetSocketAddress(this.host, this.port), this.getTimeout()); logger.info("ETHM-1 module connected successfully"); if (StringUtils.isBlank(this.encryptionKey)) { return new TCPCommunicationChannel(socket); } else { return new EncryptedCommunicationChannel(socket, this.encryptionKey); } } catch (IOException e) { logger.error("IO error occurred during connecting socket", e); } return null; } private class TCPCommunicationChannel implements CommunicationChannel { private Socket socket; public TCPCommunicationChannel(Socket socket) { this.socket = socket; } @Override public InputStream getInputStream() throws IOException { return this.socket.getInputStream(); } @Override public OutputStream getOutputStream() throws IOException { return this.socket.getOutputStream(); } @Override public void disconnect() { logger.info("Closing connection to ETHM-1 module"); try { this.socket.close(); } catch (IOException e) { logger.error("IO error occurred during closing socket", e); } } } private class EncryptedCommunicationChannel extends TCPCommunicationChannel { private EncryptionHelper aesHelper; private Random rand; private byte id_s; private byte id_r; private int rollingCounter; private InputStream inputStream; private OutputStream outputStream; public EncryptedCommunicationChannel(final Socket socket, String encryptionKey) throws IOException { super(socket); try { this.aesHelper = new EncryptionHelper(encryptionKey); } catch (Exception e) { throw new IOException("General encryption failure", e); } this.rand = new Random(); this.id_s = 0; this.id_r = 0; this.rollingCounter = 0; this.inputStream = new InputStream() { private ByteArrayInputStream inputBuffer = null; @Override public int read() throws IOException { if (inputBuffer == null || inputBuffer.available() == 0) { // read message and decrypt it byte[] data = readMessage(socket.getInputStream()); // create new buffer inputBuffer = new ByteArrayInputStream(data, 6, data.length - 6); } return inputBuffer.read(); } }; this.outputStream = new OutputStream() { private ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(256); @Override public void write(int b) throws IOException { outputBuffer.write(b); } @Override public void flush() throws IOException { writeMessage(outputBuffer.toByteArray(), socket.getOutputStream()); outputBuffer.reset(); } }; } @Override public InputStream getInputStream() throws IOException { return this.inputStream; } @Override public OutputStream getOutputStream() throws IOException { return this.outputStream; } private synchronized byte[] readMessage(InputStream is) throws IOException { logger.trace("Receiving data from ETHM-1"); // read number of bytes int bytesCount = is.read(); logger.trace("Read count of bytes: {}", bytesCount); if (bytesCount == -1) { throw new IOException("End of input stream reached"); } byte[] data = new byte[bytesCount]; // read encrypted data int bytesRead = is.read(data); if (bytesCount != bytesRead) { throw new IOException( String.format("Too few bytes read. Read: %d, expected: %d", bytesRead, bytesCount)); } // decrypt data logger.trace("Decrypting data: {}", bytesToHex(data)); try { this.aesHelper.decrypt(data); } catch (Exception e) { throw new IOException("Decryption exception", e); } logger.debug("Decrypted data: {}", bytesToHex(data)); // validate message this.id_r = data[4]; if (this.id_s != data[5]) { throw new IOException(String.format("Invalid 'id_s' value. Got: %d, expected: %d", data[5], this.id_s)); } return data; } private synchronized void writeMessage(byte[] message, OutputStream os) throws IOException { // prepare data for encryption int bytesCount = 6 + message.length; if (bytesCount < 16) { bytesCount = 16; } byte[] data = new byte[bytesCount]; int randomValue = this.rand.nextInt(); data[0] = (byte) (randomValue >> 8); data[1] = (byte) (randomValue & 0xff); data[2] = (byte) (this.rollingCounter >> 8); data[3] = (byte) (this.rollingCounter & 0xff); data[4] = this.id_s = (byte) this.rand.nextInt(); data[5] = this.id_r; ++this.rollingCounter; System.arraycopy(message, 0, data, 6, message.length); // encrypt data logger.debug("Encrypting data: {}", bytesToHex(data)); try { this.aesHelper.encrypt(data); } catch (Exception e) { throw new IOException("Encryption exception", e); } logger.trace("Encrypted data: {}", bytesToHex(data)); // write encrypted data to output stream os.write(bytesCount); os.write(data); os.flush(); } } private static String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (int i = 0; i < bytes.length; ++i) { if (i > 0) { result.append(" "); } result.append(String.format("%02X", bytes[i])); } return result.toString(); } }