2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.dsmr.internal.device;
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;
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;
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;
39 * Decodes messages send by The Luxembourgian Smart Meter "Smarty".
41 * @author Hilbrand Bouwkamp - Initial contribution
44 public class SmartyDecrypter implements TelegramParser {
47 WAITING_FOR_START_BYTE,
48 READ_SYSTEM_TITLE_LENGTH,
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;
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;
76 private State state = State.WAITING_FOR_START_BYTE;
77 private int currentBytePosition;
78 private int changeToNextStateAt;
80 private int dataLength;
81 private boolean lenientMode;
82 private final P1TelegramListener telegramListener;
87 * @param parser parser of the Cosem messages
88 * @param telegramListener
89 * @param decryptionKey The key to decrypt the messages
91 public SmartyDecrypter(final TelegramParser parser, final P1TelegramListener telegramListener,
92 final String decryptionKey) {
94 this.telegramListener = telegramListener;
95 secretKeySpec = decryptionKey.isEmpty() ? null : new SecretKeySpec(HexUtils.hexToBytes(decryptionKey), "AES");
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])) {
106 if (lenientMode && secretKeySpec == null) {
107 parser.parse(data, length);
111 private boolean processStateActions(final byte rawInput) {
113 case WAITING_FOR_START_BYTE:
114 if (rawInput == START_BYTE) {
116 state = State.READ_SYSTEM_TITLE_LENGTH;
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;
124 case READ_SYSTEM_TITLE:
127 if (currentBytePosition >= changeToNextStateAt) {
128 state = State.READ_SEPARATOR_82;
129 changeToNextStateAt++;
132 case READ_SEPARATOR_82:
133 if (rawInput == SEPARATOR_82) {
134 state = State.READ_PAYLOAD_LENGTH; // Ignore separator byte
135 changeToNextStateAt += 2;
137 logger.debug("Missing separator (0x82). Dropping telegram.");
138 state = State.WAITING_FOR_START_BYTE;
141 case READ_PAYLOAD_LENGTH:
143 dataLength |= rawInput & 0xFF;
144 if (currentBytePosition >= changeToNextStateAt) {
145 state = State.READ_SEPARATOR_30;
146 changeToNextStateAt++;
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;
155 logger.debug("Missing separator (0x30). Dropping telegram.");
156 state = State.WAITING_FOR_START_BYTE;
159 case READ_FRAME_COUNTER:
162 if (currentBytePosition >= changeToNextStateAt) {
163 state = State.READ_PAYLOAD;
164 changeToNextStateAt += dataLength - ADD_LENGTH;
168 cipherText.put(rawInput);
169 if (currentBytePosition >= changeToNextStateAt) {
170 state = State.READ_GCM_TAG;
171 changeToNextStateAt += GCM_TAG_LENGTH;
175 // All input has been read.
176 cipherText.put(rawInput);
177 if (currentBytePosition >= changeToNextStateAt) {
178 state = State.DONE_READING_TELEGRAM;
182 if (state == State.DONE_READING_TELEGRAM) {
183 state = State.WAITING_FOR_START_BYTE;
189 private void processCompleted() {
190 final byte[] plainText = decrypt();
193 if (plainText == null) {
195 .telegramReceived(new P1Telegram(Collections.emptyList(), TelegramState.INVALID_ENCRYPTION_KEY));
197 parser.parse(plainText, plainText.length);
202 * Decrypts the collected message.
204 * @return the decrypted message
206 private byte @Nullable [] decrypt() {
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());
215 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
216 | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
217 logger.warn("Decrypting smarty telegram failed: ", e);
223 public void reset() {
225 state = State.WAITING_FOR_START_BYTE;
228 currentBytePosition = 0;
229 changeToNextStateAt = 0;
235 public void setLenientMode(final boolean lenientMode) {
236 this.lenientMode = lenientMode;
237 parser.setLenientMode(lenientMode);