]> git.basschouten.com Git - openhab-addons.git/blob
a750450fdff1b9b15efdee07c3a481169631f62a
[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.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.Collections;
20
21 import javax.crypto.BadPaddingException;
22 import javax.crypto.Cipher;
23 import javax.crypto.IllegalBlockSizeException;
24 import javax.crypto.NoSuchPaddingException;
25 import javax.crypto.spec.GCMParameterSpec;
26 import javax.crypto.spec.SecretKeySpec;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.dsmr.internal.DSMRBindingConstants;
31 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram;
32 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram.TelegramState;
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         DONE_READING_TELEGRAM
58     }
59
60     private static final byte START_BYTE = (byte) 0xDB;
61     private static final byte SEPARATOR_82 = (byte) 0x82;
62     private static final byte SEPARATOR_30 = 0x30;
63     private static final int ADD_LENGTH = 17;
64     private static final int IV_BUFFER_LENGTH = 40;
65     private static final int GCM_TAG_LENGTH = 12;
66     private static final int GCM_BITS = GCM_TAG_LENGTH * Byte.SIZE;
67     private static final int MESSAGES_BUFFER_SIZE = 4096;
68     private static final String ADDITIONAL_ADD_PREFIX = "30";
69
70     private final Logger logger = LoggerFactory.getLogger(SmartyDecrypter.class);
71     private final ByteBuffer iv = ByteBuffer.allocate(IV_BUFFER_LENGTH);
72     private final ByteBuffer cipherText = ByteBuffer.allocate(MESSAGES_BUFFER_SIZE);
73     private final TelegramParser parser;
74     private @Nullable final SecretKeySpec secretKeySpec;
75
76     private State state = State.WAITING_FOR_START_BYTE;
77     private int currentBytePosition;
78     private int changeToNextStateAt;
79     private int ivLength;
80     private int dataLength;
81     private boolean lenientMode;
82     private final P1TelegramListener telegramListener;
83     private final byte[] addKey;
84
85     /**
86      * Constructor.
87      *
88      * @param parser parser of the Cosem messages
89      * @param telegramListener
90      * @param decryptionKey The key to decrypt the messages
91      * @param additionalKey Additional optional key to decrypt the message
92      */
93     public SmartyDecrypter(final TelegramParser parser, final P1TelegramListener telegramListener,
94             final String decryptionKey, final String additionalKey) {
95         this.parser = parser;
96         this.telegramListener = telegramListener;
97         secretKeySpec = decryptionKey.isEmpty() ? null : new SecretKeySpec(HexUtils.hexToBytes(decryptionKey), "AES");
98         addKey = HexUtils.hexToBytes(additionalKey.isBlank() ? DSMRBindingConstants.ADDITIONAL_KEY_DEFAULT
99                 : ((additionalKey.length() == 32 ? (ADDITIONAL_ADD_PREFIX) : "") + additionalKey));
100     }
101
102     @Override
103     public void parse(final byte[] data, final int length) {
104         for (int i = 0; i < length; i++) {
105             currentBytePosition++;
106             if (processStateActions(data[i])) {
107                 processCompleted();
108             }
109         }
110         if (lenientMode && secretKeySpec == null) {
111             parser.parse(data, length);
112         }
113     }
114
115     private boolean processStateActions(final byte rawInput) {
116         switch (state) {
117             case WAITING_FOR_START_BYTE:
118                 if (rawInput == START_BYTE) {
119                     reset();
120                     state = State.READ_SYSTEM_TITLE_LENGTH;
121                 }
122                 break;
123             case READ_SYSTEM_TITLE_LENGTH:
124                 state = State.READ_SYSTEM_TITLE;
125                 // 2 start bytes (position 0 and 1) + system title length
126                 changeToNextStateAt = 1 + rawInput;
127                 break;
128             case READ_SYSTEM_TITLE:
129                 iv.put(rawInput);
130                 ivLength++;
131                 if (currentBytePosition >= changeToNextStateAt) {
132                     state = State.READ_SEPARATOR_82;
133                     changeToNextStateAt++;
134                 }
135                 break;
136             case READ_SEPARATOR_82:
137                 if (rawInput == SEPARATOR_82) {
138                     state = State.READ_PAYLOAD_LENGTH; // Ignore separator byte
139                     changeToNextStateAt += 2;
140                 } else {
141                     logger.debug("Missing separator (0x82). Dropping telegram.");
142                     state = State.WAITING_FOR_START_BYTE;
143                 }
144                 break;
145             case READ_PAYLOAD_LENGTH:
146                 dataLength <<= 8;
147                 dataLength |= rawInput & 0xFF;
148                 if (currentBytePosition >= changeToNextStateAt) {
149                     state = State.READ_SEPARATOR_30;
150                     changeToNextStateAt++;
151                 }
152                 break;
153             case READ_SEPARATOR_30:
154                 if (rawInput == SEPARATOR_30) {
155                     state = State.READ_FRAME_COUNTER;
156                     // 4 bytes for frame counter
157                     changeToNextStateAt += 4;
158                 } else {
159                     logger.debug("Missing separator (0x30). Dropping telegram.");
160                     state = State.WAITING_FOR_START_BYTE;
161                 }
162                 break;
163             case READ_FRAME_COUNTER:
164                 iv.put(rawInput);
165                 ivLength++;
166                 if (currentBytePosition >= changeToNextStateAt) {
167                     state = State.READ_PAYLOAD;
168                     changeToNextStateAt += dataLength - ADD_LENGTH;
169                 }
170                 break;
171             case READ_PAYLOAD:
172                 cipherText.put(rawInput);
173                 if (currentBytePosition >= changeToNextStateAt) {
174                     state = State.READ_GCM_TAG;
175                     changeToNextStateAt += GCM_TAG_LENGTH;
176                 }
177                 break;
178             case READ_GCM_TAG:
179                 // All input has been read.
180                 cipherText.put(rawInput);
181                 if (currentBytePosition >= changeToNextStateAt) {
182                     state = State.DONE_READING_TELEGRAM;
183                 }
184                 break;
185         }
186         if (state == State.DONE_READING_TELEGRAM) {
187             state = State.WAITING_FOR_START_BYTE;
188             return true;
189         }
190         return false;
191     }
192
193     private void processCompleted() {
194         final byte[] plainText = decrypt();
195
196         reset();
197         if (plainText == null) {
198             telegramListener
199                     .telegramReceived(new P1Telegram(Collections.emptyList(), TelegramState.INVALID_ENCRYPTION_KEY));
200         } else {
201             parser.parse(plainText, plainText.length);
202         }
203     }
204
205     /**
206      * Decrypts the collected message.
207      *
208      * @return the decrypted message
209      */
210     private byte @Nullable [] decrypt() {
211         try {
212             if (secretKeySpec != null) {
213                 final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
214                 cipher.init(Cipher.DECRYPT_MODE, secretKeySpec,
215                         new GCMParameterSpec(GCM_BITS, iv.array(), 0, ivLength));
216                 cipher.updateAAD(addKey);
217                 return cipher.doFinal(cipherText.array(), 0, cipherText.position());
218             }
219         } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
220                 | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
221             logger.warn("Decrypting smarty telegram failed: ", e);
222         }
223         return null;
224     }
225
226     @Override
227     public void reset() {
228         parser.reset();
229         state = State.WAITING_FOR_START_BYTE;
230         iv.clear();
231         cipherText.clear();
232         currentBytePosition = 0;
233         changeToNextStateAt = 0;
234         ivLength = 0;
235         dataLength = 0;
236     }
237
238     @Override
239     public void setLenientMode(final boolean lenientMode) {
240         this.lenientMode = lenientMode;
241         parser.setLenientMode(lenientMode);
242     }
243 }