2bad3e32d3538636a1932d38fdb648cde82570bb
[pixelated-user-agent.git] / service / pixelated / config / sessions.py
1 #
2 # Copyright (c) 2015 ThoughtWorks, Inc.
3 #
4 # Pixelated is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # Pixelated is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
16
17 from __future__ import absolute_import
18
19 import os
20 import errno
21 import traceback
22 import requests
23 import sys
24
25 from twisted.internet import defer, threads, reactor
26 from twisted.logger import Logger
27
28 from leap.soledad.common.crypto import WrongMacError, UnknownMacMethodError
29 from leap.soledad.client import Soledad
30 from leap.bitmask.mail.incoming.service import IncomingMail
31 from leap.bitmask.mail.mail import Account
32 import leap.common.certs as leap_certs
33 from leap.common.events import (
34     register, unregister,
35     catalog as events
36 )
37
38 from pixelated.bitmask_libraries.keymanager import Keymanager, UploadKeyError
39 from pixelated.adapter.mailstore import LeapMailStore
40 from pixelated.config import leap_config
41 from pixelated.bitmask_libraries.certs import LeapCertificate
42 from pixelated.bitmask_libraries.smtp import LeapSMTPConfig
43
44 logger = Logger()
45
46
47 class LeapSessionFactory(object):
48     def __init__(self, provider):
49         self._provider = provider
50
51     @defer.inlineCallbacks
52     def create(self, username, password, auth):
53         key = SessionCache.session_key(self._provider, username)
54         session = SessionCache.lookup_session(key)
55         if not session:
56             session = yield self._create_new_session(username, password, auth)
57             yield session.first_required_sync()
58             SessionCache.remember_session(key, session)
59         defer.returnValue(session)
60
61     @defer.inlineCallbacks
62     def _create_new_session(self, username, password, auth):
63         account_email = self._provider.address_for(username)
64
65         self._create_database_dir(auth.uuid)
66
67         api_cert = self._provider.provider_api_cert
68
69         soledad = yield self.setup_soledad(auth.token, auth.uuid, password, api_cert)
70
71         mail_store = LeapMailStore(soledad)
72
73         keymanager = yield self.setup_keymanager(self._provider, soledad, account_email, auth.token, auth.uuid)
74
75         smtp_client_cert = self._download_smtp_cert(auth)
76         smtp_host, smtp_port = self._provider.smtp_info()
77         smtp_config = LeapSMTPConfig(account_email, smtp_client_cert, smtp_host, smtp_port)
78
79         leap_session = LeapSession(self._provider, auth, mail_store, soledad, keymanager, smtp_config)
80
81         defer.returnValue(leap_session)
82
83     @defer.inlineCallbacks
84     def setup_soledad(self,
85                       user_token,
86                       user_uuid,
87                       password,
88                       api_cert):
89         secrets = self._secrets_path(user_uuid)
90         local_db = self._local_db_path(user_uuid)
91         server_url = self._provider.discover_soledad_server(user_uuid)
92         try:
93             soledad = yield threads.deferToThread(Soledad,
94                                                   user_uuid.encode('utf-8'),
95                                                   passphrase=unicode(password, 'utf-8'),
96                                                   secrets_path=secrets,
97                                                   local_db_path=local_db,
98                                                   server_url=server_url,
99                                                   cert_file=api_cert,
100                                                   shared_db=None,
101                                                   auth_token=user_token,
102                                                   defer_encryption=False)
103             defer.returnValue(soledad)
104         except (WrongMacError, UnknownMacMethodError), e:
105             raise SoledadWrongPassphraseException(e)
106
107     @defer.inlineCallbacks
108     def setup_keymanager(self, provider, soledad, account_email, token, uuid):
109         keymanager = yield threads.deferToThread(Keymanager,
110                                                  provider,
111                                                  soledad,
112                                                  account_email,
113                                                  token,
114                                                  uuid)
115         defer.returnValue(keymanager)
116
117     def _download_smtp_cert(self, auth):
118         cert = SmtpClientCertificate(self._provider, auth, self._user_path(auth.uuid))
119         return cert.cert_path()
120
121     def _user_path(self, user_uuid):
122         return os.path.join(leap_config.leap_home, user_uuid)
123
124     def _soledad_path(self, user_uuid):
125         return os.path.join(leap_config.leap_home, user_uuid, 'soledad')
126
127     def _secrets_path(self, user_uuid):
128         return os.path.join(self._soledad_path(user_uuid), 'secrets')
129
130     def _local_db_path(self, user_uuid):
131         return os.path.join(self._soledad_path(user_uuid), 'soledad.db')
132
133     def _create_database_dir(self, user_uuid):
134         try:
135             os.makedirs(self._soledad_path(user_uuid))
136         except OSError as exc:
137             if exc.errno == errno.EEXIST and os.path.isdir(self._soledad_path(user_uuid)):
138                 pass
139             else:
140                 raise
141
142
143 class LeapSession(object):
144
145     def __init__(self, provider, user_auth, mail_store, soledad, keymanager, smtp_config):
146         self.smtp_config = smtp_config
147         self.provider = provider
148         self.user_auth = user_auth
149         self.mail_store = mail_store
150         self.soledad = soledad
151         self.keymanager = keymanager
152         self.fresh_account = False
153         self.incoming_mail_fetcher = None
154         self.account = None
155         self._has_been_initially_synced = False
156         self._is_closed = False
157         register(events.KEYMANAGER_FINISHED_KEY_GENERATION, self._set_fresh_account, uid=self.account_email())
158
159     @defer.inlineCallbacks
160     def first_required_sync(self):
161         yield self.sync()
162         yield self.finish_bootstrap()
163
164     @defer.inlineCallbacks
165     def finish_bootstrap(self):
166         yield self.keymanager.generate_openpgp_key()
167         yield self._create_account(self.soledad, self.user_auth.uuid)
168         self.incoming_mail_fetcher = yield self._create_incoming_mail_fetcher(
169             self.keymanager,
170             self.soledad,
171             self.account,
172             self.account_email())
173         reactor.callFromThread(self.incoming_mail_fetcher.startService)
174
175     def _create_account(self, soledad, user_id):
176         self.account = Account(soledad, user_id)
177         return self.account.deferred_initialization
178
179     def _set_fresh_account(self, event, email_address):
180         logger.debug('Key for email %s has been generated' % email_address)
181         if email_address == self.account_email():
182             self.fresh_account = True
183
184     def account_email(self):
185         name = self.user_auth.username
186         return self.provider.address_for(name)
187
188     def close(self):
189         self.stop_background_jobs()
190         unregister(events.KEYMANAGER_FINISHED_KEY_GENERATION, uid=self.account_email())
191         self.soledad.close()
192         self._close_account()
193         self.remove_from_cache()
194         self._is_closed = True
195
196     @property
197     def is_closed(self):
198         return self._is_closed
199
200     def _close_account(self):
201         if self.account:
202             self.account.end_session()
203
204     def remove_from_cache(self):
205         key = SessionCache.session_key(self.provider, self.user_auth.username)
206         SessionCache.remove_session(key)
207
208     @defer.inlineCallbacks
209     def _create_incoming_mail_fetcher(self, keymanager, soledad, account, user_mail):
210         inbox = yield account.callWhenReady(lambda _: account.get_collection_by_mailbox('INBOX'))
211         defer.returnValue(IncomingMail(keymanager.keymanager,
212                           soledad,
213                           inbox,
214                           user_mail))
215
216     def stop_background_jobs(self):
217         if self.incoming_mail_fetcher:
218             reactor.callFromThread(self.incoming_mail_fetcher.stopService)
219             self.incoming_mail_fetcher = None
220
221     def sync(self):
222         try:
223             return self.soledad.sync()
224         except Exception as e:
225             logger.error(e)
226             raise
227
228
229 class SessionCache(object):
230
231     sessions = {}
232
233     @staticmethod
234     def lookup_session(key):
235         session = SessionCache.sessions.get(key, None)
236         if session is not None and session.is_closed:
237             SessionCache.remove_session(key)
238             return None
239         return session
240
241     @staticmethod
242     def remember_session(key, session):
243         SessionCache.sessions[key] = session
244
245     @staticmethod
246     def remove_session(key):
247         if key in SessionCache.sessions:
248             del SessionCache.sessions[key]
249
250     @staticmethod
251     def session_key(provider, username):
252         return hash((provider, username))
253
254
255 class SmtpClientCertificate(object):
256     def __init__(self, provider, auth, user_path):
257         self._provider = provider
258         self._auth = auth
259         self._user_path = user_path
260
261     def cert_path(self):
262         if not self._is_cert_already_downloaded() or self._should_redownload():
263             self._download_smtp_cert()
264
265         return self._smtp_client_cert_path()
266
267     def _is_cert_already_downloaded(self):
268         return os.path.exists(self._smtp_client_cert_path())
269
270     def _should_redownload(self):
271         return leap_certs.should_redownload(self._smtp_client_cert_path())
272
273     def _download_smtp_cert(self):
274         cert_path = self._smtp_client_cert_path()
275
276         if not os.path.exists(os.path.dirname(cert_path)):
277             os.makedirs(os.path.dirname(cert_path))
278
279         self.download_to(cert_path)
280
281     def _smtp_client_cert_path(self):
282         return os.path.join(
283             self._user_path,
284             "providers",
285             self._provider.domain,
286             "keys", "client", "smtp.pem")
287
288     def download(self):
289         cert_url = '%s/%s/smtp_cert' % (self._provider.api_uri, self._provider.api_version)
290         headers = {}
291         headers["Authorization"] = 'Token token="{0}"'.format(self._auth.token)
292         params = {'address': self._auth.username}
293         response = requests.post(
294             cert_url,
295             params=params,
296             data=params,
297             verify=self._provider.provider_api_cert,
298             timeout=15,
299             headers=headers)
300         response.raise_for_status()
301
302         client_cert = response.content
303
304         return client_cert
305
306     def download_to(self, target_file):
307         client_cert = self.download()
308
309         with open(target_file, 'w') as f:
310             f.write(client_cert)
311
312
313 class SoledadWrongPassphraseException(Exception):
314     def __init__(self, *args, **kwargs):
315         super(SoledadWrongPassphraseException, self).__init__(*args, **kwargs)