Packages
grizzly
8.14.0
9.1.4
9.1.2
9.1.1
9.1.0
9.0.0
8.15.3
8.15.2
8.15.1
8.15.0
8.14.0
8.13.0
8.12.0
8.11.3
8.11.2
8.11.1
8.11.0
8.10.0
8.9.0
8.8.1
8.8.0
8.7.1
8.7.0
8.6.12
8.6.11
8.6.10
8.6.9
8.6.8
8.6.7
retired
8.6.6
8.6.5
8.6.4
8.6.3
8.6.2
8.6.1
8.6.0
8.5.3
8.5.2
8.5.1
8.5.0
8.4.0
8.3.0
8.2.3
8.2.2
8.2.1
8.2.0
8.1.0
8.0.1
8.0.0
7.4.3
7.4.2
7.4.1
7.4.0
7.3.0
7.2.0
7.1.4
7.1.3
7.1.2
7.1.1
7.1.0
7.0.4
7.0.3
7.0.2
7.0.1
7.0.0
6.8.8
6.8.7
6.8.6
6.8.5
6.8.4
6.8.3
6.8.2
6.8.1
6.8.0
6.7.1
6.7.0
6.6.1
6.6.0
6.5.1
6.5.0
6.4.0
6.3.0
6.2.0
6.1.1
6.1.0
6.0.1
6.0.0
5.4.1
5.4.0
5.3.0
5.2.8
5.2.7
5.2.6
5.2.5
5.2.4
5.2.3
5.2.2
5.2.1
5.2.0
5.1.2
5.1.1
5.1.0
5.0.2
5.0.1
5.0.0
4.0.1
4.0.0
3.0.0
2.1.0
2.0.0
1.0.1
1.0.0
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.1
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.2
0.16.1
0.16.0
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.3
0.12.2
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.9.0-rc.4
0.9.0-rc.3
0.9.0-rc.2
0.9.0-rc.1
0.9.0-rc.0
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.3
0.4.2
Elixir Z-Wave library
Current section
Files
Jump to
Current section
Files
lib/grizzly/zwave/command_classes/security_2.ex
defmodule Grizzly.ZWave.CommandClasses.Security2 do
@moduledoc """
Security 2 (S2) Command Class
### Definitions
- **CKDF** - CMAC-based Key Derivation Function
- **MEI** - Mutual Entropy Input
- **SPAN** - Singlecast Pre-Agreed Nonce
- **MPAN** - Multicast Pre-Agreed Nonce
- **MGRP** - Multicast Group
- **SOS** - Singlecast Out-of-Sync
- **MOS** - Multicast Out-of-Sync
"""
defmodule AAD do
@moduledoc """
S2 AAD (Additional Authenticated Data) structure
"""
import Grizzly.ZWave.Encoding, only: [bool_to_bit: 1]
@type t :: %__MODULE__{
sender_node_id: non_neg_integer(),
destination_tag: non_neg_integer(),
home_id: non_neg_integer(),
message_length: non_neg_integer(),
sequence_number: non_neg_integer(),
encrypted_extensions?: boolean(),
extensions: binary()
}
defstruct sender_node_id: nil,
destination_tag: nil,
home_id: nil,
message_length: nil,
sequence_number: nil,
encrypted_extensions?: false,
extensions: <<>>
@doc """
Create a new S2 AAD struct.
"""
def new(opts) do
struct(__MODULE__, opts)
end
@doc """
Encodes the AAD into a binary.
"""
def encode(
%__MODULE__{sender_node_id: sender_node_id, destination_tag: destination_tag} = aad
)
when sender_node_id > 255 or destination_tag > 255 do
extensions? = if(aad.extensions != <<>>, do: true, else: false)
<<sender_node_id::16, destination_tag::16, aad.home_id::32, aad.message_length::16,
aad.sequence_number::8, 0::6, bool_to_bit(aad.encrypted_extensions?)::1,
bool_to_bit(extensions?)::1, aad.extensions::binary>>
end
def encode(
%__MODULE__{sender_node_id: sender_node_id, destination_tag: destination_tag} = aad
) do
extensions? = if(aad.extensions != <<>>, do: true, else: false)
<<sender_node_id::8, destination_tag::8, aad.home_id::32, aad.message_length::16,
aad.sequence_number::8, 0::6, bool_to_bit(aad.encrypted_extensions?)::1,
bool_to_bit(extensions?)::1, aad.extensions::binary>>
end
end
@type kex_scheme :: :kex_scheme_1
@type ecdh_profile :: :curve_25519
@behaviour Grizzly.ZWave.CommandClass
@impl Grizzly.ZWave.CommandClass
def byte(), do: 0x9F
@impl Grizzly.ZWave.CommandClass
def name(), do: :security_2
# Key derivation functions
@doc """
Expands a network key into a CCM key for encryption and authorization, a
personalization string, and an MPAN key using the CKDF-Expand algorithm as
described in https://datatracker.ietf.org/doc/html/draft-moskowitz-hip-dex-02#section-6.3.
"""
def generic_expand(network_key, constant_nk) do
# ccm_key
t0 = <<constant_nk::binary-size(15), 0x01>>
ccm_key = aes_cmac_calculate(network_key, t0)
# pstring first half
t1 = <<ccm_key::binary-size(16), constant_nk::binary-size(15), 2::8>>
pstring1 = aes_cmac_calculate(network_key, t1)
# pstring second half
t2 = <<pstring1::binary-size(16), constant_nk::binary-size(15), 3::8>>
pstring2 = aes_cmac_calculate(network_key, t2)
# MPAN key
t3 = <<pstring2::binary-size(16), constant_nk::binary-size(15), 4::8>>
mpan_key = aes_cmac_calculate(network_key, t3)
{ccm_key, pstring1 <> pstring2, mpan_key}
end
@doc """
Expands a network key into a CCM key for encryption and authorization, a
personalization string, and an MPAN key using the CKDF-Expand algorithm as
described in https://datatracker.ietf.org/doc/html/draft-moskowitz-hip-dex-02#section-6.3.
"""
def network_key_expand(network_key) do
generic_expand(network_key, :binary.copy(<<0x55>>, 16))
end
@doc """
Expands a temporary network key into a CCM key for encryption and authorization, a
personalization string, and an MPAN key using the CKDF-Expand algorithm as
described in https://datatracker.ietf.org/doc/html/draft-moskowitz-hip-dex-02#section-6.3.
"""
def temp_key_expand(prk) do
generic_expand(prk, :binary.copy(<<0x88>>, 16))
end
@spec temp_key_extract(<<_::256>>, <<_::256>>, <<_::256>>) :: <<_::128>>
def temp_key_extract(ecdh_shared_secret, sender_pubkey, receiver_pubkey) do
constant_prk = :binary.copy(<<0x33>>, 16)
aes_cmac_calculate(constant_prk, ecdh_shared_secret <> sender_pubkey <> receiver_pubkey)
|> binary_slice(0..15)
end
@doc """
Mix and expand the sender and receiver entropy inputs into a nonce using CKDF-MEI.
"""
@spec ckdf_mei_expand(<<_::128>>, <<_::128>>) :: <<_::256>>
def ckdf_mei_expand(sender_entropy_input, receiver_entropy_input) do
# Extract nonce PRK
constant_nonce = :binary.copy(<<0x26>>, 16)
nonce_prk = aes_cmac_calculate(constant_nonce, sender_entropy_input <> receiver_entropy_input)
# Expand nonce PRK
const_entropy_input = :binary.copy(<<0x88>>, 15)
t0 = const_entropy_input <> <<0x00>>
t1 = aes_cmac_calculate(nonce_prk, t0 <> const_entropy_input <> <<0x01>>)
t2 = aes_cmac_calculate(nonce_prk, t1 <> const_entropy_input <> <<0x02>>)
t1 <> t2
end
@doc """
Encode the ECDH public key into a DSK string.
"""
def ecdh_public_key_to_dsk_string(public_key) do
for <<int::16 <- public_key>>, into: [] do
Integer.to_string(int) |> String.pad_leading(5, "0")
end
|> Enum.join("-")
end
@doc """
Computes an ECDH public key for the given private key.
"""
def ecdh_public_key(private_key) do
{pub_key, _} = :crypto.generate_key(:ecdh, :x25519, private_key)
pub_key
end
@doc """
Computes the shared secret using the ECDH algorithm with the local node's
private key and the remote node's public key (as reported by S2 Public Key Report).
"""
def ecdh_shared_secret(private_key, remote_public_key) do
:crypto.compute_key(:ecdh, remote_public_key, private_key, :x25519)
end
defp aes_cmac_calculate(key, message) do
:crypto.mac(:cmac, :aes_128_cbc, key, message)
end
end