Ejemplo n.º 1
0
    def render_POST(self, request):
        """
        Bulk-lookup for threepids.
        ** DEPRECATED **
        Use /bulk_lookup which returns the result encapsulated in a dict
        Params: 'threepids': list of threepids, each of which is a list of medium, address
        Returns: List of results where each result is a 3 item list of medium, address, mxid
        Threepids for which no mapping is found are omitted.
        """
        send_cors(request)

        authIfV2(self.sydent, request)

        args = get_args(request, ('threepids',))

        threepids = args['threepids']
        if not isinstance(threepids, list):
            request.setResponseCode(400)
            return {'errcode': 'M_INVALID_PARAM', 'error': 'threepids must be a list'}, None

        logger.info("Bulk lookup of %d threepids (deprecated endpoint)", len(threepids))
            
        globalAssocStore = GlobalAssociationStore(self.sydent)
        results = globalAssocStore.getMxids(threepids)

        return json.dumps(results)
Ejemplo n.º 2
0
    def render_POST(self, request):
        """
        Bulk-lookup for threepids.
        Params: 'threepids': list of threepids, each of which is a list of medium, address
        Returns: Object with key 'threepids', which is a list of results where each result
                 is a 3 item list of medium, address, mxid
                 Note that results are not streamed to the client.
        Threepids for which no mapping is found are omitted.
        """
        send_cors(request)

        authIfV2(self.sydent, request)

        args = get_args(request, ('threepids', ))

        threepids = args['threepids']
        if not isinstance(threepids, list):
            raise MatrixRestError(400, 'M_INVALID_PARAM',
                                  'threepids must be a list')

        logger.info("Bulk lookup of %d threepids", len(threepids))

        globalAssocStore = GlobalAssociationStore(self.sydent)
        results = globalAssocStore.getMxids(threepids)

        return {'threepids': results}
Ejemplo n.º 3
0
    def render_POST(self, request):
        """
        Bulk-lookup for threepids.
        ** DEPRECATED **
        Use /bulk_lookup which returns the result encapsulated in a dict
        Params: 'threepids': list of threepids, each of which is a list of medium, address
        Returns: List of results where each result is a 3 item list of medium, address, mxid
        Threepids for which no mapping is found are omitted.
        """
        send_cors(request)
        err, args = get_args(request, ('threepids',))
        if err:
            return json.dumps(err)

        threepids = args['threepids']
        if not isinstance(threepids, list):
            request.setResponseCode(400)
            return {'errcode': 'M_INVALID_PARAM', 'error': 'threepids must be a list'}, None

        logger.info("Bulk lookup of %d threepids (deprecated endpoint)", len(threepids))
            
        globalAssocStore = GlobalAssociationStore(self.sydent)
        results = globalAssocStore.getMxids(threepids)

        return json.dumps(results)
Ejemplo n.º 4
0
    def render_POST(self, request):
        """
        Bulk-lookup for threepids.
        Params: 'threepids': list of threepids, each of which is a list of medium, address
        Returns: Object with key 'threepids', which is a list of results where each result
                 is a 3 item list of medium, address, mxid
                 Note that results are not streamed to the client.
        Threepids for which no mapping is found are omitted.
        """
        send_cors(request)
        err, args = get_args(request, ('threepids',))
        if err:
            return json.dumps(err)

        threepids = args['threepids']
        if not isinstance(threepids, list):
            request.setResponseCode(400)
            return {'errcode': 'M_INVALID_PARAM', 'error': 'threepids must be a list'}, None

        logger.info("Bulk lookup of %d threepids", len(threepids))

        globalAssocStore = GlobalAssociationStore(self.sydent)
        results = globalAssocStore.getMxids(threepids)

        return json.dumps({ 'threepids': results })
Ejemplo n.º 5
0
    def __init__(self, sydent):
        super(LocalPeer, self).__init__(sydent.server_name, {})
        self.sydent = sydent

        globalAssocStore = GlobalAssociationStore(self.sydent)
        self.lastId = globalAssocStore.lastIdFromServer(self.servername)
        if self.lastId is None:
            self.lastId = -1
Ejemplo n.º 6
0
    def __init__(self, sydent):
        super(LocalPeer, self).__init__(sydent.server_name, {})
        self.sydent = sydent

        globalAssocStore = GlobalAssociationStore(self.sydent)
        self.lastId = globalAssocStore.lastIdFromServer(self.servername)
        if self.lastId is None:
            self.lastId = -1
Ejemplo n.º 7
0
    def __init__(self, sydent: "Sydent") -> None:
        super().__init__(sydent.config.general.server_name, {})
        self.sydent = sydent
        self.hashing_store = HashingMetadataStore(sydent)

        globalAssocStore = GlobalAssociationStore(self.sydent)
        lastId = globalAssocStore.lastIdFromServer(self.servername)
        self.lastId = lastId if lastId is not None else -1
Ejemplo n.º 8
0
    def pushUpdates(self, sgAssocs):
        globalAssocStore = GlobalAssociationStore(self.sydent)
        for localId in sgAssocs:
            if localId > self.lastId:
                assocObj = threePidAssocFromDict(sgAssocs[localId])

                # We can probably skip verification for the local peer (although it could be good as a sanity check)
                globalAssocStore.addAssociation(assocObj, json.dumps(sgAssocs[localId]),
                                                self.sydent.server_name, localId)

        d = twisted.internet.defer.succeed(True)
        return d
