41f352ecce902ee06c20bd1e8d6f4c1a81662da1
[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__ as _gnupg_version
27     from pkg_resources import parse_version
28     assert(parse_version(_gnupg_version) >= parse_version('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, token=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 token: The token for interacting with the webapp API.
94         :type token: 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._token = token
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._token is not None,
184             'We need a token to interact with webapp!')
185         res = self._fetcher.put(
186             uri, data=data, verify=self._ca_cert_path,
187             headers={'Authorization': 'Token token=%s' % self._token})
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             res.raise_for_status()
207             server_keys = res.json()
208             # insert keys in local database
209             if self.OPENPGP_KEY in server_keys:
210                 self._wrapper_map[OpenPGPKey].put_ascii_key(
211                     server_keys['openpgp'])
212         except requests.exceptions.HTTPError as e:
213             if e.response.status_code == 404:
214                 raise KeyNotFound(address)
215             logger.warning("HTTP error retrieving key: %r" % (e,))
216             logger.warning("%s" % (res.content,))
217         except Exception as e:
218             logger.warning("Error retrieving key: %r" % (e,))
219
220     #
221     # key management
222     #
223
224     def send_key(self, ktype):
225         """
226         Send user's key of type C{ktype} to provider.
227
228         Public key bound to user's is sent to provider, which will sign it and
229         replace any prior keys for the same address in its database.
230
231         If C{send_private} is True, then the private key is encrypted with
232         C{password} and sent to server in the same request, together with a
233         hash string of user's address and password. The encrypted private key
234         will be saved in the server in a way it is publicly retrievable
235         through the hash string.
236
237         :param ktype: The type of the key.
238         :type ktype: KeyType
239
240         :raise KeyNotFound: If the key was not found in local database.
241         """
242         leap_assert(
243             ktype is OpenPGPKey,
244             'For now we only know how to send OpenPGP public keys.')
245         # prepare the public key bound to address
246         pubkey = self.get_key(
247             self._address, ktype, private=False, fetch_remote=False)
248         data = {
249             self.PUBKEY_KEY: pubkey.key_data
250         }
251         uri = "%s/%s/users/%s.json" % (
252             self._api_uri,
253             self._api_version,
254             self._uid)
255         self._put(uri, data)
256         signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address)
257
258     @memoized_method
259     def get_key_from_cache(self, *args, **kwargs):
260         """
261         Public interface to `get_key`, that is memoized.
262         """
263         return self.get_key(*args, **kwargs)
264
265     def get_key(self, address, ktype, private=False, fetch_remote=True):
266         """
267         Return a key of type C{ktype} bound to C{address}.
268
269         First, search for the key in local storage. If it is not available,
270         then try to fetch from nickserver.
271
272         :param address: The address bound to the key.
273         :type address: str
274         :param ktype: The type of the key.
275         :type ktype: KeyType
276         :param private: Look for a private key instead of a public one?
277         :type private: bool
278
279         :return: A key of type C{ktype} bound to C{address}.
280         :rtype: EncryptionKey
281         :raise KeyNotFound: If the key was not found both locally and in
282                             keyserver.
283         """
284         logger.debug("getting key for %s" % (address,))
285         leap_assert(
286             ktype in self._wrapper_map,
287             'Unkown key type: %s.' % str(ktype))
288         try:
289             signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address)
290             # return key if it exists in local database
291             key = self._wrapper_map[ktype].get_key(address, private=private)
292             signal(proto.KEYMANAGER_KEY_FOUND, address)
293
294             return key
295         except KeyNotFound:
296             signal(proto.KEYMANAGER_KEY_NOT_FOUND, address)
297
298             # we will only try to fetch a key from nickserver if fetch_remote
299             # is True and the key is not private.
300             if fetch_remote is False or private is True:
301                 raise
302
303             signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address)
304             self._fetch_keys_from_server(address)  # might raise KeyNotFound
305             key = self._wrapper_map[ktype].get_key(address, private=False)
306             signal(proto.KEYMANAGER_KEY_FOUND, address)
307
308             return key
309
310     def get_all_keys_in_local_db(self, private=False):
311         """
312         Return all keys stored in local database.
313
314         :return: A list with all keys in local db.
315         :rtype: list
316         """
317         return map(
318             lambda doc: build_key_from_dict(
319                 self._key_class_from_type(doc.content['type']),
320                 doc.content['address'],
321                 doc.content),
322             self._soledad.get_from_index(
323                 TAGS_PRIVATE_INDEX,
324                 KEYMANAGER_KEY_TAG,
325                 '1' if private else '0'))
326
327     def refresh_keys(self):
328         """
329         Fetch keys from nickserver and update them locally.
330         """
331         addresses = set(map(
332             lambda doc: doc.address,
333             self.get_all_keys_in_local_db(private=False)))
334         for address in addresses:
335             # do not attempt to refresh our own key
336             if address == self._address:
337                 continue
338             self._fetch_keys_from_server(address)
339
340     def gen_key(self, ktype):
341         """
342         Generate a key of type C{ktype} bound to the user's address.
343
344         :param ktype: The type of the key.
345         :type ktype: KeyType
346
347         :return: The generated key.
348         :rtype: EncryptionKey
349         """
350         signal(proto.KEYMANAGER_STARTED_KEY_GENERATION, self._address)
351         key = self._wrapper_map[ktype].gen_key(self._address)
352         signal(proto.KEYMANAGER_FINISHED_KEY_GENERATION, self._address)
353
354         return key
355
356     #
357     # Setters/getters
358     #
359
360     def _get_token(self):
361         return self._token
362
363     def _set_token(self, token):
364         self._token = token
365
366     token = property(
367         _get_token, _set_token, doc='The session token.')
368
369     def _get_ca_cert_path(self):
370         return self._ca_cert_path
371
372     def _set_ca_cert_path(self, ca_cert_path):
373         self._ca_cert_path = ca_cert_path
374
375     ca_cert_path = property(
376         _get_ca_cert_path, _set_ca_cert_path,
377         doc='The path to the CA certificate.')
378
379     def _get_api_uri(self):
380         return self._api_uri
381
382     def _set_api_uri(self, api_uri):
383         self._api_uri = api_uri
384
385     api_uri = property(
386         _get_api_uri, _set_api_uri, doc='The webapp API URI.')
387
388     def _get_api_version(self):
389         return self._api_version
390
391     def _set_api_version(self, api_version):
392         self._api_version = api_version
393
394     api_version = property(
395         _get_api_version, _set_api_version, doc='The webapp API version.')
396
397     def _get_uid(self):
398         return self._uid
399
400     def _set_uid(self, uid):
401         self._uid = uid
402
403     uid = property(
404         _get_uid, _set_uid, doc='The uid of the user.')
405
406     #
407     # encrypt/decrypt and sign/verify API
408     #
409
410     def encrypt(self, data, pubkey, passphrase=None, sign=None,
411                 cipher_algo='AES256'):
412         """
413         Encrypt C{data} using public @{key} and sign with C{sign} key.
414
415         :param data: The data to be encrypted.
416         :type data: str
417         :param pubkey: The key used to encrypt.
418         :type pubkey: EncryptionKey
419         :param sign: The key used for signing.
420         :type sign: EncryptionKey
421         :param cipher_algo: The cipher algorithm to use.
422         :type cipher_algo: str
423
424         :return: The encrypted data.
425         :rtype: str
426         """
427         leap_assert_type(pubkey, EncryptionKey)
428         leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.')
429         leap_assert(pubkey.private is False, 'Key is not public.')
430         return self._wrapper_map[pubkey.__class__].encrypt(
431             data, pubkey, passphrase, sign)
432
433     def decrypt(self, data, privkey, passphrase=None, verify=None):
434         """
435         Decrypt C{data} using private @{privkey} and verify with C{verify} key.
436
437         :param data: The data to be decrypted.
438         :type data: str
439         :param privkey: The key used to decrypt.
440         :type privkey: OpenPGPKey
441         :param passphrase: The passphrase for the secret key used for
442                            decryption.
443         :type passphrase: str
444         :param verify: The key used to verify a signature.
445         :type verify: OpenPGPKey
446
447         :return: The decrypted data.
448         :rtype: str
449
450         :raise InvalidSignature: Raised if unable to verify the signature with
451             C{verify} key.
452         """
453         leap_assert_type(privkey, EncryptionKey)
454         leap_assert(
455             privkey.__class__ in self._wrapper_map,
456             'Unknown key type.')
457         leap_assert(privkey.private is True, 'Key is not private.')
458         return self._wrapper_map[privkey.__class__].decrypt(
459             data, privkey, passphrase, verify)
460
461     def sign(self, data, privkey, digest_algo='SHA512', clearsign=False,
462              detach=True, binary=False):
463         """
464         Sign C{data} with C{privkey}.
465
466         :param data: The data to be signed.
467         :type data: str
468
469         :param privkey: The private key to be used to sign.
470         :type privkey: EncryptionKey
471         :param digest_algo: The hash digest to use.
472         :type digest_algo: str
473         :param clearsign: If True, create a cleartext signature.
474         :type clearsign: bool
475         :param detach: If True, create a detached signature.
476         :type detach: bool
477         :param binary: If True, do not ascii armour the output.
478         :type binary: bool
479
480         :return: The signed data.
481         :rtype: str
482         """
483         leap_assert_type(privkey, EncryptionKey)
484         leap_assert(
485             privkey.__class__ in self._wrapper_map,
486             'Unknown key type.')
487         leap_assert(privkey.private is True, 'Key is not private.')
488         return self._wrapper_map[privkey.__class__].sign(
489             data, privkey, digest_algo=digest_algo, clearsign=clearsign,
490             detach=detach, binary=binary)
491
492     def verify(self, data, pubkey, detached_sig=None):
493         """
494         Verify signed C{data} with C{pubkey}, eventually using
495         C{detached_sig}.
496
497         :param data: The data to be verified.
498         :type data: str
499         :param pubkey: The public key to be used on verification.
500         :type pubkey: EncryptionKey
501         :param detached_sig: A detached signature. If given, C{data} is
502                              verified using this detached signature.
503         :type detached_sig: str
504
505         :return: The signed data.
506         :rtype: str
507         """
508         leap_assert_type(pubkey, EncryptionKey)
509         leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.')
510         leap_assert(pubkey.private is False, 'Key is not public.')
511         return self._wrapper_map[pubkey.__class__].verify(
512             data, pubkey, detached_sig=detached_sig)
513
514     def parse_openpgp_ascii_key(self, key_data):
515         """
516         Parses an ascii armored key (or key pair) data and returns
517         the OpenPGPKey keys.
518
519         :param key_data: the key data to be parsed.
520         :type key_data: str or unicode
521
522         :returns: the public key and private key (if applies) for that data.
523         :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
524                 the tuple may have one or both components None
525         """
526         return self._wrapper_map[OpenPGPKey].parse_ascii_key(key_data)
527
528     def delete_key(self, key):
529         """
530         Remove C{key} from storage.
531
532         May raise:
533             openpgp.errors.KeyNotFound
534             openpgp.errors.KeyAttributesDiffer
535
536         :param key: The key to be removed.
537         :type key: EncryptionKey
538         """
539         try:
540             self._wrapper_map[type(key)].delete_key(key)
541         except IndexError as e:
542             leap_assert(False, "Unsupported key type. Error {0!r}".format(e))
543
544     def put_key(self, key):
545         """
546         Put C{key} in local storage.
547
548         :param key: The key to be stored.
549         :type key: OpenPGPKey
550         """
551         try:
552             self._wrapper_map[type(key)].put_key(key)
553         except IndexError as e:
554             leap_assert(False, "Unsupported key type. Error {0!r}".format(e))
555
556 from ._version import get_versions
557 __version__ = get_versions()['version']
558 del get_versions