Do not attempt to fetch private keys from server.
[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(privkey.key_data, password)
164             data['keys'].append(privkey)
165         self._fetcher.put(
166             self._nickserver_url + '/key/' + self._address,
167             data=data,
168             auth=(self._address, self._token))
169
170     def get_key(self, address, ktype, private=False, fetch_remote=True):
171         """
172         Return a key of type C{ktype} bound to C{address}.
173
174         First, search for the key in local storage. If it is not available,
175         then try to fetch from nickserver.
176
177         @param address: The address bound to the key.
178         @type address: str
179         @param ktype: The type of the key.
180         @type ktype: KeyType
181         @param private: Look for a private key instead of a public one?
182         @type private: bool
183
184         @return: A key of type C{ktype} bound to C{address}.
185         @rtype: EncryptionKey
186         @raise KeyNotFound: If the key was not found both locally and in
187             keyserver.
188         """
189         leap_assert(
190             ktype in self._wrapper_map,
191             'Unkown key type: %s.' % str(ktype))
192         try:
193             return self._wrapper_map[ktype].get_key(address, private=private)
194         except KeyNotFound:
195             # we will only try to fetch a key from nickserver if fetch_remote
196             # is True and the key is not private.
197             if fetch_remote is False or private is True:
198                 raise
199             # fetch keys from server and discard unwanted types.
200             keys = filter(lambda k: isinstance(k, ktype),
201                           self.fetch_keys_from_server(address))
202             if len(keys) is 0:
203                 raise KeyNotFound()
204             leap_assert(
205                 len(keys) == 1,
206                 'Got more than one key of type %s for %s.' %
207                 (str(ktype), address))
208             self._wrapper_map[ktype].put_key(keys[0])
209             return self._wrapper_map[ktype].get_key(address, private=private)
210
211     def fetch_keys_from_server(self, address):
212         """
213         Fetch keys bound to C{address} from nickserver.
214
215         @param address: The address bound to the keys.
216         @type address: str
217
218         @return: A list of keys bound to C{address}.
219         @rtype: list of EncryptionKey
220         @raise KeyNotFound: If the key was not found on nickserver.
221         @raise httplib.HTTPException:
222         """
223         keydata = self._get_dict_from_http_json('/key/%s' % address)
224         leap_assert(
225             keydata['address'] == address,
226             "Fetched key for wrong address.")
227         keys = []
228         for key in keydata['keys']:
229             keys.append(
230                 build_key_from_dict(
231                     self._key_class_from_type(key['type']),
232                     address,
233                     key))
234         return keys
235
236     def get_all_keys_in_local_db(self, private=False):
237         """
238         Return all keys stored in local database.
239
240         @return: A list with all keys in local db.
241         @rtype: list
242         """
243         return map(
244             lambda doc: build_key_from_dict(
245                 self._key_class_from_type(doc.content['type']),
246                 doc.content['address'],
247                 doc.content),
248             self._soledad.get_from_index(
249                 TAGS_AND_PRIVATE_INDEX,
250                 'keymanager-key',
251                 '1' if private else '0'))
252
253     def refresh_keys(self):
254         """
255         Fetch keys from nickserver and update them locally.
256         """
257         addresses = set(map(
258             lambda doc: doc.address,
259             self.get_all_keys_in_local_db(private=False)))
260         # TODO: maybe we should not attempt to refresh our own public key?
261         for address in addresses:
262             for key in self.fetch_keys_from_server(address):
263                 self._wrapper_map[key.__class__].put_key(key)
264
265     def gen_key(self, ktype):
266         """
267         Generate a key of type C{ktype} bound to the user's address.
268
269         @param ktype: The type of the key.
270         @type ktype: KeyType
271
272         @return: The generated key.
273         @rtype: EncryptionKey
274         """
275         return self._wrapper_map[ktype].gen_key(self._address)
276
277     #
278     # Token setter/getter
279     #
280
281     def _get_token(self):
282         return self._token
283
284     def _set_token(self, token):
285         self._token = token
286
287     token = property(
288         _get_token, _set_token, doc='The auth token.')