Пример #1
0
 def _validate_userid_signature(user: User) -> Optional[Address]:
     """ Validate a userId format and signature on displayName, and return its address"""
     # display_name should be an address in the self._userid_re format
     match = MatrixTransport._userid_re.match(user.user_id)
     if not match:
         return None
     encoded_address: str = match.group(1)
     address: Address = to_canonical_address(encoded_address)
     try:
         displayname = user.get_display_name()
         recovered = MatrixTransport._recover(
             user.user_id.encode(),
             decode_hex(displayname),
         )
         if not (address and recovered and recovered == address):
             return None
     except (
         DecodeError,
         TypeError,
         InvalidSignature,
         MatrixRequestError,
         json.decoder.JSONDecodeError,
     ):
         return None
     return address
Пример #2
0
 def search_user_directory(self, term: str) -> List[User]:
     """
     Search user directory for a given term, returning a list of users
     Args:
         term: term to be searched for
     Returns:
         user_list: list of users returned by server-side search
     """
     try:
         response = self.api._send("POST", "/user_directory/search", {"search_term": term})
     except MatrixRequestError as ex:
         if ex.code >= 500:
             log.error(
                 "Ignoring Matrix error in `search_user_directory`",
                 exc_info=ex,
                 term=term,
             )
             return list()
         else:
             raise ex
     try:
         return [
             User(self.api, _user["user_id"], _user["display_name"])
             for _user in response["results"]
         ]
     except KeyError:
         return list()
Пример #3
0
def _validate_userid_signature(user: User) -> bool:
    # display_name should be an address present in the user_id
    recovered = signing.recover_address(user.user_id.encode(),
                                        signature=data_decoder(
                                            user.get_display_name()),
                                        hasher=eth_sign_sha3)
    return address_encoder(recovered).lower() in user.user_id
 def _user_from_id(self,
                   user_id: str,
                   display_name: Optional[str] = None) -> Optional[User]:
     try:
         return User(self._client.api, user_id, display_name)
     except ValueError:
         log.error("Matrix server returned an invalid user_id.")
     return None
Пример #5
0
 def get_joined_members(self) -> List[User]:
     """ Return a list of members of this room. """
     response = self.client.api.get_room_members(self.room_id)
     for event in response['chunk']:
         if event['content']['membership'] == 'join':
             user_id = event["state_key"]
             if user_id not in self._members:
                 self._mkmembers(
                     User(self.client.api, user_id,
                          event['content'].get('displayname')))
     return list(self._members.values())
Пример #6
0
def create_new_users_for_address(signer=None, number_of_users=1):
    users = list()
    if signer is None:
        signer = make_signer()

    for i in range(number_of_users):
        user_id = f"@{signer.address_hex.lower()}:server{i}"
        signature_bytes = signer.sign(user_id.encode())
        signature_hex = encode_hex(signature_bytes)
        user = User(api=None, user_id=user_id, displayname=signature_hex)
        users.append(user)
    return users
Пример #7
0
 def get_joined_members(self, force_resync: bool = False) -> List[User]:
     """ Return a list of members of this room. """
     if force_resync:
         response = self.client.api.get_room_members(self.room_id)
         for event in response["chunk"]:
             if event["content"]["membership"] == "join":
                 user_id = event["state_key"]
                 if user_id not in self._members:
                     self._mkmembers(
                         User(self.client.api, user_id,
                              event["content"].get("displayname")))
     return list(self._members.values())
Пример #8
0
    def _handle_message(self, room: Optional[Room],
                        message: MatrixMessage) -> List[SignedMessage]:
        """Handle a single Matrix message.

        The matrix message is expected to be a NDJSON, and each entry should be
        a valid JSON encoded Raiden message.

        If `room` is None this means we are processing a `to_device` message
        """
        is_valid_type = (message["type"] == "m.room.message"
                         and message["content"]["msgtype"]
                         == MatrixMessageType.TEXT.value)
        if not is_valid_type:
            return []

        sender_id = message["sender"]
        self._displayname_cache.warm_users([User(self._client.api, sender_id)])
        # handles the "Could not get 'display_name' for user" case

        try:
            displayname = self._displayname_cache.userid_to_displayname[
                sender_id]
        except KeyError:
            log.exception("Could not warm display cache", peer_user=sender_id)
            return []

        peer_address = validate_user_id_signature(sender_id, displayname)

        if not peer_address:
            log.debug(
                "Message from invalid user displayName signature",
                peer_user=sender_id,
                room=room,
            )
            return []

        data = message["content"]["body"]
        if not isinstance(data, str):
            log.warning(
                "Received message body not a string",
                peer_user=sender_id,
                peer_address=to_checksum_address(peer_address),
                room=room,
            )
            return []

        messages = deserialize_messages(data=data,
                                        peer_address=peer_address,
                                        rate_limiter=self._rate_limiter)
        if not messages:
            return []

        return messages
Пример #9
0
 def search_user_directory(self, term: str) -> List[User]:
     """
     Search user directory for a given term, returning a list of users
     Args:
         term: term to be searched for
     Returns:
         user_list: list of users returned by server-side search
     """
     response = self.api._send('POST', '/user_directory/search',
                               {'search_term': term})
     try:
         return [
             User(self.api, _user['user_id'], _user['display_name'])
             for _user in response['results']
         ]
     except KeyError:
         return []
Пример #10
0
 def search_user_directory(self, term: str) -> List[User]:
     """
     Search user directory for a given term, returning a list of users
     Args:
         term: term to be searched for
     Returns:
         user_list: list of users returned by server-side search
     """
     response = self.api._send("POST", "/user_directory/search",
                               {"search_term": term})
     try:
         return [
             User(self.api, _user["user_id"], _user["display_name"])
             for _user in response["results"]
         ]
     except KeyError:
         return []
Пример #11
0
 def _validate_userid_signature(user: User) -> Optional[Address]:
     """ Validate a userId format and signature on displayName, and return its address"""
     # display_name should be an address in the self._userid_re format
     match = MatrixTransport._userid_re.match(user.user_id)
     if not match:
         return
     encoded_address: str = match.group(1)
     address: Address = to_canonical_address(encoded_address)
     try:
         recovered = MatrixTransport._recover(
             user.user_id.encode(),
             decode_hex(user.get_display_name()),
         )
         if not address or not recovered or recovered != address:
             return
     except (DecodeError, TypeError):
         return
     return address
