def test_basicRatchet(self): aliceStore = InMemorySenderKeyStore(); bobStore = InMemorySenderKeyStore(); aliceSessionBuilder = GroupSessionBuilder(aliceStore) bobSessionBuilder = GroupSessionBuilder(bobStore) aliceGroupCipher = GroupCipher(aliceStore, GROUP_SENDER) bobGroupCipher = GroupCipher(bobStore, GROUP_SENDER); sentAliceDistributionMessage = aliceSessionBuilder.create(GROUP_SENDER); receivedAliceDistributionMessage = SenderKeyDistributionMessage(serialized = sentAliceDistributionMessage.serialize()); bobSessionBuilder.process(GROUP_SENDER, receivedAliceDistributionMessage) ciphertextFromAlice = aliceGroupCipher.encrypt("smert ze smert") ciphertextFromAlice2 = aliceGroupCipher.encrypt("smert ze smert2") ciphertextFromAlice3 = aliceGroupCipher.encrypt("smert ze smert3") plaintextFromAlice = bobGroupCipher.decrypt(ciphertextFromAlice) try: bobGroupCipher.decrypt(ciphertextFromAlice) raise AssertionError("Should have ratcheted forward!") except DuplicateMessageException as dme: # good pass plaintextFromAlice2 = bobGroupCipher.decrypt(ciphertextFromAlice2) plaintextFromAlice3 = bobGroupCipher.decrypt(ciphertextFromAlice3) self.assertEqual(plaintextFromAlice,"smert ze smert") self.assertEqual(plaintextFromAlice2, "smert ze smert2") self.assertEqual(plaintextFromAlice3, "smert ze smert3")
def test_basicEncryptDecrypt(self): aliceStore = InMemorySenderKeyStore(); bobStore = InMemorySenderKeyStore(); aliceSessionBuilder = GroupSessionBuilder(aliceStore) bobSessionBuilder = GroupSessionBuilder(bobStore) aliceGroupCipher = GroupCipher(aliceStore, GROUP_SENDER) bobGroupCipher = GroupCipher(bobStore, GROUP_SENDER); sentAliceDistributionMessage = aliceSessionBuilder.create(GROUP_SENDER); receivedAliceDistributionMessage = SenderKeyDistributionMessage(serialized = sentAliceDistributionMessage.serialize()); bobSessionBuilder.process(GROUP_SENDER, receivedAliceDistributionMessage) ciphertextFromAlice = aliceGroupCipher.encrypt("smert ze smert") plaintextFromAlice = bobGroupCipher.decrypt(ciphertextFromAlice) self.assertEqual(plaintextFromAlice, "smert ze smert")
def test_noSession(self): aliceStore = InMemorySenderKeyStore(); bobStore = InMemorySenderKeyStore(); aliceSessionBuilder = GroupSessionBuilder(aliceStore) bobSessionBuilder = GroupSessionBuilder(bobStore) aliceGroupCipher = GroupCipher(aliceStore, GROUP_SENDER) bobGroupCipher = GroupCipher(bobStore, GROUP_SENDER); sentAliceDistributionMessage = aliceSessionBuilder.create(GROUP_SENDER); receivedAliceDistributionMessage = SenderKeyDistributionMessage(serialized = sentAliceDistributionMessage.serialize()); ciphertextFromAlice = aliceGroupCipher.encrypt("smert ze smert"); try: plaintextFromAlice = bobGroupCipher.decrypt(ciphertextFromAlice); raise AssertionError("Should be no session!"); except NoSessionException as e: pass
def test_outOfOrder(self): aliceStore = InMemorySenderKeyStore(); bobStore = InMemorySenderKeyStore(); aliceSessionBuilder = GroupSessionBuilder(aliceStore) bobSessionBuilder = GroupSessionBuilder(bobStore) aliceGroupCipher = GroupCipher(aliceStore, GROUP_SENDER) bobGroupCipher = GroupCipher(bobStore, GROUP_SENDER); sentAliceDistributionMessage = aliceSessionBuilder.create(GROUP_SENDER); receivedAliceDistributionMessage = SenderKeyDistributionMessage(serialized = sentAliceDistributionMessage.serialize()); bobSessionBuilder.process(GROUP_SENDER, receivedAliceDistributionMessage) ciphertexts = [] for i in range(0, 100): ciphertexts.append(aliceGroupCipher.encrypt("up the punks")) while len(ciphertexts) > 0: index = KeyHelper.getRandomSequence(2147483647) % len(ciphertexts) ciphertext = ciphertexts.pop(index) plaintext = bobGroupCipher.decrypt(ciphertext) self.assertEqual(plaintext, "up the punks")
class AxolotlSendLayer(AxolotlBaseLayer): MAX_SENT_QUEUE = 100 def __init__(self): super(AxolotlSendLayer, self).__init__() self.sessionCiphers = {} self.groupSessionBuilder = None self.groupCiphers = {} ''' Sent messages will be put in skalti entQueue until we receive a receipt for them. This is for handling retry receipts which requires re-encrypting and resend of the original message As the receipt for a sent message might arrive at a different yowsup instance, ideally the original message should be fetched from a persistent storage. Therefore, if the original message is not in sentQueue for any reason, we will notify the upper layers and let them handle it. ''' self.sentQueue = [] def onNewStoreSet(self, store): if store is not None: self.groupSessionBuilder = GroupSessionBuilder(store) def __str__(self): return "Axolotl Layer" def send(self, node): if node.tag == "message" and node[ "to"] not in self.skipEncJids and not node.getChild( "enc") and (not node.getChild("media") or node.getChild("media")["mediakey"]): self.processPlaintextNodeAndSend(node) # elif node.tag == "iq" and node["xmlns"] == "w:m": # mediaNode = node.getChild("media") # if mediaNode and mediaNode["type"] == "image": # iqNode = IqProtocolEntity.fromProtocolTreeNode(node).toProtocolTreeNode() # iqNode.addChild(ProtocolTreeNode( # "encr_media", { # "type": mediaNode["type"], # "hash": mediaNode["hash"] # } # )) # self.toLower(iqNode) else: self.toLower(node) def receive(self, protocolTreeNode): if not self.processIqRegistry(protocolTreeNode): if protocolTreeNode.tag == "receipt": ''' Going to keep all group message enqueued, as we get receipts from each participant So can't just remove it on first receipt. Therefore, the MAX queue length mechanism should better be working ''' messageNode = self.getEnqueuedMessageNode( protocolTreeNode["id"], protocolTreeNode["participant"] is not None) if not messageNode: logger.debug( "Axolotl layer does not have the message, bubbling it upwards" ) self.toUpper(protocolTreeNode) elif protocolTreeNode["type"] == "retry": logger.info( "Got retry to for message %s, and Axolotl layer has the message" % protocolTreeNode["id"]) retryReceiptEntity = RetryIncomingReceiptProtocolEntity.fromProtocolTreeNode( protocolTreeNode) self.toLower(retryReceiptEntity.ack().toProtocolTreeNode()) self.getKeysFor([ protocolTreeNode["participant"] or protocolTreeNode["from"] ], lambda successJids, b: self.processPlaintextNodeAndSend( messageNode, retryReceiptEntity) if len(successJids) == 1 else None) else: #not interested in any non retry receipts, bubble upwards self.toUpper(protocolTreeNode) def processPlaintextNodeAndSend(self, node, retryReceiptEntity=None): recipient_id = node["to"].split('@')[0] isGroup = "-" in recipient_id if node.getChild("media"): self.toLower(node) # skip media enc for now, groups and non groups elif isGroup: self.sendToGroup(node, retryReceiptEntity) elif self.store.containsSession(recipient_id, 1): self.sendToContact(node) else: self.getKeysFor([node["to"]], lambda successJids, b: self.sendToContact(node) if len(successJids) == 1 else self.toLower(node), lambda: self.toLower(node)) def getPadding(self): num = randint(1, 255) return bytearray([num] * num) def groupSendSequence(self): """ check if senderkeyrecord exists no: - create, - get group jids from info request - for each jid without a session, get keys to create the session - send message with dist key for all participants yes: - send skmsg without any dist key received retry for a participant - request participants keys - send message with dist key only + conversation, only for this participat :return: """ def enqueueSent(self, node): if len(self.sentQueue) >= self.__class__.MAX_SENT_QUEUE: logger.warn("Discarding queued node without receipt") self.sentQueue.pop(0) self.sentQueue.append(node) def getEnqueuedMessageNode(self, messageId, keepEnqueued=False): for i in range(0, len(self.sentQueue)): if self.sentQueue[i]["id"] == messageId: if keepEnqueued: return self.sentQueue[i] return self.sentQueue.pop(i) def sendEncEntities(self, node, encEntities): mediaType = None messageEntity = EncryptedMessageProtocolEntity( encEntities, "text" if not mediaType else "media", _id=node["id"], to=node["to"], notify=node["notify"], timestamp=node["timestamp"], participant=node["participant"], offline=node["offline"], retry=node["retry"]) self.enqueueSent(node) self.toLower(messageEntity.toProtocolTreeNode()) def sendToContact(self, node): recipient_id = node["to"].split('@')[0] cipher = self.getSessionCipher(recipient_id) messageData = self.serializeToProtobuf( node).SerializeToString() + self.getPadding() ciphertext = cipher.encrypt(messageData) mediaType = node.getChild("media")["type"] if node.getChild( "media") else None return self.sendEncEntities(node, [ EncProtocolEntity( EncProtocolEntity.TYPE_MSG if ciphertext.__class__ == WhisperMessage else EncProtocolEntity.TYPE_PKMSG, 2, ciphertext.serialize(), mediaType) ]) def sendToGroupWithSessions(self, node, jidsNeedSenderKey=None, retryCount=0): jidsNeedSenderKey = jidsNeedSenderKey or [] groupJid = node["to"] ownNumber = self.getLayerInterface( YowAuthenticationProtocolLayer).getUsername(False) senderKeyName = SenderKeyName(groupJid, AxolotlAddress(ownNumber, 0)) cipher = self.getGroupCipher(groupJid, ownNumber) encEntities = [] if len(jidsNeedSenderKey): senderKeyDistributionMessage = self.groupSessionBuilder.create( senderKeyName) for jid in jidsNeedSenderKey: sessionCipher = self.getSessionCipher(jid.split('@')[0]) message = self.serializeSenderKeyDistributionMessageToProtobuf( node["to"], senderKeyDistributionMessage) if retryCount > 0: message = self.serializeToProtobuf(node, message) ciphertext = sessionCipher.encrypt( message.SerializeToString() + self.getPadding()) encEntities.append( EncProtocolEntity( EncProtocolEntity.TYPE_MSG if ciphertext.__class__ == WhisperMessage else EncProtocolEntity.TYPE_PKMSG, 2, ciphertext.serialize(), jid=jid)) if not retryCount: messageData = self.serializeToProtobuf(node).SerializeToString() ciphertext = cipher.encrypt(messageData + self.getPadding()) mediaType = node.getChild("media")["type"] if node.getChild( "media") else None encEntities.append( EncProtocolEntity(EncProtocolEntity.TYPE_SKMSG, 2, ciphertext, mediaType)) self.sendEncEntities(node, encEntities) def ensureSessionsAndSendToGroup(self, node, jids): jidsNoSession = [] for jid in jids: if not self.store.containsSession(jid.split('@')[0], 1): jidsNoSession.append(jid) if len(jidsNoSession): self.getKeysFor( jidsNoSession, lambda successJids, b: self.sendToGroupWithSessions( node, successJids)) else: self.sendToGroupWithSessions(node, jids) def sendToGroup(self, node, retryReceiptEntity=None): groupJid = node["to"] ownNumber = self.getLayerInterface( YowAuthenticationProtocolLayer).getUsername(False) ownJid = self.getLayerInterface( YowAuthenticationProtocolLayer).getUsername(True) senderKeyName = SenderKeyName(node["to"], AxolotlAddress(ownNumber, 0)) senderKeyRecord = self.store.loadSenderKey(senderKeyName) def sendToGroup(resultNode, requestEntity): groupInfo = InfoGroupsResultIqProtocolEntity.fromProtocolTreeNode( resultNode) jids = list(groupInfo.getParticipants().keys() ) #keys in py3 returns dict_keys if ownJid in jids: jids.remove(ownJid) return self.ensureSessionsAndSendToGroup(node, jids) if senderKeyRecord.isEmpty(): groupInfoIq = InfoGroupsIqProtocolEntity(groupJid) self._sendIq(groupInfoIq, sendToGroup) else: retryCount = 0 jidsNeedSenderKey = [] if retryReceiptEntity is not None: retryCount = retryReceiptEntity.getRetryCount() jidsNeedSenderKey.append(retryReceiptEntity.getRetryJid()) self.sendToGroupWithSessions(node, jidsNeedSenderKey, retryCount) def serializeToProtobuf(self, node, message=None): if node.getChild("body"): return self.serializeTextToProtobuf(node, message) elif node.getChild("media"): return self.serializeMediaToProtobuf(node.getChild("media"), message) else: raise ValueError("No body or media nodes found") def serializeTextToProtobuf(self, node, message=None): m = message or Message() m.conversation = node.getChild("body").getData() return m def serializeMediaToProtobuf(self, mediaNode, message=None): if mediaNode["type"] == "image": return self.serializeImageToProtobuf(mediaNode, message) if mediaNode["type"] == "location": return self.serializeLocationToProtobuf(mediaNode, message) if mediaNode["type"] == "vcard": return self.serializeContactToProtobuf(mediaNode, message) return None def serializeLocationToProtobuf(self, mediaNode, message=None): m = message or Message() location_message = LocationMessage() location_message.degress_latitude = float(mediaNode["latitude"]) location_message.degress_longitude = float(mediaNode["longitude"]) location_message.address = mediaNode["name"] location_message.name = mediaNode["name"] location_message.url = mediaNode["url"] m.location_message.MergeFrom(location_message) return m def serializeContactToProtobuf(self, mediaNode, message=None): vcardNode = mediaNode.getChild("vcard") m = message or Message() contact_message = ContactMessage() contact_message.display_name = vcardNode["name"] m.vcard = vcardNode.getData() m.contact_message.MergeFrom(contact_message) return m def serializeImageToProtobuf(self, mediaNode, message=None): m = message or Message() image_message = ImageMessage() image_message.url = mediaNode["url"] image_message.width = int(mediaNode["width"]) image_message.height = int(mediaNode["height"]) image_message.mime_type = mediaNode["mimetype"] image_message.file_sha256 = mediaNode["filehash"] image_message.file_length = int(mediaNode["size"]) image_message.caption = mediaNode["caption"] or "" image_message.jpeg_thumbnail = mediaNode.getData() m.image_message.MergeFrom(image_message) return m def serializeUrlToProtobuf(self, node, message=None): pass def serializeDocumentToProtobuf(self, node, message=None): pass def serializeSenderKeyDistributionMessageToProtobuf( self, groupId, senderKeyDistributionMessage, message=None): m = message or Message() sender_key_distribution_message = ProtoSenderKeyDistributionMessage() sender_key_distribution_message.groupId = groupId sender_key_distribution_message.axolotl_sender_key_distribution_message = senderKeyDistributionMessage.serialize( ) m.sender_key_distribution_message.MergeFrom( sender_key_distribution_message) # m.conversation = text return m ### def getSessionCipher(self, recipientId): if recipientId in self.sessionCiphers: sessionCipher = self.sessionCiphers[recipientId] else: sessionCipher = SessionCipher(self.store, self.store, self.store, self.store, recipientId, 1) self.sessionCiphers[recipientId] = sessionCipher return sessionCipher def getGroupCipher(self, groupId, senderId): senderKeyName = SenderKeyName(groupId, AxolotlAddress(senderId, 0)) if senderKeyName in self.groupCiphers: groupCipher = self.groupCiphers[senderKeyName] else: groupCipher = GroupCipher(self.store, senderKeyName) self.groupCiphers[senderKeyName] = groupCipher return groupCipher
class AxolotlManager(object): COUNT_GEN_PREKEYS = 812 THRESHOLD_REGEN = 10 MAX_SIGNED_PREKEY_ID = 16777215 def __init__(self, store, username): """ :param store: :type store: AxolotlStore :param username: :type username: str """ self._username = username # type: str self._store = store # type: LiteAxolotlStore self._identity = self._store.getIdentityKeyPair() # type: IdentityKeyPair self._registration_id = self._store.getLocalRegistrationId() # type: int | None assert self._registration_id is not None assert self._identity is not None self._group_session_builder = GroupSessionBuilder(self._store) # type: GroupSessionBuilder self._session_ciphers = {} # type: dict[str, SessionCipher] self._group_ciphers = {} # type: dict[str, GroupCipher] logger.debug("Initialized AxolotlManager [username=%s, db=%s]" % (self._username, store)) @property def registration_id(self): return self._registration_id @property def identity(self): return self._identity def level_prekeys(self, force=False): logger.debug("level_prekeys(force=%s)" % force) pending_prekeys = self._store.loadPreKeys() logger.debug("len(pending_prekeys) = %d" % len(pending_prekeys)) if force or len(pending_prekeys) < self.THRESHOLD_REGEN: count_gen = self.COUNT_GEN_PREKEYS - len(pending_prekeys) logger.info("Generating %d prekeys" % count_gen) ## arbitrary, should keep track of generated prekey ids and create from there prekeys = KeyHelper.generatePreKeys(KeyHelper.getRandomSequence(2**32 // 2), count_gen) logger.info("Storing %d prekeys" % len(prekeys)) for i in range(0, len(prekeys)): key = prekeys[i] if logger.level <= logging.DEBUG: sys.stdout.write("Storing prekey %d/%d \r" % (i + 1, len(prekeys))) sys.stdout.flush() self._store.storePreKey(key.getId(), key) return prekeys return [] def load_unsent_prekeys(self): logger.debug("load_unsent_prekeys") unsent = self._store.preKeyStore.loadUnsentPendingPreKeys() if len(unsent) > 0: logger.info("Loaded %d unsent prekeys" % len(unsent)) return unsent def set_prekeys_as_sent(self, prekeyIds): """ :param prekeyIds: :type prekeyIds: list :return: :rtype: """ logger.debug("set_prekeys_as_sent(prekeyIds=[%d prekeyIds])" % len(prekeyIds)) self._store.preKeyStore.setAsSent([prekey.getId() for prekey in prekeyIds]) def generate_signed_prekey(self): logger.debug("generate_signed_prekey") latest_signed_prekey = self.load_latest_signed_prekey(generate=False) if latest_signed_prekey is not None: if latest_signed_prekey.getId() == self.MAX_SIGNED_PREKEY_ID: new_signed_prekey_id = (self.MAX_SIGNED_PREKEY_ID / 2) + 1 else: new_signed_prekey_id = latest_signed_prekey.getId() + 1 else: new_signed_prekey_id = 0 signed_prekey = KeyHelper.generateSignedPreKey(self._identity, new_signed_prekey_id) self._store.storeSignedPreKey(signed_prekey.getId(), signed_prekey) return signed_prekey def load_latest_signed_prekey(self, generate=False): logger.debug("load_latest_signed_prekey") signed_prekeys = self._store.loadSignedPreKeys() if len(signed_prekeys): return signed_prekeys[-1] return self.generate_signed_prekey() if generate else None def _get_session_cipher(self, recipientid): logger.debug("get_session_cipher(recipientid=%s)" % recipientid) if recipientid in self._session_ciphers: session_cipher = self._session_ciphers[recipientid] else: session_cipher= SessionCipher(self._store, self._store, self._store, self._store, recipientid, 1) self._session_ciphers[recipientid] = session_cipher return session_cipher def _get_group_cipher(self, groupid, username): logger.debug("get_group_cipher(groupid=%s, username=%s)" % (groupid, username)) senderkeyname = SenderKeyName(groupid, AxolotlAddress(username, 0)) if senderkeyname in self._group_ciphers: group_cipher = self._group_ciphers[senderkeyname] else: group_cipher = GroupCipher(self._store.senderKeyStore, senderkeyname) self._group_ciphers[senderkeyname] = group_cipher return group_cipher def _generate_random_padding(self): logger.debug("generate_random_padding") num = random.randint(1,255) return bytes(bytearray([num] * num)) def _unpad(self, data): padding_byte = data[-1] if type(data[-1]) is int else ord(data[-1]) # bec inconsistent API? padding = padding_byte & 0xFF return data[:-padding] def encrypt(self, recipient_id, message): # to avoid the hassle of encoding issues and associated unnecessary crashes, # don't log the message content. # see https://github.com/tgalal/yowsup/issues/2732 logger.debug("encrypt(recipientid=%s, message=[omitted])" % recipient_id) """ :param recipient_id: :type recipient_id: str :param data: :type data: bytes :return: :rtype: """ cipher = self._get_session_cipher(recipient_id) return cipher.encrypt(message + self._generate_random_padding()) def decrypt_pkmsg(self, senderid, data, unpad): logger.debug("decrypt_pkmsg(senderid=%s, data=(omitted), unpad=%s)" % (senderid, unpad)) pkmsg = PreKeyWhisperMessage(serialized=data) try: plaintext = self._get_session_cipher(senderid).decryptPkmsg(pkmsg) return self._unpad(plaintext) if unpad else plaintext except NoSessionException: raise exceptions.NoSessionException() except InvalidKeyIdException: raise exceptions.InvalidKeyIdException() except InvalidMessageException: raise exceptions.InvalidMessageException() except DuplicateMessageException: raise exceptions.DuplicateMessageException() def decrypt_msg(self, senderid, data, unpad): logger.debug("decrypt_msg(senderid=%s, data=[omitted], unpad=%s)" % (senderid, unpad)) msg = WhisperMessage(serialized=data) try: plaintext = self._get_session_cipher(senderid).decryptMsg(msg) return self._unpad(plaintext) if unpad else plaintext except NoSessionException: raise exceptions.NoSessionException() except InvalidKeyIdException: raise exceptions.InvalidKeyIdException() except InvalidMessageException: raise exceptions.InvalidMessageException() except DuplicateMessageException: raise exceptions.DuplicateMessageException() def group_encrypt(self, groupid, message): """ :param groupid: :type groupid: str :param message: :type message: bytes :return: :rtype: """ # to avoid the hassle of encoding issues and associated unnecessary crashes, # don't log the message content. # see https://github.com/tgalal/yowsup/issues/2732 logger.debug("group_encrypt(groupid=%s, message=[omitted])" % groupid) group_cipher = self._get_group_cipher(groupid, self._username) return group_cipher.encrypt(message + self._generate_random_padding()) def group_decrypt(self, groupid, participantid, data): logger.debug("group_decrypt(groupid=%s, participantid=%s, data=[omitted])" % (groupid, participantid)) group_cipher = self._get_group_cipher(groupid, participantid) try: plaintext = group_cipher.decrypt(data) plaintext = self._unpad(plaintext) return plaintext except NoSessionException: raise exceptions.NoSessionException() except DuplicateMessageException: raise exceptions.DuplicateMessageException() def group_create_skmsg(self, groupid): logger.debug("group_create_skmsg(groupid=%s)" % groupid) senderKeyName = SenderKeyName(groupid, AxolotlAddress(self._username, 0)) return self._group_session_builder.create(senderKeyName) def group_create_session(self, groupid, participantid, skmsgdata): """ :param groupid: :type groupid: str :param participantid: :type participantid: str :param skmsgdata: :type skmsgdata: bytearray :return: :rtype: """ logger.debug("group_create_session(groupid=%s, participantid=%s, skmsgdata=[omitted])" % (groupid, participantid)) senderKeyName = SenderKeyName(groupid, AxolotlAddress(participantid, 0)) senderkeydistributionmessage = SenderKeyDistributionMessage(serialized=skmsgdata) self._group_session_builder.process(senderKeyName, senderkeydistributionmessage) def create_session(self, username, prekeybundle, autotrust=False): """ :param username: :type username: str :param prekeybundle: :type prekeybundle: PreKeyBundle :return: :rtype: """ logger.debug("create_session(username=%s, prekeybundle=[omitted], autotrust=%s)" % (username, autotrust)) session_builder = SessionBuilder(self._store, self._store, self._store, self._store, username, 1) try: session_builder.processPreKeyBundle(prekeybundle) except UntrustedIdentityException as ex: if autotrust: self.trust_identity(ex.getName(), ex.getIdentityKey()) else: raise exceptions.UntrustedIdentityException(ex.getName(), ex.getIdentityKey()) def session_exists(self, username): """ :param username: :type username: str :return: :rtype: """ logger.debug("session_exists(%s)?" % username) return self._store.containsSession(username, 1) def load_senderkey(self, groupid): logger.debug("load_senderkey(groupid=%s)" % groupid) senderkeyname = SenderKeyName(groupid, AxolotlAddress(self._username, 0)) return self._store.loadSenderKey(senderkeyname) def trust_identity(self, recipientid, identitykey): logger.debug("trust_identity(recipientid=%s, identitykey=[omitted])" % recipientid) self._store.saveIdentity(recipientid, identitykey)
class AxolotlSendLayer(AxolotlBaseLayer): MAX_SENT_QUEUE = 100 def __init__(self): super(AxolotlSendLayer, self).__init__() self.sessionCiphers = {} self.groupSessionBuilder = None self.groupCiphers = {} ''' Sent messages will be put in skalti entQueue until we receive a receipt for them. This is for handling retry receipts which requires re-encrypting and resend of the original message As the receipt for a sent message might arrive at a different yowsup instance, ideally the original message should be fetched from a persistent storage. Therefore, if the original message is not in sentQueue for any reason, we will notify the upper layers and let them handle it. ''' self.sentQueue = [] def onNewStoreSet(self, store): if store is not None: self.groupSessionBuilder = GroupSessionBuilder(store) def __str__(self): return "Axolotl Layer" def send(self, node): if node.tag == "message" and node["to"] not in self.skipEncJids and not node.getChild("enc") and (not node.getChild("media") or node.getChild("media")["mediakey"]): self.processPlaintextNodeAndSend(node) # elif node.tag == "iq" and node["xmlns"] == "w:m": # mediaNode = node.getChild("media") # if mediaNode and mediaNode["type"] == "image": # iqNode = IqProtocolEntity.fromProtocolTreeNode(node).toProtocolTreeNode() # iqNode.addChild(ProtocolTreeNode( # "encr_media", { # "type": mediaNode["type"], # "hash": mediaNode["hash"] # } # )) # self.toLower(iqNode) else: self.toLower(node) def receive(self, protocolTreeNode): if not self.processIqRegistry(protocolTreeNode): if protocolTreeNode.tag == "receipt": ''' Going to keep all group message enqueued, as we get receipts from each participant So can't just remove it on first receipt. Therefore, the MAX queue length mechanism should better be working ''' messageNode = self.getEnqueuedMessageNode(protocolTreeNode["id"], protocolTreeNode["participant"] is not None) if not messageNode: logger.debug("Axolotl layer does not have the message, bubbling it upwards") self.toUpper(protocolTreeNode) elif protocolTreeNode["type"] == "retry": logger.info("Got retry to for message %s, and Axolotl layer has the message" % protocolTreeNode["id"]) retryReceiptEntity = RetryIncomingReceiptProtocolEntity.fromProtocolTreeNode(protocolTreeNode) self.toLower(retryReceiptEntity.ack().toProtocolTreeNode()) self.getKeysFor( [protocolTreeNode["participant"] or protocolTreeNode["from"]], lambda successJids, b: self.processPlaintextNodeAndSend(messageNode, retryReceiptEntity) if len(successJids) == 1 else None ) else: #not interested in any non retry receipts, bubble upwards self.toUpper(protocolTreeNode) def processPlaintextNodeAndSend(self, node, retryReceiptEntity = None): recipient_id = node["to"].split('@')[0] isGroup = "-" in recipient_id if node.getChild("media"): self.toLower(node) # skip media enc for now, groups and non groups elif isGroup: self.sendToGroup(node, retryReceiptEntity) elif self.store.containsSession(recipient_id, 1): self.sendToContact(node) else: self.getKeysFor([node["to"]], lambda successJids, b: self.sendToContact(node) if len(successJids) == 1 else self.toLower(node), lambda: self.toLower(node)) def getPadding(self): num = randint(1,255) return bytearray([num] * num) def groupSendSequence(self): """ check if senderkeyrecord exists no: - create, - get group jids from info request - for each jid without a session, get keys to create the session - send message with dist key for all participants yes: - send skmsg without any dist key received retry for a participant - request participants keys - send message with dist key only + conversation, only for this participat :return: """ def enqueueSent(self, node): if len(self.sentQueue) >= self.__class__.MAX_SENT_QUEUE: logger.warn("Discarding queued node without receipt") self.sentQueue.pop(0) self.sentQueue.append(node) def getEnqueuedMessageNode(self, messageId, keepEnqueued = False): for i in range(0, len(self.sentQueue)): if self.sentQueue[i]["id"] == messageId: if keepEnqueued: return self.sentQueue[i] return self.sentQueue.pop(i) def sendEncEntities(self, node, encEntities): mediaType = None messageEntity = EncryptedMessageProtocolEntity(encEntities, "text" if not mediaType else "media", _id=node["id"], to=node["to"], notify=node["notify"], timestamp=node["timestamp"], participant=node["participant"], offline=node["offline"], retry=node["retry"] ) self.enqueueSent(node) self.toLower(messageEntity.toProtocolTreeNode()) def sendToContact(self, node): recipient_id = node["to"].split('@')[0] cipher = self.getSessionCipher(recipient_id) messageData = self.serializeToProtobuf(node).SerializeToString() + self.getPadding() ciphertext = cipher.encrypt(messageData) mediaType = node.getChild("media")["type"] if node.getChild("media") else None return self.sendEncEntities(node, [EncProtocolEntity(EncProtocolEntity.TYPE_MSG if ciphertext.__class__ == WhisperMessage else EncProtocolEntity.TYPE_PKMSG, 2, ciphertext.serialize(), mediaType)]) def sendToGroupWithSessions(self, node, jidsNeedSenderKey = None, retryCount=0): jidsNeedSenderKey = jidsNeedSenderKey or [] groupJid = node["to"] ownNumber = self.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(False) senderKeyName = SenderKeyName(groupJid, AxolotlAddress(ownNumber, 0)) cipher = self.getGroupCipher(groupJid, ownNumber) encEntities = [] if len(jidsNeedSenderKey): senderKeyDistributionMessage = self.groupSessionBuilder.create(senderKeyName) for jid in jidsNeedSenderKey: sessionCipher = self.getSessionCipher(jid.split('@')[0]) message = self.serializeSenderKeyDistributionMessageToProtobuf(node["to"], senderKeyDistributionMessage) if retryCount > 0: message = self.serializeToProtobuf(node, message) ciphertext = sessionCipher.encrypt(message.SerializeToString() + self.getPadding()) encEntities.append( EncProtocolEntity( EncProtocolEntity.TYPE_MSG if ciphertext.__class__ == WhisperMessage else EncProtocolEntity.TYPE_PKMSG , 2, ciphertext.serialize(), jid=jid ) ) if not retryCount: messageData = self.serializeToProtobuf(node).SerializeToString() ciphertext = cipher.encrypt(messageData + self.getPadding()) mediaType = node.getChild("media")["type"] if node.getChild("media") else None encEntities.append(EncProtocolEntity(EncProtocolEntity.TYPE_SKMSG, 2, ciphertext, mediaType)) self.sendEncEntities(node, encEntities) def ensureSessionsAndSendToGroup(self, node, jids): jidsNoSession = [] for jid in jids: if not self.store.containsSession(jid.split('@')[0], 1): jidsNoSession.append(jid) if len(jidsNoSession): self.getKeysFor(jidsNoSession, lambda successJids, b: self.sendToGroupWithSessions(node, successJids)) else: self.sendToGroupWithSessions(node, jids) def sendToGroup(self, node, retryReceiptEntity = None): groupJid = node["to"] ownNumber = self.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(False) ownJid = self.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(True) senderKeyName = SenderKeyName(node["to"], AxolotlAddress(ownNumber, 0)) senderKeyRecord = self.store.loadSenderKey(senderKeyName) def sendToGroup(resultNode, requestEntity): groupInfo = InfoGroupsResultIqProtocolEntity.fromProtocolTreeNode(resultNode) jids = list(groupInfo.getParticipants().keys()) #keys in py3 returns dict_keys jids.remove(ownJid) return self.ensureSessionsAndSendToGroup(node, jids) if senderKeyRecord.isEmpty(): groupInfoIq = InfoGroupsIqProtocolEntity(groupJid) self._sendIq(groupInfoIq, sendToGroup) else: retryCount = 0 jidsNeedSenderKey = [] if retryReceiptEntity is not None: retryCount = retryReceiptEntity.getRetryCount() jidsNeedSenderKey.append(retryReceiptEntity.getRetryJid()) self.sendToGroupWithSessions(node, jidsNeedSenderKey, retryCount) def serializeToProtobuf(self, node, message = None): if node.getChild("body"): return self.serializeTextToProtobuf(node, message) elif node.getChild("media"): return self.serializeMediaToProtobuf(node.getChild("media"), message) else: raise ValueError("No body or media nodes found") def serializeTextToProtobuf(self, node, message = None): m = message or Message() m.conversation = node.getChild("body").getData() return m def serializeMediaToProtobuf(self, mediaNode, message = None): if mediaNode["type"] == "image": return self.serializeImageToProtobuf(mediaNode, message) if mediaNode["type"] == "location": return self.serializeLocationToProtobuf(mediaNode, message) if mediaNode["type"] == "vcard": return self.serializeContactToProtobuf(mediaNode, message) return None def serializeLocationToProtobuf(self, mediaNode, message = None): m = message or Message() location_message = LocationMessage() location_message.degrees_latitude = float(mediaNode["latitude"]) location_message.degrees_longitude = float(mediaNode["longitude"]) location_message.address = mediaNode["name"] location_message.name = mediaNode["name"] location_message.url = mediaNode["url"] m.location_message.MergeFrom(location_message) return m def serializeContactToProtobuf(self, mediaNode, message = None): vcardNode = mediaNode.getChild("vcard") m = message or Message() contact_message = ContactMessage() contact_message.display_name = vcardNode["name"] m.vcard = vcardNode.getData() m.contact_message.MergeFrom(contact_message) return m def serializeImageToProtobuf(self, mediaNode, message = None): m = message or Message() image_message = ImageMessage() image_message.url = mediaNode["url"] image_message.width = int(mediaNode["width"]) image_message.height = int(mediaNode["height"]) image_message.mime_type = mediaNode["mimetype"] image_message.file_sha256 = mediaNode["filehash"] image_message.file_length = int(mediaNode["size"]) image_message.caption = mediaNode["caption"] or "" image_message.jpeg_thumbnail = mediaNode.getData() m.image_message.MergeFrom(image_message) return m def serializeUrlToProtobuf(self, node, message = None): pass def serializeDocumentToProtobuf(self, node, message = None): pass def serializeSenderKeyDistributionMessageToProtobuf(self, groupId, senderKeyDistributionMessage, message = None): m = message or Message() sender_key_distribution_message = ProtoSenderKeyDistributionMessage() sender_key_distribution_message.groupId = groupId sender_key_distribution_message.axolotl_sender_key_distribution_message = senderKeyDistributionMessage.serialize() m.sender_key_distribution_message.MergeFrom(sender_key_distribution_message) # m.conversation = text return m ### def getSessionCipher(self, recipientId): if recipientId in self.sessionCiphers: sessionCipher = self.sessionCiphers[recipientId] else: sessionCipher = SessionCipher(self.store, self.store, self.store, self.store, recipientId, 1) self.sessionCiphers[recipientId] = sessionCipher return sessionCipher def getGroupCipher(self, groupId, senderId): senderKeyName = SenderKeyName(groupId, AxolotlAddress(senderId, 0)) if senderKeyName in self.groupCiphers: groupCipher = self.groupCiphers[senderKeyName] else: groupCipher = GroupCipher(self.store, senderKeyName) self.groupCiphers[senderKeyName] = groupCipher return groupCipher
class AxolotlSendLayer(AxolotlBaseLayer): MAX_SENT_QUEUE = 100 def __init__(self): super(AxolotlSendLayer, self).__init__() self.sessionCiphers = {} self.groupSessionBuilder = None self.groupCiphers = {} ''' Sent messages will be put in skalti entQueue until we receive a receipt for them. This is for handling retry receipts which requires re-encrypting and resend of the original message As the receipt for a sent message might arrive at a different yowsup instance, ideally the original message should be fetched from a persistent storage. Therefore, if the original message is not in sentQueue for any reason, we will notify the upper layers and let them handle it. ''' self.sentQueue = [] def onNewStoreSet(self, store): if store is not None: self.groupSessionBuilder = GroupSessionBuilder(store) def __str__(self): return "Axolotl Layer" def handleEncNode(self, node): recipient_id = node["to"].split('@')[0] v2 = node["to"] if node.getChild("enc"): # media enc is only for v2 messsages if '-' in recipient_id: ## Handle Groups def getResultNodes(resultNode, requestEntity): groupInfo = InfoGroupsResultIqProtocolEntity.fromProtocolTreeNode(resultNode) jids = list(groupInfo.getParticipants().keys()) #keys in py3 returns dict_keys jids.remove(self.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(True)) jidsNoSession = [] for jid in jids: if not self.store.containsSession(jid.split('@')[0], 1): jidsNoSession.append(jid) else: pass if len(jidsNoSession): self.getKeysFor(jidsNoSession, lambda successJids, b: self.sendToGroupWithSessions(node, successJids)) else: self.sendToGroupWithSessions(node, jids) groupInfoIq = InfoGroupsIqProtocolEntity(Jid.normalize(node["to"])) self._sendIq(groupInfoIq, getResultNodes) else: messageData = self.serializeToProtobuf(node) if messageData: if not self.store.containsSession(recipient_id, 1): def on_get_keys(successJids,b): print(successJids) if len(successJids) == 1: self.sendToContact(node) else: self.toLower(node) self.getKeysFor([node["to"]],on_get_keys, lambda: self.toLower(node)) else : sessionCipher = self.getSessionCipher(recipient_id) messageData = messageData.SerializeToString() + self.getPadding() ciphertext = sessionCipher.encrypt(messageData) mediaType = node.getChild("enc")["type"] if node.getChild("enc") else None encEntity = EncryptedMessageProtocolEntity( [ EncProtocolEntity( EncProtocolEntity.TYPE_MSG if ciphertext.__class__ == WhisperMessage else EncProtocolEntity.TYPE_PKMSG, 2 if v2 else 1, ciphertext.serialize(), mediaType)], "text" if not mediaType else "media", _id=node["id"], to=node["to"], notify=node["notify"], timestamp=node["timestamp"], participant=node["participant"], offline=node["offline"], retry=node["retry"] ) print(encEntity.toProtocolTreeNode()) self.toLower(encEntity.toProtocolTreeNode()) else: # case of unserializable messages (audio, video) ? self.toLower(node) else: self.toLower(node) def send(self, node): print("SEND: %s" % node["type"]) print(node.tag) #print(node) # if node.tag == "message" and node["to"] not in self.skipEncJids and not node.getChild("enc") or (node.getChild("media") and node.getChild("media")["mediakey"]): if node.tag == "message" and node["to"] not in self.skipEncJids and not node.getChild("enc"): self.processPlaintextNodeAndSend(node) # elif node.tag == "iq" and node["xmlns"] == "w:m": # mediaNode = node.getChild("media") # if mediaNode and mediaNode["type"] == "image": # iqNode = IqProtocolEntity.fromProtocolTreeNode(node).toProtocolTreeNode() # iqNode.addChild(ProtocolTreeNode( # "encr_media", { # "type": mediaNode["type"], # "hash": mediaNode["hash"] # } # )) # self.toLower(iqNode) elif node.getChild("enc"): self.handleEncNode(node) else: self.toLower(node) def receive(self, protocolTreeNode): if not self.processIqRegistry(protocolTreeNode): if protocolTreeNode.tag == "receipt": ''' Going to keep all group message enqueued, as we get receipts from each participant So can't just remove it on first receipt. Therefore, the MAX queue length mechanism should better be working ''' messageNode = self.getEnqueuedMessageNode(protocolTreeNode["id"], protocolTreeNode["participant"] is not None) if not messageNode: logger.debug("Axolotl layer does not have the message, bubbling it upwards") self.toUpper(protocolTreeNode) elif protocolTreeNode["type"] == "retry": logger.info("Got retry to for message %s, and Axolotl layer has the message" % protocolTreeNode["id"]) retryReceiptEntity = RetryIncomingReceiptProtocolEntity.fromProtocolTreeNode(protocolTreeNode) self.toLower(retryReceiptEntity.ack().toProtocolTreeNode()) self.getKeysFor( [protocolTreeNode["participant"] or protocolTreeNode["from"]], lambda successJids, b: self.processPlaintextNodeAndSend(messageNode, retryReceiptEntity) if len(successJids) == 1 else None ) else: #not interested in any non retry receipts, bubble upwards self.toUpper(protocolTreeNode) def processPlaintextNodeAndSend(self, node, retryReceiptEntity = None): recipient_id = node["to"].split('@')[0] isGroup = "-" in recipient_id if isGroup: self.sendToGroup(node, retryReceiptEntity) elif self.store.containsSession(recipient_id, 1): self.sendToContact(node) else: self.getKeysFor([node["to"]], lambda successJids, b: self.sendToContact(node) if len(successJids) == 1 else self.toLower(node), lambda: self.toLower(node)) def getPadding(self): num = randint(1,255) return bytearray([num] * num) def groupSendSequence(self): pass """ check if senderkeyrecord exists no: - create, - get group jids from info request - for each jid without a session, get keys to create the session - send message with dist key for all participants yes: - send skmsg without any dist key received retry for a participant - request participants keys - send message with dist key only + conversation, only for this participat :return: """ def enqueueSent(self, node): if len(self.sentQueue) >= self.__class__.MAX_SENT_QUEUE: logger.warn("Discarding queued node without receipt") self.sentQueue.pop(0) self.sentQueue.append(node) def getEnqueuedMessageNode(self, messageId, keepEnqueued = False): for i in range(0, len(self.sentQueue)): if self.sentQueue[i]["id"] == messageId: if keepEnqueued: return self.sentQueue[i] return self.sentQueue.pop(i) def sendEncEntities(self, node, encEntities): mediaType = None messageEntity = EncryptedMessageProtocolEntity(encEntities, "text" if not mediaType else "media", _id=node["id"], to=node["to"], notify=node["notify"], timestamp=node["timestamp"], participant=node["participant"], offline=node["offline"], retry=node["retry"] ) self.enqueueSent(node) self.toLower(messageEntity.toProtocolTreeNode()) def sendToContact(self, node): recipient_id = node["to"].split('@')[0] cipher = self.getSessionCipher(recipient_id) messageData = self.serializeToProtobuf(node).SerializeToString() + self.getPadding() ciphertext = cipher.encrypt(messageData) mediaType = node.getChild("media")["type"] if node.getChild("media") else None return self.sendEncEntities(node, [EncProtocolEntity(EncProtocolEntity.TYPE_MSG if ciphertext.__class__ == WhisperMessage else EncProtocolEntity.TYPE_PKMSG, 2, ciphertext.serialize(), mediaType)]) def sendToGroupWithSessions(self, node, jidsNeedSenderKey = None, retryCount=0): jidsNeedSenderKey = jidsNeedSenderKey or [] groupJid = node["to"] ownNumber = self.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(False) senderKeyName = SenderKeyName(groupJid, AxolotlAddress(ownNumber, 0)) cipher = self.getGroupCipher(groupJid, ownNumber) encEntities = [] if len(jidsNeedSenderKey): senderKeyDistributionMessage = self.groupSessionBuilder.create(senderKeyName) for jid in jidsNeedSenderKey: sessionCipher = self.getSessionCipher(jid.split('@')[0]) message = self.serializeSenderKeyDistributionMessageToProtobuf(node["to"], senderKeyDistributionMessage) if retryCount > 0: message = self.serializeToProtobuf(node, message) ciphertext = sessionCipher.encrypt(message.SerializeToString() + self.getPadding()) encEntities.append( EncProtocolEntity( EncProtocolEntity.TYPE_MSG if ciphertext.__class__ == WhisperMessage else EncProtocolEntity.TYPE_PKMSG , 2, ciphertext.serialize(), jid=jid ) ) if not retryCount: messageData = self.serializeToProtobuf(node).SerializeToString() ciphertext = cipher.encrypt(messageData + self.getPadding()) mediaType = node.getChild("media")["type"] if node.getChild("media") else None encEntities.append(EncProtocolEntity(EncProtocolEntity.TYPE_SKMSG, 2, ciphertext, mediaType)) self.sendEncEntities(node, encEntities) def ensureSessionsAndSendToGroup(self, node, jids): jidsNoSession = [] for jid in jids: if not self.store.containsSession(jid.split('@')[0], 1): jidsNoSession.append(jid) if len(jidsNoSession): self.getKeysFor(jidsNoSession, lambda successJids, b: self.sendToGroupWithSessions(node, successJids)) else: self.sendToGroupWithSessions(node, jids) def sendToGroup(self, node, retryReceiptEntity = None): groupJid = node["to"] ownNumber = self.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(False) ownJid = self.getLayerInterface(YowAuthenticationProtocolLayer).getUsername(True) senderKeyName = SenderKeyName(node["to"], AxolotlAddress(ownNumber, 0)) senderKeyRecord = self.store.loadSenderKey(senderKeyName) def sendToGroup(resultNode, requestEntity): groupInfo = InfoGroupsResultIqProtocolEntity.fromProtocolTreeNode(resultNode) jids = list(groupInfo.getParticipants().keys()) #keys in py3 returns dict_keys if ownJid in jids: jids.remove(ownJid) return self.ensureSessionsAndSendToGroup(node, jids) if senderKeyRecord.isEmpty(): groupInfoIq = InfoGroupsIqProtocolEntity(groupJid) self._sendIq(groupInfoIq, sendToGroup) else: retryCount = 0 jidsNeedSenderKey = [] if retryReceiptEntity is not None: retryCount = retryReceiptEntity.getRetryCount() jidsNeedSenderKey.append(retryReceiptEntity.getRetryJid()) self.sendToGroupWithSessions(node, jidsNeedSenderKey, retryCount) def serializeToProtobuf(self, node, message = None): if node.getChild("body"): return self.serializeTextToProtobuf(node, message) elif node.getChild("enc"): return self.serializeMediaToProtobuf(node.getChild("enc"), message) else: raise ValueError("No body or media nodes found") def serializeTextToProtobuf(self, node, message = None): m = message or Message() m.conversation = node.getChild("body").getData() return m def serializeMediaToProtobuf(self, mediaNode, message = None): if mediaNode["type"] == "image": return self.serializeImageToProtobuf(mediaNode, message) if mediaNode["type"] == "location": return self.serializeLocationToProtobuf(mediaNode, message) if mediaNode["type"] == "vcard": return self.serializeContactToProtobuf(mediaNode, message) if mediaNode["type"] == "video": return self.serializeVideoToProtobuf(mediaNode, message) if mediaNode["type"] == "audio": return self.serializeAudioToProtobuf(mediaNode, message) if mediaNode["type"] == "document": return self.serializeDocumentToProtobuf(mediaNode, message) return None def serializeLocationToProtobuf(self, mediaNode, message = None): m = message or Message() location_message = LocationMessage() location_message.degrees_latitude = float(mediaNode["latitude"]) location_message.degrees_longitude = float(mediaNode["longitude"]) location_message.name = mediaNode["name"] location_message.address = mediaNode["address"] location_message.url = mediaNode["url"] location_message.jpeg_thumbnail = b'\377\330\377\340\000\020JFIF\000\001\001\000\000\001\000\001\000\000\377\333\000C\000\006\004\005\006\005\004\006\006\005\006\007\007\006\010\n\020\n\n\t\t\n\024\016\017\014\020\027\024\030\030\027\024\026\026\032\035%\037\032\033#\034\026\026 , #&\')*)\031\037-0-(0%()(\377\333\000C\001\007\007\007\n\010\n\023\n\n\023(\032\026\032((((((((((((((((((((((((((((((((((((((((((((((((((\377\300\000\021\010\000d\000d\003\001\"\000\002\021\001\003\021\001\377\304\000\037\000\000\001\005\001\001\001\001\001\001\000\000\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\020\000\002\001\003\003\002\004\003\005\005\004\004\000\000\001}\001\002\003\000\004\021\005\022!1A\006\023Qa\007\"q\0242\201\221\241\010#B\261\301\025R\321\360$3br\202\t\n\026\027\030\031\032%&\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\341\342\343\344\345\346\347\350\351\352\361\362\363\364\365\366\367\370\371\372\377\304\000\037\001\000\003\001\001\001\001\001\001\001\001\001\000\000\000\000\000\000\001\002\003\004\005\006\007\010\t\n\013\377\304\000\265\021\000\002\001\002\004\004\003\004\007\005\004\004\000\001\002w\000\001\002\003\021\004\005!1\006\022AQ\007aq\023\"2\201\010\024B\221\241\261\301\t#3R\360\025br\321\n\026$4\341%\361\027\030\031\032&\'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\202\203\204\205\206\207\210\211\212\222\223\224\225\226\227\230\231\232\242\243\244\245\246\247\250\251\252\262\263\264\265\266\267\270\271\272\302\303\304\305\306\307\310\311\312\322\323\324\325\326\327\330\331\332\342\343\344\345\346\347\350\351\352\362\363\364\365\366\367\370\371\372\377\332\000\014\003\001\000\002\021\003\021\000?\000\367\360H \216\010\253W\203\314\362\347A\376\260r\007\367\207Z\236\337Kv\301\235\266\217A\311\2558 \216\004\333\030\300\316y\347\232\000\307\267\323\246\227\005\207\226\276\247\257\345ZV\366\020\303\203\267{z\265K=\314P\017\3368\007\323\275gO\252;dB\273G\251\344\320\006\254\216\221\256\347`\243\324\232\250o\325\326_\263\215\314\213\270g\200k\026I\036F\334\354X\372\223N\266\227\311\235\037\260<\217Q@\016\236\346Y\377\000\3269#\320p*\032\226\352/*wA\367z\203\352;TT\000U\233\337\237\313\234\177\313E\347\375\341\301\252\325f\017\336\332M\027t\375\342\377\000Z\000\255R\332K\344\\#\366\007\237\245EJ\252X\200\240\222{\n\000\352\007#\212*\275\200\220Z\242\312\245Xq\317\245\024\000\313\213\370a\310\335\275\275\026\263\345\324\246v\0330\212\016p;\325\032(\002\305\352\0016\364\373\222\r\343\361\252\365j?\337Y\274\177\307\027\316\277N\365V\200\n\245\252jv\332lj\327,w>BF\203,\370\364\037\324\3602*\355y\237\215o\'\217V\325\244\215|\311\240\214,)\353\210\303\001\370\263\032\306\265Og\033\243\273/\302,U^I=\022\271\332\017\026\330\316!K\210\256mJ\374\236d\301vc<d\253\034}N\007\275lW\315\337\017<M\254\352\367\267\261j\353\272\004\\\206\362\202ml\343o\277\031\374\253\350\037\002\333\334^\370n\315\333\356\215\361\2537\367U\331G\327\200*(\325\224\244\343#\2470\300\323\243J5\251^\315\333_\353\310\320\253:zHn\021\243B\300\0347\246;\326\225\276\233\014|\311\231\033\337\247\345W@\n\000P\000\035\205t\236A\235\036\224\241\311\221\311\\\360\007\245^\206\030\341\030\215\002\375*J\202\340\\7\020\264h=NI\240\t\362=h\254\207\323\'v,\362\253\037RM\024\001\233E\024P\004\226\322\3713+\365\000\362=Gz[\250\274\231\331G+\325O\250\244\202\t\'lD\205\275\373\n\327K\005h\342\027\007s \307\007\250\364\240\014dF\221\266\242\226>\200W\'\361\033\303\323\305f5\250\324\003\020\021\334 \344\354\317\017\370\023\317\261\366\257Q\2164\215v\306\241G\260\250\257\226\336KYa\273\332`\225\n:\267FR0G\345Y\324\246\252E\305\2358<L\260\265\243V=?#\347\335\022\316}kV\266\323\255I\022N\330/\214\371j9f?A\371\234\016\365\364\016\237g\016\237c\005\245\252\354\202\004\021\240\366\003\037\235yg\302\213S\246\\ks\310\253,\361LlRC\330!\313\037\243e\177\357\232\364+mNA)\363\376d>\203\356\326\030:\\\220\346{\263\323\317q\236\336\277\263\207\303\037\315\232\027\027\260\302Hf\313\017\341^j\204\232\254\205\277v\212\027\337\232]Y\355r\205\344\t#\214\243c\345ol\326n\017\241\256\263\3037\264\373\277\264\243n\0002\236\202\242\273\277ky\314f F2\016z\325\r2o*\351r~V\371M^\326a\335\022\312:\247\007\351@\020\377\000k?\374\362_\316\212\315\242\200&KY^V\215\020\222\247\004\366\374\353J\333KE\301\234\357?\335\035+@\000\243\000\000\005U\270\277\206,\200\333\333\321\177\306\200-*\252(U\000\001\330SL\210$\t\274o?\303\236k\026\343P\232\\\205>Z\372/_\316\253\300\314\263+\251\371\201\316M\000jjwS@\312\261\200\252\303\357u5\220\356\316\305\235\213\023\334\3235\ry\365\0376-#N\222\340\303\037\236e\271&\004e\347\356dnc\301\035\000\351\3174\252\351\"$\221\034\307\"\207C\354FE\000Aii\005\247\235\366x\302y\3224\322rN\347=O?J\261E\024%a\266\333\273\027lR\354K\240\315\022\222@\007\004g\275jZ\231X\233yIf\333\230\356\024p\353\357\357YUn\302\361\255\233\r\223\021\352==\350\020\267\020e\330\000\026a\311Q\321\275\305jZ\310.\255>~I\033X{\322\334\304\263\302Yq\234d0\252\366\0222\344L\273X\235\244\364\311\354h\003&h\314R\274m\325N(\255\313\233(\347\223{d\034c\212(\003\036\342\356i\363\275\360\277\335\034\n\202\237<~T\316\237\3358\246P\001@$\034\203\203E\024\000\3504{]N\372\346MDM9\330\242\022\035\243T\214\365A\265\271%\201-\236\271Q\332\245\275\200[\316cU\013\030\003`\003\000/`?\225U\236[\210`2Y\227\363U\320\266\305\014\346=\343xPz\235\271\251f\325$\324o\326\030mc\205#\033\237\355R\205\231\227\276\330\207?\213c\350h\001\264QJ\212\316\330E,}\000\315\000%\025i,d$\tYc\366\352\337\220\251f\216\332\320`\2034\336\207\240\372\320\005\235\032b\360\264m\316\316\237J\255so\034S\267\2356\0279T^N?\245:\302\362F\272Eb\0263\300U\030\002\227[\217\022G\'\250\301\240\007.\252\025B\210\230\201\300%\2714V]\024\001=\351\335u!\3655\005\024P\200(\240\014\234\016MYKG\300i\230D\247\373\335O\320P\005t;]O\241\315f\306\242\326{{I\305\230\002\340\335\254\261\0267\023\374\304\200AP\252I;r[\234\020\007<ti\014p\256\355\201\177\333\237\372-bx\226(g\214\\\211\256w\250X\\#\210\204\273\235B\006l\035\252\254s\270\014\216H4\001{J\236\322\365\345\362U\344)\265\200f\013\224#!\210\352;\214\036\340\212\2755\304Q\026U\220\262\347\204\210m\037\211\254\233v\232\003.\231 \200\013H\243q\366b\333\000b\300+\006$\356\371I\311<\346\237@\026\036\362B\n\306\004J{\'\004\375MW\242\212\000Tb\216\254:\251\310\255+\353\250n\222hP\376\366\022\033\036\240\216\243\363\254\300\t \000I=\205X\266\263H\265\010<\330\360\323\304\353\363u\014;\376T\320\025\350\240\202\t\007\250\242\220\005Me\022\315:\243\222\001\364\242\212\000\321E\021^\210!\001\0063\270\014\261\374MG\250\334\274S\225\214*\234}\375\2777\347E\024\001\232\314\314IbI=\311\246\220\254\254\216\252\350\300\253+\014\206\007\250#\270\242\212\000\3200[i~\032\270k\013Kx\025b2\371q\306\025K\001\234\2201\351Xz%\343\352\032-\215\344\312\213,\360\207`\203\013\236zQE\000]\251\254\342Y\256\025\034\220\017\245\024P\005\233\311\r\244\206+p\2501\313\001\363\037\306\244\322cY\3674\243sF\373\224\222x$b\212(\002\225\362\205\273\224\016\233\215\024Q@\037\377\331' m.location_message.MergeFrom(location_message) return m def serializeContactToProtobuf(self, mediaNode, message = None): vcardNode = mediaNode.getChild("vcard") m = message or Message() contact_message = ContactMessage() contact_message.display_name = vcardNode["name"] m.contact_message.vcard = vcardNode.getData() m.contact_message.MergeFrom(contact_message) return m def serializeImageToProtobuf(self, mediaNode, message = None): m = message or Message() image_message = ImageMessage() image_message.url = mediaNode["url"] image_message.width = int(mediaNode["width"]) image_message.height = int(mediaNode["height"]) image_message.mime_type = mediaNode["mimetype"] image_message.file_sha256 = binascii.unhexlify(mediaNode["filehash"].encode()) image_message.file_length = int(mediaNode["size"]) image_message.media_key = binascii.unhexlify(mediaNode["anu"]) #image_message.file_enc_sha256 = binascii.unhexlify(mediaNode["file_enc_sha256"]) image_message.caption = mediaNode["caption"] or "" image_message.jpeg_thumbnail = mediaNode.getData() m.image_message.MergeFrom(image_message) return m def serializeVideoToProtobuf(self,mediaNode, message = None): m = message or Message() video_message = VideoMessage() video_message.url = mediaNode["url"] #video_message.width = int(mediaNode["width"]) #video_message.height = int(mediaNode["height"]) video_message.mime_type = mediaNode["mimetype"] video_message.file_sha256 = binascii.unhexlify(mediaNode["filehash"].encode()) video_message.file_length = int(mediaNode["size"]) video_message.media_key = binascii.unhexlify(mediaNode["anu"]) #video_message.file_enc_sha256 = binascii.unhexlify(mediaNode["file_enc_sha256"]) video_message.caption = mediaNode["caption"] or "" video_message.jpeg_thumbnail = mediaNode.getData() video_message.duration = int(mediaNode["duration"]) m.video_message.MergeFrom(video_message) return m def serializeAudioToProtobuf(self, mediaNode, message=None): m = message or Message() audio_message = AudioMessage() audio_message.url = mediaNode["url"] if 'ogg' in mediaNode["mimetype"]: audio_message.mime_type = "audio/ogg; codecs=opus" else: audio_message.mime_type = mediaNode["mimetype"] audio_message.file_sha256 = binascii.unhexlify(mediaNode["filehash"].encode()) audio_message.file_length = int(mediaNode["size"]) audio_message.media_key = binascii.unhexlify(mediaNode["anu"]) #audio_message.file_enc_sha256 = binascii.unhexlify(mediaNode["file_enc_sha256"]) audio_message.duration = int(mediaNode["duration"]) audio_message.unk = 0; m.audio_message.MergeFrom(audio_message) return m def serializeUrlToProtobuf(self, node, message = None): pass def serializeDocumentToProtobuf(self, mediaNode, message=None): m = message or Message() document_message = DocumentMessage() document_message.url = mediaNode["url"] document_message.mime_type = mediaNode["mimetype"] document_message.title = mediaNode["file"] document_message.file_sha256 = binascii.unhexlify(mediaNode["filehash"].encode()) document_message.file_length = int(mediaNode["size"]) document_message.media_key = binascii.unhexlify(mediaNode["anu"]) document_message.page_count = int(mediaNode["pageCount"]); if "pdf" in mediaNode["mimetype"]: document_message.jpeg_thumbnail = mediaNode.getData() else: document_message.jpeg_thumbnail = b"" m.document_message.MergeFrom(document_message) return m def serializeSenderKeyDistributionMessageToProtobuf(self, groupId, senderKeyDistributionMessage, message = None): m = message or Message() sender_key_distribution_message = ProtoSenderKeyDistributionMessage() sender_key_distribution_message.groupId = groupId sender_key_distribution_message.axolotl_sender_key_distribution_message = senderKeyDistributionMessage.serialize() m.sender_key_distribution_message.MergeFrom(sender_key_distribution_message) # m.conversation = text return m ### def getSessionCipher(self, recipientId): if recipientId in self.sessionCiphers: sessionCipher = self.sessionCiphers[recipientId] else: sessionCipher = SessionCipher(self.store, self.store, self.store, self.store, recipientId, 1) self.sessionCiphers[recipientId] = sessionCipher return sessionCipher def getGroupCipher(self, groupId, senderId): senderKeyName = SenderKeyName(groupId, AxolotlAddress(senderId, 0)) if senderKeyName in self.groupCiphers: groupCipher = self.groupCiphers[senderKeyName] else: groupCipher = GroupCipher(self.store, senderKeyName) self.groupCiphers[senderKeyName] = groupCipher return groupCipher
class AxolotlSendLayer(AxolotlBaseLayer): MAX_SENT_QUEUE = 100 def __init__(self): super(AxolotlSendLayer, self).__init__() self.sessionCiphers = {} self.groupSessionBuilder = None self.groupCiphers = {} # Sent messages will be put in skalti entQueue until we receive a receipt for them. # This is for handling retry receipts which requires re-encrypting and resend of the original message # As the receipt for a sent message might arrive at a different yowsup instance, # ideally the original message should be fetched from a persistent storage. # Therefore, if the original message is not in sentQueue for any reason, we will # notify the upper layers and let them handle it. self.sentQueue = [] def onNewStoreSet(self, store): if store is not None: self.groupSessionBuilder = GroupSessionBuilder(store) def __str__(self): return "Axolotl Layer" def handleEncNode(self, node): recipient_id = node["to"].split('@')[0] v2 = node["to"] if node.getChild("enc"): # media enc is only for v2 messsages if '-' in recipient_id: # Handle Groups def getResultNodes(resultNode, _requestEntity): groupInfo = InfoGroupsResultIqProtocolEntity.fromProtocolTreeNode( resultNode) jids = list(groupInfo.getParticipants().keys() ) # keys in py3 returns dict_keys jids.remove( self.getLayerInterface( YowAuthenticationProtocolLayer).getUsername(True)) jidsNoSession = [] for jid in jids: if not self.store.containsSession( jid.split('@')[0], 1): jidsNoSession.append(jid) if len(jidsNoSession): self.getKeysFor( jidsNoSession, lambda successJids, b: self. sendToGroupWithSessions(node, successJids)) else: self.sendToGroupWithSessions(node, jids) groupInfoIq = InfoGroupsIqProtocolEntity( Jid.normalize(node["to"])) self._sendIq(groupInfoIq, getResultNodes) else: messageData = self.serializeToProtobuf(node) if messageData: if not self.store.containsSession(recipient_id, 1): def on_get_keys(successJids, _b): if len(successJids) == 1: self.sendToContact(node) else: self.toLower(node) self.getKeysFor([node["to"]], on_get_keys, lambda: self.toLower(node)) else: sessionCipher = self.getSessionCipher(recipient_id) messageData = messageData + self.getPadding() ciphertext = sessionCipher.encrypt(messageData) mediaType = node.getChild( "enc")["type"] if node.getChild("enc") else None if ciphertext.__class__ == WhisperMessage: enc_type = EncProtocolEntity.TYPE_MSG else: enc_type = EncProtocolEntity.TYPE_PKMSG encEntity = EncryptedMessageProtocolEntity( [ EncProtocolEntity(enc_type, 2 if v2 else 1, ciphertext.serialize(), mediaType) ], "text" if not mediaType else "media", _id=node["id"], to=node["to"], notify=node["notify"], timestamp=node["timestamp"], participant=node["participant"], offline=node["offline"], retry=node["retry"]) self.toLower(encEntity.toProtocolTreeNode()) else: # case of unserializable messages (audio, video) ? self.toLower(node) else: self.toLower(node) def send(self, node): if node.tag == "message" and node[ "to"] not in self.skipEncJids and not node.getChild("enc"): self.processMessageAndSend(node) elif node.getChild("enc"): self.handleEncNode(node) else: self.toLower(node) # def receive(self, protocolTreeNode): # if not self.processIqRegistry(protocolTreeNode): # if protocolTreeNode.tag == "receipt": # # Going to keep all group message enqueued, as we get receipts from each participant # # So can't just remove it on first receipt. Therefore, the MAX queue length mechanism # # should better be working # messageNode = self.getEnqueuedMessageNode(protocolTreeNode["id"], # protocolTreeNode["participant"] is not None) # if not messageNode: # logger.debug("Axolotl layer does not have the message, bubbling it upwards") # self.toUpper(protocolTreeNode) # elif protocolTreeNode["type"] == "retry": # logger.info( # "Got retry to for message %s, and Axolotl layer has the message" % protocolTreeNode["id"]) # retryReceiptEntity = RetryIncomingReceiptProtocolEntity.fromProtocolTreeNode(protocolTreeNode) # self.toLower(retryReceiptEntity.ack().toProtocolTreeNode()) # self.getKeysFor( # [protocolTreeNode["participant"] or protocolTreeNode["from"]], # lambda successJids, b: self.processMessageAndSend(messageNode, retryReceiptEntity) if len( # successJids) == 1 else None, # errorClbk=lambda errorNode, getKeysEntity: logger.error("Failed at getting keys during retry") # ) # else: # # not interested in any non retry receipts, bubble upwards # self.toUpper(protocolTreeNode) def processMessageAndSend(self, node, retryReceiptEntity=None): recipient_id = node["to"].split('@')[0] isGroup = "-" in recipient_id if isGroup: self.sendToGroup(node, retryReceiptEntity) elif self.store.containsSession(recipient_id, 1): self.sendToContact(node) else: self.getKeysFor([node["to"]], lambda successJids, b: self.sendToContact(node) if len(successJids) == 1 else self.toLower(node), lambda: self.toLower(node)) def enqueueSent(self, node): if len(self.sentQueue) >= self.__class__.MAX_SENT_QUEUE: logger.warning("Discarding queued node without receipt") self.sentQueue.pop(0) self.sentQueue.append(node) def getEnqueuedMessageNode(self, messageId, keepEnqueued=False): for i in range(0, len(self.sentQueue)): if self.sentQueue[i]["id"] == messageId: if keepEnqueued: return self.sentQueue[i] return self.sentQueue.pop(i) def sendEncEntities(self, node, encEntities): messageEntity = EncryptedMessageProtocolEntity(node, encEntities) self.enqueueSent(node) self.toLower(messageEntity.toProtocolTreeNode()) def sendToContact(self, node): recipient_id = node["to"].split('@')[0] cipher = self.getSessionCipher(recipient_id) messageData = self.serializeToProtobuf(node) + self.getPadding() ciphertext = cipher.encrypt(messageData) mediaType = node.getChild("body")["mediatype"] if node.getChild( "body") else None return self.sendEncEntities(node, [ EncProtocolEntity( EncProtocolEntity.TYPE_MSG if ciphertext.__class__ == WhisperMessage else EncProtocolEntity.TYPE_PKMSG, 2, ciphertext.serialize(), mediaType) ]) def sendToGroupWithSessions(self, node, jidsNeedSenderKey=None, retryCount=0): jidsNeedSenderKey = jidsNeedSenderKey or [] groupJid = node["to"] ownNumber = self.getLayerInterface( YowAuthenticationProtocolLayer).getUsername(False) senderKeyName = SenderKeyName(groupJid, AxolotlAddress(ownNumber, 0)) cipher = self.getGroupCipher(groupJid, ownNumber) encEntities = [] if len(jidsNeedSenderKey): senderKeyDistributionMessage = self.groupSessionBuilder.create( senderKeyName) for jid in jidsNeedSenderKey: sessionCipher = self.getSessionCipher(jid.split('@')[0]) skdm = self.serializeSKDM(node["to"], senderKeyDistributionMessage) message = self.serializeToProtobuf( node if retryCount > 0 else None, skdm) ciphertext = sessionCipher.encrypt(message + self.getPadding()) if ciphertext.__class__ == WhisperMessage: enc_type = EncProtocolEntity.TYPE_MSG else: enc_type = EncProtocolEntity.TYPE_PKMSG encEntities.append( EncProtocolEntity(enc_type, 2, ciphertext.serialize(), jid=jid)) if not retryCount: messageData = self.serializeToProtobuf(node) ciphertext = cipher.encrypt(messageData + self.getPadding()) mediaType = node.getChild("body")["mediatype"] if node.getChild( "body") else None encEntities.append( EncProtocolEntity(EncProtocolEntity.TYPE_SKMSG, 2, ciphertext, mediaType)) self.sendEncEntities(node, encEntities) def ensureSessionsAndSendToGroup(self, node, jids): jidsNoSession = [] for jid in jids: if not self.store.containsSession(jid.split('@')[0], 1): jidsNoSession.append(jid) if len(jidsNoSession): self.getKeysFor( jidsNoSession, lambda successJids, b: self.sendToGroupWithSessions( node, successJids)) else: self.sendToGroupWithSessions(node, jids) def sendToGroup(self, node, retryReceiptEntity=None): groupJid = node["to"] ownNumber = self.getLayerInterface( YowAuthenticationProtocolLayer).getUsername(False) ownJid = self.getLayerInterface( YowAuthenticationProtocolLayer).getUsername(True) senderKeyName = SenderKeyName(node["to"], AxolotlAddress(ownNumber, 0)) senderKeyRecord = self.store.loadSenderKey(senderKeyName) def sendToGroup(resultNode, _requestEntity): groupInfo = InfoGroupsResultIqProtocolEntity.fromProtocolTreeNode( resultNode) jids = list(groupInfo.getParticipants().keys() ) # keys in py3 returns dict_keys jids.remove(ownJid) return self.ensureSessionsAndSendToGroup(node, jids) if senderKeyRecord.isEmpty(): groupInfoIq = InfoGroupsIqProtocolEntity(groupJid) self._sendIq(groupInfoIq, sendToGroup) else: retryCount = 0 jidsNeedSenderKey = [] if retryReceiptEntity is not None: retryCount = retryReceiptEntity.getRetryCount() jidsNeedSenderKey.append(retryReceiptEntity.getRetryJid()) self.sendToGroupWithSessions(node, jidsNeedSenderKey, retryCount) def getSessionCipher(self, recipientId): if recipientId in self.sessionCiphers: sessionCipher = self.sessionCiphers[recipientId] else: sessionCipher = SessionCipher(self.store, self.store, self.store, self.store, recipientId, 1) self.sessionCiphers[recipientId] = sessionCipher return sessionCipher def getGroupCipher(self, groupId, senderId): senderKeyName = SenderKeyName(groupId, AxolotlAddress(senderId, 0)) if senderKeyName in self.groupCiphers: groupCipher = self.groupCiphers[senderKeyName] else: groupCipher = GroupCipher(self.store, senderKeyName) self.groupCiphers[senderKeyName] = groupCipher return groupCipher @staticmethod def serializeToProtobuf(node=None, more=None, toString=True): params = more or {} if node: child = node.getChild("body") if child: assert child['type'], "type attribute cannot be empty" data = deep_clean_empty(child.getData()) params[child['type']] = data else: raise ValueError("No body or media nodes found") serializedData = dict_to_protobuf(Message(), params) if toString: serializedData = serializedData.SerializeToString() return serializedData @staticmethod def serializeSKDM(groupId, senderKeyDistributionMessage): return { 'sender_key_distribution_message': { 'groupId': groupId, 'axolotl_sender_key_distribution_message': senderKeyDistributionMessage.serialize() } } @staticmethod def getPadding(): num = randint(1, 15) return bytearray([num] * num)