2021-04-03 17:07:06 +00:00
|
|
|
#!/usr/bin/env perl
|
|
|
|
|
|
|
|
##
|
|
|
|
## Author......: Robert Guetzkow
|
|
|
|
## License.....: MIT
|
|
|
|
##
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
|
|
|
|
use Crypt::PBKDF2;
|
|
|
|
use Crypt::Mode::CBC;
|
|
|
|
use Crypt::Mode::ECB;
|
|
|
|
|
|
|
|
# Details of the protocol design can be found in ISO 22510:2019 and
|
|
|
|
# application notes published by the KNX Association.
|
|
|
|
|
|
|
|
# ETS 5 allows a maximum of 20 characters in a password.
|
|
|
|
# The salt is used as Secure Session Identifier, which is 2 Bytes long.
|
|
|
|
sub module_constraints { [[0, 20], [2, 2], [-1, -1], [-1, -1], [-1, -1]] }
|
|
|
|
|
|
|
|
sub device_authentication_code
|
|
|
|
{
|
|
|
|
my $password = shift;
|
|
|
|
|
|
|
|
my $pbkdf2 = Crypt::PBKDF2->new
|
|
|
|
(
|
|
|
|
hasher => Crypt::PBKDF2->hasher_from_algorithm ("HMACSHA2", 256),
|
|
|
|
iterations => 65536,
|
|
|
|
output_len => 16
|
|
|
|
);
|
|
|
|
|
|
|
|
my $device_authentication_code = $pbkdf2->PBKDF2 ("device-authentication-code.1.secure.ip.knx.org",
|
|
|
|
$password);
|
|
|
|
|
|
|
|
return $device_authentication_code;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub block_formatting
|
|
|
|
{
|
|
|
|
# Simplified block formatting function, where payload is always empty
|
|
|
|
my $b0 = shift;
|
|
|
|
my $associated_data = shift;
|
|
|
|
my $associated_data_length = pack ("s>", length ($associated_data));
|
|
|
|
my $blocks_unpadded = $associated_data_length . $associated_data;
|
|
|
|
my $pad_len = int ((length ($blocks_unpadded) + 16 - 1) / 16) * 16;
|
|
|
|
my $blocks_padded = $blocks_unpadded . "\0" x ($pad_len - length ($blocks_unpadded));
|
|
|
|
|
|
|
|
return $b0 . $blocks_padded;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub encrypt
|
|
|
|
{
|
|
|
|
# Simplified encryption that only performs steps required for the MAC, not full CCM
|
|
|
|
my $blocks = shift;
|
|
|
|
my $nonce = shift;
|
|
|
|
my $key = shift;
|
|
|
|
my $iv = "\0" x 16;
|
|
|
|
|
|
|
|
my $aes_cbc = Crypt::Mode::CBC->new ("AES", 0);
|
|
|
|
my $ciphertext = $aes_cbc->encrypt ($blocks, $key, $iv);
|
|
|
|
my $y_n = substr ($ciphertext, length ($ciphertext) - 16, 16);
|
|
|
|
|
|
|
|
my $aes_ecb = Crypt::Mode::ECB->new ("AES", 0);
|
|
|
|
my $s_0 = $aes_ecb->encrypt ($nonce, $key);
|
|
|
|
|
|
|
|
return $y_n ^ $s_0;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub generate_session_response_mac
|
|
|
|
{
|
|
|
|
my $secure_session_identifier = shift;
|
|
|
|
my $public_value_xor = shift;
|
|
|
|
my $key = shift;
|
|
|
|
|
|
|
|
# Constants used for the cryptography in Session_Response frames
|
|
|
|
my $knx_ip_header = pack ("H*", "061009520038");
|
|
|
|
my $b0 = pack ("H*", "00000000000000000000000000000000");
|
|
|
|
my $nonce = pack ("H*", "0000000000000000000000000000ff00");
|
|
|
|
|
|
|
|
my $associated_data = $knx_ip_header . $secure_session_identifier . $public_value_xor;
|
|
|
|
|
|
|
|
my $blocks = block_formatting ($b0, $associated_data);
|
|
|
|
|
|
|
|
return encrypt ($blocks, $nonce, $key);
|
|
|
|
}
|
|
|
|
|
|
|
|
sub module_generate_hash
|
|
|
|
{
|
|
|
|
my $word = shift;
|
|
|
|
|
|
|
|
# Parameters that would be found in the Session_Request and Session_Response frames
|
|
|
|
my $secure_session_identifier = shift;
|
|
|
|
my $public_value_xor = shift // random_bytes (32);
|
|
|
|
|
|
|
|
my $device_authentication_code = device_authentication_code ($word);
|
|
|
|
|
|
|
|
my $mac = generate_session_response_mac ($secure_session_identifier,
|
|
|
|
$public_value_xor,
|
|
|
|
$device_authentication_code);
|
|
|
|
|
|
|
|
my $hash = sprintf ("\$knx-ip-secure-device-authentication-code\$*%s*%s*%s",
|
|
|
|
unpack ("H*", $secure_session_identifier),
|
|
|
|
unpack ("H*", $public_value_xor),
|
|
|
|
unpack ("H*", $mac));
|
|
|
|
|
|
|
|
return $hash;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub module_verify_hash
|
|
|
|
{
|
|
|
|
my $line = shift;
|
|
|
|
|
|
|
|
my ($hash, $word) = split (':', $line);
|
|
|
|
|
|
|
|
return unless defined $hash;
|
|
|
|
return unless defined $word;
|
|
|
|
|
|
|
|
my @data = split ('\*', $hash);
|
|
|
|
|
|
|
|
return unless scalar (@data) == 4;
|
|
|
|
|
|
|
|
my $signature = shift @data;
|
|
|
|
|
|
|
|
return unless ($signature eq "\$knx-ip-secure-device-authentication-code\$");
|
|
|
|
|
|
|
|
my $secure_session_identifier = pack ("H*", shift @data); # 2 Bytes expected (using the "salt" for this purpose)
|
|
|
|
my $public_value_xor = pack ("H*", shift @data); # 32 Bytes expected (xor of client's and server's public value)
|
|
|
|
my $mac = pack ("H*", shift @data); # 16 Bytes expected
|
|
|
|
|
|
|
|
return unless (length ($secure_session_identifier) == 2);
|
|
|
|
return unless (length ($public_value_xor) == 32);
|
|
|
|
return unless (length ($mac) == 16);
|
|
|
|
|
|
|
|
my $word_packed = pack_if_HEX_notation ($word);
|
|
|
|
|
|
|
|
my $new_hash = module_generate_hash ($word_packed,
|
|
|
|
$secure_session_identifier,
|
|
|
|
$public_value_xor);
|
|
|
|
|
|
|
|
return ($new_hash, $word);
|
|
|
|
}
|
|
|
|
|
2021-04-11 12:41:16 +00:00
|
|
|
1;
|