summaryrefslogtreecommitdiffstats
path: root/config/chroot_local-includes/usr/local
diff options
context:
space:
mode:
Diffstat (limited to 'config/chroot_local-includes/usr/local')
-rwxr-xr-xconfig/chroot_local-includes/usr/local/bin/tails-additional-software-config269
-rwxr-xr-xconfig/chroot_local-includes/usr/local/bin/tails-documentation117
-rwxr-xr-xconfig/chroot_local-includes/usr/local/bin/thunderbird47
-rwxr-xr-xconfig/chroot_local-includes/usr/local/bin/tor-browser4
-rw-r--r--config/chroot_local-includes/usr/local/bin/unlock-veracrypt-volumes75
-rwxr-xr-xconfig/chroot_local-includes/usr/local/lib/generate-tor-browser-profile5
-rw-r--r--config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/__init__.py12
-rw-r--r--config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/config.py7
-rw-r--r--config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/exceptions.py10
-rw-r--r--config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py346
-rw-r--r--config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_list.py98
-rw-r--r--config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py264
-rwxr-xr-xconfig/chroot_local-includes/usr/local/lib/tails-additional-software-notify98
-rwxr-xr-xconfig/chroot_local-includes/usr/local/lib/tails-boot-device-can-have-persistence29
-rw-r--r--config/chroot_local-includes/usr/local/lib/tails-shell-library/chroot-browser.sh53
-rw-r--r--config/chroot_local-includes/usr/local/lib/tails-shell-library/thunderbird.sh34
-rw-r--r--config/chroot_local-includes/usr/local/lib/tails-shell-library/tor-browser.sh29
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/live-persist48
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/tails-additional-software573
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/tails-additional-software-remove18
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/tails-debugging-info122
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/unsafe-browser8
-rw-r--r--config/chroot_local-includes/usr/local/share/mime/packages/unlock-veracrypt-volumes.xml.in9
23 files changed, 1987 insertions, 288 deletions
diff --git a/config/chroot_local-includes/usr/local/bin/tails-additional-software-config b/config/chroot_local-includes/usr/local/bin/tails-additional-software-config
new file mode 100755
index 0000000..9717ce8
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-additional-software-config
@@ -0,0 +1,269 @@
+#!/usr/bin/env python3
+
+"""User interface to configure Tails Additional Software."""
+
+import gettext
+import os
+import subprocess
+import sys
+
+import apt.cache
+import gi
+
+from gi.repository import Gio # NOQA: E402
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk # NOQA: E402
+
+from tailslib.persistence import ( # NOQA: E402
+ has_unlocked_persistence,
+ has_persistence,
+ is_tails_media_writable,
+ launch_persistence_setup)
+
+from tailslib.additionalsoftware.config import ( # NOQA: E402
+ get_additional_packages,
+ get_packages_list_path,
+ filter_package_details)
+
+_ = gettext.gettext
+
+UI_FILE = "/usr/share/tails/additional-software/configuration-window.ui"
+
+
+class ASPConfigApplicationWindow(Gtk.ApplicationWindow):
+ def __init__(self, application, get_config_func, remove_asp_func):
+ Gtk.ApplicationWindow.__init__(self, application=application)
+
+ self.get_config_func = get_config_func
+ self.remove_asp_func = remove_asp_func
+
+ self.connect("show", self.cb_window_show)
+
+ builder = Gtk.Builder.new_from_file(UI_FILE)
+ builder.set_translation_domain("tails")
+ builder.connect_signals(self)
+
+ self.listbox = builder.get_object("listbox")
+ self.no_package_page = builder.get_object("no_package_page")
+ self.package_list_page = builder.get_object("package_list_page")
+ self.stack = builder.get_object("stack")
+ self.install_label = builder.get_object("install_label")
+ self.persistence_button = builder.get_object("persistence_button")
+
+ self.listbox.set_header_func(self._listbox_update_header_func, None)
+
+ self.set_default_size(width=500, height=-1)
+ self.set_icon_name("package-x-generic")
+ self.set_titlebar(builder.get_object("headerbar"))
+ self.add(builder.get_object("main_box"))
+
+ @staticmethod
+ def _listbox_update_header_func(row, before, user_data):
+ if not before:
+ row.set_header(None)
+ return
+
+ current = row.get_header()
+ if not current:
+ current = Gtk.Separator.new(Gtk.Orientation.HORIZONTAL)
+ current.show()
+ row.set_header(current)
+
+ def __show_exception_dialog(self, explanation, exception):
+ dialog = Gtk.MessageDialog(
+ self,
+ Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ Gtk.MessageType.ERROR,
+ Gtk.ButtonsType.OK,
+ explanation)
+ dialog.format_secondary_text(str(exception))
+ dialog.run()
+ dialog.destroy()
+
+ def cb_activate_link(self, label, uri):
+ if uri.endswith(".desktop"):
+ appinfo = Gio.DesktopAppInfo.new(uri)
+ appinfo.launch()
+ return True
+
+ def cb_listboxrow_remove_button_clicked(self, button, package_name):
+ dialog = Gtk.MessageDialog(
+ self,
+ Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ Gtk.MessageType.QUESTION,
+ Gtk.ButtonsType.NONE,
+ _("Remove {package} from your additional software? "
+ "This will stop installing the package "
+ "automatically.").format(package=package_name))
+ dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT)
+ dialog.add_button(Gtk.STOCK_REMOVE, Gtk.ResponseType.ACCEPT)
+ if dialog.run() == Gtk.ResponseType.ACCEPT:
+ try:
+ self.remove_asp_func(package_name)
+ except subprocess.CalledProcessError as e:
+ self.__show_exception_dialog(
+ _("Failed to remove {pkg}").format(pkg=package_name),
+ e)
+ dialog.destroy()
+
+ def cb_persistence_button_clicked(self, button, data=None):
+ launch_persistence_setup("--force-enable-preset", "AdditionalSoftware")
+ self.update_packages_list()
+ return True
+
+ def cb_window_show(self, window):
+ self.update_packages_list()
+
+ def update_packages_list(self):
+ try:
+ packages = self.get_config_func()
+ except Exception as e:
+ self.__show_exception_dialog(
+ _("Failed to read additional software configuration"),
+ e)
+ self.hide()
+ return
+ self.persistence_button.set_visible(False)
+ if packages:
+ self.listbox.foreach(lambda widget, data: widget.destroy(), None)
+ for package_name, package_description in packages:
+ listboxrow = Gtk.ListBoxRow.new()
+
+ hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
+ hbox.set_border_width(3)
+
+ vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
+ name_label = Gtk.Label.new("<b>{}</b>".format(package_name))
+ name_label.set_use_markup(True)
+ name_label.set_xalign(0)
+ vbox.pack_start(name_label, expand=True, fill=True, padding=0)
+ description_label = Gtk.Label.new(package_description)
+ description_label.set_xalign(0)
+ vbox.pack_start(
+ description_label, expand=True, fill=True, padding=0)
+ hbox.pack_start(vbox, expand=True, fill=True, padding=12)
+
+ remove_button = Gtk.Button.new_from_icon_name(
+ "window-close-symbolic",
+ Gtk.IconSize.SMALL_TOOLBAR)
+ remove_button.set_relief(Gtk.ReliefStyle.NONE)
+ remove_button.set_tooltip_text(
+ _("Stop installing {package} "
+ "automatically").format(package=package_name))
+ remove_button.connect(
+ "clicked", self.cb_listboxrow_remove_button_clicked,
+ package_name)
+ hbox.pack_end(
+ remove_button, expand=False, fill=False, padding=0)
+
+ listboxrow.add(hbox)
+ self.listbox.add(listboxrow)
+ # Add empty listboxrow to finish the list with a separator
+ listboxrow = Gtk.ListBoxRow.new()
+ listboxrow.set_selectable(False)
+ self.listbox.add(listboxrow)
+
+ self.listbox.show_all()
+ self.stack.set_visible_child(self.package_list_page)
+ self.install_label.set_markup(
+ _('To add more, install some software using '
+ '<a href="synaptic.desktop">Synaptic Package Manager</a> '
+ 'or <a href="org.gnome.Terminal.desktop">APT on the '
+ 'command line</a>.'))
+ else:
+ self.stack.set_visible_child(self.no_package_page)
+ self.install_label.set_markup(
+ _('To do so, install some software using '
+ '<a href="synaptic.desktop">Synaptic Package Manager</a> '
+ 'or <a href="org.gnome.Terminal.desktop">APT on the '
+ 'command line</a>.'))
+ if has_unlocked_persistence(search_new_persistence=True):
+ # The label from the UI file is good unmodified
+ pass
+ elif has_persistence():
+ self.install_label.set_markup(
+ _('To do so, unlock your persistent storage '
+ 'when starting Tails and '
+ 'install some software using '
+ '<a href="synaptic.desktop">Synaptic Package '
+ 'Manager</a> or '
+ '<a href="org.gnome.Terminal.desktop">APT on the '
+ 'command line</a>.'))
+ elif is_tails_media_writable():
+ self.persistence_button.set_visible(True)
+ self.install_label.set_markup(
+ _('To do so, create a persistent storage and install some '
+ 'software using '
+ '<a href="synaptic.desktop">Synaptic Package '
+ 'Manager</a> or '
+ '<a href="org.gnome.Terminal.desktop">APT on the '
+ 'command line</a>.'))
+ else: # It's impossible to have a persistent storage
+ self.install_label.set_markup(
+ _('To do so, install Tails on a USB stick using '
+ '<a href="tails-installer.desktop">Tails Installer</a> '
+ 'and create a persistent storage.'))
+
+
+class ASPConfigApplication(Gtk.Application):
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ *args,
+ application_id="org.boum.tails.additional-software-config",
+ **kwargs)
+
+ def do_activate(self):
+ self.window.present()
+
+ def do_startup(self):
+ Gtk.Application.do_startup(self)
+ gettext.install("tails")
+ self.window = ASPConfigApplicationWindow(
+ application=self,
+ get_config_func=self.get_asp_configuration,
+ remove_asp_func=self.remove_additional_software)
+
+ packages_list_file = Gio.File.new_for_path(
+ get_packages_list_path(search_new_persistence=True,
+ return_nonexistent=True))
+ self.packages_list_monitor = packages_list_file.monitor(
+ Gio.FileMonitorFlags.NONE, None)
+ self.packages_list_monitor.connect(
+ "changed", self.cb_packages_list_changed)
+
+ def cb_packages_list_changed(self, file_monitor, file, other_file,
+ event_type):
+ if os.access(file.get_path(), os.R_OK):
+ self.window.update_packages_list()
+
+ def get_asp_configuration(self):
+ additional_packages = get_additional_packages(
+ search_new_persistence=True)
+ apt_cache = apt.cache.Cache()
+
+ packages_with_description = []
+ for package in sorted(additional_packages):
+ package_name = filter_package_details(package)
+ try:
+ apt_package = apt_cache[package_name]
+ except KeyError:
+ summary = _("[package not available]")
+ else:
+ if apt_package.installed:
+ summary = apt_package.installed.summary
+ else:
+ summary = apt_package.candidate.summary
+ packages_with_description.append((package, summary))
+
+ return packages_with_description
+
+ def remove_additional_software(self, package_name):
+ subprocess.run(["pkexec",
+ "/usr/local/sbin/tails-additional-software-remove",
+ package_name],
+ check=True)
+
+
+asp_application = ASPConfigApplication()
+exit_status = asp_application.run(sys.argv)
+sys.exit(exit_status)
diff --git a/config/chroot_local-includes/usr/local/bin/tails-documentation b/config/chroot_local-includes/usr/local/bin/tails-documentation
index f14ecad..c65c610 100755
--- a/config/chroot_local-includes/usr/local/bin/tails-documentation
+++ b/config/chroot_local-includes/usr/local/bin/tails-documentation
@@ -1,86 +1,8 @@
#!/usr/bin/env python3
-import gettext
-import gi
-import locale
import os
import os.path
import sys
-import tailsgreeter.gui
-
-gi.require_version('Gdk', '3.0')
-from gi.repository import Gdk # NOQA: E402
-gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk # NOQA: E402
-gi.require_version('WebKit2', '4.0')
-from gi.repository import WebKit2 # NOQA: E402
-
-# We'll only use a single translation, "Tails documentation", which
-# already is translated for the launcher. For this reason, this script
-# is not managed by `refresh-translations`.
-gettext.textdomain('tails')
-
-# The browser from the Greeter is good as-is, but a button for
-# navigating backwards in the history would be nice.
-class DocumentationWindow(tailsgreeter.gui.GreeterHelpWindow):
- def _build_ui(self):
- super()._build_ui()
- # The super class' headerbar is not exposed as an instance
- # variable, but we need it!
- headerbar = next(child for child in self.get_children() \
- if isinstance(child, Gtk.HeaderBar))
- back_button = Gtk.Button.new_from_icon_name('back', Gtk.IconSize.BUTTON)
- back_button.connect("clicked", lambda x: self.webview.go_back())
- headerbar.pack_start(back_button)
- back_button.show()
- self.webview.connect(
- "load-changed",
- lambda webview, e: back_button.set_visible(webview.can_go_back())
- )
- self.find_entry = Gtk.Entry(visible=False, no_show_all=True)
- self.find_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY,
- "search")
- self.find_entry.connect("activate", self.find_forward)
- self.find_entry.connect("changed", self.find_forward)
- self.find_entry.connect("key-press-event", self.cb_find_entry_key_press)
- headerbar.pack_end(self.find_entry)
- self.connect("key-press-event", self.cb_window_key_press)
-
- def cb_window_key_press(self, window, event):
- if event.keyval == Gdk.KEY_f and event.state & Gdk.ModifierType.CONTROL_MASK:
- self.find_entry.show()
- self.find_entry.grab_focus()
-
- def cb_load_started(self, webview, ressource, request):
- super().cb_load_started(webview, ressource, request)
- if not request.get_uri().startswith("file://"):
- # An external link was clicked, let's abort following it
- # in our WebKit browser; any configured external protocol
- # handler will still open the link's uri.
- webview.stop_loading()
-
- def find_forward(self, entry, user_data=None):
- find_controller = self.webview.get_find_controller()
- find_options = WebKit2.FindOptions.CASE_INSENSITIVE | \
- WebKit2.FindOptions.WRAP_AROUND
- find_controller.search(self.find_entry.get_text(), find_options, 32)
-
- def find_previous(self):
- find_controller = self.webview.get_find_controller()
- find_controller.search_previous()
-
- def find_finish(self):
- find_controller = self.webview.get_find_controller()
- find_controller.search_finish()
- self.find_entry.set_text('')
- self.find_entry.hide()
- self.webview.grab_focus()
-
- def cb_find_entry_key_press(self, entry, event, user_data=None):
- if event.keyval == Gdk.KEY_Return and event.state & Gdk.ModifierType.SHIFT_MASK:
- self.find_previous()
- if event.keyval == Gdk.KEY_Escape:
- self.find_finish()
# Main
@@ -94,6 +16,7 @@ try:
except IndexError:
anchor = None
+tails_homepage = 'https://tails.boum.org'
wiki_path = '/usr/share/doc/tails/website'
lang_code = os.getenv('LANG', 'en')[0:2]
@@ -102,30 +25,24 @@ lang_code = os.getenv('LANG', 'en')[0:2]
if os.system('/usr/local/sbin/tor-has-bootstrapped') == 0:
if os.path.isfile(os.path.join(
wiki_path, page + '.' + lang_code + ".html")):
- uri = 'https://tails.boum.org/' + page + '/index.' + lang_code + '.html'
+ uri = tails_homepage + '/' + page + '/index.' + lang_code + '.html'
else:
- uri = 'https://tails.boum.org/' + page
- if anchor is not None:
- uri = uri + '#' + anchor
- os.execv('/usr/local/bin/tor-browser',
- ['/usr/local/bin/tor-browser', '--new-tab',
- uri])
+ uri = tails_homepage + '/' + page
+else:
+ trials = [
+ os.path.join(wiki_path, page + code + ".html")
+ for code in ['.' + lang_code, '.en', '']
+ ]
+ try:
+ uri = 'file://' + next(
+ trial for trial in trials if os.path.isfile(trial)
+ )
+ except StopIteration:
+ sys.exit('error: could not find the requested documentation page')
-trials = [
- os.path.join(wiki_path, page + code + ".html")
- for code in ['.' + lang_code, '.en', '']
-]
-try:
- uri = 'file://' + next(trial for trial in trials if os.path.isfile(trial))
-except StopIteration:
- sys.exit('error: could not find the requested documentation page')
if anchor is not None:
uri = uri + '#' + anchor
-if '..' in uri.split(os.sep):
- sys.exit('error: cannot escape from {}'.format(wiki_path))
-helpwindow = DocumentationWindow(uri)
-helpwindow.connect("delete-event", Gtk.main_quit)
-helpwindow.window.set_title(gettext.gettext('Tails documentation'))
-helpwindow.show()
-Gtk.main()
+os.environ['TOR_BROWSER_SKIP_OFFLINE_WARNING'] = 'yes'
+os.execv('/usr/local/bin/tor-browser',
+ ['/usr/local/bin/tor-browser', '--new-tab', uri])
diff --git a/config/chroot_local-includes/usr/local/bin/thunderbird b/config/chroot_local-includes/usr/local/bin/thunderbird
index cb085f9..ca23cc2 100755
--- a/config/chroot_local-includes/usr/local/bin/thunderbird
+++ b/config/chroot_local-includes/usr/local/bin/thunderbird
@@ -7,6 +7,9 @@ set -x
# Import set_mozilla_pref()
. /usr/local/lib/tails-shell-library/tor-browser.sh
+# Import guess_best_thunderbird_locale():
+. /usr/local/lib/tails-shell-library/thunderbird.sh
+
THUNDERBIRD_CONFIG_DIR="${HOME}/.thunderbird"
PROFILE="${THUNDERBIRD_CONFIG_DIR}/profile.default"
@@ -22,10 +25,38 @@ configure_default_incoming_protocol() {
else
default_protocol=1
fi
- mkdir -p "${PROFILE}/preferences"
- set_mozilla_pref "${PROFILE}/preferences/0000tails.js" \
+ mkdir -p "${PROFILE}"
+ set_mozilla_pref "${PROFILE}/prefs.js" \
"extensions.torbirdy.defaultprotocol" \
- "${default_protocol}"
+ "${default_protocol}" \
+ user_pref
+}
+
+configure_best_thunderbird_locale() {
+ local locale
+ locale=$(guess_best_thunderbird_locale)
+ mkdir -p "${PROFILE}"
+ set_mozilla_pref "${PROFILE}/prefs.js" \
+ "intl.locale.requested" \
+ "\"${locale}\"" \
+ user_pref
+}
+
+thunderbird_profile_is_new() {
+ [ ! -f "${PROFILE}/extensions.ini" ]
+}
+
+initialize_enigmail_configured_version() {
+ mkdir -p "${PROFILE}/preferences"
+ version="$(dpkg-query --show \
+ --showformat='${source:Upstream-Version}' \
+ enigmail | sed -E 's,\+.*$,,')"
+ # Set the value in prefs.js so that Enigmail can manage it itself
+ # once we've done this once.
+ set_mozilla_pref "${PROFILE}/prefs.js" \
+ "extensions.enigmail.configuredVersion" \
+ "\"${version}\"" \
+ 'user_pref'
}
start_thunderbird() {
@@ -34,6 +65,16 @@ start_thunderbird() {
configure_default_incoming_protocol
+ # Suppress Enigmail's configuration wizard by pretending that the current
+ # version was already configured. Only do this on first run though:
+ # once we've done this we let Enigmail manage this setting itself
+ # so it can run any migration code it wants to on upgrades.
+ if thunderbird_profile_is_new; then
+ initialize_enigmail_configured_version
+ fi
+
+ configure_best_thunderbird_locale
+
exec /usr/bin/thunderbird --class "Thunderbird" -profile "${PROFILE}" "${@}"
}
diff --git a/config/chroot_local-includes/usr/local/bin/tor-browser b/config/chroot_local-includes/usr/local/bin/tor-browser
index a2d0902..14a6245 100755
--- a/config/chroot_local-includes/usr/local/bin/tor-browser
+++ b/config/chroot_local-includes/usr/local/bin/tor-browser
@@ -36,7 +36,7 @@ export TOR_NO_DISPLAY_NETWORK_SETTINGS='yes'
ask_for_confirmation() {
if [ "${TOR_BROWSER_SKIP_OFFLINE_WARNING:-}" = 'yes' ] || \
- pgrep -u "${LIVE_USERNAME}" -f "${TBB_INSTALL}/firefox"; then
+ pgrep -u "${LIVE_USERNAME}" -f "${TBB_INSTALL}/firefox.real"; then
return
fi
@@ -57,6 +57,8 @@ start_browser() {
mkdir --mode=0700 -p "$TMPDIR"
export TMPDIR
+ configure_tor_browser_memory_usage "${PROFILE}"
+
# We need to set general.useragent.locale properly to get
# localized search plugins (and perhaps other things too). It is
# not enough to simply set intl.locale.matchOS to true.
diff --git a/config/chroot_local-includes/usr/local/bin/unlock-veracrypt-volumes b/config/chroot_local-includes/usr/local/bin/unlock-veracrypt-volumes
new file mode 100644
index 0000000..c7299ff
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/unlock-veracrypt-volumes
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+
+import argparse
+import logging
+from typing import List
+import sys
+import signal
+
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('UDisks', '2.0')
+gi.require_version('GUdev', '1.0')
+from gi.repository import Gtk, Gio
+
+from unlock_veracrypt_volumes.volume_manager import VolumeManager
+from unlock_veracrypt_volumes.exceptions import AlreadyUnlockedError
+
+
+logger = logging.getLogger(__name__)
+
+
+class App(Gtk.Application):
+ def __init__(self):
+ super().__init__(application_id="org.boum.tails.unlock_veracrypt_volumes", flags=Gio.ApplicationFlags.HANDLES_OPEN)
+ self.manager = None # type: VolumeManager
+
+ def do_activate(self):
+ if self.manager:
+ # Raise window of the primary instance
+ self.manager.window.present()
+ else:
+ self.manager = VolumeManager(self)
+
+ def do_open(self, files: List[Gio.File], n_files, hint: str):
+ logger.debug("in do_open. files: %s", files)
+
+ # Show the window before unlocking the files
+ self.activate()
+
+ for file in files:
+ try:
+ self.manager.unlock_file_container(file.get_path(), open_after_unlock=True)
+ except AlreadyUnlockedError:
+ self.manager.open_file_container(file.get_path())
+
+
+def parse_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--verbose", action="store_true")
+ parser.add_argument("PATH", nargs="*", help="file containers to unlock")
+ return parser.parse_args()
+
+
+def init(args):
+ if args.verbose:
+ logging.basicConfig(level=logging.DEBUG)
+ else:
+ logging.basicConfig(level=logging.INFO)
+ logger.debug("args: %r", args)
+
+
+def main():
+ args = parse_args()
+ init(args)
+ app_args = sys.argv[:1] + args.PATH
+
+ # Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=622084
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+ app = App()
+ app.run(app_args)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/config/chroot_local-includes/usr/local/lib/generate-tor-browser-profile b/config/chroot_local-includes/usr/local/lib/generate-tor-browser-profile
index 23e0c46..1ded383 100755
--- a/config/chroot_local-includes/usr/local/lib/generate-tor-browser-profile
+++ b/config/chroot_local-includes/usr/local/lib/generate-tor-browser-profile
@@ -3,6 +3,9 @@
set -e
set -u
+# Import the TBB_PROFILE variable
+. /usr/local/lib/tails-shell-library/tor-browser.sh
+
USER_PROFILE="${HOME}/.tor-browser"
if [ -e "${USER_PROFILE}" ]; then
@@ -11,4 +14,4 @@ if [ -e "${USER_PROFILE}" ]; then
fi
mkdir -p "${USER_PROFILE}"
-cp -a /etc/tor-browser/profile "${USER_PROFILE}"/profile.default
+cp -a "${TBB_PROFILE}" "${USER_PROFILE}"/profile.default
diff --git a/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/__init__.py b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/__init__.py
new file mode 100644
index 0000000..50f48c2
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/__init__.py
@@ -0,0 +1,12 @@
+# Translation stuff
+
+import os
+import gettext
+
+
+if os.path.exists('po/locale'):
+ translation = gettext.translation("tails", 'po/locale', fallback=True)
+else:
+ translation = gettext.translation("tails", '/usr/share/locale', fallback=True)
+
+_ = translation.gettext
diff --git a/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/config.py b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/config.py
new file mode 100644
index 0000000..4a40ff2
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/config.py
@@ -0,0 +1,7 @@
+from os import path
+
+APP_NAME = "unlock-veracrypt-volumes"
+DATA_DIR = "/usr/share/%s/" % APP_NAME
+UI_DIR = path.join(DATA_DIR, "ui")
+MAIN_UI_FILE = path.join(UI_DIR, "main.ui")
+VOLUME_UI_FILE = path.join(UI_DIR, "volume.ui")
diff --git a/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/exceptions.py b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/exceptions.py
new file mode 100644
index 0000000..01a728d
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/exceptions.py
@@ -0,0 +1,10 @@
+class UdisksObjectNotFoundError(Exception):
+ pass
+
+
+class VolumeNotFoundError(Exception):
+ pass
+
+
+class AlreadyUnlockedError(Exception):
+ pass \ No newline at end of file
diff --git a/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py
new file mode 100644
index 0000000..dc9bed6
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py
@@ -0,0 +1,346 @@
+from logging import getLogger
+from typing import Union
+
+from gi.repository import Gtk, GLib, Gio, UDisks
+
+from unlock_veracrypt_volumes import _
+from unlock_veracrypt_volumes.config import VOLUME_UI_FILE, APP_NAME
+from unlock_veracrypt_volumes.exceptions import UdisksObjectNotFoundError, AlreadyUnlockedError
+
+logger = getLogger(__name__)
+
+
+class Volume(object):
+ def __init__(self, manager,
+ gio_volume: Gio.Volume = None,
+ udisks_object: UDisks.Object = None,
+ with_udisks=True):
+ self.manager = manager
+ self.udisks_client = manager.udisks_client
+ self.udev_client = manager.udev_client
+ self.gio_volume = gio_volume
+
+ if udisks_object:
+ self.udisks_object = udisks_object
+ elif self.gio_volume and with_udisks:
+ self.udisks_object = self._find_udisks_object()
+ else:
+ self.udisks_object = None
+
+ self.spinner_is_showing = False
+ self.dialog_is_showing = False
+
+ self.builder = Gtk.Builder.new_from_file(VOLUME_UI_FILE)
+ self.builder.set_translation_domain(APP_NAME)
+ self.builder.connect_signals(self)
+ self.list_box_row = self.builder.get_object("volume_row") # type: Gtk.ListBoxRow
+ self.box = self.builder.get_object("volume_box") # type: Gtk.Box
+ self.label = self.builder.get_object("volume_label") # type: Gtk.Label
+ self.button_box = self.builder.get_object("volume_button_box") # type: Gtk.ButtonBox
+ self.open_button = self.builder.get_object("open_button") # type: Gtk.Button
+ self.lock_button = self.builder.get_object("lock_button") # type: Gtk.Button
+ self.unlock_button = self.builder.get_object("unlock_button") # type: Gtk.Button
+ self.detach_button = self.builder.get_object("detach_button") # type: Gtk.Button
+ self.spinner = Gtk.Spinner(visible=True, margin_right=10)
+
+ def __eq__(self, other: "Volume"):
+ return self.device_file == other.device_file
+
+ @property
+ def name(self) -> str:
+ """Short description for display to the user. The block device
+ label or partition label, if any, plus the size"""
+ block_label = self.udisks_object.get_block().props.id_label
+ partition = self.udisks_object.get_partition()
+ if block_label:
+ return _("{volume_label} ({volume_size})").format(volume_label=block_label,
+ volume_size=self.size_for_display)
+ elif partition and partition.props.name:
+ return _("{partition_name} ({partition_size})").format(partition_name=partition.props.name,
+ partition_size=self.size_for_display)
+ else:
+ return _("{volume_size} Volume").format(volume_size=self.size_for_display)
+
+ @property
+ def size_for_display(self) -> str:
+ size = self.udisks_object.get_block().props.size
+ return self.udisks_client.get_size_for_display(size, use_pow2=False, long_string=False)
+
+ @property
+ def drive_name(self) -> str:
+ if self.is_file_container:
+ return str()
+
+ if self.is_unlocked:
+ drive_object = self.udisks_client.get_object(self.backing_udisks_object.get_block().props.drive)
+ else:
+ drive_object = self.drive_object
+
+ if drive_object:
+ return "%s %s" % (drive_object.get_drive().props.vendor, drive_object.get_drive().props.model)
+ else:
+ return str()
+
+ @property
+ def backing_file_name(self) -> str:
+ if not self.is_file_container:
+ return str()
+ if self.is_unlocked:
+ return self.backing_udisks_object.get_loop().props.backing_file
+ elif self.is_loop_device:
+ return self.udisks_object.get_loop().props.backing_file
+ elif self.partition_table_object and self.partition_table_object.get_loop():
+ return self.partition_table_object.get_loop().props.backing_file
+
+ @property
+ def description(self) -> str:
+ """Longer description for display to the user."""
+ if self.udisks_object.get_block().props.read_only:
+ # Translators: Don't translate {volume_name}, it's a placeholder and
+ # will be replaced.
+ desc = _("{volume_name} (Read-Only)").format(volume_name=self.name)
+ else:
+ desc = self.name
+
+ if self.partition_table_object and self.partition_table_object.get_loop():
+ # This is a partition of a loop device, so lets include the backing file name
+ # Translators: Don't translate {partition_name} and {container_path}, they
+ # are placeholders and will be replaced.
+ return _("{partition_name} in {container_path}").format(partition_name=desc,
+ container_path=self.backing_file_name)
+ elif self.is_file_container:
+ # This is file container, lets include the file name
+ # Translators: Don't translate {volume_name} and {path_to_file_container},
+ # they are placeholders and will be replaced. You should only have to translate
+ # this string if it makes sense to reverse the order of the placeholders.
+ return _("{volume_name} – {path_to_file_container}").format(volume_name=desc,
+ path_to_file_container=self.backing_file_name)
+ elif self.is_partition and self.drive_object:
+ # This is a partition on a drive, lets include the drive name
+ # Translators: Don't translate {partition_name} and {drive_name}, they
+ # are placeholders and will be replaced.
+ return _("{partition_name} on {drive_name}").format(partition_name=desc,
+ drive_name=self.drive_name)
+ elif self.drive_name:
+ # This is probably an unpartitioned drive, so lets include the drive name
+ # Translators: Don't translate {volume_name} and {drive_name},
+ # they are placeholders and will be replaced. You should only have to translate
+ # this string if it makes sense to reverse the order of the placeholders.
+ return _("{volume_name} – {drive_name}").format(volume_name=desc,
+ drive_name=self.drive_name)
+ else:
+ return desc
+
+ @property
+ def device_file(self) -> str:
+ if self.gio_volume:
+ return self.gio_volume.get_identifier(Gio.VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)
+ elif self.udisks_object:
+ return self.udisks_object.get_block().props.device
+
+ @property
+ def backing_volume(self) -> Union["Volume", None]:
+ if self.backing_udisks_object:
+ return Volume(self.manager, udisks_object=self.backing_udisks_object)
+ return None
+
+ @property
+ def backing_udisks_object(self) -> Union[UDisks.Object, None]:
+ return self.udisks_client.get_object(self.udisks_object.get_block().props.crypto_backing_device)
+
+ @property
+ def partition_table_object(self) -> Union[UDisks.Object, None]:
+ if not self.udisks_object.get_partition():
+ return None
+ return self.udisks_client.get_object(self.udisks_object.get_partition().props.table)
+
+ @property
+ def drive_object(self) -> Union[UDisks.Object, None]:
+ return self.udisks_client.get_object(self.udisks_object.get_block().props.drive)
+
+ @property
+ def is_unlocked(self) -> bool:
+ return bool(self.backing_udisks_object)
+
+ @property
+ def is_loop_device(self) -> bool:
+ return bool(self.udisks_object.get_loop())
+
+ @property
+ def is_loop_device_partition(self) -> bool:
+ return bool(self.partition_table_object and self.partition_table_object.get_loop())
+
+ @property
+ def is_partition(self) -> bool:
+ return bool(self.udisks_object.get_partition())
+
+ @property
+ def is_tcrypt(self) -> bool:
+ if self.is_unlocked:
+ udisks_object = self.backing_udisks_object
+ else:
+ udisks_object = self.udisks_object
+
+ return bool(udisks_object.get_encrypted() and
+ udisks_object.get_block().props.id_type in ("crypto_TCRYPT", "crypto_unknown"))
+
+ @property
+ def is_file_container(self) -> bool:
+ if "/dev/loop" in self.device_file:
+ return True
+
+ if "/dev/dm" in self.device_file:
+ return bool(self.backing_udisks_object and self.backing_udisks_object.get_loop())
+
+ def unlock(self, open_after_unlock=False):
+
+ def on_mount_operation_reply(mount_op: Gtk.MountOperation, result: Gio.MountOperationResult):
+ logger.debug("in on_mount_operation_reply")
+ if result == Gio.MountOperationResult.HANDLED:
+ self.show_spinner()
+
+ def mount_cb(gio_volume: Gio.Volume, result: Gio.AsyncResult):
+ logger.debug("in mount_cb")
+ self.hide_spinner()
+ try:
+ gio_volume.mount_finish(result)
+ except GLib.Error as e:
+ if e.code == Gio.IOErrorEnum.FAILED_HANDLED:
+ logger.warning("Couldn't unlock volume: %s:", e.message)
+ return
+
+ logger.exception(e)
+
+ if "No key available with this passphrase" in e.message or \
+ "No device header detected with this passphrase" in e.message:
+ title = "Wrong passphrase or parameters"
+ else:
+ title = "Error unlocking volume"
+
+ body = "Couldn't unlock volume %s:\n%s" % (self.name, e.message)
+ self.manager.show_warning(title, body)
+ return
+ finally:
+ self.manager.mount_op_lock.release()
+
+ if open_after_unlock:
+ self.open()
+
+ if self.is_unlocked:
+ raise AlreadyUnlockedError("Volume %s is already unlocked" % self.device_file)
+
+ logger.info("Unlocking volume %s", self.device_file)
+ self.dialog_is_showing = False
+ mount_operation = Gtk.MountOperation()
+ mount_operation.set_username("user")
+ mount_operation.connect("reply", on_mount_operation_reply)
+
+ # Things break if multiple mount operations are running at the same time,
+ # so we use a lock to prevent that
+ self.manager.acquire_mount_op_lock()
+ self.gio_volume.mount(0, # Gio.MountMountFlags
+ mount_operation, # Gtk.MountOperation
+ None, # Gio.Cancellable
+ mount_cb) # callback
+
+ def lock(self):
+ logger.info("Locking volume %s", self.device_file)
+ self.udisks_object.get_encrypted().call_lock_sync(GLib.Variant('a{sv}', {}), # options
+ None) # cancellable
+
+ def unmount(self):
+ logger.info("Unmounting volume %s", self.device_file)
+ while self.udisks_object.get_filesystem().props.mount_points:
+ try:
+ self.udisks_object.get_filesystem().call_unmount_sync(GLib.Variant('a{sv}', {}), # options
+ None) # cancellable
+ except GLib.Error as e:
+ if "org.freedesktop.UDisks2.Error.NotMounted" in e.message:
+ return
+ raise
+
+ def detach_loop_device(self):
+ logger.info("Detaching volume %s", self.device_file)
+ if self.is_loop_device:
+ self.udisks_object.get_loop().call_delete_sync(GLib.Variant('a{sv}', {}), # options
+ None) # cancellable
+ elif self.is_loop_device_partition:
+ self.partition_table_object.get_loop().call_delete_sync(GLib.Variant('a{sv}', {}), # options
+ None) # cancellable
+
+ def open(self):
+ logger.info("Opening volume %s", self.device_file)
+ mount_points = self.udisks_object.get_filesystem().props.mount_points
+ if not mount_points:
+ self.mount()
+ self.open()
+ else:
+ self.manager.open_uri(GLib.filename_to_uri(mount_points[0]))
+
+ def mount(self):
+ logger.info("Mounting volume %s", self.device_file)
+ self.udisks_object.get_filesystem().call_mount_sync(GLib.Variant('a{sv}', {}), # options
+ None) # cancellable
+
+ def show_spinner(self):
+ logger.debug("in show_spinner")
+ self.button_box.hide()
+ self.button_box.set_no_show_all(True)
+ self.box.add(self.spinner)
+ self.spinner.start()
+ self.spinner.show()
+
+ def hide_spinner(self):
+ logger.debug("in hide_spinner")
+ self.button_box.set_no_show_all(False)
+ self.button_box.show()
+ self.spinner.stop()
+ self.box.remove(self.spinner)
+
+ def on_lock_button_clicked(self, button):
+ logger.debug("in on_lock_button_clicked")
+ loop = self.backing_volume.udisks_object.get_loop()
+ if loop:
+ # Ensure that the loop device is removed after locking the volume
+ loop.call_set_autoclear_sync(True,
+ GLib.Variant('a{sv}', {}), # options
+ None) # cancellable
+ self.unmount()
+ self.backing_volume.lock()
+
+ def on_unlock_button_clicked(self, button):
+ logger.debug("in on_unlock_button_clicked")
+ self.unlock()
+
+ def on_detach_button_clicked(self, button):
+ logger.debug("in on_detach_button_clicked")
+ self.detach_loop_device()
+
+ def on_open_button_clicked(self, button):
+ logger.debug("in on_open_button_clicked")
+ self.open()
+
+ def update_list_box_row(self):
+ logger.debug("in update_list_box_row. is_unlocked: %s", self.is_unlocked)
+ self.label.set_label(self.description)
+ self.open_button.set_visible(self.is_unlocked)
+ self.lock_button.set_visible(self.is_unlocked)
+ self.unlock_button.set_visible(not self.is_unlocked)
+ self.detach_button.set_visible(not self.is_unlocked and (self.is_loop_device or self.is_loop_device_partition))
+
+ def _find_udisks_object(self) -> UDisks.Object:
+ device_file = self.gio_volume.get_identifier(Gio.VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)
+ if not device_file:
+ raise UdisksObjectNotFoundError("Couldn't get device file for volume")
+
+ udev_volume = self.udev_client.query_by_device_file(device_file)
+ if not udev_volume:
+ raise UdisksObjectNotFoundError("Couldn't get udev volume for %s" % device_file)
+
+ device_number = udev_volume.get_device_number()
+ udisks_block = self.udisks_client.get_block_for_dev(device_number)
+ if not udisks_block:
+ raise UdisksObjectNotFoundError("Couldn't get UDisksBlock for volume %s" % device_file)
+
+ object_path = udisks_block.get_object_path()
+ return self.udisks_client.get_object(object_path)
diff --git a/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_list.py b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_list.py
new file mode 100644
index 0000000..12404d5
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_list.py
@@ -0,0 +1,98 @@
+from logging import getLogger
+import abc
+from typing import List, Union
+
+from gi.repository import Gtk
+
+from unlock_veracrypt_volumes import _
+from unlock_veracrypt_volumes.volume import Volume
+from unlock_veracrypt_volumes.exceptions import VolumeNotFoundError
+
+logger = getLogger(__name__)
+
+
+class VolumeList(object, metaclass=abc.ABCMeta):
+
+ placeholder_label = str()
+
+ def __init__(self):
+ self.volumes = list()
+ self.list_box = Gtk.ListBox(selection_mode=Gtk.SelectionMode.NONE)
+ self.list_box.set_header_func(self.listbox_header_func)
+ self.placeholder_row = Gtk.ListBoxRow(activatable=False, selectable=False)
+ self.placeholder_row.add(Gtk.Label(self.placeholder_label))
+ self.show_placeholder()
+
+ def __getitem__(self, item):
+ return self.volumes[item]
+
+ @staticmethod
+ def listbox_header_func(row, before, data=None):
+ if not before:
+ return
+ separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
+ row.set_header(separator)
+
+ def add(self, volume: Volume):
+ if volume in self.volumes:
+ self.update(volume)
+ return
+
+ volume.update_list_box_row()
+ self.list_box.add(volume.list_box_row)
+ self.volumes.append(volume)
+
+ if len(self.volumes) == 1:
+ self.hide_placeholder()
+
+ self.list_box.show_all()
+
+ def remove(self, volume: Volume):
+ # Note that we can't use any properties and functions of the volume here
+ # which use udisks, because the volume might be already removed from udisks
+ if volume not in self.volumes:
+ logger.warning("Can't remove volume %s: Not in list", volume.device_file)
+ return
+
+ index = self.volumes.index(volume)
+ self.list_box.remove(self.list_box.get_children()[index])
+ self.volumes.remove(volume)
+
+ if not self.volumes:
+ self.show_placeholder()
+
+ self.list_box.show_all()
+
+ def update(self, volume: Volume):
+ self.remove(volume)
+ self.add(volume)
+
+ def clear(self):
+ for child in self.list_box.get_children():
+ self.list_box.remove(child)
+
+ def show_placeholder(self):
+ self.list_box.add(self.placeholder_row)
+
+ def hide_placeholder(self):
+ self.list_box.remove(self.placeholder_row)
+
+
+class ContainerList(VolumeList):
+ """Manages attached file containers"""
+ placeholder_label = _("No file containers added")
+
+ @property
+ def backing_file_paths(self) -> List[str]:
+ return [volume.backing_file_name for volume in self.volumes]
+
+ def find_by_backing_file(self, path: str) -> Union[Volume, None]:
+ for volume in self.volumes:
+ if volume.backing_file_name == path:
+ return volume
+ raise VolumeNotFoundError()
+
+
+class DeviceList(VolumeList):
+ """Manages physically connected drives and partitions"""
+ placeholder_label = _("No VeraCrypt devices detected")
diff --git a/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py
new file mode 100644
index 0000000..764beae
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py
@@ -0,0 +1,264 @@
+import subprocess
+import time
+import os
+from logging import getLogger
+from typing import List, Union
+from threading import Lock
+
+from gi.repository import Gtk, Gio, UDisks, GUdev, GLib
+
+from unlock_veracrypt_volumes import _
+from unlock_veracrypt_volumes.volume_list import ContainerList, DeviceList
+from unlock_veracrypt_volumes.volume import Volume
+from unlock_veracrypt_volumes.exceptions import UdisksObjectNotFoundError, VolumeNotFoundError
+from unlock_veracrypt_volumes.config import APP_NAME, MAIN_UI_FILE
+
+
+WAIT_FOR_LOOP_SETUP_TIMEOUT = 1
+
+
+logger = getLogger(__name__)
+
+
+class VolumeManager(object):
+ def __init__(self, application: Gtk.Application):
+ self.udisks_client = UDisks.Client.new_sync()
+ self.udisks_manager = self.udisks_client.get_manager()
+ self.gio_volume_monitor = Gio.VolumeMonitor.get()
+ self.gio_volume_monitor.connect("volume-changed", self.on_volume_changed)
+ self.gio_volume_monitor.connect("volume-added", self.on_volume_added)
+ self.gio_volume_monitor.connect("volume-removed", self.on_volume_removed)
+ self.udev_client = GUdev.Client()
+ self.mount_op_lock = Lock()
+
+ self.builder = Gtk.Builder.new_from_file(MAIN_UI_FILE)
+ self.builder.set_translation_domain(APP_NAME)
+ self.builder.connect_signals(self)
+
+ self.window = self.builder.get_object("window") # type: Gtk.ApplicationWindow
+ self.window.set_application(application)
+ self.window.set_title("Unlock VeraCrypt Volumes")
+
+ self.container_list = ContainerList()
+ self.device_list = DeviceList()
+
+ containers_frame = self.builder.get_object("containers_frame")
+ containers_frame.add(self.container_list.list_box)
+ devices_frame = self.builder.get_object("devices_frame")
+ devices_frame.add(self.device_list.list_box)
+
+ self.add_tcrypt_volumes()
+
+ logger.debug("showing window")
+ self.window.show_all()
+ self.window.present()
+
+ def add_tcrypt_volumes(self):
+ logger.debug("in add_tcrypt_volumes")
+ for volume in self.get_tcrypt_volumes():
+ self.add_volume(volume)
+
+ def add_volume(self, volume: Volume):
+ logger.info("Adding volume %s", volume.device_file)
+ if volume.is_file_container:
+ self.container_list.add(volume)
+ else:
+ self.device_list.add(volume)
+
+ def remove_volume(self, volume: Volume):
+ logger.info("Removing volume %s", volume.device_file)
+ if volume in self.container_list:
+ self.container_list.remove(volume)
+ elif volume in self.device_list:
+ self.device_list.remove(volume)
+
+ def update_volume(self, volume: Volume):
+ logger.debug("Updating volume %s", volume.device_file)
+ if volume.is_file_container:
+ self.container_list.remove(volume)
+ self.container_list.add(volume)
+ else:
+ self.device_list.remove(volume)
+ self.device_list.add(volume)
+
+ def get_tcrypt_volumes(self) -> List[Volume]:
+ """Returns all connected TCRYPT volumes"""
+ return [volume for volume in self.get_all_volumes() if volume.is_tcrypt]
+
+ def get_all_volumes(self) -> List[Volume]:
+ """Returns all connected volumes"""
+ volumes = list()
+ gio_volumes = self.gio_volume_monitor.get_volumes()
+
+ for gio_volume in gio_volumes:
+ device_file = gio_volume.get_identifier(Gio.VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)
+ if not device_file:
+ continue
+
+ logger.debug("volume: %s", device_file)
+
+ try:
+ volumes.append(Volume(self, gio_volume))
+ logger.debug("is_file_container: %s", volumes[-1].is_file_container)
+ logger.debug("is_tcrypt: %s", volumes[-1].is_tcrypt)
+ logger.debug("is_unlocked: %s", volumes[-1].is_unlocked)
+ except UdisksObjectNotFoundError as e:
+ logger.exception(e)
+
+ return volumes
+
+ def on_add_file_container_button_clicked(self, button, data=None):
+ path = self.choose_container_path()
+
+ if path in self.container_list.backing_file_paths:
+ self.show_warning(title=_("Container already added"),
+ body=_("The file container %s should already be listed.") % path)
+ return
+
+ if path:
+ self.unlock_file_container(path)
+
+ def attach_file_container(self, path: str) -> Union[Volume, None]:
+ logger.debug("attaching file %s. backing_file_paths: %s", path, self.container_list.backing_file_paths)
+ warning = dict()
+
+ try:
+ fd = os.open(path, os.O_RDWR)
+ except PermissionError as e:
+ # Try opening read-only
+ try:
+ fd = os.open(path, os.O_RDONLY)
+ warning["title"] = _("Container opened read-only")
+ warning["body"] = _("The file container {path} could not be opened with write access. "
+ "It was opened read-only instead. You will not be able to modify the "
+ "content of the container.\n"
+ "{error_message}").format(path=path, error_message=str(e))
+ except PermissionError as e:
+ self.show_warning(title=_("Error opening file"), body=str(e))
+ return None
+
+ fd_list = Gio.UnixFDList()
+ fd_list.append(fd)
+ udisks_path, __ = self.udisks_manager.call_loop_setup_sync(GLib.Variant('h', 0), # fd index
+ GLib.Variant('a{sv}', {}), # options
+ fd_list, # the fd list
+ None) # cancellable
+ logger.debug("Created loop device %s", udisks_path)
+
+ volume = self._wait_for_loop_setup(path)
+ if volume:
+ if warning:
+ self.show_warning(title=warning["title"], body=warning["body"])
+ return volume
+ elif not self._udisks_object_is_tcrypt(udisks_path):
+ # Remove the loop device
+ self.udisks_client.get_object(udisks_path).get_loop().call_delete(GLib.Variant('a{sv}', {}), # options
+ None, # cancellable
+ None, # callback
+ None) # user data
+ self.show_warning(title=_("Not a VeraCrypt container"),
+ body=_("The file %s does not seem to be a VeraCrypt container.") % path)
+ else:
+ self.show_warning(title=_("Failed to add container"),
+ body=_("Could not add file container %s: Timeout while waiting for loop setup."
+ "Please try using the <i>Disks</i> application instead.") % path)
+
+ def _wait_for_loop_setup(self, path: str) -> Union[Volume, None]:
+ start_time = time.perf_counter()
+ while time.perf_counter() - start_time < WAIT_FOR_LOOP_SETUP_TIMEOUT:
+ try:
+ return self.container_list.find_by_backing_file(path)
+ except VolumeNotFoundError:
+ self.process_mainloop_events()
+ time.sleep(0.1)
+
+ def _udisks_object_is_tcrypt(self, path: str) -> bool:
+ if not path:
+ return False
+
+ udisks_object = self.udisks_client.get_object(path)
+ if not udisks_object:
+ return False
+
+ return Volume(self, udisks_object=udisks_object).is_tcrypt
+
+ @staticmethod
+ def process_mainloop_events():
+ context = GLib.MainLoop().get_context()
+ while context.pending():
+ context.iteration()
+
+ def open_file_container(self, path: str):
+ volume = self.ensure_file_container_is_attached(path)
+ if volume:
+ volume.open()
+
+ def unlock_file_container(self, path: str, open_after_unlock=False):
+ volume = self.ensure_file_container_is_attached(path)
+ if volume:
+ volume.unlock(open_after_unlock=open_after_unlock)
+
+ def ensure_file_container_is_attached(self, path: str) -> Volume:
+ try:
+ return self.container_list.find_by_backing_file(path)
+ except VolumeNotFoundError:
+ return self.attach_file_container(path)
+
+ def choose_container_path(self):
+ dialog = Gtk.FileChooserDialog(_("Choose File Container"),
+ self.window,
+ Gtk.FileChooserAction.OPEN,
+ (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
+ Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT))
+ result = dialog.run()
+ if result != Gtk.ResponseType.ACCEPT:
+ dialog.destroy()
+ return
+
+ path = dialog.get_filename()
+ dialog.destroy()
+ return path
+
+ def on_volume_changed(self, volume_monitor: Gio.VolumeMonitor, gio_volume: Gio.Volume):
+ logger.debug("in on_volume_changed. volume: %s",
+ gio_volume.get_identifier(Gio.VOLUME_IDENTIFIER_KIND_UNIX_DEVICE))
+ try:
+ volume = Volume(self, gio_volume)
+ if volume.is_tcrypt:
+ self.update_volume(volume)
+ except UdisksObjectNotFoundError:
+ self.remove_volume(Volume(self, gio_volume, with_udisks=False))
+
+ def on_volume_added(self, volume_monitor: Gio.VolumeMonitor, gio_volume: Gio.Volume):
+ logger.debug("in on_volume_added. volume: %s",
+ gio_volume.get_identifier(Gio.VOLUME_IDENTIFIER_KIND_UNIX_DEVICE))
+ volume = Volume(self, gio_volume)
+ if volume.is_tcrypt:
+ self.add_volume(volume)
+
+ def on_volume_removed(self, volume_monitor: Gio.VolumeMonitor, gio_volume: Gio.Volume):
+ logger.debug("in on_volume_removed. volume: %s",
+ gio_volume.get_identifier(Gio.VOLUME_IDENTIFIER_KIND_UNIX_DEVICE))
+ self.remove_volume(Volume(self, gio_volume, with_udisks=False))
+
+ def open_uri(self, uri: str):
+ # This is the recommended way, but it turns the cursor into wait status for up to
+ # 10 seconds after the file manager was already opened.
+ # Gtk.show_uri_on_window(self.window, uri, Gtk.get_current_event_time())
+ subprocess.Popen(["xdg-open", uri])
+
+ def show_warning(self, title: str, body: str):
+ dialog = Gtk.MessageDialog(self.window,
+ Gtk.DialogFlags.DESTROY_WITH_PARENT,
+ Gtk.MessageType.WARNING,
+ Gtk.ButtonsType.CLOSE,
+ title)
+ dialog.format_secondary_markup(body)
+ dialog.run()
+ dialog.close()
+
+ def acquire_mount_op_lock(self):
+ while True:
+ if self.mount_op_lock.acquire(timeout=0.1):
+ return
+ self.process_mainloop_events()
diff --git a/config/chroot_local-includes/usr/local/lib/tails-additional-software-notify b/config/chroot_local-includes/usr/local/lib/tails-additional-software-notify
new file mode 100755
index 0000000..2435fe9
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-additional-software-notify
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+
+import gettext
+import os
+import os.path
+import subprocess
+import sys
+
+import gi
+
+from gi.repository import GLib
+
+gi.require_version('Notify', '0.7')
+from gi.repository import Notify # NOQA: E402
+
+_ = gettext.gettext
+
+
+class ASPNotifier(object):
+ """Display a notification and exit with a meaningful code."""
+
+ def __init__(self, title, body, accept_label=None, deny_label=None,
+ documentation_target=None, urgent=False):
+ """Shows a notification with two optional action buttons.
+
+ If there are no buttons, exit straight away with a meaningful code.
+ """
+ Notify.init("org.boum.tails.additional-software-packages")
+
+ # We need to hold a reference to the notification until the callbacks
+ # are called. That's why we use an instance variable.
+ self.notification = Notify.Notification.new(
+ title, body, icon="package-x-generic")
+ if urgent:
+ self.notification.set_urgency(Notify.Urgency.CRITICAL)
+ if documentation_target:
+ self.notification.add_action("documentation", _("Documentation"),
+ self.cb_notification_clicked,
+ documentation_target)
+ if deny_label:
+ self.notification.add_action("deny", deny_label,
+ self.cb_notification_clicked, None)
+ if accept_label:
+ self.notification.add_action("accept", accept_label,
+ self.cb_notification_clicked, None)
+ self.notification.connect("closed", self.cb_notification_closed)
+ self.notification.show()
+ sys.stdout.write("id=%i" % self.notification.props.id)
+ if not (accept_label or deny_label or documentation_target):
+ sys.exit(2)
+
+ def cb_notification_clicked(self, notification, action, user_data=None):
+ """Exit the program with a meaningful code on action triggering."""
+ if action == "accept":
+ sys.exit(0)
+ elif action == "deny":
+ sys.exit(3)
+ elif action == "documentation":
+ subprocess.Popen(["tails-documentation", user_data])
+ sys.exit(5)
+
+ def cb_notification_closed(self, notification):
+ """Exit the program with a meaningful code on notification close."""
+ sys.exit(4)
+
+
+def print_help():
+ """The subcommand which displays help
+ """
+ program_name = os.path.basename(sys.argv[0])
+ sys.stderr.write(
+ "Usage: %s <summary> <body> [<accept_label> [<deny_label> "
+ "[documentation_target [<urgent>]]]]\n" % program_name)
+ sys.stderr.write(
+ "Shows a notification with <summary>, <body> and optional "
+ "buttons.\n"
+ "\n"
+ "Returns: 0 if the button with <accept_label> is selected\n"
+ " 2 if the arguments are wrong\n"
+ " 3 if the button with <deny_label> is selected\n"
+ " 4 if the notification is closed another way\n",
+ " 5 if the documentation button is selected and the"
+ " documentation helper is launched.\n")
+
+
+if __name__ == "__main__":
+ os.environ["DBUS_SESSION_BUS_ADDRESS"] = \
+ "unix:path=/run/user/{uid}/bus".format(uid=os.getuid())
+
+ gettext.install("tails")
+
+ if not 3 <= len(sys.argv) <= 7:
+ print_help()
+ sys.exit(2)
+
+ mainloop = GLib.MainLoop.new(None, False)
+ ASPNotifier(*sys.argv[1:])
+ mainloop.run()
diff --git a/config/chroot_local-includes/usr/local/lib/tails-boot-device-can-have-persistence b/config/chroot_local-includes/usr/local/lib/tails-boot-device-can-have-persistence
new file mode 100755
index 0000000..4ff759f
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-boot-device-can-have-persistence
@@ -0,0 +1,29 @@
+#!/usr/bin/perl
+
+=head1 NAME
+
+tails-boot-device-can-have-persistence - test if the boot device is supported for persistence
+
+=cut
+
+use strictures 2;
+use 5.10.1;
+
+use FindBin;
+use lib "$FindBin::Bin/../lib";
+
+use Tails::RunningSystem;
+
+my $running_system = Tails::RunningSystem->new;
+
+if (! $running_system->started_from_writable_device) {
+ say STDERR "Tails was started from a DVD or a read-only device";
+ exit 16;
+}
+
+if (! $running_system->started_from_device_installed_with_tails_installer) {
+ say STDERR "The boot device was not created using Tails Installer";
+ exit 32;
+}
+
+exit 0;
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/chroot-browser.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/chroot-browser.sh
index cb4b436..3deed22 100644
--- a/config/chroot_local-includes/usr/local/lib/tails-shell-library/chroot-browser.sh
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/chroot-browser.sh
@@ -65,7 +65,8 @@ setup_chroot_for_browser () {
mount -t tmpfs tmpfs "${cow}" && \
mount -t aufs -o "noatime,noxino,dirs=${aufs_dirs}" aufs "${chroot}" && \
mount -t proc proc "${chroot}/proc" && \
- mount --bind "/dev" "${chroot}/dev" || \
+ mount --bind "/dev" "${chroot}/dev" && \
+ mount -t tmpfs -o rw,nosuid,nodev tmpfs "${chroot}/dev/shm" || \
return 1
# Workaround for #6110
@@ -125,9 +126,8 @@ configure_chroot_browser_profile () {
done
# Set preferences
- local browser_prefs="${browser_profile}/preferences/prefs.js"
+ local browser_prefs="${browser_profile}/user.js"
local chroot_browser_config="/usr/share/tails/chroot-browsers"
- mkdir -p "$(dirname "${browser_prefs}")"
cat "${chroot_browser_config}/common/prefs.js" \
"${chroot_browser_config}/${browser_name}/prefs.js" > "${browser_prefs}"
@@ -137,9 +137,6 @@ configure_chroot_browser_profile () {
"${browser_prefs}"
fi
- # Remove all bookmarks
- rm "${chroot}/${TBB_PROFILE}/bookmarks.html"
-
# Set an appropriate theme
cat "${chroot_browser_config}/${browser_name}/theme.js" >> "${browser_prefs}"
@@ -181,7 +178,7 @@ set_chroot_browser_name () {
# Surprisingly, the default locale is en, not en-US
torbutton_locale_dir="${chroot}/usr/share/xul-ext/torbutton/chrome/locale/en"
fi
- sed -i "s/<"'!'"ENTITY\s\+brand\(Full\|Short\)Name.*$/<"'!'"ENTITY brand\1Name \"${human_readable_name}\">/" "${torbutton_locale_dir}/brand.dtd"
+ sed -i "s/<"'!'"ENTITY\s\+brand\(Full\|Short\|Shorter\)Name.*$/<"'!'"ENTITY brand\1Name \"${human_readable_name}\">/" "${torbutton_locale_dir}/brand.dtd"
# Since Torbutton decides the name, we don't have to mess with
# with the browser's own branding, which will save time and
# memory.
@@ -199,14 +196,47 @@ set_chroot_browser_name () {
rest="en-US/locale"
fi
local tmp="$(mktemp -d)"
- local branding="${top}/${rest}/branding/brand.dtd"
- 7z x -o"${tmp}" "${pack}" "${branding}"
- sed -i "s/<"'!'"ENTITY\s\+brand\(Full\|Short\)Name.*$/<"'!'"ENTITY brand\1Name \"${human_readable_name}\">/" "${tmp}/${branding}"
+ local branding_dtd="${top}/${rest}/branding/brand.dtd"
+ local branding_properties="${top}/${rest}/branding/brand.properties"
+ 7z x -o"${tmp}" "${pack}" "${branding_dtd}" "${branding_properties}"
+ sed -i "s/<"'!'"ENTITY\s\+brand\(Full\|Short\|Shorter\)Name.*$/<"'!'"ENTITY brand\1Name \"${human_readable_name}\">/" "${tmp}/${branding_dtd}"
+ perl -pi -E \
+ 's/^(brand(?:Full|Short|Shorter)Name=).*$/$1'"${human_readable_name}/" \
+ "${tmp}/${branding_properties}"
(cd ${tmp} ; 7z u -tzip "${pack}" .)
chmod a+r "${pack}"
rm -Rf "${tmp}"
}
+delete_chroot_browser_searchplugins() {
+ local chroot="${1}"
+ local locale="${2}"
+ local ext_dir="${chroot}/${TBB_EXT}"
+
+ if [ "${locale}" != "en-US" ]; then
+ pack="${ext_dir}/langpack-${locale}@firefox.mozilla.org.xpi"
+ top="browser/chrome"
+ rest="${locale}/locale"
+ else
+ pack="${chroot}/${TBB_INSTALL}/browser/omni.ja"
+ top="chrome"
+ rest="en-US/locale"
+ fi
+ local searchplugins_dir="${top}/${rest}/browser/searchplugins"
+ local searchplugins_list="${searchplugins_dir}/list.json"
+ local tmp="$(mktemp -d)"
+ (
+ cd "${tmp}"
+ 7z x -tzip "${pack}" "${searchplugins_dir}"
+ ls "${searchplugins_dir}"/*.xml | xargs 7z d -tzip "${pack}"
+ echo '{"default": {"visibleDefaultEngines": []}, "experimental-hidden": {"visibleDefaultEngines": []}}' \
+ > "${searchplugins_list}"
+ 7z u -tzip "${pack}" "${searchplugins_list}"
+ )
+ rm -r "${tmp}"
+ chmod a+r "${pack}"
+}
+
configure_chroot_browser () {
local chroot="${1}" ; shift
local browser_user="${1}" ; shift
@@ -223,6 +253,7 @@ configure_chroot_browser () {
"${best_locale}"
set_chroot_browser_name "${chroot}" "${human_readable_name}" \
"${browser_name}" "${browser_user}" "${best_locale}"
+ delete_chroot_browser_searchplugins "${chroot}" "${best_locale}"
set_chroot_browser_permissions "${chroot}" "${browser_name}" \
"${browser_user}"
}
@@ -233,12 +264,14 @@ run_browser_in_chroot () {
local browser_name="${2}"
local chroot_user="${3}"
local local_user="${4}"
+ local wm_class="${5}"
local profile="$(browser_profile_dir ${browser_name} ${chroot_user})"
sudo -u "${local_user}" xhost "+SI:localuser:${chroot_user}"
chroot "${chroot}" sudo -u "${chroot_user}" /bin/sh -c \
". /usr/local/lib/tails-shell-library/tor-browser.sh && \
exec_firefox -DISPLAY='${DISPLAY}' \
+ --class='${wm_class}' \
-profile '${profile}'"
sudo -u "${local_user}" xhost "-SI:localuser:${chroot_user}"
}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/thunderbird.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/thunderbird.sh
new file mode 100644
index 0000000..13f8bff
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/thunderbird.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+#
+# Heavily inspired by tor-browser.sh, needed since TB60 and the demise
+# of the intl.locale.matchOS setting.
+#
+# Instead of configuring a specific file in the profile directory, just
+# implement returning the appropriate locale, so that the caller can
+# save it along with other settings in a single file.
+
+TB_EXT=/usr/share/thunderbird/extensions
+
+guess_best_thunderbird_locale() {
+ local long_locale short_locale similar_locale
+ long_locale="$(echo ${LANG} | sed -e 's/\..*$//' -e 's/_/-/')"
+ short_locale="$(echo ${long_locale} | cut -d"-" -f1)"
+ if [ -e "${TB_EXT}/langpack-${long_locale}@firefox.mozilla.org.xpi" ]; then
+ echo "${long_locale}"
+ return
+ elif [ -e "${TB_EXT}/langpack-${short_locale}@firefox.mozilla.org.xpi" ]; then
+ echo "${short_locale}"
+ return
+ fi
+ # If we use locale xx-YY and there is no langpack for xx-YY nor xx
+ # there may be a similar locale xx-ZZ that we should use instead.
+ similar_locale="$(ls -1 "${TB_EXT}" | \
+ sed -n "s,^langpack-\(${short_locale}-[A-Z]\+\)@firefox.mozilla.org.xpi$,\1,p" | \
+ head -n 1)" || :
+ if [ -n "${similar_locale:-}" ]; then
+ echo "${similar_locale}"
+ return
+ fi
+
+ echo 'en-US'
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/tor-browser.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/tor-browser.sh
index f2ba8ca..71cdfe5 100644
--- a/config/chroot_local-includes/usr/local/lib/tails-shell-library/tor-browser.sh
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/tor-browser.sh
@@ -39,7 +39,7 @@ exec_firefox_helper() {
}
exec_firefox() {
- exec_firefox_helper firefox "${@}"
+ exec_firefox_helper firefox.real "${@}"
}
exec_unconfined_firefox() {
@@ -89,8 +89,9 @@ configure_xulrunner_app_locale() {
profile="${1}"
locale="${2}"
mkdir -p "${profile}"/preferences
- echo "pref(\"general.useragent.locale\", \"${locale}\");" > \
- "${profile}"/preferences/0000locale.js
+ set_mozilla_pref "${profile}"/prefs.js \
+ "intl.locale.requested" "\"${locale}\"" \
+ "user_pref"
}
configure_best_tor_browser_locale() {
@@ -99,7 +100,7 @@ configure_best_tor_browser_locale() {
best_locale="$(guess_best_tor_browser_locale)"
configure_xulrunner_app_locale "${profile}" "${best_locale}"
cat "/etc/tor-browser/locale-profiles/${best_locale}.js" \
- >> "${profile}/preferences/0000locale.js"
+ >> "${profile}/prefs.js"
}
configure_best_tor_launcher_locale() {
@@ -113,3 +114,23 @@ supported_tor_browser_locales() {
basename "${langpack}" | sed 's,^langpack-\([^@]\+\)@.*$,\1,'
done
}
+
+set_firefox_content_process_count() {
+ local profile="$1"
+ local count="$2"
+
+ set_mozilla_pref "${profile}/prefs.js" \
+ "dom.ipc.processCount" "$count" \
+ user_pref
+}
+
+configure_tor_browser_memory_usage() {
+ local profile="${1}"
+
+ # Unit: KiB
+ system_ram=$(awk '/^MemTotal:/ { print $2 }' /proc/meminfo)
+
+ if [ "$system_ram" -lt "$((3 * 1024 * 1024))" ]; then
+ set_firefox_content_process_count "$profile" 2
+ fi
+}
diff --git a/config/chroot_local-includes/usr/local/sbin/live-persist b/config/chroot_local-includes/usr/local/sbin/live-persist
index fe1eb93..8bdba4f 100755
--- a/config/chroot_local-includes/usr/local/sbin/live-persist
+++ b/config/chroot_local-includes/usr/local/sbin/live-persist
@@ -226,9 +226,9 @@ other::r-x"
persistence_conf_file_has_correct_access_rights ()
{
local conf="$1"
+ local expected_perms="$2"
local expected_user=tails-persistence-setup
local expected_group=tails-persistence-setup
- local expected_perms=600
local expected_acl=""
if [ $(stat -c %U "$conf") != "$expected_user" ]
@@ -258,18 +258,25 @@ persistence_conf_file_has_correct_access_rights ()
disable_and_create_empty_persistence_conf_file ()
{
local conf="$1"
+ local mode="$2"
+
+ if [ -z "$mode" ]
+ then
+ mode=0600
+ fi
mv "$conf" "${conf}.insecure_disabled" \
|| error "Failed to disable '$conf': $?"
- create_empty_persistence_conf_file "$conf"
+ create_empty_persistence_conf_file "$conf" "$mode"
}
create_empty_persistence_conf_file ()
{
local conf="$1"
+ local mode="$2"
install --owner tails-persistence-setup \
- --group tails-persistence-setup --mode 0600 \
+ --group tails-persistence-setup --mode "$mode" \
/dev/null "$conf" \
|| error "Failed to create empty '$conf': $?"
}
@@ -341,7 +348,7 @@ activate_volumes ()
do
if test ! -f "$mountpoint/live-additional-software.conf"
then
- create_empty_persistence_conf_file "$mountpoint/live-additional-software.conf"
+ create_empty_persistence_conf_file "$mountpoint/live-additional-software.conf" "0644"
fi
done
@@ -349,25 +356,40 @@ activate_volumes ()
# has wrong access rights.
if [ "$ACCESS_RIGHTS_ARE_CORRECT" != true ]
then
- for f in $(ls /live/persistence/*_unlocked/persistence.conf \
- /live/persistence/*_unlocked/live-additional-software.conf || true)
+ for f in $(ls /live/persistence/*_unlocked/persistence.conf || true)
do
warning "Disabling '$f': persistent volume has unsafe access rights"
disable_and_create_empty_persistence_conf_file "$f"
done
+ for f in $(ls /live/persistence/*_unlocked/live-additional-software.conf || true)
+ do
+ warning "Disabling '$f': persistent volume has unsafe access rights"
+ disable_and_create_empty_persistence_conf_file "$f" "644"
+ done
fi
# Regardless of the mountpoint access rights, disable persistence
# configuration files with wrong access rights.
- for f in $(ls /live/persistence/*_unlocked/persistence.conf \
- /live/persistence/*_unlocked/live-additional-software.conf || true)
+ for f in $(ls /live/persistence/*_unlocked/persistence.conf || true)
do
- if ! persistence_conf_file_has_correct_access_rights "$f"
+ if ! persistence_conf_file_has_correct_access_rights "$f" "600"
then
warning "Disabling '$f', that has unsafe access rights"
disable_and_create_empty_persistence_conf_file "$f"
fi
done
+ for f in $(ls /live/persistence/*_unlocked/live-additional-software.conf || true)
+ do
+ if persistence_conf_file_has_correct_access_rights "$f" "600"
+ then
+ chmod 0644 "$f"
+ fi
+ if ! persistence_conf_file_has_correct_access_rights "$f" "644"
+ then
+ warning "Disabling '$f', that has unsafe access rights"
+ disable_and_create_empty_persistence_conf_file "$f" "644"
+ fi
+ done
# Fix permissions on persistent directories that were created
# with unsafe permissions.
@@ -437,6 +459,14 @@ activate_volumes ()
fi
fi
+ # Get rid of any Enigmail configuredVersion that we previously used
+ # to set in a way that would persistently override the value maintained
+ # by Enigmail itself (#12680, #15693). We stopped writing this pref
+ # there a long time ago but recently instructed users to reintroduce
+ # this problem as a workaround (#15692).
+ tb_profile="$(dirname "${conf}")/thunderbird/profile.default"
+ rm -f "${tb_profile}/preferences/0000tails.js"
+
for vol in ${open_volumes}
do
if grep -qe "^${vol}\>" /proc/mounts
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-additional-software b/config/chroot_local-includes/usr/local/sbin/tails-additional-software
index ab31a96..cdca343 100755
--- a/config/chroot_local-includes/usr/local/sbin/tails-additional-software
+++ b/config/chroot_local-includes/usr/local/sbin/tails-additional-software
@@ -1,23 +1,53 @@
-#!/usr/bin/env python3
+#!/usr/bin/python3
import gettext
+import json
+import logging
+import logging.handlers
import os
import os.path
+import pwd
import shutil
import subprocess
import sys
-import syslog
+
+import apt.cache
+
+from tailslib import LIVE_USERNAME
+
+from tailslib.additionalsoftware.config import (
+ add_additional_packages,
+ filter_package_details,
+ get_additional_packages,
+ get_packages_list_path,
+ remove_additional_packages)
+
+from tailslib.persistence import (
+ has_unlocked_persistence,
+ has_persistence,
+ is_tails_media_writable,
+ launch_persistence_setup,
+ PERSISTENCE_DIR)
+
+from tailslib.utils import launch_x_application
_ = gettext.gettext
-PERSISTENCE_DIR = "/live/persistence/TailsData_unlocked"
-PACKAGES_LIST_FILE = os.path.join(
- PERSISTENCE_DIR, "live-additional-software.conf")
+ASP_STATE_DIR = "/run/live-additional-software"
+ASP_STATE_PACKAGES = os.path.join(ASP_STATE_DIR, "packages")
+ASP_STATE_INSTALLER_ASKED = os.path.join(ASP_STATE_DIR, "installer-asked")
+ASP_LOG_FILE = os.path.join(ASP_STATE_DIR, "log")
OLD_APT_LISTS_DIR = os.path.join(PERSISTENCE_DIR, 'apt', 'lists.old')
APT_ARCHIVES_DIR = "/var/cache/apt/archives"
APT_LISTS_DIR = "/var/lib/apt/lists"
+def _exit_if_in_live_build():
+ """Exits with success if running inside live-build."""
+ if "SOURCE_DATE_EPOCH" in os.environ:
+ sys.exit(0)
+
+
def _launch_apt_get(specific_args):
"""Launch apt-get with given arguments.
@@ -41,47 +71,171 @@ def _launch_apt_get(specific_args):
stdout=subprocess.PIPE)
for line in iter(apt_get.stdout.readline, ''):
if not line.startswith('('):
- syslog.syslog(line.rstrip())
+ logging.info(line.rstrip())
apt_get.wait()
if apt_get.returncode:
- syslog.syslog(syslog.LOG_WARNING,
- "apt-get exited with returncode %i" % apt_get.returncode)
+ logging.warn("apt-get exited with returncode %i" % apt_get.returncode)
return apt_get.returncode
-def _notify(title, body):
- """Display a notification to the user of the live system."""
- cmd = "/usr/local/sbin/tails-notify-user"
+def _notify(title, body="", accept_label="", deny_label="",
+ documentation_target="", urgent=False, return_id=False):
+ """Display a notification to the user of the live system.
+
+ The notification will show title and body.
+
+ If accept_label or deny_label are set, they will be shown on action buttons
+ and the method will wait for user input and return 1 if the button with
+ accept_label was clicked or 0 if the button with deny_label was
+ clicked.
+
+ If documentation_target is set, a "Documentation" action button will open
+ corresponding tails documentation when clicked.
+
+ If return_id is true, returns the notification ID, which may be used to
+ close the notification.
+
+ Else, return None.
+ """
+
+ cmd = "/usr/local/lib/tails-additional-software-notify"
+ if urgent:
+ urgent = "urgent"
+ else:
+ urgent = ""
+
try:
- subprocess.check_call([cmd, title, body], stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError as e:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: unable to notify the user. %s returned "
- "with exit code %s" % (cmd, e.returncode))
- syslog.syslog(syslog.LOG_WARNING,
- "The notification was: %s %s" % (title, body))
+ completed_process = subprocess.run(["sudo", "-u", LIVE_USERNAME, cmd,
+ title, body, accept_label,
+ deny_label, documentation_target,
+ urgent],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True)
+ if completed_process.returncode == 1:
+ # sudo failed to execute the command
+ raise OSError(completed_process.stderr)
except OSError as e:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: unable to notify the user. %s" % e)
- syslog.syslog(syslog.LOG_WARNING,
- "The notification was: %s %s" % (title, body))
+ logging.warn("Warning: unable to notify the user. %s" % e)
+ logging.warn("The notification was: %s %s" % (title, body))
+ return None
+
+ if return_id:
+ for line in completed_process.stdout.splitlines():
+ if line.startswith("id="):
+ return line[3:]
+ else:
+ if completed_process.returncode == 0:
+ return 1
+ elif completed_process.returncode == 3:
+ return 0
+ else:
+ return None
-def has_additional_packages_list():
- """Return true iff PACKAGES_LIST_FILE exists."""
- return os.path.isfile(PACKAGES_LIST_FILE)
+def _notify_failure(summary, details=None):
+ """Display a failure notification to the user of the live system.
+ The user has the option to edit the configuration of to view the system
+ log.
+ """
+ if details:
+ details = _("{details} Please check your list of additional "
+ "software or read the system log to "
+ "understand the problem.").format(details=details)
-def get_additional_packages():
- """Return the list of all additional packages configured."""
- packages = []
- if has_additional_packages_list():
- with open(PACKAGES_LIST_FILE) as f:
- for line in f:
- line = line.strip()
- if line:
- packages.append(line)
- return packages
+ else:
+ details = _("Please check your list of additional "
+ "software or read the system log to "
+ "understand the problem.").format(details=details)
+
+ action_clicked = _notify(summary, details, _("Show Log"), _("Configure"),
+ urgent=True)
+ if action_clicked == 1:
+ show_system_log()
+ elif action_clicked == 0:
+ show_configuration_window()
+
+
+def _close_notification(notification_id):
+ """Close a notification shown to the user of the live system."""
+ subprocess.run(
+ ["sudo", "-u", LIVE_USERNAME,
+ "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus".format(
+ uid=pwd.getpwnam(LIVE_USERNAME).pw_uid),
+ "gdbus", "call",
+ "--session",
+ "--dest", "org.freedesktop.Notifications",
+ "--object-path", "/org/freedesktop/Notifications",
+ "--method", "org.freedesktop.Notifications.CloseNotification",
+ str(notification_id)],
+ stdout=subprocess.DEVNULL)
+
+
+def _spawn_daemon(func):
+ """Spawn func after double-forking.
+
+ Do the UNIX double-fork magic, see Stevens' "Advanced
+ Programming in the UNIX Environment" for details (ISBN 0201563177).
+
+ From https://stackoverflow.com/questions/6011235/run-a-program-from-
+ python-and-have-it-continue-to-run-after-the-script-is-kille
+ """
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # parent process, return and keep running
+ return
+ except OSError as e:
+ logging.error("fork #1 failed: %d (%s)" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ os.setsid()
+
+ # do second fork
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # exit from second parent
+ sys.exit(0)
+ except OSError as e:
+ logging.error("fork #2 failed: %d (%s)" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ # do stuff
+ func()
+
+
+def _format_iterable(iterable):
+ """Return a nice formatted string with the elements of iterable."""
+ iterable = sorted(iterable)
+
+ if len(iterable) == 1:
+ return iterable[0]
+ elif len(iterable) > 1:
+ return _("{beginning} and {last}").format(
+ beginning=_(", ").join(iterable[:-1]), last=iterable[-1])
+ else:
+ return str(iterable)
+
+
+def has_additional_packages_list(search_new_persistence=False):
+ """Return true iff a packages list file is found in a persistence.
+
+ Log warnings in syslog.
+ The search_new_persistence argument is passed to get_persistence_path.
+ """
+ try:
+ packages_list_path = get_packages_list_path(search_new_persistence)
+ except FileNotFoundError as e:
+ logging.warn("Warning: {}".format(e))
+ return False
+ if os.path.isfile(packages_list_path):
+ logging.info("Found additional packages list.")
+ return True
+ else:
+ logging.warn("Warning: no configuration file found.")
+ return False
def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
@@ -92,9 +246,8 @@ def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
def save_old_apt_lists(srcdir=APT_LISTS_DIR, destdir=OLD_APT_LISTS_DIR):
"""Save a copy of the APT lists"""
if os.path.exists(destdir):
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: a copy of the APT lists already exists, "
- "which should never happen. Removing it.")
+ logging.warn("Warning: a copy of the APT lists already exists, "
+ "which should never happen. Removing it.")
delete_old_apt_lists(destdir)
shutil.copytree(srcdir, destdir, symlinks=True)
@@ -117,15 +270,227 @@ def restore_old_apt_lists(srcdir=OLD_APT_LISTS_DIR, dstdir=APT_LISTS_DIR):
shutil.move(path, dstdir)
-def install_additional_packages(ignore_old_apt_lists=False):
- """Subcommand which activates and installs all additional packages."""
- syslog.syslog("Starting to install additional software...")
-
- if has_additional_packages_list():
- syslog.syslog("Found additional packages list")
- else:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: no configuration file found, exiting")
+def handle_installed_packages(packages):
+ """Configure packages as additional software packages if the user wants to.
+
+ Ask the user if packages should be added to additional software, and
+ actually add them if requested.
+ """
+ logging.info("New packages manually installed: %s" % packages)
+ if has_unlocked_persistence(search_new_persistence=True):
+ if _notify(_("Add {packages} to your additional software?").format(
+ packages=_format_iterable(packages)),
+ _("To install it automatically from your persistent "
+ "storage when starting Tails."),
+ _("Install Every Time"),
+ _("Install Only Once"),
+ urgent=True):
+ try:
+ setup_additional_packages()
+ add_additional_packages(packages, search_new_persistence=True)
+ except Exception as e:
+ _notify_failure(_("The configuration of your additional "
+ "software failed."))
+ raise e
+ elif has_persistence():
+ # When a package is installed with a persistent storage locked, don't
+ # show any notification.
+ #
+ # People who have a persistent storage but don't unlock it, probably do
+ # this only sometimes and for a reason. They probably otherwise unlock
+ # their persistent storage most of the time.
+ #
+ # If they install packages with their persistent storage locked, they
+ # probably do it with their persistent storage unlock as well and would
+ # learn about this feature when it's most relevant for them.
+ logging.warn("Warning: persistence storage is locked, can't add "
+ "additional software.")
+ elif is_tails_media_writable():
+ if _notify(_("Add {packages} to your additional software?").format(
+ packages=_format_iterable(packages)),
+ _("To install it automatically when starting Tails, you "
+ "can create a persistent storage and activate the "
+ "<b>Additional Software</b> feature."),
+ _("Create Persistent Storage"),
+ _("Install Only Once"),
+ urgent=True):
+ try:
+ create_persistence_and_setup_additional_packages(packages)
+ except Exception as e:
+ _notify_failure(_("The configuration of your additional "
+ "software failed."),
+ _("Creating your persistent storage "
+ "failed."))
+ raise e
+ else: # It's impossible to have a persistent storage
+ logging.warn("Cannot create persistent storage on this media.")
+ if not os.path.isfile(ASP_STATE_INSTALLER_ASKED):
+ open(ASP_STATE_INSTALLER_ASKED, 'a').close()
+ _notify(_("You could install {packages} automatically when "
+ "starting Tails").format(
+ packages=_format_iterable(packages)),
+ _("To do so, you need to run Tails from a USB stick "
+ "installed using <i>Tails Installer</i>."),
+ documentation_target="install/clone",
+ urgent=True)
+
+
+def handle_removed_packages(packages):
+ """Removes packages from additional software packages if the user wants to.
+
+ Ask the user if packages should be removed from additional software, and
+ actually remove them if requested.
+ """
+ logging.info("Additional packages removed: %s" % packages)
+ if _notify(_("Remove {packages} from your additional software?").format(
+ packages=_format_iterable(packages)),
+ _("This will stop installing {packages} automatically.").format(
+ packages=_format_iterable(packages)),
+ _("Remove"),
+ _("Cancel"),
+ urgent=True):
+ try:
+ remove_additional_packages(packages, search_new_persistence=True)
+ except Exception as e:
+ _notify_failure(_("The configuration of your additional "
+ "software failed."))
+ raise e
+
+
+def setup_additional_packages():
+ """Enable additional software in persistence."""
+ launch_persistence_setup("--no-gui",
+ "--no-display-finished-message",
+ "--force-enable-preset", "AdditionalSoftware")
+
+
+def create_persistence_and_setup_additional_packages(packages):
+ """Create persistence and add packages to its configuration.
+
+ Create a new persistence with additional packages enabled.
+ Then add the packages to additional packages configuration.
+
+ packages should be a list of packages names.
+ """
+ logging.info("Creating new persistent volume")
+ launch_persistence_setup("--step", "bootstrap",
+ "--no-display-finished-message",
+ "--force-enable-preset", "AdditionalSoftware")
+ add_additional_packages(packages, search_new_persistence=True)
+ # show persistence configuration
+ launch_persistence_setup()
+ # APT lists and APT archive cache will be synchronized at shutdown by
+ # tails-synchronize-data-to-new-persistent-volume-on-shutdown.service
+
+
+def show_configuration_window():
+ """Show additional packages configuration window."""
+ launch_x_application(LIVE_USERNAME,
+ "/usr/local/bin/tails-additional-software-config")
+
+
+def show_system_log():
+ """Show additional packages configuration window."""
+ launch_x_application(LIVE_USERNAME,
+ "/usr/bin/gedit",
+ ASP_LOG_FILE)
+
+
+def apt_hook_pre():
+ """Subcommand to handle Dpkg::Pre-Install-Pkgs."""
+ _exit_if_in_live_build()
+ logging.info("Saving package changes")
+
+ apt_cache = apt.cache.Cache()
+
+ installed_packages = []
+ removed_packages = []
+
+ line = sys.stdin.readline()
+ assert line.startswith("VERSION 3")
+ line = sys.stdin.readline()
+ # Ignore configuration space, which ends with an empty line
+ while line != "\n":
+ line = sys.stdin.readline()
+ # Package action lines
+ for line in sys.stdin:
+ # Package action lines consist of five fields in Version 2: package
+ # name (without architecture qualification even if foreign), old
+ # version, direction of version change (< for upgrades, > for
+ # downgrades, = for no change), new version, action. The version
+ # fields are "-" for no version at all (for example when installing
+ # a package for the first time; no version is treated as earlier
+ # than any real version, so that is an upgrade, indicated as - <
+ # 1.23.4). The action field is "**CONFIGURE**" if the package is
+ # being configured, "**REMOVE**" if it is being removed, or the
+ # filename of a .deb file if it is being unpacked.
+ #
+ # In Version 3 after each version field follows the architecture of
+ # this version, which is "-" if there is no version, and a field
+ # showing the MultiArch type "same", "foreign", "allowed" or "none".
+ # Note that "none" is an incorrect typename which is just kept to
+ # remain compatible, it should be read as "no" and users are
+ # encouraged to support both.
+ #
+ # Example:
+ #
+ # colordif - - none < 1.0.16-1 all none **CONFIGURE**
+ package_name, old_version, old_arch, old_multiarch, direction, \
+ new_version, new_arch, new_multiarch, action = line.split()
+ if action.endswith(".deb"):
+ # Filter packages that will only be upgraded
+ if not apt_cache[package_name].is_installed:
+ installed_packages.append(package_name)
+ elif action.endswith("**REMOVE**"):
+ removed_packages.append(package_name)
+
+ result = {"installed": installed_packages, "removed": removed_packages}
+ with open(ASP_STATE_PACKAGES, 'w') as f:
+ json.dump(result, f)
+
+
+def apt_hook_post():
+ """Subcommand to handle Dpkg::Post-Invoke.
+
+ Retrieve the list of packages saved by apt_hook_pre, filter packages not
+ interesting and pass the resulting list to the appropriate method.
+ """
+ _exit_if_in_live_build()
+ logging.info("Examining package changes")
+
+ with open(ASP_STATE_PACKAGES) as f:
+ packages = json.load(f)
+ os.remove(ASP_STATE_PACKAGES)
+
+ additional_packages_names = map(
+ filter_package_details,
+ get_additional_packages(search_new_persistence=True))
+
+ apt_cache = apt.cache.Cache()
+ # Filter automatically installed packages and packages already configured
+ # as additional software
+ new_manually_installed_packages = set(filter(
+ lambda pkg: not apt_cache[pkg].is_auto_installed
+ and pkg not in additional_packages_names, # NOQA: E131
+ set(packages["installed"])))
+ if new_manually_installed_packages:
+ handle_installed_packages(new_manually_installed_packages)
+
+ # Filter non-additional software packages
+ additional_packages_removed = set(packages["removed"]).intersection(
+ additional_packages_names)
+ if additional_packages_removed:
+ handle_removed_packages(additional_packages_removed)
+
+
+def install_additional_packages(upgrade_mode=False):
+ """Subcommand which activates and installs all additional packages.
+
+ If upgrade_mode is True, don't attempt to restore old apt lists and don't
+ notify the user using desktop notifications."""
+ logging.info("Starting to install additional software...")
+
+ if not has_additional_packages_list():
return True
# If a copy of old APT lists is found, then the previous upgrade
@@ -136,15 +501,13 @@ def install_additional_packages(ignore_old_apt_lists=False):
# installation step below in this function will fail. To avoid
# that, we restore the old APT lists: there are greater chances
# that the APT packages cache still has the corresponding packages.
- if os.path.isdir(OLD_APT_LISTS_DIR) and not ignore_old_apt_lists:
- syslog.syslog(syslog.LOG_WARNING,
- "Found a copy of old APT lists, restoring it.")
+ if os.path.isdir(OLD_APT_LISTS_DIR) and not upgrade_mode:
+ logging.warn("Found a copy of old APT lists, restoring it.")
try:
restore_old_apt_lists()
except Exception as e:
- syslog.syslog(syslog.LOG_WARNING,
- "Restoring old APT lists failed with %r, "
- "deleting them and proceeding anyway." % e)
+ logging.warn("Restoring old APT lists failed with %r, "
+ "deleting them and proceeding anyway." % e)
# In all cases, delete the old APT lists: if they could be
# restored we don't need them anymore (and we don't want to
# restore them again next time); if they could not be
@@ -154,58 +517,72 @@ def install_additional_packages(ignore_old_apt_lists=False):
packages = get_additional_packages()
if not packages:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: no packages to install, exiting")
+ logging.warn("Warning: no packages to install, exiting")
return True
- syslog.syslog("Will install the following packages: %s"
- % " ".join(packages))
+ if not upgrade_mode:
+ installing_notification_id = _notify(
+ _("Installing your additional software from persistent "
+ "storage..."),
+ _("This can take several minutes."),
+ return_id=True)
+ logging.info("Will install the following packages: %s"
+ % " ".join(packages))
apt_get_returncode = _launch_apt_get(
["--no-remove",
"--option", "DPkg::Options::=--force-confold",
- "install"] + packages)
+ "install"] + list(packages))
if apt_get_returncode:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: installation of %s failed"
- % " ".join(packages))
- _notify(_("Your additional software installation failed"),
- _("The installation failed. Please check your additional "
- "software configuration, or read the system log to "
- "understand better the problem."))
+ logging.warn("Warning: installation of %s failed" % " ".join(packages))
+ if not upgrade_mode:
+ _close_notification(installing_notification_id)
+ _notify_failure(_("The installation of your additional software "
+ "failed"))
return False
else:
- syslog.syslog("Installation completed successfully.")
- _notify(_("Your additional software are installed"),
- _("Your additional software are ready to use."))
+ logging.info("Installation completed successfully.")
+ if not upgrade_mode:
+ _close_notification(installing_notification_id)
+ # XXX: there should be a "Configure" button in this notification.
+ # However, the easy way to implement it makes this process not
+ # return until the notification is clicked. The notification
+ # process could be detached, and handle the "configure" action
+ # itself.
+ # if _notify(_("Additional software installed successfully"),
+ # accept_label=_("Configure")):
+ # show_configuration_window()
+ _notify(_("Additional software installed successfully"))
return True
def upgrade_additional_packages():
"""Subcommand which upgrades all additional packages."""
+ logging.info("Starting to upgrade additional software...")
+
+ if not has_additional_packages_list():
+ return True
+
# Save a copy of APT lists that we'll delete only once the upgrade
# has succeeded, to ensure that the APT packages cache is up-to-date
# wrt. the APT lists.
- syslog.syslog("Saving old APT lists...")
+ logging.info("Saving old APT lists...")
save_old_apt_lists()
- syslog.syslog("Starting to upgrade additional software...")
apt_get_returncode = _launch_apt_get(["update"])
if apt_get_returncode:
- syslog.syslog(syslog.LOG_WARNING, "Warning: the update failed.")
- _notify(_("Your additional software upgrade failed"),
- _("The check for upgrades failed. This might be due to a "
- "network problem. Please check your network connection, try "
- "to restart Tails, or read the system log to understand "
- "better the problem."))
+ logging.warn("Warning: the update failed.")
+ _notify_failure(_("The check for upgrades of your additional software "
+ "failed"),
+ _("Please check your network connection, "
+ "restart Tails, or read the system log to "
+ "understand the problem."))
return False
- if install_additional_packages(ignore_old_apt_lists=True):
- _notify(_("Your additional software are up to date"),
- _("The upgrade was successful."))
+ if install_additional_packages(upgrade_mode=True):
+ logging.info("The upgrade was successful.")
else:
- _notify(_("Your additional software upgrade failed"),
- _("The upgrade failed. This might be due to a network "
- "problem. Please check your network connection, try to "
- "restart Tails, or read the system log to understand better "
- "the problem."))
+ _notify_failure(_("The upgrade of your additional software failed"),
+ _("Please check your network connection, "
+ "restart Tails, or read the system log to "
+ "understand the problem."))
return False
# We now know that the APT packages cache is up-to-date wrt. the APT lists,
@@ -222,8 +599,7 @@ def upgrade_additional_packages():
# must have been upgraded already.
apt_get_returncode = _launch_apt_get(["autoclean"])
if apt_get_returncode:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: autoclean failed.")
+ logging.warn("Warning: autoclean failed.")
return True
@@ -238,7 +614,26 @@ def print_help():
if __name__ == "__main__":
program_name = os.path.basename(sys.argv[0])
- syslog.openlog("%s[%i]" % (program_name, os.getpid()))
+ # Exits with success if running inside live-build.
+ if "SOURCE_DATE_EPOCH" in os.environ:
+ sys.exit(0)
+
+ # Set loglevel if debug is found in kernel command line.
+ with open('/proc/cmdline') as cmdline_fd:
+ cmdline = cmdline_fd.read()
+ if "DEBUG" in os.environ or "debug" in cmdline.split():
+ log_level = logging.DEBUG
+ log_format = "[%(levelname)s] %(filename)s:%(lineno)d " \
+ "%(funcName)s: %(message)s"
+ else:
+ log_level = logging.INFO
+ log_format = "[%(levelname)s] %(message)s"
+ syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
+ file_handler = logging.FileHandler(ASP_LOG_FILE)
+ logging.basicConfig(format=log_format,
+ handlers=[syslog_handler, file_handler],
+ level=log_level)
+
gettext.install("tails")
if len(sys.argv) < 2:
@@ -251,6 +646,10 @@ if __name__ == "__main__":
elif sys.argv[1] == "upgrade":
if not upgrade_additional_packages():
sys.exit(151)
+ elif sys.argv[1] == "apt-pre":
+ apt_hook_pre()
+ elif sys.argv[1] == "apt-post":
+ _spawn_daemon(apt_hook_post)
else:
print_help()
sys.exit(2)
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-additional-software-remove b/config/chroot_local-includes/usr/local/sbin/tails-additional-software-remove
new file mode 100755
index 0000000..a97bf1b
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/tails-additional-software-remove
@@ -0,0 +1,18 @@
+#!/usr/bin/python3
+
+import sys
+
+from tailslib.additionalsoftware.config import (
+ remove_additional_packages,
+ get_additional_packages)
+
+if len(sys.argv) != 2:
+ sys.exit(2)
+
+old_package = str(sys.argv[1])
+
+additional_packages = get_additional_packages(search_new_persistence=True)
+if old_package in additional_packages:
+ remove_additional_packages({old_package}, search_new_persistence=True)
+else:
+ sys.exit(1)
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-debugging-info b/config/chroot_local-includes/usr/local/sbin/tails-debugging-info
index 883c88a..a0babde 100755
--- a/config/chroot_local-includes/usr/local/sbin/tails-debugging-info
+++ b/config/chroot_local-includes/usr/local/sbin/tails-debugging-info
@@ -37,10 +37,11 @@ investigated carefully.
...
"""
+import json
import os
import sys
-from pwd import getpwuid
import subprocess
+from pwd import getpwuid
# AppArmor Ux rules don't sanitize PATH, which can lead to an
@@ -52,121 +53,100 @@ os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
def main():
- """Print debug information.
+ """Print debug information serialized as json.
>>> main()
<BLANKLINE>
...
"""
- debug_file('root', '/proc/cmdline')
-
- # General hardware and filesystems information
- debug_command('/usr/sbin/dmidecode', '-s', 'system-manufacturer')
- debug_command('/usr/sbin/dmidecode', '-s', 'system-product-name')
- debug_command('/usr/sbin/dmidecode', '-s', 'system-version')
- debug_command('/usr/bin/lspci', '-nn')
- debug_command('/bin/df', '--human-readable', '--print-type')
- debug_command('/bin/mount', '--show-labels')
- debug_command('/bin/lsmod')
- debug_file('root', '/proc/asound/cards')
- debug_file('root', '/proc/asound/devices')
- debug_file('root', '/proc/asound/modules')
-
- # Miscellaneous configuration and log files
- debug_file('root', '/etc/X11/xorg.conf')
- debug_file('Debian-gdm', '/var/log/gdm3/tails-greeter.errors')
- debug_file('root', '/var/log/live/boot.log')
- debug_file('root', '/var/log/live/config.log')
- debug_file('root', '/var/lib/live/config/tails.physical_security')
-
- # Persistence
- debug_file('root', '/var/lib/gdm3/tails.persistence')
- debug_file('tails-persistence-setup', '/live/persistence/TailsData_unlocked/persistence.conf')
- debug_file('tails-persistence-setup', '/live/persistence/TailsData_unlocked/live-additional-software.conf')
- debug_directory('root', '/live/persistence/TailsData_unlocked/apt-sources.list.d')
- debug_file('root', '/var/log/live-persist')
-
- # The Journal
- debug_command('/bin/journalctl', '--catalog', '--no-pager')
+ config = None
+ with open('/etc/whisperback/debugging-info.json', 'r') as conf_file:
+ config = json.load(conf_file)
+
+ info = []
+ for _type, _args in config:
+ if _type == 'command':
+ info.append(debug_command(_args['args'][0], *_args['args'][1:]))
+ elif _type == 'directory':
+ info.append(debug_directory(_args['user'], _args['path']))
+ else:
+ info.append(debug_file(_args['user'], _args['path']))
+ print()
+ print(json.dumps(info, indent=4))
def debug_command(command, *args):
- """Print the command and then run it.
+ """Return the command and it's standard output as dict.
>>> debug_command('echo', 'foo')
- <BLANKLINE>
- ===== output of command echo foo =====
- foo
+ {...'key': 'echo foo'...}
"""
- print()
- print('===== output of command {} ====='.format(' '.join((command,) + args)))
- print(subprocess.check_output([command, *args]).decode().strip())
+ command_output = subprocess.check_output([command, *args])
+ command_output = command_output.decode('UTF-8').strip().split('\n')
+ return {'key': '{}'.format(' '.join((command,) + args)), 'content': command_output}
def debug_file(user, filename):
- """Print file content.
+ """Return the filename and the file content as dict.
>>> import tempfile, getpass
>>> with tempfile.NamedTemporaryFile('w') as f:
... _ = f.write("foo\\nbar")
... _ = f.seek(0)
... debug_file(getpass.getuser(), f.name)
- <BLANKLINE>
- ===== content of ... =====
- foo
- bar
+ {...'content': ['foo', 'bar']...}
"""
if not os.path.isfile(filename):
- return
+ return {'key': filename, 'content': 'Not found'}
# This check is not sufficient, see the comment at the top of the file
# for the complete requirements required for security
owner = getpwuid(os.stat(filename).st_uid).pw_name
if owner != user:
- print()
- print('WARNING: not opening file {}, '.format(filename), end='')
- print('because it is owned by {} instead of {}'.format(owner, user))
- return
+ return {'key': filename, 'content': '''WARNING: not opening file {}, because it is '''
+ '''owned by {} instead of {}'''.format(filename, owner, user)}
- print()
- print('===== content of {} ====='.format(filename))
+ file_content = []
with open(filename) as f:
- print(f.read(), end='')
+ for l in f:
+ file_content.append(l.replace('\n', ''))
+ return {'key': filename, 'content': file_content}
def debug_directory(user, dir_name):
- """List directory and print content of all contained files (non-recursively).
-
- >>> import tempfile, getpass
- >>> with tempfile.TemporaryDirectory() as tmpdir:
- ... open(os.path.join(tmpdir, 'foo'), 'w').close()
- ... debug_directory(getpass.getuser(), tmpdir)
- <BLANKLINE>
- ===== listing of ... =====
- foo
+ """Return a dict with the dir_name and dicts with
+ the content of all contained files (non-recursively).
+
+ >>> import os, getpass
+ >>> tmpdir = '/tmp/mytempdir'
+ >>> os.makedirs(tmpdir)
+ >>> with open(os.path.join(tmpdir, 'foo'), 'w') as f:
+ ... _ = f.write("foobar\\nbar")
+ ... _ = f.seek(0)
+ ... result = debug_directory(getpass.getuser(), tmpdir)
+ >>> os.remove(os.path.join(tmpdir, 'foo'))
+ >>> os.rmdir(tmpdir)
+ >>> result
+ {...[{...['foobar', 'bar']...}]}
"""
if not os.path.isdir(dir_name):
- return
-
- print()
+ return {'key': dir_name, 'content': 'Not found'}
# This check is not sufficient, see the comment at the top of the file
# for the complete requirements required for security
owner = getpwuid(os.stat(dir_name).st_uid).pw_name
if owner != user:
- print('WARNING: not opening directory {}, '.format(dir_name), end='')
- print('because it is owned by {} instead of {}'.format(owner, user))
- return
+ return {'key': dir_name, 'content': '''WARNING: not opening directory {}, because '''
+ '''it is owned by {} instead of {}'''.format(dir_name, owner, user)}
files = os.listdir(dir_name)
- print('===== listing of {} ====='.format(dir_name))
- for f in files:
- print(f)
+ listing = []
for f in files:
- debug_file(user, f)
+ listing.append(debug_file(user, os.path.join(dir_name, f)))
+ return {'key': dir_name, 'content': listing}
if __name__ == '__main__':
diff --git a/config/chroot_local-includes/usr/local/sbin/unsafe-browser b/config/chroot_local-includes/usr/local/sbin/unsafe-browser
index 0ca9eb6..24eb30e 100755
--- a/config/chroot_local-includes/usr/local/sbin/unsafe-browser
+++ b/config/chroot_local-includes/usr/local/sbin/unsafe-browser
@@ -82,7 +82,6 @@ CHROOT="${CONF_DIR}/chroot"
BROWSER_NAME="unsafe-browser"
BROWSER_USER="clearnet"
HUMAN_READABLE_NAME="`gettext \"Unsafe Browser\"`"
-NM_ENV_FILE="/var/lib/NetworkManager/env"
WARNING_PAGE='/usr/share/doc/tails/website/misc/unsafe_browser_warning'
HOME_PAGE="$(localized_tails_doc_page "${WARNING_PAGE}")"
@@ -112,8 +111,13 @@ else
fi
echo "* Starting Unsafe Browser"
+# Do not localize the 5th argument: it becomes WM_CLASS and then GNOME
+# displays the localized app name found in the matching .desktop file;
+# if WM_CLASS were localized then not only string encoding problems
+# would happen, but GNOME would pick the wrong icon.
run_browser_in_chroot "${CHROOT}" "${BROWSER_NAME}" "${BROWSER_USER}" \
- "${SUDO_USER}" || \
+ "${SUDO_USER}" \
+ 'Unsafe Browser' || \
error "`gettext \"Failed to run browser.\"`"
echo "* Exiting the Unsafe Browser"
diff --git a/config/chroot_local-includes/usr/local/share/mime/packages/unlock-veracrypt-volumes.xml.in b/config/chroot_local-includes/usr/local/share/mime/packages/unlock-veracrypt-volumes.xml.in
new file mode 100644
index 0000000..58f2615
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/share/mime/packages/unlock-veracrypt-volumes.xml.in
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
+ <mime-type type="application/x-tcrypt-container">
+ <_comment>TrueCrypt/VeraCrypt container</_comment>
+ <glob pattern="*.tc"/>
+ <glob pattern="*.hc"/>
+ <icon name="unlock-veracrypt-volumes"/>
+ </mime-type>
+</mime-info>