diff --git a/README.md b/README.md index eb2b950..0bac49b 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,12 @@ end - M-bus - wM-bus -- TPL Encryption modes: 0, 5 -- (partly) supports ELL (supports encryption modes: 0 (none), 1 (aes_128_ctr) +- TPL - supports security profiles `0`, `5`, `7` (ephemeral keys for enc and mac via KDF) +- ELL - supports encryption modes: `0` (none), `1` (aes_128_ctr) +- AFL - only unfragmented messages, No MAC check - Compact Frames and Format Frames - Compact Profiles -- VIF extension tables 0xFD and 0xFB (with a few exceptions) +- VIF extension tables `0xFD` and `0xFB` (with a few exceptions) ### Feature Requests @@ -45,19 +46,6 @@ No effort will be made towards supporting a device without an example of a paylo > Under no circumstances should you publish data to the issue tracker or repository from a device that is actually deployed, as those might contain PII or other sensitive information. > The same is true encryption keys where the key might potentially be used in more than once device. - -### Planned - -- [ ] Better API for encryption key storage. -- [ ] Transition the parser fully to handler + context based, and avoid raising on error but instead attach an error to the context. The APL layer in particular needs a lot of work here. -- [ ] DLL CRC support. -- [ ] Split parsing and decoding into two steps. Currently the code parses the binary and decodes it in the same walk, but this causes errors when we can't decode an unsupported feature. - -### Not Planned - -- [ ] Authentication and Fragmentation layer CI=90 - - ## Performance It's fine. @@ -107,6 +95,7 @@ iex> "2E4493157856341233037A2A0020255923C95AAA26D1B2E7493B013EC4A6F6D3529B520EDF version: 51, device: %Exmbus.Parser.Tpl.Device{id: 3} }, + afl: %Exmbus.Parser.Afl.None{}, ell: %Exmbus.Parser.Ell.None{}, tpl: %Exmbus.Parser.Tpl{ frame_type: :full_frame, diff --git a/lib/exmbus/crypto.ex b/lib/exmbus/crypto.ex index 416587e..526fec0 100644 --- a/lib/exmbus/crypto.ex +++ b/lib/exmbus/crypto.ex @@ -51,4 +51,45 @@ defmodule Exmbus.Crypto do :error, {_tag, _c_file_info, _description} = e -> {:error, {:crypto_error, e}} end end + + @doc """ + Runs the KDF-A key derivation function as described in EN 13757-7:2018 (9.6.2) + """ + @spec kdf_a( + direction :: :from_meter | :to_meter, + mode :: :enc | :mac, + counter :: integer(), + meter_id :: binary(), + message_key :: binary() + ) :: {:ok, binary()} | {:error, reason :: any()} + def kdf_a(direction, mode, counter, meter_id, message_key) + when is_number(counter) and is_binary(meter_id) and is_binary(message_key) do + # DC is described as follows: + # | Sequence | Applicable key | + # |----------|--------------------------------------------------| + # | 0x00 | Encryption from the meter (Kenc) | + # | 0x01 | MAC from the meter (Kmac) | + # | 0x10 | Encryption from the communication partner (Lenc) | + # | 0x11 | MAC from the communication partner (Lmac) | + dc = + case {direction, mode} do + {:from_meter, :enc} -> 0x00 + {:from_meter, :mac} -> 0x01 + {:to_meter, :enc} -> 0x10 + {:to_meter, :mac} -> 0x11 + end + + data = + <> + + case :crypto.mac(:cmac, :aes_128_cbc, message_key, data) do + binary when is_binary(binary) -> {:ok, binary} + end + end + + def kdf_a!(direction, mode, counter, meter_id, message_key) do + case kdf_a(direction, mode, counter, meter_id, message_key) do + {:ok, key} -> key + end + end end diff --git a/lib/exmbus/parser/afl.ex b/lib/exmbus/parser/afl.ex new file mode 100644 index 0000000..4502415 --- /dev/null +++ b/lib/exmbus/parser/afl.ex @@ -0,0 +1,168 @@ +defmodule Exmbus.Parser.Afl do + @moduledoc """ + Authentication and Fragmentation Layer (AFL) as per EN 13757-7:2018 + + + > The Authentication and Fragmentation Sublayer provides three essential services: + > + > — fragmentation of long messages in multiple datagrams; + > — a Message Authentication Code (MAC) to prove the authenticity of the TPL and APL; + > — a Message counter which supplies a security relevant message identification that may be used for the key derivation function (refer to 9.6.1). + > + > This optional layer shall be applied if at least one of these services is required. + + Overview of the AFL layer: + + | Size (bytes) | Field Name | Description | + |--------------|------------|-----------------------------------------------------------------------------| + | 1 | CI | Indicates that an Authentication and Fragmentation Sublayer follows. | + | 1 | AFLL | AFL-Length | + | 2 | FCL | Fragmentation Control field | + | 1 | MCL | Message Control field *a | + | 2 | KI | Key Information field *a | + | 4 | MCR | Message counter field *a | + | N *b | MAC | Message Authentication Code *a | + | 2 | ML | Message Length field *a | + + - `*a` This is an optional field. Their inclusion is defined by the Fragmentation Control Field specified in 6.3.2. + - `*b` The length of MAC depends on AT-subfield in AFL.MCL. + + > All multi byte fields of AFL except the AFL.MAC shall be transmitted with least significant byte first (little endian). + """ + + alias Exmbus.Parser.Afl.MessageControlField + alias Exmbus.Parser.Afl.FragmentationControlField + alias Exmbus.Parser.Context + alias Exmbus.Parser.Afl.None + + defstruct [ + # Fragmentation Control Field + fcl: nil, + # Message Control Field + mcl: nil, + # Key Information Field + ki: nil, + # Message Counter Field + mcr: nil, + # Message Authentication Code (MAC) + mac: nil, + # Message Length + ml: nil + ] + + @doc """ + Returns true if this message is part of a fragmented message. + """ + def fragmented?(%__MODULE__{fcl: fcl}) do + FragmentationControlField.fragmented?(fcl) + end + + @doc """ + Parses an AFL and add it to the parse context. + + In contrast to `parse/1`, this function will not fail if the data + doesn't contain an AFL. Instead, it will assign a `%None{}` struct + to the ell context field. + """ + def maybe_parse(%{bin: <<0x90, _::binary>>} = ctx), do: parse(ctx) + def maybe_parse(%{bin: <<_ci, _rest::binary>>} = ctx), do: {:next, %{ctx | afl: %None{}}} + + @doc """ + Parses an AFL and add it to the parse context. + """ + + # AFL Fields: AFLL, FCL, MCL, KI, MCR, MAC, ML + # see table in module doc + # we can parse up to the MAC + def parse(%{bin: <<0x90, afll::8, afl_bytes::binary-size(afll), rest::binary>>} = ctx) do + <> = afl_bytes + {:ok, fcl} = FragmentationControlField.decode(fcl_bytes) + + # our "accumulator" for the AFL layer + afl = %__MODULE__{fcl: fcl} + + # consume the rest of the AFL. Each field is optional and + # may or may not be present. It's presence is defined by the + # Fragmentation Control Field (FCL) and the Message Control Field (MCL) (which itself is optional) + with {:ok, afl, bytes} <- consume_mcl(bytes, afl), + {:ok, afl, bytes} <- consume_ki(bytes, afl), + {:ok, afl, bytes} <- consume_mcr(bytes, afl), + {:ok, afl, bytes} <- consume_mac(bytes, afl), + {:ok, afl, bytes} <- consume_ml(bytes, afl) do + # AFL should be the afl struct + %__MODULE__{} = afl + # there should be no remaining AFL bytes + <<>> = bytes + # we have a complete AFL layer + {:next, %{ctx | afl: afl, bin: rest}} + else + {:error, reason} -> {:halt, Context.add_error(ctx, reason)} + end + end + + def parse(%{bin: <>} = ctx) do + {:halt, Context.add_error(ctx, {:ci_not_afl, ci})} + end + + defp consume_mcl( + <>, + %{fcl: %{message_control_present?: true}} = afl + ) do + with {:ok, mcl} <- MessageControlField.decode(bytes) do + {:ok, %{afl | mcl: mcl}, rest} + end + end + + defp consume_mcl(rest, %{fcl: %{message_control_present?: false}} = afl) do + {:ok, afl, rest} + end + + defp consume_ki( + <>, + %{fcl: %{key_information_present?: true}} = afl + ) do + with {:ok, ki} <- Exmbus.Parser.Afl.KeyInformationField.decode(bytes) do + {:ok, %{afl | ki: ki}, rest} + end + end + + defp consume_ki(rest, %{fcl: %{key_information_present?: false}} = afl) do + {:ok, afl, rest} + end + + defp consume_mcr( + <>, + %{fcl: %{message_counter_present?: true}} = afl + ) do + with {:ok, mcr} <- Exmbus.Parser.Afl.MessageCounterField.decode(bytes) do + {:ok, %{afl | mcr: mcr}, rest} + end + end + + defp consume_mcr(rest, %{fcl: %{message_counter_present?: false}} = afl) do + {:ok, afl, rest} + end + + defp consume_mac(bytes, %{fcl: %{mac_present?: true}, mcl: %MessageControlField{} = mcl} = afl) do + {_, mac_size} = MessageControlField.authentication_type(mcl) + <> = bytes + {:ok, %{afl | mac: mac}, rest} + end + + defp consume_mac(rest, %{fcl: %{mac_present?: false}} = afl) do + {:ok, afl, rest} + end + + defp consume_ml( + <>, + %{fcl: %{message_length_present?: true}} = afl + ) do + with {:ok, ml} <- Exmbus.Parser.Afl.MessageLengthField.decode(bytes) do + {:ok, %{afl | ml: ml}, rest} + end + end + + defp consume_ml(rest, %{fcl: %{message_length_present?: false}} = afl) do + {:ok, afl, rest} + end +end diff --git a/lib/exmbus/parser/afl/fragmentation_control_field.ex b/lib/exmbus/parser/afl/fragmentation_control_field.ex new file mode 100644 index 0000000..aae9008 --- /dev/null +++ b/lib/exmbus/parser/afl/fragmentation_control_field.ex @@ -0,0 +1,128 @@ +defmodule Exmbus.Parser.Afl.FragmentationControlField do + @moduledoc """ + AFL Fragmentation Control Field (FCL) as per EN 13757-7:2018 + + | Bit | Field Name | Description | + |----------|------------|--------------------------------------------------| + | 15 | RES | Reserved (`0b0` by default) | + | 14 | MF | More-Fragments | + | | | `0b0` This is the last fragment | + | | | `0b1` More fragments are following | + | 13 | MCLP | Message Control in this Fragment Present *a | + | 12 | MLP | Message Length in this Fragment Present *a | + | 11 | MCRP | Message counter in this Fragment Present *a | + | 10 | MACP | MAC in this Fragment Present *a | + | 9 | KIP | Key Information in this Fragment Present *a | + | 8 | RES | Reserved (`0b0` by default) | + | 7 to 0 | FID | Fragment-ID | + + - `*a` 0 = field is not present; 1 = field is present + + The Fragment ID is used for the identification of each single fragment of a long message. + Set FID to 1 for the first fragment of a fragmented message. FID shall increment with each fragment. The FID shall never roll over. For unfragmented messages the FID shall be set to 0. + + """ + + defstruct [ + # more fragments? + more_fragments?: nil, + # Message Control (MCL) present? + message_control_present?: nil, + # Message Length (ML) present? + message_length_present?: nil, + # Message Counter (MCR) present? + message_counter_present?: nil, + # Message Authentication Code (MAC) present? + mac_present?: nil, + # Key Information (KI) present? + key_information_present?: nil, + # fragment ID (FID) + # A fragment ID of 0x00 indicates that the message is not fragmented. + # The first fragment of a message has the fragment ID 0x01. + fragment_id: nil + ] + + @doc "If the fragment ID is greater than 0, the message is fragmented" + def fragmented?(%__MODULE__{fragment_id: fid}), do: fid > 0 + + @doc """ + Parses the Fragmentation Control Field (FCL) from the AFL. + + ## Examples + + iex> decode(<<0b00000000, 0b00101100>>) + {:ok, %Exmbus.Parser.Afl.FragmentationControlField{ + more_fragments?: false, + message_control_present?: true, + message_length_present?: false, + message_counter_present?: true, + mac_present?: true, + key_information_present?: false, + fragment_id: 0 + }} + """ + def decode(<< + # fragment ID (FID) + fragment_id::8, + # reserved + _::1, + # more fragments? 1==more fragments, 0==last fragment + more_fragments::1, + # Message Control (MCL) present? + message_control_present::1, + # Message Length (ML) present? + message_length_present::1, + # Message Counter (MCR) present? + message_counter_present::1, + # Message Authentication Code (MAC) present? + mac_present::1, + # Key Information (KI) present? + key_information_present::1, + # reserved + _::1 + >>) do + {:ok, + %__MODULE__{ + more_fragments?: more_fragments == 1, + message_control_present?: message_control_present == 1, + message_length_present?: message_length_present == 1, + message_counter_present?: message_counter_present == 1, + mac_present?: mac_present == 1, + key_information_present?: key_information_present == 1, + fragment_id: fragment_id + }} + end + + @doc """ + Encodes the Fragmentation Control Field (FCL) to a binary format. + + ## Examples + + iex> encode(%Exmbus.Parser.Afl.FragmentationControlField{ + ...> more_fragments?: false, + ...> message_control_present?: true, + ...> message_length_present?: false, + ...> message_counter_present?: true, + ...> mac_present?: true, + ...> key_information_present?: false, + ...> fragment_id: 0 + ...> }) + <<0b00000000, 0b00101100>> + """ + def encode(%__MODULE__{} = fcl) do + << + fcl.fragment_id::8, + 0::1, + bool_to_int(fcl.more_fragments?)::1, + bool_to_int(fcl.message_control_present?)::1, + bool_to_int(fcl.message_length_present?)::1, + bool_to_int(fcl.message_counter_present?)::1, + bool_to_int(fcl.mac_present?)::1, + bool_to_int(fcl.key_information_present?)::1, + 0::1 + >> + end + + defp bool_to_int(true), do: 1 + defp bool_to_int(false), do: 0 +end diff --git a/lib/exmbus/parser/afl/key_information_field.ex b/lib/exmbus/parser/afl/key_information_field.ex new file mode 100644 index 0000000..2d66193 --- /dev/null +++ b/lib/exmbus/parser/afl/key_information_field.ex @@ -0,0 +1,77 @@ +defmodule Exmbus.Parser.Afl.KeyInformationField do + @moduledoc """ + AFL Key Information Field (KIF) as per EN 13757-7:2018 + + | Bit | Field Name | Description | + |----------|--------------|-----------------------------------------------------------------------------| + | 15 to 8 | Key Version | The Key Version identifies the applied key version, as specified in 7.7.1. | + | 7 to 6 | RES | Reserved (`0b0` by default) | + | 5 to 4 | KDF-Selection| The KDF-Selection identifies the applied Key Derivation Function, as specified in Table 25. | + | 3 to 0 | Key ID | The Key ID identifies the applied key, as specified in Table 24. | + + In case of individual fragment authentication, the message key shall be applied for all fragments of the message. + If the KI is not present, values of the configuration field in the TPL shall be used for the key selection. + """ + + defstruct [ + # Key Version + key_version: nil, + # KDF-Selection + kdf_selection: nil, + # Key ID + key_id: nil + ] + + def kdf(%__MODULE__{kdf_selection: kdf_selection}) do + case kdf_selection do + 0 -> :persistent_key + 1 -> :kdf_a + n -> {:reserved, n} + end + end + + @doc """ + Parses the Key Information Field (KI) from the AFL. + + ## Examples + + iex> decode(<<0b00000000, 0b00000000>>) + {:ok, %Exmbus.Parser.Afl.KeyInformationField{ + key_version: 0, + kdf_selection: 0, + key_id: 0 + }} + """ + def decode(<< + # Key Version + key_version::8, + # reserved + _::2, + # KDF-Selection + kdf_selection::2, + # Key ID + key_id::4 + >>) do + {:ok, %__MODULE__{key_version: key_version, kdf_selection: kdf_selection, key_id: key_id}} + end + + @doc """ + Encode the Key Information Field (KI) to binary. + + ## Examples + + iex> encode(%Exmbus.Parser.Afl.KeyInformationField{ + ...> key_version: 0, + ...> kdf_selection: 0, + ...> key_id: 0 + ...> }) + <<0x00, 0x00>> + """ + def encode(%__MODULE__{ + key_version: key_version, + kdf_selection: kdf_selection, + key_id: key_id + }) do + <> + end +end diff --git a/lib/exmbus/parser/afl/message_control_field.ex b/lib/exmbus/parser/afl/message_control_field.ex new file mode 100644 index 0000000..e4b2895 --- /dev/null +++ b/lib/exmbus/parser/afl/message_control_field.ex @@ -0,0 +1,117 @@ +defmodule Exmbus.Parser.Afl.MessageControlField do + @moduledoc """ + AFL Message Control Field (MCL) as per EN 13757-7:2018 + + + | Bit | Field Name | Description | + |-----|------------|--------------------------------------------------| + | 7 | RES | Reserved (`0b0` by default) | + | 6 | MLMP | Message Length in Message Present *a | + | 5 | MCMP | Message counter in Message Present *a | + | 4 | KIMP | Key Information in Message Present *a | + | 3 | AT | | + | 2 | AT | Authentication-Type (see Table 6) | + | 1 | AT | | + | 0 | AT | | + + - `*a` 0 = field is not present; 1 = field is present + + The bits 4 to 7 in the AFL.MCL field define the presence of additional fields in the message. + + > If the AFL.MCL field is used it always shall be present in the first fragment. It shall not be present in any following fragments of the same message. + + That is, we can expect the MCL field in fragment_id == 0x01. + + """ + + defstruct [ + # Message Length in Message Present + message_length_present?: nil, + # Message counter in Message Present + message_counter_present?: nil, + # Key Information in Message Present + key_information_present?: nil, + # Authentication-Type (AT) + authentication_type: nil + ] + + @doc """ + Returns the authentication type and it's length + """ + def authentication_type(%__MODULE__{authentication_type: 0}), do: {:none, 0} + def authentication_type(%__MODULE__{authentication_type: 3}), do: {:aes_cmac_128, 2} + def authentication_type(%__MODULE__{authentication_type: 4}), do: {:aes_cmac_128, 4} + def authentication_type(%__MODULE__{authentication_type: 5}), do: {:aes_cmac_128, 8} + def authentication_type(%__MODULE__{authentication_type: 6}), do: {:aes_cmac_128, 12} + def authentication_type(%__MODULE__{authentication_type: 7}), do: {:aes_cmac_128, 16} + def authentication_type(%__MODULE__{authentication_type: 8}), do: {:aes_gmac_128, 12} + def authentication_type(%__MODULE__{authentication_type: 9}), do: {:aes_gmac_128, 16} + def authentication_type(%__MODULE__{authentication_type: n}), do: {{:reserved, n}, 0} + + @doc """ + Parses the Message Control Field (MCL) from the AFL. + + ## Examples + + iex> decode(<<0b00000000>>) + {:ok, %Exmbus.Parser.Afl.MessageControlField{ + message_length_present?: false, + message_counter_present?: false, + key_information_present?: false, + authentication_type: 0 + }} + """ + def decode(<< + # reserved + _::1, + # Message Length in Message Present + mlmp::1, + # Message counter in Message Present + mcmp::1, + # Key Information in Message Present + kimp::1, + # Authentication-Type (AT) + at::4 + >>) do + {:ok, + %__MODULE__{ + message_length_present?: mlmp == 1, + message_counter_present?: mcmp == 1, + key_information_present?: kimp == 1, + authentication_type: at + }} + end + + @doc """ + Encodes the Message Control Field (MCL) to a binary. + + ## Examples + + iex> encode(%Exmbus.Parser.Afl.MessageControlField{ + ...> message_length_present?: false, + ...> message_counter_present?: false, + ...> key_information_present?: false, + ...> authentication_type: 0b0000 + ...> }) + <<0b00000000>> + + iex> encode(%Exmbus.Parser.Afl.MessageControlField{ + ...> message_length_present?: true, + ...> message_counter_present?: true, + ...> key_information_present?: true, + ...> authentication_type: 0b0011 + ...> }) + <<0b01110011>> + """ + def encode(%__MODULE__{ + message_length_present?: mlmp, + message_counter_present?: mcmp, + key_information_present?: kimp, + authentication_type: at + }) do + <<0::1, bool_to_int(mlmp)::1, bool_to_int(mcmp)::1, bool_to_int(kimp)::1, at::4>> + end + + defp bool_to_int(true), do: 1 + defp bool_to_int(false), do: 0 +end diff --git a/lib/exmbus/parser/afl/message_counter_field.ex b/lib/exmbus/parser/afl/message_counter_field.ex new file mode 100644 index 0000000..acfcd49 --- /dev/null +++ b/lib/exmbus/parser/afl/message_counter_field.ex @@ -0,0 +1,19 @@ +defmodule Exmbus.Parser.Afl.MessageCounterField do + @moduledoc """ + AFL Message Counter Field (MCR) as per EN 13757-7:2018 + The Message Counter Field (MCR) is a 4 byte field that contains a counter + + The presence of the filed AFL.MCR depends on the selected Security mode. See 9.4 for details. + If the Message counter Field is used, the AFL.MCR field shall always be present in the first fragment. + It shall not be present in any following fragments of the same message. + If the AFL.MCR is not present, values of the Message counter field in the TPL shall be used for the AFL.MCR. + """ + + def decode(<>) do + {:ok, mcr} + end + + def encode(mcr) when is_integer(mcr) do + <> + end +end diff --git a/lib/exmbus/parser/afl/message_length_field.ex b/lib/exmbus/parser/afl/message_length_field.ex new file mode 100644 index 0000000..4fe2463 --- /dev/null +++ b/lib/exmbus/parser/afl/message_length_field.ex @@ -0,0 +1,20 @@ +defmodule Exmbus.Parser.Afl.MessageLengthField do + @moduledoc """ + AFL Message Length Field (ML) as per EN 13757-7:2018 + + The field AFL.ML (see Table 9) declares the number of bytes following AFL.ML until the end + of the unfragmented message (excluding Link Layer fields like CRC or checksum). + The message length shall be calculated before the message is separated in several fragments. + + The AFL.ML Message Length Field shall only be present in the first fragment of a fragmented message + to indicate the total message length. For unfragmented messages, the field AFL.ML can be disabled. + """ + + def decode(<>) do + {:ok, mcr} + end + + def encode(mcr) when is_integer(mcr) do + <> + end +end diff --git a/lib/exmbus/parser/afl/none.ex b/lib/exmbus/parser/afl/none.ex new file mode 100644 index 0000000..aa282e0 --- /dev/null +++ b/lib/exmbus/parser/afl/none.ex @@ -0,0 +1,7 @@ +defmodule Exmbus.Parser.Afl.None do + @moduledoc """ + This module represents the case where no AFL is present in the data. + """ + + defstruct [] +end diff --git a/lib/exmbus/parser/context.ex b/lib/exmbus/parser/context.ex index 709b511..89604c2 100644 --- a/lib/exmbus/parser/context.ex +++ b/lib/exmbus/parser/context.ex @@ -10,8 +10,10 @@ defmodule Exmbus.Parser.Context do bin: binary | nil, # dll: any, - tpl: any, ell: any, + # + afl: any, + tpl: any, apl: any, # dib: any, @@ -27,6 +29,8 @@ defmodule Exmbus.Parser.Context do &Exmbus.Parser.Ell.maybe_parse/1, # apply decryption from the ELL to remaining data &Exmbus.Parser.Ell.maybe_decrypt_bin/1, + # Parse the AFL + &Exmbus.Parser.Afl.maybe_parse/1, # parse the TPL &Exmbus.Parser.Tpl.parse/1, # apply decryption from the TPL to remaining data @@ -51,6 +55,7 @@ defmodule Exmbus.Parser.Context do # lower layers: dll: nil, ell: nil, + afl: nil, tpl: nil, apl: nil, # state for when parsing data record: diff --git a/lib/exmbus/parser/ell.ex b/lib/exmbus/parser/ell.ex index 8348d12..335138e 100644 --- a/lib/exmbus/parser/ell.ex +++ b/lib/exmbus/parser/ell.ex @@ -6,6 +6,7 @@ defmodule Exmbus.Parser.Ell do See also the Exmbus.Parser.CI module. """ + alias Exmbus.Parser.Ell.UnencryptedWithReceiver alias Exmbus.Parser.Context alias Exmbus.Parser.Ell.CommunicationControl alias Exmbus.Parser.Ell.SessionNumber @@ -73,12 +74,13 @@ defmodule Exmbus.Parser.Ell do # > This extended link layer specifies the receiver address. # > Table 46 below shows the complete extension block in this case. # Fields: CC, ACC, M2, A2 - def parse(%{ - bin: - <<0x8E, _cc::binary-size(1), _acc, _m2::binary-size(2), _a2::binary-size(6), - _rest::binary>> - }) do - raise "TODO: ELL III" + def parse(%{bin: <<0x8E, ell::binary-size(10), rest::binary>>} = ctx) do + with {:ok, ell} <- UnencryptedWithReceiver.decode(ell) do + {:next, %{ctx | ell: ell, bin: rest}} + else + {:error, reason} -> + {:halt, Context.add_error(ctx, {:ell_parse_error, reason, ci: 0x8E})} + end end # > This value of the CI-field is used if data encryption at the link layer is used in the frame. diff --git a/lib/exmbus/parser/ell/unencrypted_with_receiver.ex b/lib/exmbus/parser/ell/unencrypted_with_receiver.ex new file mode 100644 index 0000000..1cd029b --- /dev/null +++ b/lib/exmbus/parser/ell/unencrypted_with_receiver.ex @@ -0,0 +1,28 @@ +defmodule Exmbus.Parser.Ell.UnencryptedWithReceiver do + @moduledoc """ + This module represents an unencrypted ELL layer from EN 13757-4:2019 + + This is used if data encryption at the link layer is not used in the frame. + This extended link layer specifies the receiver address. + Table 46 shows the complete extension block in this case. + """ + alias Exmbus.Parser.Identity + alias Exmbus.Parser.Ell.CommunicationControl + + defstruct communication_control: nil, + access_no: nil, + receiver: nil + + def decode(<>) do + {:ok, control} = CommunicationControl.decode(cc) + + with {:ok, receiver} <- Identity.decode(<>) do + {:ok, + %__MODULE__{ + communication_control: control, + access_no: acc, + receiver: receiver + }} + end + end +end diff --git a/lib/exmbus/parser/identity.ex b/lib/exmbus/parser/identity.ex new file mode 100644 index 0000000..fa98156 --- /dev/null +++ b/lib/exmbus/parser/identity.ex @@ -0,0 +1,34 @@ +defmodule Exmbus.Parser.Identity do + @moduledoc """ + Identity of a device (meter, gateway, partner, etc) + """ + alias Exmbus.Parser.IdentificationNo + alias Exmbus.Parser.Tpl.Device + alias Exmbus.Parser.Manufacturer + + defstruct identification_no: nil, + manufacturer: nil, + version: nil, + device: nil + + @type t :: %__MODULE__{ + identification_no: String.t(), + manufacturer: String.t(), + version: integer(), + device: Exmbus.Parser.Tpl.Device.t() + } + + def decode(<>) do + with {:ok, identification_no} <- IdentificationNo.decode(id_b), + {:ok, device} <- Device.decode(d_b), + {:ok, manufacturer} <- Manufacturer.decode(man_b) do + {:ok, + %__MODULE__{ + identification_no: identification_no, + manufacturer: manufacturer, + version: v, + device: device + }} + end + end +end diff --git a/lib/exmbus/parser/tpl.ex b/lib/exmbus/parser/tpl.ex index e0f5f9a..503ef58 100644 --- a/lib/exmbus/parser/tpl.ex +++ b/lib/exmbus/parser/tpl.ex @@ -6,6 +6,7 @@ defmodule Exmbus.Parser.Tpl do See also the Exmbus.Parser.CI module. """ + alias Exmbus.Parser.Afl alias Exmbus.Parser.IdentificationNo alias Exmbus.Parser.CI alias Exmbus.Parser.Context @@ -32,6 +33,11 @@ defmodule Exmbus.Parser.Tpl do is not a CI field describing a transport layer. """ def parse(ctx) do + # guard against fragmented messages, which we don't currently have a good solution for. + if is_struct(ctx.afl, Afl) and Afl.fragmented?(ctx.afl) do + raise "AFL is fragmented, cannot parse TPL" + end + # Allow only TPL and APL CI codes. # If if hit an ELL, AFL or similar, we error out. case CI.lookup(ctx.bin) do @@ -146,19 +152,16 @@ defmodule Exmbus.Parser.Tpl do end # TPL header decoders - defp parse_tpl_header_short( - <> - ) do - # NOTE: this decode currently does not return {:error, _} - case ConfigurationField.decode(cf_bytes) do - {:ok, configuration_field} -> - header = %Short{ - access_no: access_no, - status: Status.decode(status_byte), - configuration_field: configuration_field - } + defp parse_tpl_header_short(<>) do + # parse the configuration field (which is variable width depending on security mode) + with {:ok, configuration_field, rest} <- ConfigurationField.parse(rest) do + header = %Short{ + access_no: access_no, + status: Status.decode(status_byte), + configuration_field: configuration_field + } - {:ok, header, rest} + {:ok, header, rest} end end @@ -178,13 +181,12 @@ defmodule Exmbus.Parser.Tpl do """ def parse_tpl_header_long( <> + device_byte::binary-size(1), access_no, status_byte::binary-size(1), rest::binary>> ) do with {:ok, identification_no} <- IdentificationNo.decode(ident_bytes), {:ok, device} <- Device.decode(device_byte), {:ok, manufacturer} <- Manufacturer.decode(man_bytes), - {:ok, configuration_field} <- ConfigurationField.decode(cf_bytes) do + {:ok, configuration_field, rest} <- ConfigurationField.parse(rest) do header = %Long{ identification_no: identification_no, manufacturer: manufacturer, diff --git a/lib/exmbus/parser/tpl/configuration_field.ex b/lib/exmbus/parser/tpl/configuration_field.ex index 88bd64f..28dc6f2 100644 --- a/lib/exmbus/parser/tpl/configuration_field.ex +++ b/lib/exmbus/parser/tpl/configuration_field.ex @@ -12,7 +12,14 @@ defmodule Exmbus.Parser.Tpl.ConfigurationField do syncrony: false, accessibility: false, bidirectional: false, - blocks: nil + blocks: nil, + # present in mode 7: + padding: nil, + content_index: nil, + counter: nil, + key_version: nil, + key_id: nil, + kdf: nil @type t :: %__MODULE__{ hop_count: 0..1, @@ -25,13 +32,6 @@ defmodule Exmbus.Parser.Tpl.ConfigurationField do blocks: nil | 0..15 } - @spec decode(binary()) :: {:ok, t()} - def decode(<>) do - # we flip the bits so they are MSB,LSB. Easier to read. - # we could collapse this but performance benefit is effectively 0. - be_decode(<>) - end - # common bit names: # - H :: Hop Count Used in repeated messages. # - R :: Repeater Access Used in repeated messages. @@ -64,8 +64,23 @@ defmodule Exmbus.Parser.Tpl.ConfigurationField do # DD=00 Persistent Key, no key derivation # DD=01 Key Derivation Function A (see 9.6.1) # DD=10 and DD=11 are reserved + + @doc """ + Return a symbolic name for the KDF used in the configuration field. + """ + def kdf_selection(%__MODULE__{kdf: kdf}) do + case kdf do + 0 -> :persistent_key + 1 -> :kdf_a + _ -> :reserved + end + end + + # NOTE: binary comes in as little endian, so the order of the below + # parse is byte-wise reversed from the order in the spec. + # security Mode 0 - defp be_decode(<>) do + def parse(<<_res::4, cc::2, r::1, h::1, b::1, a::1, s::1, 0::5, rest::binary>>) do cf = %__MODULE__{ hop_count: h, repeater_access: r, @@ -77,11 +92,11 @@ defmodule Exmbus.Parser.Tpl.ConfigurationField do blocks: nil } - {:ok, cf} + {:ok, cf, rest} end # Security Mode 5. AES-128 CBC (9.4.4 for details) - defp be_decode(<>) do + def parse(<>) do cf = %__MODULE__{ hop_count: h, repeater_access: r, @@ -93,11 +108,48 @@ defmodule Exmbus.Parser.Tpl.ConfigurationField do blocks: blocks } - {:ok, cf} + {:ok, cf, rest} + end + + # Security mode 7 Configuration field (from 7.7.5) + # NOTE: mode 7 has an additional 8 bits extension of configuration field data, and optionally + # even more depending on the data in the extension + def parse( + <> + ) do + counter_bits = if(z == 1, do: 32, else: 0) + key_version_bits = if(v == 1, do: 8, else: 0) + + << + counter::little-size(counter_bits), + key_version::little-size(key_version_bits), + rest::binary + >> = rest + + cf = %__MODULE__{ + hop_count: 0, + repeater_access: 0, + content_of_message: cc, + syncrony: false, + accessibility: false, + bidirectional: false, + mode: 7, + blocks: blocks, + padding: p == 1, + content_index: iiii, + counter: if(z == 1, do: counter), + key_version: if(v == 1, do: key_version), + key_id: kkkk, + kdf: dd + } + + {:ok, cf, rest} end # raise if unknown encryption mode - defp be_decode(<<_::3, mode::5, _::8>> = cfbin) do + def parse(<<_::8, _::3, mode::5, _rest::binary>> = bin) do + <> = bin + raise "Encryption mode #{mode} not implemented. configuration field bits were #{Exmbus.Debug.to_bits(cfbin)}" end end diff --git a/lib/exmbus/parser/tpl/encryption.ex b/lib/exmbus/parser/tpl/encryption.ex index ed938fb..bcb203b 100644 --- a/lib/exmbus/parser/tpl/encryption.ex +++ b/lib/exmbus/parser/tpl/encryption.ex @@ -3,6 +3,9 @@ defmodule Exmbus.Parser.Tpl.Encryption do This module handles the encryption of the TPL layer. """ + alias Exmbus.Parser.Tpl.ConfigurationField + alias Exmbus.Crypto + alias Exmbus.Parser.Afl alias Exmbus.Key alias Exmbus.Parser.Context alias Exmbus.Parser.Dll.Wmbus @@ -74,6 +77,36 @@ defmodule Exmbus.Parser.Tpl.Encryption do end end + defp decrypt_to_context(7, ctx) do + {:ok, encrypted_byte_count} = encrypted_byte_count(ctx.tpl) + <> = ctx.bin + + key_result = + case ConfigurationField.kdf_selection(ctx.tpl.header.configuration_field) do + :persistent_key -> + Key.get(ctx) + + # An ephemeral key shall be used which is generated with the + # Key Derivation Function (KDF) and which is described in 9.6 + :kdf_a -> + with {:ok, keys} <- Key.get(ctx) do + kdf_a(keys, :enc, ctx) + end + end + + # Security mode 7 uses AES-128-CBC with an ephemeral key of 128 bits + # and a static Initialization Vector IV = 0 (16 bytes of 0x00). + iv = <<0::128>> + + with {:ok, keys} <- key_result, + {:ok, decrypted} <- decrypt_mode_5(encrypted, keys, iv) do + {:next, %{ctx | bin: <>}} + else + {:error, reason} -> + {:halt, Context.add_error(ctx, reason)} + end + end + defp decrypt_to_context(mode, ctx) do {:halt, Context.add_error(ctx, {:unknown_encryption_mode, mode})} end @@ -105,32 +138,73 @@ defmodule Exmbus.Parser.Tpl.Encryption do # Generate the IV for mode 5 encryption defp ctx_to_mode_5_iv(%{tpl: %Tpl{header: %Tpl.Header.Short{} = header}, dll: %Wmbus{} = wmbus}) do - mode_5_iv( - wmbus.manufacturer, - wmbus.identification_no, - wmbus.version, - wmbus.device, - header.access_no - ) + mode_5_iv(wmbus, header.access_no) end defp ctx_to_mode_5_iv(%{tpl: %Tpl{header: %Tpl.Header.Long{} = header}}) do - mode_5_iv( - header.manufacturer, - header.identification_no, - header.version, - header.device, - header.access_no - ) + mode_5_iv(header, header.access_no) end - defp mode_5_iv(manufacturer, identification_no, version, device, access_no) do - {:ok, man_bytes} = Manufacturer.encode(manufacturer) - {:ok, id_bytes} = IdentificationNo.encode(identification_no) - {:ok, device_byte} = Device.encode(device) - + defp mode_5_iv(%{} = meter_id, access_no) do {:ok, - <>} + <>} + end + + defp encode_meter_id(%{manufacturer: m, identification_no: i, version: v, device: d}) do + {:ok, man_bytes} = Manufacturer.encode(m) + {:ok, id_bytes} = IdentificationNo.encode(i) + {:ok, device_byte} = Device.encode(d) + + <> + end + + defp find_meter_id(%{tpl: %Tpl{header: %Tpl.Header.Long{} = header}}) do + header + end + + defp find_meter_id(%{tpl: %Tpl{header: %Tpl.Header.Short{}}, dll: %Wmbus{} = wmbus}) do + wmbus + end + + # The Key Derivation Function shall apply the CMAC-Function according to NIST/SP 800-38B. + # This Key Derivation Function bases on key expansion procedure of NIST/SP 800–56C. + # The calculation of key K shall be as follows: K=CMAC(MK,DC||C||ID||07h ||07h ||07h ||07h ||07h ||07h ||07h) + # where + # MK is Message key; + # DC is Derivation constant; + # C is Message counter; + # ID is Meter ID. + defp kdf_a(master_keys, mode, ctx) when mode in [:mac, :enc] do + # depending on direction and mode, we pick DC + {:ok, direction} = Wmbus.direction(ctx.dll) + + # > The KDF requires a Message counter. The KDF shall use the Message counter provided by the TPL. + counter = find_message_counter(ctx) + + # For messages from the meter to the communication partner which use a short TPL-header, (like CI = 7Ah; see 7.3) + # the ID corresponds to the Identification Number in the Link Layer Address (see 8.3) of the meter. + # For messages with long header (like CI = 72h; see 7.4) the ID corresponds to the + # Application Layer Identification Number (see 7.5.1) of the meter. + # For messages from the communication partner to the meter the Long Header is always used. + # The ID corresponds to the Identification Number in the + # Application Layer Address (see 7.5.1) of the meter (not the communication partner). + {:ok, meter_id} = IdentificationNo.encode(find_meter_id(ctx).identification_no) + # derrive the keys + keys = Enum.map(master_keys, &Crypto.kdf_a!(direction, mode, counter, meter_id, &1)) + + {:ok, keys} + end + + defp find_message_counter(ctx) do + # > If no TPL counter is present (bit Z = 0 in configuration field, see Table 33) + # > then the counter of the AFL (AFL.MCR) shall be used instead. + cond do + not is_nil(ctx.tpl.header.configuration_field.counter) -> + ctx.tpl.header.configuration_field.counter + + is_struct(ctx.afl, Afl) and not is_nil(ctx.afl.mcr) -> + ctx.afl.mcr + end end end diff --git a/test/crypto_test.exs b/test/crypto_test.exs new file mode 100644 index 0000000..a9234db --- /dev/null +++ b/test/crypto_test.exs @@ -0,0 +1,31 @@ +defmodule CryptoTest do + @moduledoc """ + Test the wrapper module that Exmbus uses to wrap :crypto functions. + """ + + use ExUnit.Case, async: true + + alias Exmbus.Crypto + + describe "kdf_a" do + @master_key Base.decode16!("000102030405060708090A0B0C0D0E0F") + @encrypted_session_key Base.decode16!("ECCF39D475D730B8284FDFDC1995D52F") + @mac_session_key Base.decode16!("C9CD19FF5A9AAD5A6BBDA13BD2C4C7AD") + @message_counter :binary.decode_unsigned(Base.decode16!("B30A0000"), :little) + @meter_id Base.decode16!("78563412") + + test "Kenc - CEN/TR 17167:2018 - F.3 Security mode 7 example" do + {:ok, ephemeral_key} = + Crypto.kdf_a(:from_meter, :enc, @message_counter, @meter_id, @master_key) + + assert ephemeral_key == @encrypted_session_key + end + + test "Kmac - CEN/TR 17167:2018 - F.3 Security mode 7 example" do + {:ok, ephemeral_key} = + Crypto.kdf_a(:from_meter, :mac, @message_counter, @meter_id, @master_key) + + assert ephemeral_key == @mac_session_key + end + end +end diff --git a/test/parser/afl/afl_example_test.exs b/test/parser/afl/afl_example_test.exs new file mode 100644 index 0000000..703cbfb --- /dev/null +++ b/test/parser/afl/afl_example_test.exs @@ -0,0 +1,69 @@ +defmodule Parser.Afl.AflExampleTest do + @moduledoc """ + Example of frame using an AFL from CEN/TR 17167:2018 (page) + + It also uses encryption mode 7. + + The example is "F.3 Security mode 7 example" starting at page 34. + """ + alias Exmbus.Parser.Ell.UnencryptedWithReceiver + alias Exmbus.Parser.Apl.DataRecord + + use ExUnit.Case, async: true + + @absolute_meter_volume 28504.27 + @absolute_meter_volume_unit "m^3" + + @date_and_time ~N[2008-05-31 23:50:00] + + @master_key "000102030405060708090A0B0C0D0E0F" + # @encrypted_session_key "ECCF39D475D730B8284FDFDC1995D52F" + # @mac_session_key "C9CD19FF5A9AAD5A6BBDA13BD2C4C7AD" + + # from the example (CRC stripped) + @message [ + # DLL + "53082448443322110337", + # ELL + "8E80753A63665544330A31", + # AFL + "900F002C25B30A0000AF5D74DF73A600D9", + # TPL + "7278563412931533037500200710", + # APL + "9058475F4BC91DF878B80A1B0F98B629", + # APL + "024AAC727942BFC549233C0140829B93" + ] + + test "parse F.3 Security mode 7 example" do + frame = Base.decode16!(Enum.join(@message)) + key = Base.decode16!(@master_key) + + {:ok, ctx} = Exmbus.parse(frame, key: key) + + # we expect to be able to find the values in the description of the example + assert is_list(ctx.apl.records) + + # meter: + assert ctx.tpl.header.manufacturer == "ELS" + assert ctx.tpl.header.identification_no == "12345678" + assert ctx.tpl.header.version == 51 + # radio module: + assert ctx.dll.manufacturer == "RAD" + assert ctx.dll.identification_no == "11223344" + # receiver (from the ELL): + assert is_struct(ctx.ell, UnencryptedWithReceiver) + assert ctx.ell.receiver.manufacturer == "XYZ" + assert ctx.ell.receiver.identification_no == "33445566" + + assert [_ | _] = records = ctx.apl.records + + values = Enum.map(records, &%{value: DataRecord.value!(&1), unit: DataRecord.unit!(&1)}) + # check expected values present in records: + assert %{unit: @absolute_meter_volume_unit, value: @absolute_meter_volume} in values + assert %{unit: nil, value: @date_and_time} in values + # errors flags, all 0: + assert %{unit: nil, value: for(_ <- 1..16, do: false)} in values + end +end diff --git a/test/parser/afl_test.exs b/test/parser/afl_test.exs new file mode 100644 index 0000000..4cb282d --- /dev/null +++ b/test/parser/afl_test.exs @@ -0,0 +1,7 @@ +defmodule Parser.AflTest do + use ExUnit.Case, async: true + + doctest Exmbus.Parser.Afl.FragmentationControlField, import: true + doctest Exmbus.Parser.Afl.MessageControlField, import: true + doctest Exmbus.Parser.Afl.KeyInformationField, import: true +end