Пример #12
0
 def _validate_userid_signature(user: User) -> Optional[Address]:
     """ Validate a userId format and signature on displayName, and return its address"""
     # display_name should be an address in the self._userid_re format
     match = MatrixTransport._userid_re.match(user.user_id)
     if not match:
         return
     encoded_address: str = match.group(1)
     address: Address = to_canonical_address(encoded_address)
     try:
         recovered = MatrixTransport._recover(
             user.user_id.encode(),
             decode_hex(user.get_display_name()),
         )
         if not address or not recovered or recovered != address:
             return
     except (DecodeError, TypeError):
         return
     return address
Пример #13
0
 def mock_get_user(klass, user: Union[User, str]) -> User:
     return User(None, USERID1)
Пример #14
0
    def _validate_userid_signature(user: User) -> Optional[Address]:
        match = USERID_RE.match(user.user_id)
        if not match:
            return None
        return to_canonical_address(match.group(1))


ADDR1 = Address(b"\x11" * 20)
ADDR2 = Address(b'""""""""""""""""""""')
INVALID_USER_ID = "bla:bla"
USER0_ID = "@0x0000000000000000000000000000000000000000:server1"
USER1_S1_ID = "@0x1111111111111111111111111111111111111111:server1"
USER1_S2_ID = "@0x1111111111111111111111111111111111111111:server2"
USER2_S1_ID = "@0x2222222222222222222222222222222222222222:server1"
USER2_S2_ID = "@0x2222222222222222222222222222222222222222:server2"
USER1_S1 = User(api=None, user_id=USER1_S1_ID)
USER1_S2 = User(api=None, user_id=USER1_S2_ID)
USER2_S1 = User(api=None, user_id=USER2_S1_ID)
USER2_S2 = User(api=None, user_id=USER2_S2_ID)


@pytest.fixture
def user_directory_content():
    return []


@pytest.fixture
def dummy_matrix_client(user_directory_content):
    return DummyMatrixClient(USER0_ID, user_directory_content)

Пример #15
0
 def user(self):
     return User(self.cli.api, self.user_id)