Ejemplo n.º 9
0
    def render_GET(self, request: Request) -> JsonDict:
        """
        Look up an individual threepid.

        ** DEPRECATED **

        Params: 'medium': the medium of the threepid
                'address': the address of the threepid
        Returns: A signed association if the threepid has a corresponding mxid, otherwise the empty object.
        """
        send_cors(request)

        args = get_args(request, ("medium", "address"))

        medium = args["medium"]
        address = args["address"]

        globalAssocStore = GlobalAssociationStore(self.sydent)

        sgassoc_raw = globalAssocStore.signedAssociationStringForThreepid(
            medium, address)

        if not sgassoc_raw:
            return {}

        # TODO validate this really is a dict
        sgassoc: JsonDict = json_decoder.decode(sgassoc_raw)
        if self.sydent.config.general.server_name not in sgassoc["signatures"]:
            # We have not yet worked out what the proper trust model should be.
            #
            # Maybe clients implicitly trust a server they talk to (and so we
            # should sign every assoc we return as ourselves, so they can
            # verify this).
            #
            # Maybe clients really want to know what server did the original
            # verification, and want to only know exactly who signed the assoc.
            #
            # Until we work out what we should do, sign all assocs we return as
            # ourself. This is vaguely ok because there actually is only one
            # identity server, but it happens to have two names (matrix.org and
            # vector.im), and so we're not really lying too much.
            #
            # We do this when we return assocs, not when we receive them over
            # replication, so that we can undo this decision in the future if
            # we wish, without having destroyed the raw underlying data.
            sgassoc = signedjson.sign.sign_json(
                sgassoc,
                self.sydent.config.general.server_name,
                self.sydent.keyring.ed25519,
            )
        return sgassoc
Ejemplo n.º 10
0
    def pushUpdates(self, sgAssocs):
        globalAssocStore = GlobalAssociationStore(self.sydent)
        for localId in sgAssocs:
            if localId > self.lastId:
                assocObj = threePidAssocFromDict(sgAssocs[localId])

                # We can probably skip verification for the local peer (although it could be good as a sanity check)
                globalAssocStore.addAssociation(assocObj,
                                                json.dumps(sgAssocs[localId]),
                                                self.sydent.server_name,
                                                localId)

        d = twisted.internet.defer.succeed(True)
        return d
Ejemplo n.º 11
0
    def pushUpdates(self, sgAssocs):
        globalAssocStore = GlobalAssociationStore(self.sydent)
        for localId in sgAssocs:
            if localId > self.lastId:
                assocObj = threePidAssocFromDict(sgAssocs[localId])
                if assocObj.mxid is not None:
                    # We can probably skip verification for the local peer (although it could be good as a sanity check)
                    globalAssocStore.addAssociation(assocObj, json.dumps(sgAssocs[localId]),
                                                    self.sydent.server_name, localId)
                else:
                    globalAssocStore.removeAssociation(assocObj.medium, assocObj.address)

        d = defer.succeed(True)
        return d
Ejemplo n.º 12
0
    def render_GET(self, request):
        """
        Look up an individual threepid.
        Params: 'medium': the medium of the threepid
                'address': the address of the threepid
        Returns: A signed association if the threepid has a corresponding mxid, otherwise the empty object.
        """
        send_cors(request)
        err, args = get_args(request, ('medium', 'address'))
        if err:
            return json.dumps(err)

        medium = args['medium']
        address = args['address']

        globalAssocStore = GlobalAssociationStore(self.sydent)

        sgassoc = globalAssocStore.signedAssociationStringForThreepid(medium, address)

        if not sgassoc:
            return json.dumps({})

        sgassoc = json.loads(sgassoc.encode('utf8'))
        if not self.sydent.server_name in sgassoc['signatures']:
            # We have not yet worked out what the proper trust model should be.
            #
            # Maybe clients implicitly trust a server they talk to (and so we
            # should sign every assoc we return as ourselves, so they can
            # verify this).
            #
            # Maybe clients really want to know what server did the original
            # verification, and want to only know exactly who signed the assoc.
            #
            # Until we work out what we should do, sign all assocs we return as
            # ourself. This is vaguely ok because there actually is only one
            # identity server, but it happens to have two names (matrix.org and
            # vector.im), and so we're not really lying too much.
            #
            # We do this when we return assocs, not when we receive them over
            # replication, so that we can undo this decision in the future if
            # we wish, without having destroyed the raw underlying data.
            sgassoc = signedjson.sign.sign_json(
                sgassoc,
                self.sydent.server_name,
                self.sydent.keyring.ed25519
            )
        return json.dumps(sgassoc)
Ejemplo n.º 13
0
    def render_GET(self, request):
        """
        Look up an individual threepid.
        Params: 'medium': the medium of the threepid
                'address': the address of the threepid
        Returns: A signed association if the threepid has a corresponding mxid, otherwise the empty object.
        """
        send_cors(request)
        err, args = get_args(request, ('medium', 'address'))
        if err:
            return json.dumps(err)

        medium = args['medium']
        address = args['address']

        globalAssocStore = GlobalAssociationStore(self.sydent)

        sgassoc = globalAssocStore.signedAssociationStringForThreepid(
            medium, address)

        if not sgassoc:
            return json.dumps({})

        sgassoc = json.loads(sgassoc.encode('utf8'))
        if not self.sydent.server_name in sgassoc['signatures']:
            # We have not yet worked out what the proper trust model should be.
            #
            # Maybe clients implicitly trust a server they talk to (and so we
            # should sign every assoc we return as ourselves, so they can
            # verify this).
            #
            # Maybe clients really want to know what server did the original
            # verification, and want to only know exactly who signed the assoc.
            #
            # Until we work out what we should do, sign all assocs we return as
            # ourself. This is vaguely ok because there actually is only one
            # identity server, but it happens to have two names (matrix.org and
            # vector.im), and so we're not really lying too much.
            #
            # We do this when we return assocs, not when we receive them over
            # replication, so that we can undo this decision in the future if
            # we wish, without having destroyed the raw underlying data.
            sgassoc = signedjson.sign.sign_json(sgassoc,
                                                self.sydent.server_name,
                                                self.sydent.keyring.ed25519)
        return json.dumps(sgassoc)
Ejemplo n.º 14
0
    def render_GET(self, request):
        send_cors(request)
        err = require_args(request, ('medium', 'address'))
        if err:
            return err

        medium = request.args['medium'][0]
        address = request.args['address'][0]

        globalAssocStore = GlobalAssociationStore(self.sydent)

        sgassoc = globalAssocStore.signedAssociationStringForThreepid(medium, address)

        if not sgassoc:
            return json.dumps({})

        return sgassoc.encode('utf8')
