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