Esempio n. 1
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
Esempio n. 2
0
def calculate_lookup_hash(sydent, address):
    cur = sydent.db.cursor()
    pepper_result = cur.execute("SELECT lookup_pepper from hashing_metadata")
    pepper = pepper_result.fetchone()[0]
    combo = "%s %s %s" % (address, "email", pepper)
    lookup_hash = sha256_and_url_safe_base64(combo)
    return lookup_hash
Esempio n. 3
0
def calculate_lookup_hash(sydent: Sydent, address: str) -> str:
    pepper = sydent.threepidBinder.hashing_store.get_lookup_pepper()
    if pepper is None:
        raise RuntimeError(
            "No lookup pepper found; Sydent should have generated one on startup."
        )
    combo = "%s %s %s" % (address, "email", pepper)
    lookup_hash = sha256_and_url_safe_base64(combo)
    return lookup_hash
Esempio n. 4
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
Esempio n. 5
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
Esempio n. 6
0
    def addBinding(self, medium, address, mxid):
        """
        Binds the given 3pid to the given mxid.

        It's assumed that we have somehow validated that the given user owns
        the given 3pid

        :param medium: The medium of the 3PID to bind.
        :type medium: unicode
        :param address: The address of the 3PID to bind.
        :type address: unicode
        :param mxid: The MXID to bind the 3PID to.
        :type mxid: unicode

        :return: The signed association.
        :rtype: dict[str, any]
        """
        localAssocStore = LocalAssociationStore(self.sydent)

        # Fill out the association details
        createdAt = time_msec()
        expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS

        # Hash the medium + address and store that hash for the purposes of
        # later lookups
        str_to_hash = u' '.join(
            [address, medium,
             self.hashing_store.get_lookup_pepper()], )
        lookup_hash = sha256_and_url_safe_base64(str_to_hash)

        assoc = ThreepidAssociation(
            medium,
            address,
            lookup_hash,
            mxid,
            createdAt,
            createdAt,
            expires,
        )

        localAssocStore.addOrUpdateAssociation(assoc)

        self.sydent.pusher.doLocalPush()

        joinTokenStore = JoinTokenStore(self.sydent)
        pendingJoinTokens = joinTokenStore.getTokens(medium, address)
        invites = []
        for token in pendingJoinTokens:
            token["mxid"] = mxid
            token["signed"] = {
                "mxid": mxid,
                "token": token["token"],
            }
            token["signed"] = signedjson.sign.sign_json(
                token["signed"], self.sydent.server_name,
                self.sydent.keyring.ed25519)
            invites.append(token)
        if invites:
            assoc.extra_fields["invites"] = invites
            joinTokenStore.markTokensAsSent(medium, address)

        signer = Signer(self.sydent)
        sgassoc = signer.signedThreePidAssociation(assoc)

        self._notify(sgassoc, 0)

        return sgassoc
Esempio n. 7
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}
Esempio n. 8
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}
Esempio n. 9
0
    def addBinding(self, medium: str, address: str,
                   mxid: str) -> Dict[str, Any]:
        """
        Binds the given 3pid to the given mxid.

        It's assumed that we have somehow validated that the given user owns
        the given 3pid

        :param medium: The medium of the 3PID to bind.
        :param address: The address of the 3PID to bind.
        :param mxid: The MXID to bind the 3PID to.

        :return: The signed association.
        """

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

        localAssocStore = LocalAssociationStore(self.sydent)

        # Fill out the association details
        createdAt = time_msec()
        expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS

        # Hash the medium + address and store that hash for the purposes of
        # later lookups
        lookup_pepper = self.hashing_store.get_lookup_pepper()
        assert lookup_pepper is not None
        str_to_hash = " ".join([normalised_address, medium, lookup_pepper], )
        lookup_hash = sha256_and_url_safe_base64(str_to_hash)

        assoc = ThreepidAssociation(
            medium,
            normalised_address,
            lookup_hash,
            mxid,
            createdAt,
            createdAt,
            expires,
        )

        localAssocStore.addOrUpdateAssociation(assoc)

        self.sydent.pusher.doLocalPush()

        joinTokenStore = JoinTokenStore(self.sydent)
        pendingJoinTokens = joinTokenStore.getTokens(medium,
                                                     normalised_address)
        invites = []
        # Widen the value type to Any: we're going to set the signed key
        # to point to a dict, but pendingJoinTokens yields Dict[str, str]
        token: Dict[str, Any]
        for token in pendingJoinTokens:
            token["mxid"] = mxid
            presigned = {
                "mxid": mxid,
                "token": token["token"],
            }
            token["signed"] = signedjson.sign.sign_json(
                presigned,
                self.sydent.config.general.server_name,
                self.sydent.keyring.ed25519,
            )
            invites.append(token)
        if invites:
            assoc.extra_fields["invites"] = invites
            joinTokenStore.markTokensAsSent(medium, normalised_address)

        signer = Signer(self.sydent)
        sgassoc = signer.signedThreePidAssociation(assoc)

        defer.ensureDeferred(self._notify(sgassoc, 0))

        return sgassoc