Ejemplo n.º 15
0
    def pushUpdates(self, sgAssocs):
        globalAssocStore = GlobalAssociationStore(self.sydent)
        for localId in sgAssocs:
            if localId > self.lastId:
                assocObj = threePidAssocFromDict(sgAssocs[localId])

                if assocObj.mxid is not None:
                    # Assign a lookup_hash to this association
                    str_to_hash = ' '.join([
                        assocObj.address, assocObj.medium,
                        self.hashing_store.get_lookup_pepper()
                    ], )
                    assocObj.lookup_hash = sha256_and_url_safe_base64(
                        str_to_hash)

                    # We can probably skip verification for the local peer (although it could
                    # be good as a sanity check)
                    globalAssocStore.addAssociation(
                        assocObj, json.dumps(sgAssocs[localId]),
                        self.sydent.server_name, localId)
                else:
                    globalAssocStore.removeAssociation(assocObj.medium,
                                                       assocObj.address)

        d = defer.succeed(True)
        return d
Ejemplo n.º 16
0
    def pushUpdates(self, sgAssocs):
        globalAssocStore = GlobalAssociationStore(self.sydent)
        for localId in sgAssocs:
            if localId > self.lastId:
                assocObj = threePidAssocFromDict(sgAssocs[localId])
                if assocObj.mxid is not None:
                    # We can probably skip verification for the local peer (although it could be good as a sanity check)
                    globalAssocStore.addAssociation(
                        assocObj, json.dumps(sgAssocs[localId]),
                        self.sydent.server_name, localId)
                else:
                    globalAssocStore.removeAssociation(assocObj.medium,
                                                       assocObj.address)

        d = defer.succeed(True)
        return d
Ejemplo n.º 17
0
    def pushUpdates(self, sgAssocs: SignedAssociations) -> "Deferred[bool]":
        """
        Saves the given associations in the global associations store. Only stores an
        association if its ID is greater than the last seen ID.

        :param sgAssocs: The associations to save.

        :return: A deferred that succeeds with the value `True`.
        """
        globalAssocStore = GlobalAssociationStore(self.sydent)
        for localId in sgAssocs:
            if localId > self.lastId:
                assocObj = threePidAssocFromDict(sgAssocs[localId])

                # ensure we are casefolding email addresses
                assocObj.address = normalise_address(assocObj.address,
                                                     assocObj.medium)

                if assocObj.mxid is not None:
                    # Assign a lookup_hash to this association
                    pepper = self.hashing_store.get_lookup_pepper()
                    if not pepper:
                        raise RuntimeError("No lookup_pepper in the database.")
                    str_to_hash = " ".join([
                        assocObj.address,
                        assocObj.medium,
                        pepper,
                    ], )
                    assocObj.lookup_hash = sha256_and_url_safe_base64(
                        str_to_hash)

                    # We can probably skip verification for the local peer (although it could
                    # be good as a sanity check)
                    globalAssocStore.addAssociation(
                        assocObj,
                        json.dumps(sgAssocs[localId]),
                        self.sydent.config.general.server_name,
                        localId,
                    )
                else:
                    globalAssocStore.removeAssociation(assocObj.medium,
                                                       assocObj.address)

        d = defer.succeed(True)
        return d
Ejemplo n.º 18
0
    def pushUpdates(self, sgAssocs):
        """
        Saves the given associations in the global associations store. Only stores an
        association if its ID is greater than the last seen ID.

        :param sgAssocs: The associations to save.
        :type sgAssocs: dict[int, dict[str, any]]

        :return: True
        :rtype: twisted.internet.defer.Deferred[bool]
        """
        globalAssocStore = GlobalAssociationStore(self.sydent)
        for localId in sgAssocs:
            if localId > self.lastId:
                assocObj = threePidAssocFromDict(sgAssocs[localId])

                if assocObj.mxid is not None:
                    # Assign a lookup_hash to this association
                    str_to_hash = u' '.join([
                        assocObj.address, assocObj.medium,
                        self.hashing_store.get_lookup_pepper()
                    ], )
                    assocObj.lookup_hash = sha256_and_url_safe_base64(
                        str_to_hash)

                    # We can probably skip verification for the local peer (although it could
                    # be good as a sanity check)
                    globalAssocStore.addAssociation(
                        assocObj, json.dumps(sgAssocs[localId]),
                        self.sydent.server_name, localId)
                else:
                    globalAssocStore.removeAssociation(assocObj.medium,
                                                       assocObj.address)

        d = defer.succeed(True)
        return d
Ejemplo n.º 19
0
    def render_POST(self, request):
        peerCert = request.transport.getPeerCertificate()
        peerCertCn = peerCert.get_subject().commonName

        peerStore = PeerStore(self.sydent)

        peer = peerStore.getPeerByName(peerCertCn)

        if not peer:
            logger.warn("Got connection from %s but no peer found by that name", peerCertCn)
            request.setResponseCode(403)
            return {'errcode': 'M_UNKNOWN_PEER', 'error': 'This peer is not known to this server'}

        logger.info("Push connection made from peer %s", peer.servername)

        if not request.requestHeaders.hasHeader('Content-Type') or \
                request.requestHeaders.getRawHeaders('Content-Type')[0] != 'application/json':
            logger.warn("Peer %s made push connection with non-JSON content (type: %s)",
                        peer.servername, request.requestHeaders.getRawHeaders('Content-Type')[0])
            return {'errcode': 'M_NOT_JSON', 'error': 'This endpoint expects JSON'}

        try:
            inJson = json.load(request.content)
        except ValueError:
            logger.warn("Peer %s made push connection with malformed JSON", peer.servername)
            return {'errcode': 'M_BAD_JSON', 'error': 'Malformed JSON'}

        if 'sgAssocs' not in inJson:
            logger.warn("Peer %s made push connection with no 'sgAssocs' key in JSON", peer.servername)
            return {'errcode': 'M_BAD_JSON', 'error': 'No "sgAssocs" key in JSON'}

        failedIds = []

        globalAssocsStore = GlobalAssociationStore(self.sydent)

        for originId,sgAssoc in inJson['sgAssocs'].items():
            try:
                peer.verifySignedAssociation(sgAssoc)
                logger.debug("Signed association from %s with origin ID %s verified", peer.servername, originId)

                # Don't bother adding if one has already failed: we add all of them or none so we're only going to
                # roll back the transaction anyway (but we continue to try & verify the rest so we can give a
                # complete list of the ones that don't verify)
                if len(failedIds) > 0:
                    continue

                assocObj = threePidAssocFromDict(sgAssoc)

                if assocObj.mxid is not None:
                    globalAssocsStore.addAssociation(assocObj, json.dumps(sgAssoc), peer.servername, originId, commit=False)
                else:
                    logger.info("Incoming deletion: removing associations for %s / %s", assocObj.medium, assocObj.address)
                    globalAssocsStore.removeAssociation(assocObj.medium, assocObj.address)
                logger.info("Stored association origin ID %s from %s", originId, peer.servername)
            except:
                failedIds.append(originId)
                logger.warn("Failed to verify signed association from %s with origin ID %s",
                            peer.servername, originId)
                twisted.python.log.err()

        if len(failedIds) > 0:
            self.sydent.db.rollback()
            request.setResponseCode(400)
            return {'errcode': 'M_VERIFICATION_FAILED', 'error': 'Verification failed for one or more associations',
                    'failed_ids':failedIds}
        else:
            self.sydent.db.commit()
            return {'success':True}
