1"""
2All muacrypt states are managed through this module.
3We follow the Kappa architecture style
4(http://milinda.pathirage.org/kappa-architecture.com/)
5i.e. all state changes are added to append-only chains and they contain
6immutable entries that may cross-reference other entries (even
7from other chains). The linking between entries is done using
8crytographic hashes.
9"""
10from __future__ import unicode_literals, print_function
11
12import os
13import logging
14from .chainstore import HeadTracker, BlockService, Chain
15from .myattr import (
16    v, attr, attrs, attrib, attrib_text, attrib_bytes,
17    attrib_bytes_or_none, attrib_text_or_none, attrib_float,
18)
19
20# ==================================================
21# States
22# =================================================
23
24
25class States:
26    """ Persisting Muacrypt and per-account settings."""
27    _account_pat = "."
28    _own_pat = "own:{id}"
29    _oob_pat = "oob:{id}"
30    _peer_pat = "peer:{id}:{addr}"
31
32    def __init__(self, dirpath):
33        self.dirpath = dirpath
34        blockdir = os.path.join(dirpath, "blocks")
35        if not os.path.exists(blockdir):
36            os.makedirs(blockdir)
37        self._heads = HeadTracker(os.path.join(dirpath, "heads"))
38        self._blocks = BlockService(blockdir)
39
40    def _makechain(self, headname):
41        return Chain(self._blocks, self._heads, headname)
42
43    def get_accountmanager_state(self):
44        chain = self._makechain(self._account_pat)
45        return AccountManagerState(chain)
46
47    def get_account_names(self):
48        return sorted(self._heads._getheads(prefix=self._own_pat.format(id="")))
49
50    def get_num_peers(self, account):
51        return len(self.get_peername_list())
52
53    def get_peername_list(self, account_name):
54        prefix = self._peer_pat.format(id=account_name, addr="")
55        return sorted(self._heads._getheads(prefix=prefix))
56
57    def get_peerstate(self, account_name, addr):
58        head_name = self._peer_pat.format(id=account_name, addr=addr)
59        chain = self._makechain(head_name)
60        return PeerState(chain)
61
62    def get_ownstate(self, account_name):
63        head_name = self._own_pat.format(id=account_name)
64        chain = self._makechain(head_name)
65        return OwnState(chain)
66
67    def get_own_gpghome(self, account_name):
68        return os.path.join(self.dirpath, "gpg", account_name)
69
70    def get_oobstate(self, account_name):
71        head_name = self._oob_pat.format(id=account_name)
72        chain = self._makechain(head_name)
73        return OOBState(chain)
74
75    def remove_account(self, account_name):
76        def match_account(key, value):
77            l = key.split(":", 2)
78            if l[0] in ("own", "peer") and l[1] == account_name:
79                return True
80        self._heads.remove_if(match_account)
81
82# ===========================================================
83# PeerState for keeping track of incoming messages per peer
84# ===========================================================
85
86
87@attr.s
88class MsgEntry(object):
89    TAG = "msg"
90    msg_id = attrib_text()
91    msg_date = attrib_float()
92    prefer_encrypt = attrib(validator=v.in_(['nopreference', 'mutual']))
93    keydata = attrib_bytes()
94    keyhandle = attrib_text()
95
96
97@attr.s
98class MsgGossipEntry(object):
99    TAG = "mge"
100    msg_id = attrib_text()
101    msg_date = attrib_float()
102    keydata = attrib_bytes()
103    keyhandle = attrib_text()
104
105
106@attrs
107class PeerState(object):
108    """Synthesized Autocrypt state from parsing messages from a peer. """
109    _chain = attrib()
110
111    def __str__(self):
112        return "PeerState addr={addr} key={keyhandle}".format(
113            addr=self.addr, keyhandle=self.public_keyhandle
114        )
115
116    @property
117    def addr(self):
118        return self._chain.name.split(":", 2)[-1]
119
120    @property
121    def last_seen(self):
122        return getattr(self._latest_msg_entry(), "msg_date", 0.0)
123
124    @property
125    def autocrypt_timestamp(self):
126        return getattr(self._latest_ac_entry(), "msg_date", 0.0)
127
128    @property
129    def public_keyhandle(self):
130        return getattr(self.entry_for_encryption(), "keyhandle", '')
131
132    @property
133    def public_keydata(self):
134        return getattr(self.entry_for_encryption(), "keydata", b'')
135
136    def has_direct_key(self):
137        return bool(getattr(self._latest_ac_entry(), "keyhandle", ''))
138
139    def entry_for_encryption(self):
140        direct = self._latest_ac_entry()
141        # TODO: perform propper checks on usability of ac entry here
142        if getattr(direct, "keyhandle", None):
143            return direct
144        else:
145            return self.latest_gossip_entry()
146
147    @property
148    def prefer_encrypt(self):
149        return getattr(self.entry_for_encryption(), "prefer_encrypt", '')
150
151    def _latest_ac_entry(self):
152        """ Return latest message with Autocrypt header. """
153        for entry in self._chain.iter_entries(MsgEntry):
154            if entry.keydata:
155                return entry
156
157    def latest_gossip_entry(self):
158        """ Return latest gossip entry. """
159        return self._chain.latest_entry_of(MsgGossipEntry)
160
161    def _latest_msg_entry(self):
162        """ Return latest message with or without Autocrypt header. """
163        return self._chain.latest_entry_of(MsgEntry)
164
165    def has_message(self, msg_id):
166        return self.get_message_entry(msg_id) is not None
167
168    def get_message_entry(self, msg_id, class_=MsgEntry):
169        # XXX make this less expensive
170        for entry in self._chain.iter_entries(class_):
171            if entry.msg_id == msg_id:
172                return entry
173
174    # methods which modify/add state
175    def update_from_msg(self, msg_id, effective_date, prefer_encrypt,
176                        keydata, keyhandle):
177        if effective_date < self.autocrypt_timestamp:
178            return
179        entry = self.get_message_entry(msg_id)
180        if entry is not None:
181            if (entry.msg_date == effective_date and
182                    entry.keydata == keydata and
183                    entry.keyhandle == keyhandle and
184                    entry.prefer_encrypt == prefer_encrypt):
185                return
186
187        if not keydata:
188            if effective_date > self.last_seen:
189                self._append_noac_entry(
190                    msg_id=msg_id, msg_date=effective_date,
191                )
192                logging.debug("append noac %s", msg_id)
193            return
194
195        self._append_ac_entry(
196            msg_id=msg_id, msg_date=effective_date,
197            prefer_encrypt=prefer_encrypt,
198            keydata=keydata or b'', keyhandle=keyhandle or '',
199        )
200
201    def update_from_msg_gossip(self, msg_id, effective_date, keydata, keyhandle):
202        if effective_date < self.autocrypt_timestamp:
203            return
204        assert keydata
205        entry = self.get_message_entry(msg_id, class_=MsgGossipEntry)
206        if entry is not None:
207            if (entry.msg_date == effective_date and
208                    entry.keydata == keydata and
209                    entry.keyhandle == keyhandle):
210                return
211        self._append_ac_gossip_entry(
212            msg_id=msg_id, msg_date=effective_date,
213            keydata=keydata, keyhandle=keyhandle,
214        )
215
216    def _append_ac_entry(self, msg_id, msg_date, prefer_encrypt, keydata, keyhandle):
217        """append an Autocrypt message entry. """
218        self._chain.append_entry(MsgEntry(
219            msg_id=msg_id, msg_date=msg_date, prefer_encrypt=prefer_encrypt,
220            keydata=keydata, keyhandle=keyhandle))
221
222    def _append_ac_gossip_entry(self, msg_id, msg_date, keydata, keyhandle):
223        """append an Autocrypt gossip entry. """
224        self._chain.append_entry(MsgGossipEntry(
225            msg_id=msg_id, msg_date=msg_date,
226            keydata=keydata, keyhandle=keyhandle))
227
228    def _append_noac_entry(self, msg_id, msg_date):
229        """append a non-Autocrypt message entry. """
230        self._chain.append_entry(MsgEntry(
231            msg_id=msg_id, msg_date=msg_date,
232            prefer_encrypt="nopreference", keyhandle="", keydata=b""
233        ))
234
235
236# ===========================================================
237# OwnState keeps track of own crypto settings
238# ===========================================================
239
240def config_property(name):
241    def get(self):
242        return getattr(self._latest_config(), name)
243    return property(get)
244
245
246@attr.s
247class KeygenEntry(object):
248    TAG = "keygen"
249    keydata = attrib_bytes_or_none()
250    keyhandle = attrib_text_or_none()
251
252
253def convert_bytes(x):
254    if hasattr(x, "decode"):
255        return x.decode("ascii")
256    return x
257
258
259@attr.s
260class OwnConfigEntry(object):
261    TAG = "cfg"
262    prefer_encrypt = attrib(validator=v.in_(['nopreference', 'mutual']), converter=convert_bytes)
263    name = attrib_text()
264    email_regex = attrib_text()
265    gpgmode = attrib(validator=v.in_(['system', 'own']))
266    gpgbin = attrib_text()
267
268
269@attrs
270class OwnState(object):
271    """Synthesized own state for an account. """
272    _chain = attrib()
273
274    def __str__(self):
275        return "OwnState key={}".format(self.keyhandle)
276
277    name = config_property("name")
278    email_regex = config_property("email_regex")
279    gpgmode = config_property("gpgmode")
280    gpgbin = config_property("gpgbin")
281    prefer_encrypt = config_property("prefer_encrypt")
282
283    @property
284    def keyhandle(self):
285        return self._latest_keygen().keyhandle
286
287    def exists(self):
288        return self.name
289
290    def _latest_keygen(self):
291        return self._chain.latest_entry_of(KeygenEntry)
292
293    def _latest_config(self):
294        return self._chain.latest_entry_of(OwnConfigEntry)
295
296    # methods which modify/add state
297    def new_config(self, name, prefer_encrypt, email_regex, gpgmode, gpgbin):
298        self._chain.append_entry(OwnConfigEntry(
299            name=name, prefer_encrypt=prefer_encrypt, email_regex=email_regex,
300            gpgmode=gpgmode, gpgbin=gpgbin,
301        ))
302
303    def change_config(self, **kwargs):
304        entry = self._latest_config()
305        new_entry = attr.evolve(entry, **kwargs)
306        if new_entry != entry:
307            self._chain.append_entry(new_entry)
308            return True
309
310    def append_keygen(self, keydata, keyhandle):
311        self._chain.append_entry(KeygenEntry(
312            keydata=keydata,
313            keyhandle=keyhandle
314        ))
315
316    def is_configured(self):
317        return self._latest_config() and self._latest_keygen()
318
319
320# ===========================================================
321# OOBChain keeps track of out-of-band verifications
322# ===========================================================
323
324@attr.s
325class VerificationEntry(object):
326    TAG = "oobverify"
327    addr = attrib_text()
328    public_keydata = attrib_bytes()
329    origin = attrib(validator=v.in_(["self", "peer"]))
330
331
332@attrs
333class OOBState(object):
334    """Synthesized Out of Band verification state for an account. """
335    _chain = attrib()
336
337    def get_verification(self, addr):
338        for entry in self._chain.iter_entries(VerificationEntry):
339            if addr == entry.addr:
340                return entry
341
342    def append_self_verification(self, addr, public_keydata):
343        self._chain.append_entry(VerificationEntry(
344            addr=addr,
345            public_keydata=public_keydata,
346            origin="self",
347        ))
348
349    def append_peer_verification(self, addr, public_keydata):
350        self._chain.append_entry(VerificationEntry(
351            addr=addr,
352            public_keydata=public_keydata,
353            origin="peer",
354        ))
355
356
357# ===========================================================
358# AccountManagerState keeps track of account modifications
359# ===========================================================
360
361@attr.s
362class AConfigEntry(object):
363    TAG = "acfg"
364    version = attrib_text()
365
366
367@attrs
368class AccountManagerState(object):
369    """Synthesized AccountManagerState. """
370    _chain = attrib()
371
372    def _latest_config(self):
373        return self._chain.latest_entry_of(AConfigEntry)
374
375    @property
376    def version(self):
377        return getattr(self._latest_config(), "version", None)
378
379    def __str__(self):
380        return "AccountManagerState version={version}".format(version=self.version)
381
382    def set_version(self, version):
383        assert not self._latest_config()
384        self._chain.append_entry(AConfigEntry(version=version))
385