예제 #1
0
def authV2(
    sydent: "Sydent",
    request: Request,
    requireTermsAgreed: bool = True,
) -> "Account":
    """For v2 APIs check that the request has a valid access token associated with it

    :param sydent: The Sydent instance to use.
    :param request: The request to look for an access token in.
    :param requireTermsAgreed: Whether to deny authentication if the user hasn't accepted
        the terms of service.

    :returns Account: The account object if there is correct auth
    :raises MatrixRestError: If the request is v2 but could not be authed or the user has
        not accepted terms.
    """
    token = tokenFromRequest(request)

    if token is None:
        raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized")

    accountStore = AccountStore(sydent)

    account = accountStore.getAccountByToken(token)
    if account is None:
        raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized")

    if requireTermsAgreed:
        terms = get_terms(sydent)
        if (terms.getMasterVersion() is not None
                and account.consentVersion != terms.getMasterVersion()):
            raise MatrixRestError(403, "M_TERMS_NOT_SIGNED",
                                  "Terms not signed")

    return account
예제 #2
0
    def render_POST(self, request: Request) -> JsonDict:
        send_cors(request)

        if self.require_auth:
            authV2(self.sydent, request)

        args = get_args(request, ("private_key", "token", "mxid"))

        private_key_base64 = args["private_key"]
        token = args["token"]
        mxid = args["mxid"]

        sender = self.tokenStore.getSenderForToken(token)
        if sender is None:
            raise MatrixRestError(404, "M_UNRECOGNIZED", "Didn't recognize token")

        to_sign = {
            "mxid": mxid,
            "sender": sender,
            "token": token,
        }
        try:
            private_key = signedjson.key.decode_signing_key_base64(
                "ed25519", "0", private_key_base64
            )
            signed: JsonDict = signedjson.sign.sign_json(
                to_sign, self.server_name, private_key
            )
        except Exception:
            logger.exception("signing failed")
            raise MatrixRestError(500, "M_UNKNOWN", "Internal Server Error")

        return signed
예제 #3
0
파일: auth.py 프로젝트: jzillioux/sydent
def authIfV2(sydent, request, requireTermsAgreed=True):
    """For v2 APIs check that the request has a valid access token associated with it

    :returns Account|None: The account object if there is correct auth, or None for v1 APIs
    :raises MatrixRestError: If the request is v2 but could not be authed or the user has not accepted terms
    """
    if request.path.startswith('/_matrix/identity/v2'):
        token = tokenFromRequest(request)

        if token is None:
            raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized")

        accountStore = AccountStore(sydent)

        account = accountStore.getAccountByToken(token)
        if account is None:
            raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized")

        if requireTermsAgreed:
            terms = get_terms(sydent)
            if (terms.getMasterVersion() is not None
                    and account.consentVersion != terms.getMasterVersion()):
                raise MatrixRestError(403, "M_TERMS_NOT_SIGNED",
                                      "Terms not signed")

        return account
    return None
예제 #4
0
    def render_POST(self, request: Request) -> JsonDict:
        send_cors(request)

        account = None
        if self.require_auth:
            account = authV2(self.sydent, request)

        args = get_args(request, ("sid", "client_secret", "mxid"))

        sid = args["sid"]
        mxid = args["mxid"]
        clientSecret = args["client_secret"]

        if not is_valid_client_secret(clientSecret):
            raise MatrixRestError(400, "M_INVALID_PARAM",
                                  "Invalid client_secret provided")

        if account:
            # This is a v2 API so only allow binding to the logged in user id
            if account.userId != mxid:
                raise MatrixRestError(
                    403,
                    "M_UNAUTHORIZED",
                    "This user is prohibited from binding to the mxid",
                )

        try:
            valSessionStore = ThreePidValSessionStore(self.sydent)
            s = valSessionStore.getValidatedSession(sid, clientSecret)
        except (IncorrectClientSecretException, InvalidSessionIdException):
            # Return the same error for not found / bad client secret otherwise
            # people can get information about sessions without knowing the
            # secret.
            raise MatrixRestError(
                404,
                "M_NO_VALID_SESSION",
                "No valid session was found matching that sid and client secret",
            )
        except SessionExpiredException:
            raise MatrixRestError(
                400,
                "M_SESSION_EXPIRED",
                "This validation session has expired: call requestToken again",
            )
        except SessionNotValidatedException:
            raise MatrixRestError(
                400,
                "M_SESSION_NOT_VALIDATED",
                "This validation session has not yet been completed",
            )

        res = self.sydent.threepidBinder.addBinding(s.medium, s.address, mxid)
        return res
