[feat] Make EncryptionKey aware of the active address
[keymanager.git] / src / leap / keymanager / keys.py
1 # -*- coding: utf-8 -*-
2 # keys.py
3 # Copyright (C) 2013 LEAP
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18
19 """
20 Abstact key type and encryption scheme representations.
21 """
22
23
24 try:
25     import simplejson as json
26 except ImportError:
27     import json  # noqa
28 import logging
29 import re
30 import time
31 import traceback
32
33
34 from abc import ABCMeta, abstractmethod
35 from datetime import datetime
36 from leap.common.check import leap_assert
37 from twisted.internet import defer
38
39 from leap.keymanager.validation import ValidationLevels
40
41 logger = logging.getLogger(__name__)
42
43
44 #
45 # Dictionary keys used for storing cryptographic keys.
46 #
47
48 KEY_VERSION_KEY = 'version'
49 KEY_UIDS_KEY = 'uids'
50 KEY_ADDRESS_KEY = 'address'
51 KEY_TYPE_KEY = 'type'
52 KEY_FINGERPRINT_KEY = 'fingerprint'
53 KEY_DATA_KEY = 'key_data'
54 KEY_PRIVATE_KEY = 'private'
55 KEY_LENGTH_KEY = 'length'
56 KEY_EXPIRY_DATE_KEY = 'expiry_date'
57 KEY_LAST_AUDITED_AT_KEY = 'last_audited_at'
58 KEY_REFRESHED_AT_KEY = 'refreshed_at'
59 KEY_VALIDATION_KEY = 'validation'
60 KEY_ENCR_USED_KEY = 'encr_used'
61 KEY_SIGN_USED_KEY = 'sign_used'
62 KEY_TAGS_KEY = 'tags'
63
64
65 #
66 # Key storage constants
67 #
68
69 KEYMANAGER_KEY_TAG = 'keymanager-key'
70 KEYMANAGER_ACTIVE_TAG = 'keymanager-active'
71 KEYMANAGER_ACTIVE_TYPE = '-active'
72
73 # Version of the Soledad Document schema,
74 # it should be bumped each time the document format changes
75 KEYMANAGER_DOC_VERSION = 1
76
77
78 #
79 # key indexing constants.
80 #
81
82 TAGS_PRIVATE_INDEX = 'by-tags-private'
83 TYPE_FINGERPRINT_PRIVATE_INDEX = 'by-type-fingerprint-private'
84 TYPE_ADDRESS_PRIVATE_INDEX = 'by-type-address-private'
85 INDEXES = {
86     TAGS_PRIVATE_INDEX: [
87         KEY_TAGS_KEY,
88         'bool(%s)' % KEY_PRIVATE_KEY,
89     ],
90     TYPE_FINGERPRINT_PRIVATE_INDEX: [
91         KEY_TYPE_KEY,
92         KEY_FINGERPRINT_KEY,
93         'bool(%s)' % KEY_PRIVATE_KEY,
94     ],
95     TYPE_ADDRESS_PRIVATE_INDEX: [
96         KEY_TYPE_KEY,
97         KEY_ADDRESS_KEY,
98         'bool(%s)' % KEY_PRIVATE_KEY,
99     ]
100 }
101
102
103 #
104 # Key handling utilities
105 #
106
107 def is_address(address):
108     """
109     Return whether the given C{address} is in the form user@provider.
110
111     :param address: The address to be tested.
112     :type address: str
113     :return: Whether C{address} is in the form user@provider.
114     :rtype: bool
115     """
116     return bool(re.match('[\w.-]+@[\w.-]+', address))
117
118
119 def build_key_from_dict(kClass, key, active=None):
120     """
121     Build an C{kClass} key based on info in C{kdict}.
122
123     :param key: Dictionary with key data.
124     :type key: dict
125     :param active: Dictionary with active data.
126     :type active: dict
127     :return: An instance of the key.
128     :rtype: C{kClass}
129     """
130     address = None
131     validation = ValidationLevels.Weak_Chain
132     last_audited_at = None
133     encr_used = False
134     sign_used = False
135
136     if active:
137         address = active[KEY_ADDRESS_KEY]
138         try:
139             validation = ValidationLevels.get(active[KEY_VALIDATION_KEY])
140         except ValueError:
141             logger.error("Not valid validation level (%s) for key %s",
142                          (active[KEY_VALIDATION_KEY],
143                           active[KEY_FINGERPRINT_KEY]))
144         last_audited_at = _to_datetime(active[KEY_LAST_AUDITED_AT_KEY])
145         encr_used = active[KEY_ENCR_USED_KEY]
146         sign_used = active[KEY_SIGN_USED_KEY]
147
148     expiry_date = _to_datetime(key[KEY_EXPIRY_DATE_KEY])
149     refreshed_at = _to_datetime(key[KEY_REFRESHED_AT_KEY])
150
151     return kClass(
152         address=address,
153         uids=key[KEY_UIDS_KEY],
154         fingerprint=key[KEY_FINGERPRINT_KEY],
155         key_data=key[KEY_DATA_KEY],
156         private=key[KEY_PRIVATE_KEY],
157         length=key[KEY_LENGTH_KEY],
158         expiry_date=expiry_date,
159         last_audited_at=last_audited_at,
160         refreshed_at=refreshed_at,
161         validation=validation,
162         encr_used=encr_used,
163         sign_used=sign_used,
164     )
165
166
167 def _to_datetime(unix_time):
168     if unix_time != 0:
169         return datetime.fromtimestamp(unix_time)
170     else:
171         return None
172
173
174 def _to_unix_time(date):
175     if date is not None:
176         return int(time.mktime(date.timetuple()))
177     else:
178         return 0
179
180
181 #
182 # Abstraction for encryption keys
183 #
184
185 class EncryptionKey(object):
186     """
187     Abstract class for encryption keys.
188
189     A key is "validated" if the nicknym agent has bound the user address to a
190     public key.
191     """
192
193     __metaclass__ = ABCMeta
194
195     def __init__(self, address=None, uids=[], fingerprint="",
196                  key_data="", private=False, length=0, expiry_date=None,
197                  validation=ValidationLevels.Weak_Chain, last_audited_at=None,
198                  refreshed_at=None, encr_used=False, sign_used=False):
199         self.address = address
200         if not uids and address:
201             self.uids = [address]
202         else:
203             self.uids = uids
204         self.fingerprint = fingerprint
205         self.key_data = key_data
206         self.private = private
207         self.length = length
208         self.expiry_date = expiry_date
209
210         self.validation = validation
211         self.last_audited_at = last_audited_at
212         self.refreshed_at = refreshed_at
213         self.encr_used = encr_used
214         self.sign_used = sign_used
215
216     def get_json(self):
217         """
218         Return a JSON string describing this key.
219
220         :return: The JSON string describing this key.
221         :rtype: str
222         """
223         expiry_date = _to_unix_time(self.expiry_date)
224         refreshed_at = _to_unix_time(self.refreshed_at)
225
226         return json.dumps({
227             KEY_UIDS_KEY: self.uids,
228             KEY_TYPE_KEY: self.__class__.__name__,
229             KEY_FINGERPRINT_KEY: self.fingerprint,
230             KEY_DATA_KEY: self.key_data,
231             KEY_PRIVATE_KEY: self.private,
232             KEY_LENGTH_KEY: self.length,
233             KEY_EXPIRY_DATE_KEY: expiry_date,
234             KEY_REFRESHED_AT_KEY: refreshed_at,
235             KEY_VERSION_KEY: KEYMANAGER_DOC_VERSION,
236             KEY_TAGS_KEY: [KEYMANAGER_KEY_TAG],
237         })
238
239     def get_active_json(self):
240         """
241         Return a JSON string describing this key.
242
243         :return: The JSON string describing this key.
244         :rtype: str
245         """
246         last_audited_at = _to_unix_time(self.last_audited_at)
247
248         return json.dumps({
249             KEY_ADDRESS_KEY: self.address,
250             KEY_TYPE_KEY: self.__class__.__name__ + KEYMANAGER_ACTIVE_TYPE,
251             KEY_FINGERPRINT_KEY: self.fingerprint,
252             KEY_PRIVATE_KEY: self.private,
253             KEY_VALIDATION_KEY: str(self.validation),
254             KEY_LAST_AUDITED_AT_KEY: last_audited_at,
255             KEY_ENCR_USED_KEY: self.encr_used,
256             KEY_SIGN_USED_KEY: self.sign_used,
257             KEY_VERSION_KEY: KEYMANAGER_DOC_VERSION,
258             KEY_TAGS_KEY: [KEYMANAGER_ACTIVE_TAG],
259         })
260
261     def __repr__(self):
262         """
263         Representation of this class
264         """
265         return u"<%s 0x%s (%s - %s)>" % (
266             self.__class__.__name__,
267             self.fingerprint,
268             self.address,
269             "priv" if self.private else "publ")
270
271
272 #
273 # Encryption schemes
274 #
275
276 class EncryptionScheme(object):
277     """
278     Abstract class for Encryption Schemes.
279
280     A wrapper for a certain encryption schemes should know how to get and put
281     keys in local storage using Soledad, how to generate new keys and how to
282     find out about possibly encrypted content.
283     """
284
285     __metaclass__ = ABCMeta
286
287     def __init__(self, soledad):
288         """
289         Initialize this Encryption Scheme.
290
291         :param soledad: A Soledad instance for local storage of keys.
292         :type soledad: leap.soledad.Soledad
293         """
294         self._soledad = soledad
295         self.deferred_init = self._init_indexes()
296         self.deferred_init.addCallback(self._migrate_documents_schema)
297
298     def _init_indexes(self):
299         """
300         Initialize the database indexes.
301         """
302         leap_assert(self._soledad is not None,
303                     "Cannot init indexes with null soledad")
304
305         def init_idexes(indexes):
306             deferreds = []
307             db_indexes = dict(indexes)
308             # Loop through the indexes we expect to find.
309             for name, expression in INDEXES.items():
310                 if name not in db_indexes:
311                     # The index does not yet exist.
312                     d = self._soledad.create_index(name, *expression)
313                     deferreds.append(d)
314                 elif expression != db_indexes[name]:
315                     # The index exists but the definition is not what expected,
316                     # so we delete it and add the proper index expression.
317                     d = self._soledad.delete_index(name)
318                     d.addCallback(
319                         lambda _:
320                             self._soledad.create_index(name, *expression))
321                     deferreds.append(d)
322             return defer.gatherResults(deferreds, consumeErrors=True)
323
324         d = self._soledad.list_indexes()
325         d.addCallback(init_idexes)
326         return d
327
328     def _migrate_documents_schema(self, _):
329         from leap.keymanager.migrator import KeyDocumentsMigrator
330         migrator = KeyDocumentsMigrator(self._soledad)
331         return migrator.migrate()
332
333     def _wait_indexes(self, *methods):
334         """
335         Methods that need to wait for the indexes to be ready.
336
337         Heavily based on
338         http://blogs.fluidinfo.com/terry/2009/05/11/a-mixin-class-allowing-python-__init__-methods-to-work-with-twisted-deferreds/
339
340         :param methods: methods that need to wait for the indexes to be ready
341         :type methods: tuple(str)
342         """
343         self.waiting = []
344         self.stored = {}
345
346         def restore(_):
347             for method in self.stored:
348                 setattr(self, method, self.stored[method])
349             for d in self.waiting:
350                 d.callback(None)
351
352         def makeWrapper(method):
353             def wrapper(*args, **kw):
354                 d = defer.Deferred()
355                 d.addCallback(lambda _: self.stored[method](*args, **kw))
356                 self.waiting.append(d)
357                 return d
358             return wrapper
359
360         for method in methods:
361             self.stored[method] = getattr(self, method)
362             setattr(self, method, makeWrapper(method))
363
364         self.deferred_init.addCallback(restore)
365
366     @abstractmethod
367     def get_key(self, address, private=False):
368         """
369         Get key from local storage.
370
371         :param address: The address bound to the key.
372         :type address: str
373         :param private: Look for a private key instead of a public one?
374         :type private: bool
375
376         :return: A Deferred which fires with the EncryptionKey bound to
377                  address, or which fails with KeyNotFound if the key was not
378                  found on local storage.
379         :rtype: Deferred
380         """
381         pass
382
383     @abstractmethod
384     def put_key(self, key):
385         """
386         Put a key in local storage.
387
388         :param key: The key to be stored.
389         :type key: EncryptionKey
390
391         :return: A Deferred which fires when the key is in the storage.
392         :rtype: Deferred
393         """
394         pass
395
396     @abstractmethod
397     def gen_key(self, address):
398         """
399         Generate a new key.
400
401         :param address: The address bound to the key.
402         :type address: str
403
404         :return: The key bound to C{address}.
405         :rtype: EncryptionKey
406         """
407         pass
408
409     @abstractmethod
410     def delete_key(self, key):
411         """
412         Remove C{key} from storage.
413
414         :param key: The key to be removed.
415         :type key: EncryptionKey
416
417         :return: A Deferred which fires when the key is deleted, or which
418                  fails with KeyNotFound if the key was not found on local
419                  storage.
420         :rtype: Deferred
421         """
422         pass
423
424     @abstractmethod
425     def encrypt(self, data, pubkey, passphrase=None, sign=None):
426         """
427         Encrypt C{data} using public @{pubkey} and sign with C{sign} key.
428
429         :param data: The data to be encrypted.
430         :type data: str
431         :param pubkey: The key used to encrypt.
432         :type pubkey: EncryptionKey
433         :param sign: The key used for signing.
434         :type sign: EncryptionKey
435
436         :return: The encrypted data.
437         :rtype: str
438         """
439         pass
440
441     @abstractmethod
442     def decrypt(self, data, privkey, passphrase=None, verify=None):
443         """
444         Decrypt C{data} using private @{privkey} and verify with C{verify} key.
445
446         :param data: The data to be decrypted.
447         :type data: str
448         :param privkey: The key used to decrypt.
449         :type privkey: OpenPGPKey
450         :param verify: The key used to verify a signature.
451         :type verify: OpenPGPKey
452
453         :return: The decrypted data and if signature verifies
454         :rtype: (unicode, bool)
455
456         :raise DecryptError: Raised if failed decrypting for some reason.
457         """
458         pass
459
460     @abstractmethod
461     def sign(self, data, privkey):
462         """
463         Sign C{data} with C{privkey}.
464
465         :param data: The data to be signed.
466         :type data: str
467
468         :param privkey: The private key to be used to sign.
469         :type privkey: EncryptionKey
470
471         :return: The signed data.
472         :rtype: str
473         """
474         pass
475
476     @abstractmethod
477     def verify(self, data, pubkey, detached_sig=None):
478         """
479         Verify signed C{data} with C{pubkey}, eventually using
480         C{detached_sig}.
481
482         :param data: The data to be verified.
483         :type data: str
484         :param pubkey: The public key to be used on verification.
485         :type pubkey: EncryptionKey
486         :param detached_sig: A detached signature. If given, C{data} is
487                              verified against this sdetached signature.
488         :type detached_sig: str
489
490         :return: signature matches
491         :rtype: bool
492         """
493         pass
494
495     def _repair_key_docs(self, doclist):
496         """
497         If there is more than one key for a key id try to self-repair it
498
499         :return: a Deferred that will be fired with the valid key doc once all
500                  the deletions are completed
501         :rtype: Deferred
502         """
503         def log_key_doc(doc):
504             logger.error("\t%s: %s" % (doc.content[KEY_UIDS_KEY],
505                                        doc.content[KEY_FINGERPRINT_KEY]))
506
507         def cmp_key(d1, d2):
508             return cmp(d1.content[KEY_REFRESHED_AT_KEY],
509                        d2.content[KEY_REFRESHED_AT_KEY])
510
511         return self._repair_docs(doclist, cmp_key, log_key_doc)
512
513     def _repair_active_docs(self, doclist):
514         """
515         If there is more than one active doc for an address try to self-repair
516         it
517
518         :return: a Deferred that will be fired with the valid active doc once
519                  all the deletions are completed
520         :rtype: Deferred
521         """
522         def log_active_doc(doc):
523             logger.error("\t%s: %s" % (doc.content[KEY_ADDRESS_KEY],
524                                        doc.content[KEY_FINGERPRINT_KEY]))
525
526         def cmp_active(d1, d2):
527             res = cmp(d1.content[KEY_LAST_AUDITED_AT_KEY],
528                       d2.content[KEY_LAST_AUDITED_AT_KEY])
529             if res != 0:
530                 return res
531
532             used1 = (d1.content[KEY_SIGN_USED_KEY] +
533                      d1.content[KEY_ENCR_USED_KEY])
534             used2 = (d2.content[KEY_SIGN_USED_KEY] +
535                      d2.content[KEY_ENCR_USED_KEY])
536             return cmp(used1, used2)
537
538         return self._repair_docs(doclist, cmp_active, log_active_doc)
539
540     def _repair_docs(self, doclist, cmp_func, log_func):
541         logger.error("BUG ---------------------------------------------------")
542         logger.error("There is more than one doc of type %s:"
543                      % (doclist[0].content[KEY_TYPE_KEY],))
544
545         doclist.sort(cmp=cmp_func, reverse=True)
546         log_func(doclist[0])
547         deferreds = []
548         for doc in doclist[1:]:
549             log_func(doc)
550             d = self._soledad.delete_doc(doc)
551             deferreds.append(d)
552
553         logger.error("")
554         logger.error(traceback.extract_stack())
555         logger.error("BUG (please report above info) ------------------------")
556         d = defer.gatherResults(deferreds, consumeErrors=True)
557         d.addCallback(lambda _: doclist[0])
558         return d