Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 5 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 41 additions & 0 deletions lib/exmbus/crypto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
<<dc, counter::little-size(32), meter_id::binary, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07>>

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
168 changes: 168 additions & 0 deletions lib/exmbus/parser/afl.ex
Original file line number Diff line number Diff line change
@@ -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
<<fcl_bytes::binary-size(2), bytes::binary>> = 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: <<ci, _rest::binary>>} = ctx) do
{:halt, Context.add_error(ctx, {:ci_not_afl, ci})}
end

defp consume_mcl(
<<bytes::binary-size(1), rest::binary>>,
%{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(
<<bytes::binary-size(2), rest::binary>>,
%{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(
<<bytes::binary-size(4), rest::binary>>,
%{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)
<<mac::binary-size(mac_size), rest::binary>> = bytes
{:ok, %{afl | mac: mac}, rest}
end

defp consume_mac(rest, %{fcl: %{mac_present?: false}} = afl) do
{:ok, afl, rest}
end

defp consume_ml(
<<bytes::binary-size(2), rest::binary>>,
%{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
128 changes: 128 additions & 0 deletions lib/exmbus/parser/afl/fragmentation_control_field.ex
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading