]> git.basschouten.com Git - openhab-addons.git/blob
52a8f2dda3b93ea4ab83f3a610373ad586df0d47
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.satel.internal.protocol;
14
15 import java.io.ByteArrayInputStream;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.net.InetSocketAddress;
21 import java.net.Socket;
22 import java.net.SocketTimeoutException;
23 import java.util.Random;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.openhab.core.util.HexUtils;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29
30 /**
31  * Represents Satel ETHM-1 module. Implements method required to connect and
32  * communicate with that module over TCP/IP protocol. The module must have
33  * integration protocol enable in DLOADX configuration options.
34  *
35  * @author Krzysztof Goworek - Initial contribution
36  */
37 @NonNullByDefault
38 public class Ethm1Module extends SatelModule {
39
40     private static final ByteArrayInputStream EMPTY_INPUT_STREAM = new ByteArrayInputStream(new byte[0]);
41
42     private final Logger logger = LoggerFactory.getLogger(Ethm1Module.class);
43
44     private final String host;
45     private final int port;
46     private final String encryptionKey;
47
48     /**
49      * Creates new instance with host, port, timeout and encryption key set to
50      * specified values.
51      *
52      * @param host host name or IP of ETHM-1 module
53      * @param port TCP port the module listens on
54      * @param timeout timeout value in milliseconds for connect/read/write operations
55      * @param encryptionKey encryption key for encrypted communication
56      * @param extPayloadSupport if <code>true</code>, the module supports extended command payload for reading
57      *            INTEGRA 256 state
58      */
59     public Ethm1Module(String host, int port, int timeout, String encryptionKey, boolean extPayloadSupport) {
60         super(timeout, extPayloadSupport);
61
62         this.host = host;
63         this.port = port;
64         this.encryptionKey = encryptionKey;
65     }
66
67     @Override
68     protected CommunicationChannel connect() throws ConnectionFailureException {
69         logger.info("Connecting to ETHM-1 module at {}:{}", host, port);
70
71         try {
72             Socket socket = new Socket();
73             socket.connect(new InetSocketAddress(host, port), this.getTimeout());
74             logger.info("ETHM-1 module connected successfully");
75
76             if (encryptionKey.isBlank()) {
77                 return new TCPCommunicationChannel(socket);
78             } else {
79                 return new EncryptedCommunicationChannel(socket, encryptionKey);
80             }
81         } catch (SocketTimeoutException e) {
82             throw new ConnectionFailureException("Connection timeout", e);
83         } catch (IOException e) {
84             throw new ConnectionFailureException("IO error occurred while connecting socket", e);
85         }
86     }
87
88     private class TCPCommunicationChannel implements CommunicationChannel {
89
90         private Socket socket;
91
92         public TCPCommunicationChannel(Socket socket) {
93             this.socket = socket;
94         }
95
96         @Override
97         public InputStream getInputStream() throws IOException {
98             return this.socket.getInputStream();
99         }
100
101         @Override
102         public OutputStream getOutputStream() throws IOException {
103             return this.socket.getOutputStream();
104         }
105
106         @Override
107         public void disconnect() {
108             logger.info("Closing connection to ETHM-1 module");
109             try {
110                 this.socket.close();
111             } catch (IOException e) {
112                 logger.error("IO error occurred during closing socket", e);
113             }
114         }
115     }
116
117     private class EncryptedCommunicationChannel extends TCPCommunicationChannel {
118
119         private EncryptionHelper aesHelper;
120         private Random rand;
121         private byte idS;
122         private byte idR;
123         private int rollingCounter;
124         private InputStream inputStream;
125         private OutputStream outputStream;
126
127         public EncryptedCommunicationChannel(final Socket socket, String encryptionKey) throws IOException {
128             super(socket);
129
130             try {
131                 this.aesHelper = new EncryptionHelper(encryptionKey);
132             } catch (Exception e) {
133                 throw new IOException("General encryption failure", e);
134             }
135             this.rand = new Random();
136             this.idS = 0;
137             this.idR = 0;
138             this.rollingCounter = 0;
139
140             this.inputStream = new InputStream() {
141                 private ByteArrayInputStream inputBuffer = EMPTY_INPUT_STREAM;
142
143                 @Override
144                 public int read() throws IOException {
145                     if (inputBuffer.available() == 0) {
146                         // read message and decrypt it
147                         byte[] data = readMessage(socket.getInputStream());
148                         // create new buffer
149                         inputBuffer = new ByteArrayInputStream(data, 6, data.length - 6);
150                     }
151                     return inputBuffer.read();
152                 }
153             };
154
155             this.outputStream = new OutputStream() {
156                 private ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream(256);
157
158                 @Override
159                 public void write(int b) throws IOException {
160                     outputBuffer.write(b);
161                 }
162
163                 @Override
164                 public void flush() throws IOException {
165                     writeMessage(outputBuffer.toByteArray(), socket.getOutputStream());
166                     outputBuffer.reset();
167                 }
168             };
169         }
170
171         @Override
172         public InputStream getInputStream() throws IOException {
173             return this.inputStream;
174         }
175
176         @Override
177         public OutputStream getOutputStream() throws IOException {
178             return this.outputStream;
179         }
180
181         private synchronized byte[] readMessage(InputStream is) throws IOException {
182             logger.trace("Receiving data from ETHM-1");
183             // read number of bytes
184             int bytesCount = is.read();
185             logger.trace("Read count of bytes: {}", bytesCount);
186             if (bytesCount == -1) {
187                 throw new IOException("End of input stream reached");
188             }
189             byte[] data = new byte[bytesCount];
190             // read encrypted data
191             int bytesRead = is.read(data);
192             if (bytesCount != bytesRead) {
193                 throw new IOException(
194                         String.format("Too few bytes read. Read: %d, expected: %d", bytesRead, bytesCount));
195             }
196             // decrypt data
197             if (logger.isTraceEnabled()) {
198                 logger.trace("Decrypting data: {}", HexUtils.bytesToHex(data));
199             }
200
201             try {
202                 this.aesHelper.decrypt(data);
203             } catch (Exception e) {
204                 throw new IOException("Decryption exception", e);
205             }
206
207             if (logger.isDebugEnabled()) {
208                 logger.debug("Decrypted data: {}", HexUtils.bytesToHex(data));
209             }
210
211             // validate message
212             this.idR = data[4];
213             if (this.idS != data[5]) {
214                 throw new IOException(String.format("Invalid 'idS' value. Got: %d, expected: %d", data[5], this.idS));
215             }
216
217             return data;
218         }
219
220         private synchronized void writeMessage(byte[] message, OutputStream os) throws IOException {
221             // prepare data for encryption
222             int bytesCount = 6 + message.length;
223             if (bytesCount < 16) {
224                 bytesCount = 16;
225             }
226             byte[] data = new byte[bytesCount];
227             int randomValue = this.rand.nextInt();
228             data[0] = (byte) (randomValue >> 8);
229             data[1] = (byte) (randomValue & 0xff);
230             data[2] = (byte) (this.rollingCounter >> 8);
231             data[3] = (byte) (this.rollingCounter & 0xff);
232             data[4] = this.idS = (byte) this.rand.nextInt();
233             data[5] = this.idR;
234             ++this.rollingCounter;
235             System.arraycopy(message, 0, data, 6, message.length);
236
237             // encrypt data
238             if (logger.isDebugEnabled()) {
239                 logger.debug("Encrypting data: {}", HexUtils.bytesToHex(data));
240             }
241
242             try {
243                 this.aesHelper.encrypt(data);
244             } catch (Exception e) {
245                 throw new IOException("Encryption exception", e);
246             }
247
248             if (logger.isTraceEnabled()) {
249                 logger.trace("Encrypted data: {}", HexUtils.bytesToHex(data));
250             }
251
252             // write encrypted data to output stream
253             os.write(bytesCount);
254             os.write(data);
255             os.flush();
256         }
257     }
258 }