Ejemplo n.º 20
0
    def render_POST(self, request: Request) -> JsonDict:
        # Cast safety: This request has an ISSLTransport because this servlet
        # is a resource under the ReplicationHttpsServer and nowhere else.
        request.transport = cast(ISSLTransport, request.transport)
        peerCert = cast(X509, request.transport.getPeerCertificate())
        peerCertCn = peerCert.get_subject().commonName

        peerStore = PeerStore(self.sydent)

        peer = peerStore.getPeerByName(peerCertCn)

        if not peer:
            logger.warning(
                "Got connection from %s but no peer found by that name",
                peerCertCn)
            raise MatrixRestError(403, "M_UNKNOWN_PEER",
                                  "This peer is not known to this server")

        logger.info("Push connection made from peer %s", peer.servername)

        if (not request.requestHeaders.hasHeader("Content-Type")
                # Type safety: the hasHeader call returned True, so getRawHeaders()
                # returns a nonempty list.
                or request.requestHeaders.getRawHeaders("Content-Type")[
                    0]  # type: ignore[index]
                != "application/json"):
            logger.warning(
                "Peer %s made push connection with non-JSON content (type: %s)",
                peer.servername,
                # Type safety: the hasHeader call returned True, so getRawHeaders()
                # returns a nonempty list.
                request.requestHeaders.getRawHeaders("Content-Type")
                [0],  # type: ignore[index]
            )
            raise MatrixRestError(400, "M_NOT_JSON",
                                  "This endpoint expects JSON")

        try:
            # json.loads doesn't allow bytes in Python 3.5
            inJson = json_decoder.decode(
                request.content.read().decode("UTF-8"))
        except ValueError:
            logger.warning("Peer %s made push connection with malformed JSON",
                           peer.servername)
            raise MatrixRestError(400, "M_BAD_JSON", "Malformed JSON")

        if "sgAssocs" not in inJson:
            logger.warning(
                "Peer %s made push connection with no 'sgAssocs' key in JSON",
                peer.servername,
            )
            raise MatrixRestError(400, "M_BAD_JSON",
                                  'No "sgAssocs" key in JSON')

        failedIds: List[int] = []

        globalAssocsStore = GlobalAssociationStore(self.sydent)

        # Ensure items are pulled out of the dictionary in order of origin_id.
        sg_assocs_raw: SignedAssociations = inJson.get("sgAssocs", {})
        sg_assocs = sorted(sg_assocs_raw.items(), key=lambda k: int(k[0]))

        for originId, sgAssoc in sg_assocs:
            try:
                peer.verifySignedAssociation(sgAssoc)
                logger.debug(
                    "Signed association from %s with origin ID %s verified",
                    peer.servername,
                    originId,
                )

                # Don't bother adding if one has already failed: we add all of them or none so
                # we're only going to roll back the transaction anyway (but we continue to try
                # & verify the rest so we can give a complete list of the ones that don't
                # verify)
                if len(failedIds) > 0:
                    continue

                assocObj = threePidAssocFromDict(sgAssoc)

                # ensure we are casefolding email addresses before hashing/storing
                assocObj.address = normalise_address(assocObj.address,
                                                     assocObj.medium)

                if assocObj.mxid is not None:
                    # Calculate the lookup hash with our own pepper for this association
                    pepper = self.hashing_store.get_lookup_pepper()
                    assert pepper is not None
                    str_to_hash = " ".join(
                        [assocObj.address, assocObj.medium, pepper], )
                    assocObj.lookup_hash = sha256_and_url_safe_base64(
                        str_to_hash)

                    # Add this association
                    globalAssocsStore.addAssociation(
                        assocObj,
                        json.dumps(sgAssoc),
                        peer.servername,
                        originId,
                        commit=False,
                    )
                else:
                    logger.info(
                        "Incoming deletion: removing associations for %s / %s",
                        assocObj.medium,
                        assocObj.address,
                    )
                    globalAssocsStore.removeAssociation(
                        assocObj.medium, assocObj.address)
                logger.info("Stored association origin ID %s from %s",
                            originId, peer.servername)
            except Exception:
                failedIds.append(originId)
                logger.warning(
                    "Failed to verify signed association from %s with origin ID %s",
                    peer.servername,
                    originId,
                )
                twisted.python.log.err()

        if len(failedIds) > 0:
            self.sydent.db.rollback()
            request.setResponseCode(400)
            return {
                "errcode": "M_VERIFICATION_FAILED",
                "error": "Verification failed for one or more associations",
                "failed_ids": failedIds,
            }
        else:
            self.sydent.db.commit()
            return {"success": True}
