def _getKeyForLevel(userLevel: UserLevel, settings): _LOGGER.debug(f"get key for level {userLevel}") if settings.initializedKeys == False and userLevel != UserLevel.setup: raise CrownstoneBleException( EncryptionError.NO_ENCRYPTION_KEYS_SET, "Could not encrypt: Keys not set.") key = None if userLevel == UserLevel.admin: key = settings.adminKey elif userLevel == UserLevel.member: key = settings.memberKey elif userLevel == UserLevel.basic: key = settings.basicKey elif userLevel == UserLevel.setup: key = settings.setupKey else: raise CrownstoneBleException( EncryptionError.NO_ENCRYPTION_KEYS_SET, "Could not encrypt: Invalid key for encryption.") if key is None: raise CrownstoneBleException( EncryptionError.NO_ENCRYPTION_KEYS_SET, "Could not encrypt: Keys not set.") return key
async def waitForMode(self, address, requiredMode: CrownstoneOperationMode, scanDuration=5): """ This will wait until it has received an advertisement from the Crownstone with the specified address. Once it has received an advertisement, it knows the mode. We will scan for the scanDuration amount of seconds or until the Crownstone is in the required mode. It can throw the following CrownstoneBleException - BleError.NO_SCANS_RECEIVED We have not received any scans from this Crownstone, and can't say anything about it's state. - BleError.DIFFERENT_MODE_THAN_REQUIRED During the {scanDuration} seconds of scanning, the Crownstone was not in the required mode. """ _LOGGER.debug( f"waitForMode address={address} requiredMode={requiredMode} scanDuration={scanDuration}" ) checker = ModeChecker(address, requiredMode, True) subscriptionId = BleEventBus.subscribe( BleTopics.rawAdvertisement, lambda scanData: checker.handleAdvertisement(scanData)) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionId) result = checker.getResult() if result is None: raise CrownstoneBleException( BleError.NO_SCANS_RECEIVED, f'During the {scanDuration} seconds of scanning, no advertisement was received from this address.' ) if result != requiredMode: raise CrownstoneBleException( BleError.DIFFERENT_MODE_THAN_REQUIRED, f'During the {scanDuration} seconds of scanning, the Crownstone was not in the required mode..' )
def encrypt(dataArray, settings): if settings.sessionNonce is None: raise CrownstoneBleException( EncryptionError.NO_SESSION_NONCE_SET, "Can't Decrypt: No session nonce set") if settings.userLevel == UserLevel.unknown: raise CrownstoneBleException( EncryptionError.NO_ENCRYPTION_KEYS_SET, "Can't Decrypt: No encryption keys set.") packetNonce = [0] * PACKET_NONCE_LENGTH # create a random nonce for i in range(0, PACKET_NONCE_LENGTH): packetNonce[i] = EncryptionHandler.getRandomNumber() key = EncryptionHandler._getKey(settings) encryptedData = EncryptionHandler.encryptCTR(dataArray, packetNonce, settings, key) result = packetNonce + [settings.userLevel.value] for byte in encryptedData: result.append(byte) return bytes(result)
async def setupNotificationStream(self, serviceUUID, characteristicUUID, writeCommand, resultHandler, timeout): _LOGGER.debug( f"setupNotificationStream serviceUUID={serviceUUID} characteristicUUID={characteristicUUID} timeout={timeout}" ) await self.is_connected_guard() # setup the collecting of the notification data. _LOGGER.debug(f"setupNotificationStream: subscribe for notifications.") notificationDelegate = NotificationDelegate(None, self.settings) await self.activeClient.subscribeNotifications( characteristicUUID, notificationDelegate.handleNotification) # execute something that will trigger the notifications _LOGGER.debug(f"setupNotificationStream: writeCommand().") await writeCommand() # wait for the results to come in. self.notificationLoopActive = True loopCount = 0 successful = False polInterval = 0.1 while self.notificationLoopActive and loopCount < (timeout / polInterval): await asyncio.sleep(polInterval) _LOGGER.debug( f"loopActive={self.notificationLoopActive} loopCount={loopCount}" ) loopCount += 1 if notificationDelegate.result is not None: command = resultHandler(notificationDelegate.result) notificationDelegate.reset() if command == ProcessType.ABORT_ERROR: _LOGGER.debug("abort") self.notificationLoopActive = False self.activeClient.unsubscribeNotifications( characteristicUUID) raise CrownstoneBleException( BleError.ABORT_NOTIFICATION_STREAM_W_ERROR, "Aborting the notification stream because the resultHandler raised an error." ) elif command == ProcessType.FINISHED: _LOGGER.debug("finished") self.notificationLoopActive = False successful = True elif command == ProcessType.CONTINUE: _LOGGER.debug("continue") if not successful: self.activeClient.unsubscribeNotifications(characteristicUUID) raise CrownstoneBleException( BleError.NOTIFICATION_STREAM_TIMEOUT, "Notification stream not finished within timeout.") # remove subscription from this characteristic self.activeClient.unsubscribeNotifications(characteristicUUID)
async def getMode(self, address, scanDuration=3) -> CrownstoneOperationMode: """ This will wait until it has received an advertisement from the Crownstone with the specified address. Once it has received an advertisement, it knows the mode. We will return once we know. It can throw the following CrownstoneBleException - BleError.NO_SCANS_RECEIVED We have not received any scans from this Crownstone, and can't say anything about it's state. """ _LOGGER.debug(f"getMode address={address} scanDuration={scanDuration}") checker = ModeChecker(address, None) subscriptionId = BleEventBus.subscribe( BleTopics.rawAdvertisement, lambda scanData: checker.handleAdvertisement(scanData)) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionId) result = checker.getResult() if result is None: raise CrownstoneBleException( BleError.NO_SCANS_RECEIVED, f'During the {scanDuration} seconds of scanning, no advertisement was received from this address.' ) return result
async def isCrownstoneInSetupMode(self, address: str, scanDuration=3, waitUntilInSetupMode=False) -> bool: _LOGGER.warning( "isCrownstoneInSetupMode is deprecated. Will be removed in v3. Use either getMode or waitForMode instead." ) """ This will wait until it has received an advertisement from the Crownstone with the specified address. Once it has received an advertisement, it knows the mode. With default value for waitUntilInSetupMode (False), it will return True if the Crownstone is in setup mode, False if it isn't. You can use the boolean waitUntilInSetupMode to have it ignore advertisements from this Crownstone in other modes than setup mode. It can throw the following CrownstoneBleException - BleError.NO_SCANS_RECEIVED We have not received any scans from this Crownstone, and can't say anything about it's state. """ _LOGGER.debug( f"isCrownstoneInSetupMode address={address} scanDuration={scanDuration} waitUntilInSetupMode={waitUntilInSetupMode}" ) checker = ModeChecker(address, CrownstoneOperationMode.SETUP, waitUntilInSetupMode) subscriptionId = BleEventBus.subscribe(BleTopics.advertisement, checker.handleAdvertisement) await self.ble.scan(duration=scanDuration) BleEventBus.unsubscribe(subscriptionId) result = checker.getResult() if result is None: raise CrownstoneBleException( BleError.NO_SCANS_RECEIVED, f'During the {scanDuration} seconds of scanning, no advertisement was received from this address.' ) return result
async def is_connected_guard(self): connected = await self.is_connected() if not connected: _LOGGER.debug( f"Could not perform action since the client is not connected!." ) raise CrownstoneBleException("Not connected.")
async def setupSingleNotification(self, serviceUUID, characteristicUUID, writeCommand): _LOGGER.debug( f"setupSingleNotification serviceUUID={serviceUUID} characteristicUUID={characteristicUUID}" ) await self.is_connected_guard() # setup the collecting of the notification data. _LOGGER.debug(f"setupSingleNotification: subscribe for notifications.") notificationDelegate = NotificationDelegate(self._killNotificationLoop, self.settings) await self.activeClient.subscribeNotifications( characteristicUUID, notificationDelegate.handleNotification) # execute something that will trigger the notifications _LOGGER.debug(f"setupSingleNotification: writeCommand().") await writeCommand() # wait for the results to come in. self.notificationLoopActive = True loopCount = 0 polInterval = 0.1 while self.notificationLoopActive and loopCount < (12.5 / polInterval): await asyncio.sleep(polInterval) loopCount += 1 if notificationDelegate.result is None: self.activeClient.unsubscribeNotifications(characteristicUUID) raise CrownstoneBleException( BleError.NO_NOTIFICATION_DATA_RECEIVED, "No notification data received.") self.activeClient.unsubscribeNotifications(characteristicUUID) return notificationDelegate.result
def __init__(self, dataArray): self.nonce = None self.userLevel = None self.payload = None prefixLength = PACKET_NONCE_LENGTH + PACKET_USER_LEVEL_LENGTH if len(dataArray) < prefixLength + BLOCK_LENGTH: raise CrownstoneBleException( EncryptionError.INVALID_ENCRYPTION_PACKAGE, "Invalid package for encryption. It is too short (min length 20) got " + str(len(dataArray)) + " bytes.") self.nonce = [0] * PACKET_NONCE_LENGTH for i in range(0, PACKET_NONCE_LENGTH): self.nonce[i] = dataArray[i] if dataArray[PACKET_NONCE_LENGTH] > 2 and dataArray[ PACKET_NONCE_LENGTH] != UserLevel.setup.value: raise CrownstoneBleException( EncryptionError.INVALID_ENCRYPTION_USER_LEVEL, "User level in read packet is invalid:" + str(dataArray[PACKET_NONCE_LENGTH])) try: self.userLevel = UserLevel(dataArray[PACKET_NONCE_LENGTH]) except ValueError: raise CrownstoneBleException( EncryptionError.INVALID_ENCRYPTION_USER_LEVEL, "User level in read packet is invalid:" + str(dataArray[PACKET_NONCE_LENGTH])) payload = [0] * (len(dataArray) - prefixLength) for i in range(0, (len(dataArray) - prefixLength)): payload[i] = dataArray[i + prefixLength] if len(payload) % 16 != 0: raise CrownstoneBleException( EncryptionError.INVALID_ENCRYPTION_PACKAGE, f"Invalid size for encrypted payload: len={len(payload)} payload={payload}" ) self.payload = payload
async def setupCrownstone(self, address, sphereId, crownstoneId, meshDeviceKey, ibeaconUUID, ibeaconMajor, ibeaconMinor): if not self.defaultKeysOverridden: raise CrownstoneBleException( BleError.NO_ENCRYPTION_KEYS_SET, "Keys are not initialized so I can't put anything on the Crownstone. Make sure you call .setSettings, loadSettingsFromFile or loadSettingsFromDictionary" ) await self.setup.setup(address, sphereId, crownstoneId, meshDeviceKey, ibeaconUUID, ibeaconMajor, ibeaconMinor)
def ProcessSessionNoncePacket(encryptedPacket, key, settings): # decrypt it decrypted = EncryptionHandler.decryptECB(encryptedPacket, key) packet = SessionDataPacket(decrypted) if packet.validation == CHECKSUM: # load into the settings object settings.setSessionNonce(packet.sessionNonce) settings.setValidationKey(packet.validationKey) settings.setCrownstoneProtocolVersion(packet.protocol) else: raise CrownstoneBleException(BleError.COULD_NOT_VALIDATE_SESSION_NONCE, "Could not validate the session nonce.")
def _verifyDecryption(decrypted, validationKey): # the conversion to uint32 only takes the first 4 bytes if Conversion.uint8_array_to_uint32( decrypted) == Conversion.uint8_array_to_uint32(validationKey): # remove checksum from decryption and return payload result = [0] * (len(decrypted) - SESSION_KEY_LENGTH) for i in range(0, len(result)): result[i] = decrypted[i + SESSION_KEY_LENGTH] return result else: raise CrownstoneBleException( EncryptionError.ENCRYPTION_VALIDATION_FAILED, "Failed to validate result, Could not decrypt")
def decrypt(data, settings): if settings.sessionNonce is None: raise CrownstoneBleException( EncryptionError.NO_SESSION_NONCE_SET, "Can't Decrypt: No session nonce set") if settings.userLevel == UserLevel.unknown: raise CrownstoneBleException( EncryptionError.NO_ENCRYPTION_KEYS_SET, "Can't Decrypt: No encryption keys set.") #unpack the session data package = EncryptedPackage(data) key = EncryptionHandler._getKeyForLevel(package.userLevel, settings) # decrypt data decrypted = EncryptionHandler.decryptCTR(package.payload, package.nonce, settings.sessionNonce, key) return EncryptionHandler._verifyDecryption(decrypted, settings.validationKey)
def generateIV(packetNonce, sessionData): if len(packetNonce) != PACKET_NONCE_LENGTH: raise CrownstoneBleException( EncryptionError.INVALID_SESSION_NONCE, "Invalid size for session nonce packet") IV = [0] * NONCE_LENGTH # the IV used in the CTR mode is 8 bytes, the first 3 are random for i in range(0, PACKET_NONCE_LENGTH): IV[i] = packetNonce[i] # the IV used in the CTR mode is 8 bytes, the last 5 are from the session data for i in range(0, SESSION_DATA_LENGTH): IV[i + PACKET_NONCE_LENGTH] = sessionData[i] return IV
async def fastSetupV2(self, sphereId, crownstoneId, meshDeviceKey, ibeaconUUID, ibeaconMajor, ibeaconMinor): if not self.core.settings.initializedKeys: raise CrownstoneBleException( BleError.NO_ENCRYPTION_KEYS_SET, "Keys are not initialized so I can't put anything on the Crownstone. Make sure you call .setSettings(adminKey, memberKey, basicKey, serviceDataKey, localizationKey, meshApplicationKey, meshNetworkKey" ) await self.handleSetupPhaseEncryption() await self.core.ble.setupNotificationStream( CSServices.SetupService, SetupCharacteristics.Result, lambda: self._writeFastSetupV2( sphereId, crownstoneId, meshDeviceKey, ibeaconUUID, ibeaconMajor, ibeaconMinor), lambda notificationResult: self._handleResult(notificationResult), 3) _LOGGER.info("Closing Setup V2.") self.core.settings.exitSetup()
def loadSettingsFromDictionary(self, data): if "admin" not in data: raise CrownstoneBleException(CrownstoneError.ADMIN_KEY_REQURED) if "member" not in data: raise CrownstoneBleException(CrownstoneError.MEMBER_KEY_REQUIRED) if "basic" not in data: raise CrownstoneBleException(CrownstoneError.BASIC_KEY_REQURED) if "serviceDataKey" not in data: raise CrownstoneBleException( CrownstoneError.SERVICE_DATA_KEY_REQUIRED) if "localizationKey" not in data: raise CrownstoneBleException( CrownstoneError.LOCALIZATION_KEY_REQUIRED) if "meshApplicationKey" not in data: raise CrownstoneBleException(CrownstoneError.MESH_APP_KEY) if "meshNetworkKey" not in data: raise CrownstoneBleException(CrownstoneError.MESH_NETWORK_KEY) self.setSettings(data["admin"], data["member"], data["basic"], data["serviceDataKey"], data["localizationKey"], data["meshApplicationKey"], data["meshNetworkKey"])
def setValidationKey(self, validationKey): if len(validationKey) != SESSION_KEY_LENGTH: raise CrownstoneBleException(EncryptionError.INVALID_SESSION_DATA, "Invalid Session Data") self.validationKey = validationKey
def setSessionNonce(self, sessionNonce): if len(sessionNonce) != SESSION_DATA_LENGTH: raise CrownstoneBleException(EncryptionError.INVALID_SESSION_DATA, "Invalid Session Data") self.sessionNonce = sessionNonce