]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/common.py
Download Tor Browser Developers signing key using requests instead of gnupg, and...
[torbrowser-launcher.git] / torbrowser_launcher / common.py
1 """
2 Tor Browser Launcher
3 https://github.com/micahflee/torbrowser-launcher/
4
5 Copyright (c) 2013-2021 Micah Lee <micah@micahflee.com>
6
7 Permission is hereby granted, free of charge, to any person
8 obtaining a copy of this software and associated documentation
9 files (the "Software"), to deal in the Software without
10 restriction, including without limitation the rights to use,
11 copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following
14 conditions:
15
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
18
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 OTHER DEALINGS IN THE SOFTWARE.
27 """
28
29 import os
30 import sys
31 import platform
32 import subprocess
33 import locale
34 import pickle
35 import json
36 import re
37 import gettext
38 import gpg
39 import requests
40
41 SHARE = os.getenv("TBL_SHARE", sys.prefix + "/share") + "/torbrowser-launcher"
42
43 gettext.install("torbrowser-launcher")
44
45 # We're looking for output which:
46 #
47 #  1. The first portion must be `[GNUPG:] IMPORT_OK`
48 #  2. The second must be an integer between [0, 15], inclusive
49 #  3. The third must be an uppercased hex-encoded 160-bit fingerprint
50 gnupg_import_ok_pattern = re.compile(
51     b"(\[GNUPG\:\]) (IMPORT_OK) ([0-9]|[1]?[0-5]) ([A-F0-9]{40})"
52 )
53
54
55 class Common(object):
56     def __init__(self, tbl_version):
57         self.tbl_version = tbl_version
58
59         # initialize the app
60         self.default_mirror = "https://dist.torproject.org/"
61         self.discover_arch_lang()
62         self.build_paths()
63         for d in self.paths["dirs"]:
64             self.mkdir(self.paths["dirs"][d])
65         self.load_mirrors()
66         self.load_settings()
67         self.mkdir(self.paths["download_dir"])
68         self.mkdir(self.paths["tbb"]["dir"])
69         self.init_gnupg()
70
71     # discover the architecture and language
72     def discover_arch_lang(self):
73         # figure out the architecture
74         self.architecture = "x86_64" if "64" in platform.architecture()[0] else "i686"
75
76         # figure out the language
77         available_languages = [
78             "ar",
79             "ca",
80             "cs",
81             "da",
82             "de",
83             "el",
84             "en-US",
85             "es-AR",
86             "es-ES",
87             "fa",
88             "fr",
89             "ga-IE",
90             "he",
91             "hu",
92             "id",
93             "is",
94             "it",
95             "ja",
96             "ka",
97             "ko",
98             "lt",
99             "mk",
100             "ms",
101             "my",
102             "nb-NO",
103             "nl",
104             "pl",
105             "pt-BR",
106             "ro",
107             "ru",
108             "sv-SE",
109             "th",
110             "tr",
111             "vi",
112             "zh-CN",
113             "zh-TW",
114         ]
115
116         # a list of manually configured language fallback overriding
117         language_overrides = {
118             "zh-HK": "zh-TW",
119         }
120
121         locale.setlocale(locale.LC_MESSAGES, "")
122         default_locale = locale.getlocale(locale.LC_MESSAGES)[0]
123         if default_locale is None:
124             self.language = "en-US"
125         else:
126             self.language = default_locale.replace("_", "-")
127             if self.language in language_overrides:
128                 self.language = language_overrides[self.language]
129             if self.language not in available_languages:
130                 self.language = self.language.split("-")[0]
131                 if self.language not in available_languages:
132                     for l in available_languages:
133                         if l[0:2] == self.language:
134                             self.language = l
135             # if language isn't available, default to english
136             if self.language not in available_languages:
137                 self.language = "en-US"
138
139     # get value of environment variable, if it is not set return the default value
140     @staticmethod
141     def get_env(var_name, default_value):
142         value = os.getenv(var_name)
143         if not value:
144             value = default_value
145         return value
146
147     # build all relevant paths
148     def build_paths(self, tbb_version=None):
149         homedir = os.getenv("HOME")
150         if not homedir:
151             homedir = "/tmp/.torbrowser-" + os.getenv("USER")
152             if not os.path.exists(homedir):
153                 try:
154                     os.mkdir(homedir, 0o700)
155                 except:
156                     self.set_gui(
157                         "error", _("Error creating {0}").format(homedir), [], False
158                     )
159
160         tbb_config = "{0}/torbrowser".format(
161             self.get_env("XDG_CONFIG_HOME", "{0}/.config".format(homedir))
162         )
163         tbb_cache = "{0}/torbrowser".format(
164             self.get_env("XDG_CACHE_HOME", "{0}/.cache".format(homedir))
165         )
166         tbb_local = "{0}/torbrowser".format(
167             self.get_env("XDG_DATA_HOME", "{0}/.local/share".format(homedir))
168         )
169         old_tbb_data = "{0}/.torbrowser".format(homedir)
170
171         if tbb_version:
172             # tarball filename
173             if self.architecture == "x86_64":
174                 arch = "linux64"
175             else:
176                 arch = "linux32"
177
178             if hasattr(self, "settings") and self.settings["force_en-US"]:
179                 language = "en-US"
180             else:
181                 language = self.language
182             tarball_filename = (
183                 "tor-browser-" + arch + "-" + tbb_version + "_" + language + ".tar.xz"
184             )
185
186             # tarball
187             self.paths["tarball_url"] = (
188                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename
189             )
190             self.paths["tarball_file"] = tbb_cache + "/download/" + tarball_filename
191             self.paths["tarball_filename"] = tarball_filename
192
193             # sig
194             self.paths["sig_url"] = (
195                 "{0}torbrowser/" + tbb_version + "/" + tarball_filename + ".asc"
196             )
197             self.paths["sig_file"] = (
198                 tbb_cache + "/download/" + tarball_filename + ".asc"
199             )
200             self.paths["sig_filename"] = tarball_filename + ".asc"
201         else:
202             self.paths = {
203                 "dirs": {
204                     "config": tbb_config,
205                     "cache": tbb_cache,
206                     "local": tbb_local,
207                 },
208                 "old_data_dir": old_tbb_data,
209                 "tbl_bin": sys.argv[0],
210                 "icon_file": os.path.join(
211                     os.path.dirname(SHARE), "pixmaps/torbrowser.png"
212                 ),
213                 "torproject_pem": os.path.join(SHARE, "torproject.pem"),
214                 "signing_keys": {
215                     "tor_browser_developers": os.path.join(
216                         SHARE, "tor-browser-developers.asc"
217                     ),
218                     "wkd_tmp": os.path.join(tbb_cache, "torbrowser.gpg")
219                 },
220                 "mirrors_txt": [
221                     os.path.join(SHARE, "mirrors.txt"),
222                     tbb_config + "/mirrors.txt",
223                 ],
224                 "download_dir": tbb_cache + "/download",
225                 "gnupg_homedir": tbb_local + "/gnupg_homedir",
226                 "settings_file": tbb_config + "/settings.json",
227                 "settings_file_pickle": tbb_config + "/settings",
228                 "version_check_url": "https://aus1.torproject.org/torbrowser/update_3/release/Linux_x86_64-gcc3/x/en-US",
229                 "version_check_file": tbb_cache + "/download/release.xml",
230                 "tbb": {
231                     "changelog": tbb_local
232                     + "/tbb/"
233                     + self.architecture
234                     + "/tor-browser_"
235                     + self.language
236                     + "/Browser/TorBrowser/Docs/ChangeLog.txt",
237                     "dir": tbb_local + "/tbb/" + self.architecture,
238                     "dir_tbb": tbb_local
239                     + "/tbb/"
240                     + self.architecture
241                     + "/tor-browser_"
242                     + self.language,
243                     "start": tbb_local
244                     + "/tbb/"
245                     + self.architecture
246                     + "/tor-browser_"
247                     + self.language
248                     + "/start-tor-browser.desktop",
249                 },
250             }
251
252         # Add the expected fingerprint for imported keys:
253         tor_browser_developers_fingerprint = "EF6E286DDA85EA2A4BA7DE684E2C6E8793298290"
254         self.fingerprints = {
255             "tor_browser_developers": tor_browser_developers_fingerprint,
256             "wkd_tmp": tor_browser_developers_fingerprint,
257         }
258
259     # create a directory
260     @staticmethod
261     def mkdir(path):
262         try:
263             if not os.path.exists(path):
264                 os.makedirs(path, 0o700)
265                 return True
266         except:
267             print(_("Cannot create directory {0}").format(path))
268             return False
269         if not os.access(path, os.W_OK):
270             print(_("{0} is not writable").format(path))
271             return False
272         return True
273
274     # if gnupg_homedir isn't set up, set it up
275     def init_gnupg(self):
276         if not os.path.exists(self.paths["gnupg_homedir"]):
277             print(_("Creating GnuPG homedir"), self.paths["gnupg_homedir"])
278             self.mkdir(self.paths["gnupg_homedir"])
279         self.import_keys()
280
281     def proxies(self):
282         # Use tor socks5 proxy, if enabled
283         if self.settings["download_over_tor"]:
284             socks5_address = "socks5h://{}".format(self.settings["tor_socks_address"])
285             return {"https": socks5_address, "http": socks5_address}
286         else:
287             return None
288
289     def refresh_keyring(self):
290         print("Downloading latest Tor Browser signing key...")
291
292         # Fetch key from wkd, as per https://support.torproject.org/tbb/how-to-verify-signature/
293         # Sometimes GPG throws errors, so comment this out and download it directly
294         # p = subprocess.Popen(
295         #     [
296         #         "gpg",
297         #         "--status-fd",
298         #         "2",
299         #         "--homedir",
300         #         self.paths["gnupg_homedir"],
301         #         "--auto-key-locate",
302         #         "nodefault,wkd",
303         #         "--locate-keys",
304         #         "torbrowser@torproject.org",
305         #     ],
306         #     stderr=subprocess.PIPE,
307         # )
308         # p.wait()
309
310         # Download the key from WKD directly
311         r = requests.get(
312             "https://torproject.org/.well-known/openpgpkey/hu/kounek7zrdx745qydx6p59t9mqjpuhdf?l=torbrowser",
313             proxies=self.proxies(),
314         )
315         if r.status_code != 200:
316             print(f"Error fetching key, status code = {r.status_code}")
317         else:
318             with open(self.paths["signing_keys"]["wkd_tmp"], "wb") as f:
319                 f.write(r.content)
320
321             if self.import_key_and_check_status("wkd_tmp"):
322                 print("Key imported successfully")
323             else:
324                 print("Key failed to import")
325
326     def import_key_and_check_status(self, key):
327         """Import a GnuPG key and check that the operation was successful.
328         :param str key: A string specifying the key's filepath from
329             ``Common.paths``
330         :rtype: bool
331         :returns: ``True`` if the key is now within the keyring (or was
332             previously and hasn't changed). ``False`` otherwise.
333         """
334         with gpg.Context() as c:
335             c.set_engine_info(
336                 gpg.constants.protocol.OpenPGP, home_dir=self.paths["gnupg_homedir"]
337             )
338
339             impkey = self.paths["signing_keys"][key]
340             try:
341                 c.op_import(gpg.Data(file=impkey))
342             except:
343                 return False
344             else:
345                 result = c.op_import_result()
346                 if result and self.fingerprints[key] in result.imports[0].fpr:
347                     return True
348                 else:
349                     return False
350
351     # import gpg keys
352     def import_keys(self):
353         """Import all GnuPG keys.
354         :rtype: bool
355         :returns: ``True`` if all keys were successfully imported; ``False``
356             otherwise.
357         """
358         keys = [
359             "tor_browser_developers",
360         ]
361         all_imports_succeeded = True
362
363         for key in keys:
364             imported = self.import_key_and_check_status(key)
365             if not imported:
366                 print(
367                     _(
368                         "Could not import key with fingerprint: %s."
369                         % self.fingerprints[key]
370                     )
371                 )
372                 all_imports_succeeded = False
373
374         if not all_imports_succeeded:
375             print(_("Not all keys were imported successfully!"))
376
377         return all_imports_succeeded
378
379     # load mirrors
380     def load_mirrors(self):
381         self.mirrors = []
382         for srcfile in self.paths["mirrors_txt"]:
383             if not os.path.exists(srcfile):
384                 continue
385             for mirror in open(srcfile, "r").readlines():
386                 if mirror.strip() not in self.mirrors:
387                     self.mirrors.append(mirror.strip())
388
389     # load settings
390     def load_settings(self):
391         default_settings = {
392             "tbl_version": self.tbl_version,
393             "installed": False,
394             "download_over_tor": False,
395             "tor_socks_address": "127.0.0.1:9050",
396             "mirror": self.default_mirror,
397             "force_en-US": False,
398         }
399
400         if os.path.isfile(self.paths["settings_file"]):
401             settings = json.load(open(self.paths["settings_file"]))
402             resave = False
403
404             # detect installed
405             settings["installed"] = os.path.isfile(self.paths["tbb"]["start"])
406
407             # make sure settings file is up-to-date
408             for setting in default_settings:
409                 if setting not in settings:
410                     settings[setting] = default_settings[setting]
411                     resave = True
412
413             # make sure tor_socks_address doesn't start with 'tcp:'
414             if settings["tor_socks_address"].startswith("tcp:"):
415                 settings["tor_socks_address"] = settings["tor_socks_address"][4:]
416                 resave = True
417
418             # make sure the version is current
419             if settings["tbl_version"] != self.tbl_version:
420                 settings["tbl_version"] = self.tbl_version
421                 resave = True
422
423             self.settings = settings
424             if resave:
425                 self.save_settings()
426
427         # if settings file is still using old pickle format, convert to json
428         elif os.path.isfile(self.paths["settings_file_pickle"]):
429             self.settings = pickle.load(open(self.paths["settings_file_pickle"]))
430             self.save_settings()
431             os.remove(self.paths["settings_file_pickle"])
432             self.load_settings()
433
434         else:
435             self.settings = default_settings
436             self.save_settings()
437
438     # save settings
439     def save_settings(self):
440         json.dump(self.settings, open(self.paths["settings_file"], "w"))
441         return True