Ejemplo n.º 21
0
    def render_POST(self, request):
        send_cors(request)
        err = require_args(request, ("medium", "address", "room_id", "sender",))
        if err:
            return json.dumps(err)
        medium = request.args["medium"][0]
        address = request.args["address"][0]
        roomId = request.args["room_id"][0]
        sender = request.args["sender"][0]

        globalAssocStore = GlobalAssociationStore(self.sydent)
        mxid = globalAssocStore.getMxid(medium, address)
        if mxid:
            request.setResponseCode(400)
            return json.dumps({
                "errcode": "THREEPID_IN_USE",
                "error": "Binding already known",
                "mxid": mxid,
            })

        if medium != "email":
            request.setResponseCode(400)
            return json.dumps({
                "errcode": "M_UNRECOGNIZED",
                "error": "Didn't understand medium '%s'" % (medium,),
            })

        token = self._randomString(128)

        tokenStore = JoinTokenStore(self.sydent)

        ephemeralPrivateKey = nacl.signing.SigningKey.generate()
        ephemeralPublicKey = ephemeralPrivateKey.verify_key

        ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True)
        ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True)

        tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64)
        tokenStore.storeToken(medium, address, roomId, sender, token)

        substitutions = {}
        for key, values in request.args.items():
            if len(values) == 1 and type(values[0]) == str:
                substitutions[key] = values[0]
        substitutions["token"] = token

        required = [
            'sender_display_name',
            'token',
            'room_name',
            'bracketed_room_name',
            'room_avatar_url',
            'sender_display_name',
            'guest_user_id',
            'guest_access_token',
        ]
        for k in required:
            substitutions.setdefault(k, '')

        substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64
        if substitutions["room_name"] != '':
            substitutions["bracketed_room_name"] = "(%s)" % substitutions["room_name"]

        subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8')
        substitutions["subject_header_value"] = subject_header.encode()

        sendEmail(self.sydent, "email.invite_template", address, substitutions)

        pubKey = self.sydent.keyring.ed25519.verify_key
        pubKeyBase64 = encode_base64(pubKey.encode())

        baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),)

        keysToReturn = []
        keysToReturn.append({
            "public_key": pubKeyBase64,
            "key_validity_url": baseUrl + "/pubkey/isvalid",
        })
        keysToReturn.append({
            "public_key": ephemeralPublicKeyBase64,
            "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid",
        })

        resp = {
            "token": token,
            "public_key": pubKeyBase64,
            "public_keys": keysToReturn,
            "display_name": self.redact(address),
        }

        return json.dumps(resp)
Ejemplo n.º 22
0
 def __init__(self, syd: "Sydent", lookup_pepper: str) -> None:
     self.sydent = syd
     self.globalAssociationStore = GlobalAssociationStore(self.sydent)
     self.lookup_pepper = lookup_pepper
Ejemplo n.º 23
0
    def render_POST(self, request):
        peerCert = request.transport.getPeerCertificate()
        peerCertCn = peerCert.get_subject().commonName

        peerStore = PeerStore(self.sydent)

        peer = peerStore.getPeerByName(peerCertCn)

        if not peer:
            logger.warn(
                "Got connection from %s but no peer found by that name",
                peerCertCn)
            request.setResponseCode(403)
            return {
                'errcode': 'M_UNKNOWN_PEER',
                'error': 'This peer is not known to this server'
            }

        logger.info("Push connection made from peer %s", peer.servername)

        if not request.requestHeaders.hasHeader('Content-Type') or \
                request.requestHeaders.getRawHeaders('Content-Type')[0] != 'application/json':
            logger.warn(
                "Peer %s made push connection with non-JSON content (type: %s)",
                peer.servername,
                request.requestHeaders.getRawHeaders('Content-Type')[0])
            return {
                'errcode': 'M_NOT_JSON',
                'error': 'This endpoint expects JSON'
            }

        try:
            inJson = json.load(request.content)
        except ValueError:
            logger.warn("Peer %s made push connection with malformed JSON",
                        peer.servername)
            return {'errcode': 'M_BAD_JSON', 'error': 'Malformed JSON'}

        if 'sgAssocs' not in inJson:
            logger.warn(
                "Peer %s made push connection with no 'sgAssocs' key in JSON",
                peer.servername)
            return {
                'errcode': 'M_BAD_JSON',
                'error': 'No "sgAssocs" key in JSON'
            }

        failedIds = []

        globalAssocsStore = GlobalAssociationStore(self.sydent)

        for originId, sgAssoc in inJson['sgAssocs'].items():
            try:
                peer.verifyMessage(sgAssoc)
                logger.debug(
                    "Signed association from %s with origin ID %s verified",
                    peer.servername, originId)

                # Don't bother adding if one has already failed: we add all of them or none so we're only going to
                # roll back the transaction anyway (but we continue to try & verify the rest so we can give a
                # complete list of the ones that don't verify)
                if len(failedIds) > 0:
                    continue

                assocObj = threePidAssocFromDict(sgAssoc)

                if assocObj.mxid is not None:
                    globalAssocsStore.addAssociation(assocObj,
                                                     json.dumps(sgAssoc),
                                                     peer.servername,
                                                     originId,
                                                     commit=False)
                else:
                    logger.info(
                        "Incoming deletion: removing associations for %s / %s",
                        assocObj.medium, assocObj.address)
                    globalAssocsStore.removeAssociation(
                        assocObj.medium, assocObj.address)
                logger.info("Stored association origin ID %s from %s",
                            originId, peer.servername)
            except:
                failedIds.append(originId)
                logger.warn(
                    "Failed to verify signed association from %s with origin ID %s",
                    peer.servername, originId)
                twisted.python.log.err()

        if len(failedIds) > 0:
            self.sydent.db.rollback()
            request.setResponseCode(400)
            return {
                'errcode': 'M_VERIFICATION_FAILED',
                'error': 'Verification failed for one or more associations',
                'failed_ids': failedIds
            }
        else:
            self.sydent.db.commit()
            return {'success': True}
