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