379 lines
11 KiB
Python
Executable File
379 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (c) 2016, The OpenThread Authors.
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
# 1. Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# 2. Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
# 3. Neither the name of the copyright holder nor the
|
|
# names of its contributors may be used to endorse or promote products
|
|
# derived from this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
#
|
|
|
|
import hmac
|
|
import hashlib
|
|
import struct
|
|
|
|
from binascii import hexlify
|
|
|
|
from Crypto.Cipher import AES
|
|
|
|
|
|
class CryptoEngine:
|
|
""" Class responsible for encryption and decryption of data. """
|
|
|
|
def __init__(self, crypto_material_creator):
|
|
"""
|
|
Args:
|
|
network_key (bytearray)
|
|
|
|
"""
|
|
self._crypto_material_creator = crypto_material_creator
|
|
|
|
@property
|
|
def mic_length(self):
|
|
return self._crypto_material_creator.mic_length
|
|
|
|
def encrypt(self, data, message_info):
|
|
""" Encrypt message.
|
|
|
|
Args:
|
|
data (bytearray)
|
|
message_info (MessageInfo)
|
|
|
|
Returns:
|
|
tuple: Encrypted message (bytearray), MIC (bytearray)
|
|
|
|
"""
|
|
key, nonce, auth_data = self._crypto_material_creator.create_key_and_nonce_and_authenticated_data(message_info)
|
|
|
|
cipher = AES.new(key, AES.MODE_CCM, nonce, mac_len=self.mic_length)
|
|
cipher.update(auth_data)
|
|
|
|
return cipher.encrypt_and_digest(bytes(data))
|
|
|
|
def decrypt(self, enc_data, mic, message_info):
|
|
""" Decrypt MLE message.
|
|
|
|
Args:
|
|
enc_data (bytearray)
|
|
mic (bytearray)
|
|
message_info (MessageInfo)
|
|
|
|
Returns:
|
|
bytearray: Decrypted message.
|
|
|
|
"""
|
|
key, nonce, auth_data = self._crypto_material_creator.create_key_and_nonce_and_authenticated_data(message_info)
|
|
|
|
cipher = AES.new(key, AES.MODE_CCM, nonce, mac_len=self.mic_length)
|
|
cipher.update(auth_data)
|
|
|
|
dec_data = cipher.decrypt_and_verify(bytes(enc_data), bytes(mic))
|
|
return bytearray(dec_data)
|
|
|
|
|
|
class CryptoMaterialCreator(object):
|
|
|
|
_salt = b'Thread'
|
|
|
|
def __init__(self, network_key):
|
|
"""
|
|
Args:
|
|
network_key (bytearray)
|
|
|
|
"""
|
|
self.network_key = network_key
|
|
|
|
def _generate_keys(self, sequence_counter):
|
|
""" Generate MLE and MAC keys.
|
|
|
|
Read more: 7.1.4. Key Generation - Thread v1.1 Specification Final
|
|
|
|
Args:
|
|
sequence_counter (int)
|
|
|
|
Returns:
|
|
tuple: MLE and MAC as bytes
|
|
|
|
"""
|
|
k = self.network_key
|
|
s = struct.pack(">L", sequence_counter) + self._salt
|
|
d = hmac.new(k, s, digestmod=hashlib.sha256).digest()
|
|
|
|
mle = d[:16]
|
|
mac = d[16:]
|
|
return mle, mac
|
|
|
|
def create_key_and_nonce_and_authenticated_data(self, message_info):
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def mic_length(self):
|
|
raise NotImplementedError
|
|
|
|
|
|
class MacCryptoMaterialCreator(CryptoMaterialCreator):
|
|
|
|
def __init__(self, network_key):
|
|
"""
|
|
Args:
|
|
network_key (bytearray)
|
|
|
|
"""
|
|
super(MacCryptoMaterialCreator, self).__init__(network_key)
|
|
|
|
def _create_nonce(self, eui64, frame_counter, security_level):
|
|
""" Create CCM Nonce required by AES-128 CCM for encryption and decryption.
|
|
|
|
Read more: 7.6.3.2 CCM Nonce - Std 802.15.4-2006
|
|
|
|
Args:
|
|
eui64 (bytes)
|
|
frame_counter (int)
|
|
security_level (int)
|
|
|
|
Returns:
|
|
bytes: created Nonce
|
|
|
|
"""
|
|
return bytes(eui64 + struct.pack(">LB", frame_counter, security_level))
|
|
|
|
def _create_authenticated_data(self, mhr, auxiliary_security_header, extra_open_fields):
|
|
""" Create Authenticated Data
|
|
|
|
Read more: 7.6.3.3 CCM prerequisites - Std 802.15.4-2006
|
|
|
|
Args:
|
|
mhr (bytes)
|
|
auxiliary_security_header (bytes)
|
|
extra_open_fields (bytes)
|
|
|
|
Returns:
|
|
bytes: Authenticated Data
|
|
|
|
"""
|
|
return bytes(mhr + auxiliary_security_header + extra_open_fields)
|
|
|
|
def create_key_and_nonce_and_authenticated_data(self, message_info):
|
|
_, mac_key = self._generate_keys(message_info.aux_sec_hdr.sequence_counter)
|
|
|
|
nonce = self._create_nonce(
|
|
message_info.source_mac_address,
|
|
message_info.aux_sec_hdr.frame_counter,
|
|
message_info.aux_sec_hdr.security_level,
|
|
)
|
|
|
|
auth_data = self._create_authenticated_data(
|
|
message_info.mhr_bytes,
|
|
message_info.aux_sec_hdr_bytes,
|
|
message_info.extra_open_fields,
|
|
)
|
|
|
|
return mac_key, nonce, auth_data
|
|
|
|
@property
|
|
def mic_length(self):
|
|
return 4
|
|
|
|
|
|
class MleCryptoMaterialCreator(CryptoMaterialCreator):
|
|
|
|
def __init__(self, network_key):
|
|
"""
|
|
Args:
|
|
network_key (bytearray)
|
|
|
|
"""
|
|
super(MleCryptoMaterialCreator, self).__init__(network_key)
|
|
|
|
def _create_nonce(self, source_eui64, frame_counter, security_level):
|
|
""" Create CCM Nonce required by AES-128 CCM for encryption and decryption.
|
|
|
|
Read more: 7.6.3.2 CCM Nonce - Std 802.15.4-2006
|
|
|
|
Args:
|
|
eui64 (bytearray)
|
|
frame_counter (int)
|
|
security_level (int)
|
|
|
|
Returns:
|
|
bytes: created Nonce
|
|
|
|
"""
|
|
return bytes(source_eui64[:8] + struct.pack(">LB", frame_counter, security_level))
|
|
|
|
def _create_authenticated_data(self, source_address, destination_address, auxiliary_security_header):
|
|
""" Create Authenticated Data
|
|
|
|
Read more: 4.8 - Thread v1.0 Specification
|
|
|
|
Args:
|
|
source_address (ip_address)
|
|
destination_address (ip_address)
|
|
auxiliary_security_header (bytearray)
|
|
|
|
Returns:
|
|
bytes: Authenticated Data
|
|
|
|
"""
|
|
return bytes(source_address.packed + destination_address.packed + auxiliary_security_header)
|
|
|
|
def create_key_and_nonce_and_authenticated_data(self, message_info):
|
|
mle_key, _ = self._generate_keys(message_info.aux_sec_hdr.sequence_counter)
|
|
|
|
nonce = self._create_nonce(
|
|
message_info.source_mac_address.mac_address,
|
|
message_info.aux_sec_hdr.frame_counter,
|
|
message_info.aux_sec_hdr.security_level,
|
|
)
|
|
|
|
auth_data = self._create_authenticated_data(
|
|
message_info.source_ipv6,
|
|
message_info.destination_ipv6,
|
|
message_info.aux_sec_hdr_bytes,
|
|
)
|
|
|
|
return mle_key, nonce, auth_data
|
|
|
|
@property
|
|
def mic_length(self):
|
|
return 4
|
|
|
|
|
|
class AuxiliarySecurityHeader:
|
|
|
|
def __init__(
|
|
self,
|
|
key_id_mode,
|
|
security_level,
|
|
frame_counter,
|
|
key_id,
|
|
big_endian=True,
|
|
):
|
|
"""
|
|
Args:
|
|
key_id_mode (int)
|
|
security_level (int)
|
|
frame_counter (int)
|
|
key_id (bytearray)
|
|
"""
|
|
self._key_id_mode = key_id_mode
|
|
self._security_level = security_level
|
|
self._frame_counter = frame_counter
|
|
self._key_id = key_id
|
|
self._big_endian = big_endian
|
|
|
|
@property
|
|
def sequence_counter(self):
|
|
""" Compute or extract sequence counter based on currently set Key Index Mode. """
|
|
|
|
if self.key_id_mode == 0:
|
|
key_source = self.key_id[:8]
|
|
format = ">Q" if self._big_endian else "<Q"
|
|
elif self.key_id_mode == 1:
|
|
# Try to guess valid Key Sequence Counter based on Key Index. This
|
|
# one should work for now.
|
|
return self.key_index - 1
|
|
elif self.key_id_mode == 2:
|
|
# In this mode sequence counter is stored on the first four bytes
|
|
# of Key ID.
|
|
key_source = self.key_id[:4]
|
|
format = ">I" if self._big_endian else "<I"
|
|
else:
|
|
raise ValueError("Unsupported Key Index Mode: {}".format(self.key_id_mode))
|
|
|
|
return struct.unpack(format, key_source)[0]
|
|
|
|
@property
|
|
def key_index(self):
|
|
return struct.unpack(">B", self.key_id[-1:])[0]
|
|
|
|
@property
|
|
def key_id_mode(self):
|
|
return self._key_id_mode
|
|
|
|
@property
|
|
def security_level(self):
|
|
return self._security_level
|
|
|
|
@property
|
|
def frame_counter(self):
|
|
return self._frame_counter
|
|
|
|
@property
|
|
def key_id(self):
|
|
return self._key_id
|
|
|
|
def __repr__(self):
|
|
return "AuxiliarySecurityHeader(key_id_mode={}, security_level={}, frame_counter={}, key_id={})".format(
|
|
self.key_id_mode,
|
|
self.security_level,
|
|
self.frame_counter,
|
|
hexlify(self.key_id),
|
|
)
|
|
|
|
|
|
class AuxiliarySecurityHeaderFactory:
|
|
|
|
_SECURITY_CONTROL_LENGTH = 1
|
|
_FRAME_COUNTER_LENGTH = 4
|
|
|
|
_KEY_ID_LENGTH_KEY_ID_0 = 0
|
|
_KEY_ID_LENGTH_KEY_ID_1 = 1
|
|
_KEY_ID_LENGTH_KEY_ID_2 = 5
|
|
_KEY_ID_LENGTH_KEY_ID_3 = 9
|
|
|
|
_key_id_lengths = {
|
|
0: _KEY_ID_LENGTH_KEY_ID_0,
|
|
1: _KEY_ID_LENGTH_KEY_ID_1,
|
|
2: _KEY_ID_LENGTH_KEY_ID_2,
|
|
3: _KEY_ID_LENGTH_KEY_ID_3,
|
|
}
|
|
|
|
def _parse_security_control(self, security_control_byte):
|
|
security_level = security_control_byte & 0x07
|
|
key_id_mode = (security_control_byte >> 3) & 0x03
|
|
|
|
return security_level, key_id_mode
|
|
|
|
def _parse_frame_counter(self, frame_counter_bytes):
|
|
return struct.unpack("<I", frame_counter_bytes)[0]
|
|
|
|
def _key_id_length(self, key_id_mode):
|
|
return self._key_id_lengths[key_id_mode]
|
|
|
|
def parse(self, data, message_info):
|
|
security_control_bytes = bytearray(data.read(self._SECURITY_CONTROL_LENGTH))
|
|
frame_counter_bytes = bytearray(data.read(self._FRAME_COUNTER_LENGTH))
|
|
|
|
security_level, key_id_mode = self._parse_security_control(security_control_bytes[0])
|
|
frame_counter = self._parse_frame_counter(frame_counter_bytes)
|
|
|
|
key_id_length = self._key_id_length(key_id_mode)
|
|
key_id_bytes = bytearray(data.read(key_id_length))
|
|
|
|
aux_sec_hdr = AuxiliarySecurityHeader(key_id_mode, security_level, frame_counter, key_id_bytes)
|
|
|
|
message_info.aux_sec_hdr_bytes = (security_control_bytes + frame_counter_bytes + key_id_bytes)
|
|
message_info.aux_sec_hdr = aux_sec_hdr
|
|
|
|
return aux_sec_hdr
|