Ejemplo n.º 24
0
    def render_POST(self, request):
        send_cors(request)

        authIfV2(self.sydent, request)

        args = get_args(request, ("medium", "address", "room_id", "sender",))
        medium = args["medium"]
        address = args["address"]
        roomId = args["room_id"]
        sender = args["sender"]

        globalAssocStore = GlobalAssociationStore(self.sydent)
        mxid = globalAssocStore.getMxid(medium, address)
        if mxid:
            request.setResponseCode(400)
            return {
                "errcode": "M_THREEPID_IN_USE",
                "error": "Binding already known",
                "mxid": mxid,
            }

        if medium != "email":
            request.setResponseCode(400)
            return {
                "errcode": "M_UNRECOGNIZED",
                "error": "Didn't understand medium '%s'" % (medium,),
            }

        token = self._randomString(128)

        tokenStore = JoinTokenStore(self.sydent)

        ephemeralPrivateKey = nacl.signing.SigningKey.generate()
        ephemeralPublicKey = ephemeralPrivateKey.verify_key

        ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True)
        ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True)

        tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64)
        tokenStore.storeToken(medium, address, roomId, sender, token)

        # Variables to substitute in the template.
        substitutions = {}
        # Include all arguments sent via the request.
        for k, v in args.items():
            if isinstance(v, string_types):
                substitutions[k] = v
        substitutions["token"] = token

        # Substitutions that the template requires, but are optional to provide
        # to the API.
        extra_substitutions = [
            'sender_display_name',
            'token',
            'room_name',
            'bracketed_room_name',
            'room_avatar_url',
            'sender_avatar_url',
            'guest_user_id',
            'guest_access_token',
        ]
        for k in extra_substitutions:
            substitutions.setdefault(k, '')

        substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64
        if substitutions["room_name"] != '':
            substitutions["bracketed_room_name"] = "(%s)" % substitutions["room_name"]

        substitutions["web_client_location"] = self.sydent.default_web_client_location
        if 'org.matrix.web_client_location' in substitutions:
            substitutions["web_client_location"] = substitutions.pop("org.matrix.web_client_location")

        subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8')
        substitutions["subject_header_value"] = subject_header.encode()

        brand = self.sydent.brand_from_request(request)
        templateFile = self.sydent.get_branded_template(
            brand,
            "invite_template.eml",
            ('email', 'email.invite_template'),
        )

        sendEmail(self.sydent, templateFile, address, substitutions)

        pubKey = self.sydent.keyring.ed25519.verify_key
        pubKeyBase64 = encode_base64(pubKey.encode())

        baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),)

        keysToReturn = []
        keysToReturn.append({
            "public_key": pubKeyBase64,
            "key_validity_url": baseUrl + "/pubkey/isvalid",
        })
        keysToReturn.append({
            "public_key": ephemeralPublicKeyBase64,
            "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid",
        })

        resp = {
            "token": token,
            "public_key": pubKeyBase64,
            "public_keys": keysToReturn,
            "display_name": self.redact_email_address(address),
        }

        return resp
Ejemplo n.º 25
0
    def render_POST(self, request):
        peerCert = request.transport.getPeerCertificate()
        peerCertCn = peerCert.get_subject().commonName

        peerStore = PeerStore(self.sydent)

        peer = peerStore.getPeerByName(peerCertCn)

        if not peer:
            logger.warn(
                "Got connection from %s but no peer found by that name",
                peerCertCn)
            raise MatrixRestError(403, 'M_UNKNOWN_PEER',
                                  'This peer is not known to this server')

        logger.info("Push connection made from peer %s", peer.servername)

        if not request.requestHeaders.hasHeader('Content-Type') or \
                request.requestHeaders.getRawHeaders('Content-Type')[0] != 'application/json':
            logger.warn(
                "Peer %s made push connection with non-JSON content (type: %s)",
                peer.servername,
                request.requestHeaders.getRawHeaders('Content-Type')[0])
            raise MatrixRestError(400, 'M_NOT_JSON',
                                  'This endpoint expects JSON')

        try:
            # json.loads doesn't allow bytes in Python 3.5
            inJson = json.loads(request.content.read().decode("UTF-8"))
        except ValueError:
            logger.warn("Peer %s made push connection with malformed JSON",
                        peer.servername)
            raise MatrixRestError(400, 'M_BAD_JSON', 'Malformed JSON')

        if 'sgAssocs' not in inJson:
            logger.warn(
                "Peer %s made push connection with no 'sgAssocs' key in JSON",
                peer.servername)
            raise MatrixRestError(400, 'M_BAD_JSON',
                                  'No "sgAssocs" key in JSON')

        failedIds = []

        globalAssocsStore = GlobalAssociationStore(self.sydent)

        # Ensure items are pulled out of the dictionary in order of origin_id.
        sg_assocs = inJson.get('sgAssocs', {})
        sg_assocs = sorted(sg_assocs.items(), key=lambda k: int(k[0]))

        for originId, sgAssoc in sg_assocs:
            try:
                peer.verifySignedAssociation(sgAssoc)
                logger.debug(
                    "Signed association from %s with origin ID %s verified",
                    peer.servername, originId)

                # Don't bother adding if one has already failed: we add all of them or none so
                # we're only going to roll back the transaction anyway (but we continue to try
                # & verify the rest so we can give a complete list of the ones that don't
                # verify)
                if len(failedIds) > 0:
                    continue

                assocObj = threePidAssocFromDict(sgAssoc)

                if assocObj.mxid is not None:
                    # Calculate the lookup hash with our own pepper for this association
                    str_to_hash = u' '.join([
                        assocObj.address, assocObj.medium,
                        self.hashing_store.get_lookup_pepper()
                    ], )
                    assocObj.lookup_hash = sha256_and_url_safe_base64(
                        str_to_hash)

                    # Add this association
                    globalAssocsStore.addAssociation(assocObj,
                                                     json.dumps(sgAssoc),
                                                     peer.servername,
                                                     originId,
                                                     commit=False)
                else:
                    logger.info(
                        "Incoming deletion: removing associations for %s / %s",
                        assocObj.medium, assocObj.address)
                    globalAssocsStore.removeAssociation(
                        assocObj.medium, assocObj.address)
                logger.info("Stored association origin ID %s from %s",
                            originId, peer.servername)
            except:
                failedIds.append(originId)
                logger.warn(
                    "Failed to verify signed association from %s with origin ID %s",
                    peer.servername, originId)
                twisted.python.log.err()

        if len(failedIds) > 0:
            self.sydent.db.rollback()
            request.setResponseCode(400)
            return {
                'errcode': 'M_VERIFICATION_FAILED',
                'error': 'Verification failed for one or more associations',
                'failed_ids': failedIds
            }
        else:
            self.sydent.db.commit()
            return {'success': True}
