def testConversationIds(self): '''Test the increment of conversation ids''' db = supersimpledb.MurmeliDb() DbI.setDb(db) # If we start with an empty db, the id should start at 1 for i in range(100): self.assertEqual(DbI.getNewConversationId(), i+1, "id should be 1 more than i") # Add profiles for us and a messages sender DbI.updateProfile("F055", {"keyid":"ZYXW987", "status":"self", "name":"That's me"}) DbI.updateProfile("ABC312", {"keyid":"ZYXW987", "status":"trusted", "name":"Best friend"}) # Add an inbox message with a conversation id DbI.addToInbox({"messageBody":"Gorgonzola and Camembert", "timestamp":"early 2017", "fromId":"ABC312"}) # Get this message again, and check the conversation id, should be 101 msg0 = DbI.getInboxMessages()[0] self.assertEqual(msg0.get("conversationid", 0), 101, "Id should now be 101") # Add another message with the parent hash referring to the first one DbI.addToInbox({"messageBody":"Fried egg sandwich", "timestamp":"middle 2017", "fromId":"ABC312", "parentHash":'40e98bae7a811c23b59b89bd0f11b0a0'}) msg1 = DbI.getInboxMessages()[0] self.assertTrue("Fried egg" in msg1.get("messageBody", ""), "Fried egg should be first") self.assertEqual(msg1.get("conversationid", 0), 101, "Id should now also be 101") # Add another message with an unrecognised parent hash DbI.addToInbox({"messageBody":"Red wine and chocolate", "timestamp":"late 2017", "fromId":"ABC312", "parentHash":'ff3'}) msg2 = DbI.getInboxMessages()[0] self.assertTrue("Red wine" in msg2.get("messageBody", ""), "Red wine should be first") self.assertEqual(msg2.get("conversationid", 0), 102, "Id should take 102") # done DbI.releaseDb()
def testOutbox(self): '''Test the storage and retrieval of messages in the outbox''' db = supersimpledb.MurmeliDb() DbI.setDb(db) self.assertEqual(len(DbI.getOutboxMessages()), 0, "Outbox should be empty") # Add profile for this the target recipient DbI.updateProfile("ABC312", {"keyid":"ZYXW987", "status":"trusted", "name":"Best friend"}) # add one message to the outbox DbI.addToOutbox(ExampleMessage(["ABC312"], "Doesn't matter really what the message is")) self.assertEqual(len(DbI.getOutboxMessages()), 1, "Outbox should have one message") self.checkMessageIndexes(DbI.getOutboxMessages()) DbI.addToOutbox(ExampleMessage(["ABC312"], "A second message")) self.assertEqual(len(DbI.getOutboxMessages()), 2, "Outbox should have 2 messages") self.checkMessageIndexes(DbI.getOutboxMessages()) self.assertTrue(DbI.deleteFromOutbox(0)) self.assertEqual(len(DbI.getOutboxMessages()), 1, "Outbox should only have 1 message (1 empty)") nonEmptyMessages = DbI.getOutboxMessages() self.assertEqual(len(nonEmptyMessages), 1, "Outbox should only have 1 non-empty message") self.assertEqual(nonEmptyMessages[0]["_id"], 1, "Message 0 should have index 1") # See if index of third message is properly assigned DbI.addToOutbox(ExampleMessage(["ABC312"], "A third message")) self.assertEqual(len(DbI.getOutboxMessages()), 2, "Outbox should have 2 messages again") self.assertEqual(DbI.getOutboxMessages()[0]["_id"], 1, "Message 0 should have index 1") self.assertEqual(DbI.getOutboxMessages()[1]["_id"], 2, "Message 1 should have index 2") # done DbI.releaseDb()
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 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 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 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 testBasics(self): '''Testing the basics of the interface with a super-simple db''' # Create new, empty database without file-storage db = supersimpledb.MurmeliDb() DbI.setDb(db) # Lists should be empty self.assertEqual(len(DbI.getMessageableProfiles()), 0, "Should be 0 messageables") self.assertEqual(len(DbI.getTrustedProfiles()), 0, "Should be 0 trusted") self.assertFalse(DbI.hasFriends(), "Shouldn't have any friends") # Store some profiles DbI.updateProfile("abc123", {"keyid":"ZYXW987", "status":"self", "name":"That's me"}) DbI.updateProfile("def123", {"keyid":"JKLM987", "status":"trusted", "name":"Jimmy"}) DbI.updateProfile("ghi123", {"keyid":"TUVWX987", "status":"untrusted", "name":"Dave"}) # Get my ids self.assertEqual(DbI.getOwnTorid(), "abc123", "Should find correct tor id") self.assertEqual(DbI.getOwnKeyid(), "ZYXW987", "Should find correct key id") # Get all profiles self.assertEqual(len(DbI.getProfiles()), 3, "Should be three profiles in total") self.assertEqual(len(DbI.getMessageableProfiles()), 2, "Should be two messageables") self.assertEqual(len(DbI.getTrustedProfiles()), 1, "Should be one trusted") self.assertEqual(DbI.getTrustedProfiles()[0]['displayName'], "Jimmy", "Jimmy should be trusted") self.assertTrue(DbI.hasFriends(), "Should have friends") # Update an existing profile DbI.updateProfile("def123", {"displayName":"Slippin' Jimmy"}) self.assertEqual(len(DbI.getTrustedProfiles()), 1, "Should still be one trusted") self.assertEqual(DbI.getTrustedProfiles()[0]['displayName'], "Slippin' Jimmy", "Slippin' Jimmy should be trusted") # Finished DbI.releaseDb()
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 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 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 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 handleDeleteContact(torId): '''For whatever reason, we don't trust this contact any more, so status is set to "deleted"''' if torId and torId != DbI.getOwnTorid(): DbI.updateProfile(torId, {"status": "deleted"})
def handleReceiveDeny(torId): '''We have requested contact with another id, but this has been denied. So we need to update their status accordingly''' if torId and torId != DbI.getOwnTorid(): DbI.updateProfile(torId, {"status": "deleted"})
def dealWithAsymmetricMessage(message): '''Decide what to do with the given asymmetric message''' if message.senderId == DbI.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) # 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": DbI.getOwnTorid() } DbI.addToInbox(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 } DbI.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 = DbI.getProfile(message.senderId) if prof: storedHash = prof.get("profileHash", "empty") if message.profileHash != storedHash: reply = InfoRequestMessage( infoType=InfoRequestMessage.INFO_PROFILE) reply.recipients = [message.senderId] DbI.addToOutbox(reply) if message.ping: print("Now sending back a pong, too") reply = StatusNotifyMessage(online=True, ping=False, profileHash=None) reply.recipients = [message.senderId] DbI.addToOutbox(reply) else: print("It's already a pong so I won't reply") Contacts.instance().comeOnline(message.senderId) else: print("One of our contacts is going offline -", message.senderId) Contacts.instance().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] DbI.addToOutbox(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 DbI.updateProfile(message.senderId, message.profile, Config.getWebCacheDir()) elif message.messageType == Message.TYPE_FRIEND_REFERRAL: print("I've received a friend referral message from:", message.senderId, "for:", message.friendName) if MessageShuffler._isProfileStatusOk(message.senderId, ['trusted']): # Store new referral message in inbox rowToStore = { "messageType": "contactrefer", "fromId": message.senderId, "friendId": message.friendId, "friendName": message.friendName, "messageBody": message.message, "publicKey": message.publicKey, "timestamp": message.timestamp, "messageRead": False, "messageReplied": False } DbI.addToInbox(rowToStore) elif message.messageType == Message.TYPE_FRIENDREFER_REQUEST: print("I've received a friend referral request from:", message.senderId, "who wants me to refer:", message.friendId) if MessageShuffler._isProfileStatusOk(message.senderId, ['trusted']): # Store message in the inbox rowToStore = { "messageType": "referrequest", "fromId": message.senderId, "friendId": message.friendId, "friendName": message.friendName, "messageBody": message.message, "publicKey": message.publicKey, "timestamp": message.timestamp, "messageRead": False, "messageReplied": False } DbI.addToInbox(rowToStore) 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']): rowToStore = { "messageType": "normal", "fromId": message.senderId, "messageBody": message.messageBody, "timestamp": message.timestamp, "messageRead": False, "messageReplied": False, "recipients": message.sendTo, "parentHash": message.replyToHash } DbI.addToInbox(rowToStore) Contacts.instance().comeOnline(message.senderId) else: # It's another asymmetric message type print("Hä? What kind of asymmetric message type is that? ", message.messageType)
def handleDeny(torId): '''We want to deny a contact request - remember that this id is blocked''' DbI.updateProfile(torId, {"status": "blocked"}) # Delete request from Inbox message = ContactMaker._getInboxMessage(torId, "contactrequest") DbI.deleteFromInbox(message.get("_id"))
def servePage(self, view, url, params): self.requirePageResources([ 'button-addperson.png', 'button-drawgraph.png', 'avatar-none.jpg' ]) DbI.exportAllAvatars(Config.getWebCacheDir()) if url == "/add" or url == "/add/": contents = self.generateAddPage() view.setHtml(contents) return elif url == "/submitaddrequest": print("Submit add request!:", url) if len(params) > 0: # request to add a new friend recipientid = params.get('murmeliid', '') dispname = params.get('displayname', '') intromessage = params.get('intromessage', '') if len(recipientid) == 16: # TODO: How to react if: person already added (untrusted/trusted); request already sent (requested) # update the database accordingly ContactMaker.handleInitiate(recipientid, dispname) print("I should send an add request to '%s' now." % recipientid) outmsg = message.ContactRequestMessage( introMessage=intromessage) outmsg.recipients = [recipientid] DbI.addToOutbox(outmsg) else: print("Hmm, show an error message here?") # in any case, go back to contact list url = "/" + recipientid # ensure that picture is generated for new id DbI.exportAllAvatars(Config.getWebCacheDir()) contents = None userid = None pageParams = {} # Split url into components /userid/command command = [i for i in url.split("/") if i != ""] if len(command) > 0 and len(command[0]) == 16 and re.match( "([a-zA-Z0-9]+)$", command[0]): userid = command[0] # check for command edit or submit-edit if len(command) == 2: if command[1] == "edit": contents = self.generateListPage( doEdit=True, userid=userid) # show edit fields elif command[1] == "submitedit": DbI.updateProfile(userid, params, Config.getWebCacheDir()) # TODO: If we've updated our own details, can we trigger a broadcast? # don't generate contents, go back to details elif command[1] == "delete": ContactMaker.handleDeleteContact(userid) userid = None elif command[1] == "checkfingerprint": contents = self.generateFingerprintsPage(userid) elif command[1] == "checkedfingerprint": givenAnswer = int(params.get('answer', -1)) fc = self._makeFingerprintChecker(userid) expectedAnswer = fc.getCorrectAnswer() if expectedAnswer == givenAnswer: ContactMaker.keyFingerprintChecked(userid) # Show page again contents = self.generateFingerprintsPage(userid) else: # Add a message to show when the list page is re-generated pageParams['fingerprint_check_failed'] = True elif len(command) == 3 and command[1] == "refer" and len( command[2]) == 16: intro = str(params.get('introMessage', "")) ContactMaker.sendReferralMessages(command[0], command[2], intro) pageParams['message_sent'] = True # go back to details page elif len(command) == 3 and command[1] == "requestrefer" and len( command[2]) == 16: intro = str(params.get('introMessage', "")) ContactMaker.sendReferRequestMessage(command[0], command[2], intro) pageParams['message_sent'] = True # go back to details page # If we haven't got any contents yet, then do a show details if not contents: # Show details for selected userid (or for self if userid is None) contents = self.generateListPage(doEdit=False, userid=userid, extraParams=pageParams) view.setHtml(contents)