From 94bc055ac652f9a4080b7a311a6137173c17611e Mon Sep 17 00:00:00 2001 From: kaihs <> Date: Sun, 24 Jun 2018 19:58:13 +0000 Subject: [PATCH] 36_WMBUS: support for type C and Kamstrup Multical git-svn-id: https://svn.fhem.de/fhem/trunk@16905 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 6 + fhem/FHEM/36_WMBUS.pm | 24 ++- fhem/FHEM/WMBus.pm | 474 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 448 insertions(+), 56 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index e08b1fca8..6398b8172 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,11 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: 36_WMBUS: support for WMBUS type C and Kamstrup Multical 21 + encoding. + ATTENTION: decryption has changed, please install + the perl modules Crypt::Mode::CBC and + Crypt::Mode::CTR if you want to decrypt messages. + (sudo cpan -i Crypt::Mode::CBC Crypt::Mode::CTR) - change: 93_DbLog: commandref hint for special character usage in passwords - change: 74_AMADtasker: AMAD Taskerproject change to new battery states - feature: 49_SSCamSTRM: new attr hideDisplayName regarding to Forum #88667 diff --git a/fhem/FHEM/36_WMBUS.pm b/fhem/FHEM/36_WMBUS.pm index 738db4539..3bc426cdd 100644 --- a/fhem/FHEM/36_WMBUS.pm +++ b/fhem/FHEM/36_WMBUS.pm @@ -54,6 +54,10 @@ WMBUS_HandleEncoding($$) my $msglen = sprintf("%1x", hex(substr($msg,4,1)) - 1); $msg = "b" . $msglen . substr($msg,5); } else { + if (substr($msg,1,1) eq "Y") { + $mb->setFrameType(WMBus::FRAME_TYPE_B); + $msg = "b" . substr($msg,2); + } $msg .= WMBUS_RSSIAsRaw($rssi); } return ($msg, $rssi, $encoding); @@ -456,8 +460,8 @@ WMBUS_Attr(@) It uses the 868 MHz band for radio transmissions. Therefore you need a device which can receive Wireless M-Bus messages, e.g. a CUL with culfw >= 1.59 or an AMBER Wireless AMB8465M.
- WMBus uses two different radio protocols, T-Mode and S-Mode. The receiver must be configured to use the same protocol as the sender. - In case of a CUL this can be done by setting rfmode to WMBus_T or WMBus_S respectively. + WMBus uses three different radio protocols, T-Mode, S-Mode and C-Mode. The receiver must be configured to use the same protocol as the sender. + In case of a CUL this can be done by setting rfmode to WMBus_T, WMBus_S or WMBus_C respectively.
WMBus devices send data periodically depending on their configuration. It can take days between individual messages or they might be sent every minute. @@ -466,11 +470,11 @@ WMBUS_Attr(@) will fail and no relevant data will be available.

Prerequisites
- This module requires the perl modules Crypt::CBC, Digest::CRC and Crypt::OpenSSL::AES (AES only if encrypted messages should be processed).
+ This module requires the perl modules Digest::CRC, Crypt::Mode::CBC and Crypt::Mode::CTR (Crypt modules only if encrypted messages should be processed).
On a debian based system these can be installed with
- sudo apt-get install libcrypt-cbc-perl libdigest-crc-perl libssl-dev
- sudo cpan -i Crypt::OpenSSL::AES + sudo apt-get install libdigest-crc-perl
+ sudo cpan -i Crypt::Mode::CBC Crypt::Mode:CTR