Ejemplo n.º 26
0
    def render_POST(self, request: Request) -> JsonDict:
        send_cors(request)

        args = get_args(
            request,
            (
                "medium",
                "address",
                "room_id",
                "sender",
            ),
        )
        medium = args["medium"]
        address = args["address"]
        roomId = args["room_id"]
        sender = args["sender"]

        # ensure we are casefolding email address before storing
        normalised_address = normalise_address(address, medium)

        verified_sender = None
        if self.require_auth:
            account = authV2(self.sydent, request)
            verified_sender = sender
            if account.userId != sender:
                raise MatrixRestError(403, "M_UNAUTHORIZED",
                                      "'sender' doesn't match")

        globalAssocStore = GlobalAssociationStore(self.sydent)
        mxid = globalAssocStore.getMxid(medium, normalised_address)
        if mxid:
            request.setResponseCode(400)
            return {
                "errcode": "M_THREEPID_IN_USE",
                "error": "Binding already known",
                "mxid": mxid,
            }

        if medium != "email":
            request.setResponseCode(400)
            return {
                "errcode": "M_UNRECOGNIZED",
                "error": "Didn't understand medium '%s'" % (medium, ),
            }

        if not (0 < len(address) <= MAX_EMAIL_ADDRESS_LENGTH):
            request.setResponseCode(400)
            return {
                "errcode": "M_INVALID_PARAM",
                "error": "Invalid email provided"
            }

        token = self._randomString(128)

        tokenStore = JoinTokenStore(self.sydent)

        ephemeralPrivateKey = nacl.signing.SigningKey.generate()
        ephemeralPublicKey = ephemeralPrivateKey.verify_key

        ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(),
                                                  True)
        ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(),
                                                 True)

        tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64)
        tokenStore.storeToken(medium, normalised_address, roomId, sender,
                              token)

        # Variables to substitute in the template.
        substitutions = {}
        # Include all arguments sent via the request.
        for k, v in args.items():
            if isinstance(v, str):
                substitutions[k] = v
        substitutions["token"] = token

        # Substitutions that the template requires, but are optional to provide
        # to the API.
        extra_substitutions = [
            "sender_display_name",
            "token",
            "room_name",
            "bracketed_room_name",
            "room_avatar_url",
            "sender_avatar_url",
            "guest_user_id",
            "guest_access_token",
        ]
        for k in extra_substitutions:
            substitutions.setdefault(k, "")

        # For MSC3288 room type, prefer the stable field, but fallback to the
        # unstable field.
        if "room_type" not in substitutions:
            substitutions["room_type"] = substitutions.get(
                "org.matrix.msc3288.room_type", "")

        substitutions["bracketed_verified_sender"] = ""
        if verified_sender:
            substitutions["bracketed_verified_sender"] = "(%s) " % (
                verified_sender, )

        substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64
        if substitutions["room_name"] != "":
            substitutions[
                "bracketed_room_name"] = "(%s) " % substitutions["room_name"]

        substitutions[
            "web_client_location"] = self.sydent.config.email.default_web_client_location
        if "org.matrix.web_client_location" in substitutions:
            substitutions["web_client_location"] = substitutions[
                "org.matrix.web_client_location"]

        if substitutions["room_type"] == "m.space":
            subject = self.sydent.config.email.invite_subject_space % substitutions
        else:
            subject = self.sydent.config.email.invite_subject % substitutions

        substitutions["subject_header_value"] = Header(subject,
                                                       "utf8").encode()

        brand = self.sydent.brand_from_request(request)

        # self.sydent.config.email.invite_template is deprecated
        if self.sydent.config.email.invite_template is None:
            templateFile = self.sydent.get_branded_template(
                brand,
                "invite_template.eml",
            )
        else:
            templateFile = self.sydent.config.email.invite_template

        try:
            sendEmail(self.sydent, templateFile, normalised_address,
                      substitutions)
        except EmailAddressException:
            request.setResponseCode(HTTPStatus.BAD_REQUEST)
            return {
                "errcode": "M_INVALID_EMAIL",
                "error": "Invalid email address"
            }

        pubKey = self.sydent.keyring.ed25519.verify_key
        pubKeyBase64 = encode_base64(pubKey.encode())

        baseUrl = "%s/_matrix/identity/api/v1" % (
            self.sydent.config.http.server_http_url_base, )

        keysToReturn = []
        keysToReturn.append({
            "public_key": pubKeyBase64,
            "key_validity_url": baseUrl + "/pubkey/isvalid",
        })
        keysToReturn.append({
            "public_key":
            ephemeralPublicKeyBase64,
            "key_validity_url":
            baseUrl + "/pubkey/ephemeral/isvalid",
        })

        resp = {
            "token": token,
            "public_key": pubKeyBase64,
            "public_keys": keysToReturn,
            "display_name": self.redact_email_address(address),
        }

        return resp
Ejemplo n.º 27
0
 def __init__(self, syd, lookup_pepper):
     self.sydent = syd
     self.globalAssociationStore = GlobalAssociationStore(self.sydent)
     self.lookup_pepper = lookup_pepper
