]> git.basschouten.com Git - openhab-addons.git/blob
934e32efc1389106c5252781d18e11ca5bc6827b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.device.p1telegram.P1Telegram;
31 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram.TelegramState;
32 import org.openhab.binding.dsmr.internal.device.p1telegram.P1TelegramListener;
33 import org.openhab.binding.dsmr.internal.device.p1telegram.TelegramParser;
34 import org.openhab.core.util.HexUtils;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * Decodes messages send by The Luxembourgian Smart Meter "Smarty".
40  *
41  * @author Hilbrand Bouwkamp - Initial contribution
42  */
43 @NonNullByDefault
44 public class SmartyDecrypter implements TelegramParser {
45
46     private enum State {
47         WAITING_FOR_START_BYTE,
48         READ_SYSTEM_TITLE_LENGTH,
49         READ_SYSTEM_TITLE,
50         READ_SEPARATOR_82,
51         READ_PAYLOAD_LENGTH,
52         READ_SEPARATOR_30,
53         READ_FRAME_COUNTER,
54         READ_PAYLOAD,
55         READ_GCM_TAG,
56         DONE_READING_TELEGRAM
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 String ADD = "3000112233445566778899AABBCCDDEEFF";
64     private static final byte[] ADD_DECODED = HexUtils.hexToBytes(ADD);
65     private static final int IV_BUFFER_LENGTH = 40;
66     private static final int GCM_TAG_LENGTH = 12;
67     private static final int GCM_BITS = GCM_TAG_LENGTH * Byte.SIZE;
68     private static final int MESSAGES_BUFFER_SIZE = 4096;
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
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      */
91     public SmartyDecrypter(final TelegramParser parser, final P1TelegramListener telegramListener,
92             final String decryptionKey) {
93         this.parser = parser;
94         this.telegramListener = telegramListener;
95         secretKeySpec = decryptionKey.isEmpty() ? null : new SecretKeySpec(HexUtils.hexToBytes(decryptionKey), "AES");
96     }
97
98     @Override
99     public void parse(final byte[] data, final int length) {
100         for (int i = 0; i < length; i++) {
101             currentBytePosition++;
102             if (processStateActions(data[i])) {
103                 processCompleted();
104             }
105         }
106         if (lenientMode && secretKeySpec == null) {
107             parser.parse(data, length);
108         }
109     }
110
111     private boolean processStateActions(final byte rawInput) {
112         switch (state) {
113             case WAITING_FOR_START_BYTE:
114                 if (rawInput == START_BYTE) {
115                     reset();
116                     state = State.READ_SYSTEM_TITLE_LENGTH;
117                 }
118                 break;
119             case READ_SYSTEM_TITLE_LENGTH:
120                 state = State.READ_SYSTEM_TITLE;
121                 // 2 start bytes (position 0 and 1) + system title length
122                 changeToNextStateAt = 1 + rawInput;
123                 break;
124             case READ_SYSTEM_TITLE:
125                 iv.put(rawInput);
126                 ivLength++;
127                 if (currentBytePosition >= changeToNextStateAt) {
128                     state = State.READ_SEPARATOR_82;
129                     changeToNextStateAt++;
130                 }
131                 break;
132             case READ_SEPARATOR_82:
133                 if (rawInput == SEPARATOR_82) {
134                     state = State.READ_PAYLOAD_LENGTH; // Ignore separator byte
135                     changeToNextStateAt += 2;
136                 } else {
137                     logger.debug("Missing separator (0x82). Dropping telegram.");
138                     state = State.WAITING_FOR_START_BYTE;
139                 }
140                 break;
141             case READ_PAYLOAD_LENGTH:
142                 dataLength <<= 8;
143                 dataLength |= rawInput & 0xFF;
144                 if (currentBytePosition >= changeToNextStateAt) {
145                     state = State.READ_SEPARATOR_30;
146                     changeToNextStateAt++;
147                 }
148                 break;
149             case READ_SEPARATOR_30:
150                 if (rawInput == SEPARATOR_30) {
151                     state = State.READ_FRAME_COUNTER;
152                     // 4 bytes for frame counter
153                     changeToNextStateAt += 4;
154                 } else {
155                     logger.debug("Missing separator (0x30). Dropping telegram.");
156                     state = State.WAITING_FOR_START_BYTE;
157                 }
158                 break;
159             case READ_FRAME_COUNTER:
160                 iv.put(rawInput);
161                 ivLength++;
162                 if (currentBytePosition >= changeToNextStateAt) {
163                     state = State.READ_PAYLOAD;
164                     changeToNextStateAt += dataLength - ADD_LENGTH;
165                 }
166                 break;
167             case READ_PAYLOAD:
168                 cipherText.put(rawInput);
169                 if (currentBytePosition >= changeToNextStateAt) {
170                     state = State.READ_GCM_TAG;
171                     changeToNextStateAt += GCM_TAG_LENGTH;
172                 }
173                 break;
174             case READ_GCM_TAG:
175                 // All input has been read.
176                 cipherText.put(rawInput);
177                 if (currentBytePosition >= changeToNextStateAt) {
178                     state = State.DONE_READING_TELEGRAM;
179                 }
180                 break;
181         }
182         if (state == State.DONE_READING_TELEGRAM) {
183             state = State.WAITING_FOR_START_BYTE;
184             return true;
185         }
186         return false;
187     }
188
189     private void processCompleted() {
190         final byte[] plainText = decrypt();
191
192         reset();
193         if (plainText == null) {
194             telegramListener
195                     .telegramReceived(new P1Telegram(Collections.emptyList(), TelegramState.INVALID_ENCRYPTION_KEY));
196         } else {
197             parser.parse(plainText, plainText.length);
198         }
199     }
200
201     /**
202      * Decrypts the collected message.
203      *
204      * @return the decrypted message
205      */
206     private byte @Nullable [] decrypt() {
207         try {
208             if (secretKeySpec != null) {
209                 final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
210                 cipher.init(Cipher.DECRYPT_MODE, secretKeySpec,
211                         new GCMParameterSpec(GCM_BITS, iv.array(), 0, ivLength));
212                 cipher.updateAAD(ADD_DECODED);
213                 return cipher.doFinal(cipherText.array(), 0, cipherText.position());
214             }
215         } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
216                 | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
217             logger.warn("Decrypting smarty telegram failed: ", e);
218         }
219         return null;
220     }
221
222     @Override
223     public void reset() {
224         parser.reset();
225         state = State.WAITING_FOR_START_BYTE;
226         iv.clear();
227         cipherText.clear();
228         currentBytePosition = 0;
229         changeToNextStateAt = 0;
230         ivLength = 0;
231         dataLength = 0;
232     }
233
234     @Override
235     public void setLenientMode(final boolean lenientMode) {
236         this.lenientMode = lenientMode;
237         parser.setLenientMode(lenientMode);
238     }
239 }