diff --git a/blatann/device.py b/blatann/device.py index 6944af3..f9cc313 100644 --- a/blatann/device.py +++ b/blatann/device.py @@ -349,7 +349,7 @@ def set_privacy_settings(self, enabled: bool, resolvable_address: bool = True, :param enabled: True to enable device privacy. Note that only device privacy is supported, network privacy is not :param resolvable_address: True to use a private random resolvable address. If the address is resolvable, bonded peers can use the device's IRK to determine the - device's actual public/random address. + device's actual public/random address. :param update_rate_seconds: How often the address should be changed/updated, in seconds. Default is 900 (15min) """ params = nrf_types.BLEGapPrivacyParams(enabled, resolvable_address, update_rate_seconds) diff --git a/blatann/gap/default_bond_db.py b/blatann/gap/default_bond_db.py index bba5953..c64c409 100644 --- a/blatann/gap/default_bond_db.py +++ b/blatann/gap/default_bond_db.py @@ -23,18 +23,40 @@ class DatabaseStrategy: + """ + Abstract base class defining the methods and properties for serializing/deserializing bond databases + into different formats + """ @property def file_extension(self) -> str: + """ + The file extension that this strategy can serialize/deserialize + """ raise NotImplementedError - def load(self, filename) -> DefaultBondDatabase: + def load(self, filename: str) -> DefaultBondDatabase: + """ + Loads/deserializes a database file + + :param filename: Name of the file to deserialize + :return: The loaded bond database + """ raise NotImplementedError def save(self, filename: str, db: DefaultBondDatabase): + """ + Saves/serializes a database to a file + + :param filename: Filename to save the database to + :param db: The database object serialize + """ raise NotImplementedError class JsonDatabaseStrategy(DatabaseStrategy): + """ + Strategy for serializing/deseriralizing bond databases in JSON format + """ @property def file_extension(self) -> str: return ".json" @@ -52,6 +74,9 @@ def save(self, filename: str, db: DefaultBondDatabase): class PickleDatabaseStrategy(DatabaseStrategy): + """ + Strategy for serializing/deserializing bond databases in pickle format + """ @property def file_extension(self) -> str: return ".pkl" @@ -75,15 +100,18 @@ def save(self, filename, db: DefaultBondDatabase): PickleDatabaseStrategy(), JsonDatabaseStrategy() ] +"""List of supported database strategies""" + database_strategies_by_extension: typing.Dict[str, DatabaseStrategy] = { s.file_extension: s for s in database_strategies } +"""Mapping of database file extensions to their respective strategies""" # TODO 04.16.2019: Replace pickling with something more secure class DefaultBondDatabaseLoader(BondDatabaseLoader): - def __init__(self, filename=user_default_db_base_filename): + def __init__(self, filename="user"): if filename in special_bond_db_filemap: base_filename = special_bond_db_filemap[filename] self.filename = self.migrate_to_json(base_filename) @@ -191,6 +219,16 @@ def find_entry(self, own_address: PeerAddress, peer_address: PeerAddress, peer_is_client: bool, master_id: BLEGapMasterId = None) -> Optional[BondDbEntry]: + """ + Attempts to find a bond entry which satisfies the parameters provided + + :param own_address: The local device's BLE address + :param peer_address: The peer's BLE address + :param peer_is_client: Flag indicating the role of the peer. + True if the peer is a client/central, False if the peer is a server/peripheral + :param master_id: If during a security info request, this is the Master ID provided by the peer to search for + :return: The first entry that satisfies the above parameters, or None if no entry was found + """ for record in self._records: if record.matches_peer(own_address, peer_address, peer_is_client, master_id): return record @@ -199,6 +237,14 @@ def find_entry(self, own_address: PeerAddress, def migrate_bond_database(from_file: str, to_file: str): + """ + Migrates a bond database file from one format to another. + + For supported extensions/formats, check ``database_strategies_by_extension.keys()`` + + :param from_file: File to migrate from + :param to_file: File to migrate to + """ from_ext = os.path.splitext(from_file)[1] to_ext = os.path.splitext(to_file)[1] supported_extensions = ", ".join(database_strategies_by_extension.keys()) diff --git a/blatann/gap/smp_crypto.py b/blatann/gap/smp_crypto.py index ed8023f..d3b4b1a 100644 --- a/blatann/gap/smp_crypto.py +++ b/blatann/gap/smp_crypto.py @@ -4,13 +4,14 @@ from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from blatann.gap.gap_types import PeerAddress # Elliptic Curve used for LE Secure connections _lesc_curve = ec.SECP256R1 _backend = default_backend() -def lesc_pubkey_to_raw(public_key, little_endian=True): +def lesc_pubkey_to_raw(public_key: ec.EllipticCurvePublicKey, little_endian=True) -> bytearray: """ Converts from a python public key to the raw (x, y) bytes for the nordic """ @@ -26,7 +27,7 @@ def lesc_pubkey_to_raw(public_key, little_endian=True): return pubkey_raw -def lesc_privkey_to_raw(private_key, little_endian=True): +def lesc_privkey_to_raw(private_key: ec.EllipticCurvePrivateKey, little_endian=True) -> bytearray: pk = private_key.private_numbers() x = bytearray.fromhex("{:064x}".format(pk.private_value)) if little_endian: @@ -34,7 +35,7 @@ def lesc_privkey_to_raw(private_key, little_endian=True): return x -def lesc_pubkey_from_raw(raw_key, little_endian=True): +def lesc_pubkey_from_raw(raw_key: bytes, little_endian=True) -> ec.EllipticCurvePublicKey: """ Converts from raw (x, y) bytes to a public key that can be used for the DH request """ @@ -53,7 +54,7 @@ def lesc_pubkey_from_raw(raw_key, little_endian=True): return public_numbers.public_key(_backend) -def lesc_privkey_from_raw(raw_priv_key, raw_pub_key, little_endian=True): +def lesc_privkey_from_raw(raw_priv_key: bytes, raw_pub_key: bytes, little_endian=True) -> ec.EllipticCurvePrivateKey: key_len = len(raw_pub_key) x_raw = raw_pub_key[:key_len//2] y_raw = raw_pub_key[key_len//2:] @@ -71,7 +72,7 @@ def lesc_privkey_from_raw(raw_priv_key, raw_pub_key, little_endian=True): return priv_numbers.private_key(_backend) -def lesc_generate_private_key(): +def lesc_generate_private_key() -> ec.EllipticCurvePrivateKey: """ Generates a new private key that can be used for LESC pairing @@ -80,7 +81,9 @@ def lesc_generate_private_key(): return ec.generate_private_key(_lesc_curve, _backend) -def lesc_compute_dh_key(private_key, peer_public_key, little_endian=False): +def lesc_compute_dh_key(private_key: ec.EllipticCurvePrivateKey, + peer_public_key: ec.EllipticCurvePublicKey, + little_endian=False) -> bytes: """ Computes the DH key for LESC pairing given our private key and the peer's public key @@ -95,7 +98,7 @@ def lesc_compute_dh_key(private_key, peer_public_key, little_endian=False): return dh_key -def ble_ah(key, p_rand): +def ble_ah(key: bytes, p_rand: bytes) -> bytes: """ Function for calculating the ah() hash function described in Bluetooth core specification 4.2 section 3.H.2.2.2. @@ -103,8 +106,8 @@ def ble_ah(key, p_rand): is prand[3] || aes-128(irk, prand[3]) % 2^24 :param key: the IRK to use, in big endian format - :param p_rand: - :return: + :param p_rand: The random component, first 3 bytes of the address + :return: The last 3 bytes of the encrypted hash """ if len(p_rand) != 3: raise ValueError("Prand must be a str or bytes of length 3") @@ -120,7 +123,7 @@ def ble_ah(key, p_rand): return encrypted_hash[-3:] -def private_address_resolves(peer_addr, irk): +def private_address_resolves(peer_addr: PeerAddress, irk: bytes) -> bool: """ Checks if the given peer address can be resolved with the IRK diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 5bd9c99..40f6d52 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,6 +1,45 @@ Changelog ========= +v0.5.0 +------ + +v0.5.0 reworks bonding database to JSON, adds a few features, and fixes a few bugs. +Full list of issues and PRs for this release can be found here: `0.5.0 Milestone`_ + +**Highlights** + +- Adds support for the scanner to resolve peer addresses. + + - :class:`~blatann.gap.advertise_data.ScanReport` has 2 new properties: ``is_bonded_device: bool`` and ``resolved_address: Optional[PeerAddress]`` + +- Adds support for privacy settings to advertise with a private resolvable or non-resolvable address + +- Adds parameter to device configuration to set a different connection event length + +**Fixes** + +- Fixes incorrect variable name when a queued GATT operation times out (thanks @klow68) + +- Fixes incorrect key length when converting an LESC private key to a raw bytearray (thanks @klow68). Function is unused within blatann + +- Fixes issue where the service changed characteristic was not correctly getting added to the GATT server when configured + +**Changes** + +- Reworks the bond database to be saved using JSON instead of pickle. + + - Existing ``"system"`` and ``"user"`` database files configured in the BLE device will automatically be migrated to JSON + + - Other database files configured by filepath will continue to use pickle and can be updated manually using :meth:`~blatann.gap.default_bond_db.migrate_bond_database` + +- Bond DB entries will now save the local BLE address that was used to generate the bonding data. + + - This will allow multiple nRF boards to use the same DB file and not resolve bond entries if it was not created with that board/address. This fixes potential issues where restoring a connection to a peer that was bonded to a different nRF board can cause the local device to think it has a bond, however the peer has bond info with a different, mismatched address. + +- Moves bond-related resolve logic out of the security manager and into the bond database + + v0.4.0 ------ @@ -274,6 +313,7 @@ public API should be mostly unchanged except for the noted changes below. - Added ``AdvertisingData.to_bytes()`` to retrieve the data packet that will be advertised over the air .. _0.4.0 Milestone: https://github.com/ThomasGerstenberg/blatann/milestone/7?closed=1 +.. _0.5.0 Milestone: https://github.com/ThomasGerstenberg/blatann/milestone/8?closed=1 .. _Event callback example: https://github.com/ThomasGerstenberg/blatann/blob/1f85c68cf6db84ba731a55d3d22b8c2eb0d2779b/tests/integrated/test_advertising_duration.py#L48 .. _ScanFinishedWaitable example: https://github.com/ThomasGerstenberg/blatann/blob/1f85c68cf6db84ba731a55d3d22b8c2eb0d2779b/blatann/examples/scanner.py#L20 .. _Peripheral Descriptor Example: https://github.com/ThomasGerstenberg/blatann/blob/master/blatann/examples/peripheral_descriptors.py diff --git a/docs/source/conf.py b/docs/source/conf.py index 6d7d20a..5bd9116 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,9 +24,9 @@ author = u'Thomas Gerstenberg' # The short X.Y version -version = u'v0.4.x' +version = u'v0.5.x' # The full version, including alpha/beta/rc tags -release = u'v0.4.0' +release = u'v0.5.0' # -- General configuration --------------------------------------------------- @@ -65,7 +65,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/setup.py b/setup.py index 9d15859..1dea6af 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from os import path -VERSION = "v0.4.0" +VERSION = "v0.5.0" HERE = path.dirname(__file__) with open(path.join(HERE, "README.md"), "r", encoding="utf-8") as f: