def testProfileHash(self): # Clear out and reset own profile myTorId = "ABC123DEF456GH78" myprofile = {"name" : "Constantin Taylor", "keyid" : "someKeyId", "displayName" : "Me", "status" : "self", "ownprofile" : True, "description":"Some fairly descriptive text", "birthday":None, "interests":"chocolate pudding with fudge"} DbClient.updateContact(myTorId, myprofile) firstHash = DbClient.calculateHash(DbClient.getProfile()) self.assertEqual(firstHash, "12ae5c8dc8e1c2186b4ed4918040bb16", "First hash not what I was expecting") # Now change interests and check that hash changes DbClient.updateContact(myTorId, {"interests":"roasted vegetables and hummus"}) secondHash = DbClient.calculateHash(DbClient.getProfile()) self.assertNotEqual(firstHash, secondHash, "Profile hash should have changed")
def generateListPage(self, doEdit=False, userid=None, extraParams=None): self.requirePageResources(['avatar-none.jpg', 'status-self.png', 'status-requested.png', 'status-untrusted.png', 'status-trusted.png']) # List of contacts, and show details for the selected one (or self if userid=None) selectedprofile = DbClient.getProfile(userid) if selectedprofile is None: selectedprofile = DbClient.getProfile() userid = selectedprofile['torid'] ownPage = userid == DbClient.getOwnTorId() # Build list of contacts userboxes = [] for p in DbClient.getContactList(): box = Bean() box.dispName = p['displayName'] box.torid = p['torid'] box.tilestyle = "contacttile" + ("selected" if p['torid'] == userid else "") box.status = p['status'] box.isonline = Contacts.isOnline(box.torid) userboxes.append(box) # expand templates using current details lefttext = self.listtemplate.getHtml({'webcachedir' : Config.getWebCacheDir(), 'contacts' : userboxes}) pageProps = {"webcachedir" : Config.getWebCacheDir(), 'person':selectedprofile} # Add extra parameters if necessary if extraParams: pageProps.update(extraParams) # See which contacts we have in common with this person (sharedContactIds, possIdsForThem, possIdsForMe, nameMap) = ContactMaker.getSharedAndPossibleContacts(userid) sharedContacts = self._makeIdAndNameBeanList(sharedContactIds, nameMap) pageProps.update({"sharedcontacts" : sharedContacts}) possibleContacts = self._makeIdAndNameBeanList(possIdsForThem, nameMap) pageProps.update({"possiblecontactsforthem" : possibleContacts}) possibleContacts = self._makeIdAndNameBeanList(possIdsForMe, nameMap) pageProps.update({"possiblecontactsforme" : possibleContacts}) # Which template to use depends on whether we're just showing or also editing if doEdit: # Use two different details templates, one for self and one for others detailstemplate = self.editowndetailstemplate if ownPage else self.editdetailstemplate righttext = detailstemplate.getHtml(pageProps) else: detailstemplate = self.detailstemplate # just show righttext = detailstemplate.getHtml(pageProps) contents = self.buildTwoColumnPage({'pageTitle' : I18nManager.getText("contacts.title"), 'leftColumn' : lefttext, 'rightColumn' : righttext, 'pageFooter' : "<p>Footer</p>"}) return contents
def generateFingerprintsPage(self, userid): '''Build the page for checking the fingerprints of the selected user''' # First, get the name of the user person = DbClient.getProfile(userid, False) dispName = person.get('displayName', '') fullName = person.get('name', '') if not dispName: dispName = fullName if dispName != fullName: fullName = dispName + " (" + fullName + ")" fc = self._makeFingerprintChecker(userid) # check it's ok to generate status = person.get('status', '') if not fc.valid \ or status not in ['untrusted', 'trusted']: print("Not generating fingerprints page because status is", status) return None # Get one set of words for us and three sets for them printsAlreadyChecked = (person.get('status', '') == "trusted") bodytext = self.fingerprintstemplate.getHtml( {"mywords":fc.getCodeWords(True, 0, "en"), "theirwords0":fc.getCodeWords(False, 0, "en"), "theirwords1":fc.getCodeWords(False, 1, "en"), "theirwords2":fc.getCodeWords(False, 2, "en"), "fullname":fullName, "shortname":dispName, "userid":userid, "alreadychecked":printsAlreadyChecked}) return self.buildPage({'pageTitle' : I18nManager.getText("contacts.title"), 'pageBody' : bodytext, 'pageFooter' : "<p>Footer</p>"})
def getPublicKey(self, torid): '''Use the keyid stored in mongo, and get the corresponding public key from the Crypto module''' profile = DbClient.getProfile(torid) if profile is not None: keyid = profile.get('keyid', None) if keyid is not None: return CryptoClient.getPublicKey(keyid)
def dealWithUnencryptedMessage(message): '''Decide what to do with the given unencrypted message''' if message.messageType == Message.TYPE_CONTACT_REQUEST: print("Received a contact request from", message.senderId) # Check config to see whether we accept these or not if Config.getProperty(Config.KEY_ALLOW_FRIEND_REQUESTS) \ and MessageShuffler._isProfileStatusOk(message.senderId, [None, 'requested', 'untrusted', 'trusted']): # Call DbClient to store new message in inbox rowToStore = {"messageType":"contactrequest", "fromId":message.senderId, "fromName":message.senderName, "messageBody":message.message, "publicKey":message.publicKey, "timestamp":message.timestamp, "messageRead":False, "messageReplied":False} DbClient.addMessageToInbox(rowToStore) elif message.messageType == Message.TYPE_CONTACT_RESPONSE: print("It's an unencrypted contact response, so it must be a refusal") sender = DbClient.getProfile(message.senderId, False) if MessageShuffler._isProfileStatusOk(message.senderId, ['requested']): senderName = sender.get("displayName") if sender else "" ContactMaker.handleReceiveDeny(message.senderId) # Call DbClient to store new message in inbox rowToStore = {"messageType":"contactresponse", "fromId":message.senderId, "fromName":senderName, "messageBody":"", "accepted":False, "messageRead":False, "messageReplied":False, "timestamp":message.timestamp, "recipients":MessageShuffler.getOwnTorId()} DbClient.addMessageToInbox(rowToStore) else: print("Hä? It's unencrypted but the message type is", message.messageType)
def dealWithMessage(message): '''Examine the received message and decide what to do with it''' print("Hmm, the MessageShuffler has been given some kind of message") # We must be online if we've received a message Contacts.comeOnline(MessageShuffler.getOwnTorId()) if message.senderMustBeTrusted: sender = DbClient.getProfile(message.senderId, False) if not sender or sender['status'] != "trusted": return # throw message away if not message.isComplete(): print("A message of type", message.encryptionType, "was received but it's not complete - throwing away") return # throw message away # if it's not encrypted, it's for us -> save in inbox if message.encryptionType == Message.ENCTYPE_NONE: MessageShuffler.dealWithUnencryptedMessage(message) elif message.encryptionType == Message.ENCTYPE_SYMM: # if it's symmetric, forget it for now pass elif message.encryptionType == Message.ENCTYPE_ASYM: MessageShuffler.dealWithAsymmetricMessage(message) else: print("Hä? What kind of encryption type is that? ", message.encryptionType) # Log receipt of message (do we want to know about relays at all?) if message.encryptionType in [Message.ENCTYPE_NONE, Message.ENCTYPE_ASYM]: logMessage = "Message of type: %s received from %s" % (message.getMessageTypeKey(), message.senderId) MessageShuffler.getTannoy().shout(logMessage)
def _createSubpayload(self): '''Use the stored fields to pack the payload contents together''' if self.profileHash is None or self.profileHash == "": self.profileHash = DbClient.calculateHash(DbClient.getProfile()) return self.packBytesTogether([ self.encodeNumberToBytes(1 if self.online else 0, 1), self.encodeNumberToBytes(1 if self.ping else 0, 1), self.profileHash])
def testProfileResponse(self): m = message.InfoResponseMessage(message.InfoRequestMessage.INFO_PROFILE) output = m.createUnencryptedOutput() bac = message.Message.MessageFromReceivedData(output, False) self.assertIsNotNone(bac, "couldn't decode the data") self.assertEqual(message.InfoRequestMessage.INFO_PROFILE, bac.infoType) mydescription = DbClient.getProfile()["description"] bacProfile = bac.profile self.assertEqual(mydescription, bacProfile["description"])
def testGetProfile(self): # Delete whole profiles table DbClient._getProfileTable().remove({}) self.assertEqual(DbClient._getProfileTable().count(), 0, "Profiles table should be empty") # Add own profile myTorId = "ABC123DEF456GH78" myprofile = {"name" : "Constantin Taylor", "keyid" : "someKeyId", "displayName" : "Me", "status" : "self", "ownprofile" : True} DbClient.updateContact(myTorId, myprofile) self.assertEqual(DbClient._getProfileTable().count(), 1, "Profiles table should have my profile in it") profileFromDb = DbClient.getProfile(None) self.assertIsNotNone(profileFromDb, "Couldn't retrieve own profile") profileFromDb = DbClient.getProfile(myTorId) self.assertIsNotNone(profileFromDb, "Couldn't retrieve profile using own id") # Initiate contact with a new person otherTorId = "PQR123STU456VWX78" otherName = "Olivia Barnacles" DbClient.updateContact(otherTorId, {"status" : "untrusted", "keyid" : "donotknow", "name" : otherName}) self.assertEqual(DbClient._getProfileTable().count(), 2, "Profiles table should have 2 profiles") self.assertEqual(DbClient.getMessageableContacts().count(), 1, "Profiles table should have 1 messageable") self.assertEqual(DbClient.getTrustedContacts().count(), 0, "Profiles table should have 0 trusted") profileFromDb = DbClient.getProfile(otherTorId) self.assertIsNotNone(profileFromDb, "Couldn't retrieve profile using other id") self.assertEqual(profileFromDb.get("name", None), otherName, "Profile name doesn't match what was stored") self.assertEqual(profileFromDb.get("status", None), "untrusted", "Profile status doesn't match what was stored") # Update existing record, change status DbClient.updateContact(otherTorId, {"status" : "trusted"}) self.assertEqual(DbClient._getProfileTable().count(), 2, "Profiles table should still have 2 profiles") profileFromDb = DbClient.getProfile(otherTorId) self.assertIsNotNone(profileFromDb, "Couldn't retrieve profile using other id") self.assertEqual(profileFromDb.get("status", None), "trusted", "Profile status should have been updated") self.assertEqual(DbClient.getMessageableContacts().count(), 1, "Profiles table should have 1 messageable") self.assertEqual(DbClient.getTrustedContacts().count(), 1, "Profiles table should have 1 trusted") # Delete other contact DbClient.updateContact(otherTorId, {"status" : "deleted"}) self.assertEqual(DbClient.getMessageableContacts().count(), 0, "Profiles table should have 0 messageable") self.assertEqual(DbClient.getTrustedContacts().count(), 0, "Profiles table should have 0 trusted") self.assertFalse(DbClient.hasFriends(), "Shouldn't have any friends any more")
def keyFingerprintChecked(torId): '''The fingerprint of this contact's public key has been checked (over a separate channel)''' # Check that userid exists and that status is currently "untrusted" (trusted also doesn't hurt) profile = DbClient.getProfile(torId, False) if profile and profile.get("status", "nostatus") in ["untrusted", "trusted"]: # Update the user's status to trusted DbClient.updateContact(torId, {"status" : "trusted"}) # Trigger a StatusNotify to tell them we're online notify = StatusNotifyMessage(online=True, ping=True, profileHash=None) notify.recipients = [torId] DbClient.addMessageToOutbox(notify)
def _createSubpayload(self): '''Use the stored fields to pack the payload contents together''' if self.infoType is None: self.infoType = InfoRequestMessage.INFO_PROFILE if self.profileString is None: self.profileString = dbutils.getOwnProfileAsString() self.profileHash = DbClient.calculateHash(DbClient.getProfile()) return self.packBytesTogether([ self.encodeNumberToBytes(self.infoType), self.encodeNumberToBytes(len(self.profileString), 4), self.profileString, self.encodeNumberToBytes(len(self.profileHash), 4), self.profileHash])
def servePage(self, view, url, params): DbClient.exportAvatars(Config.getWebCacheDir()) if url == "/send": print("send message of type '%(messageType)s' to id '%(sendTo)s'" % params) elif url.startswith("/delete/"): DbClient.deleteMessageFromInbox(params.get("msgId", "")) # Make dictionary to convert ids to names contactNames = {c['torid']:c['displayName'] for c in DbClient.getContactList()} unknownSender = I18nManager.getText("messages.sender.unknown") unknownRecpt = I18nManager.getText("messages.recpt.unknown") # Get contact requests, responses and mails from inbox conreqs = [] conresps = [] mails = [] for m in DbClient.getInboxMessages(): m['msgId'] = str(m.get("_id", "")) if m['messageType'] == "contactrequest": conreqs.append(m) elif m['messageType'] == "contactrefer": senderId = m.get('fromId', None) m['senderName'] = contactNames.get(senderId, unknownSender) conreqs.append(m) elif m['messageType'] == "contactresponse": if not m.get('accepted', False): m['messageBody'] = I18nManager.getText("messages.contactrequest.refused") m['fromName'] = DbClient.getProfile(m['fromId'], True).get("displayName") elif not m.get('messageBody', False): m['messageBody'] = I18nManager.getText("messages.contactrequest.accepted") conresps.append(m) else: senderId = m.get('fromId', None) if not senderId and m.get('signatureKeyId', None): senderId = DbClient.findUserIdFromKeyId(m['signatureKeyId']) m['senderName'] = contactNames.get(senderId, unknownSender) m['sentTimeStr'] = self.makeLocalTimeString(m['timestamp']) # Split m['recipients'] by commas, and look up each id with contactNames recpts = m.get('recipients', '') if recpts: m['recipients'] = ", ".join([contactNames.get(i, unknownRecpt) for i in recpts.split(",")]) else: m['recipients'] = unknownRecpt mails.append(m) bodytext = self.messagestemplate.getHtml({"contactrequests":conreqs, "contactresponses":conresps, "mails":mails, "nummessages":len(conreqs)+len(conresps)+len(mails), "webcachedir" : Config.getWebCacheDir()}) contents = self.buildPage({'pageTitle' : I18nManager.getText("messages.title"), 'pageBody' : bodytext, 'pageFooter' : "<p>Footer</p>"}) view.setHtml(contents)
def _createSubpayload(self): '''Pack the specific fields into the subpayload''' # Get own name if self.senderName is None: self.senderName = DbClient.getProfile(None).get('name', self.senderId) # Get own public key (first get identifier from DbClient, then use that id to ask crypto module) myPublicKey = self.getOwnPublicKey() messageAsBytes = self.message.encode('utf-8') nameAsBytes = self.senderName.encode('utf-8') subpayload = Message.packBytesTogether([ self.encodeNumberToBytes(len(nameAsBytes), 4), nameAsBytes, self.encodeNumberToBytes(len(messageAsBytes), 4), messageAsBytes, myPublicKey]) return subpayload
def _createSubpayload(self): '''Pack the specific fields into the subpayload''' # Get own name if self.friendName is None: self.friendName = DbClient.getProfile(self.friendId).get('name', self.friendId) publicKey = self.getPublicKey(torid=self.friendId) # TODO: Complain if publicKey is empty messageAsBytes = self.message.encode('utf-8') nameAsBytes = self.friendName.encode('utf-8') subpayload = Message.packBytesTogether([ self.friendId, self.encodeNumberToBytes(len(nameAsBytes), 4), nameAsBytes, self.encodeNumberToBytes(len(messageAsBytes), 4), messageAsBytes, publicKey]) return subpayload
def _createSubpayload(self): '''Use the stored fields to pack the payload contents together''' if self.senderKey is None: self.senderKey = self.getOwnPublicKey() # Get own torid and name if not self.senderId: self.senderId = DbClient.getOwnTorId() if not self.senderName: self.senderName = DbClient.getProfile(None).get('name', self.senderId) if not self.introMessage: self.introMessage = "" nameAsBytes = self.senderName.encode('utf-8') messageAsBytes = self.introMessage.encode('utf-8') print("Packing contact request with senderId", self.senderId) return self.packBytesTogether([ self.senderId, self.encodeNumberToBytes(len(nameAsBytes), 4), nameAsBytes, self.encodeNumberToBytes(len(messageAsBytes), 4), messageAsBytes, self.senderKey])
def getSharedAndPossibleContacts(torid): '''Check which contacts we share with the given torid and which ones we could recommend to each other''' nameMap = {} ourContactIds = set() trustedContactIds = set() theirContactIds = set() # Get our id so we can exclude it from the sets myTorId = DbClient.getOwnTorId() if torid == myTorId: return (None, None, None, None) # Find the contacts of the specified person selectedProfile = DbClient.getProfile(torid, False) selectedContacts = selectedProfile.get('contactlist', None) if selectedProfile else None if selectedContacts: for s in selectedContacts.split(","): if s and len(s) >= 16: foundid = s[0:16] if foundid != myTorId: foundName = s[16:] theirContactIds.add(foundid) nameMap[foundid] = foundName foundTheirContacts = len(theirContactIds) > 0 # Now get information about our contacts for c in DbClient.getMessageableContacts(): foundid = c['torid'] ourContactIds.add(foundid) if c['status'] == 'trusted' and foundid != torid: trustedContactIds.add(foundid) nameMap[foundid] = c.get('displayName', c.get('name', None)) # Should we check the contact information too? if not foundTheirContacts: foundContacts = c.get('contactlist', None) if foundContacts: for s in foundContacts.split(","): if s[0:16] == torid: theirContactIds.add(foundid) # Now we have three sets of torids: our contacts, our trusted contacts, and their contacts. sharedContactIds = ourContactIds.intersection(theirContactIds) # might be empty suggestionsForThem = trustedContactIds.difference(theirContactIds) possibleForMe = theirContactIds.difference(ourContactIds) # Some or all of these sets may be empty, but we still return the map so we can look up names return (sharedContactIds, suggestionsForThem, possibleForMe, nameMap)
def sendMessage(self, message, whoto): # Check status of recipient in profile profile = DbClient.getProfile(whoto, False) status = profile['status'] if profile else "deleted" if status in ['deleted', 'blocked']: return self.RC_MESSAGE_IGNORED print("Trying to send message to '%s'" % whoto) if whoto is not None and len(whoto) == 16: try: s = socks.socksocket() s.setproxy(socks.PROXY_TYPE_SOCKS4, "localhost", 11109) s.connect((whoto + ".onion", 11009)) numsent = s.send(message) s.close() if numsent != len(message): print("Oops - num bytes sent:", numsent, "but message has length:", len(message)) # For really long messages, maybe need to chunk into 4k blocks or something? else: Contacts.comeOnline(whoto) return self.RC_MESSAGE_SENT except Exception as e: print("Woah, that threw something:", e) print("Bailed from the send attempt, returning failure") return self.RC_MESSAGE_FAILED # it didn't work
print("Please stop this mongod service before running Murmeli.") exit() print("No password is saved, so we can't use Auth on the db: go to startupwizard") canStartMurmeli = False if canStartMurmeli: print("I think I can start Murmeli, checking database status") if dbStatus == DbClient.NOT_RUNNING: canStartMurmeli = DbClient.startDatabase(useAuth=True) # Either the database was already running with auth, or we've just started it with auth if canStartMurmeli: # if we can't connect, or if we haven't got our own keypair stored, then we need the startupwizard print("Database is now running, now checking for profile") try: ownprofile = DbClient.getProfile() if ownprofile is None or ownprofile.get("keyid", None) is None: print("I didn't get a profile or didn't get a key, so I can't start Murmeli") canStartMurmeli = False else: print("I think I got a profile and a keyid: '", ownprofile.get("keyid", ""), "' so I'm going to start Murmeli") except Exception: canStartMurmeli = False # maybe authentication failed? if not canStartMurmeli: # Ask DbClient to stop mongo again DbClient.stopDatabase() # Get ready to launch a Qt GUI I18nManager.setLanguage() Config.registerSubscriber(I18nManager.instance())
def _isProfileStatusOk(torId, allowedStatuses): profile = DbClient.getProfile(torId, False) status = profile.get("status", None) if profile else None return status in allowedStatuses
def _makeFingerprintChecker(self, userid): '''Use the given userid to make a FingerprintChecker between me and them''' person = DbClient.getProfile(userid, False) ownFingerprint = CryptoClient.getFingerprint(DbClient.getOwnKeyId()) usrFingerprint = CryptoClient.getFingerprint(person['keyid']) return FingerprintChecker(ownFingerprint, usrFingerprint)
def dealWithAsymmetricMessage(message): '''Decide what to do with the given asymmetric message''' if message.senderId == MessageShuffler.getOwnTorId(): print("*** Shouldn't receive a message from myself!") return # Sort message according to type if message.messageType == Message.TYPE_CONTACT_RESPONSE: print("Received a contact accept from", message.senderId, "name", message.senderName) if MessageShuffler._isProfileStatusOk(message.senderId, ['pending', 'requested', 'untrusted']): print(message.senderName, "'s public key is", message.senderKey) ContactMaker.handleReceiveAccept(message.senderId, message.senderName, message.senderKey) # Call DbClient to store new message in inbox rowToStore = {"messageType":"contactresponse", "fromId":message.senderId, "fromName":message.senderName, "messageBody":message.introMessage, "accepted":True, "messageRead":False, "messageReplied":False, "timestamp":message.timestamp, "recipients":MessageShuffler.getOwnTorId()} DbClient.addMessageToInbox(rowToStore) elif MessageShuffler._isProfileStatusOk(message.senderId, [None, 'blocked']): print("Received a contact response but I didn't send them a request!") print("Encrypted contents are:", message.encryptedContents) rowToStore = {"messageType":"contactresponse", "fromId":message.senderId, "fromName":message.senderName, "messageBody":message.introMessage, "accepted":True, "timestamp":message.timestamp, "encryptedMsg":message.encryptedContents} DbClient.addMessageToPendingContacts(rowToStore) elif message.messageType == Message.TYPE_STATUS_NOTIFY: if message.online: print("One of our contacts has just come online- ", message.senderId, "and hash is", message.profileHash) prof = DbClient.getProfile(userid=message.senderId, extend=False) if prof: storedHash = prof.get("profileHash", "empty") if message.profileHash != storedHash: reply = InfoRequestMessage(infoType=InfoRequestMessage.INFO_PROFILE) reply.recipients = [message.senderId] DbClient.addMessageToOutbox(reply) if message.ping: print("Now sending back a pong, too") reply = StatusNotifyMessage(online=True, ping=False, profileHash=None) reply.recipients = [message.senderId] DbClient.addMessageToOutbox(reply) else: print("It's already a pong so I won't reply") Contacts.comeOnline(message.senderId) else: print("One of our contacts is going offline -", message.senderId) Contacts.goneOffline(message.senderId) elif message.messageType == Message.TYPE_INFO_REQUEST: print("I've received an info request message for type", message.infoType) if MessageShuffler._isProfileStatusOk(message.senderId, ['trusted']): reply = InfoResponseMessage(message.messageType) reply.recipients = [message.senderId] DbClient.addMessageToOutbox(reply) elif message.messageType == Message.TYPE_INFO_RESPONSE: if message.profile and MessageShuffler._isProfileStatusOk(message.senderId, ['trusted', 'untrusted']): if message.profileHash: message.profile['profileHash'] = message.profileHash DbClient.updateContact(message.senderId, message.profile) elif message.messageType == Message.TYPE_ASYM_MESSAGE: print("It's a general kind of message, this should go in the Inbox, right?") if MessageShuffler._isProfileStatusOk(message.senderId, ['trusted', 'untrusted']): Contacts.comeOnline(message.senderId) else: # It's another asymmetric message type print("Hä? What kind of asymmetric message type is that? ", message.messageType)