Adapt get_key() and send_key() to the spec.
[leap_pycommon.git] / src / leap / common / 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 requests
24
25 try:
26     import simplejson as json
27 except ImportError:
28     import json  # noqa
29
30 from leap.common.check import leap_assert
31 from leap.common.keymanager.errors import (
32     KeyNotFound,
33     NoPasswordGiven,
34 )
35 from leap.common.keymanager.keys import (
36     build_key_from_dict,
37     KEYMANAGER_KEY_TAG,
38 )
39 from leap.common.keymanager.openpgp import (
40     OpenPGPKey,
41     OpenPGPScheme,
42     encrypt_sym,
43 )
44
45
46 #
47 # key indexing constants.
48 #
49
50 TAGS_INDEX = 'by-tags'
51 TAGS_AND_PRIVATE_INDEX = 'by-tags-and-private'
52 INDEXES = {
53     TAGS_INDEX: ['tags'],
54     TAGS_AND_PRIVATE_INDEX: ['tags', 'bool(private)'],
55 }
56
57
58 #
59 # The Key Manager
60 #
61
62 class KeyManager(object):
63
64     #
65     # server's key storage constants
66     #
67
68     OPENPGP_KEY = 'openpgp'
69     PUBKEY_KEY = "user[public_key]"
70
71     def __init__(self, address, nickserver_uri, soledad, session_id=None,
72                  ca_cert_path=None, api_uri=None, api_version=None, uid=None):
73         """
74         Initialize a Key Manager for user's C{address} with provider's
75         nickserver reachable in C{url}.
76
77         @param address: The address of the user of this Key Manager.
78         @type address: str
79         @param url: The URL of the nickserver.
80         @type url: str
81         @param soledad: A Soledad instance for local storage of keys.
82         @type soledad: leap.soledad.Soledad
83         @param session_id: The session ID for interacting with the webapp API.
84         @type session_id: str
85         @param ca_cert_path: The path to the CA certificate.
86         @type ca_cert_path: str
87         @param api_uri: The URI of the webapp API.
88         @type api_uri: str
89         @param api_version: The version of the webapp API.
90         @type api_version: str
91         @param uid: The users' UID.
92         @type uid: str
93         """
94         self._address = address
95         self._nickserver_uri = nickserver_uri
96         self._soledad = soledad
97         self._session_id = session_id
98         self.ca_cert_path = ca_cert_path
99         self.api_uri = api_uri
100         self.api_version = api_version
101         self.uid = uid
102         # a dict to map key types to their handlers
103         self._wrapper_map = {
104             OpenPGPKey: OpenPGPScheme(soledad),
105             # other types of key will be added to this mapper.
106         }
107         # initialize the indexes needed to query the database
108         self._init_indexes()
109         # the following are used to perform https requests
110         self._fetcher = requests
111         self._session = self._fetcher.session()
112
113     #
114     # utilities
115     #
116
117     def _key_class_from_type(self, ktype):
118         """
119         Return key class from string representation of key type.
120         """
121         return filter(
122             lambda klass: str(klass) == ktype,
123             self._wrapper_map).pop()
124
125     def _init_indexes(self):
126         """
127         Initialize the database indexes.
128         """
129         # Ask the database for currently existing indexes.
130         db_indexes = dict(self._soledad.list_indexes())
131         # Loop through the indexes we expect to find.
132         for name, expression in INDEXES.items():
133             if name not in db_indexes:
134                 # The index does not yet exist.
135                 self._soledad.create_index(name, *expression)
136                 continue
137             if expression == db_indexes[name]:
138                 # The index exists and is up to date.
139                 continue
140             # The index exists but the definition is not what expected, so we
141             # delete it and add the proper index expression.
142             self._soledad.delete_index(name)
143             self._soledad.create_index(name, *expression)
144
145     def _get(self, uri, data=None):
146         """
147         Send a GET request to C{uri} containing C{data}.
148
149         @param uri: The URI of the request.
150         @type uri: str
151         @param data: The body of the request.
152         @type data: dict, str or file
153
154         @return: The response to the request.
155         @rtype: requests.Response
156         """
157         leap_assert(
158             self._ca_cert_path is not None,
159             'We need the CA certificate path!')
160         res = self._fetcher.get(uri, data=data, verify=self._ca_cert_path)
161         # assert that the response is valid
162         res.raise_for_status()
163         leap_assert(
164             res.headers['content-type'].startswith('application/json'),
165             'Content-type is not JSON.')
166         return res
167
168     def _put(self, uri, data=None):
169         """
170         Send a PUT request to C{uri} containing C{data}.
171
172         The request will be sent using the configured CA certificate path to
173         verify the server certificate and the configured session id for
174         authentication.
175
176         @param uri: The URI of the request.
177         @type uri: str
178         @param data: The body of the request.
179         @type data: dict, str or file
180
181         @return: The response to the request.
182         @rtype: requests.Response
183         """
184         leap_assert(
185             self._ca_cert_path is not None,
186             'We need the CA certificate path!')
187         leap_assert(
188             self._session_id is not None,
189             'We need a session_id to interact with webapp!')
190         res = self._fetcher.put(
191             uri, data=data, verify=self._ca_cert_path,
192             cookies={'_session_id': self._session_id})
193         # assert that the response is valid
194         res.raise_for_status()
195         return res
196
197     def _fetch_keys_from_server(self, address):
198         """
199         Fetch keys bound to C{address} from nickserver and insert them in
200         local database.
201
202         @param address: The address bound to the keys.
203         @type address: str
204
205         @raise KeyNotFound: If the key was not found on nickserver.
206         """
207         # request keys from the nickserver
208         server_keys = self._get(
209             self._nickserver_uri, {'address': address}).json()
210         # insert keys in local database
211         if self.OPENPGP_KEY in server_keys:
212             self._wrapper_map[OpenPGPKey].put_ascii_key(
213                 server_keys['openpgp'])
214
215     #
216     # key management
217     #
218
219     def send_key(self, ktype):
220         """
221         Send user's key of type C{ktype} to provider.
222
223         Public key bound to user's is sent to provider, which will sign it and
224         replace any prior keys for the same address in its database.
225
226         If C{send_private} is True, then the private key is encrypted with
227         C{password} and sent to server in the same request, together with a
228         hash string of user's address and password. The encrypted private key
229         will be saved in the server in a way it is publicly retrievable
230         through the hash string.
231
232         @param ktype: The type of the key.
233         @type ktype: KeyType
234
235         @raise KeyNotFound: If the key was not found in local database.
236         """
237         leap_assert(
238             ktype is OpenPGPKey,
239             'For now we only know how to send OpenPGP public keys.')
240         # prepare the public key bound to address
241         pubkey = self.get_key(
242             self._address, ktype, private=False, fetch_remote=False)
243         data = {
244             self.PUBKEY_KEY: pubkey.key_data
245         }
246         uri = "%s/%s/users/%s.json" % (
247             self._api_uri,
248             self._api_version,
249             self._uid)
250         self._put(uri, data)
251
252     def get_key(self, address, ktype, private=False, fetch_remote=True):
253         """
254         Return a key of type C{ktype} bound to C{address}.
255
256         First, search for the key in local storage. If it is not available,
257         then try to fetch from nickserver.
258
259         @param address: The address bound to the key.
260         @type address: str
261         @param ktype: The type of the key.
262         @type ktype: KeyType
263         @param private: Look for a private key instead of a public one?
264         @type private: bool
265
266         @return: A key of type C{ktype} bound to C{address}.
267         @rtype: EncryptionKey
268         @raise KeyNotFound: If the key was not found both locally and in
269             keyserver.
270         """
271         leap_assert(
272             ktype in self._wrapper_map,
273             'Unkown key type: %s.' % str(ktype))
274         try:
275             # return key if it exists in local database
276             return self._wrapper_map[ktype].get_key(address, private=private)
277         except KeyNotFound:
278             # we will only try to fetch a key from nickserver if fetch_remote
279             # is True and the key is not private.
280             if fetch_remote is False or private is True:
281                 raise
282             self._fetch_keys_from_server(address)
283             return self._wrapper_map[ktype].get_key(address, private=False)
284
285     def get_all_keys_in_local_db(self, private=False):
286         """
287         Return all keys stored in local database.
288
289         @return: A list with all keys in local db.
290         @rtype: list
291         """
292         return map(
293             lambda doc: build_key_from_dict(
294                 self._key_class_from_type(doc.content['type']),
295                 doc.content['address'],
296                 doc.content),
297             self._soledad.get_from_index(
298                 TAGS_AND_PRIVATE_INDEX,
299                 KEYMANAGER_KEY_TAG,
300                 '1' if private else '0'))
301
302     def refresh_keys(self):
303         """
304         Fetch keys from nickserver and update them locally.
305         """
306         addresses = set(map(
307             lambda doc: doc.address,
308             self.get_all_keys_in_local_db(private=False)))
309         for address in addresses:
310             # do not attempt to refresh our own key
311             if address == self._address:
312                 continue
313             self._fetch_keys_from_server(address)
314
315     def gen_key(self, ktype):
316         """
317         Generate a key of type C{ktype} bound to the user's address.
318
319         @param ktype: The type of the key.
320         @type ktype: KeyType
321
322         @return: The generated key.
323         @rtype: EncryptionKey
324         """
325         return self._wrapper_map[ktype].gen_key(self._address)
326
327     #
328     # Setters/getters
329     #
330
331     def _get_session_id(self):
332         return self._session_id
333
334     def _set_session_id(self, session_id):
335         self._session_id = session_id
336
337     session_id = property(
338         _get_session_id, _set_session_id, doc='The session id.')
339
340     def _get_ca_cert_path(self):
341         return self._ca_cert_path
342
343     def _set_ca_cert_path(self, ca_cert_path):
344         self._ca_cert_path = ca_cert_path
345
346     ca_cert_path = property(
347         _get_ca_cert_path, _set_ca_cert_path,
348         doc='The path to the CA certificate.')
349
350     def _get_api_uri(self):
351         return self._api_uri
352
353     def _set_api_uri(self, api_uri):
354         self._api_uri = api_uri
355
356     api_uri = property(
357         _get_api_uri, _set_api_uri, doc='The webapp API URI.')
358
359     def _get_api_version(self):
360         return self._api_version
361
362     def _set_api_version(self, api_version):
363         self._api_version = api_version
364
365     api_version = property(
366         _get_api_version, _set_api_version, doc='The webapp API version.')
367
368     def _get_uid(self):
369         return self._uid
370
371     def _set_uid(self, uid):
372         self._uid = uid
373
374     uid = property(
375         _get_uid, _set_uid, doc='The uid of the user.')