示例#1
0
    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")
示例#2
0
    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")
示例#3
0
    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
示例#4
0
    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")
示例#5
0
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
示例#6
0
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)
示例#7
0
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
示例#9
0
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)