Пример #16
0
class TestOlmDevice:
    cli = MatrixClient(HOSTNAME)
    user_id = '@user:matrix.org'
    room_id = '!test:example.com'
    device_id = 'QBUAZIFURK'
    alice = '@alice:example.com'
    alice_device_id = 'JLAFKJWSCS'
    alice_curve_key = 'mmFRSHuJVq3aTudx3KB3w5ZvSFQhgEcy8d+m+vkEfUQ'
    alice_ed_key = '4VjV3OhFUxWFAcO5YOaQVmTIn29JdRmtNh9iAxoyhkc'
    alice_device = Device(cli.api,
                          alice,
                          alice_device_id,
                          database=DummyStore(),
                          curve25519_key=alice_curve_key,
                          ed25519_key=alice_ed_key)
    room = cli._mkroom(room_id)
    room._members[alice] = User(cli.api, alice)

    upload_url = HOSTNAME + MATRIX_V2_API_PATH + '/keys/upload'
    claim_url = HOSTNAME + MATRIX_V2_API_PATH + '/keys/claim'
    to_device_url = HOSTNAME + MATRIX_V2_API_PATH + '/sendToDevice/m.room.encrypted/1'

    @pytest.fixture()
    def device(self):
        device = OlmDevice(self.cli.api, self.user_id, self.device_id)
        # allow to_device api call to work well with responses
        device.api._make_txn_id = lambda: 1
        return device

    @pytest.fixture()
    def signing_key(self, device):
        return device.olm_account.identity_keys['ed25519']

    @pytest.fixture()
    def olm_session_with_alice(self, device):
        session = olm.OutboundSession(device.olm_account, self.alice_curve_key,
                                      self.alice_curve_key)
        device.device_keys[self.alice][
            self.alice_device_id] = self.alice_device
        device.olm_sessions[self.alice_curve_key] = [session]

    @pytest.fixture()
    def alice_olm_device(self, device):
        """Establish an Olm session from Alice to us, and return Alice's Olm device."""
        alice_device = OlmDevice(device.api, self.alice, self.alice_device_id)
        alice_device.device_keys[self.user_id][self.device_id] = device
        device.device_keys[self.alice][self.alice_device_id] = alice_device

        device.olm_account.generate_one_time_keys(1)
        otk = next(
            iter(device.olm_account.one_time_keys['curve25519'].values()))
        device.olm_account.mark_keys_as_published()
        sender_key = device.curve25519
        session = olm.OutboundSession(alice_device.olm_account, sender_key,
                                      otk)
        alice_device.olm_sessions[sender_key] = [session]
        return alice_device

    def test_sign_json(self, device):
        example_payload = {
            "name": "example.org",
            "unsigned": {
                "age_ts": 922834800000
            }
        }
        saved_payload = deepcopy(example_payload)

        signed_payload = device.sign_json(example_payload)
        signature = signed_payload.pop('signatures')
        # We should not have modified the payload besides the signatures key
        assert example_payload == saved_payload
        key_id = 'ed25519:' + device.device_id
        assert signature[self.user_id][key_id]

    def test_verify_json(self, device):
        example_payload = {
            "test": "test",
            "unsigned": {
                "age_ts": 922834800000
            },
            "signatures": {
                "@user:matrix.org": {
                    "ed25519:QBUAZIFURK":
                    ("WI7TgwqTp4YVn1dFWmDu7xrJvEikEzAbmoqyM5JY5t0P"
                     "6fVaiMFAirmwb13GzIyYDLR+nQfoksNBcrp7xSaMCA")
                }
            }
        }
        saved_payload = deepcopy(example_payload)
        signing_key = "WQF5z9b4DV1DANI5HUMJfhTIDvJs1jkoGTLY6AQdjF0"

        assert device.verify_json(example_payload, signing_key, self.user_id,
                                  device.device_id)

        # We should not have modified the payload
        assert example_payload == saved_payload

        # Try to verify an object that has been tampered with
        example_payload['test'] = 'test1'
        assert not device.verify_json(example_payload, signing_key,
                                      self.user_id, device.device_id)

        # Try to verify invalid payloads
        example_payload['signatures'].pop(self.user_id)
        assert not device.verify_json(example_payload, signing_key,
                                      self.user_id, device.device_id)
        example_payload.pop('signatures')
        assert not device.verify_json(example_payload, signing_key,
                                      self.user_id, device.device_id)

    def test_sign_verify(self, device, signing_key):
        example_payload = {
            "name": "example.org",
        }

        signed_payload = device.sign_json(example_payload)
        assert device.verify_json(signed_payload, signing_key, self.user_id,
                                  device.device_id)

    @responses.activate
    def test_upload_identity_keys(self, device, signing_key):
        device.one_time_keys_manager.server_counts = {}
        resp = deepcopy(example_key_upload_response)

        responses.add(responses.POST, self.upload_url, json=resp)

        assert device.upload_identity_keys() is None
        assert device.one_time_keys_manager.server_counts == \
            resp['one_time_key_counts']

        req_device_keys = json.loads(
            responses.calls[0].request.body)['device_keys']
        assert req_device_keys['user_id'] == self.user_id
        assert req_device_keys['device_id'] == self.device_id
        assert req_device_keys['algorithms'] == device._algorithms
        assert 'keys' in req_device_keys
        assert 'signatures' in req_device_keys
        assert device.verify_json(req_device_keys, signing_key, self.user_id,
                                  self.device_id)

    @pytest.mark.parametrize('proportion', [-1, 2])
    def test_upload_identity_keys_invalid(self, proportion):
        with pytest.raises(ValueError):
            OlmDevice(self.cli.api,
                      self.user_id,
                      self.device_id,
                      signed_keys_proportion=proportion)

    @responses.activate
    @pytest.mark.parametrize('proportion', [0, 1, 0.5, 0.33])
    def test_upload_one_time_keys(self, proportion):
        resp = deepcopy(example_key_upload_response)
        counts = resp['one_time_key_counts']
        counts['curve25519'] = counts['signed_curve25519'] = 10
        responses.add(responses.POST, self.upload_url, json=resp)

        device = OlmDevice(self.cli.api,
                           self.user_id,
                           self.device_id,
                           signed_keys_proportion=proportion)
        assert not device.one_time_keys_manager.server_counts

        max_keys = device.olm_account.max_one_time_keys // 2
        signed_keys_to_upload = \
            max(round(max_keys * proportion) - counts['signed_curve25519'], 0)
        unsigned_keys_to_upload = \
            max(round(max_keys * (1 - proportion)) - counts['curve25519'], 0)
        expected_return = {}
        if signed_keys_to_upload:
            expected_return['signed_curve25519'] = signed_keys_to_upload
        if unsigned_keys_to_upload:
            expected_return['curve25519'] = unsigned_keys_to_upload

        assert device.upload_one_time_keys() == expected_return
        assert len(responses.calls) == 2
        assert device.one_time_keys_manager.server_counts == resp[
            'one_time_key_counts']

        req_otk = json.loads(responses.calls[1].request.body)['one_time_keys']
        assert len(req_otk) == unsigned_keys_to_upload + signed_keys_to_upload
        assert len([key for key in req_otk if not key.startswith('signed')]) == \
            unsigned_keys_to_upload
        assert len([key for key in req_otk if key.startswith('signed')]) == \
            signed_keys_to_upload
        for k in req_otk:
            if k == 'signed_curve25519':
                device.verify_json(req_otk[k], device.signing_key,
                                   device.user_id, device.device_id)

    @responses.activate
    def test_upload_one_time_keys_enough(self, device):
        device.one_time_keys_manager.server_counts = {}
        limit = device.olm_account.max_one_time_keys // 2
        resp = {'one_time_key_counts': {'signed_curve25519': limit}}
        responses.add(responses.POST, self.upload_url, json=resp)

        assert not device.upload_one_time_keys()

    @responses.activate
    def test_upload_one_time_keys_force_update(self, device):
        device.one_time_keys_manager.server_counts = {'curve25519': 10}
        resp = deepcopy(example_key_upload_response)
        responses.add(responses.POST, self.upload_url, json=resp)

        device.upload_one_time_keys()
        assert len(responses.calls) == 1

        device.upload_one_time_keys(force_update=True)
        assert len(responses.calls) == 3

    @responses.activate
    @pytest.mark.parametrize('count,should_upload', [(0, True), (25, False),
                                                     (4, True)])
    def test_update_one_time_key_counts(self, device, count, should_upload):
        responses.add(responses.POST,
                      self.upload_url,
                      json={'one_time_key_counts': {}})
        device.one_time_keys_manager.target_counts['signed_curve25519'] = 50

        count_dict = {}
        if count:
            count_dict['signed_curve25519'] = count

        device.update_one_time_key_counts(count_dict)

        if should_upload:
            if count:
                req_otk = json.loads(
                    responses.calls[0].request.body)['one_time_keys']
                assert len(responses.calls) == 1
            else:
                req_otk = json.loads(
                    responses.calls[1].request.body)['one_time_keys']
                assert len(responses.calls) == 2
            assert len(req_otk) == 50 - count
        else:
            assert not len(responses.calls)

    @pytest.mark.parametrize('threshold', [-1, 2])
    def test_invalid_keys_threshold(self, threshold):
        with pytest.raises(ValueError):
            OlmDevice(self.cli.api,
                      self.user_id,
                      self.device_id,
                      keys_threshold=threshold)

    @responses.activate
    def test_olm_start_sessions(self, device):
        responses.add(responses.POST,
                      self.claim_url,
                      json=example_claim_keys_response)

        user_devices = {self.alice: {self.alice_device_id}}

        # We don't have alice's keys
        device.olm_start_sessions(user_devices)
        assert not device.olm_sessions[self.alice_curve_key]

        # Cover logging part
        olm_device.logger.setLevel(logging.WARNING)
        # Now should be good
        device.device_keys[self.alice][
            self.alice_device_id] = self.alice_device
        device.olm_start_sessions(user_devices)
        assert device.olm_sessions[self.alice_curve_key]

        # With failures and wrong signature
        device.olm_sessions.clear()
        payload = deepcopy(example_claim_keys_response)
        payload['failures'] = {'dummy': 1}
        key = payload['one_time_keys'][self.alice][self.alice_device_id]
        key['signed_curve25519:AAAAAQ']['test'] = 1
        responses.replace(responses.POST, self.claim_url, json=payload)

        device.olm_start_sessions(user_devices)
        assert not device.olm_sessions[self.alice_curve_key]

        # Missing requested user and devices
        user_devices[self.alice].add('test')
        user_devices['test'] = 'test'

        device.olm_start_sessions(user_devices)

    @responses.activate
    def test_olm_build_encrypted_event(self, device):
        event_content = {'dummy': 'example'}

        # We don't have Alice's keys
        with pytest.raises(RuntimeError):
            device.olm_build_encrypted_event('m.text', event_content,
                                             self.alice, self.alice_device_id)

        # We don't have a session with Alice
        device.device_keys[self.alice][
            self.alice_device_id] = self.alice_device
        with pytest.raises(RuntimeError):
            device.olm_build_encrypted_event('m.text', event_content,
                                             self.alice, self.alice_device_id)

        responses.add(responses.POST,
                      self.claim_url,
                      json=example_claim_keys_response)
        user_devices = {self.alice: {self.alice_device_id}}
        device.olm_start_sessions(user_devices)
        assert device.olm_build_encrypted_event('m.text', event_content,
                                                self.alice,
                                                self.alice_device_id)

    def test_olm_decrypt(self, device):
        # Since this method doesn't care about high-level event formatting, we will
        # generate things at low level
        our_account = device.olm_account
        # Alice needs to start a session with us
        alice = olm.Account()
        sender_key = alice.identity_keys['curve25519']
        our_account.generate_one_time_keys(1)
        otk = next(iter(our_account.one_time_keys['curve25519'].values()))
        device.olm_account.mark_keys_as_published()
        session = olm.OutboundSession(alice,
                                      our_account.identity_keys['curve25519'],
                                      otk)

        plaintext = {"test": "test"}
        message = session.encrypt(json.dumps(plaintext))
        assert device._olm_decrypt(message, sender_key) == plaintext

        # New pre-key message, but the session exists this time
        message = session.encrypt(json.dumps(plaintext))
        assert device._olm_decrypt(message, sender_key) == plaintext

        # Try to decrypt the same message twice
        with pytest.raises(RuntimeError):
            device._olm_decrypt(message, sender_key)

        # Answer Alice in order to have a type 1 message
        message = device.olm_sessions[sender_key][0].encrypt(
            json.dumps(plaintext))
        session.decrypt(message)
        message = session.encrypt(json.dumps(plaintext))
        assert device._olm_decrypt(message, sender_key) == plaintext

        # Try to decrypt the same message type 1 twice
        with pytest.raises(RuntimeError):
            device._olm_decrypt(message, sender_key)

        # Try to decrypt a message from a session that reused a one-time key
        otk_reused_session = olm.OutboundSession(
            alice, our_account.identity_keys['curve25519'], otk)
        message = otk_reused_session.encrypt(json.dumps(plaintext))
        with pytest.raises(RuntimeError):
            device._olm_decrypt(message, sender_key)

        # Try to decrypt an invalid type 0 message
        our_account.generate_one_time_keys(1)
        otk = next(iter(our_account.one_time_keys['curve25519'].values()))
        wrong_session = olm.OutboundSession(alice, sender_key, otk)
        message = wrong_session.encrypt(json.dumps(plaintext))
        with pytest.raises(RuntimeError):
            device._olm_decrypt(message, sender_key)

        # Try to decrypt a type 1 message for which we have no sessions
        message = session.encrypt(json.dumps(plaintext))
        device.olm_sessions.clear()
        with pytest.raises(RuntimeError):
            device._olm_decrypt(message, sender_key)

    def test_olm_decrypt_event(self, device, alice_olm_device):
        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'example_type', {'content': 'test'}, self.user_id, self.device_id)

        # Now we can test
        device.olm_decrypt_event(encrypted_event, self.alice)

        # Device verification
        alice_olm_device.verified = True
        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'example_type', {'content': 'test'}, self.user_id, self.device_id)
        device.olm_decrypt_event(encrypted_event, self.alice)

        # The signing_key is wrong
        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'example_type', {'content': 'test'}, self.user_id, self.device_id)
        device.device_keys[self.alice][self.alice_device_id]._ed25519 = 'wrong'
        with pytest.raises(RuntimeError):
            device.olm_decrypt_event(encrypted_event, self.alice)

        # We do not have the keys
        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'example_type', {'content': 'test'}, self.user_id, self.device_id)
        device.device_keys[self.alice].clear()
        device.olm_decrypt_event(encrypted_event, self.alice)
        device.device_keys[self.alice][self.alice_device_id] = alice_olm_device
        alice_olm_device.verified = False

        # Type 1 Olm payload
        alice_olm_device.olm_decrypt_event(
            device.olm_build_encrypted_event('example_type',
                                             {'content': 'test'}, self.alice,
                                             self.alice_device_id),
            self.user_id)
        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'example_type', {'content': 'test'}, self.user_id, self.device_id)
        device.olm_decrypt_event(encrypted_event, self.alice)

        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'example_type', {'content': 'test'}, self.user_id, self.device_id)
        with pytest.raises(RuntimeError):
            device.olm_decrypt_event(encrypted_event, 'wrong')

        wrong_event = deepcopy(encrypted_event)
        wrong_event['algorithm'] = 'wrong'
        with pytest.raises(RuntimeError):
            device.olm_decrypt_event(wrong_event, self.alice)

        wrong_event = deepcopy(encrypted_event)
        wrong_event['ciphertext'] = {}
        with pytest.raises(RuntimeError):
            device.olm_decrypt_event(wrong_event, self.alice)

        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'example_type', {'content': 'test'}, self.user_id, self.device_id)
        device.user_id = 'wrong'
        with pytest.raises(RuntimeError):
            device.olm_decrypt_event(encrypted_event, self.alice)
        device.user_id = self.user_id

        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'example_type', {'content': 'test'}, self.user_id, self.device_id)
        device._ed25519 = 'wrong'
        with pytest.raises(RuntimeError):
            device.olm_decrypt_event(encrypted_event, self.alice)

    @responses.activate
    def test_olm_ensure_sessions(self, device):
        responses.add(responses.POST,
                      self.claim_url,
                      json=example_claim_keys_response)
        device.device_keys[self.alice][
            self.alice_device_id] = self.alice_device
        user_devices = {self.alice: [self.alice_device_id]}

        device.olm_ensure_sessions(user_devices)
        assert device.olm_sessions[self.alice_curve_key]
        assert len(responses.calls) == 1

        device.olm_ensure_sessions(user_devices)
        assert len(responses.calls) == 1

    @responses.activate
    def test_megolm_share_session(self, device):
        responses.add(responses.POST,
                      self.claim_url,
                      json=example_claim_keys_response)
        responses.add(responses.PUT, self.to_device_url, json={})
        device.device_keys[self.alice][
            self.alice_device_id] = self.alice_device
        device.device_keys['dummy']['dummy'] = \
            Device(self.cli.api, 'dummy', 'dummy', curve25519_key='a', ed25519_key='a')
        user_devices = {self.alice: [self.alice_device_id], 'dummy': ['dummy']}
        session = MegolmOutboundSession()

        # Sharing with Alice should succeed, but dummy will fail
        device.megolm_share_session(self.room_id, user_devices, session)
        assert session.devices == {self.alice_device_id, 'dummy'}

        req = json.loads(responses.calls[1].request.body)['messages']
        assert self.alice in req
        assert 'dummy' not in req

    @responses.activate
    @pytest.mark.usefixtures('olm_session_with_alice')
    def test_megolm_start_session(self, device):
        responses.add(responses.PUT, self.to_device_url, json={})
        device.device_list.tracked_user_ids.add(self.alice)
        user_devices = {self.alice: [self.alice_device_id]}

        device.megolm_start_session(self.room, user_devices)
        session = device.megolm_outbound_sessions[self.room_id]
        assert self.alice_device_id in session.devices

        # Check that we can decrypt our own messages
        plaintext = {
            'type': 'test',
            'content': {
                'test': 'test'
            },
        }
        encrypted_event = device.megolm_build_encrypted_event(
            self.room, plaintext)
        event = {
            'sender': self.alice,
            'room_id': self.room_id,
            'content': encrypted_event,
            'type': 'm.room.encrypted',
            'origin_server_ts': 1,
            'event_id': 1
        }
        device.megolm_decrypt_event(event)
        assert event['content'] == plaintext['content']

    @responses.activate
    @pytest.mark.usefixtures('olm_session_with_alice')
    def test_megolm_share_session_with_new_devices(self, device):
        responses.add(responses.PUT, self.to_device_url, json={})
        session = MegolmOutboundSession()
        device.megolm_outbound_sessions[self.room_id] = session
        user_devices = {self.alice: [self.alice_device_id]}

        device.megolm_share_session_with_new_devices(self.room, user_devices,
                                                     session)
        assert self.alice_device_id in session.devices
        assert len(responses.calls) == 1

        device.megolm_share_session_with_new_devices(self.room, user_devices,
                                                     session)
        assert len(responses.calls) == 1

    def test_megolm_get_recipients(self, device):
        device.device_keys[self.alice][
            self.alice_device_id] = self.alice_device

        user_devices, _ = device.megolm_get_recipients(self.room)
        assert user_devices == {self.alice: [self.alice_device_id]}

        session = MegolmOutboundSession()
        device.megolm_outbound_sessions[self.room_id] = session

        user_devices, removed = device.megolm_get_recipients(
            self.room, session)
        assert user_devices == {
            self.alice: [self.alice_device_id]
        } and not removed

        self.alice_device.blacklisted = True
        _, removed = device.megolm_get_recipients(self.room, session)
        assert not removed
        session.add_device(self.alice_device_id)
        _, removed = device.megolm_get_recipients(self.room, session)
        assert removed and self.room_id not in device.megolm_outbound_sessions
        self.alice_device.blacklisted = False

        self.room.verify_devices = True
        with pytest.raises(E2EUnknownDevices) as e:
            device.megolm_get_recipients(self.room)
        assert e.value.user_devices == {self.alice: [self.alice_device]}
        self.room.verify_devices = False

    @responses.activate
    @pytest.mark.usefixtures('olm_session_with_alice')
    def test_megolm_build_encrypted_event(self, device):
        responses.add(responses.PUT, self.to_device_url, json={})
        device.device_list.tracked_user_ids.add(self.alice)
        event = {'type': 'm.room.message', 'content': {'body': 'test'}}

        self.room.rotation_period_msgs = 1
        device.megolm_build_encrypted_event(self.room, event)

        device.megolm_build_encrypted_event(self.room, event)

        session = device.megolm_outbound_sessions[self.room_id]
        session.encrypt('test')
        device.megolm_build_encrypted_event(self.room, event)
        assert device.megolm_outbound_sessions[self.room_id].id != session.id

    def test_megolm_remove_outbound_session(self, device):
        session = MegolmOutboundSession()
        device.megolm_outbound_sessions[self.room_id] = session
        device.megolm_remove_outbound_session(self.room_id)
        device.megolm_remove_outbound_session(self.room_id)

    @responses.activate
    @pytest.mark.usefixtures('olm_session_with_alice')
    def test_send_encrypted_message(self, device):
        message_url = HOSTNAME + MATRIX_V2_API_PATH + \
            '/rooms/{}/send/m.room.encrypted/1'.format(quote(self.room.room_id))
        responses.add(responses.PUT, message_url, json={})
        session = MegolmOutboundSession()
        session.add_device(self.alice_device_id)
        device.megolm_outbound_sessions[self.room_id] = session

        device.send_encrypted_message(self.room, {'test': 'test'})

    def test_megolm_add_inbound_session(self, device):
        session = MegolmOutboundSession()

        with pytest.raises(ValueError):
            device.megolm_add_inbound_session(self.room_id,
                                              self.alice_curve_key,
                                              self.alice_ed_key, session.id,
                                              'wrong')
        assert device.megolm_add_inbound_session(self.room_id,
                                                 self.alice_curve_key,
                                                 self.alice_ed_key, session.id,
                                                 session.session_key)
        assert session.id in \
            device.megolm_inbound_sessions[self.room_id][self.alice_curve_key]
        assert not device.megolm_add_inbound_session(
            self.room_id, self.alice_curve_key, self.alice_ed_key, session.id,
            session.session_key)
        with pytest.raises(ValueError):
            device.megolm_add_inbound_session(self.room_id,
                                              self.alice_curve_key,
                                              self.alice_ed_key, 'wrong',
                                              session.session_key)

    def test_handle_room_key_event(self, device):
        device.handle_room_key_event(example_room_key_event,
                                     self.alice_curve_key)
        assert self.room_id in device.megolm_inbound_sessions

        device.handle_room_key_event(example_room_key_event,
                                     self.alice_curve_key)

        event = deepcopy(example_room_key_event)
        event['content']['algorithm'] = 'wrong'
        device.handle_room_key_event(event, self.alice_curve_key)

        event = deepcopy(example_room_key_event)
        event['content']['session_id'] = 'wrong'
        device.handle_room_key_event(event, self.alice_curve_key)

    def test_olm_handle_encrypted_event(self, device, alice_olm_device):
        content = example_room_key_event['content']
        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'm.room_key', content, self.user_id, self.device_id)
        event = {
            'type': 'm.room.encrypted',
            'content': encrypted_event,
            'sender': self.alice
        }

        device.olm_handle_encrypted_event(event)

        # Decrypting the same event twice will trigger an error
        device.olm_handle_encrypted_event(event)

        # Forwarded key event
        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'm.forwarded_room_key', content, self.user_id, self.device_id)
        event = {
            'type': 'm.room.encrypted',
            'content': encrypted_event,
            'sender': self.alice
        }
        device.olm_handle_encrypted_event(event)

        # Unhandled event
        encrypted_event = alice_olm_device.olm_build_encrypted_event(
            'm.other', content, self.user_id, self.device_id)
        event = {
            'type': 'm.room.encrypted',
            'content': encrypted_event,
            'sender': self.alice
        }
        device.olm_handle_encrypted_event(event)

        # Simulate redacted event
        event['content'].pop('algorithm')
        device.olm_handle_encrypted_event(event)

    def test_megolm_decrypt_event(self, device):
        out_session = MegolmOutboundSession()

        plaintext = {
            'content': {
                "test": "test"
            },
            'type': 'm.text',
        }
        ciphertext = out_session.encrypt(json.dumps(plaintext))

        content = {
            'ciphertext': ciphertext,
            'session_id': out_session.id,
            'sender_key': self.alice_curve_key,
            'algorithm': 'm.megolm.v1.aes-sha2',
            'device_id': self.alice_device_id,
        }

        event = {
            'sender': self.alice,
            'room_id': self.room_id,
            'content': content,
            'type': 'm.room.encrypted',
            'origin_server_ts': 1,
            'event_id': 1
        }

        with pytest.raises(UnableToDecryptError):
            device.megolm_decrypt_event(event)

        session_key = out_session.session_key
        in_session = MegolmInboundSession(session_key, self.alice_ed_key)
        sessions = device.megolm_inbound_sessions[self.room_id]
        sessions[self.alice_curve_key][in_session.id] = in_session

        # Unknown message index
        with pytest.raises(RuntimeError):
            device.megolm_decrypt_event(event)

        ciphertext = out_session.encrypt(json.dumps(plaintext))
        event['content']['ciphertext'] = ciphertext
        device.megolm_decrypt_event(event)
        assert event['content'] == plaintext['content']

        # No replay attack
        event['content'] = content
        device.megolm_decrypt_event(event)
        assert event['content'] == plaintext['content']

        # Replay attack
        event['content'] = content
        event['event_id'] = 2
        with pytest.raises(RuntimeError):
            device.megolm_decrypt_event(event)
        event['event_id'] = 1

        # Device verification
        device.device_keys[self.alice][
            self.alice_device_id] = self.alice_device
        event['content'] = content
        # Unverified
        device.megolm_decrypt_event(event)
        assert event['content'] == plaintext['content']
        assert isinstance(event, dict)

        event['content'] = content
        # Verified
        self.alice_device.verified = True
        decrypted_event = device.megolm_decrypt_event(event)
        assert decrypted_event['content'] == plaintext['content']
        assert isinstance(decrypted_event, VerifiedEvent)

        in_session = MegolmInboundSession(session_key, self.alice_curve_key)
        sessions = device.megolm_inbound_sessions[self.room_id]
        sessions[self.alice_curve_key][in_session.id] = in_session
        # Wrong signing key
        with pytest.raises(RuntimeError):
            device.megolm_decrypt_event(event)
        self.alice_device.verified = False

        event['content']['algorithm'] = 'wrong'
        with pytest.raises(RuntimeError):
            device.megolm_decrypt_event(event)

        event['content'].pop('algorithm')
        event['type'] = 'encrypted'
        device.megolm_decrypt_event(event)
        assert event['type'] == 'encrypted'
