memoize call to get_key
[keymanager.git] / src / leap / keymanager / __init__.py
1 # -*- coding: utf-8 -*-
2 # __init__.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 Key Manager is a Nicknym agent for LEAP client.
19 """
20 # let's do a little sanity check to see if we're using the wrong gnupg
21 import sys
22
23 try:
24     from gnupg.gnupg import GPGUtilities
25     assert(GPGUtilities)  # pyflakes happy
26     from gnupg import __version__
27     from distutils.version import LooseVersion as V
28     assert(V(__version__) >= V('1.2.3'))
29
30 except (ImportError, AssertionError):
31     print "*******"
32     print "Ooops! It looks like there is a conflict in the installed version "
33     print "of gnupg."
34     print
35     print "Disclaimer: Ideally, we would need to work a patch and propose the "
36     print "merge to upstream. But until then do: "
37     print
38     print "% pip uninstall python-gnupg"
39     print "% pip install gnupg"
40     print "*******"
41     sys.exit(1)
42
43 import logging
44 import requests
45
46 from leap.common.check import leap_assert, leap_assert_type
47 from leap.common.events import signal
48 from leap.common.events import events_pb2 as proto
49 from leap.common.decorators import memoized_method
50
51 from leap.keymanager.errors import KeyNotFound
52
53 from leap.keymanager.keys import (
54     EncryptionKey,
55     build_key_from_dict,
56     KEYMANAGER_KEY_TAG,
57     TAGS_PRIVATE_INDEX,
58 )
59 from leap.keymanager.openpgp import (
60     OpenPGPKey,
61     OpenPGPScheme,
62 )
63
64 logger = logging.getLogger(__name__)
65
66
67 #
68 # The Key Manager
69 #
70
71 class KeyManager(object):
72
73     #
74     # server's key storage constants
75     #
76
77     OPENPGP_KEY = 'openpgp'
78     PUBKEY_KEY = "user[public_key]"
79
80     def __init__(self, address, nickserver_uri, soledad, session_id=None,
81                  ca_cert_path=None, api_uri=None, api_version=None, uid=None,
82                  gpgbinary=None):
83         """
84         Initialize a Key Manager for user's C{address} with provider's
85         nickserver reachable in C{url}.
86
87         :param address: The address of the user of this Key Manager.
88         :type address: str
89         :param url: The URL of the nickserver.
90         :type url: str
91         :param soledad: A Soledad instance for local storage of keys.
92         :type soledad: leap.soledad.Soledad
93         :param session_id: The session ID for interacting with the webapp API.
94         :type session_id: str
95         :param ca_cert_path: The path to the CA certificate.
96         :type ca_cert_path: str
97         :param api_uri: The URI of the webapp API.
98         :type api_uri: str
99         :param api_version: The version of the webapp API.
100         :type api_version: str
101         :param uid: The users' UID.
102         :type uid: str
103         :param gpgbinary: Name for GnuPG binary executable.
104         :type gpgbinary: C{str}
105         """
106         self._address = address
107         self._nickserver_uri = nickserver_uri
108         self._soledad = soledad
109         self._session_id = session_id
110         self.ca_cert_path = ca_cert_path
111         self.api_uri = api_uri
112         self.api_version = api_version
113         self.uid = uid
114         # a dict to map key types to their handlers
115         self._wrapper_map = {
116             OpenPGPKey: OpenPGPScheme(soledad, gpgbinary=gpgbinary),
117             # other types of key will be added to this mapper.
118         }
119         # the following are used to perform https requests
120         self._fetcher = requests
121         self._session = self._fetcher.session()
122
123     #
124     # utilities
125     #
126
127     def _key_class_from_type(self, ktype):
128         """
129         Return key class from string representation of key type.
130         """
131         return filter(
132             lambda klass: str(klass) == ktype,
133             self._wrapper_map).pop()
134
135     def _get(self, uri, data=None):
136         """
137         Send a GET request to C{uri} containing C{data}.
138
139         :param uri: The URI of the request.
140         :type uri: str
141         :param data: The body of the request.
142         :type data: dict, str or file
143
144         :return: The response to the request.
145         :rtype: requests.Response
146         """
147         leap_assert(
148             self._ca_cert_path is not None,
149             'We need the CA certificate path!')
150         res = self._fetcher.get(uri, data=data, verify=self._ca_cert_path)
151         # Nickserver now returns 404 for key not found and 500 for
152         # other cases (like key too small), so we are skipping this
153         # check for the time being
154         # res.raise_for_status()
155
156         # Responses are now text/plain, although it's json anyway, but
157         # this will fail when it shouldn't
158         # leap_assert(
159         #     res.headers['content-type'].startswith('application/json'),
160         #     'Content-type is not JSON.')
161         return res
162
163     def _put(self, uri, data=None):
164         """
165         Send a PUT request to C{uri} containing C{data}.
166
167         The request will be sent using the configured CA certificate path to
168         verify the server certificate and the configured session id for
169         authentication.
170
171         :param uri: The URI of the request.
172         :type uri: str
173         :param data: The body of the request.
174         :type data: dict, str or file
175
176         :return: The response to the request.
177         :rtype: requests.Response
178         """
179         leap_assert(
180             self._ca_cert_path is not None,
181             'We need the CA certificate path!')
182         leap_assert(
183             self._session_id is not None,
184             'We need a session_id to interact with webapp!')
185         res = self._fetcher.put(
186             uri, data=data, verify=self._ca_cert_path,
187             cookies={'_session_id': self._session_id})
188         # assert that the response is valid
189         res.raise_for_status()
190         return res
191
192     def _fetch_keys_from_server(self, address):
193         """
194         Fetch keys bound to C{address} from nickserver and insert them in
195         local database.
196
197         :param address: The address bound to the keys.
198         :type address: str
199
200         @raise KeyNotFound: If the key was not found on nickserver.
201         """
202         # request keys from the nickserver
203         res = None
204         try:
205             res = self._get(self._nickserver_uri, {'address': address})
206             server_keys = res.json()
207             # insert keys in local database
208             if self.OPENPGP_KEY in server_keys:
209                 self._wrapper_map[OpenPGPKey].put_ascii_key(
210                     server_keys['openpgp'])
211         except Exception as e:
212             logger.warning("Error retrieving the keys: %r" % (e,))
213             if res:
214                 logger.warning("%s" % (res.content,))
215
216     #
217     # key management
218     #
219
220     def send_key(self, ktype):
221         """
222         Send user's key of type C{ktype} to provider.
223
224         Public key bound to user's is sent to provider, which will sign it and
225         replace any prior keys for the same address in its database.
226
227         If C{send_private} is True, then the private key is encrypted with
228         C{password} and sent to server in the same request, together with a
229         hash string of user's address and password. The encrypted private key
230         will be saved in the server in a way it is publicly retrievable
231         through the hash string.
232
233         :param ktype: The type of the key.
234         :type ktype: KeyType
235
236         @raise KeyNotFound: If the key was not found in local database.
237         """
238         leap_assert(
239             ktype is OpenPGPKey,
240             'For now we only know how to send OpenPGP public keys.')
241         # prepare the public key bound to address
242         pubkey = self.get_key(
243             self._address, ktype, private=False, fetch_remote=False)
244         data = {
245             self.PUBKEY_KEY: pubkey.key_data
246         }
247         uri = "%s/%s/users/%s.json" % (
248             self._api_uri,
249             self._api_version,
250             self._uid)
251         self._put(uri, data)
252         signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address)
253
254     @memoized_method
255     def get_key(self, address, ktype, private=False, fetch_remote=True):
256         """
257         Return a key of type C{ktype} bound to C{address}.
258
259         First, search for the key in local storage. If it is not available,
260         then try to fetch from nickserver.
261
262         :param address: The address bound to the key.
263         :type address: str
264         :param ktype: The type of the key.
265         :type ktype: KeyType
266         :param private: Look for a private key instead of a public one?
267         :type private: bool
268
269         :return: A key of type C{ktype} bound to C{address}.
270         :rtype: EncryptionKey
271         :raise KeyNotFound: If the key was not found both locally and in
272                             keyserver.
273         """
274         logger.debug("getting key for %s" % (address,))
275         leap_assert(
276             ktype in self._wrapper_map,
277             'Unkown key type: %s.' % str(ktype))
278         try:
279             signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address)
280             # return key if it exists in local database
281             key = self._wrapper_map[ktype].get_key(address, private=private)
282             signal(proto.KEYMANAGER_KEY_FOUND, address)
283
284             return key
285         except KeyNotFound:
286             signal(proto.KEYMANAGER_KEY_NOT_FOUND, address)
287
288             # we will only try to fetch a key from nickserver if fetch_remote
289             # is True and the key is not private.
290             if fetch_remote is False or private is True:
291                 raise
292
293             signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address)
294             self._fetch_keys_from_server(address)
295             key = self._wrapper_map[ktype].get_key(address, private=False)
296             signal(proto.KEYMANAGER_KEY_FOUND, address)
297
298             return key
299
300     def get_all_keys_in_local_db(self, private=False):
301         """
302         Return all keys stored in local database.
303
304         :return: A list with all keys in local db.
305         :rtype: list
306         """
307         return map(
308             lambda doc: build_key_from_dict(
309                 self._key_class_from_type(doc.content['type']),
310                 doc.content['address'],
311                 doc.content),
312             self._soledad.get_from_index(
313                 TAGS_PRIVATE_INDEX,
314                 KEYMANAGER_KEY_TAG,
315                 '1' if private else '0'))
316
317     def refresh_keys(self):
318         """
319         Fetch keys from nickserver and update them locally.
320         """
321         addresses = set(map(
322             lambda doc: doc.address,
323             self.get_all_keys_in_local_db(private=False)))
324         for address in addresses:
325             # do not attempt to refresh our own key
326             if address == self._address:
327                 continue
328             self._fetch_keys_from_server(address)
329
330     def gen_key(self, ktype):
331         """
332         Generate a key of type C{ktype} bound to the user's address.
333
334         :param ktype: The type of the key.
335         :type ktype: KeyType
336
337         :return: The generated key.
338         :rtype: EncryptionKey
339         """
340         signal(proto.KEYMANAGER_STARTED_KEY_GENERATION, self._address)
341         key = self._wrapper_map[ktype].gen_key(self._address)
342         signal(proto.KEYMANAGER_FINISHED_KEY_GENERATION, self._address)
343
344         return key
345
346     #
347     # Setters/getters
348     #
349
350     def _get_session_id(self):
351         return self._session_id
352
353     def _set_session_id(self, session_id):
354         self._session_id = session_id
355
356     session_id = property(
357         _get_session_id, _set_session_id, doc='The session id.')
358
359     def _get_ca_cert_path(self):
360         return self._ca_cert_path
361
362     def _set_ca_cert_path(self, ca_cert_path):
363         self._ca_cert_path = ca_cert_path
364
365     ca_cert_path = property(
366         _get_ca_cert_path, _set_ca_cert_path,
367         doc='The path to the CA certificate.')
368
369     def _get_api_uri(self):
370         return self._api_uri
371
372     def _set_api_uri(self, api_uri):
373         self._api_uri = api_uri
374
375     api_uri = property(
376         _get_api_uri, _set_api_uri, doc='The webapp API URI.')
377
378     def _get_api_version(self):
379         return self._api_version
380
381     def _set_api_version(self, api_version):
382         self._api_version = api_version
383
384     api_version = property(
385         _get_api_version, _set_api_version, doc='The webapp API version.')
386
387     def _get_uid(self):
388         return self._uid
389
390     def _set_uid(self, uid):
391         self._uid = uid
392
393     uid = property(
394         _get_uid, _set_uid, doc='The uid of the user.')
395
396     #
397     # encrypt/decrypt and sign/verify API
398     #
399
400     def encrypt(self, data, pubkey, passphrase=None, sign=None,
401                 cipher_algo='AES256'):
402         """
403         Encrypt C{data} using public @{key} and sign with C{sign} key.
404
405         :param data: The data to be encrypted.
406         :type data: str
407         :param pubkey: The key used to encrypt.
408         :type pubkey: EncryptionKey
409         :param sign: The key used for signing.
410         :type sign: EncryptionKey
411         :param cipher_algo: The cipher algorithm to use.
412         :type cipher_algo: str
413
414         :return: The encrypted data.
415         :rtype: str
416         """
417         leap_assert_type(pubkey, EncryptionKey)
418         leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.')
419         leap_assert(pubkey.private is False, 'Key is not public.')
420         return self._wrapper_map[pubkey.__class__].encrypt(
421             data, pubkey, passphrase, sign)
422
423     def decrypt(self, data, privkey, passphrase=None, verify=None):
424         """
425         Decrypt C{data} using private @{privkey} and verify with C{verify} key.
426
427         :param data: The data to be decrypted.
428         :type data: str
429         :param privkey: The key used to decrypt.
430         :type privkey: OpenPGPKey
431         :param passphrase: The passphrase for the secret key used for
432                            decryption.
433         :type passphrase: str
434         :param verify: The key used to verify a signature.
435         :type verify: OpenPGPKey
436
437         :return: The decrypted data.
438         :rtype: str
439
440         @raise InvalidSignature: Raised if unable to verify the signature with
441             C{verify} key.
442         """
443         leap_assert_type(privkey, EncryptionKey)
444         leap_assert(
445             privkey.__class__ in self._wrapper_map,
446             'Unknown key type.')
447         leap_assert(privkey.private is True, 'Key is not private.')
448         return self._wrapper_map[privkey.__class__].decrypt(
449             data, privkey, passphrase, verify)
450
451     def sign(self, data, privkey, digest_algo='SHA512', clearsign=False,
452              detach=True, binary=False):
453         """
454         Sign C{data} with C{privkey}.
455
456         :param data: The data to be signed.
457         :type data: str
458
459         :param privkey: The private key to be used to sign.
460         :type privkey: EncryptionKey
461         :param digest_algo: The hash digest to use.
462         :type digest_algo: str
463         :param clearsign: If True, create a cleartext signature.
464         :type clearsign: bool
465         :param detach: If True, create a detached signature.
466         :type detach: bool
467         :param binary: If True, do not ascii armour the output.
468         :type binary: bool
469
470         :return: The signed data.
471         :rtype: str
472         """
473         leap_assert_type(privkey, EncryptionKey)
474         leap_assert(
475             privkey.__class__ in self._wrapper_map,
476             'Unknown key type.')
477         leap_assert(privkey.private is True, 'Key is not private.')
478         return self._wrapper_map[privkey.__class__].sign(
479             data, privkey, digest_algo=digest_algo, clearsign=clearsign,
480             detach=detach, binary=binary)
481
482     def verify(self, data, pubkey, detached_sig=None):
483         """
484         Verify signed C{data} with C{pubkey}, eventually using
485         C{detached_sig}.
486
487         :param data: The data to be verified.
488         :type data: str
489         :param pubkey: The public key to be used on verification.
490         :type pubkey: EncryptionKey
491         :param detached_sig: A detached signature. If given, C{data} is
492                              verified using this detached signature.
493         :type detached_sig: str
494
495         :return: The signed data.
496         :rtype: str
497         """
498         leap_assert_type(pubkey, EncryptionKey)
499         leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.')
500         leap_assert(pubkey.private is False, 'Key is not public.')
501         return self._wrapper_map[pubkey.__class__].verify(
502             data, pubkey, detached_sig=detached_sig)
503
504     def parse_openpgp_ascii_key(self, key_data):
505         """
506         Parses an ascii armored key (or key pair) data and returns
507         the OpenPGPKey keys.
508
509         :param key_data: the key data to be parsed.
510         :type key_data: str or unicode
511
512         :returns: the public key and private key (if applies) for that data.
513         :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
514                 the tuple may have one or both components None
515         """
516         return self._wrapper_map[OpenPGPKey].parse_ascii_key(key_data)
517
518     def delete_key(self, key):
519         """
520         Remove C{key} from storage.
521
522         May raise:
523             openpgp.errors.KeyNotFound
524             openpgp.errors.KeyAttributesDiffer
525
526         :param key: The key to be removed.
527         :type key: EncryptionKey
528         """
529         try:
530             self._wrapper_map[type(key)].delete_key(key)
531         except IndexError as e:
532             leap_assert(False, "Unsupported key type. Error {0!r}".format(e))
533
534     def put_key(self, key):
535         """
536         Put C{key} in local storage.
537
538         :param key: The key to be stored.
539         :type key: OpenPGPKey
540         """
541         try:
542             self._wrapper_map[type(key)].put_key(key)
543         except IndexError as e:
544             leap_assert(False, "Unsupported key type. Error {0!r}".format(e))
545
546 from ._version import get_versions
547 __version__ = get_versions()['version']
548 del get_versions