Ejemplo n.º 28
0
    def render_POST(self, request):
        send_cors(request)
        err, args = get_args(request, ("medium", "address", "room_id", "sender",))
        if err:
            return json.dumps(err)
        medium = args["medium"]
        address = args["address"]
        roomId = args["room_id"]
        sender = args["sender"]

        globalAssocStore = GlobalAssociationStore(self.sydent)
        mxid = globalAssocStore.getMxid(medium, address)
        if mxid:
            request.setResponseCode(400)
            return json.dumps({
                "errcode": "THREEPID_IN_USE",
                "error": "Binding already known",
                "mxid": mxid,
            })

        if medium != "email":
            request.setResponseCode(400)
            return json.dumps({
                "errcode": "M_UNRECOGNIZED",
                "error": "Didn't understand medium '%s'" % (medium,),
            })

        token = self._randomString(128)

        tokenStore = JoinTokenStore(self.sydent)

        ephemeralPrivateKey = nacl.signing.SigningKey.generate()
        ephemeralPublicKey = ephemeralPrivateKey.verify_key

        ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True)
        ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True)

        tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64)
        tokenStore.storeToken(medium, address, roomId, sender, token)

        substitutions = {}
        for key, values in request.args.items():
            if len(values) == 1 and type(values[0]) == str:
                substitutions[key] = values[0]
        substitutions["token"] = token

        required = [
            'sender_display_name',
            'token',
            'room_name',
            'bracketed_room_name',
            'room_avatar_url',
            'sender_display_name',
            'guest_user_id',
            'guest_access_token',
        ]
        for k in required:
            substitutions.setdefault(k, '')

        substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64
        if substitutions["room_name"] != '':
            substitutions["bracketed_room_name"] = "(%s)" % substitutions["room_name"]

        subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8')
        substitutions["subject_header_value"] = subject_header.encode()

        sendEmail(self.sydent, "email.invite_template", address, substitutions)

        pubKey = self.sydent.keyring.ed25519.verify_key
        pubKeyBase64 = encode_base64(pubKey.encode())

        baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),)

        keysToReturn = []
        keysToReturn.append({
            "public_key": pubKeyBase64,
            "key_validity_url": baseUrl + "/pubkey/isvalid",
        })
        keysToReturn.append({
            "public_key": ephemeralPublicKeyBase64,
            "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid",
        })

        resp = {
            "token": token,
            "public_key": pubKeyBase64,
            "public_keys": keysToReturn,
            "display_name": self.redact(address),
        }

        return json.dumps(resp)
Ejemplo n.º 29
0
class LookupV2Servlet(Resource):
    isLeaf = True

    def __init__(self, syd, lookup_pepper):
        self.sydent = syd
        self.globalAssociationStore = GlobalAssociationStore(self.sydent)
        self.lookup_pepper = lookup_pepper

    @jsonwrap
    def render_POST(self, request):
        """
        Perform lookups with potentially hashed 3PID details.

        Depending on our response to /hash_details, the client will choose a
        hash algorithm and pepper, hash the 3PIDs it wants to lookup, and
        send them to us, along with the algorithm and pepper it used.

        We first check this algorithm/pepper combo matches what we expect,
        then compare the 3PID details to what we have in the database.

        Params: A JSON object containing the following keys:
                * 'addresses': List of hashed/plaintext (depending on the
                               algorithm) 3PID addresses and mediums.
                * 'algorithm': The algorithm the client has used to process
                               the 3PIDs.
                * 'pepper': The pepper the client has attached to the 3PIDs.

        Returns: Object with key 'mappings', which is a dictionary of results
                 where each result is a key/value pair of what the client sent, and
                 the matching Matrix User ID that claims to own that 3PID.

                 User IDs for which no mapping is found are omitted.
        """
        send_cors(request)

        authIfV2(self.sydent, request)

        args = get_args(request, ('addresses', 'algorithm', 'pepper'))

        addresses = args['addresses']
        if not isinstance(addresses, list):
            request.setResponseCode(400)
            return {'errcode': 'M_INVALID_PARAM', 'error': 'addresses must be a list'}

        algorithm = str(args['algorithm'])
        if algorithm not in HashDetailsServlet.known_algorithms:
            request.setResponseCode(400)
            return {'errcode': 'M_INVALID_PARAM', 'error': 'algorithm is not supported'}

        # Ensure address count is under the configured limit
        limit = int(self.sydent.cfg.get("general", "address_lookup_limit"))
        if len(addresses) > limit:
            request.setResponseCode(400)
            return {'errcode': 'M_TOO_LARGE', 'error': 'More than the maximum amount of '
                                                       'addresses provided'}

        pepper = str(args['pepper'])
        if pepper != self.lookup_pepper:
            request.setResponseCode(400)
            return {
                'errcode': 'M_INVALID_PEPPER',
                'error': "pepper does not match '%s'" % (self.lookup_pepper,),
                'algorithm': algorithm,
                'lookup_pepper': self.lookup_pepper,
            }

        logger.info("Lookup of %d threepid(s) with algorithm %s", len(addresses), algorithm)
        if algorithm == "none":
            # Lookup without hashing
            medium_address_tuples = []
            for address_and_medium in addresses:
                # Parse medium, address components
                address_medium_split = address_and_medium.split()

                # Forbid addresses that contain a space
                if len(address_medium_split) != 2:
                    request.setResponseCode(400)
                    return {
                        'errcode': 'M_UNKNOWN',
                        'error': 'Invalid "address medium" pair: "%s"' % address_and_medium
                    }

                # Get the mxid for the address/medium combo if known
                address, medium = address_medium_split
                medium_address_tuples.append((medium, address))

            # Lookup the mxids
            medium_address_mxid_tuples = self.globalAssociationStore.getMxids(medium_address_tuples)

            # Return a dictionary of lookup_string: mxid values
            return {'mappings': {"%s %s" % (x[1], x[0]): x[2]
                                 for x in medium_address_mxid_tuples}}

        elif algorithm == "sha256":
            # Lookup using SHA256 with URL-safe base64 encoding
            mappings = self.globalAssociationStore.retrieveMxidsForHashes(addresses)

            return {'mappings': mappings}

        request.setResponseCode(400)
        return {'errcode': 'M_INVALID_PARAM', 'error': 'algorithm is not supported'}

    def render_OPTIONS(self, request):
        send_cors(request)
        return b''