]> git.basschouten.com Git - openhab-addons.git/blob
b232ce79fd63169430441be8c5c9016d7f08694e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.dsmr.internal.device;
14
15 import java.nio.ByteBuffer;
16 import java.security.InvalidAlgorithmParameterException;
17 import java.security.InvalidKeyException;
18 import java.security.NoSuchAlgorithmException;
19 import java.util.Arrays;
20 import java.util.Optional;
21
22 import javax.crypto.BadPaddingException;
23 import javax.crypto.Cipher;
24 import javax.crypto.IllegalBlockSizeException;
25 import javax.crypto.NoSuchPaddingException;
26 import javax.crypto.spec.GCMParameterSpec;
27 import javax.crypto.spec.SecretKeySpec;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.dsmr.internal.DSMRBindingConstants;
32 import org.openhab.binding.dsmr.internal.device.connector.DSMRErrorStatus;
33 import org.openhab.binding.dsmr.internal.device.p1telegram.P1TelegramListener;
34 import org.openhab.binding.dsmr.internal.device.p1telegram.TelegramParser;
35 import org.openhab.core.util.HexUtils;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * Decodes messages send by The Luxembourgian Smart Meter "Smarty".
41  *
42  * @author Hilbrand Bouwkamp - Initial contribution
43  */
44 @NonNullByDefault
45 public class SmartyDecrypter implements TelegramParser {
46
47     private enum State {
48         WAITING_FOR_START_BYTE,
49         READ_SYSTEM_TITLE_LENGTH,
50         READ_SYSTEM_TITLE,
51         READ_SEPARATOR_82,
52         READ_PAYLOAD_LENGTH,
53         READ_SEPARATOR_30,
54         READ_FRAME_COUNTER,
55         READ_PAYLOAD,
56         READ_GCM_TAG
57     }
58
59     private static final byte START_BYTE = (byte) 0xDB;
60     private static final byte SEPARATOR_82 = (byte) 0x82;
61     private static final byte SEPARATOR_30 = 0x30;
62     private static final int ADD_LENGTH = 17;
63     private static final int IV_BUFFER_LENGTH = 40;
64     private static final int GCM_TAG_LENGTH = 12;
65     private static final int GCM_BITS = GCM_TAG_LENGTH * Byte.SIZE;
66     private static final int MESSAGES_BUFFER_SIZE = 4096;
67     private static final String ADDITIONAL_ADD_PREFIX = "30";
68
69     private final Logger logger = LoggerFactory.getLogger(SmartyDecrypter.class);
70     private final ByteBuffer iv = ByteBuffer.allocate(IV_BUFFER_LENGTH);
71     private final ByteBuffer cipherText = ByteBuffer.allocate(MESSAGES_BUFFER_SIZE);
72     private final TelegramParser parser;
73     private @Nullable final SecretKeySpec secretKeySpec;
74
75     private State state = State.WAITING_FOR_START_BYTE;
76     private int currentBytePosition;
77     private int changeToNextStateAt;
78     private int ivLength;
79     private int dataLength;
80     private boolean lenientMode;
81     private final P1TelegramListener telegramListener;
82     private final byte[] addKey;
83
84     /**
85      * Constructor.
86      *
87      * @param parser parser of the Cosem messages
88      * @param telegramListener
89      * @param decryptionKey The key to decrypt the messages
90      * @param additionalKey Additional optional key to decrypt the message
91      */
92     public SmartyDecrypter(final TelegramParser parser, final P1TelegramListener telegramListener,
93             final String decryptionKey, final String additionalKey) {
94         this.parser = parser;
95         this.telegramListener = telegramListener;
96         secretKeySpec = decryptionKey.isEmpty() ? null : new SecretKeySpec(HexUtils.hexToBytes(decryptionKey), "AES");
97         addKey = HexUtils.hexToBytes(additionalKey.isBlank() ? DSMRBindingConstants.CONFIGURATION_ADDITIONAL_KEY_DEFAULT
98                 : ((additionalKey.length() == 32 ? (ADDITIONAL_ADD_PREFIX) : "") + additionalKey));
99     }
100
101     @Override
102     public void parse(final byte[] data, final int length) {
103         for (int i = 0; i < length; i++) {
104             currentBytePosition++;
105             if (processStateActions(data[i])) {
106                 processCompleted();
107             }
108         }
109         if (lenientMode && secretKeySpec == null) {
110             parser.parse(data, length);
111         }
112     }
113
114     private boolean processStateActions(final byte rawInput) {
115         // Safeguard against buffer overrun in case corrupt data is received.
116         if (ivLength == IV_BUFFER_LENGTH) {
117             reset();
118             return false;
119         }
120         switch (state) {
121             case WAITING_FOR_START_BYTE:
122                 if (rawInput == START_BYTE) {
123                     reset();
124                     state = State.READ_SYSTEM_TITLE_LENGTH;
125                 }
126                 break;
127             case READ_SYSTEM_TITLE_LENGTH:
128                 state = State.READ_SYSTEM_TITLE;
129                 // 2 start bytes (position 0 and 1) + system title length
130                 changeToNextStateAt = 1 + rawInput;
131                 break;
132             case READ_SYSTEM_TITLE:
133                 iv.put(rawInput);
134                 ivLength++;
135                 if (currentBytePosition >= changeToNextStateAt) {
136                     state = State.READ_SEPARATOR_82;
137                     changeToNextStateAt++;
138                 }
139                 break;
140             case READ_SEPARATOR_82:
141                 if (rawInput == SEPARATOR_82) {
142                     state = State.READ_PAYLOAD_LENGTH; // Ignore separator byte
143                     changeToNextStateAt += 2;
144                 } else {
145                     logger.debug("Missing separator (0x82). Dropping telegram.");
146                     state = State.WAITING_FOR_START_BYTE;
147                 }
148                 break;
149             case READ_PAYLOAD_LENGTH:
150                 dataLength <<= 8;
151                 dataLength |= rawInput & 0xFF;
152                 if (currentBytePosition >= changeToNextStateAt) {
153                     state = State.READ_SEPARATOR_30;
154                     changeToNextStateAt++;
155                 }
156                 break;
157             case READ_SEPARATOR_30:
158                 if (rawInput == SEPARATOR_30) {
159                     state = State.READ_FRAME_COUNTER;
160                     // 4 bytes for frame counter
161                     changeToNextStateAt += 4;
162                 } else {
163                     logger.debug("Missing separator (0x30). Dropping telegram.");
164                     state = State.WAITING_FOR_START_BYTE;
165                 }
166                 break;
167             case READ_FRAME_COUNTER:
168                 iv.put(rawInput);
169                 ivLength++;
170                 if (currentBytePosition >= changeToNextStateAt) {
171                     state = State.READ_PAYLOAD;
172                     changeToNextStateAt += dataLength - ADD_LENGTH;
173                 }
174                 break;
175             case READ_PAYLOAD:
176                 cipherText.put(rawInput);
177                 if (currentBytePosition >= changeToNextStateAt) {
178                     state = State.READ_GCM_TAG;
179                     changeToNextStateAt += GCM_TAG_LENGTH;
180                 }
181                 break;
182             case READ_GCM_TAG:
183                 // All input has been read.
184                 cipherText.put(rawInput);
185                 if (currentBytePosition >= changeToNextStateAt) {
186                     state = State.WAITING_FOR_START_BYTE;
187                     return true;
188                 }
189                 break;
190         }
191         return false;
192     }
193
194     private void processCompleted() {
195         try {
196             final byte[] plainText = decrypt();
197
198             if (plainText != null) {
199                 parser.parse(plainText, plainText.length);
200             }
201         } finally {
202             reset();
203         }
204     }
205
206     /**
207      * Decrypts the collected message.
208      *
209      * @return the decrypted message
210      */
211     private byte @Nullable [] decrypt() {
212         try {
213             if (secretKeySpec != null) {
214                 final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
215                 cipher.init(Cipher.DECRYPT_MODE, secretKeySpec,
216                         new GCMParameterSpec(GCM_BITS, iv.array(), 0, ivLength));
217                 cipher.updateAAD(addKey);
218                 return cipher.doFinal(cipherText.array(), 0, cipherText.position());
219             }
220         } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
221                 | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
222             if (lenientMode || logger.isDebugEnabled()) {
223                 // log in lenient mode or when debug is enabled. But log to warn to also work when lenientMode is
224                 // enabled.
225                 logger.warn("Failed encrypted telegram: {}",
226                         HexUtils.bytesToHex(Arrays.copyOf(cipherText.array(), cipherText.position())));
227                 logger.warn("Exception of failed decryption of telegram: ", e);
228             }
229             telegramListener.onError(DSMRErrorStatus.INVALID_DECRYPTION_KEY,
230                     Optional.ofNullable(e.getMessage()).orElse(""));
231         }
232         return null;
233     }
234
235     @Override
236     public void reset() {
237         parser.reset();
238         state = State.WAITING_FOR_START_BYTE;
239         iv.clear();
240         cipherText.clear();
241         currentBytePosition = 0;
242         changeToNextStateAt = 0;
243         ivLength = 0;
244         dataLength = 0;
245     }
246
247     @Override
248     public void setLenientMode(final boolean lenientMode) {
249         this.lenientMode = lenientMode;
250         parser.setLenientMode(lenientMode);
251     }
252 }