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