Пример #1
0
 def _load_state(self):
     self.state = State()
     try:
         with open(os.path.join(self.accountdir, 'state.json'), 'r') as fp:
             for k, v in json.load(fp).items():
                 self.state[k] = v
     except IOError:
         pass
Пример #2
0
 def init_crypto_identity(self):
     identity_file = os.path.join(self.accountdir, 'identity.json')
     if not os.path.exists(identity_file):
         self.params = LocalParams.generate()
         self.state = State()
         self.state.identity_info = "Hi, I'm " + pet2ascii(
             self.params.dh.pk)
         assert self.head_imprint is None
         self.commit_to_chain()
         assert self.head_imprint
         with open(identity_file, 'w') as fp:
             json.dump(self.params.private_export(), fp)
     else:
         with open(identity_file, 'r') as fp:
             params_raw = json.load(fp)
             self.params = LocalParams.from_dict(params_raw)
         self._load_state()
Пример #3
0
    def __init__(self, email):
        self.email = email
        self.params = LocalParams.generate()
        self.chain_store = ObjectStore()
        self.tree_store = ObjectStore()
        self.chain = Chain(self.chain_store)
        self.state = State()

        # Stats
        self.nb_sent_emails = 0
        self.date_of_last_key_update = None

        # Committed views and capabilities
        self.committed_caps = {}
        self.committed_views = {}
        # ...and the ones queued to be committed.
        self.queued_identity_info = None
        self.queued_caps = defaultdict(set)
        self.queued_views = {}
        # Capabilities for unknown yet contacts
        # and views that have no readers.
        self.expected_caps = defaultdict(set)
        self.expected_views = defaultdict(set)

        # Known beliefs of other people about other people.
        self.global_views = defaultdict(dict)
        # Contacts that senders have made available to this agent.
        self.contacts_by_sender = defaultdict(set)

        # Objects that were sent to each recipient.
        self.sent_object_keys_to_recipients = {}
        # Objects that were received from other people.
        self.gossip_store = ObjectStore()

        # Generate initial encryption key, and add first block
        # to the chain
        self.update_key()
