From 043f8832f50cedbda126697e497bb3063d7574f4 Mon Sep 17 00:00:00 2001 From: Matthias Ringwald Date: Sun, 12 Jan 2020 21:14:32 +0100 Subject: [PATCH] tool/compile_gatt:calculated GATT Database Hash --- tool/compile_gatt.py | 123 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 8 deletions(-) diff --git a/tool/compile_gatt.py b/tool/compile_gatt.py index 06b7c3340..8a40618a1 100755 --- a/tool/compile_gatt.py +++ b/tool/compile_gatt.py @@ -1,12 +1,15 @@ #!/usr/bin/env python # # BLE GATT configuration generator for use with BTstack -# Copyright 2018 BlueKitchen GmbH +# Copyright 2019 BlueKitchen GmbH # # Format of input file: # PRIMARY_SERVICE, SERVICE_UUID # CHARACTERISTIC, ATTRIBUTE_TYPE_UUID, [READ | WRITE | DYNAMIC], VALUE +# dependencies: +# - pip3 install pycryptodomex + import codecs import csv import io @@ -17,6 +20,16 @@ import sys import argparse import tempfile +# try to import Cryptodome +try: + from Cryptodome2.Cipher import AES + from Cryptodome.Hash import CMAC + have_crypto = True +except ImportError: + have_crypto = False + print("\n[!] PyCryptodome required to calculate GATT Database Hash but not installed (using dummy hash value 00..00)") + print("[!] Please install PyCryptodome, e.g. 'pip install pycryptodomex'\n") + header = ''' // {0} generated from {1} for BTstack // it needs to be regenerated when the .gatt file is updated. @@ -48,6 +61,7 @@ assigned_uuids = { 'GAP_RECONNECTION_ADDRESS' : 0x2A03, 'GAP_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS' : 0x2A04, 'GATT_SERVICE_CHANGED' : 0x2a05, + 'GATT_DATABASE_HASH' : 0x2b2a } security_permsission = ['ANYBODY','ENCRYPTED', 'AUTHENTICATED', 'AUTHORIZED', 'AUTHENTICATED_SC'] @@ -117,10 +131,20 @@ current_characteristic_uuid_string = "" defines_for_characteristics = [] defines_for_services = [] include_paths = [] +database_hash_message = bytearray() handle = 1 total_size = 0 +def aes_cmac(key, n): + if have_crypto: + cobj = CMAC.new(key, ciphermod=AES) + cobj.update(n) + return cobj.digest() + else: + # return dummy value + return b'\x00' * 16 + def read_defines(infile): defines = dict() with open (infile, 'rt') as fin: @@ -276,6 +300,9 @@ def write_sequence(fout, text): for part in parts: fout.write("0x%s, " % (part.strip())) +def write_database_hash(fout): + fout.write("THE-DATABASE-HASH") + def write_indent(fout): fout.write(" ") @@ -346,6 +373,20 @@ def dump_flags(fout, flags): fout.write('ENCRYPTION_KEY_SIZE=%u' % encryption_key_size) fout.write('\n') +def database_hash_append_uint8(value): + global database_hash_message + database_hash_message.append(value) + +def database_hash_append_uint16(value): + global database_hash_message + database_hash_append_uint8(value & 0xff) + database_hash_append_uint8((value >> 8) & 0xff) + +def database_hash_append_value(value): + global database_hash_message + for byte in value: + database_hash_append_uint8(byte) + def parseService(fout, parts, service_type): global handle global total_size @@ -375,6 +416,10 @@ def parseService(fout, parts, service_type): write_uuid(fout, uuid) fout.write("\n") + database_hash_append_uint16(handle) + database_hash_append_uint16(service_type) + database_hash_append_value(uuid) + current_service_uuid_string = c_string_for_uuid(parts[1]) current_service_start_handle = handle handle = handle + 1 @@ -416,6 +461,13 @@ def parseIncludeService(fout, parts): write_uuid(fout, uuid) fout.write("\n") + database_hash_append_uint16(handle) + database_hash_append_uint16(0x2802) + database_hash_append_uint16(services[keyUUID][0]) + database_hash_append_uint16(services[keyUUID][1]) + if uuid_size > 0: + database_hash_append_value(uuid) + handle = handle + 1 total_size = total_size + size @@ -466,11 +518,22 @@ def parseCharacteristic(fout, parts): handle = handle + 1 total_size = total_size + size + database_hash_append_uint16(handle) + database_hash_append_uint16(0x2803) + database_hash_append_uint8(characteristic_properties) + database_hash_append_uint16(handle+1) + database_hash_append_value(uuid) + + uuid_is_database_hash = len(uuid) == 2 and uuid[0] == 0x2a and uuid[1] == 0x2b + size = 2 + 2 + 2 + uuid_size - if is_string(value): - size = size + len(value) + if uuid_is_database_hash: + size += 16 else: - size = size + len(value.split()) + if is_string(value): + size = size + len(value) + else: + size = size + len(value.split()) value_flags = att_flags(properties) @@ -488,10 +551,13 @@ def parseCharacteristic(fout, parts): write_16(fout, value_flags) write_16(fout, handle) write_uuid(fout, uuid) - if is_string(value): - write_string(fout, value) + if uuid_is_database_hash: + write_database_hash(fout) else: - write_sequence(fout,value) + if is_string(value): + write_string(fout, value) + else: + write_sequence(fout,value) fout.write("\n") defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_VALUE_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) @@ -518,9 +584,14 @@ def parseCharacteristic(fout, parts): write_16(fout, 0x2902) write_16(fout, 0) fout.write("\n") + + database_hash_append_uint16(handle) + database_hash_append_uint16(0x2902) + defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_CLIENT_CONFIGURATION_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) handle = handle + 1 + if properties & property_flags['RELIABLE_WRITE']: size = 2 + 2 + 2 + 2 + 2 write_indent(fout) @@ -532,6 +603,11 @@ def parseCharacteristic(fout, parts): write_16(fout, 0x2900) write_16(fout, 1) # Reliable Write fout.write("\n") + + database_hash_append_uint16(handle) + database_hash_append_uint16(0x2900) + database_hash_append_uint16(1) + handle = handle + 1 def parseCharacteristicUserDescription(fout, parts): @@ -568,6 +644,10 @@ def parseCharacteristicUserDescription(fout, parts): else: write_sequence(fout,value) fout.write("\n") + + database_hash_append_uint16(handle) + database_hash_append_uint16(0x2901) + defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_USER_DESCRIPTION_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) handle = handle + 1 @@ -596,6 +676,10 @@ def parseServerCharacteristicConfiguration(fout, parts): write_16(fout, handle) write_16(fout, 0x2903) fout.write("\n") + + database_hash_append_uint16(handle) + database_hash_append_uint16(0x2903) + defines_for_characteristics.append('#define ATT_CHARACTERISTIC_%s_SERVER_CONFIGURATION_HANDLE 0x%04x' % (current_characteristic_uuid_string, handle)) handle = handle + 1 @@ -630,6 +714,10 @@ def parseCharacteristicFormat(fout, parts): write_sequence(fout, name_space) write_uuid(fout, description) fout.write("\n") + + database_hash_append_uint16(handle) + database_hash_append_uint16(0x2904) + handle = handle + 1 @@ -654,6 +742,10 @@ def parseCharacteristicAggregateFormat(fout, parts): sys.exit(1) write_16(fout, format_handle) fout.write("\n") + + database_hash_append_uint16(handle) + database_hash_append_uint16(0x2905) + handle = handle + 1 def parseReportReference(fout, parts): @@ -930,11 +1022,26 @@ try: parse(args.gattfile, fin, filename, sys.argv[0], ftemp) listHandles(ftemp) + # calc GATT Database Hash + db_hash = aes_cmac(bytearray(16), database_hash_message) + if isinstance(db_hash, str): + # python2 + db_hash_sequence = [('0x%02x' % ord(i)) for i in db_hash] + elif isinstance(db_hash, bytes): + # python3 + db_hash_sequence = [('0x%02x' % i) for i in db_hash] + else: + print("AES CMAC returns unexpected type %s, abort" % type(db_hash)) + sys.exit(1) + # reverse hash to get little endian + db_hash_sequence.reverse() + db_hash_string = ', '.join(db_hash_sequence) + ', ' + # pass 2: insert GATT Database Hash fout = open (filename, 'w') ftemp.seek(0) for line in ftemp: - fout.write(line) + fout.write(line.replace('THE-DATABASE-HASH', db_hash_string)) fout.close() ftemp.close()