예제 #5
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}
예제 #6
0
    def render_POST(self, request: Request) -> JsonDict:
        """
        Mark a set of terms and conditions as having been agreed to
        """
        send_cors(request)

        account = authV2(self.sydent, request, False)

        args = get_args(request, ("user_accepts", ))

        user_accepts = args["user_accepts"]

        terms = get_terms(self.sydent)
        unknown_urls = list(set(user_accepts) - terms.getUrlSet())
        if len(unknown_urls) > 0:
            raise MatrixRestError(
                400, "M_UNKNOWN",
                "Unrecognised URLs: %s" % (", ".join(unknown_urls), ))

        termsStore = TermsStore(self.sydent)
        termsStore.addAgreedUrls(account.userId, user_accepts)

        all_accepted_urls = termsStore.getAgreedUrls(account.userId)

        if terms.urlListIsSufficient(all_accepted_urls):
            accountStore = AccountStore(self.sydent)
            accountStore.setConsentVersion(account.userId,
                                           terms.getMasterVersion())

        return {}
예제 #7
0
    def render_POST(self, request):
        send_cors(request)

        account = authIfV2(self.sydent, request)

        args = get_args(request, ('sid', 'client_secret', 'mxid'))

        sid = args['sid']
        mxid = args['mxid']
        clientSecret = args['client_secret']

        if not is_valid_client_secret(clientSecret):
            request.setResponseCode(400)
            return {
                'errcode': 'M_INVALID_PARAM',
                'error': 'Invalid client_secret provided'
            }

        # Return the same error for not found / bad client secret otherwise people can get information about
        # sessions without knowing the secret
        noMatchError = {
            'errcode':
            'M_NO_VALID_SESSION',
            'error':
            "No valid session was found matching that sid and client secret"
        }

        if account:
            # This is a v2 API so only allow binding to the logged in user id
            if account.userId != mxid:
                raise MatrixRestError(
                    403, 'M_UNAUTHORIZED',
                    "This user is prohibited from binding to the mxid")

        try:
            valSessionStore = ThreePidValSessionStore(self.sydent)
            s = valSessionStore.getValidatedSession(sid, clientSecret)
        except IncorrectClientSecretException:
            return noMatchError
        except SessionExpiredException:
            return {
                'errcode':
                'M_SESSION_EXPIRED',
                'error':
                "This validation session has expired: call requestToken again"
            }
        except InvalidSessionIdException:
            return noMatchError
        except SessionNotValidatedException:
            return {
                'errcode': 'M_SESSION_NOT_VALIDATED',
                'error': "This validation session has not yet been completed"
            }

        res = self.sydent.threepidBinder.addBinding(s.medium, s.address, mxid)
        return res
예제 #8
0
def authIfV2(sydent, request, requireTermsAgreed=True):
    """For v2 APIs check that the request has a valid access token associated with it

    :param sydent: The Sydent instance to use.
    :type sydent: sydent.sydent.Sydent
    :param request: The request to look for an access token in.
    :type request: twisted.web.server.Request
    :param requireTermsAgreed: Whether to deny authentication if the user hasn't accepted
        the terms of service.

    :returns Account|None: The account object if there is correct auth, or None for v1
        APIs.
    :raises MatrixRestError: If the request is v2 but could not be authed or the user has
        not accepted terms.
    """
    if request.path.startswith(b'/_matrix/identity/v2'):
        token = tokenFromRequest(request)

        if token is None:
            raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized")

        accountStore = AccountStore(sydent)

        account = accountStore.getAccountByToken(token)
        if account is None:
            raise MatrixRestError(401, "M_UNAUTHORIZED", "Unauthorized")

        if requireTermsAgreed:
            terms = get_terms(sydent)
            if (
                terms.getMasterVersion() is not None and
                account.consentVersion != terms.getMasterVersion()
            ):
                raise MatrixRestError(403, "M_TERMS_NOT_SIGNED", "Terms not signed")

        return account
    return None
예제 #9
0
    def render_POST(self, request: Request) -> JsonDict:
        """
        Invalidate the given access token
        """
        send_cors(request)

        authV2(self.sydent, request, False)

        token = tokenFromRequest(request)
        if token is None:
            raise MatrixRestError(400, "M_MISSING_PARAMS", "Missing token")

        accountStore = AccountStore(self.sydent)
        accountStore.delToken(token)
        return {}
예제 #10
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}
예제 #11
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}
예제 #12
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