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
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
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
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
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
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
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}
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}
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