class Agent(object):
    '''
    Simulated claimchain user in the online deployment mode.
    '''
    def __init__(self, email):
        self.email = email
        self.params = LocalParams.generate()
        self.chain_store = ObjectStore()
        self.tree_store = ObjectStore()
        self.chain = Chain(self.chain_store)
        self.state = State()

        # Stats
        self.nb_sent_emails = 0

        # Committed views and capabilities
        self.committed_caps = {}
        self.committed_views = {}
        # ...and the ones queued to be committed.
        self.queued_identity_info = None
        self.queued_caps = defaultdict(set)
        self.queued_views = {}
        # Capabilities for unknown yet contacts
        # and views that have no readers.
        self.expected_caps = defaultdict(set)
        self.expected_views = defaultdict(set)

        # Known beliefs of other people about other people.
        self.global_views = defaultdict(dict)
        # Contacts that senders have made available to this agent.
        self.contacts_by_sender = defaultdict(set)

        # Objects that were sent to each recipient.
        self.sent_object_keys_to_recipients = {}
        # Objects that were received from other people.
        self.gossip_store = ObjectStore()

        # Generate initial encryption key, and add first block
        # to the chain
        self.update_key()

    @property
    def head(self):
        return self.chain.head

    @property
    def current_enc_key(self):
        """
        Current encryption key
        """
        return self.state.identity_info

    @staticmethod
    def generate_public_key():
        """
        Generate (fake) public encryption key
        """
        # 4096 random bits in base64
        return base64.b64encode(os.urandom(4096 // 8))

    def add_expected_reader(self, reader, contacts):
        """
        Add contacts to be accessible by the reader

        No need to know the reader's DH key, and contact
        views at this point. As soon as these will be learned,
        expected capabilities will be moved to the queue.
        """
        if isinstance(contacts, six.string_types):
            warnings.warn("Contacts is a string type, an iterable of "
                          "identifiers is expected.")

        if not contacts:
            return

        logger.debug('%s / expected cap / %s: %s', self.email, reader,
                     contacts)

        self.expected_caps[reader].update(contacts)

    def _update_buffer(self):
        accepted_caps_by_reader = defaultdict(set)

        for reader, contacts in self.expected_caps.items():
            reader_view = self.get_latest_view(reader)
            if reader_view is None and reader != PUBLIC_READER_LABEL:
                continue

            for contact in contacts:
                contact_view = self.get_latest_view(contact)
                if contact_view is not None:
                    # Copy expected cap into queue.
                    self.queued_caps[reader].add(contact)

                    # Move expected view into queue if needed.
                    if contact in self.expected_views:
                        self.queued_views[contact] = contact_view
                        del self.expected_views[contact]

                    accepted_caps_by_reader[reader].add(contact)

            # Move reader view into queue if needed.
            if accepted_caps_by_reader[reader]:
                if reader in self.expected_views:
                    self.queued_views[reader] = reader_view
                    del self.expected_views[reader]

        # Clean empty expected_caps entries.
        for reader, contacts in accepted_caps_by_reader.items():
            self.expected_caps[reader] -= contacts
            if not self.expected_caps[reader]:
                del self.expected_caps[reader]

    def get_latest_view(self, contact, save=True):
        """
        Resolve latest view for contact through social policy

        The method gathers candidate views for contact across local state,
        and runs social validation policy to decide on the best candidate.

        As a side effect, puts the resolved view in the queue.

        :param contact: Contact identifier
        :param save: Whether to save the resolved view to the queue
        """
        policy = AgentSettings.get_default().conflict_resolution_policy

        # Collect possible candidates
        candidate_views = set()
        # ...starting with existing views of the contact.
        if contact in self.committed_views:
            candidate_views.add(self.committed_views[contact])
        if contact in self.queued_views:
            candidate_views.add(self.queued_views[contact])
        if contact in self.expected_views:
            candidate_views.add(self.expected_views[contact])

        # Get a view of contact in question from every friend.
        current_friends = set(self.committed_views.keys()) \
                        | set(self.queued_views.keys())    \
                        | set(self.expected_views.keys())
        for friend in current_friends - {contact}:
            candidate_view = self.global_views[friend].get(contact)
            if candidate_view is not None:
                candidate_views.add(candidate_view)

        # If no candidates, return None.
        if len(candidate_views) == 0:
            return None

        # Otherwise, resolve conflicts using a policy
        view = policy(self, candidate_views)
        # ...and add the resolved view to the queue.
        self.expected_views[contact] = view

        # Remove from queue if resolved view is the same as committed.
        if save:
            committed_view = self.committed_views.get(contact)
            if view == committed_view:
                del self.expected_views[contact]

        return view

    def send_message(self, recipients):
        """
        Compute additional data to be sent to recipients

        NOTE: May update the chain if required by the update policy

        :param recipients: An iterable of recipient identifiers (emails)
        :returns: ``MessageMetadata`` object
        """
        logger.debug('%s -> %s', self.email, recipients)

        if len(recipients) == 0:
            return
        if isinstance(recipients, six.string_types):
            warnings.warn("Recipients is a string type, an iterable of "
                          "identifiers is expected.")
        if not isinstance(recipients, set):
            recipients = set(recipients)

        with self.params.as_default():
            intro_policy = AgentSettings.get_default().introduction_policy
            # Grant accesses according to introduction policy.
            intro_policy(self, recipients)

            # Move expected views and caps into queue
            self._update_buffer()

            # Decide whether to update the encryption key.
            # TODO: Make key update decision a policy.
            nb_sent_emails_thresh = AgentSettings.get_default() \
                    .key_update_every_nb_sent_emails

            if nb_sent_emails_thresh is not None and \
               self.nb_sent_emails > nb_sent_emails_thresh:
                self.update_key()

            else:
                # Decide whether to update the chain.
                update_policy = AgentSettings.get_default().chain_update_policy
                if update_policy(self, recipients):
                    self.update_chain()

            local_object_keys = set()
            global_object_keys = set()

            # Add own chain blocks.
            # NOTE: Requires that chain and tree use separate stores
            local_object_keys.update(self.chain_store.keys())

            # Add evidence for public claims.
            public_contacts = self.committed_caps.get(PUBLIC_READER_LABEL) \
                              or set()
            for contact in public_contacts:
                object_keys = self.state.compute_evidence_keys(
                    PUBLIC_READER_PARAMS.dh.pk, contact)
                local_object_keys.update(object_keys)
                contact_view = self.committed_views.get(contact)
                if contact_view is not None:
                    global_object_keys.add(contact_view.head)

            # Compute evidence that needs to be sent to all recipients.
            for recipient in recipients:
                # NOTE: This assumes that claims contain heads
                accessible_contacts = self.committed_caps.get(recipient) \
                                      or set()
                for contact in accessible_contacts:
                    recipient_view = self.committed_views.get(recipient)
                    if recipient_view is None:
                        continue
                    contact_view = self.committed_views.get(contact)
                    if contact_view is not None:
                        # Add evidence for cross-references.
                        recipient_dh_pk = recipient_view.params.dh.pk
                        evidence_keys = self.state.compute_evidence_keys(
                            recipient_dh_pk, contact)
                        local_object_keys.update(evidence_keys)

                        # Add contact's latest block.
                        global_object_keys.add(contact_view.head)

            # Compute the minimal amount of objects that need to be sent in
            # this message.
            relevant_keys = local_object_keys | global_object_keys
            object_keys_to_send = set()
            for recipient in recipients:
                if recipient not in self.sent_object_keys_to_recipients:
                    self.sent_object_keys_to_recipients[recipient] = \
                            relevant_keys
                    object_keys_to_send = relevant_keys
                else:
                    object_keys_for_recipient = relevant_keys.difference(
                        self.sent_object_keys_to_recipients[recipient])
                    object_keys_to_send |= object_keys_for_recipient

            # Gather the objects by keys.
            message_store = {}
            for key in local_object_keys.intersection(object_keys_to_send):
                value = self.chain_store.get(key) or self.tree_store.get(key)
                if value is not None:
                    message_store[key] = value

            for key in global_object_keys.intersection(object_keys_to_send):
                value = self.gossip_store.get(key)
                if value is not None:
                    message_store[key] = value

            self.nb_sent_emails += 1
            return MessageMetadata(self.chain.head, public_contacts,
                                   message_store)

    def get_accessible_contacts(self,
                                sender,
                                message_metadata,
                                other_recipients=None):
        """
        Get the contacts that are expected to be accessible on sender's chain
        """
        # NOTE: Assumes other people's introduction policy is the same
        contacts = self.contacts_by_sender[sender]
        other_recipients = set(other_recipients) - {sender, self.email}
        for recipient in other_recipients | message_metadata.public_contacts:
            contacts.add(recipient)
        return contacts

    def receive_message(self, sender, message_metadata, other_recipients=None):
        """
        Interpret incoming additional data

        :param sender: Sender identifier
        :param message_metadata: Additional data obtained by ``send_message``
        :param other_recipients: Identifiers of other known recipients of the
                                 message
        """
        logger.debug('%s <- %s', self.email, sender)
        if other_recipients is None:
            other_recipients = set()

        with self.params.as_default():
            # Merge stores temporarily.
            merged_store = ObjectStore(self.gossip_store)
            for key, obj in message_metadata.store.items():
                merged_store[key] = obj

            sender_head = message_metadata.head
            sender_latest_block = merged_store[sender_head]
            self.gossip_store[sender_head] = \
                    sender_latest_block
            self.expected_views[sender] = View(
                Chain(self.gossip_store, root_hash=sender_head))
            full_sender_view = View(Chain(merged_store, root_hash=sender_head))
            logger.debug('%s / expected view / %s', self.email, sender)

            # Add relevant objects from the message store.
            contacts = self.get_accessible_contacts(sender, message_metadata,
                                                    other_recipients)
            for contact in contacts - {self.email}:
                contact_head = self.get_contact_head_from_view(
                    full_sender_view, contact)
                if contact_head is None:
                    continue
                contact_latest_block = message_metadata.store.get(contact_head)
                if contact_latest_block is not None:
                    self.gossip_store[contact_head] = contact_latest_block

                # NOTE: Assumes people send only contacts' latest blocks
                contact_chain = Chain(self.gossip_store,
                                      root_hash=contact_head)
                self.global_views[sender][contact] = View(contact_chain)

            # TODO: Needs a special check for contact==self.email.

            # Recompute the latest beliefs.
            for contact in {sender} | contacts:
                self.get_latest_view(contact)

    def get_contact_head_from_view(self, view, contact):
        """
        Try accessing cross-reference claim as yourself, and as a public reader

        :param view: View to query
        :param contact: Contact of interest
        :returns: Contacts head, or None
        """
        with self.params.as_default():
            claim = view.get(contact)
            if claim is not None:
                return claim
        with PUBLIC_READER_PARAMS.as_default():
            claim = view.get(contact)
        return claim

    def update_chain(self):
        """
        Force chain update

        Commits views and capabilities in the queues to the chain
        """
        logger.debug('%s / chain update', self.email)

        with self.params.as_default():
            # Refresh views of all friends and contacts in queued capabilities.
            for friend, contacts in self.queued_caps.items():
                self.get_latest_view(friend)
                for contact in contacts:
                    self.get_latest_view(contact)

            # Add the latest own encryption key.
            if self.queued_identity_info is not None:
                self.state.identity_info = self.queued_identity_info

            # Get heads of queued views into the claimchain state.
            for friend, view in self.queued_views.items():
                claim = view.chain.head
                if claim is not None:
                    self.state[friend] = claim
                    self.committed_views[friend] = view

            # Get capabilities in the capability queue into the claimchain
            # state, for those subjects whose keys are known.
            for friend, contacts in self.queued_caps.items():
                if len(contacts) == 0:
                    continue

                friend_dh_pk = None
                # If the buffer is for the public 'reader':
                if friend == PUBLIC_READER_LABEL:
                    friend_dh_pk = PUBLIC_READER_PARAMS.dh.pk

                # Otherwise, try to find the DH key in views.
                else:
                    view = self.get_latest_view(friend, save=False)
                    if view is not None:
                        friend_dh_pk = view.params.dh.pk

                if friend_dh_pk is not None:
                    self.state.grant_access(friend_dh_pk, contacts)
                    if friend in self.committed_caps:
                        self.committed_caps[friend].update(contacts)
                    else:
                        self.committed_caps[friend] = set(contacts)

                else:
                    # This should not happen
                    raise warnings.warn('Cap reader DH key not known at the '
                                        'time of committing.')

            # Commit state
            head = self.state.commit(target_chain=self.chain,
                                     tree_store=self.tree_store)

            # Flush the view and caps queues
            self.queued_views.clear()
            self.queued_caps.clear()

    def update_key(self):
        """
        Force update of the encryption key, and the chain
        """
        logger.debug('%s / key update', self.email)
        self.queued_identity_info = Agent.generate_public_key()
        self.update_chain()

    def __repr__(self):
        return 'Agent("%s")' % self.email
Пример #5
0
def test_read_claim_from_other_chain():
    for i in range(1, 100):
        alice_params = LocalParams.generate()
        alice_state = State()
        alice_store = {}
        alice_chain = Chain(alice_store, None)
        alice_state.identity_info = "Hi, I'm " + pet2ascii(alice_params.dh.pk)
        with alice_params.as_default():
            alice_head = alice_state.commit(alice_chain)
        alice_chain = Chain(alice_store, alice_head)

        bob_params = LocalParams.generate()
        bob_state = State()
        bob_store = {}
        bob_chain = Chain(bob_store, None)
        bob_state.identity_info = "Hi, I'm " + pet2ascii(bob_params.dh.pk)
        with bob_params.as_default():
            bob_head = bob_state.commit(bob_chain)
        bob_chain = Chain(bob_store, bob_head)

        bob_pk = bob_params.dh.pk

        with alice_params.as_default():
            alice_state[b"bobs_key"] = b"123abc"
            alice_state.grant_access(bob_pk, [b"bobs_key"])
            alice_head = alice_state.commit(alice_chain)
        alice_chain = Chain(alice_store, alice_head)

        with alice_params.as_default():
            value = View(alice_chain)[b'bobs_key'].decode('utf-8')

        assert value == "123abc"

        with bob_params.as_default():
            value = View(alice_chain)[b'bobs_key'].decode('utf-8')

        assert value == "123abc"
Пример #6
0
class CCAccount(object):
    def __init__(self, accountdir, store=None):
        self.accountdir = accountdir
        if not os.path.exists(accountdir):
            os.makedirs(accountdir)
        self.store = store
        self.init_crypto_identity()

    #
    # muacrypt plugin hook implementations
    #
    @hookimpl
    def process_incoming_gossip(self, addr2pagh, account_key, dec_msg):
        sender_addr = parse_email_addr(dec_msg["From"])
        root_hash = dec_msg["GossipClaims"]
        store_url = dec_msg["ClaimStore"]
        if not root_hash or not store_url:
            # this peer has no CC support
            return
        self.register_peer(sender_addr, root_hash, store_url)

        peers_chain = self.get_chain(store_url, root_hash)
        recipients = get_target_emailadr(dec_msg)
        for addr in recipients:
            pagh = addr2pagh[addr]
            self.verify_claim(peers_chain, addr, pagh.keydata)
            value = self.read_claim(addr, chain=peers_chain)
            if value and value.get('store_url'):
                self.register_peer(addr, value['root_hash'],
                                   value['store_url'])

    @hookimpl
    def process_before_encryption(self, sender_addr, sender_keyhandle,
                                  recipient2keydata, payload_msg, _account):
        addrs = recipient2keydata.keys()
        if not addrs:
            logging.error("no recipients found.\n")

        for addr in addrs:
            self.add_claim(self.claim_about(addr, recipient2keydata.get(addr)))

        for reader in addrs:
            if self.can_share_with(reader):
                self.share_claims(addrs, reader)

        self.commit_to_chain()
        payload_msg["GossipClaims"] = self.head_imprint
        # TODO: what do we do with dict stores?
        payload_msg["ClaimStore"] = self.store.url

    def init_crypto_identity(self):
        identity_file = os.path.join(self.accountdir, 'identity.json')
        if not os.path.exists(identity_file):
            self.params = LocalParams.generate()
            self.state = State()
            self.state.identity_info = "Hi, I'm " + pet2ascii(
                self.params.dh.pk)
            assert self.head_imprint is None
            self.commit_to_chain()
            assert self.head_imprint
            with open(identity_file, 'w') as fp:
                json.dump(self.params.private_export(), fp)
        else:
            with open(identity_file, 'r') as fp:
                params_raw = json.load(fp)
                self.params = LocalParams.from_dict(params_raw)
            self._load_state()

    @property
    def head_imprint(self):
        if self._head:
            x = bytes2ascii(self._head)
            if hasattr(x, "decode"):
                x = x.decode("ascii")
            return x

    def register_peer(self, addr, root_hash, store_url, chain=None):
        # TODO: check for existing entries
        if not chain:
            chain = self.get_chain(store_url, root_hash)
        assert chain
        view = View(chain)
        pk = view.params.dh.pk
        assert pk
        self.add_claim((addr,
                        dict(root_hash=root_hash,
                             store_url=store_url,
                             public_key=pet2ascii(pk))))

    def get_chain(self, store_url, root_hash):
        cache_dir = os.path.join(self.accountdir, 'cache')
        store = FileStore(cache_dir, store_url)
        return Chain(store, root_hash=ascii2bytes(root_hash))

    def verify_claim(self, chain, addr, keydata, store_url='', root_hash=''):
        autocrypt_key = bytes2ascii(keydata).decode("ascii")
        claim = self.read_claim(addr, chain=chain)
        if claim:
            assert claim['autocrypt_key'] == autocrypt_key
            if store_url:
                assert claim['store_url'] == store_url
            if root_hash:
                assert claim['root_hash'] == root_hash

    def claim_about(self, addr, keydata):
        info = self.read_claim(addr) or {}
        info['autocrypt_key'] = bytes2ascii(keydata).decode("ascii")
        return (addr, info)

    def commit_to_chain(self):
        with self.params.as_default():
            self._head = self.state.commit(self.chain)

    def upload(self):
        if hasattr(self.store, 'send'):
            self.store.send()

    def read_claim(self, claimkey, chain=None):
        if chain is None:
            try:
                value = self.state[claimkey.encode('utf-8')]
                return json.loads(value.decode('utf-8'))
            except KeyError:
                chain = self.chain
        try:
            with self.params.as_default():
                value = View(chain)[claimkey.encode('utf-8')]
                return json.loads(value.decode('utf-8'))
        except (KeyError, ValueError):
            return None

    def add_claim(self, claim):
        key = claim[0].encode('utf-8')
        value = json.dumps(claim[1]).encode('utf-8')
        assert isinstance(key, bytes)
        assert isinstance(value, bytes)
        self.state[key] = value
        self._persist_state()

    def can_share_with(self, peer):
        reader_info = self.read_claim(peer) or {}
        return bool(reader_info.get('public_key'))

    def share_claims(self, claim_keys, reader):
        claim_keys = [key.encode('utf-8') for key in claim_keys]
        reader_info = self.read_claim(reader) or {}
        pk = ascii2pet(reader_info.get("public_key"))
        assert pk
        with self.params.as_default():
            self.state.grant_access(pk, claim_keys)

    @property
    def chain(self):
        return Chain(self.store, root_hash=self._head)

    def _head():
        def fget(self):
            try:
                with open(os.path.join(self.accountdir, 'head'), 'rb') as fp:
                    return fp.read()
            except IOError:
                return None

        def fset(self, val):
            with open(os.path.join(self.accountdir, 'head'), 'wb') as fp:
                fp.write(val)

        return property(fget, fset)

    _head = _head()

    def _persist_state(self):
        with open(os.path.join(self.accountdir, 'state.json'), 'w') as fp:
            json.dump(self.state._claim_content_by_label, fp)

    def _load_state(self):
        self.state = State()
        try:
            with open(os.path.join(self.accountdir, 'state.json'), 'r') as fp:
                for k, v in json.load(fp).items():
                    self.state[k] = v
        except IOError:
            pass
Пример #7
0
class CCAccount(object):
    def __init__(self, accountdir, store=None):
        self.accountdir = accountdir
        if not os.path.exists(accountdir):
            os.makedirs(accountdir)
        self.addr2root_hash = {}
        self.addr2pk = {}
        self.store = store
        self.init_crypto_identity()

    #
    # muacrypt plugin hook implementations
    #
    @hookimpl
    def process_incoming_gossip(self, addr2pagh, account_key, dec_msg):
        root_hash = dec_msg["GossipClaims"]
        store = FileStore(dec_msg["ChainStore"])
        peers_chain = Chain(store, root_hash=ascii2bytes(root_hash))
        assert peers_chain
        view = View(peers_chain)
        peers_pk = view.params.dh.pk
        assert peers_pk
        sender = parse_email_addr(dec_msg["From"])
        self.addr2root_hash[sender] = root_hash
        self.addr2pk[sender] = peers_pk
        recipients = get_target_emailadr(dec_msg)
        for recipient in recipients:
            pagh = addr2pagh[recipient]
            value = self.read_claim_from(peers_chain, recipient)
            if value:
                # for now we can only read claims about ourselves...
                # so if we get a value it must be our head imprint.
                assert value == bytes2ascii(pagh.keydata)

    @hookimpl
    def process_before_encryption(self, sender_addr, sender_keyhandle,
                                  recipient2keydata, payload_msg, _account):
        recipients = recipient2keydata.keys()
        if not recipients:
            logging.error("no recipients found.\n")

        for recipient in recipients:
            claim = recipient, bytes2ascii(recipient2keydata.get(recipient))
            for reader in recipients:
                pk = self.addr2pk.get(reader)
                self.add_claim(claim, access_pk=pk)

        self.commit_to_chain()
        payload_msg["GossipClaims"] = self.head_imprint
        # TODO: what do we do with dict stores?
        payload_msg["ChainStore"] = self.store._dir

    def init_crypto_identity(self):
        identity_file = os.path.join(self.accountdir, 'identity.json')
        if not os.path.exists(identity_file):
            self.params = LocalParams.generate()
            self.state = State()
            self.state.identity_info = "Hi, I'm " + pet2ascii(self.params.dh.pk)
            assert self.head is None
            self.commit_to_chain()
            assert self.head
            with open(identity_file, 'w') as fp:
                json.dump(self.params.private_export(), fp)
        else:
            with open(identity_file, 'r') as fp:
                params_raw = json.load(fp)
                self.params = LocalParams.from_dict(params_raw)
                # TODO: load state from last block
                self.state = State()

    def get_public_key(self):
        return self.params.dh.pk
        # chain = self._get_current_chain()
        # with self.params.as_default():
        #     return View(chain).params.dh.pk

    def head():
        def fget(self):
            try:
                with open(os.path.join(self.accountdir, 'head'), 'rb') as fp:
                    return fp.read()
            except IOError:
                return None

        def fset(self, val):
            with open(os.path.join(self.accountdir, 'head'), 'wb') as fp:
                fp.write(val)
        return property(fget, fset)
    head = head()

    @property
    def head_imprint(self):
        return bytes2ascii(self.head)

    def commit_to_chain(self):
        chain = self._get_current_chain()
        with self.params.as_default():
            self.head = self.state.commit(chain)

    def read_claim(self, claimkey):
        return self.read_claim_as(self, claimkey)

    def read_claim_as(self, other, claimkey):
        assert callable(getattr(claimkey, 'encode', None))
        print("read-claim-as", other, repr(claimkey))
        chain = self._get_current_chain()
        with other.params.as_default():
            return View(chain)[claimkey.encode('utf-8')].decode('utf-8')

    def read_claim_from(self, chain, claimkey):
        assert callable(getattr(claimkey, 'encode', None))
        try:
            with self.params.as_default():
                return View(chain)[claimkey.encode('utf-8')].decode('utf-8')
        except (KeyError, ValueError):
            return None

    def has_readable_claim(self, claimkey):
        return self.has_readable_claim_for(self, claimkey)

    def has_readable_claim_for(self, other, claimkey):
        assert isinstance(claimkey, bytes)
        try:
            self.read_claim_as(other, claimkey)
        except (KeyError, ValueError):
            return False
        return True

    def add_claim(self, claim, access_pk=None):
        key, value = claim[0].encode('utf-8'), claim[1].encode('utf-8')
        assert isinstance(key, bytes)
        assert isinstance(value, bytes)
        self.state[key] = value
        with self.params.as_default():
            self.state.grant_access(self.get_public_key(), [key])
            if access_pk is not None:
                self.state.grant_access(access_pk, [key])

    def _get_current_chain(self):
        return Chain(self.store, root_hash=self.head)
Пример #8
0
class Agent(object):
    """
    Simulated ClaimChain user.
    """
    def __init__(self, email):
        self.email = email
        self.params = LocalParams.generate()
        self.chain_store = ObjectStore()
        self.tree_store = ObjectStore()
        self.chain = Chain(self.chain_store)
        self.state = State()

        # Stats
        self.nb_sent_emails = 0
        self.date_of_last_key_update = None

        # Committed views and capabilities
        self.committed_caps = {}
        self.committed_views = {}
        # ...and the ones queued to be committed.
        self.queued_identity_info = None
        self.queued_caps = defaultdict(set)
        self.queued_views = {}
        # Capabilities for unknown yet contacts
        # and views that have no readers.
        self.expected_caps = defaultdict(set)
        self.expected_views = defaultdict(set)

        # Known beliefs of other people about other people.
        self.global_views = defaultdict(dict)
        # Contacts that senders have made available to this agent.
        self.contacts_by_sender = defaultdict(set)

        # Objects that were sent to each recipient.
        self.sent_object_keys_to_recipients = {}
        # Objects that were received from other people.
        self.gossip_store = ObjectStore()

        # Generate initial encryption key, and add first block
        # to the chain
        self.update_key()

    @property
    def head(self):
        """Chain head."""
        return self.chain.head

    @property
    def current_enc_key(self):
        """
        Current encryption key.
        """
        return self.state.identity_info

    @staticmethod
    def generate_public_key():
        """
        Generate (fake) public encryption key.
        """
        # 4096 random bits in base64
        return base64.b64encode(os.urandom(4096 // 8))

    def add_expected_reader(self, reader, contacts):
        """
        Make contacts accessible to the reader.

        No need to know the reader's DH key, and contact
        views at this point. As soon as these will be learned,
        expected capabilities will be moved to the queue.
        """
        if isinstance(contacts, six.string_types):
            # Quick check to prevent a silly bug.
            warnings.warn("Contacts is a string type, an iterable of "
                          "identifiers is expected.")

        if not contacts:
            return

        logger.debug('%s / expected cap / %s: %s', self.email, reader,
                     contacts)
        self.expected_caps[reader].update(contacts)

    def _update_buffer(self):
        """Update claim 'expected' and 'queued' buffers.

        Traverses all claims in the 'expected' buffer, and checks if there
        has been collected enough information to actually make the claims.
        Those for which the information in the gossip store is sufficient,
        are moved to the 'queued' buffer.
        """

        accepted_caps_by_reader = defaultdict(set)

        for reader, contacts in self.expected_caps.items():
            reader_view = self.get_latest_view(reader)
            if reader_view is None and reader != PUBLIC_READER_LABEL:
                continue

            for contact in contacts:
                contact_view = self.get_latest_view(contact)
                if contact_view is not None:
                    # Copy expected cap into queue.
                    self.queued_caps[reader].add(contact)

                    # Move the expected view into queue if needed.
                    if contact in self.expected_views:
                        self.queued_views[contact] = contact_view
                        del self.expected_views[contact]

                    accepted_caps_by_reader[reader].add(contact)

            # Move the reader view into queue if needed.
            if accepted_caps_by_reader[reader]:
                if reader in self.expected_views:
                    self.queued_views[reader] = reader_view
                    del self.expected_views[reader]

        # Clean empty expected_caps entries.
        for reader, contacts in accepted_caps_by_reader.items():
            self.expected_caps[reader] -= contacts
            if not self.expected_caps[reader]:
                del self.expected_caps[reader]

    def get_social_evidence(self, contact):
        """Gather social evidence about the contact."""

        # Collect possible candidates
        own_views = set()
        # ...starting with existing views of the contact.
        if contact in self.committed_views:
            own_views.add(self.committed_views[contact])
        if contact in self.queued_views:
            own_views.add(self.queued_views[contact])
        if contact in self.expected_views:
            own_views.add(self.expected_views[contact])

        # Get a view of the contact in question from every friend.
        current_friends = set(self.committed_views.keys()) \
                        | set(self.queued_views.keys())    \
                        | set(self.expected_views.keys())
        views_by_friend = {}
        for friend in current_friends - {contact, self.email}:
            candidate_view = self.global_views[friend].get(contact)
            if candidate_view is not None:
                views_by_friend[friend] = candidate_view

        return own_views, views_by_friend

    def get_latest_view(self, contact, save=True):
        """Resolve latest view for contact through a social policy.

        Gathers candidate views for contact across local state, and runs
        a social validation policy to decide on the best candidate.

        As a side effect, puts the resolved view in the 'expected' buffer.

        :param contact: Contact identifier
        :param bool save: Whether to save the resolved view to the queue
        """
        own_views, views_by_friend = self.get_social_evidence(contact)
        policy = AgentSettings.get_default().conflict_resolution_policy
        candidate_views = own_views | set(views_by_friend.values())

        if len(candidate_views) == 0:
            return None

        # Resolve conflicts using a policy
        view = policy(self, candidate_views)
        # ...and add the resolved view to the 'expected' buffer.
        self.expected_views[contact] = view

        # Remove from the buffer if the resolved view is the same as committed.
        if save:
            committed_view = self.committed_views.get(contact)
            if view == committed_view:
                del self.expected_views[contact]

        return view

    def send_message(self, recipients, mtime=0):
        """Build an ClaimChain embedded data packet.

        NOTE: May update the chain if required by the update policy.

        :param recipients: An iterable of recipient identifiers (emails)
        :param mtime: Timestamp
        :returns: ``MessageMetadata`` object
        """
        logger.debug('%s -> %s', self.email, recipients)

        if len(recipients) == 0:
            return
        if isinstance(recipients, six.string_types):
            warnings.warn("Recipients is a string type, an iterable of "
                          "identifiers is expected.")
        if not isinstance(recipients, set):
            recipients = set(recipients)

        with self.params.as_default():
            intro_policy = AgentSettings.get_default().introduction_policy
            # Grant accesses according to the introduction policy.
            intro_policy(self, recipients)

            # Move expected views and caps into the queue.
            self._update_buffer()

            # Decide whether to update the encryption key.
            # TODO: Make key update decision a policy.
            nb_sent_emails_thresh = AgentSettings.get_default() \
                    .key_update_every_nb_sent_emails
            min_nb_days = AgentSettings.get_default() \
                    .key_update_every_nb_days

            nb_sent_based_update = nb_sent_emails_thresh is not None and \
                    self.nb_sent_emails >= nb_sent_emails_thresh

            time_based_update = False
            sending_date = datetime.fromtimestamp(mtime)
            if self.date_of_last_key_update is not None:
                days_since_last_update = (sending_date -
                                          self.date_of_last_key_update).days
                if min_nb_days is not None:
                    time_based_update = days_since_last_update >= min_nb_days
                time_based_update = min_nb_days is not None and (
                    days_since_last_update >= min_nb_days)
            else:
                self.date_of_last_key_update = sending_date

            if nb_sent_based_update or time_based_update:
                self.update_key(mtime)
                self.nb_sent_emails = 0

            else:
                # Decide whether to update the chain.
                update_policy = AgentSettings.get_default().chain_update_policy
                if update_policy(self, recipients):
                    self.update_chain()

            local_object_keys = set()

            # Add own chain blocks.
            # NOTE: Requires that chain and tree use separate stores
            local_object_keys.update(self.chain_store.keys())

            # Add authentication proofs for public claims.
            public_contacts = self.committed_caps.get(PUBLIC_READER_LABEL) \
                              or set()
            for contact in public_contacts:
                object_keys = self.state.compute_evidence_keys(
                    PUBLIC_READER_PARAMS.dh.pk, contact)
                local_object_keys.update(object_keys)
                contact_view = self.committed_views.get(contact)

            # Find a minimal amount of proof nodes that need to be included.
            for recipient in recipients:
                accessible_contacts = self.committed_caps.get(recipient) \
                                      or set()
                for contact in accessible_contacts:
                    recipient_view = self.committed_views.get(recipient)
                    if recipient_view is None:
                        continue
                    contact_view = self.committed_views.get(contact)
                    if contact_view is not None:
                        # Add the proof for the cross-reference.
                        recipient_dh_pk = recipient_view.params.dh.pk
                        proof_keys = self.state.compute_evidence_keys(
                            recipient_dh_pk, contact)
                        local_object_keys.update(proof_keys)

            # Find the minimal amount of objects that need to be sent in
            # this message.
            relevant_keys = local_object_keys
            object_keys_to_send = set()
            for recipient in recipients:
                if recipient not in self.sent_object_keys_to_recipients:
                    if AgentSettings.get_default().optimize_sent_objects:
                        self.sent_object_keys_to_recipients[recipient] = \
                                relevant_keys
                    object_keys_to_send = relevant_keys
                else:
                    object_keys_for_recipient = relevant_keys.difference(
                        self.sent_object_keys_to_recipients[recipient])
                    object_keys_to_send |= object_keys_for_recipient

            # Collect the objects by keys.
            message_store = {}
            # * Local (own) objects...
            for key in local_object_keys.intersection(object_keys_to_send):
                value = self.chain_store.get(key) or self.tree_store.get(key)
                if value is not None:
                    message_store[key] = value

            self.nb_sent_emails += 1
            return MessageMetadata(self.chain.head, public_contacts,
                                   message_store)

    def get_accessible_contacts(self,
                                sender,
                                message_metadata,
                                other_recipients=None):
        """
        Get the contacts that are expected to be accessible on sender's chain.
        """
        # NOTE: Assumes other people's introduction policy is the same
        contacts = self.contacts_by_sender[sender]
        other_recipients = set(other_recipients) - {sender, self.email}
        for recipient in other_recipients | message_metadata.public_contacts:
            contacts.add(recipient)
        return contacts

    def receive_message(self, sender, message_metadata, other_recipients=None):
        """Interpret an incoming data packet.

        :param sender: Sender identifier
        :param message_metadata: Additional data obtained by ``send_message``
        :param other_recipients: Identifiers of other known recipients of the
                                 message
        """
        logger.debug('%s <- %s', self.email, sender)
        if other_recipients is None:
            other_recipients = set()

        with self.params.as_default():
            # Merge stores temporarily.
            merged_store = ObjectStore(self.gossip_store)
            for key, obj in message_metadata.store.items():
                merged_store[key] = obj

            sender_head = message_metadata.head
            sender_latest_block = merged_store[sender_head]
            self.gossip_store[sender_head] = \
                    sender_latest_block
            self.expected_views[sender] = View(
                Chain(self.gossip_store, root_hash=sender_head))
            full_sender_view = View(Chain(merged_store, root_hash=sender_head))
            logger.debug('%s / expected view / %s', self.email, sender)

            # Add relevant objects from the message store.
            contacts = self.get_accessible_contacts(sender, message_metadata,
                                                    other_recipients)
            for contact in contacts - {self.email}:
                contact_latest_block = self.get_contact_head_from_view(
                    full_sender_view, contact)
                if contact_latest_block is not None:
                    contact_head_hash = contact_latest_block.hid
                    self.gossip_store[contact_head_hash] = contact_latest_block

                    # NOTE: Assumes people send only contacts' latest blocks
                    contact_chain = Chain(self.gossip_store,
                                          root_hash=contact_head_hash)
                    self.global_views[sender][contact] = View(contact_chain)

            # TODO: Needs a special check for contact==self.email.

            # Recompute the latest beliefs.
            for contact in {sender} | contacts:
                self.get_latest_view(contact)

    def get_contact_head_from_view(self, view, contact):
        """
        Try accessing a claim as oneself, and fall back to a public reader.

        :param view: View to query
        :param contact: Contact of interest
        :returns: Contact's head block, or None
        """
        with self.params.as_default():
            claim = view.get(contact)
            if claim is not None:
                return deserialize_block(claim)
        with PUBLIC_READER_PARAMS.as_default():
            claim = view.get(contact)
            if claim is not None:
                return deserialize_block(claim)

    def update_chain(self):
        """Force a chain update.

        Commits views and capabilities in the queues to the chain.
        """
        logger.debug('%s / chain update', self.email)

        with self.params.as_default():
            # Refresh views of all friends and contacts in queued capabilities.
            for friend, contacts in self.queued_caps.items():
                self.get_latest_view(friend)
                for contact in contacts:
                    self.get_latest_view(contact)

            # Add the latest own encryption key.
            if self.queued_identity_info is not None:
                self.state.identity_info = self.queued_identity_info

            # Mark queued views as committed.
            for friend, view in self.queued_views.items():
                self.committed_views[friend] = view

            # Put heads of previously committed views into the state.
            for friend, view in self.committed_views.items():
                latest_block = view.chain.store.get(view.head)
                self.state[friend] = serialize_block(latest_block)
                self.committed_views[friend] = view

            # Collect DH keys for all readers.
            dh_pk_by_reader = {}
            readers = set(self.queued_caps.keys()) | self.committed_caps.keys()
            for reader in readers:
                reader_dh_pk = None
                # If the buffer is for the public reader:
                if reader == PUBLIC_READER_LABEL:
                    reader_dh_pk = PUBLIC_READER_PARAMS.dh.pk

                # Otherwise, try to find the DH key in views.
                else:
                    view = self.get_latest_view(reader, save=False)
                    if view is not None:
                        reader_dh_pk = view.params.dh.pk

                if reader_dh_pk is not None:
                    dh_pk_by_reader[reader] = reader_dh_pk

            # Grant the accesses.
            for reader, contacts in self.queued_caps.items():
                if len(contacts) == 0:
                    continue
                if reader in self.committed_caps:
                    self.committed_caps[reader].update(contacts)
                else:
                    self.committed_caps[reader] = set(contacts)

            for reader, reader_dh_pk in dh_pk_by_reader.items():
                contacts = self.committed_caps.get(reader)
                if contacts:
                    self.state.grant_access(reader_dh_pk, contacts)

            # Commit state.
            head = self.state.commit(target_chain=self.chain,
                                     tree_store=self.tree_store)

            # Flush the view and caps queues.
            self.queued_views.clear()
            self.queued_caps.clear()

    def update_key(self, mtime=None):
        """
        Force update of the encryption key, and the chain.
        """
        logger.debug('%s / key update', self.email)
        self.queued_identity_info = Agent.generate_public_key()
        self.update_chain()
        if mtime is not None:
            self.date_of_last_key_update = datetime.fromtimestamp(mtime)

    def __repr__(self):
        return 'Agent("%s")' % self.email