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