Skip to content

Commit

Permalink
Merge pull request #135 from ThomasGerstenberg/v0.5.0
Browse files Browse the repository at this point in the history
Bump version to v0.5.0
  • Loading branch information
ThomasGerstenberg committed Mar 2, 2023
2 parents 2c877ec + c803535 commit 6caf2c3
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 17 deletions.
2 changes: 1 addition & 1 deletion blatann/device.py
Expand Up @@ -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)
Expand Down
50 changes: 48 additions & 2 deletions blatann/gap/default_bond_db.py
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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())
Expand Down
23 changes: 13 additions & 10 deletions blatann/gap/smp_crypto.py
Expand Up @@ -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
"""
Expand All @@ -26,15 +27,15 @@ 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:
x = x[::-1]
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
"""
Expand All @@ -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:]
Expand All @@ -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
Expand All @@ -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
Expand All @@ -95,16 +98,16 @@ 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.
This is used for resolving private addresses where a private address
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")
Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions 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
------

Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/source/conf.py
Expand Up @@ -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 ---------------------------------------------------
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -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:
Expand Down

0 comments on commit 6caf2c3

Please sign in to comment.