@@ -563,8 +567,8 @@ WMBUS_Attr(@) Es verwendet das 868 MHz Band für Radioübertragungen. Daher wird ein Gerät benötigt das die Wireless M-Bus Nachrichten empfangen kann, z. B. ein CUL mit culfw >= 1.59 oder ein AMBER Wireless AMB8465-M.
- WMBus verwendet zwei unterschiedliche Radioprotokolle, T-Mode und S-Mode. Der Empfänger muss daher so konfiguriert werden, dass er das selbe Protokoll - verwendet wie der Sender. Im Falle eines CUL kann das erreicht werden, in dem das Attribut rfmode auf WMBus_T bzw. WMBus_S gesetzt wird. + WMBus verwendet drei unterschiedliche Radioprotokolle, T-Mode, S-Mode und C-Mode. Der Empfänger muss daher so konfiguriert werden, dass er das selbe Protokoll + verwendet wie der Sender. Im Falle eines CUL kann das erreicht werden, in dem das Attribut rfmode auf WMBus_T, WMBus_S bzw. WMBus_C gesetzt wird.
WMBus Geräte senden Daten periodisch abhängig von ihrer Konfiguration. Es können u. U. Tage zwischen einzelnen Nachrichten vergehen oder sie können im Minutentakt gesendet werden. @@ -573,11 +577,11 @@ WMBUS_Attr(@) Andernfalls wird die Entschlüsselung fehlschlagen und es können keine relevanten Daten ausgelesen werden.

Voraussetzungen
- Dieses Modul benötigt die perl Module Crypt::CBC, Digest::CRC and Crypt::OpenSSL::AES (AES wird nur benötigt wenn verschlüsselte Nachrichten verarbeitet werden sollen).
+ Dieses Modul benötigt die perl Module Digest::CRC, Crypt::Mode::CBC und Crypt::ModeL::CTR (die Crypt Module werden nur benötigt wenn verschlüsselte Nachrichten verarbeitet werden sollen).
Bei einem Debian basierten System können diese so installiert werden
- sudo apt-get install libcrypt-cbc-perl libdigest-crc-perl libssl-dev
- sudo cpan -i Crypt::OpenSSL::AES + sudo apt-get install libdigest-crc-perl
+ sudo cpan -i Crypt::Mode::CBC Crypt::Mode::CTR