Пример #17
0
class TestCryptoStore(object):

    # Initialise a store and test some init code
    device_id = 'AUIETSRN'
    user_id = '@user:matrix.org'
    room_id = '!test:example.com'
    room = Room(None, room_id)
    user = User(None, user_id, '')
    room._members[user_id] = user
    db_name = 'test.db'
    db_path = mkdtemp()
    store_conf = {
        'db_name': db_name,
        'db_path': db_path
    }
    store = CryptoStore(
        user_id, device_id=device_id, db_path=db_path, db_name=db_name)
    db_filepath = os.path.join(db_path, db_name)
    assert os.path.exists(db_filepath)
    store.close()
    store = CryptoStore(
        user_id, device_id=device_id, db_path=db_path, db_name=db_name)

    @pytest.fixture(autouse=True, scope='class')
    def cleanup(self):
        yield
        os.remove(self.db_filepath)

    @pytest.fixture()
    def account(self):
        account = self.store.get_olm_account()
        if account is None:
            account = olm.Account()
            self.store.save_olm_account(account)
        return account

    @pytest.fixture()
    def curve_key(self, account):
        return account.identity_keys['curve25519']

    @pytest.fixture()
    def ed_key(self, account):
        return account.identity_keys['ed25519']

    @pytest.fixture()
    def device(self):
        return OlmDevice(None, self.user_id, self.device_id, store_conf=self.store_conf)

    def test_olm_account_persistence(self):
        account = olm.Account()
        identity_keys = account.identity_keys
        self.store.remove_olm_account()

        # Try to load inexisting account
        saved_account = self.store.get_olm_account()
        assert saved_account is None

        # Try to load inexisting account without device_id
        self.store.device_id = None
        with pytest.raises(ValueError):
            self.store.get_olm_account()
        self.store.device_id = self.device_id

        # Save and load
        self.store.save_olm_account(account)
        saved_account = self.store.get_olm_account()
        assert saved_account.identity_keys == identity_keys

        # Save and load without device_id
        self.store.save_olm_account(account)
        self.store.device_id = None
        saved_account = self.store.get_olm_account()
        assert saved_account.identity_keys == identity_keys
        assert self.store.device_id == self.device_id

        # Replace the account, causing foreign keys to be deleted
        self.store.save_sync_token('test')
        self.store.replace_olm_account(account)
        assert self.store.get_sync_token() is None

        # Load the account from an OlmDevice
        device = OlmDevice(None, self.user_id, self.device_id, store_conf=self.store_conf)
        assert device.olm_account.identity_keys == account.identity_keys

        # Load the account from an OlmDevice, without device_id
        device = OlmDevice(None, self.user_id, store_conf=self.store_conf)
        assert device.device_id == self.device_id

    def test_olm_sessions_persistence(self, account, curve_key, device):
        session = olm.OutboundSession(account, curve_key, curve_key)
        sessions = defaultdict(list)

        self.store.load_olm_sessions(sessions)
        assert not sessions
        assert not self.store.get_olm_sessions(curve_key)

        self.store.save_olm_session(curve_key, session)
        self.store.load_olm_sessions(sessions)
        assert sessions[curve_key][0].id == session.id

        saved_sessions = self.store.get_olm_sessions(curve_key)
        assert saved_sessions[0].id == session.id

        sessions.clear()
        saved_sessions = self.store.get_olm_sessions(curve_key, sessions)
        assert sessions[curve_key][0].id == session.id

        # Replace the session when its internal state has changed
        pickle = session.pickle()
        session.encrypt('test')
        self.store.save_olm_session(curve_key, session)
        saved_sessions = self.store.get_olm_sessions(curve_key)
        assert saved_sessions[0].pickle != pickle

        # Load sessions dynamically
        assert not device.olm_sessions
        with pytest.raises(AttributeError):
            device._olm_decrypt(None, curve_key)
        assert device.olm_sessions[curve_key][0].id == session.id

        device.olm_sessions.clear()
        device.device_keys[self.user_id][self.device_id] = device
        device.olm_ensure_sessions({self.user_id: [self.device_id]})
        assert device.olm_sessions[curve_key][0].id == session.id

        # Test cascade deletion
        self.store.remove_olm_account()
        assert not self.store.get_olm_sessions(curve_key)

    def test_megolm_inbound_persistence(self, curve_key, ed_key, device):
        out_session = olm.OutboundGroupSession()
        session = MegolmInboundSession(out_session.session_key, ed_key)
        session.forwarding_chain.append(curve_key)
        sessions = defaultdict(lambda: defaultdict(dict))

        self.store.load_inbound_sessions(sessions)
        assert not sessions
        assert not self.store.get_inbound_session(self.room_id, curve_key, session.id)

        self.store.save_inbound_session(self.room_id, curve_key, session)
        self.store.load_inbound_sessions(sessions)
        assert sessions[self.room_id][curve_key][session.id].id == session.id

        saved_session = self.store.get_inbound_session(self.room_id, curve_key,
                                                       session.id)
        assert saved_session.id == session.id
        assert saved_session.forwarding_chain == [curve_key]

        sessions = {}
        saved_session = self.store.get_inbound_session(self.room_id, curve_key,
                                                       session.id, sessions)
        assert sessions[session.id].id == session.id

        assert not device.megolm_inbound_sessions
        created = device.megolm_add_inbound_session(
            self.room_id, curve_key, ed_key, session.id, out_session.session_key)
        assert not created
        assert device.megolm_inbound_sessions[self.room_id][curve_key][session.id].id == \
            session.id

        device.megolm_inbound_sessions.clear()
        content = {
            'sender_key': curve_key,
            'session_id': session.id,
            'algorithm': device._megolm_algorithm,
            'device_id': ''
        }
        event = {
            'sender': '',
            'room_id': self.room_id,
            'content': content
        }
        with pytest.raises(KeyError):
            device.megolm_decrypt_event(event)
        assert device.megolm_inbound_sessions[self.room_id][curve_key][session.id].id == \
            session.id

        self.store.remove_olm_account()
        assert not self.store.get_inbound_session(self.room_id, curve_key, session.id)

    @pytest.mark.usefixtures('account')
    def test_megolm_outbound_persistence(self, device):
        session = MegolmOutboundSession(max_messages=2, max_age=100000)
        session.message_count = 1
        session.add_device(self.device_id)
        sessions = {}

        self.store.load_outbound_sessions(sessions)
        assert not sessions
        assert not self.store.get_outbound_session(self.room_id)

        self.store.save_outbound_session(self.room_id, session)
        self.store.save_megolm_outbound_devices(self.room_id, {self.device_id})
        self.store.load_outbound_sessions(sessions)
        assert sessions[self.room_id].id == session.id
        assert sessions[self.room_id].devices == session.devices
        assert sessions[self.room_id].creation_time == session.creation_time
        assert sessions[self.room_id].max_messages == session.max_messages
        assert sessions[self.room_id].message_count == session.message_count
        assert sessions[self.room_id].max_age == session.max_age

        saved_session = self.store.get_outbound_session(self.room_id)
        assert saved_session.id == session.id
        assert saved_session.devices == session.devices
        assert saved_session.creation_time == session.creation_time
        assert saved_session.max_messages == session.max_messages
        assert saved_session.message_count == session.message_count
        assert saved_session.max_age == session.max_age

        sessions.clear()
        saved_session = self.store.get_outbound_session(self.room_id, sessions)
        assert sessions[self.room_id].id == session.id

        self.store.remove_outbound_session(self.room_id)
        assert not self.store.get_outbound_session(self.room_id)

        self.store.save_outbound_session(self.room_id, session)
        saved_session = self.store.get_outbound_session(self.room_id)
        # Verify the saved devices have been erased with the session
        assert not saved_session.devices

        room = Room(None, self.room_id)
        with pytest.raises(AttributeError):
            device.megolm_build_encrypted_event(room, {})
        assert device.megolm_outbound_sessions[self.room_id].id == session.id

        self.store.remove_olm_account()
        assert not self.store.get_outbound_session(self.room_id)

    @pytest.mark.usefixtures('account')
    def test_device_keys_persistence(self, device):
        user_devices = {self.user_id: [self.device_id]}
        device_keys = defaultdict(dict)
        device._verified = True

        self.store.load_device_keys(None, device_keys)
        assert not device_keys
        assert not self.store.get_device_keys(None, user_devices, device_keys)
        assert not device_keys

        device_keys_to_save = {self.user_id: {self.device_id: device}}
        self.store.save_device_keys(device_keys_to_save)
        self.store.load_device_keys(None, device_keys)
        assert device_keys[self.user_id][self.device_id].curve25519 == \
            device.curve25519
        assert device_keys[self.user_id][self.device_id].verified

        device_keys.clear()
        devices = self.store.get_device_keys(None, user_devices)[self.user_id]
        assert devices[self.device_id].curve25519 == device.curve25519
        assert self.store.get_device_keys(None, user_devices, device_keys)
        assert device_keys[self.user_id][self.device_id].curve25519 == \
            device.curve25519
        assert device_keys[self.user_id][self.device_id].verified

        # Test device verification persistence
        device.verified = False
        device.ignored = True
        devices = self.store.get_device_keys(None, user_devices)[self.user_id]
        assert not devices[self.device_id].verified
        assert devices[self.device_id].ignored

        # Test [] wildcard
        devices = self.store.get_device_keys(None, {self.user_id: []})[self.user_id]
        assert devices[self.device_id].curve25519 == device.curve25519

        device.device_list.tracked_user_ids = {self.user_id}
        device.device_list.get_room_device_keys(self.room)
        assert device_keys[self.user_id][self.device_id].curve25519 == \
            device.curve25519

        # Test multiples []
        device_keys.clear()
        user_id = 'test'
        device_id = 'test'
        device_keys_to_save[user_id] = {device_id: device}
        self.store.save_device_keys(device_keys_to_save)
        user_devices[user_id] = []
        user_devices[self.user_id] = []
        device_keys = self.store.get_device_keys(None, user_devices)
        assert device_keys[self.user_id][self.device_id].curve25519 == device.curve25519
        assert device_keys[user_id][device_id].curve25519 == device.curve25519

        # Try to verify a device that has no keys
        device._ed25519 = None
        with pytest.raises(ValueError):
            device.verified = False

        self.store.remove_olm_account()
        assert not self.store.get_device_keys(None, user_devices)

    @pytest.mark.usefixtures('account')
    def test_tracked_users_persistence(self):
        tracked_user_ids = set()
        tracked_user_ids_to_save = {self.user_id}

        self.store.load_tracked_users(tracked_user_ids)
        assert not tracked_user_ids

        self.store.save_tracked_users(tracked_user_ids_to_save)
        self.store.load_tracked_users(tracked_user_ids)
        assert tracked_user_ids == tracked_user_ids_to_save

        self.store.remove_tracked_users({self.user_id})
        tracked_user_ids.clear()
        self.store.load_tracked_users(tracked_user_ids)
        assert not tracked_user_ids

    @pytest.mark.usefixtures('account')
    def test_sync_token_persistence(self):
        sync_token = 'test'

        assert not self.store.get_sync_token()

        self.store.save_sync_token(sync_token)
        assert self.store.get_sync_token() == sync_token

        sync_token = 'new'
        self.store.save_sync_token(sync_token)
        assert self.store.get_sync_token() == sync_token

    @pytest.mark.usefixtures('account')
    def test_key_requests(self):
        session_id = 'test'
        session_ids = set()

        self.store.load_outgoing_key_requests(session_ids)
        assert not session_ids

        self.store.add_outgoing_key_request(session_id)
        self.store.load_outgoing_key_requests(session_ids)
        assert session_id in session_ids

        session_ids.clear()
        self.store.remove_outgoing_key_request(session_id)
        self.store.load_outgoing_key_requests(session_ids)
        assert not session_ids

    def test_load_all(self, account, curve_key, ed_key, device):
        curve_key = account.identity_keys['curve25519']
        session = olm.OutboundSession(account, curve_key, curve_key)
        out_session = MegolmOutboundSession()
        out_session.add_device(self.device_id)
        in_session = MegolmInboundSession(out_session.session_key, ed_key)
        device_keys_to_save = {self.user_id: {self.device_id: device}}

        self.store.save_inbound_session(self.room_id, curve_key, in_session)
        self.store.save_olm_session(curve_key, session)
        self.store.save_outbound_session(self.room_id, out_session)
        self.store.save_megolm_outbound_devices(self.room_id, {self.device_id})
        self.store.save_device_keys(device_keys_to_save)

        device = OlmDevice(
            None, self.user_id, self.device_id, store_conf=self.store_conf, load_all=True)

        assert session.id in {s.id for s in device.olm_sessions[curve_key]}
        saved_in_session = \
            device.megolm_inbound_sessions[self.room_id][curve_key][in_session.id]
        assert saved_in_session.id == in_session.id
        saved_out_session = device.megolm_outbound_sessions[self.room_id]
        assert saved_out_session.id == out_session.id
        assert saved_out_session.devices == out_session.devices
        assert device.device_keys[self.user_id][self.device_id].curve25519 == \
            device.curve25519
Пример #18
0
try:
    client.login(username, password)
except MatrixRequestError as e:
    print(e)
    if e.code == 403:
        print("Bad username or password.")
        sys.exit(4)
    else:
        print("Check your server details are correct.")
        sys.exit(2)
except MissingSchema as e:
    print("Bad URL format.")
    print(e)
    sys.exit(3)

user = User(client.api, client.user_id)

if len(sys.argv) < 5:
    print("Current Display Name: %s" % user.get_display_name())

    displayname = input("New Display Name: ")
else:
    displayname = sys.argv[4]

try:
    user.set_display_name(displayname)
except MatrixRequestError as e:
    print(e)
    sys.exit(11)