30a9146cfcd7f220fff3eaadae19068f19837faf
[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 )
38 from leap.common.keymanager.openpgp import (
39     OpenPGPKey,
40     OpenPGPScheme,
41     encrypt_sym,
42 )
43
44
45 TAGS_INDEX = 'by-tags'
46 TAGS_AND_PRIVATE_INDEX = 'by-tags-and-private'
47 INDEXES = {
48     TAGS_INDEX: ['tags'],
49     TAGS_AND_PRIVATE_INDEX: ['tags', 'bool(private)'],
50 }
51
52
53 class KeyManager(object):
54
55     def __init__(self, address, nickserver_url, soledad, token=None):
56         """
57         Initialize a Key Manager for user's C{address} with provider's
58         nickserver reachable in C{url}.
59
60         @param address: The address of the user of this Key Manager.
61         @type address: str
62         @param url: The URL of the nickserver.
63         @type url: str
64         @param soledad: A Soledad instance for local storage of keys.
65         @type soledad: leap.soledad.Soledad
66         """
67         self._address = address
68         self._nickserver_url = nickserver_url
69         self._soledad = soledad
70         self.token = token
71         self._wrapper_map = {
72             OpenPGPKey: OpenPGPScheme(soledad),
73             # other types of key will be added to this mapper.
74         }
75         self._init_indexes()
76         self._fetcher = requests
77
78     #
79     # utilities
80     #
81
82     def _key_class_from_type(self, ktype):
83         """
84         Return key class from string representation of key type.
85         """
86         return filter(
87             lambda klass: str(klass) == ktype,
88             self._wrapper_map).pop()
89
90     def _init_indexes(self):
91         """
92         Initialize the database indexes.
93         """
94         # Ask the database for currently existing indexes.
95         db_indexes = dict(self._soledad.list_indexes())
96         # Loop through the indexes we expect to find.
97         for name, expression in INDEXES.items():
98             if name not in db_indexes:
99                 # The index does not yet exist.
100                 self._soledad.create_index(name, *expression)
101                 continue
102             if expression == db_indexes[name]:
103                 # The index exists and is up to date.
104                 continue
105             # The index exists but the definition is not what expected, so we
106             # delete it and add the proper index expression.
107             self._soledad.delete_index(name)
108             self._soledad.create_index(name, *expression)
109
110     def _get_dict_from_http_json(self, path):
111         """
112         Make a GET HTTP request and return a dictionary containing the
113         response.
114         """
115         response = self._fetcher.get(self._nickserver_url+path)
116         leap_assert(response.status_code == 200, 'Invalid response.')
117         leap_assert(
118             response.headers['content-type'].startswith('application/json')
119             is True,
120             'Content-type is not JSON.')
121         return response.json()
122
123     #
124     # key management
125     #
126
127     def send_key(self, ktype, send_private=False, password=None):
128         """
129         Send user's key of type C{ktype} to provider.
130
131         Public key bound to user's is sent to provider, which will sign it and
132         replace any prior keys for the same address in its database.
133
134         If C{send_private} is True, then the private key is encrypted with
135         C{password} and sent to server in the same request, together with a
136         hash string of user's address and password. The encrypted private key
137         will be saved in the server in a way it is publicly retrievable
138         through the hash string.
139
140         @param ktype: The type of the key.
141         @type ktype: KeyType
142
143         @raise httplib.HTTPException:
144         @raise KeyNotFound: If the key was not found both locally and in
145             keyserver.
146         """
147         # prepare the public key bound to address
148         pubkey = self.get_key(
149             self._address, ktype, private=False, fetch_remote=False)
150         data = {
151             'address': self._address,
152             'keys': [
153                 json.loads(pubkey.get_json()),
154             ]
155         }
156         # prepare the private key bound to address
157         if send_private:
158             if password is None or password == '':
159                 raise NoPasswordGiven('Can\'t send unencrypted private keys!')
160             privkey = self.get_key(
161                 self._address, ktype, private=True, fetch_remote=False)
162             privkey = json.loads(privkey.get_json())
163             privkey.key_data = encrypt_sym(
164                 privkey.key_data, passphrase=password)
165             data['keys'].append(privkey)
166         self._fetcher.put(
167             self._nickserver_url + '/key/' + self._address,
168             data=data,
169             auth=(self._address, self._token))
170
171     def get_key(self, address, ktype, private=False, fetch_remote=True):
172         """
173         Return a key of type C{ktype} bound to C{address}.
174
175         First, search for the key in local storage. If it is not available,
176         then try to fetch from nickserver.
177
178         @param address: The address bound to the key.
179         @type address: str
180         @param ktype: The type of the key.
181         @type ktype: KeyType
182         @param private: Look for a private key instead of a public one?
183         @type private: bool
184
185         @return: A key of type C{ktype} bound to C{address}.
186         @rtype: EncryptionKey
187         @raise KeyNotFound: If the key was not found both locally and in
188             keyserver.
189         """
190         leap_assert(
191             ktype in self._wrapper_map,
192             'Unkown key type: %s.' % str(ktype))
193         try:
194             return self._wrapper_map[ktype].get_key(address, private=private)
195         except KeyNotFound:
196             # we will only try to fetch a key from nickserver if fetch_remote
197             # is True and the key is not private.
198             if fetch_remote is False or private is True:
199                 raise
200             # fetch keys from server and discard unwanted types.
201             keys = filter(lambda k: isinstance(k, ktype),
202                           self.fetch_keys_from_server(address))
203             if len(keys) is 0:
204                 raise KeyNotFound()
205             leap_assert(
206                 len(keys) == 1,
207                 'Got more than one key of type %s for %s.' %
208                 (str(ktype), address))
209             self._wrapper_map[ktype].put_key(keys[0])
210             return self._wrapper_map[ktype].get_key(address, private=private)
211
212     def fetch_keys_from_server(self, address):
213         """
214         Fetch keys bound to C{address} from nickserver.
215
216         @param address: The address bound to the keys.
217         @type address: str
218
219         @return: A list of keys bound to C{address}.
220         @rtype: list of EncryptionKey
221         @raise KeyNotFound: If the key was not found on nickserver.
222         @raise httplib.HTTPException:
223         """
224         keydata = self._get_dict_from_http_json('/key/%s' % address)
225         leap_assert(
226             keydata['address'] == address,
227             "Fetched key for wrong address.")
228         keys = []
229         for key in keydata['keys']:
230             keys.append(
231                 build_key_from_dict(
232                     self._key_class_from_type(key['type']),
233                     address,
234                     key))
235         return keys
236
237     def get_all_keys_in_local_db(self, private=False):
238         """
239         Return all keys stored in local database.
240
241         @return: A list with all keys in local db.
242         @rtype: list
243         """
244         return map(
245             lambda doc: build_key_from_dict(
246                 self._key_class_from_type(doc.content['type']),
247                 doc.content['address'],
248                 doc.content),
249             self._soledad.get_from_index(
250                 TAGS_AND_PRIVATE_INDEX,
251                 'keymanager-key',
252                 '1' if private else '0'))
253
254     def refresh_keys(self):
255         """
256         Fetch keys from nickserver and update them locally.
257         """
258         addresses = set(map(
259             lambda doc: doc.address,
260             self.get_all_keys_in_local_db(private=False)))
261         # TODO: maybe we should not attempt to refresh our own public key?
262         for address in addresses:
263             for key in self.fetch_keys_from_server(address):
264                 self._wrapper_map[key.__class__].put_key(key)
265
266     def gen_key(self, ktype):
267         """
268         Generate a key of type C{ktype} bound to the user's address.
269
270         @param ktype: The type of the key.
271         @type ktype: KeyType
272
273         @return: The generated key.
274         @rtype: EncryptionKey
275         """
276         return self._wrapper_map[ktype].gen_key(self._address)
277
278     #
279     # Token setter/getter
280     #
281
282     def _get_token(self):
283         return self._token
284
285     def _set_token(self, token):
286         self._token = token
287
288     token = property(
289         _get_token, _set_token, doc='The auth token.')