diff --git a/fhem/FHEM/WMBus.pm b/fhem/FHEM/WMBus.pm index fa5ff66b9..b2273ddce 100644 --- a/fhem/FHEM/WMBus.pm +++ b/fhem/FHEM/WMBus.pm @@ -5,12 +5,11 @@ package WMBus; use strict; use warnings; use feature qw(say); -use Crypt::CBC; # libcrypt-cbc-perl use Digest::CRC; # libdigest-crc-perl - -# there seems to be no debian package for Crypt::OpenSSL::AES, so use -# sudo apt-get install libssl-dev -# sudo cpan -i Crypt::OpenSSL::AES +eval "use Crypt::Mode::CBC"; # cpan -i Crypt::Mode::CBC +my $hasCBC = ($@)?0:1; +eval "use Crypt::Mode::CTR"; # cpan -i Crypt::Mode::CTR +my $hasCTR = ($@)?0:1; require Exporter; my @ISA = qw(Exporter); @@ -44,6 +43,11 @@ use constant { CI_ERROR => 0x70, # Error from device, only specified for wired M-Bus but used by Easymeter WMBUS module CI_TL_4 => 0x8a, # Transport layer from device, 4 Bytes CI_TL_12 => 0x8b, # Transport layer from device, 12 Bytes + CI_ELL_2 => 0x8c, # Extended Link Layer, 2 Bytes + CI_ELL_6 => 0x8e, # Extended Link Layer, 6 Bytes + CI_ELL_8 => 0x8d, # Extended Link Layer, 8 Bytes (see https://www.telit.com/wp-content/uploads/2017/09/Telit_Wireless_M-bus_2013_Part4_User_Guide_r14.pdf, 2.3.4) + CI_ELL_16 => 0x8f, # Extended Link Layer, 16 Bytes (see https://www.telit.com/wp-content/uploads/2017/09/Telit_Wireless_M-bus_2013_Part4_User_Guide_r14.pdf, 2.3.4) + CI_AFL => 0x90, # Authentification and Fragmentation Layer, variable size CI_RESP_SML_4 => 0x7e, # Response from device, 4 Bytes, application layer SML encoded CI_RESP_SML_12 => 0x7f, # Response from device, 12 Bytes, application layer SML encoded @@ -88,8 +92,14 @@ use constant { ERR_TOO_MANY_VIFE => 11, ERR_MSG_TOO_SHORT => 12, ERR_SML_PAYLOAD => 13, + ERR_FRAGMENT_UNSUPPORTED => 14, + ERR_UNKNOWN_COMPACT_FORMAT => 15, + ERR_CIPHER_NOT_INSTALLED => 16, - + # TYPE C transmission uses two different frame types + # see http://www.st.com/content/ccc/resource/technical/document/application_note/3f/fb/35/5a/25/4e/41/ba/DM00233038.pdf/files/DM00233038.pdf/jcr:content/translations/en.DM00233038.pdf + FRAME_TYPE_A => 'A', + FRAME_TYPE_B => 'B', }; sub valueCalcNumeric($$) { @@ -796,6 +806,18 @@ my %VIFInfo_ESY = ( }, ); +# For Kamstrup (manufacturer specific) +my %VIFInfo_KAM = ( + VIF_KAMSTRUP_INFO => { + typeMask => 0b00000000, + expMask => 0b00000000, + type => 0b00000000, + bias => 0, + unit => '', + }, +); + + # see 4.2.3, page 24 my %validDeviceTypes = ( 0x00 => 'Other', @@ -917,6 +939,7 @@ sub checkCRC($$) { return $ctx->digest; } + sub removeCRC($$) { my $self = shift; @@ -995,6 +1018,7 @@ sub _initialize { my $self = shift; $self->{crc_size} = CRC_SIZE; + $self->{frame_type} = FRAME_TYPE_A; # default } sub setCRCsize { @@ -1144,6 +1168,9 @@ sub decodeValueInformationBlock($$$) { # Easymeter $vif = unpack('C', substr($vib,$offset++,1)); $vifInfoRef = \%VIFInfo_ESY; + } elsif ($self->{manufacturer} eq 'KAM') { + $vif = unpack('C', substr($vib,$offset++,1)); + $vifInfoRef = \%VIFInfo_KAM; } else { # manufacturer specific data, can't be interpreted @@ -1421,21 +1448,183 @@ sub decrypt($) { for (1..8) { $initVector .= pack('C',$self->{access_no}); } - my $cipher = Crypt::CBC->new( - -key => $self->{aeskey}, - -cipher => "Crypt::OpenSSL::AES", - -header => "none", - -iv => $initVector, - -literal_key => "true", - -keysize => 16, - ); + my $cipher = Crypt::Mode::CBC->new('AES', 1); + return $cipher->decrypt($encrypted, $self->{aeskey}, $initVector); +} - return $cipher->decrypt($encrypted); +sub decrypt_mode7($) { + my $self = shift; + my $encrypted = shift; + + # see 9.2.4, page 59 + my $initVector = ''; + for (1..16) { + $initVector .= pack('C',0x00); + } + my $cipher = Crypt::Mode::CBC->new('AES', 1); + return $cipher->decrypt($encrypted, $self->{aeskey}, $initVector); +} + +# Generate MAC of data +# +# Parameter 1: private key as byte string, 16bytes +# Parameter 2: data fro which mac should be calculated in hexadecimal format, len variable +# Parameter 3: length of MAC to be generated in bytes +# +# Returns: MAC in hexadecimal format +# +# This function currently supports data with lentgh of less then 16bytes, +# MAC for longer data is untested but specified +# +# copied from 10_EnOcean.pm +sub generateMAC($$$$) { + my $self = shift; + my $private_key = $_[0]; + my $data = $_[1]; + my $cmac_len = $_[2]; + + #print "Calculating MAC for data $data\n"; + + # Pack data to 16byte byte string, padd with 10..0 binary + my $data_expanded = pack('H32', $data.'80'); + + #print "Exp. data ".unpack('H32', $data_expanded)."\n"; + + # Constants according to specification + my $const_zero = pack('H32','00'); + my $const_rb = pack('H32', '00000000000000000000000000000087'); + + # Encrypt zero data with private key to get L + my $cipher = Crypt::Rijndael->new($private_key); + my $l = $cipher->encrypt($const_zero); + #print "L ".unpack('H32', $l)."\n"; + #print "L ".unpack('B128', $l)."\n"; + + # Expand L to 128bit string + my $l_bit = unpack('B128', $l); + + # K1 and K2 stored as 128bit string + my $k1_bit; + my $k2_bit; + + # K1 and K2 as binary + my $k1; + my $k2; + + # Store L << 1 in K1 + $l_bit =~ /^.(.{127})/; + $k1_bit = $1.'0'; + $k1 = pack('B128', $k1_bit); + + # If MSB of L == 1, K1 = K1 XOR const_Rb + if($l_bit =~ m/^1/) { + #print "MSB of L is set\n"; + $k1 = $k1 ^ $const_rb; + $k1_bit = unpack('B128', $k1); + } else { + #print "MSB of L is unset\n"; + } + + # Store K1 << 1 in K2 + $k1_bit =~ /^.(.{127})/; + $k2_bit = $1.'0'; + $k2 = pack('B128', $k2_bit); + + # If MSB of K1 == 1, K2 = K2 XOR const_Rb + if($k1_bit =~ m/^1/) { + #print "MSB of K1 is set\n"; + $k2 = $k2 ^ $const_rb; + } else { + #print "MSB of K1 is unset\n"; + } + + # XOR data with K2 + $data_expanded ^= $k2; + + # Encrypt data + my $cmac = $cipher->encrypt($data_expanded); + + #print "CMAC ".unpack('H32', $cmac)."\n"; + + # Extract specified len of MAC + my $cmac_pattern = '^(.{'.($cmac_len * 2).'})'; + unpack('H32', $cmac) =~ /$cmac_pattern/; + + # Return MAC in hexadecimal format + return uc($1); +} + + +sub decodeAFL($$) { + my $self = shift; + my $afl = shift; + my $offset = 0; + + + $self->{afl}{fcl} = unpack('v', $afl); + $offset += 2; + $self->{afl}{fcl_mf} = ($self->{afl}{fcl} & 0b0100000000000000) != 0; + $self->{afl}{fcl_mclp} = ($self->{afl}{fcl} & 0b0010000000000000) != 0; + $self->{afl}{fcl_mlp} = ($self->{afl}{fcl} & 0b0001000000000000) != 0; + $self->{afl}{fcl_mcrp} = ($self->{afl}{fcl} & 0b0000100000000000) != 0; + $self->{afl}{fcl_macp} = ($self->{afl}{fcl} & 0b0000010000000000) != 0; + $self->{afl}{fcl_kip} = ($self->{afl}{fcl} & 0b0000001000000000) != 0; + $self->{afl}{fcl_fid} = $self->{afl}{fcl} & 0b0000000011111111; + + if ($self->{afl}{fcl_mclp}) { + # AFL Message Control Field (AFL.MCL) + $self->{afl}{mcl} = unpack('C', substr($afl, $offset, 1)); + $offset += 1; + $self->{afl}{mcl_mlmp} = ($self->{afl}{mcl} & 0b01000000) != 0; + $self->{afl}{mcl_mcmp} = ($self->{afl}{mcl} & 0b00100000) != 0; + $self->{afl}{mcl_kimp} = ($self->{afl}{mcl} & 0b00010000) != 0; + $self->{afl}{mcl_at} = ($self->{afl}{mcl} & 0b00001111); + } + if ($self->{afl}{fcl_mcrp}) { + # AFL Message Counter Field (AFL.MCR) + $self->{afl}{mcr} = unpack('V', substr($afl, $offset)); + #printf "AFL MC %08x\n", $self->{afl}{mcr}; + $offset += 4; + } + if ($self->{afl}{fcl_mlp}) { + # AFL Message Length Field (AFL.ML) + $self->{afl}{ml} = unpack('v', substr($afl, $offset)); + $offset += 2; + } + if ($self->{afl}{fcl_macp}) { + # AFL MAC Field (AFL.MCL) + # The length of the MAC field depends on the selected option AFL.MCL.AT indicated by the + # AFL.MCL field. + my $mac_len = 0; + if ($self->{afl}{mcl_at} == 4) { + $mac_len = 4; + $self->{afl}{mac} = unpack('N', substr($afl, $offset, $mac_len)); + } elsif ($self->{afl}{mcl_at} == 5) { + $mac_len = 8; + $self->{afl}{mac} = (unpack('N', substr($afl, $offset, 4))) << 32 | ((unpack('N', substr($afl, $offset+4, 4)))); + } elsif ($self->{afl}{mcl_at} == 6) { + $mac_len = 12; + } elsif ($self->{afl}{mcl_at} == 7) { + $mac_len = 16; + } + #printf "AFL MAC %16x\n", $self->{afl}{mac}; + $offset += $mac_len; + } + if ($self->{afl}{fcl_kip}) { + # AFL Key Information-Field (AFL.KI) + $self->{afl}{ki} = unpack('v', $afl); + $self->{afl}{ki_key_version} = ($self->{afl}{ki} & 0b1111111100000000) >> 8; + $self->{afl}{ki_kdf_selection} = ($self->{afl}{ki} & 0b0000000001110000) >> 4; + $self->{afl}{ki_key_id} = ($self->{afl}{ki} & 0b0000000000001111); + $offset += 2; + } + return $offset; } sub decodeApplicationLayer($) { my $self = shift; - my $applicationlayer = $self->removeCRC(substr($self->{msg},TL_BLOCK_SIZE + $self->{crc_size})); + my $applicationlayer = $self->{applicationlayer}; + my $payload; #print unpack("H*", $applicationlayer) . "\n"; @@ -1447,6 +1636,102 @@ sub decodeApplicationLayer($) { my $offset = 1; + if ($self->{cifield} == CI_ELL_2) { + # Extended Link Layer + ($self->{ell}{cc}, $self->{ell}{access_no}) = unpack('CC', substr($applicationlayer,$offset)); + $offset += 2; + } elsif ($self->{cifield} == CI_ELL_6) { + # Extended Link Layer + ($self->{ell}{cc}, $self->{ell}{access_no}) = unpack('CC', substr($applicationlayer,$offset)); + $offset += 6; + } elsif ($self->{cifield} == CI_ELL_8) { + # Extended Link Layer, payload CRC is part of (encrypted) payload + ($self->{ell}{cc}, $self->{ell}{access_no}, $self->{ell}{session_number}) = unpack('CCV', substr($applicationlayer, $offset)); + $offset += 6; + } elsif ($self->{cifield} == CI_ELL_16) { + # Extended Link Layer + ($self->{ell}{cc}, $self->{ell}{access_no}, $self->{ell}{m2}, $self->{ell}{a2}, $self->{ell}{session_number}) = unpack('CCvC6V', substr($applicationlayer,$offset)); + $offset += 14; + } + + if (exists($self->{ell})) { + $self->{ell}{session_number_enc} = $self->{ell}{session_number} >> 29; + $self->{ell}{session_number_time} = ($self->{ell}{session_number} & 0b0001111111111111111111111111111) >> 4; + $self->{ell}{session_number_session} = $self->{ell}{session_number} & 0b1111; + $self->{isEncrypted} = $self->{ell}{session_number_enc} != 0; + $self->{decrypted} = 0; + + if ($self->{isEncrypted}) { + if ($self->{aeskey}) { + if ($hasCTR) { + # AES IV + # M-field, A-field, CC, SN, 00, 0000 + my $initVector = pack("v", $self->{mfield}) . $self->{afield} . pack("CV", $self->{ell}{cc}, $self->{ell}{session_number}) . pack("H*", "000000"); + my $m = Crypt::Mode::CTR->new('AES', 1); + my $ciphertext = substr($applicationlayer,$offset); # payload CRC must also be decrypted + #printf("##ciphertext: %s\n", unpack("H*", $ciphertext)); + $payload = $m->decrypt($ciphertext, $self->{aeskey}, $initVector); + + #printf("##plaintext %s\n", unpack("H*", $payload)); + } else { + $self->{errormsg} = 'Crypt::Mode::CTR is not installed, please install it (sudo cpan -i Crypt::Mode::CTR)'; + $self->{errorcode} = ERR_CIPHER_NOT_INSTALLED; + return 0; + } + } else { + $self->{errormsg} = 'encrypted message and no aeskey provided'; + $self->{errorcode} = ERR_NO_AESKEY; + return 0; + } + } + $self->{ell}{crc} = unpack('v', $payload); + $offset += 2; + # PayloadCRC is a cyclic redundancy check covering the remainder of the frame (excluding the CRC fields) + # payload CRC is also encrypted + if ($self->{ell}{crc} != $self->checkCRC(substr($payload, 2, $self->{lfield}-20))) { + #printf("crc %x, calculated %x\n", $self->{ell}{crc}, $self->checkCRC(substr($payload, 2, $self->{lfield}-20))); + $self->{errormsg} = "Payload CRC check failed on ELL" . ($self->{isEncrypted} ? ", wrong AES key?" : ""); + $self->{errorcode} = ERR_CRC_FAILED; + return 0; + } else { + $self->{decrypted} = 1; + } + $applicationlayer = $payload; + $offset = 2; # skip PayloadCRC + } + + if ($offset > 1) { + $applicationlayer = substr($applicationlayer,$offset); + $self->{cifield} = unpack('C', $applicationlayer); + $offset = 1; + if ($self->{cifield} == CI_AFL) { + # Authentification and Fragmentation Layer + $self->{afl}{afll} = unpack('C', substr($applicationlayer, $offset)); + #printf "AFL AFLL %02x\n", $self->{afl}{afll}; + $offset += 1; + $self->decodeAFL(substr($applicationlayer,$offset,$self->{afl}{afll})); + $offset += $self->{afl}{afll}; + if ($self->{afl}{fcl_mf}) { + $self->{errormsg} = "fragmented messages are not yet supported"; + $self->{errorcode} = ERR_FRAGMENT_UNSUPPORTED; + return 0; + } + } + } + + if ($offset > 1) { + $applicationlayer = substr($applicationlayer,$offset); + $self->{cifield} = unpack('C', $applicationlayer); + $offset = 1; + } + + # initialize some fields + $self->{cw_1} = 0; + $self->{cw_2} = 0; + $self->{status} = 0; + $self->{statusstring} = ""; + $self->{access_no} = 0; + if ($self->{cifield} == CI_RESP_4 || $self->{cifield} == CI_RESP_SML_4) { # Short header #print "short header\n"; @@ -1463,10 +1748,49 @@ sub decodeApplicationLayer($) { $offset += 12; } elsif ($self->{cifield} == CI_RESP_0) { # no header - $self->{cw} = 0; + #print "No header\n"; + + } elsif ($self->{cifield} == 0x79 && $self->{manufacturer} eq 'KAM') { + #print "Kamstrup compact frame header\n"; + $self->{format_signature} = unpack("v", substr($applicationlayer,$offset, 2)); + $offset += 2; + $self->{full_frame_payload_crc} = unpack("v", substr($applicationlayer, $offset, 2)); + $offset += 2; + if ($self->{format_signature} == $self->checkCRC(pack("H*", "02FF20" . "0413" . "4413"))) { + # Info, Volume, Target Volume + # convert into full frame + $applicationlayer = pack("H*", "02FF20") . substr($applicationlayer, 5, 2) # Info + . pack("H*", "0413") . substr($applicationlayer,7,4) # volume + . pack("H*", "4413") . substr($applicationlayer,11,4); # target volume + $offset = 0; + } elsif ($self->{format_signature} == $self->checkCRC(pack("H*", "02FF20" . "0413" . "523B"))) { + # Info, Volume, Max flow + # convert into full frame + $applicationlayer = pack("H*", "02FF20") . substr($applicationlayer, 5, 2) # Info + . pack("H*", "0413") . substr($applicationlayer,7,4) # volume + . pack("H*", "523B") . substr($applicationlayer,11,2); # max flow + $offset = 0; + } elsif ($self->{format_signature} == $self->checkCRC(pack("H*", "02FF20" . "0413" . "4413" . "615B" . "6167"))) { + # Info, Volume, Max flow, flow temp, external temp + # convert into full frame + $applicationlayer = pack("H*", "02FF20") . substr($applicationlayer, 5, 2) # Info + . pack("H*", "0413") . substr($applicationlayer,7,4) # volume + . pack("H*", "4413") . substr($applicationlayer,11,4) # target volume + . pack("H*", "615B") . substr($applicationlayer,15,1) # flow temp + . pack("H*", "6167") . substr($applicationlayer,16,1); # external temp + $offset = 0; + } else { + $self->{errormsg} = 'Unknown Kamstrup compact frame format'; + $self->{errorcode} = ERR_UNKNOWN_COMPACT_FORMAT; + return 0; + } + if ($self->{full_frame_payload_crc} != $self->checkCRC($applicationlayer)) { + $self->{errormsg} = 'Kamstrup compact frame format payload CRC error'; + $self->{errorcode} = ERR_CRC_FAILED; + return 0; + } } else { # unsupported - $self->{cw} = 0; $self->decodeConfigword(); $self->{errormsg} = 'Unsupported CI Field ' . sprintf("%x", $self->{cifield}) . ", remaining payload is " . unpack("H*", substr($applicationlayer,$offset)); $self->{errorcode} = ERR_UNKNOWN_CIFIELD; @@ -1476,12 +1800,13 @@ sub decodeApplicationLayer($) { $self->decodeConfigword(); - my $payload; $self->{encryptionMode} = $encryptionModes{$self->{cw_parts}{mode}}; if ($self->{cw_parts}{mode} == 0) { # no encryption - $self->{isEncrypted} = 0; - $self->{decrypted} = 1; + if (!defined $self->{isEncrypted}) { + $self->{isEncrypted} = 0; + $self->{decrypted} = 1; + } $payload = substr($applicationlayer, $offset); } elsif ($self->{cw_parts}{mode} == 5) { # data is encrypted with AES 128, dynamic init vector @@ -1490,15 +1815,21 @@ sub decodeApplicationLayer($) { $self->{decrypted} = 0; if ($self->{aeskey}) { - $payload = $self->decrypt(substr($applicationlayer,$offset)); - if (unpack('n', $payload) == 0x2f2f) { - $self->{decrypted} = 1; - #printf("decrypted payload %s\n", unpack("H*", $payload)); + if ($hasCBC) { + $payload = $self->decrypt(substr($applicationlayer,$offset)); + if (unpack('n', $payload) == 0x2f2f) { + $self->{decrypted} = 1; + #printf("decrypted payload %s\n", unpack("H*", $payload)); + } else { + # Decryption verification failed + $self->{errormsg} = 'Decryption failed, wrong key?'; + $self->{errorcode} = ERR_DECRYPTION_FAILED; + #printf("%x\n", unpack('n', $payload)); + return 0; + } } else { - # Decryption verification failed - $self->{errormsg} = 'Decryption failed, wrong key?'; - $self->{errorcode} = ERR_DECRYPTION_FAILED; - #printf("%x\n", unpack('n', $payload)); + $self->{errormsg} = 'Crypt::Mode::CBC is not installed, please install it (sudo cpan -i Crypt::Mode::CBC)'; + $self->{errorcode} = ERR_CIPHER_NOT_INSTALLED; return 0; } } else { @@ -1533,33 +1864,73 @@ sub decodeLinkLayer($$) my $linklayer = shift; - ($self->{lfield}, $self->{cfield}, $self->{mfield}) = unpack('CCv', $linklayer); + $self->{afield} = substr($linklayer,4,6); $self->{afield_id} = sprintf("%08d", $self->decodeBCD(8,substr($linklayer,4,4))); ($self->{afield_ver}, $self->{afield_type}) = unpack('CC', substr($linklayer,8,2)); #printf("lfield %d\n", $self->{lfield}); - if ($self->{crc_size} > 0) { - $self->{crc0} = unpack('n', substr($linklayer,TL_BLOCK_SIZE, $self->{crc_size})); + if ($self->{frame_type} eq FRAME_TYPE_A) { + if ($self->{crc_size} > 0) { + $self->{crc0} = unpack('n', substr($linklayer,TL_BLOCK_SIZE, $self->{crc_size})); + + #printf("crc0 %x calc %x\n", $self->{crc0}, $self->checkCRC(substr($linklayer,0,10))); + + if ($self->{crc0} != $self->checkCRC(substr($linklayer,0,TL_BLOCK_SIZE))) { + $self->{errormsg} = "CRC check failed on link layer"; + $self->{errorcode} = ERR_CRC_FAILED; + #print "CRC check failed on link layer\n"; + return 0; + } + } + + # header block is 10 bytes + 2 bytes CRC, each following block is 16 bytes + 2 bytes CRC, the last block may be smaller + $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1); # this is without CRCs and the lfield itself + $self->{datablocks} = int($self->{datalen} / LL_BLOCK_SIZE); + $self->{datablocks}++ if $self->{datalen} % LL_BLOCK_SIZE != 0; + $self->{msglen} = TL_BLOCK_SIZE + $self->{crc_size} + $self->{datalen} + $self->{datablocks} * $self->{crc_size}; + + #printf("calc len %d, actual %d\n", $self->{msglen}, length($self->{msg})); + $self->{applicationlayer} = $self->removeCRC(substr($self->{msg},TL_BLOCK_SIZE + $self->{crc_size})); - #printf("crc0 %x calc %x\n", $self->{crc0}, $self->checkCRC(substr($linklayer,0,10))); - - if ($self->{crc0} != $self->checkCRC(substr($linklayer,0,TL_BLOCK_SIZE))) { - $self->{errormsg} = "CRC check failed on link layer"; - $self->{errorcode} = ERR_CRC_FAILED; - #print "CRC check failed on link layer\n"; - return 0; + } else { + # FRAME TYPE B + # each block is at most 129 bytes long. + # first contains the header (TL_BLOCK), L field and trailing crc + # L field is included in crc calculation + # each following block contains only data and trailing crc + my $length = 129; + if ($self->{lfield} < $length) { + $length = $self->{lfield}; + } + if ($self->{crc_size} > 0) { + $length -= $self->{crc_size}; + $length++; # for L field + #print "length: $length\n"; + $self->{crc0} = unpack('n', substr($self->{msg}, $length, $self->{crc_size})); + + #printf "crc in msg %x crc calculated %x\n", $self->{crc0}, $self->checkCRC(substr($self->{msg}, 0, $length)); + if ($self->{crc0} != $self->checkCRC(substr($self->{msg}, 0, $length))) { + $self->{errormsg} = "CRC check failed on block 1"; + $self->{errorcode} = ERR_CRC_FAILED; + return 0; + } + } + + $self->{datablocks} = int($self->{lfield} / 129); + $self->{datablocks}++ if $self->{lfield} % 129 != 0; + # header block is 10 bytes, following block + $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1) - ($self->{datablocks} * $self->{crc_size}) ; # this is with CRCs but without the lfield itself + $self->{msglen} = $self->{lfield}; + + if ($self->{datablocks} == 2) { + # TODO + } else { + $self->{applicationlayer} = substr($self->{msg}, TL_BLOCK_SIZE, $length - TL_BLOCK_SIZE); # - $self->{crc_size}); } } - # header block is 10 bytes + 2 bytes CRC, each following block is 16 bytes + 2 bytes CRC, the last block may be smaller - $self->{datalen} = $self->{lfield} - (TL_BLOCK_SIZE - 1); # this is without CRCs and the lfield itself - $self->{datablocks} = int($self->{datalen} / LL_BLOCK_SIZE); - $self->{datablocks}++ if $self->{datalen} % LL_BLOCK_SIZE != 0; - $self->{msglen} = TL_BLOCK_SIZE + $self->{crc_size} + $self->{datalen} + $self->{datablocks} * $self->{crc_size}; - - #printf("calc len %d, actual %d\n", $self->{msglen}, length($self->{msg})); if (length($self->{msg}) > $self->{msglen}) { $self->{remainingData} = substr($self->{msg},$self->{msglen}); } elsif (length($self->{msg}) < $self->{msglen}) { @@ -1567,6 +1938,8 @@ sub decodeLinkLayer($$) $self->{errorcode} = ERR_MSG_TOO_SHORT; return 0; } + + # according to the MBus spec only upper case letters are allowed. # some devices send lower case letters none the less # convert to upper case to make them spec conformant @@ -1575,6 +1948,11 @@ sub decodeLinkLayer($$) return 1; } +sub setFrameType($) +{ + my $self = shift; + $self->{frame_type} = shift; +} sub parse($$) { @@ -1583,6 +1961,10 @@ sub parse($$) $self->{errormsg} = ''; $self->{errorcode} = ERR_NO_ERROR; + if (substr($self->{msg}, 0, 4) == pack("H*", "543D543D")) { + $self->setFrameType(FRAME_TYPE_B); + $self->{msg} = substr($self->{msg},4); + } if ($self->decodeLinkLayer(substr($self->{msg},0,12)) != 0) { $self->{linkLayerOk} = 1; return $self->decodeApplicationLayer();