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_decode_separated(self): data = 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 byte 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 ]) result = tlv_parser.decode(data) expected_result = [{ TlvCode.identifier: 'hello', TlvCode.permissions: 0 }, { TlvCode.identifier: 'world', TlvCode.permissions: 1 }] self.assertEqual(result, expected_result)
def test_tlv_decode_merge(self): data = [ 0x06, # state 0x01, # 1 byte value size 0x03, # M3 0x09, # certificate 0xff, # 255 byte value size 0x61, # ASCII 'a' ] data.extend([0x61] * 254) # 254 more bytes containing 0x61 (ASCII 'a') data.extend([ 0x09, # certificate, continuation of previous TLV 0x2d, # 45 byte value size 0x61, # ASCII 'a' ]) data.extend([0x61] * 44) # 44 more bytes containing 0x61 (ASCII 'a') data.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' ]) result = tlv_parser.decode(bytes(data))[0] expected_result = { TlvCode.state: 3, TlvCode.certificate: b'a' * 300, TlvCode.identifier: 'hello' } self.assertEqual(result, expected_result)
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, }]
def verify_finish(config: Config, context: dict, encrypted_data: bytes) -> List[dict]: """pair_verify M3 and M4""" session_key = context.get('session_key') accessory_curve25519_public_key = context.get('accessory_curve25519_public_key') ios_device_curve25519_public_key = context.get('ios_device_curve25519_public_key') if not session_key or not accessory_curve25519_public_key or not ios_device_curve25519_public_key: return _error(TlvState.m4, TlvError.authentication, 'verify_finished call before successful verify_start') chacha = ChaCha20Poly1305(session_key) try: data = chacha.decrypt(NONCE_VERIFY_M3, encrypted_data, None) except InvalidTag: return _error(TlvState.m4, TlvError.authentication, 'invalid auth tag during chacha decryption') try: tlv = tlv_parser.decode(data)[0] except ValueError: return _error(TlvState.m4, TlvError.authentication, 'unable to decode decrypted tlv data') ios_device_ltpk = config.get_pairing(tlv[TlvCode.identifier])[1] if not ios_device_ltpk: return _error(TlvState.m4, TlvError.authentication, 'unable to find requested ios device in config file') ios_device_info = ios_device_curve25519_public_key + tlv[TlvCode.identifier].encode() + \ accessory_curve25519_public_key if not _verify_ed25519(ios_device_ltpk, message=ios_device_info, signature=tlv[TlvCode.signature]): return _error(TlvState.m4, TlvError.authentication, 'ios_device_info ed25519 signature verification is failed') context['paired'] = True context['ios_device_pairing_id'] = tlv[TlvCode.identifier] hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_CONTROL, info=INFO_CONTROL_WRITE, backend=default_backend()) context['decrypt_key'] = hkdf.derive(context['shared_secret']) hkdf = HKDF(algorithm=SHA512(), length=32, salt=SALT_CONTROL, info=INFO_CONTROL_READ, backend=default_backend()) context['encrypt_key'] = hkdf.derive(context['shared_secret']) return [{ TlvCode.state: TlvState.m4, }]
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_decode(self): data = 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' ]) result = tlv_parser.decode(data)[0] expected_result = {TlvCode.state: 3, TlvCode.identifier: 'hello'} self.assertEqual(result, expected_result) with self.assertRaises(ValueError): tlv_parser.decode(bytes([ 0xfa, # unknown TlvCode ])) with self.assertRaises(ValueError): tlv_parser.decode( bytes([ 0x01, # identifier (string type) 0x01, # 1 byte value size 0xf0, # invalid unicode symbol ])) with self.assertRaises(ValueError): tlv_parser.decode( bytes([ 0x00, # method (integer type) 0x02, # 2 byte value size 0x00, # first integer byte 0x00, # second integer byte (only 1-byte length integers is supported) ]))
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)