async def pair_setup(request: Request) -> Response: global_context = request.global_context config = global_context['config'] parsed_body = tlv_parser.decode(await request.read())[0] requested_state = parsed_body.get(TlvCode.state) expected_state = global_context['pair_setup_expected_state'] logger.debug(f'Requested pair_setup state: {requested_state}') if requested_state == TlvState.m1 and parsed_body.get(TlvCode.method) == TlvMethod.reserved: result = srp_start(config, request.context, expected_state) global_context['pair_setup_expected_state'] = TlvState.m3 elif requested_state == TlvState.m3: result = srp_verify(request.context, expected_state, parsed_body[TlvCode.public_key], parsed_body[TlvCode.proof]) global_context['pair_setup_expected_state'] = TlvState.m5 elif requested_state == TlvState.m5: result = exchange(config, request.context, expected_state, parsed_body[TlvCode.encrypted_data]) global_context['pair_setup_expected_state'] = TlvState.m1 else: raise ValueError('Unknown data received: {}'.format(parsed_body)) if TlvCode.error in result[0]: config.pair_setup_mode = False global_context['pair_setup_expected_state'] = TlvState.m1 return Response(PAIRING_CONTENT_TYPE, data=tlv_parser.encode(result))
def test_tlv_encode_separated(self): data = [{ TlvCode.identifier: 'hello', TlvCode.permissions: 0 }, { TlvCode.identifier: 'world', TlvCode.permissions: 1 }] result = tlv_parser.encode(data) expected_result = bytes([ 0x01, # identifier 0x05, # 5 byte value size 0x68, # ASCII 'h' 0x65, # ASCII 'e' 0x6c, # ASCII 'l' 0x6c, # ASCII 'l' 0x6f, # ASCII 'o' 0x0b, # permissions 0x01, # 1 byte value size 0x00, # user permission 0xff, # separator 0x00, # 0 bytes value size 0x01, # identifier 0x05, # 5 byte value size 0x77, # ASCII 'w' 0x6f, # ASCII 'o' 0x72, # ASCII 'r' 0x6c, # ASCII 'l' 0x64, # ASCII 'd' 0x0b, # permissions 0x01, # 1 byte value size 0x01, # admin permission ]) self.assertEqual(result, expected_result)
def test_tlv_encode_merge(self): data = [{ TlvCode.state: 3, TlvCode.certificate: b'a' * 300, TlvCode.identifier: 'hello', }] result = tlv_parser.encode(data) expected_result = [ 0x06, # state 0x01, # 1 byte value size 0x03, # M3 0x09, # certificate 0xff, # 255 byte value size 0x61, # ASCII 'a' ] expected_result.extend( [0x61] * 254) # 254 more bytes containing 0x61 (ASCII 'a') expected_result.extend([ 0x09, # certificate, continuation of previous TLV 0x2d, # 45 byte value size 0x61, # ASCII 'a' ]) expected_result.extend([0x61] * 44) # 44 more bytes containing 0x61 (ASCII 'a') expected_result.extend([ 0x01, # identifier, new TLV item 0x05, # 5 byte value size 0x68, # ASCII 'h' 0x65, # ASCII 'e' 0x6c, # ASCII 'l' 0x6c, # ASCII 'l' 0x6f, # ASCII 'o' ]) self.assertEqual(result, bytes(expected_result))
def verify_start(config: Config, context: dict, ios_device_public_key: bytes) -> List[dict]: """pair_verify M1 and M2""" curve25519 = X25519PrivateKey.generate() accessory_curve25519_public_key: bytes = curve25519.public_key().public_bytes() shared_secret: bytes = curve25519.exchange(X25519PublicKey.from_public_bytes(ios_device_public_key)) accessory_info: bytes = accessory_curve25519_public_key + config.device_id.encode() + ios_device_public_key signing_key = ed25519.SigningKey(config.accessory_ltsk) accessory_signature = signing_key.sign(accessory_info) sub_tlv = tlv_parser.encode([{ TlvCode.identifier: config.device_id, TlvCode.signature: accessory_signature, }]) hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_VERIFY, info=INFO_VERIFY, backend=default_backend()) session_key = hkdf.derive(shared_secret) chacha = ChaCha20Poly1305(session_key) encrypted_data = chacha.encrypt(NONCE_VERIFY_M2, sub_tlv, None) context['session_key'] = session_key context['shared_secret'] = shared_secret context['accessory_curve25519_public_key'] = accessory_curve25519_public_key context['ios_device_curve25519_public_key'] = ios_device_public_key return [{ TlvCode.state: TlvState.m2, TlvCode.public_key: accessory_curve25519_public_key, TlvCode.encrypted_data: encrypted_data, }]
def exchange(config: Config, context: dict, expected_tlv_state: TlvState, encrypted_data: bytes) -> List[dict]: """pair_setup M5 and M6""" srp = context.get('srp') if expected_tlv_state != TlvState.m5 or not srp: return _error(TlvState.m6, TlvError.unknown, 'Unexpected pair_setup state') hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_ENCRYPT, info=INFO_ENCRYPT, backend=default_backend()) decrypt_key = hkdf.derive(srp.session_key) chacha = ChaCha20Poly1305(decrypt_key) try: data = chacha.decrypt(NONCE_SETUP_M5, encrypted_data, None) except InvalidTag: return _error(TlvState.m6, TlvError.authentication, 'pair_setup M5: invalid auth tag during chacha decryption') try: tlv = tlv_parser.decode(data)[0] except ValueError: return _error(TlvState.m6, TlvError.authentication, 'unable to decode decrypted tlv data') hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_CONTROLLER, info=INFO_CONTROLLER, backend=default_backend()) ios_device_x = hkdf.derive(srp.session_key) ios_device_info = ios_device_x + tlv[TlvCode.identifier].encode() + tlv[TlvCode.public_key] if not _verify_ed25519(key=tlv[TlvCode.public_key], message=ios_device_info, signature=tlv[TlvCode.signature]): return _error(TlvState.m6, TlvError.authentication, 'ios_device_info ed25519 signature verification is failed') config.add_pairing(tlv[TlvCode.identifier], tlv[TlvCode.public_key], ControllerPermission.admin) # save pairing # M6 response generation hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_ACCESSORY, info=INFO_ACCESSORY, backend=default_backend()) accessory_x = hkdf.derive(srp.session_key) signing_key = ed25519.SigningKey(config.accessory_ltsk) public_key = signing_key.get_verifying_key().to_bytes() accessory_info = accessory_x + config.device_id.encode() + public_key accessory_signature = signing_key.sign(accessory_info) sub_tlv = tlv_parser.encode([{ TlvCode.identifier: config.device_id, TlvCode.public_key: public_key, TlvCode.signature: accessory_signature, }]) encrypted_data = chacha.encrypt(NONCE_SETUP_M6, sub_tlv, None) config.pair_setup_mode = False return [{ TlvCode.state: TlvState.m6, TlvCode.encrypted_data: encrypted_data, }]
async def pairings(request: Request) -> Response: logger.debug('/pairings called') config = request.global_context['config'] if config.get_pairing(request.context['ios_device_pairing_id'])[2] != ControllerPermission.admin: logger.error('Controller without admin permission is trying to call /pairings') return Response(PAIRING_CONTENT_TYPE, data=tlv_parser.encode([{ TlvCode.state: TlvState.m2, TlvCode.error: TlvError.authentication, }])) parsed_body = tlv_parser.decode(await request.read())[0] method = parsed_body.get(TlvCode.method) requested_state = parsed_body.get(TlvCode.state) keep_alive = True if method == TlvMethod.list_pairings and requested_state == TlvState.m1: logger.debug('/pairings list_pairings called') result = list_pairings(config) elif method == TlvMethod.add_pairing and requested_state == TlvState.m1: logger.debug('/pairings add_pairing called') ios_device_pairing_id = parsed_body[TlvCode.identifier] ios_device_public_key = parsed_body[TlvCode.public_key] permission = parsed_body[TlvCode.permissions] result = add_pairing(config, ios_device_pairing_id, ios_device_public_key, ControllerPermission(permission)) elif method == TlvMethod.remove_pairing and requested_state == TlvState.m1: logger.debug('/pairings remove_pairing called') ios_device_pairing_id = parsed_body[TlvCode.identifier] result = remove_pairing(config, ios_device_pairing_id) if not config.get_pairing(ios_device_pairing_id)[0]: keep_alive = False else: raise ValueError('Unknown data received: {}'.format(parsed_body)) return Response(PAIRING_CONTENT_TYPE, data=tlv_parser.encode(result), keep_alive=keep_alive)
async def pair_verify(request: Request) -> Response: config = request.global_context['config'] upgrade = False parsed_body = tlv_parser.decode(await request.read())[0] requested_state = parsed_body.get(TlvCode.state) logger.debug(f'Requested pair_verify state: {requested_state}') if requested_state == TlvState.m1: result = verify_start(config, request.context, parsed_body[TlvCode.public_key]) elif requested_state == TlvState.m3: result = verify_finish(config, request.context, parsed_body[TlvCode.encrypted_data]) if request.context.get('paired'): upgrade = True # verify_finish end up without errors, upgrade to fully encrypted communication else: raise ValueError('Unknown data received: {}'.format(parsed_body)) return Response(PAIRING_CONTENT_TYPE, data=tlv_parser.encode(result), upgrade=upgrade)
def test_tlv_encode(self): data = [{ TlvCode.state: 3, TlvCode.identifier: 'hello', }] result = tlv_parser.encode(data) expected_result = bytes([ 0x06, # state 0x01, # 1 byte value size 0x03, # M3 0x01, # identifier 0x05, # 5 byte value size 0x68, # ASCII 'h' 0x65, # ASCII 'e' 0x6c, # ASCII 'l' 0x6c, # ASCII 'l' 0x6f, # ASCII 'o' ]) self.assertEqual(result, expected_result)