Update docstrings of the public API
[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{nickserver_uri}.
86
87         :param address: The email address of the user of this Key Manager.
88         :type address: str
89         :param nickserver_uri: The URI of the nickserver.
90         :type nickserver_uri: 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 user's 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         :param ktype: The type of the key.
232         :type ktype: KeyType
233
234         :raise KeyNotFound: If the key was not found in local database.
235         """
236         leap_assert(
237             ktype is OpenPGPKey,
238             'For now we only know how to send OpenPGP public keys.')
239         # prepare the public key bound to address
240         pubkey = self.get_key(
241             self._address, ktype, private=False, fetch_remote=False)
242         data = {
243             self.PUBKEY_KEY: pubkey.key_data
244         }
245         uri = "%s/%s/users/%s.json" % (
246             self._api_uri,
247             self._api_version,
248             self._uid)
249         self._put(uri, data)
250         signal(proto.KEYMANAGER_DONE_UPLOADING_KEYS, self._address)
251
252     @memoized_method
253     def get_key_from_cache(self, *args, **kwargs):
254         """
255         Public interface to `get_key`, that is memoized.
256         """
257         return self.get_key(*args, **kwargs)
258
259     def get_key(self, address, ktype, private=False, fetch_remote=True):
260         """
261         Return a key of type C{ktype} bound to C{address}.
262
263         First, search for the key in local storage. If it is not available,
264         then try to fetch from nickserver.
265
266         :param address: The address bound to the key.
267         :type address: str
268         :param ktype: The type of the key.
269         :type ktype: KeyType
270         :param private: Look for a private key instead of a public one?
271         :type private: bool
272         :param fetch_remote: If key not found in local storage try to fetch
273                              from nickserver
274         :type fetch_remote: bool
275
276         :return: A key of type C{ktype} bound to C{address}.
277         :rtype: EncryptionKey
278         :raise KeyNotFound: If the key was not found both locally and in
279                             keyserver.
280         """
281         logger.debug("getting key for %s" % (address,))
282         leap_assert(
283             ktype in self._wrapper_map,
284             'Unkown key type: %s.' % str(ktype))
285         try:
286             signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address)
287             # return key if it exists in local database
288             key = self._wrapper_map[ktype].get_key(address, private=private)
289             signal(proto.KEYMANAGER_KEY_FOUND, address)
290
291             return key
292         except KeyNotFound:
293             signal(proto.KEYMANAGER_KEY_NOT_FOUND, address)
294
295             # we will only try to fetch a key from nickserver if fetch_remote
296             # is True and the key is not private.
297             if fetch_remote is False or private is True:
298                 raise
299
300             signal(proto.KEYMANAGER_LOOKING_FOR_KEY, address)
301             self._fetch_keys_from_server(address)  # might raise KeyNotFound
302             key = self._wrapper_map[ktype].get_key(address, private=False)
303             signal(proto.KEYMANAGER_KEY_FOUND, address)
304
305             return key
306
307     def get_all_keys_in_local_db(self, private=False):
308         """
309         Return all keys stored in local database.
310
311         :param private: Include private keys
312         :type private: bool
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 passphrase: The passphrase for the secret key used for the
420                            signature.
421         :type passphrase: str
422         :param sign: The key used for signing.
423         :type sign: EncryptionKey
424         :param cipher_algo: The cipher algorithm to use.
425         :type cipher_algo: str
426
427         :return: The encrypted data.
428         :rtype: str
429         """
430         leap_assert_type(pubkey, EncryptionKey)
431         leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.')
432         leap_assert(pubkey.private is False, 'Key is not public.')
433         return self._wrapper_map[pubkey.__class__].encrypt(
434             data, pubkey, passphrase, sign)
435
436     def decrypt(self, data, privkey, passphrase=None, verify=None):
437         """
438         Decrypt C{data} using private @{privkey} and verify with C{verify} key.
439
440         :param data: The data to be decrypted.
441         :type data: str
442         :param privkey: The key used to decrypt.
443         :type privkey: OpenPGPKey
444         :param passphrase: The passphrase for the secret key used for
445                            decryption.
446         :type passphrase: str
447         :param verify: The key used to verify a signature.
448         :type verify: OpenPGPKey
449
450         :return: The decrypted data.
451         :rtype: str
452
453         :raise InvalidSignature: Raised if unable to verify the signature with
454                                  C{verify} key.
455         """
456         leap_assert_type(privkey, EncryptionKey)
457         leap_assert(
458             privkey.__class__ in self._wrapper_map,
459             'Unknown key type.')
460         leap_assert(privkey.private is True, 'Key is not private.')
461         return self._wrapper_map[privkey.__class__].decrypt(
462             data, privkey, passphrase, verify)
463
464     def sign(self, data, privkey, digest_algo='SHA512', clearsign=False,
465              detach=True, binary=False):
466         """
467         Sign C{data} with C{privkey}.
468
469         :param data: The data to be signed.
470         :type data: str
471
472         :param privkey: The private key to be used to sign.
473         :type privkey: EncryptionKey
474         :param digest_algo: The hash digest to use.
475         :type digest_algo: str
476         :param clearsign: If True, create a cleartext signature.
477         :type clearsign: bool
478         :param detach: If True, create a detached signature.
479         :type detach: bool
480         :param binary: If True, do not ascii armour the output.
481         :type binary: bool
482
483         :return: The signed data.
484         :rtype: str
485         """
486         leap_assert_type(privkey, EncryptionKey)
487         leap_assert(
488             privkey.__class__ in self._wrapper_map,
489             'Unknown key type.')
490         leap_assert(privkey.private is True, 'Key is not private.')
491         return self._wrapper_map[privkey.__class__].sign(
492             data, privkey, digest_algo=digest_algo, clearsign=clearsign,
493             detach=detach, binary=binary)
494
495     def verify(self, data, pubkey, detached_sig=None):
496         """
497         Verify signed C{data} with C{pubkey}, eventually using
498         C{detached_sig}.
499
500         :param data: The data to be verified.
501         :type data: str
502         :param pubkey: The public key to be used on verification.
503         :type pubkey: EncryptionKey
504         :param detached_sig: A detached signature. If given, C{data} is
505                              verified using this detached signature.
506         :type detached_sig: str
507
508         :return: The signed data.
509         :rtype: str
510         """
511         leap_assert_type(pubkey, EncryptionKey)
512         leap_assert(pubkey.__class__ in self._wrapper_map, 'Unknown key type.')
513         leap_assert(pubkey.private is False, 'Key is not public.')
514         return self._wrapper_map[pubkey.__class__].verify(
515             data, pubkey, detached_sig=detached_sig)
516
517     def parse_openpgp_ascii_key(self, key_data):
518         """
519         Parses an ascii armored key (or key pair) data and returns
520         the OpenPGPKey keys.
521
522         :param key_data: the key data to be parsed.
523         :type key_data: str or unicode
524
525         :returns: the public key and private key (if applies) for that data.
526         :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
527                 the tuple may have one or both components None
528         """
529         return self._wrapper_map[OpenPGPKey].parse_ascii_key(key_data)
530
531     def delete_key(self, key):
532         """
533         Remove C{key} from storage.
534
535         May raise:
536             openpgp.errors.KeyNotFound
537             openpgp.errors.KeyAttributesDiffer
538
539         :param key: The key to be removed.
540         :type key: EncryptionKey
541         """
542         try:
543             self._wrapper_map[type(key)].delete_key(key)
544         except IndexError as e:
545             leap_assert(False, "Unsupported key type. Error {0!r}".format(e))
546
547     def put_key(self, key):
548         """
549         Put C{key} in local storage.
550
551         :param key: The key to be stored.
552         :type key: OpenPGPKey
553         """
554         try:
555             self._wrapper_map[type(key)].put_key(key)
556         except IndexError as e:
557             leap_assert(False, "Unsupported key type. Error {0!r}".format(e))
558
559 from ._version import get_versions
560 __version__ = get_versions()['version']
561 del get_versions