def servePage(self, view, url, params): if url == "/edit": selectedLang = params.get('lang', None) if selectedLang and len(selectedLang) == 2: Config.setProperty(Config.KEY_LANGUAGE, selectedLang) # I18nManager will be triggered here because it listens to the Config fsf = params.get('friendsseefriends', None) friendsseefriends = fsf is not None and len(fsf) > 0 Config.setProperty(Config.KEY_ALLOW_FRIENDS_TO_SEE_FRIENDS, friendsseefriends) slw = params.get('showlogwindow', None) showlogwindow = slw is not None and len(slw) > 0 Config.setProperty(Config.KEY_SHOW_LOG_WINDOW, showlogwindow) # If Config has changed, may need to update profile to include/hide friends info DbI.updateContactList(friendsseefriends) # When friends are notified next time, the profile's hash will be calculated and sent afw = params.get('allowfriendrequests', None) allowfriendrequests = afw is not None and len(afw) > 0 Config.setProperty(Config.KEY_ALLOW_FRIEND_REQUESTS, allowfriendrequests) # Save config to file in case it's changed Config.save() contents = self.buildPage({ 'pageTitle': I18nManager.getText("settings.title"), 'pageBody': "<p>Settings changed... should I go back to settings or back to home now?</p>", 'pageFooter': "<p>Footer</p>" }) view.setHtml(contents) else: pageProps = { "friendsseefriends": "checked" if Config.getProperty( Config.KEY_ALLOW_FRIENDS_TO_SEE_FRIENDS) else "", "allowfriendrequests": "checked" if Config.getProperty( Config.KEY_ALLOW_FRIEND_REQUESTS) else "", "showlogwindow": "checked" if Config.getProperty(Config.KEY_SHOW_LOG_WINDOW) else "", "language_en": "", "language_de": "" } pageProps["language_" + Config.getProperty(Config.KEY_LANGUAGE)] = "selected" #print("body:", self.formtemplate.getHtml(pageProps)) contents = self.buildPage({ 'pageTitle': I18nManager.getText("settings.title"), 'pageBody': self.formtemplate.getHtml(pageProps), 'pageFooter': "<p>Footer</p>" }) view.setHtml(contents)
def closeEvent(self, event): '''Clean up before closing by stopping all services''' print("Closing Murmeli") # Tell postmen to stop working for p in self.postmen: p.stop() DbI.releaseDb() TorClient.stopTor() self.clearWebCache() event.accept()
def processPendingContacts(torId): print("Process pending contact accept responses from:", torId) foundReq = False for resp in DbI.getPendingContactMessages(torId): name = resp.get("fromName", None) if not name: profile = DbI.getProfile(torId) name = profile["displayName"] print("Found pending contact accept request from: ", name) # Check signature using keyring _, signatureKey = CryptoClient.decryptAndCheckSignature( resp.get("encryptedMsg", None)) if signatureKey: foundReq = True # Insert new message into inbox with message contents rowToStore = { "messageType": "contactresponse", "fromId": resp.get("fromId", None), "fromName": name, "accepted": True, "messageBody": resp.get("messageBody", ""), "timestamp": resp.get("timestamp", None), "messageRead": True, "messageReplied": True, "recipients": DbI.getOwnTorid() } DbI.addToInbox(rowToStore) if foundReq: DbI.updateProfile(torId, {"status": "untrusted"}) # Delete all pending contact responses from this torId DbI.deletePendingContactMessages(torId)
def sendReferRequestMessage(sendToId, requestedId, intro): '''Send a message to sendToId, to ask that they recommend you to requestedId''' sendToProfile = DbI.getProfile(sendToId) if sendToProfile and sendToProfile.get("status", "nostatus") == "trusted" \ and requestedId != DbI.getOwnTorid(): print("Send message to", sendToId, "requesting referral of", requestedId) notify = ContactReferRequestMessage(friendId=requestedId, introMessage=intro) notify.recipients = [sendToId] DbI.addToOutbox(notify)
def broadcastOnlineStatus(self): '''Queue a status notification message for each of our trusted contacts''' print("Outgoing postman is broadcasting the status...") self._broadcasting = True profileList = DbI.getTrustedProfiles() if profileList: msg = StatusNotifyMessage(online=True, ping=True, profileHash=None) msg.recipients = [c['torid'] for c in profileList] DbI.addToOutbox(msg) self._broadcasting = False self.flushSignal.emit()
def handleReceiveAccept(torId, name, keyStr): '''We have requested contact with another id, and this has now been accepted. So we can import their public key into our keyring and update their status from "requested" to "untrusted"''' # Use keyStr to update keyring and get the keyId keyId = CryptoClient.importPublicKey(keyStr) # Store the keyId and name in their existing row, and update status to "untrusted" DbI.updateProfile(torId, { "name": name, "status": "untrusted", "keyid": keyId })
def handleInitiate(torId, displayName): '''We have requested contact with another id, so we can set up this new contact's name with a status of "requested"''' # TODO: If row already exists then get status (and name/displayname) and error with it # Add new row in db with id, name and "requested" if torId and torId != DbI.getOwnTorid(): DbI.updateProfile( torId, { 'displayName': displayName, 'name': displayName, 'status': 'requested' })
def __init__(self, *args): self.logPanel = LogWindow() GuiWindow.__init__(self, lowerItem=self.logPanel) self.clearWebCache() self.postmen = None self.toolbar = self.makeToolbar([ ("images/toolbar-home.png", self.onHomeClicked, "mainwindow.toolbar.home"), ("images/toolbar-people.png", self.onContactsClicked, "mainwindow.toolbar.contacts"), ("images/toolbar-messages.png", self.onMessagesClicked, "mainwindow.toolbar.messages"), ("images/toolbar-messages-highlight.png", self.onMessagesClicked, "mainwindow.toolbar.messages"), ("images/toolbar-calendar.png", self.onCalendarClicked, "mainwindow.toolbar.calendar"), ("images/toolbar-settings.png", self.onSettingsClicked, "mainwindow.toolbar.settings") ]) self.addToolBar(self.toolbar) self.setContextMenuPolicy(QtCore.Qt.NoContextMenu) # status bar self.statusbar = QtWidgets.QStatusBar(self) self.statusbar.setObjectName("statusbar") self.setStatusBar(self.statusbar) self.setWindowTitle(I18nManager.getText("mainwindow.title")) self.setStatusTip("Murmeli") self.setPageServer(PageServer()) self.navigateTo("/") # we want to be notified of Config changes Config.registerSubscriber(self) self.postmen = [ postmen.IncomingPostman(self), postmen.OutgoingPostman(self) ] self.postmen[1].messageSentSignal.connect(self.logPanel.notifyLogEvent) MessageShuffler.getTannoy().updateSignal.connect( self.logPanel.notifyLogEvent) # Make sure Tor client is started if not TorClient.isStarted(): TorClient.startTor() # Create database instance if not already set if not DbI.hasDbSet(): DbI.setDb(MurmeliDb(Config.getSsDatabaseFile())) # Make sure the status of the contacts matches our keyring missingKeyNames = ContactMaker.checkAllContactsKeys() if missingKeyNames: warningTexts = [I18nManager.getText("warning.keysnotfoundfor") ] + missingKeyNames QtWidgets.QMessageBox.warning(self, "Murmeli", "\n ".join(warningTexts))
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 = DbI.getProfile(torId) if profile and profile["status"] in ["untrusted", "trusted"]: # Update the user's status to trusted DbI.updateProfile(torId, {"status": "trusted"}) # Trigger a StatusNotify to tell them we're online notify = StatusNotifyMessage(online=True, ping=True, profileHash=None) notify.recipients = [torId] DbI.addToOutbox(notify)
def setUp(self): Config.load() CryptoClient.useTestKeyring() self.FRIEND_TORID = "zo7quhgn1nq1uppt" FRIEND_KEYID = "3B898548F994C536" TestUtils.setupOwnProfile("46944E14D24D711B") # id of key1 DbI.updateProfile( self.FRIEND_TORID, { "status": "trusted", "keyid": FRIEND_KEYID, "name": "Norbert Jones", "displayName": "Uncle Norbert" }) TestUtils.setupKeyring(["key1_private", "key2_public"])
def _createUnencryptedPayload(self): if self.senderId is None: self.senderId = DbI.getOwnTorid() return self.packBytesTogether([ self.encodeNumberToBytes(self.messageType, 1), self.senderId, self._createSubpayload() ])
def sendMessage(self, message, whoto): # Check status of recipient in profile profile = DbI.getProfile(whoto) 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.instance().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
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 = DbI.getOwnTorid() if not self.senderName: self.senderName = DbI.getProfile().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 = DbI.getOwnTorid() if torid == myTorId: return (None, None, None, None) # Find the contacts of the specified person selectedProfile = DbI.getProfile(torid) 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 DbI.getMessageableProfiles(): foundid = c['torid'] ourContactIds.add(foundid) if c['status'] == 'trusted' and foundid != torid: trustedContactIds.add(foundid) nameMap[foundid] = c['displayName'] # 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) # TODO: Maybe subtract requested contacts from the "possibleForMe" set? # 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 _createSubpayload(self): '''Use the stored fields to pack the payload contents together''' if self.profileHash is None or self.profileHash == "": self.profileHash = dbutils.calculateHash(DbI.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 construct(payload, isEncrypted=True): '''Factory constructor using a given payload and extracting the fields''' if not payload: return None signatureKey = None if isEncrypted: # Decrypt the payload with our key decrypted, signatureKey = CryptoClient.decryptAndCheckSignature( payload) else: decrypted = payload if decrypted: print("Asymmetric message, length of decrypted is", len(decrypted)) else: print("Asymmetric message has no decrypted") # Separate fields of message into common ones and the type-specific payload msgType, subpayload, tstmp = AsymmetricMessage._stripFields(decrypted) print("Recovered timestamp='", tstmp, "' (", len(tstmp), ")") # Find a suitable subclass to call using the messageType msg = None if msgType == Message.TYPE_CONTACT_RESPONSE: msg = ContactResponseMessage.constructFrom(subpayload) elif msgType == Message.TYPE_STATUS_NOTIFY: msg = StatusNotifyMessage.constructFrom(subpayload) elif msgType == Message.TYPE_ASYM_MESSAGE: msg = RegularMessage.constructFrom(subpayload) elif msgType == Message.TYPE_INFO_REQUEST: msg = InfoRequestMessage.constructFrom(subpayload) elif msgType == Message.TYPE_INFO_RESPONSE: msg = InfoResponseMessage.constructFrom(subpayload) elif msgType == Message.TYPE_FRIEND_REFERRAL: msg = ContactReferralMessage.constructFrom(subpayload) elif msgType == Message.TYPE_FRIENDREFER_REQUEST: msg = ContactReferRequestMessage.constructFrom(subpayload) # Ask the message if it's ok to have no signature if isEncrypted and msg: if msg.acceptUnrecognisedSignature(): # Save the encrypted contents so we can verify it later msg.encryptedContents = payload elif not signatureKey: msg = None if msg: try: msgTimestamp = tstmp.decode('utf-8') except: msgTimestamp = msg.makeCurrentTimestamp() msg.timestamp = Message.convertTimestampFromString(msgTimestamp) msg.signatureKeyId = signatureKey if signatureKey: print( "Asymm setting senderId because I've got a signatureKey: '%s'" % signatureKey) signatureId = DbI.findUserIdFromKeyId(signatureKey) if signatureId: msg.senderId = signatureId return msg
def checkAllContactsKeys(): '''Return a list of names for which the key can't be found''' nameList = [] for c in DbI.getMessageableProfiles(): torId = c['torid'] if c else None if torId: keyId = c['keyid'] if not keyId: print("No keyid found for torid", torId) nameList.append(c['displayName']) elif not CryptoClient.getPublicKey(keyId): print("CryptoClient hasn't got a public key for torid", torId) nameList.append(c['displayName']) if not keyId or not CryptoClient.getPublicKey(keyId): # We haven't got their key in our keyring! DbI.updateProfile(torId, {"status": "requested"}) return nameList
def setupOwnProfile(keyId): tempDb = MurmeliDb() DbI.setDb(tempDb) DbI.updateProfile( TestUtils._ownTorId, { "status": "self", "ownprofile": True, "keyid": keyId, "name": "Geoffrey Lancaster", "displayName": "Me", "description": "Ä fictitious person with a couple of Umläute in his description." })
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.instance().comeOnline(DbI.getOwnTorid()) if message.senderMustBeTrusted: sender = DbI.getProfile(message.senderId) 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) elif message.encryptionType == Message.ENCTYPE_RELAY: # Get received bytes of message, and add to Outbox, send to everybody EXCEPT the sender bytesToSend = message.createOutput(None) if bytesToSend: # add to outbox, but don't send it back to message.senderId DbI.addRelayMessageToOutbox(bytesToSend, message.senderId) 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 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 = DbI.getProfile()['description'] bacProfile = bac.profile self.assertEqual(mydescription, bacProfile['description'])
def generateAddPage(self): '''Build the form page for adding a new user, using the template''' bodytext = self.addtemplate.getHtml({"owntorid": DbI.getOwnTorid()}) return self.buildPage({ 'pageTitle': I18nManager.getText("contacts.title"), 'pageBody': bodytext, 'pageFooter': "<p>Footer</p>" })
def finish(self): '''Finished the key gen''' # Store key, name in the database for our own profile selectedKey = self.privateKeys[self.keypairListWidget.currentRow()] ownid = TorClient.getOwnId() # See if a name was entered before, if so use that myname = self.keygenParamBoxes['name'].text() if not myname: # Extract the name from the string which comes back from the key as "'Some Name (no comment) <*****@*****.**>'" myname = self.extractName(selectedKey['uids']) profile = { "name": myname, "keyid": selectedKey['keyid'], "torid": ownid, "status": "self", "ownprofile": True } # Store this in the database DbI.updateProfile(ownid, profile) return True
def handleAccept(torId): '''We want to accept a contact request, so we need to find the request(s), and use it/them to update our keyring and our database entry''' # Get this person's current status from the db, if available profile = DbI.getProfile(torId) status = profile.get("status", None) if profile else None # Look for the contact request(s) in the inbox, and extract the name and publicKey senderName, senderKeystr, directRequest = ContactMaker.getContactRequestDetails( torId) keyValid = senderKeystr and len(senderKeystr) > 20 if keyValid: if status in [None, "requested"]: # add key to keyring keyId = CryptoClient.importPublicKey(senderKeystr) # work out what name and status to stores storedSenderName = profile["name"] if profile else None nameToStore = storedSenderName if storedSenderName else senderName statusToStore = "untrusted" if directRequest else "pending" # add or update the profile DbI.updateProfile( torId, { "status": statusToStore, "keyid": keyId, "name": nameToStore, "displayName": nameToStore }) ContactMaker.processPendingContacts(torId) elif status == "pending": print("Request already pending, nothing to do") elif status in ["untrusted", "trusted"]: # set status to untrusted? Send response? print("Trying to handle an accept but status is already", status) # Move all corresponding requests to be regular messages instead DbI.changeRequestMessagesToRegular(torId) else: print("Trying to handle an accept but key isn't valid")
def run(self): # Check each of the services in turn self.successFlags = {} # Database time.sleep(0.5) DbI.setDb(MurmeliDb(Config.getSsDatabaseFile())) self.successFlags['database'] = True self.updatedSignal.emit() time.sleep(0.5) # Gnupg self.successFlags['gpg'] = CryptoClient.checkGpg() self.updatedSignal.emit() time.sleep(1) # Tor if TorClient.startTor(): torid = TorClient.getOwnId() if torid: print("Started tor, our own id is: ", torid) self.successFlags['tor'] = True else: print("Failed to start tor") else: print("startTor returned false :(")
def _createSubpayload(self): '''Pack the specific fields into the subpayload''' # Get own name if self.senderName is None: self.senderName = DbI.getProfile().get('name', self.senderId) 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): '''Use the stored fields to pack the payload contents together''' ownProfile = DbI.getProfile() if self.infoType is None: self.infoType = InfoRequestMessage.INFO_PROFILE if self.profileString is None: self.profileString = dbutils.getProfileAsString(ownProfile) self.profileHash = dbutils.calculateHash(ownProfile) return self.packBytesTogether([ self.encodeNumberToBytes(self.infoType), self.encodeNumberToBytes(len(self.profileString), 4), self.profileString, self.encodeNumberToBytes(len(self.profileHash), 4), self.profileHash ])
def construct(payload, isEncrypted): '''Construct a message from its payload''' originalPayload, signKey = CryptoClient.verifySignedData( payload) if isEncrypted else (payload, None) if originalPayload: # The payload could be verified and extracted, but we still don't know # if the contents are for me or for somebody else (probably for somebody else!) messageForMe = Message.MessageFromReceivedData( originalPayload, isEncrypted) if messageForMe: return messageForMe else: msg = RelayingMessage(rcvdBytes=originalPayload) msg.senderId = DbI.findUserIdFromKeyId(signKey) return msg
def testAvatars(self): '''Test the loading, storing and exporting of binary avatar images''' db = supersimpledb.MurmeliDb() DbI.setDb(db) inputPath = "testdata/example-avatar.jpg" outPath = "cache/avatar-deadbeef.jpg" if os.path.exists(outPath): os.remove(outPath) os.makedirs('cache', exist_ok=True) self.assertFalse(os.path.exists(outPath)) DbI.updateProfile("deadbeef", {"profilepicpath":inputPath}) # Output file still shouldn't exist, we didn't give a path to write picture to self.assertFalse(os.path.exists(outPath)) DbI.updateProfile("deadbeef", {"profilepicpath":inputPath}, "cache") # Output file should exist now self.assertTrue(os.path.exists(outPath)) # TODO: Any way to compare input with output? They're not the same. DbI.releaseDb()
def _createSubpayload(self): '''Pack the specific fields into the subpayload''' # Get own name if not self.friendName: self.friendName = DbI.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 generateFingerprintsPage(self, userid): '''Build the page for checking the fingerprints of the selected user''' # First, get the name of the user person = DbI.getProfile(userid) dispName = person['displayName'] fullName = person['name'] 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>" })