+
+## Required: what port to advertise for Tor connections.
+#ORPort 9001
+## If you need to listen on a port other than the one advertised
+## in ORPort (e.g. to advertise 443 but bind to 9090), uncomment the
+## line below too. You'll need to do ipchains or other port forwarding
+## yourself to make this work.
+#ORListenAddress 0.0.0.0:9090
+
+## Uncomment this to mirror directory information for others. Please do
+## if you have enough bandwidth.
+#DirPort 9030 # what port to advertise for directory connections
+## If you need to listen on a port other than the one advertised
+## in DirPort (e.g. to advertise 80 but bind to 9091), uncomment the line
+## below too. You'll need to do ipchains or other port forwarding yourself
+## to make this work.
+#DirListenAddress 0.0.0.0:9091
+
+## Uncomment this if you run more than one Tor server, and add the
+## nickname of each Tor server you control, even if they're on different
+## networks. You declare it here so Tor clients can avoid using more than
+## one of your servers in a single circuit. See
+## http://wiki.noreply.org/noreply/TheOnionRouter/TorFAQ#MultipleServers
+#MyFamily nickname1,nickname2,...
+
+## A comma-separated list of exit policies. They're considered first
+## to last, and the first match wins. If you want to _replace_
+## the default exit policy, end this with either a reject *:* or an
+## accept *:*. Otherwise, you're _augmenting_ (prepending to) the
+## default exit policy. Leave commented to just use the default, which is
+## available in the man page or at https://www.torproject.org/documentation.html
+##
+## Look at https://www.torproject.org/faq-abuse.html#TypicalAbuses
+## for issues you might encounter if you use the default exit policy.
+##
+## If certain IPs and ports are blocked externally, e.g. by your firewall,
+## you should update your exit policy to reflect this -- otherwise Tor
+## users will be told that those destinations are down.
+##
+#ExitPolicy accept *:6660-6667,reject *:* # allow irc ports but no more
+#ExitPolicy accept *:119 # accept nntp as well as default exit policy
+#ExitPolicy reject *:* # no exits allowed
+#
+################ This section is just for bridge relays ##############
+#
+## Bridge relays (or "bridges" ) are Tor relays that aren't listed in the
+## main directory. Since there is no complete public list of them, even if an
+## ISP is filtering connections to all the known Tor relays, they probably
+## won't be able to block all the bridges. Unlike running an exit relay,
+## running a bridge relay just passes data to and from the Tor network --
+## so it shouldn't expose the operator to abuse complaints.
+
+#ORPort 443
+#BridgeRelay 1
+#RelayBandwidthRate 50KBytes
+#ExitPolicy reject *:*
+
+
+################ Local settings ########################################
+
+## Torified DNS
+DNSPort 5353
+AutomapHostsOnResolve 1
+AutomapHostsSuffixes .exit,.onion
+
+## Transparent proxy
+TransPort 9040
+TransListenAddress 127.0.0.1
+
+## Misc
+AvoidDiskWrites 1
+
+## We don't care if applications do their own DNS lookups since our Tor
+## enforcement will handle it safely.
+WarnUnsafeSocks 0
+
+## Disable default warnings on StartTLS for email. Let's not train our
+## users to click through security warnings.
+WarnPlaintextPorts 23,109
+
+## Tor 0.3.x logs to syslog by default, which we redirect to the Journal;
+## but we have some code that reads Tor's logs and only supports plaintext
+## log files at the moment, so let's keep logging to a file.
+Log notice file /var/log/tor/log
diff --git a/config/chroot_local-includes/etc/ttdnsd.conf b/config/chroot_local-includes/etc/ttdnsd.conf
new file mode 100644
index 0000000000000000000000000000000000000000..8c78190700d56579dba3be6b5040367bc196dfa8
--- /dev/null
+++ b/config/chroot_local-includes/etc/ttdnsd.conf
@@ -0,0 +1,2 @@
+# OpenDNS
+208.67.222.222
diff --git a/config/chroot_local-includes/etc/udev/rules.d/00-mac-spoof.rules b/config/chroot_local-includes/etc/udev/rules.d/00-mac-spoof.rules
new file mode 100644
index 0000000000000000000000000000000000000000..f1c6666f5a9f0b96454051473884e177d25e2dbd
--- /dev/null
+++ b/config/chroot_local-includes/etc/udev/rules.d/00-mac-spoof.rules
@@ -0,0 +1,3 @@
+# Note: ATTR{type}=="1" means ethernet (ARPHDR_ETHER, see Linux'
+# sources, beginning of include/linux/if_arp.h)
+SUBSYSTEM=="net", ACTION=="add", ATTR{type}=="1", RUN+="/usr/local/lib/tails-spoof-mac $name"
diff --git a/config/chroot_local-includes/etc/udev/rules.d/70-protect-boot-medium-for-udisks.rules b/config/chroot_local-includes/etc/udev/rules.d/70-protect-boot-medium-for-udisks.rules
new file mode 100644
index 0000000000000000000000000000000000000000..b4cdfbd36894e756a1bd5fc64464a608642fc041
--- /dev/null
+++ b/config/chroot_local-includes/etc/udev/rules.d/70-protect-boot-medium-for-udisks.rules
@@ -0,0 +1,18 @@
+SUBSYSTEM!="block", GOTO="bilibop_end"
+ACTION!="add|change", GOTO="bilibop_end"
+KERNEL!="sd?*|mmcblk?*|mspblk?*", GOTO="bilibop_end"
+
+SUBSYSTEMS=="usb|firewire|memstick|mmc", \
+ PROGRAM=="/lib/bilibop/test $tempnode", \
+ ENV{UDISKS_SYSTEM}:="1", \
+ GROUP:="disk", \
+ GOTO="bilibop_disk"
+
+GOTO="bilibop_end"
+LABEL="bilibop_disk"
+
+KERNEL=="sd?|mmcblk?|mspblk?", \
+ ENV{ID_DRIVE_DETACHABLE}:="0", \
+ SYMLINK+="TailsBootDev"
+
+LABEL="bilibop_end"
diff --git a/config/chroot_local-includes/etc/udev/rules.d/80-net-setup-link.rules b/config/chroot_local-includes/etc/udev/rules.d/80-net-setup-link.rules
new file mode 120000
index 0000000000000000000000000000000000000000..dc1dc0cde0f7dff7b7f7c9347fff75936d705cb8
--- /dev/null
+++ b/config/chroot_local-includes/etc/udev/rules.d/80-net-setup-link.rules
@@ -0,0 +1 @@
+/dev/null
\ No newline at end of file
diff --git a/config/chroot_local-includes/etc/udev/rules.d/99-hide-TailsData.rules b/config/chroot_local-includes/etc/udev/rules.d/99-hide-TailsData.rules
new file mode 100644
index 0000000000000000000000000000000000000000..67493f5b1dda7f5d80a92951bfa31472f3e39946
--- /dev/null
+++ b/config/chroot_local-includes/etc/udev/rules.d/99-hide-TailsData.rules
@@ -0,0 +1 @@
+ENV{ID_FS_LABEL}=="TailsData", ENV{UDISKS_IGNORE}="1"
diff --git a/config/chroot_local-includes/etc/udev/rules.d/99-make-removable-devices-user-writable.rules b/config/chroot_local-includes/etc/udev/rules.d/99-make-removable-devices-user-writable.rules
new file mode 100644
index 0000000000000000000000000000000000000000..482f550d02cad24d281b8f5bc49ca7e405b5b029
--- /dev/null
+++ b/config/chroot_local-includes/etc/udev/rules.d/99-make-removable-devices-user-writable.rules
@@ -0,0 +1,3 @@
+# This is essentially borrowed from /lib/udev/rules.d/91-permissions.rules
+# in order to workaround #8273.
+SUBSYSTEM=="block", ACTION=="add", SUBSYSTEMS=="usb|mmc", GROUP="floppy"
diff --git a/config/chroot_local-includes/etc/udisks2/tcrypt.conf b/config/chroot_local-includes/etc/udisks2/tcrypt.conf
new file mode 100644
index 0000000000000000000000000000000000000000..a350fdcd893b6e26e6c31915888d05133bec3696
--- /dev/null
+++ b/config/chroot_local-includes/etc/udisks2/tcrypt.conf
@@ -0,0 +1,2 @@
+# This flag file needs to exist in order to activate VeraCrypt detection
+# heuristics in udisks. Its content does not matter.
diff --git a/config/chroot_local-includes/etc/vim/vimrc.local b/config/chroot_local-includes/etc/vim/vimrc.local
new file mode 100644
index 0000000000000000000000000000000000000000..9aa99b46d4b7f288903d450d20bc3d7d10ed361a
--- /dev/null
+++ b/config/chroot_local-includes/etc/vim/vimrc.local
@@ -0,0 +1,8 @@
+syntax on
+set showcmd " Show (partial) command in status line.
+set showmatch " Show matching brackets.
+set ignorecase " Do case insensitive matching
+set smartcase " Do smart case matching
+set incsearch " Incremental search
+set autowrite " Automatically save before commands like :next and :make
+set hidden " Hide buffers when they are abandoned
diff --git a/config/chroot_local-includes/etc/whisperback/4mvq3pnvid3awjln.onion.pem b/config/chroot_local-includes/etc/whisperback/4mvq3pnvid3awjln.onion.pem
new file mode 100644
index 0000000000000000000000000000000000000000..7e7bb65a9ac7c22df7a5feeffbb427a18697d9a3
--- /dev/null
+++ b/config/chroot_local-includes/etc/whisperback/4mvq3pnvid3awjln.onion.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEvjCCAqYCCQC0kD+1GKCKDzANBgkqhkiG9w0BAQUFADAhMR8wHQYDVQQDExY0
+bXZxM3BudmlkM2F3amxuLm9uaW9uMB4XDTA5MDkxMjE3MjUzM1oXDTE5MDkxMDE3
+MjUzM1owITEfMB0GA1UEAxMWNG12cTNwbnZpZDNhd2psbi5vbmlvbjCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAOsO/hK2WNClY4lq82nTrJ8CGCfUYUzL
+TiBz7P0XIAcGB34DkFO96aCyFz4TsjpPalIhna8BabuIPRZo6Gt0gpE6lpqLbYiG
+qyvXK63ENlL92LzennKdeYSBlAUi9Bt38T3jDZ8dAWxFhkjKKPrtqjpN9d6gA7aU
+IDWzczXUl0rmE5UC5bh8NmcaHDogiNR5r/Wj24FqncS6Ilfjhn1mXacINX9mqzOV
+IX/WHWQ6NGFWd+uV3D0U/7TkVDQDJ+Ni/RZGXgcVJ5JoSgJAPAefUTKuC9X7FGVt
+i1iu/Cy0DYLVjTHO30oazKTcJuMdOVZ70fRnDyXI8m4hHqyUB16lyw4lHN10OgY/
+haOm6C6VW1OoBAcgxvOdNo/h/jkhSKzgC5iWbtDPB2+qrBvgDVqgYPFDVaBCXewd
+qnd02ysMe0axu4nSjRSODG3CRJjuQ0Zhaz4cPsHeOBeLOlDTFuHN/YyPDaWOD7xH
+FyDE0hn7kMQWh955Hh5CVZZrx6GI2AbppCgLiWFo2M9e16YAssiZ86yOg2j3PYhW
+RDcNuInfj1U/YHv/DnunS0o2eNKxmNLbcaQMu8+4AdGPBhUX7dNtR1iK3o3OB25Z
+6ZU2FloEYJGaTiRV3ZMPuABlyxOJxLCSuSfaUk5Xl0+K51sW8FoQU4XhKyej561y
+/fSgAJST37/RAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBACf9gKIdXBnNuLiaBmt2
+t78Ton2geH8IT5eT6sr7NaWRRfaHkKgTNzqE4/KItNM5LDMCN96JulIns9mSnfyE
+gYB4KYgfVtHdm5RJ6vlnO1q/UveYXvwTvBcOQBr62ztG41L4xGPZZb7NVOihXr1f
+2iGyc/mEy9QiB3b26Tx5+3N4lyJWEadNyqDNbP4RhHSdDMcDhbTPu9tzbhjMVehL
+FLEhHGCIxKoFqyc/OWZRrvrKZtjhvRxkCiQ3sCc7qdVMnK8qaa6JUyrPrZomeF2p
+csOTEyVc24iWz9XuQgZjmx9b5BKreUPoZuQ8mQqU7CdVasIKREe/+pk8VyBUgAvV
++vdxJ77WI7XUarILH0ZuSbSgl5iV4Krkqk4o65GoVW9VCjw9aiEeb6X5ijZxe1/j
+h2PWFb1603eHDuuCrZYywzl3DLjZ6xzQF5kQGcpR+ORs/wcaxxcDveiBk7oEfRkr
+pxGo8batLy5NtCbp5eB2Nvd0oWA1ALsdIwNCSZyuYOUO5lr4cjl98KF9KBhHGbhN
+AHn4jKgDHsJwfMRBUaiYtcPQLfhsyM5C4t2xXBJ2ycZuoOCEDxx0yONatRAnbHAX
+UdPjN/ijn12c6uvrjAOVK54o0uZ+h1DiGvwU5i31yCHFQX11TZY4FHohuCKl4oTj
+39ZLEAZs4/tAyWm1eAZK4GrF
+-----END CERTIFICATE-----
diff --git a/config/chroot_local-includes/etc/whisperback/config.py b/config/chroot_local-includes/etc/whisperback/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..32ee87db60c222ee32a8ad5a679fbff161f5d410
--- /dev/null
+++ b/config/chroot_local-includes/etc/whisperback/config.py
@@ -0,0 +1,188 @@
+# -*- coding: UTF-8 -*-
+#
+# Tails configuration file for WhisperBack
+# ==========================================
+#
+# This is a Python script that will be read at startup. Any Python
+# syntax is valid.
+
+# IMPORTS
+
+# Custom imports
+import os
+import subprocess
+import random
+import re
+import locale
+import gettext
+
+# DOCUMENTATION
+
+
+def __get_localised_doc_link():
+ """Return the link to the localised documentation
+
+ @returns the link to the localised documentation if available, or fallback
+ to the English version
+ """
+
+ # Try to get the list of supported languages codes supported by the
+ # documentation according to the $TAILS_WIKI_SUPPORTED_LANGUAGES
+ # environnement variable. If unset, fallback to `en`
+ try:
+ wiki_supported_languages = os.environ["TAILS_WIKI_SUPPORTED_LANGUAGES"].split(' ')
+ except KeyError:
+ wiki_supported_languages = ['en']
+
+ # locale.getlocale returns a tuple (language code, encoding)
+ # the language is the two first character of the RFC 1766 "language code"
+ system_language_code = locale.getdefaultlocale()[0]
+ if system_language_code:
+ system_language = system_language_code[0:2]
+ else:
+ system_language = None
+
+ # Get the language code of the localised documentation if available, or
+ # fallback to `en`
+ if system_language in wiki_supported_languages:
+ localised_doc_language = system_language
+ else:
+ localised_doc_language = 'en'
+
+ return ("file:///usr/share/doc/tails/website/doc/first_steps/bug_reporting." +
+ localised_doc_language +
+ ".html")
+
+
+def _(string):
+ try:
+ encoded = gettext.translation("tails", "/usr/share/locale").lgettext(string)
+ string = encoded.decode('utf-8')
+ except IOError:
+ pass
+ finally:
+ return string
+
+
+# The right panel help (HTML string)
+html_help = _(
+"""Help us fix your bug!
+Read our bug reporting instructions.
+Do not include more personal information than
+needed!
+About giving us an email address
+
+Giving us an email address allows us to contact you to clarify the problem. This
+is needed for the vast majority of the reports we receive as most reports
+without any contact information are useless. On the other hand it also provides
+an opportunity for eavesdroppers, like your email or Internet provider, to
+confirm that you are using Tails.
+
+""") % __get_localised_doc_link()
+
+# ENCRYPTION
+#
+# This section defines encryption parameters
+
+# The path to the OpenPGP keyring to use. If None, use OpenPGP default
+# keyring.
+gnupg_keyring = "/usr/share/keyrings/whisperback-keyring.gpg"
+
+# RECIPIENT
+#
+# This section defines the recepient parameters
+
+# The address of the recipient
+to_address = "tails-bugs@boum.org"
+
+# The fingerprint of the recipient's GPG key
+to_fingerprint = "1F56EDD30741048035DAC1C5EC57B56EF0C43132"
+
+# SENDER
+#
+# This section defines the sender parameters
+
+# The address of the sender
+from_address = "devnull@tails.boum.org"
+
+# SMTP
+#
+# This section defines the SMTP server parameters
+#
+# The SMTP server to use to send the mail
+smtp_host = "4mvq3pnvid3awjln.onion"
+# The port to connect to on that SMTP server
+smtp_port = 25
+# The path to a file containing the certificate to trust
+# This can be either a CA certificate used to sign the SMTP server
+# certificate or the certificate of the SMTP server itself
+smtp_tlscafile = "/etc/whisperback/4mvq3pnvid3awjln.onion.pem"
+
+# SOCKS
+#
+# This section defines the SOCKS proxy parameters
+#
+# The SOCKS proxy to use to send the mail
+socks_host = "127.0.0.1"
+# The port to connect to on that SOCKS proxy
+socks_port = 9062
+
+# MESSAGE
+#
+# This section defines the message parameters
+
+# The subject of the email to be sent
+# Please take into account that this will not be encrypted
+mail_subject = "Bug report: %x" % random.randrange(16**32)
+
+
+def mail_prepended_info():
+ """Returns the version of the running Tails system
+ A callback function to get information to prepend to the mail
+ (this information will be encrypted). This is useful to add
+ software version.
+
+ It should not take any parameter, and should return a string to be
+ preprended to the email
+
+ @return The output of tails-version, if any, or an English string
+ explaining the error
+ """
+
+ try:
+ tails_version_process = subprocess.Popen("tails-version",
+ stdout=subprocess.PIPE)
+ tails_version_process.wait()
+ tails_version = tails_version_process.stdout.read().decode('utf-8')
+ except OSError:
+ tails_version = "tails-version command not found"
+ except subprocess.CalledProcessError:
+ tails_version = "tails-version returned an error"
+
+ return "Tails-Version: %s\n" % tails_version
+
+
+def mail_appended_info():
+ """Returns debugging information on the running Tails system
+ A callback function to get information to append to the email
+ (this information will be encrypted). This is useful to add
+ configuration files useful for debugging.
+
+ It should not take any parameter, and should return a string serialized
+ json to be deserialized to append infos to the email
+
+ @return a string containing serialized json with debugging information
+ """
+ debugging_info = ""
+
+ try:
+ process = subprocess.Popen(["sudo", "/usr/local/sbin/tails-debugging-info"],
+ stdout=subprocess.PIPE)
+ for line in process.stdout:
+ debugging_info += re.sub(r'^--\s*', '', line.decode('utf-8'))
+ process.wait()
+ except OSError:
+ debugging_info += "sudo command not found\n"
+ except subprocess.CalledProcessError:
+ debugging_info += "debugging command returned an error\n"
+ return debugging_info
diff --git a/config/chroot_local-includes/etc/whisperback/debugging-info.json b/config/chroot_local-includes/etc/whisperback/debugging-info.json
new file mode 100644
index 0000000000000000000000000000000000000000..44f2d70fb9d05a21f95c01cb50077006187adcf7
--- /dev/null
+++ b/config/chroot_local-includes/etc/whisperback/debugging-info.json
@@ -0,0 +1,26 @@
+[
+["file", {"user": "root", "path": "/proc/cmdline"}],
+["command", {"args": ["/usr/sbin/dmidecode", "-s", "system-manufacturer"]}],
+["command", {"args": ["/usr/sbin/dmidecode", "-s", "system-product-name"]}],
+["command", {"args": ["/usr/sbin/dmidecode", "-s", "system-version"]}],
+["command", {"args": ["/usr/bin/lspci", "-nn"]}],
+["command", {"args": ["/bin/df", "--human-readable", "--print-type"]}],
+["command", {"args": ["/bin/mount", "--show-labels"]}],
+["command", {"args": ["/sbin/dmsetup", "ls", "--tree", "--options=blkdevname,uuid,active,open,rw,notrunc"]}],
+["command", {"args": ["/sbin/losetup", "--list", "--output=NAME,BACK-FILE,AUTOCLEAR,RO,PARTSCAN,SIZELIMIT,OFFSET"]}],
+["command", {"args": ["/bin/lsmod"]}],
+["file", {"user": "root", "path": "/proc/asound/cards"}],
+["file", {"user": "root", "path": "/proc/asound/devices"}],
+["file", {"user": "root", "path": "/proc/asound/modules"}],
+["file", {"user": "root", "path": "/etc/X11/xorg.conf"}],
+["file", {"user": "Debian-gdm", "path": "/var/log/gdm3/tails-greeter.errors"}],
+["file", {"user": "root", "path": "/var/log/live/boot.log"}],
+["file", {"user": "root", "path": "/var/log/live/config.log"}],
+["file", {"user": "root", "path": "/var/lib/live/config/tails.physical_security"}],
+["file", {"user": "root", "path": "/var/lib/gdm3/tails.persistence"}],
+["file", {"user": "tails-persistence-setup", "path": "/live/persistence/TailsData_unlocked/persistence.conf"}],
+["file", {"user": "tails-persistence-setup", "path": "/live/persistence/TailsData_unlocked/live-additional-software.conf"}],
+["directory", {"user": "root", "path": "/live/persistence/TailsData_unlocked/apt-sources.list.d"}],
+["file", {"user": "root", "path": "/var/log/live-persist"}],
+["command", {"args": ["/bin/journalctl", "--catalog", "--no-pager"]}]
+]
diff --git a/config/chroot_local-includes/etc/xdg/autostart/systemd-desktop-target.desktop b/config/chroot_local-includes/etc/xdg/autostart/systemd-desktop-target.desktop
new file mode 100644
index 0000000000000000000000000000000000000000..fd3882340d1d8997eb1cdf22f0af9ff01760f989
--- /dev/null
+++ b/config/chroot_local-includes/etc/xdg/autostart/systemd-desktop-target.desktop
@@ -0,0 +1,8 @@
+[Desktop Entry]
+Name=systemd Desktop target
+GenericName=Start the Desktop target in the systemd user session
+Version=1.0
+Exec=/usr/local/lib/start-systemd-desktop-target
+Terminal=false
+Type=Application
+Categories=
diff --git a/config/chroot_local-includes/etc/xdg/autostart/systemd-gnome-early-initialization-target.desktop b/config/chroot_local-includes/etc/xdg/autostart/systemd-gnome-early-initialization-target.desktop
new file mode 100644
index 0000000000000000000000000000000000000000..3bd42bfe603dd14259d1d18911e8e67ed89743ba
--- /dev/null
+++ b/config/chroot_local-includes/etc/xdg/autostart/systemd-gnome-early-initialization-target.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Name=systemd GNOME EarlyInitialization target
+GenericName=Start the GNOME EarlyInitialization target in the systemd user session
+Version=1.0
+Exec=/bin/systemctl --user start gnome-early-initialization.target
+Terminal=false
+Type=Application
+Categories=
+X-GNOME-Autostart-Phase=EarlyInitialization
diff --git a/config/chroot_local-includes/etc/xul-ext/torbirdy.js b/config/chroot_local-includes/etc/xul-ext/torbirdy.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f5b467ba68bc9dc000baec8c184d2f533ede25c
--- /dev/null
+++ b/config/chroot_local-includes/etc/xul-ext/torbirdy.js
@@ -0,0 +1,6 @@
+// Place your preferences for xul-ext-torbirdy in this file.
+// You can override here the preferences specified in
+// /usr/share/xul-ext/torbirdy/defaults/preferences/prefs.js
+
+pref("extensions.torbirdy.emailwizard", true);
+pref("extensions.torbirdy.gpg_already_torified", true);
diff --git a/config/chroot_local-includes/lib/live/config/0000-boot-profile b/config/chroot_local-includes/lib/live/config/0000-boot-profile
new file mode 100755
index 0000000000000000000000000000000000000000..9daaa41dad251e02d058b5ceac18842ba1ee3391
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/0000-boot-profile
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+Boot_profile ()
+{
+ # Only start profiler when "profile" appears on kernel command line
+ grep -qw "profile" /proc/cmdline || return 0
+
+ echo -n " boot-profile"
+
+ Start_boot_profile
+}
+
+Start_boot_profile ()
+{
+
+ # Schedule stop script
+ mkdir -p /etc/skel/.config/autostart
+ cat <"/etc/skel/.config/autostart/end-profile.desktop"
+[Desktop Entry]
+Version=1.0
+Name=EndProfile
+GenericName=EndProfile
+Exec=/usr/local/lib/end-profile
+Terminal=false
+Type=Application
+EOF
+
+ echo 32768 >/proc/sys/fs/inotify/max_user_watches
+ /usr/local/lib/boot-profile /var/log/boot-profile
+
+ # Creating state file
+ touch /var/lib/live/config/boot-profile
+}
+
+Boot_profile
diff --git a/config/chroot_local-includes/lib/live/config/0001-sane-clock b/config/chroot_local-includes/lib/live/config/0001-sane-clock
new file mode 100755
index 0000000000000000000000000000000000000000..b9d96cbed45493b234664093e587153dfbd0fd9e
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/0001-sane-clock
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+echo "- making sure the system clock is sane"
+
+# If the system clock is before the source date, then we know it's
+# incorrect and set it too the source date. However, to account for
+# potential issues due to timezone differences etc we ignore clocks
+# that are up to 1 day before the source date.
+SOURCE_DATE="$(sed -n -e '1s/^.* - \([0-9]\+\)$/\1/p;q' /etc/amnesia/version)"
+if [ "$(date +%s)" -lt "$(date -d "${SOURCE_DATE} - 1 day" +%s)" ]; then
+ date --set "${SOURCE_DATE}"
+fi
diff --git a/config/chroot_local-includes/lib/live/config/1000-remount-procfs b/config/chroot_local-includes/lib/live/config/1000-remount-procfs
new file mode 100755
index 0000000000000000000000000000000000000000..82a7a73d77e46cf4a403fa28b10bb67d74e9769f
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/1000-remount-procfs
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+Remount_Proc_Fs ()
+{
+ mount -o remount /proc
+}
+
+Remount_Proc_Fs
diff --git a/config/chroot_local-includes/lib/live/config/1500-reconfigure-APT b/config/chroot_local-includes/lib/live/config/1500-reconfigure-APT
new file mode 100755
index 0000000000000000000000000000000000000000..60c68876a18485e0f98d12c1a47768370419bfa8
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/1500-reconfigure-APT
@@ -0,0 +1,115 @@
+#!/bin/sh
+
+echo "- configuring APT sources"
+
+sed -i 's,^\(\#\?\s*deb\(-src\)\?\s\+\)http://,\1tor+http://,' \
+ /etc/apt/sources.list /etc/apt/sources.list.d/*.list
+
+sed -E -i 's,\s+non-free$,,' \
+ /etc/apt/sources.list /etc/apt/sources.list.d/*.list
+
+sed -E -i '/^deb(-src)?\s+\S+\s+\S+-proposed-updates\s+/d' \
+ /etc/apt/sources.list /etc/apt/sources.list.d/*.list
+
+echo '
+
+### Time-based snapshots
+
+s{
+ ^
+ (deb(?:-src)?\s+)
+ tor[+]https?://time-based[.]snapshots[.]deb[.]tails[.]boum[.]org
+ /debian
+ /[0-9]{10} # serial
+ /?
+ (\s+)
+}{$1tor+http://vwakviie2ienjx6t.onion/debian/$2}xms;
+
+s{
+ ^
+ (deb(?:-src)?\s+)
+ tor[+]https?://time-based[.]snapshots[.]deb[.]tails[.]boum[.]org
+ /debian-security
+ /[0-9]{10} # serial
+ /?
+ (\s+)
+}{$1tor+http://sgvtcaew4bxjd7ln.onion/$2}xms;
+
+s{
+ ^
+ (deb(?:-src)?\s+)
+ tor[+]https?://time-based[.]snapshots[.]deb[.]tails[.]boum[.]org
+ /torproject
+ /[0-9]{10} # serial
+ /?
+ (\s+)
+}{$1tor+http://sdscoq7snqtznauu.onion/torproject.org/$2}xms;
+
+### Tagged snapshots
+
+s{
+ ^
+ (deb(?:-src)?\s+)
+ tor[+]https?://tagged[.]snapshots[.]deb[.]tails[.]boum[.]org
+ /[0-9a-z.-]+ # tag
+ /debian
+ /?
+ (\s+)
+}{$1tor+http://vwakviie2ienjx6t.onion/debian/$2}xms;
+
+s{
+ ^
+ (deb(?:-src)?\s+)
+ tor[+]https?://tagged[.]snapshots[.]deb[.]tails[.]boum[.]org
+ /[0-9a-z.-]+ # tag
+ /debian-security
+ /?
+ (\s+)
+}{$1tor+http://sgvtcaew4bxjd7ln.onion/$2}xms;
+
+s{
+ ^
+ (deb(?:-src)?\s+)
+ tor[+]https?://tagged[.]snapshots[.]deb[.]tails[.]boum[.]org
+ /[0-9a-z.-]+ # tag
+ /torproject
+ /?
+ (\s+)
+}{$1tor+http://sdscoq7snqtznauu.onion/torproject.org/$2}xms;
+
+### Custom APT repository
+
+s{
+ ^
+ (deb(?:-src)?\s+)
+ tor[+]https?://deb[.]tails[.]boum[.]org
+ /?
+ (\s+)
+}{$1tor+http://jenw7xbd6tf7vfhp.onion/$2}xms;
+
+' | perl -pi - /etc/apt/sources.list /etc/apt/sources.list.d/*.list
+
+echo "- configuring APT preferences"
+
+echo '
+### Custom APT repository
+
+s{
+ ^
+ (Pin:\s+origin\s+)
+ deb[.]tails[.]boum[.]org
+ $
+}{$1jenw7xbd6tf7vfhp.onion}xms;
+
+### Fix origin for backports
+
+s{
+ ^
+ (Pin:\s+release\s+)
+ o=Debian
+ (,[an]=stretch-backports)
+ $
+}{$1o=Debian Backports$2}xms;
+' | perl -pi - /etc/apt/preferences
+
+find /etc/apt/sources.list* -size 0 -delete
diff --git a/config/chroot_local-includes/lib/live/config/1600-undivert-APT b/config/chroot_local-includes/lib/live/config/1600-undivert-APT
new file mode 100755
index 0000000000000000000000000000000000000000..93f8b539d4f74082eac1e346319620b085412ac1
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/1600-undivert-APT
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+echo "- undiverting APT"
+
+if [ -f /usr/bin/apt-get.real ]; then
+ rm -f usr/bin/apt-get
+ dpkg-divert --rename --remove /usr/bin/apt-get
+fi
diff --git a/config/chroot_local-includes/lib/live/config/2000-aesthetics b/config/chroot_local-includes/lib/live/config/2000-aesthetics
new file mode 100755
index 0000000000000000000000000000000000000000..07847f0ddbcb0e7998c7f5298ed8c2108176b269
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/2000-aesthetics
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+echo ""
+echo "Configuring Tails"
diff --git a/config/chroot_local-includes/lib/live/config/2000-import-gnupg-key b/config/chroot_local-includes/lib/live/config/2000-import-gnupg-key
new file mode 100755
index 0000000000000000000000000000000000000000..b409e9015608bf87a55daeb5b6608e9806851f89
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/2000-import-gnupg-key
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+Import_GnuPG_key ()
+{
+ echo "- importing Tails' GnuPG keys into the ${LIVE_USERNAME}'s keyring"
+ sudo -H -u "${LIVE_USERNAME}" gpg --batch --import /usr/share/doc/tails/website/*.key
+
+ echo "- importing Tails' GnuPG signing key into tails-iuk's trusted keyring"
+ gpg --batch --homedir /usr/share/tails-iuk/trusted_gnupg_homedir \
+ --import /usr/share/doc/tails/website/tails-signing.key
+ chmod -R go+rX /usr/share/tails-iuk
+
+ echo "- importing Tails help desk's GnuPG key into WhisperBack's keyring"
+ gpg --batch --no-default-keyring \
+ --keyring /usr/share/keyrings/whisperback-keyring.gpg \
+ --import /usr/share/doc/tails/website/tails-bugs.key
+ chmod a+r /usr/share/keyrings/whisperback-keyring.gpg
+
+ # Creating state file
+ touch /var/lib/live/config/import-gnupg-key
+}
+
+Import_GnuPG_key
diff --git a/config/chroot_local-includes/lib/live/config/2010-pidgin b/config/chroot_local-includes/lib/live/config/2010-pidgin
new file mode 100755
index 0000000000000000000000000000000000000000..df5f2f94f117dadb7b683fd492b0c46e83072431
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/2010-pidgin
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+# List of at least 2000 possible nicknames
+NICKS_LIST=/usr/share/tails/firstnames.txt
+
+# returns true with probability $1
+prob()
+{
+ # sh doesn't have bash's $RANDOM, so we implement it
+ RANDOM=$(expr $(od -Anv -N4 -tu4 /dev/urandom) % 32768)
+ # we compare with > instead of <= due to mismatch between bash and bc
+ # on what values true and false have.
+ return $(echo "scale = 10; ${RANDOM}/32767 > ${1}" | bc)
+}
+
+leet_nick()
+{
+ # we leave the first letter due to restrictions on nicks in IRC
+ first=$(expr substr ${1} 1 1)
+ rest=${1#?}
+ rest=$(echo "${rest}" | tr 'e' '3' | tr 'i' '1' | tr 'o' '0')
+ echo "${first}${rest}"
+}
+
+generate_nick()
+{
+ NICK=$(/usr/local/bin/lc.py -g 1 --min 4 --max 10 "${NICKS_LIST}")
+
+ if prob 0.90; then
+ NICK=$(echo "${NICK}" | tr '[:upper:]' '[:lower:]')
+ fi
+
+ if prob 0.05; then
+ if prob 0.50; then
+ NICK="${NICK}_"
+ else
+ NICK="${NICK}^"
+ fi
+ fi
+
+ if prob 0.05; then
+ NICK=$(leet_nick "${NICK}")
+ fi
+
+ echo ${NICK}
+}
+
+Configure_pidgin ()
+{
+
+ echo "- configuring Pidgin"
+
+ NICK=$(generate_nick)
+
+ for file in accounts.xml blist.xml ; do
+ sudo -H -u "${LIVE_USERNAME}" sed -i'' "s,XXX_NICK_XXX,${NICK}," "/home/${LIVE_USERNAME}/.purple/${file}"
+ done
+
+ # Creating state file
+ touch /var/lib/live/config/pidgin
+}
+
+Configure_pidgin
diff --git a/config/chroot_local-includes/lib/live/config/2030-systemd b/config/chroot_local-includes/lib/live/config/2030-systemd
new file mode 100755
index 0000000000000000000000000000000000000000..6ee1820067d24e80feecdffa299f3ab98a779c8b
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/2030-systemd
@@ -0,0 +1,10 @@
+#!/bin/sh
+#
+# Recreate files (eg. /var/lib/systemd/catalog/database, /etc/machine-id, etc.)
+# at boot-time as we remove them to ensure a deterministic build.
+
+echo "- configuring systemd"
+
+systemd-machine-id-setup
+
+journalctl --update-catalog
diff --git a/config/chroot_local-includes/lib/live/config/3000-tps-media-directory b/config/chroot_local-includes/lib/live/config/3000-tps-media-directory
new file mode 100755
index 0000000000000000000000000000000000000000..8957a791be2e38c2a19809f5dd7a708a2a553154
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/3000-tps-media-directory
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# We need laxer permissions than the default (tails-persistence-setup:root,
+# 0700) here so that a newly created persistent volume is accessible
+# to the amnesia user, that runs the tails-additional-software-config
+# GUI app which needs to read
+# /media/tails-persistence-setup/TailsData/live-additional-software.conf.
+
+Create_tps_media_directory ()
+{
+ echo "- creating tails-persistence-setup's directory under /media"
+ install -o tails-persistence-setup -g amnesia \
+ -m 0710 -d /media/tails-persistence-setup
+
+ # Creating state file
+ touch /var/lib/live/config/tps-media-directory
+}
+
+Create_tps_media_directory
diff --git a/config/chroot_local-includes/lib/live/config/7000-debug b/config/chroot_local-includes/lib/live/config/7000-debug
new file mode 100755
index 0000000000000000000000000000000000000000..8090dce65af1efd4944c50f9e4abb2944113b992
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/7000-debug
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+Set_root_password ()
+{
+ echo "- setting root password"
+ echo "root:root" | chpasswd
+ touch /var/lib/live/config/rootpassword
+}
+
+if grep -qw "debug=root" /proc/cmdline
+then
+ Set_root_password
+fi
diff --git a/config/chroot_local-includes/lib/live/config/8000-rootpw b/config/chroot_local-includes/lib/live/config/8000-rootpw
new file mode 100755
index 0000000000000000000000000000000000000000..7a48eedd7baaa4f3001c2e97c8037b5bb6ad7689
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/8000-rootpw
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+for X in $(cat /proc/cmdline); do
+ case ${X} in
+ rootpw=*)
+ PW=${X#rootpw=}
+ usermod -p $(echo ${PW} | mkpasswd -s) root
+ exit 0
+ ;;
+ esac
+done
diff --git a/config/chroot_local-includes/lib/live/config/9000-hosts-file b/config/chroot_local-includes/lib/live/config/9000-hosts-file
new file mode 100644
index 0000000000000000000000000000000000000000..461a85dce39dc79ecbcc94c2d40526bcb9c03555
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/9000-hosts-file
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# Note: must run after /lib/live/config/0020-hostname since it
+# otherwise will overwrite any hosts file generated at build time with
+# a bloated one that also include the IPv6 host `::1 localhost`, which
+# can lead to IPv6 traffic, which we block, which may lead to stuff
+# breaking (for instance APT's tor+http transport).
+#
+# localhost.localdomain added to prevent loopback leaks to Tor circuits
+# for applications that use the FQDN (fully qualified domain name) model
+#
+
+echo "- setting up hosts file"
+
+. /etc/live/config.d/hostname.conf
+
+cat > /etc/hosts << EOF
+127.0.0.1 localhost localhost.localdomain ${LIVE_HOSTNAME}
+EOF
diff --git a/config/chroot_local-includes/lib/live/config/9980-permissions b/config/chroot_local-includes/lib/live/config/9980-permissions
new file mode 100755
index 0000000000000000000000000000000000000000..adc07a2fef1452d085dee668e4539a64e2a8dac2
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/9980-permissions
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+Fix_permissions ()
+{
+ echo "- fixing permissions"
+ chown -R "${LIVE_USERNAME}":"${LIVE_USERNAME}" "/home/${LIVE_USERNAME}"
+ chmod go= "/home/${LIVE_USERNAME}"
+
+ # Creating state file
+ touch /var/lib/live/config/permissions
+}
+
+Fix_permissions
diff --git a/config/chroot_local-includes/lib/live/config/9999-unset-user-account-comment b/config/chroot_local-includes/lib/live/config/9999-unset-user-account-comment
new file mode 100755
index 0000000000000000000000000000000000000000..802e27e348c3fd570387df0acc42f8dab0f5c5a9
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/config/9999-unset-user-account-comment
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -e
+
+usermod -c "" "${LIVE_USERNAME}"
diff --git a/config/chroot_local-includes/lib/live/mount/persistence b/config/chroot_local-includes/lib/live/mount/persistence
new file mode 120000
index 0000000000000000000000000000000000000000..fc4cc90aaa0707215ae4e08272c382a1f0d8475b
--- /dev/null
+++ b/config/chroot_local-includes/lib/live/mount/persistence
@@ -0,0 +1 @@
+/live/persistence
\ No newline at end of file
diff --git a/config/chroot_local-includes/lib/systemd/system-shutdown/tails b/config/chroot_local-includes/lib/systemd/system-shutdown/tails
new file mode 100755
index 0000000000000000000000000000000000000000..3800121b055a44005770dc192a13b0b87ed3e64f
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system-shutdown/tails
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+set -x
+
+# This script is only run by the instance of systemd-shutdown that's
+# run outside of the initramfs, and not by the other instance of
+# systemd-shutdown that's run (as /shutdown) after returning to the
+# initramfs during shutdown: in the initramfs, this script is
+# overwritten with /usr/local/lib/initramfs-pre-shutdown-hook.
+
+# Debugging
+/bin/ls -l /run/initramfs
+
+echo 3 > /proc/sys/vm/drop_caches
diff --git a/config/chroot_local-includes/lib/systemd/system-sleep/toggle-tails-shutdown-on-media-removal.sh b/config/chroot_local-includes/lib/systemd/system-sleep/toggle-tails-shutdown-on-media-removal.sh
new file mode 100755
index 0000000000000000000000000000000000000000..859d21eb83de7c283a32ab90b6643212546ddbbb
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system-sleep/toggle-tails-shutdown-on-media-removal.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -e
+
+case "$1" in
+ pre)
+ systemctl stop tails-shutdown-on-media-removal.service
+ ;;
+ post)
+ systemctl start tails-shutdown-on-media-removal.service
+ ;;
+esac
diff --git a/config/chroot_local-includes/lib/systemd/system.conf.d/lower-DefaultTimeoutStopSec.conf b/config/chroot_local-includes/lib/systemd/system.conf.d/lower-DefaultTimeoutStopSec.conf
new file mode 100644
index 0000000000000000000000000000000000000000..90dce4f6602dfaaf6f5d0b3ed887cab4f4fec599
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system.conf.d/lower-DefaultTimeoutStopSec.conf
@@ -0,0 +1,2 @@
+[Manager]
+DefaultTimeoutStopSec=5s
diff --git a/config/chroot_local-includes/lib/systemd/system/alsa-restore.service.d/dont-store-state-on-shutdown.conf b/config/chroot_local-includes/lib/systemd/system/alsa-restore.service.d/dont-store-state-on-shutdown.conf
new file mode 100644
index 0000000000000000000000000000000000000000..f178f07fcd4797f33fd105baca3119d35d1b3989
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/alsa-restore.service.d/dont-store-state-on-shutdown.conf
@@ -0,0 +1,2 @@
+[Service]
+ExecStop=
diff --git a/config/chroot_local-includes/lib/systemd/system/cups.service.d/after-AppArmor.conf b/config/chroot_local-includes/lib/systemd/system/cups.service.d/after-AppArmor.conf
new file mode 100644
index 0000000000000000000000000000000000000000..544fb7da8dd73d7dc871f88290ac427a777a4678
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/cups.service.d/after-AppArmor.conf
@@ -0,0 +1,2 @@
+[Unit]
+After=apparmor.service
diff --git a/config/chroot_local-includes/lib/systemd/system/gdm.service.d/failure.conf b/config/chroot_local-includes/lib/systemd/system/gdm.service.d/failure.conf
new file mode 100644
index 0000000000000000000000000000000000000000..f05a8d528b05d3f43af63effb4341551b0cf9e72
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/gdm.service.d/failure.conf
@@ -0,0 +1,2 @@
+[Unit]
+OnFailure=tails-gdm-failed-to-start.service
diff --git a/config/chroot_local-includes/lib/systemd/system/gdm.service.d/permissions.conf b/config/chroot_local-includes/lib/systemd/system/gdm.service.d/permissions.conf
new file mode 100644
index 0000000000000000000000000000000000000000..ebde20ee883095c561d0cd234c90a39cb6f7e626
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/gdm.service.d/permissions.conf
@@ -0,0 +1,5 @@
+[Service]
+
+# Tails Greeter writes to the tails.persistence file in there
+ExecStartPre=/bin/chgrp Debian-gdm /var/lib/live/config/
+ExecStartPre=/bin/chmod g+w /var/lib/live/config/
diff --git a/config/chroot_local-includes/lib/systemd/system/gdm.service.d/restart.conf b/config/chroot_local-includes/lib/systemd/system/gdm.service.d/restart.conf
new file mode 100644
index 0000000000000000000000000000000000000000..1a68d481e546eee3377bcd53ac5fa85914b60e8c
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/gdm.service.d/restart.conf
@@ -0,0 +1,6 @@
+[Service]
+
+# The GDM unit file has a Restart=always directive, which is good in the
+# general case. However, it breaks our emergency shutdown on boot medium
+# removal feature, so we disable it.
+Restart=no
diff --git a/config/chroot_local-includes/lib/systemd/system/htpdate.service b/config/chroot_local-includes/lib/systemd/system/htpdate.service
new file mode 100644
index 0000000000000000000000000000000000000000..dda8aef88f3405ff30aa2e3131c3765463fd1c3c
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/htpdate.service
@@ -0,0 +1,38 @@
+[Unit]
+Description=Setting time using HTP
+Documentation=https://tails.boum.org/contribute/design/Time_syncing/
+Before=time-sync.target
+Wants=time-sync.target
+
+[Service]
+Type=oneshot
+Environment=DONE_FILE=/run/htpdate/done
+Environment=SUCCESS_FILE=/run/htpdate/success
+Environment=LOG=/var/log/htpdate.log
+EnvironmentFile=/etc/default/htpdate.*
+ExecStartPre=/bin/sh -c \
+ '[ -n "${HTTP_USER_AGENT}" ] && \
+ [ -n "${HTP_POOL_1}" ] && \
+ [ -n "${HTP_POOL_2}" ] && \
+ [ -n "${HTP_POOL_3}" ]'
+ExecStartPre=/bin/rm -f "${DONE_FILE}"
+ExecStartPre=/bin/rm -f "${SUCCESS_FILE}"
+ExecStartPre=/usr/bin/install -o htp -g nogroup -m 0644 /dev/null "${LOG}"
+ExecStart=/usr/local/sbin/htpdate \
+ --debug \
+ --log_file "${LOG}" \
+ --user_agent "${HTTP_USER_AGENT}" \
+ --allowed_per_pool_failure_ratio 0.34 \
+ --user htp \
+ --done_file "${DONE_FILE}" \
+ --success_file "${SUCCESS_FILE}" \
+ --pool1 "${HTP_POOL_1}" \
+ --pool2 "${HTP_POOL_2}" \
+ --pool3 "${HTP_POOL_3}" \
+ --proxy 127.0.0.1:9062
+RemainAfterExit=yes
+CapabilityBoundingSet=CAP_CHOWN CAP_FOWNER CAP_SETUID CAP_SYS_TIME
+PrivateDevices=yes
+PrivateTmp=yes
+ProtectHome=yes
+ProtectSystem=full
diff --git a/config/chroot_local-includes/lib/systemd/system/initramfs-shutdown.service b/config/chroot_local-includes/lib/systemd/system/initramfs-shutdown.service
new file mode 100644
index 0000000000000000000000000000000000000000..431c05cc79f30dfe6bbe091ae13ad75ced14b54b
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/initramfs-shutdown.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Prepare /run/initramfs for shutdown
+Documentation=https://tails.boum.org/contribute/design/memory_erasure/
+ConditionPathExists=!/run/initramfs/bin/sh
+
+[Service]
+RemainAfterExit=yes
+Type=oneshot
+ExecStart=/usr/local/lib/initramfs-restore
+
+[Install]
+WantedBy=multi-user.target
diff --git a/config/chroot_local-includes/lib/systemd/system/live-config.service.d/after-tmpfiles.conf b/config/chroot_local-includes/lib/systemd/system/live-config.service.d/after-tmpfiles.conf
new file mode 100644
index 0000000000000000000000000000000000000000..5ec278cad061caf0ab39fcc9504fcbca713f303a
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/live-config.service.d/after-tmpfiles.conf
@@ -0,0 +1,2 @@
+[Unit]
+After=systemd-tmpfiles-setup.service
diff --git a/config/chroot_local-includes/lib/systemd/system/memlockd.service.d/oom.conf b/config/chroot_local-includes/lib/systemd/system/memlockd.service.d/oom.conf
new file mode 100644
index 0000000000000000000000000000000000000000..bac841be5747038bfef47c1ed223d60f18220a51
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/memlockd.service.d/oom.conf
@@ -0,0 +1,2 @@
+[Service]
+OOMScoreAdjust=-1000
diff --git a/config/chroot_local-includes/lib/systemd/system/onion-grater.service b/config/chroot_local-includes/lib/systemd/system/onion-grater.service
new file mode 100644
index 0000000000000000000000000000000000000000..2032e7021c70ff0a8dcaa6951e119687f5fd8792
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/onion-grater.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=Tor control port filter proxy
+Documentation=https://tails.boum.org/contribute/design/
+
+[Service]
+Type=simple
+ExecStart=/usr/local/lib/onion-grater
+CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_SYS_PTRACE
+PrivateDevices=yes
+PrivateTmp=yes
+ProtectHome=yes
+ProtectSystem=full
+
+[Install]
+WantedBy=multi-user.target
diff --git a/config/chroot_local-includes/lib/systemd/system/run-initramfs.mount b/config/chroot_local-includes/lib/systemd/system/run-initramfs.mount
new file mode 100644
index 0000000000000000000000000000000000000000..20ad14e9b71c560fe926646804dc3788237b746c
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/run-initramfs.mount
@@ -0,0 +1,18 @@
+# This allows systemd-shutdown to execute /run/initramfs/shutdown.
+# XXX:Bullseye: if https://github.com/systemd/systemd/pull/9429 is merged,
+# we can remove this custom code.
+
+[Unit]
+Description=Extracted initrd directory
+ConditionPathIsSymbolicLink=!/run/initramfs
+DefaultDependencies=no
+Before=initramfs-shutdown.service local-fs.target
+
+[Mount]
+What=tmpfs
+Where=/run/initramfs
+Type=tmpfs
+Options=mode=755
+
+[Install]
+WantedBy=local-fs.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-additional-software-install.service b/config/chroot_local-includes/lib/systemd/system/tails-additional-software-install.service
new file mode 100644
index 0000000000000000000000000000000000000000..72a4603a053402e1bbfbcff9197a04bbb6a3de12
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-additional-software-install.service
@@ -0,0 +1,22 @@
+[Unit]
+Description=Install Additional Software Packages
+Documentation=https://tails.boum.org/contribute/design/persistence/
+ConditionFileNotEmpty=/live/persistence/TailsData_unlocked/live-additional-software.conf
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=-/usr/local/sbin/tails-additional-software install
+ExecStartPost=/usr/bin/install -m 0644 -D /dev/null /run/live-additional-software/installed
+TimeoutStartSec=infinity
+PrivateDevices=yes
+PrivateTmp=yes
+# Capabilities needed by tails-additional-software
+CapabilityBoundingSet=CAP_DAC_READ_SEARCH
+# Capabilities needed by apt/dpkg
+CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER CAP_FSETID
+CapabilityBoundingSet=CAP_SETGID CAP_SETUID
+ProtectSystem=no
+# Capabilities needed by tails-notify-user
+CapabilityBoundingSet=CAP_SYS_PTRACE CAP_AUDIT_WRITE CAP_SYS_RESOURCE
+ProtectHome=no
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-additional-software-upgrade.path b/config/chroot_local-includes/lib/systemd/system/tails-additional-software-upgrade.path
new file mode 100644
index 0000000000000000000000000000000000000000..27fa138459b137275a64ac38ec316acd7e1142d5
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-additional-software-upgrade.path
@@ -0,0 +1,9 @@
+[Unit]
+Description=Trigger upgrade of Additional Software Packages
+Documentation=https://tails.boum.org/contribute/design/persistence/
+After=tails-additional-software-install.service
+After=tor-has-bootstrapped.service
+ConditionFileNotEmpty=/live/persistence/TailsData_unlocked/live-additional-software.conf
+
+[Path]
+PathExists=/run/live-additional-software/installed
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-additional-software-upgrade.service b/config/chroot_local-includes/lib/systemd/system/tails-additional-software-upgrade.service
new file mode 100644
index 0000000000000000000000000000000000000000..f9d235f6df9f56243eb58cc1e64951e9cad2cea5
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-additional-software-upgrade.service
@@ -0,0 +1,24 @@
+[Unit]
+Description=Upgrade Additional Software Packages
+Documentation=https://tails.boum.org/contribute/design/persistence/
+After=tails-additional-software-install.service
+After=tor-has-bootstrapped.service
+ConditionFileNotEmpty=/live/persistence/TailsData_unlocked/live-additional-software.conf
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/usr/local/sbin/tails-additional-software upgrade
+ExecStartPost=/usr/bin/install -m 0644 -D /dev/null /run/live-additional-software/upgraded
+TimeoutStartSec=infinity
+PrivateDevices=yes
+PrivateTmp=yes
+# Capabilities needed by tails-additional-software
+CapabilityBoundingSet=CAP_DAC_READ_SEARCH
+# Capabilities needed by apt/dpkg
+CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER CAP_FSETID
+CapabilityBoundingSet=CAP_SETGID CAP_SETUID
+ProtectSystem=no
+# Capabilities needed by tails-notify-user
+CapabilityBoundingSet=CAP_SYS_PTRACE CAP_AUDIT_WRITE CAP_SYS_RESOURCE
+ProtectHome=no
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-autotest-broken-Xorg.service b/config/chroot_local-includes/lib/systemd/system/tails-autotest-broken-Xorg.service
new file mode 100644
index 0000000000000000000000000000000000000000..705fd089df5fae3ff2358ee19720c9c1cd32adc7
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-autotest-broken-Xorg.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Break Xorg for Tails test suite
+Documentation=https://tails.boum.org/contribute/release_process/test/automated_tests/
+ConditionKernelCommandLine=autotest_broken_Xorg
+Before=gdm.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/bin/sh -c 'echo "#!/bin/sh\nexit 1" > /usr/bin/Xorg'
+
+[Install]
+WantedBy=multi-user.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-autotest-remote-shell.service b/config/chroot_local-includes/lib/systemd/system/tails-autotest-remote-shell.service
new file mode 100644
index 0000000000000000000000000000000000000000..aeb31ed0b3db65c0f5ca0ce676aa43c46074d317
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-autotest-remote-shell.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Remote shell used in Tails test suite
+Documentation=https://tails.boum.org/contribute/release_process/test/automated_tests/
+ConditionKernelCommandLine=autotest_never_use_this_option
+Before=gdm.service
+
+[Service]
+Type=notify
+ExecStart=/usr/local/lib/tails-autotest-remote-shell
+OOMScoreAdjust=-1000
+
+[Install]
+WantedBy=multi-user.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-gdm-failed-to-start.service b/config/chroot_local-includes/lib/systemd/system/tails-gdm-failed-to-start.service
new file mode 100644
index 0000000000000000000000000000000000000000..ae598f88d472e4283c49dc492e1a255b8d504683
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-gdm-failed-to-start.service
@@ -0,0 +1,38 @@
+[Unit]
+Description=Guide the user when GDM fails to start
+# GDM normally runs "plymouth --quit" when it fails to start the X server
+# (see on_display_status_changed that calls plymouth_quit_without_transition).
+# But when this happens we kill -9 GDM in our gdm-x-session wrapper, so it
+# might not have time to quit plymouth yet. Therefore we ensure plymouth
+# has quit before we start: we run after plymouth-quit.service (which is started
+# by gdm.service's default OnFailure=), we have plymouth-quit-wait.service start,
+# and we wait for it to complete.
+After=plymouth-quit.service
+Requires=plymouth-quit-wait.service
+After=plymouth-quit-wait.service
+
+[Service]
+Type=oneshot
+# We use VT5 that is clean of boot messages and does not get a getty started
+# when we switch there, thanks to our custom NAutoVTs=4 logind.conf setting
+#
+# There are queued udev events when we run plymouthd so on Stretch, so
+# watch_for_coldplug_completion will set up a watcher and return before
+# there's any place where plymouthd can create a seat to display its
+# splash and messages on. So we tell plymouthd to ignore udev which makes
+# it create a fallback seat.
+# XXX:Buster: check if plymouth.ignore-udev is still necessary (this code path
+# has changed in plymouth 0.9.3)
+ExecStart=/bin/sh -c \
+ '/sbin/plymouthd --mode=shutdown --tty=tty5 \
+ --kernel-command-line="plymouth.ignore-udev $(cat /proc/cmdline)"'
+ExecStart=/bin/chvt 5
+ExecStart=/bin/plymouth show-splash
+ExecStart=/bin/sh -c \
+ 'MAX_LENGTH=254 ; \
+ PREFIX="Error starting GDM with your graphics card: " ; \
+ SUFFIX=". Please take note of this error and visit https://tails.boum.org/gdm for troubleshooting." ; \
+ MAX_VIDEO_CARD_LENGTH=$(($MAX_LENGTH - $(echo -n "$PREFIX$SUFFIX" | wc -c))) ; \
+ VIDEO_CARD=$(lspci -d::0300 -nn | sed -E "s,.* VGA compatible controller \[0300\]:\s*,," | cut -c "1-$MAX_VIDEO_CARD_LENGTH") ; \
+ /bin/plymouth display-message --text="$PREFIX$VIDEO_CARD$SUFFIX" \
+ '
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-set-wireless-devices-state.service b/config/chroot_local-includes/lib/systemd/system/tails-set-wireless-devices-state.service
new file mode 100644
index 0000000000000000000000000000000000000000..6c41e28947f66c345fc5b01360569cf0de7c53bc
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-set-wireless-devices-state.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=Set proper default state on wireless devices
+Documentation=https://tails.boum.org/contribute/design/
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/lib/tails-set-wireless-devices-state
+CapabilityBoundingSet=~CAP_SYS_ADMIN
+PrivateDevices=yes
+PrivateNetwork=yes
+PrivateTmp=yes
+ProtectHome=yes
+ProtectSystem=full
+
+[Install]
+WantedBy=multi-user.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-shutdown-on-media-removal.service b/config/chroot_local-includes/lib/systemd/system/tails-shutdown-on-media-removal.service
new file mode 100644
index 0000000000000000000000000000000000000000..d469a1d9127a3f9450c35912be45bb823aa16604
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-shutdown-on-media-removal.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=Wipe memory on live media removal
+Documentation=https://tails.boum.org/contribute/design/memory_erasure/
+After=memlockd.service initramfs-shutdown.service
+
+[Service]
+Type=simple
+ExecStart=/usr/local/lib/udev-watchdog-wrapper
+CapabilityBoundingSet=~CAP_SYS_ADMIN
+PrivateNetwork=yes
+PrivateTmp=yes
+ProtectHome=yes
+ProtectSystem=full
+
+[Install]
+WantedBy=multi-user.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-synchronize-data-to-new-persistent-volume-on-shutdown.service b/config/chroot_local-includes/lib/systemd/system/tails-synchronize-data-to-new-persistent-volume-on-shutdown.service
new file mode 100644
index 0000000000000000000000000000000000000000..e38097c61013d75cc3308f6ce4edd5fbc931639b
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-synchronize-data-to-new-persistent-volume-on-shutdown.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=Synchronize data to newly created persistent volume on shutdown
+
+[Service]
+RemainAfterExit=yes
+Type=oneshot
+ExecStop=/bin/sh -c ' \
+ if mountpoint -q /media/tails-persistence-setup/TailsData \
+ && test ! -d /media/tails-persistence-setup/TailsData/apt; then \
+ echo "Copy APT data to newly created persistent volume"; \
+ mkdir /media/tails-persistence-setup/TailsData/apt/ && \
+ cp -a /var/cache/apt/archives \
+ /media/tails-persistence-setup/TailsData/apt/cache && \
+ cp -a /var/lib/apt/lists \
+ /media/tails-persistence-setup/TailsData/apt/; \
+ fi'
+
+[Install]
+WantedBy=multi-user.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-tor-has-bootstrapped-flag-file.service b/config/chroot_local-includes/lib/systemd/system/tails-tor-has-bootstrapped-flag-file.service
new file mode 100644
index 0000000000000000000000000000000000000000..1d932c53cf1956414920f5c477e71602723efea5
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-tor-has-bootstrapped-flag-file.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=Manage the flag file that indicates whether Tor has bootstrapped
+Documentation=https://tails.boum.org/contribute/design/
+After=tails-wait-until-tor-has-bootstrapped.service
+PartOf=tails-tor-has-bootstrapped.target
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+User=debian-tor
+ExecStart=/bin/touch /run/tor-has-bootstrapped/done
+ExecStop=/bin/rm -f /run/tor-has-bootstrapped/done
+CapabilityBoundingSet=
+PrivateDevices=yes
+PrivateNetwork=yes
+PrivateTmp=yes
+ProtectHome=yes
+ProtectSystem=full
+
+[Install]
+WantedBy=tails-tor-has-bootstrapped.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-tor-has-bootstrapped.target b/config/chroot_local-includes/lib/systemd/system/tails-tor-has-bootstrapped.target
new file mode 100644
index 0000000000000000000000000000000000000000..0aca04f33838696849991995107cea53d03216df
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-tor-has-bootstrapped.target
@@ -0,0 +1,10 @@
+[Unit]
+Description=Tor has bootstrapped
+Documentation=https://tails.boum.org/contribute/design/
+Requires=graphical.target
+Conflicts=rescue.service rescue.target
+After=graphical.target rescue.service rescue.target
+AllowIsolate=yes
+
+[Install]
+WantedBy=graphical.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-unblock-network.service b/config/chroot_local-includes/lib/systemd/system/tails-unblock-network.service
new file mode 100644
index 0000000000000000000000000000000000000000..e8feacad7cac3c6d5b3bd9d1b0a660dd902114a3
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-unblock-network.service
@@ -0,0 +1,31 @@
+[Unit]
+Description=Unblock network device drivers
+Documentation=https://tails.boum.org/contribute/design/MAC_address/
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+EnvironmentFile=/var/lib/gdm3/tails.physical_security
+
+# It's important we "export" the settings from tails.physical_security
+# before unblocking the network; doing so will make the user-set MAC spoofing
+# option apply (via the custom udev rule) when loading the modules for the
+# previously blocked network devices.
+ExecStartPre=/usr/bin/install -m 0640 -o root -g root \
+ /var/lib/gdm3/tails.physical_security \
+ /var/lib/live/config/tails.physical_security
+ExecStartPre=/bin/sync
+ExecStartPre=/bin/sh -c \
+ 'if [ "${TAILS_NETCONF}" = "obstacle" ] ; then \
+ . /usr/local/lib/tails-shell-library/tor.sh ; \
+ tor_set_in_torrc "DisableNetwork" "1" ; \
+ fi'
+
+# We sync to make sure the blacklist has disappeared from the
+# filesystem
+ExecStart=/bin/sh -c \
+ 'if [ "${TAILS_NETCONF}" != "disabled" ] ; then \
+ /bin/rm -f /etc/modprobe.d/all-net-blacklist.conf ; \
+ /bin/touch /etc/modprobe.d ; \
+ /bin/sync ; \
+ fi'
diff --git a/config/chroot_local-includes/lib/systemd/system/tails-wait-until-tor-has-bootstrapped.service b/config/chroot_local-includes/lib/systemd/system/tails-wait-until-tor-has-bootstrapped.service
new file mode 100644
index 0000000000000000000000000000000000000000..64f2afde0c11511fcc08a713e13e9db5b6f63ff1
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tails-wait-until-tor-has-bootstrapped.service
@@ -0,0 +1,22 @@
+[Unit]
+Description=Wait for Tor to Have Bootstrapped
+Documentation=https://tails.boum.org/contribute/design/
+After=tor@default.service
+Before=tails-tor-has-bootstrapped.target
+
+[Service]
+Type=oneshot
+User=debian-tor
+ExecStart=/bin/sh -c '. /usr/local/lib/tails-shell-library/tor.sh ; \
+ while ! tor_is_working ; do \
+ /bin/sleep 1 ; \
+ done'
+TimeoutStartSec=0
+CapabilityBoundingSet=
+PrivateDevices=yes
+PrivateTmp=yes
+ProtectHome=yes
+ProtectSystem=full
+
+[Install]
+WantedBy=tails-tor-has-bootstrapped.target
diff --git a/config/chroot_local-includes/lib/systemd/system/tor@default.service.d/fix-obfs4proxy.conf b/config/chroot_local-includes/lib/systemd/system/tor@default.service.d/fix-obfs4proxy.conf
new file mode 100644
index 0000000000000000000000000000000000000000..9cb80b8795aafeada799df91166339d2a33d7dec
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tor@default.service.d/fix-obfs4proxy.conf
@@ -0,0 +1,4 @@
+[Service]
+
+# obfs4proxy can't start otherwise
+NoNewPrivileges=no
diff --git a/config/chroot_local-includes/lib/systemd/system/tor@default.service.d/writable-etc-tor.conf b/config/chroot_local-includes/lib/systemd/system/tor@default.service.d/writable-etc-tor.conf
new file mode 100644
index 0000000000000000000000000000000000000000..096151a2142dbcf9b143725440e5c19a446332e0
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/tor@default.service.d/writable-etc-tor.conf
@@ -0,0 +1,5 @@
+[Service]
+
+# Tor Launcher needs to make Tor modify its configuration
+ProtectSystem=yes
+ReadWriteDirectories=-/etc/tor
diff --git a/config/chroot_local-includes/lib/systemd/system/var-tmp.mount b/config/chroot_local-includes/lib/systemd/system/var-tmp.mount
new file mode 100644
index 0000000000000000000000000000000000000000..cef423d97c1b9f37b4813733d29fda6ece235de6
--- /dev/null
+++ b/config/chroot_local-includes/lib/systemd/system/var-tmp.mount
@@ -0,0 +1,15 @@
+[Unit]
+Description=Temporary Directory
+ConditionPathIsSymbolicLink=!/var/tmp
+DefaultDependencies=no
+Conflicts=umount.target
+Before=local-fs.target umount.target
+
+[Mount]
+What=tmpfs
+Where=/var/tmp
+Type=tmpfs
+Options=mode=1777,strictatime
+
+[Install]
+WantedBy=local-fs.target
diff --git a/config/chroot_local-includes/live/overlay b/config/chroot_local-includes/live/overlay
new file mode 120000
index 0000000000000000000000000000000000000000..1f1f1cf62f0f7fc0d93fa4eb61a88b9a1b149f3e
--- /dev/null
+++ b/config/chroot_local-includes/live/overlay
@@ -0,0 +1 @@
+/lib/live/mount/overlay
\ No newline at end of file
diff --git a/config/chroot_local-includes/root/.synaptic/synaptic.conf b/config/chroot_local-includes/root/.synaptic/synaptic.conf
new file mode 100644
index 0000000000000000000000000000000000000000..3a18e4aa4b37d782e6ec51ce1fcf632c93b90f60
--- /dev/null
+++ b/config/chroot_local-includes/root/.synaptic/synaptic.conf
@@ -0,0 +1,4 @@
+Synaptic "" {
+ showWelcomeDialog "0";
+ Maximized "1";
+};
diff --git a/config/chroot_local-includes/usr/lib/gdm3/gdm-x-session.tails b/config/chroot_local-includes/usr/lib/gdm3/gdm-x-session.tails
new file mode 100755
index 0000000000000000000000000000000000000000..9441ab738984277899c7f26f669983b8504cf81a
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/gdm3/gdm-x-session.tails
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+# No "set -e" because we need to capture the exit status of gdm-x-session.real
+set -u
+
+FAILURES_COUNT_FILE=/var/lib/gdm3/gdm-x-session_failures
+MAX_FAILURES=5
+
+get_failures () {
+ local failures=0
+ if [ -f "$FAILURES_COUNT_FILE" ] ; then
+ failures=$(cat "$FAILURES_COUNT_FILE")
+ fi
+ echo -n "$failures"
+}
+
+increment_failures () {
+ failures=$(($(get_failures) + 1))
+ echo -n "$failures" > "$FAILURES_COUNT_FILE"
+}
+
+/usr/lib/gdm3/gdm-x-session.real "$@"
+
+RET=$?
+
+if [ $RET -ne 0 ] ; then
+ increment_failures
+ if [ $(get_failures) -ge "$MAX_FAILURES" ] ; then
+ # Trigger OnFailure=tails-gdm-failed-to-start.service
+ echo "gdm-x-session failed too many times, stopping GDM"
+ sudo -n /bin/systemctl kill --signal=9 gdm
+ fi
+fi
+
+exit $RET
diff --git a/config/chroot_local-includes/usr/lib/systemd/logind.conf.d/lower-NAutoVTs.conf b/config/chroot_local-includes/usr/lib/systemd/logind.conf.d/lower-NAutoVTs.conf
new file mode 100644
index 0000000000000000000000000000000000000000..df32fcca78999ef850565020f00a1d1e7ccccb05
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/logind.conf.d/lower-NAutoVTs.conf
@@ -0,0 +1,3 @@
+# Leaves a free virtual terminal to run tails-gdm-failed-to-start.service on
+[Login]
+NAutoVTs=4
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/desktop.target b/config/chroot_local-includes/usr/lib/systemd/user/desktop.target
new file mode 100644
index 0000000000000000000000000000000000000000..5f4ee0f2e0dede96e298d18822a2435064cb1659
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/desktop.target
@@ -0,0 +1,5 @@
+[Unit]
+Description=Desktop
+Requires=default.target
+After=default.target
+AllowIsolate=yes
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/gnome-early-initialization.target b/config/chroot_local-includes/usr/lib/systemd/user/gnome-early-initialization.target
new file mode 100644
index 0000000000000000000000000000000000000000..0a024d7ee6ade527ea520be2d9f535594dd7cd5a
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/gnome-early-initialization.target
@@ -0,0 +1,5 @@
+[Unit]
+Description=Bits of GNOME EarlyInitialization managed by systemd
+Requires=default.target
+After=default.target
+AllowIsolate=yes
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/tails-add-GNOME-bookmarks.service b/config/chroot_local-includes/usr/lib/systemd/user/tails-add-GNOME-bookmarks.service
new file mode 100644
index 0000000000000000000000000000000000000000..834af27fd27bf40d67739ea362ddfbb23c8f9cb1
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/tails-add-GNOME-bookmarks.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Add GTK bookmarks to some directories
+Documentation=https://tails.boum.org/contribute/design/application_isolation/
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/lib/add-GNOME-bookmarks
+RemainAfterExit=yes
+
+[Install]
+WantedBy=basic.target
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/tails-additional-software-install.service b/config/chroot_local-includes/usr/lib/systemd/user/tails-additional-software-install.service
new file mode 100644
index 0000000000000000000000000000000000000000..fff9413ba385453bb7ded96ab4772109afb5ea9c
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/tails-additional-software-install.service
@@ -0,0 +1,18 @@
+# This user unit triggers installation of additional software packages after
+# the desktop has started by starting the identically named system unit.
+
+[Unit]
+Description=Trigger installation of Additional Software Packages
+Documentation=https://tails.boum.org/contribute/design/persistence/
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/usr/bin/sudo /bin/systemctl start tails-additional-software-install.service
+# XXX:Buster: when policykit-1 >= 0.106 is available in Tails, we should
+# use the following, and remove sudoers.d configuration:
+#ExecStart=/bin/systemctl start tails-additional-software-install.service
+TimeoutStartSec=0
+
+[Install]
+WantedBy=desktop.target
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/tails-configure-keyboard.service b/config/chroot_local-includes/usr/lib/systemd/user/tails-configure-keyboard.service
new file mode 100644
index 0000000000000000000000000000000000000000..21dff6800384a79152cd40f919786ef00770d12b
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/tails-configure-keyboard.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Configure the keyboard layout according to settings chosen in Tails Greeter
+Documentation=https://tails.boum.org/contribute/design/
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/lib/tails-configure-keyboard
+RemainAfterExit=yes
+
+[Install]
+WantedBy=gnome-early-initialization.target
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/tails-create-tor-browser-directories.service b/config/chroot_local-includes/usr/lib/systemd/user/tails-create-tor-browser-directories.service
new file mode 100644
index 0000000000000000000000000000000000000000..bb02fd6f72abfcd0722b68624add41526723a3af
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/tails-create-tor-browser-directories.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Create the Tor Browser amnesiac and persistent directories
+Documentation=https://tails.boum.org/contribute/design/application_isolation/
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/lib/create-tor-browser-directories
+RemainAfterExit=yes
+
+[Install]
+WantedBy=basic.target
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/tails-security-check.service b/config/chroot_local-includes/usr/lib/systemd/user/tails-security-check.service
new file mode 100644
index 0000000000000000000000000000000000000000..01260af3ba4fc3f0e367e9e5b285b5a41dad4252
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/tails-security-check.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Check Tails known, unfixed security issues
+Documentation=https://tails.boum.org/contribute/design/
+After=tails-wait-until-tor-has-bootstrapped.service
+
+[Service]
+ExecStart=/usr/local/bin/tails-security-check
+RemainAfterExit=yes
+
+[Install]
+WantedBy=desktop.target
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/tails-upgrade-frontend.service b/config/chroot_local-includes/usr/lib/systemd/user/tails-upgrade-frontend.service
new file mode 100644
index 0000000000000000000000000000000000000000..2b4c9e902de8c5b961616c732af59b134bfc31b6
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/tails-upgrade-frontend.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Check available Tails upgrades
+Documentation=https://tails.boum.org/contribute/design/incremental_upgrades/
+After=tails-wait-until-tor-has-bootstrapped.service
+
+[Service]
+ExecStart=/usr/local/bin/tails-upgrade-frontend-wrapper
+RemainAfterExit=yes
+
+[Install]
+WantedBy=desktop.target
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/tails-virt-notify-user.service b/config/chroot_local-includes/usr/lib/systemd/user/tails-virt-notify-user.service
new file mode 100644
index 0000000000000000000000000000000000000000..591cee38c155d552e97875d840b03a051e8b4c1d
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/tails-virt-notify-user.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=Warn the user if Tails is running inside a virtual machine
+Documentation=https://tails.boum.org/contribute/design/virtualization_support/
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/lib/tails-virt-notify-user
+RemainAfterExit=yes
+
+[Install]
+WantedBy=desktop.target
diff --git a/config/chroot_local-includes/usr/lib/systemd/user/tails-wait-until-tor-has-bootstrapped.service b/config/chroot_local-includes/usr/lib/systemd/user/tails-wait-until-tor-has-bootstrapped.service
new file mode 100644
index 0000000000000000000000000000000000000000..191f8bdd47dfd496ae6a8a6adf79d36db78fb01e
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/systemd/user/tails-wait-until-tor-has-bootstrapped.service
@@ -0,0 +1,19 @@
+# The systemd user and system instances don't share units. So, we have
+# the *system* tails-wait-until-tor-has-bootstrapped.service create
+# a file upon completion -- and the identically named *user* unit file wait for
+# that file to appear.
+
+[Unit]
+Description=Wait for Tor to Have Bootstrapped
+Documentation=https://tails.boum.org/contribute/design/
+
+[Service]
+Type=oneshot
+ExecStart=/bin/sh -c '[ "$(/usr/bin/id -u)" = 1000 ] || exit 0 ; \
+ while ! [ -e /run/tor-has-bootstrapped/done ] ; do \
+ /bin/sleep 1 ; \
+ done'
+TimeoutStartSec=0
+
+[Install]
+WantedBy=desktop.target
diff --git a/config/chroot_local-includes/usr/lib/tmpfiles.d/htpdate.conf b/config/chroot_local-includes/usr/lib/tmpfiles.d/htpdate.conf
new file mode 100644
index 0000000000000000000000000000000000000000..2604bc1e8d0f2448990ef446c7908a65a8fd6eb7
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/tmpfiles.d/htpdate.conf
@@ -0,0 +1,2 @@
+# Type Path Mode UID GID Age Argument
+d /run/htpdate 00755 root root - -
diff --git a/config/chroot_local-includes/usr/lib/tmpfiles.d/tails-additional-software.conf b/config/chroot_local-includes/usr/lib/tmpfiles.d/tails-additional-software.conf
new file mode 100644
index 0000000000000000000000000000000000000000..d2fd48b5d58e378ed0f8a2054a1945dace544653
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/tmpfiles.d/tails-additional-software.conf
@@ -0,0 +1,2 @@
+# Type Path Mode UID GID Age Argument
+d /run/live-additional-software 00775 root root - -
diff --git a/config/chroot_local-includes/usr/lib/tmpfiles.d/tails-upgrader.conf b/config/chroot_local-includes/usr/lib/tmpfiles.d/tails-upgrader.conf
new file mode 100644
index 0000000000000000000000000000000000000000..fd4cb3d5922283045b471edd6a2feb61f9a41385
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/tmpfiles.d/tails-upgrader.conf
@@ -0,0 +1,3 @@
+# Type Path Mode UID GID Age Argument
+d /run/tails-upgrader 00775 root tails-upgrade-frontend - -
+d /usr/share/tails-iuk/trusted_gnupg_homedir 00700 root root - -
diff --git a/config/chroot_local-includes/usr/lib/tmpfiles.d/tor-has-bootstrapped.conf b/config/chroot_local-includes/usr/lib/tmpfiles.d/tor-has-bootstrapped.conf
new file mode 100644
index 0000000000000000000000000000000000000000..28b7456a716efef99a55445f261871f41b8f8dcf
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/tmpfiles.d/tor-has-bootstrapped.conf
@@ -0,0 +1,2 @@
+# Type Path Mode UID GID Age Argument
+d /run/tor-has-bootstrapped 00755 debian-tor debian-tor - -
diff --git a/config/chroot_local-includes/usr/lib/tmpfiles.d/tordate.conf b/config/chroot_local-includes/usr/lib/tmpfiles.d/tordate.conf
new file mode 100644
index 0000000000000000000000000000000000000000..6af8297420a9b50e08f204bda3d611ac66d73035
--- /dev/null
+++ b/config/chroot_local-includes/usr/lib/tmpfiles.d/tordate.conf
@@ -0,0 +1,2 @@
+# Type Path Mode UID GID Age Argument
+d /run/tordate 00755 root root - -
diff --git a/config/chroot_local-includes/usr/local/bin/electrum b/config/chroot_local-includes/usr/local/bin/electrum
new file mode 100755
index 0000000000000000000000000000000000000000..9c6e371f546dab33393859cc73430182a3c94a56
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/electrum
@@ -0,0 +1,82 @@
+#! /usr/bin/env python3
+"""
+Tails electrum wrapper
+
+Test with "python3 electrum.py doctest".
+The tests will start the tor-browser so you probably
+want to use a tester that handles user interaction or
+run the tests from the command line and answer prompts as needed.
+
+goodcrypto.com converted from bash to python and added basic tests.
+
+>>> # run script
+>>> sh.Command(sys.argv[0])()
+
+"""
+
+import os
+import sys
+from gettext import gettext
+
+import sh
+
+os.environ['TEXTDOMAIN'] = 'tails'
+
+HOME_DIR = os.environ['HOME']
+CONF_DIR = os.path.join(HOME_DIR, '.electrum')
+
+
+def main(*args):
+ if not electrum_config_is_persistent():
+ if not verify_start():
+ return
+
+ os.execv('/usr/bin/electrum', ['/usr/bin/electrum'] + list(args))
+
+
+def electrum_config_is_persistent():
+ """Return True iff electrum config is persistent.
+
+ >>> electrum_config_is_persistent()
+ False
+ """
+
+ filesystem = sh.findmnt('--noheadings',
+ '--output', 'SOURCE',
+ '--target', CONF_DIR).stdout.decode().strip()
+ return filesystem in sh.glob('/dev/mapper/TailsData_unlocked[/electrum]')
+
+
+def verify_start():
+ """Ask user whether to start Electrum.
+
+ >>> verify_start() in (True, False)
+ True
+ """
+
+ disabled_text = gettext('Persistence is disabled for Electrum')
+ warning_text = gettext(
+ "When you reboot Tails, all of Electrum's data will be lost, including your Bitcoin wallet. It is strongly recommended to only run Electrum when its persistence feature is activated.")
+ question_text = gettext('Do you want to start Electrum anyway?')
+ dialog_msg = ('{}\n\n{}\n\n{}\n'.
+ format(disabled_text, warning_text, question_text))
+ launch_text = gettext('_Launch')
+ exit_text = gettext('_Exit')
+
+ # results 0 == True; 1 == False; 5 == Timeout
+ results = sh.zenity('--question', '--title', '', '--default-cancel',
+ '--ok-label', launch_text,
+ '--cancel-label', exit_text,
+ '--text', dialog_msg,
+ _ok_code=[0,1,5])
+ start = results.exit_code == 0
+
+ return start
+
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1 and sys.argv[1] == 'doctest':
+ import doctest
+ doctest.testmod()
+ else:
+ main(*sys.argv[1:])
diff --git a/config/chroot_local-includes/usr/local/bin/git b/config/chroot_local-includes/usr/local/bin/git
new file mode 100755
index 0000000000000000000000000000000000000000..487236252dd95cda0cbc4924e7538616aeed72d6
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/git
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec /usr/bin/torsocks /usr/bin/git "$@"
diff --git a/config/chroot_local-includes/usr/local/bin/gnome-terminal-pkexec b/config/chroot_local-includes/usr/local/bin/gnome-terminal-pkexec
new file mode 100755
index 0000000000000000000000000000000000000000..4982d5203a35b35b7a379e04cd43c09b7839a89a
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/gnome-terminal-pkexec
@@ -0,0 +1,2 @@
+#!/bin/sh
+pkexec /usr/bin/gnome-terminal "${@}"
diff --git a/config/chroot_local-includes/usr/local/bin/keepassx b/config/chroot_local-includes/usr/local/bin/keepassx
new file mode 100755
index 0000000000000000000000000000000000000000..806259ca8e0e3312bc4f4e952e90c3f2d9c8c5db
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/keepassx
@@ -0,0 +1,70 @@
+#!/bin/sh
+
+set -e
+set -u
+
+. gettext.sh
+TEXTDOMAIN="tails"
+export TEXTDOMAIN
+
+PERSISTENT_DATA_DIR="${HOME}/Persistent"
+OLD_DB="${PERSISTENT_DATA_DIR}/keepassx.kdb"
+MIGRATED_DB="${PERSISTENT_DATA_DIR}/New database.kdbx"
+NEW_DB="${PERSISTENT_DATA_DIR}/keepassx.kdbx"
+
+prompt_for_database_renaming() {
+ local filename="${1}"
+ local dialog_msg="`eval_gettext \"Do you want to rename your KeePassX database?
+
+You have a KeePassX database in your Persistent folder:
+
+\\\${filename}
+
+Renaming it to keepassx.kdbx would allow KeePassX to open it automatically in the future.\"`
+"
+ local rename="`gettext \"Rename\"`"
+ local open="`gettext \"Keep current name\"`"
+ zenity --question --title "" --text "${dialog_msg}" --default-cancel \
+ --ok-label "${rename}" --cancel-label "${open}"
+}
+
+# There's a migrated DB named 'New database' => rename it before opening it:
+if mountpoint -q "$PERSISTENT_DATA_DIR" && \
+ [ -e "$MIGRATED_DB" ] && ! [ -e "$NEW_DB" ]; then
+ mv "$MIGRATED_DB" "$NEW_DB"
+ exec /usr/bin/keepassx "$NEW_DB"
+
+# New database file is not named keepassx.kdbx, prompt for renaming it.
+elif mountpoint -q "$PERSISTENT_DATA_DIR" && \
+ ! [ -e "${NEW_DB}" ] && \
+ [ "$(find "$PERSISTENT_DATA_DIR" -maxdepth 1 -name '*.kdbx' 2>/dev/null | wc -l)" = "1" ]; then
+ user_db="$(find "$PERSISTENT_DATA_DIR" -maxdepth 1 -name '*.kdbx' 2>/dev/null)"
+ if ! [ -e "${PERSISTENT_DATA_DIR}/.no_keepassx_db_renaming" ] \
+ && prompt_for_database_renaming "${user_db}"; then
+ mv "${user_db}" "${NEW_DB}"
+ exec /usr/bin/keepassx "${NEW_DB}"
+ else
+ touch "${PERSISTENT_DATA_DIR}/.no_keepassx_db_renaming"
+ exec /usr/bin/keepassx
+ fi
+
+# There's an old DB but no new one => import the old DB:
+elif mountpoint -q "$PERSISTENT_DATA_DIR" \
+ && ! [ -e "$NEW_DB" ] \
+ && [ -e "$OLD_DB" ]; then
+
+ # Ensure $PERSISTENT_DATA_DIR is pre-selected for saving
+ # the migrated database
+ cd "$PERSISTENT_DATA_DIR"
+
+ # Trigger the migration from the old KeePassX database to the new format
+ # used in KeePassX 2.0.x.
+ /usr/bin/keepassx "$OLD_DB"
+
+ # Avoid the user being prompted to open the old DB on next run.
+ mv "$OLD_DB" "${OLD_DB}.bak"
+
+# Default case:
+else
+ exec /usr/bin/keepassx "$@"
+fi
diff --git a/config/chroot_local-includes/usr/local/bin/lc.py b/config/chroot_local-includes/usr/local/bin/lc.py
new file mode 100755
index 0000000000000000000000000000000000000000..5a817927996edc2d348690d950f08da29beebc7c
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/lc.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python3
+
+# The MIT License
+#
+# Copyright (c) 2011 Christopher Pound
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+# lc.py -- language confluxer (http://www.ruf.rice.edu/~pound/lc.py)
+#
+# - Written by Christopher Pound (pound@rice.edu), July 1993.
+# - Loren Miller suggested I make sure lc starts by picking a
+# letter pair that was at the beginning of a data word, Oct 95.
+# - Cleaned it up a little bit, March 95; more, September 01
+# - Python version, Jul 09
+#
+# The datafile should be a bunch of words from some language
+# with minimal punctuation or garbage (# starts a comment).
+
+from optparse import OptionParser
+import random
+import re
+import sys
+
+class Pseudolanguage:
+
+ def __init__(self, **dict):
+ """Set up a new pseudolanguage"""
+ dict.setdefault('name', '')
+ self.name = dict['name']
+ self.parsed = False
+ self.data = {}
+ self.inits = {}
+ self.pairs = {}
+
+ def incorporate(self, files):
+ """Load list of files for this pseudolanguage into self.data"""
+ self.parsed = False
+ for f in files:
+ words = []
+ with open(f) as text:
+ for line in text:
+ line = line.strip()
+ line = re.sub(r"#.*", "", line)
+ words.extend(re.split(r"\s+", line))
+ self.data[f] = words
+
+ def delete(self, files):
+ """Delete a list of languages from self.data"""
+ self.parsed = False
+ for f in files:
+ del self.data[f]
+
+ def parse(self):
+ """Parse pseudolanguage's data into self.inits and self.pairs"""
+ if not self.parsed:
+ self.inits.clear()
+ self.pairs.clear()
+ for f in self.data:
+ for word in self.data[f]:
+ word += ' '
+ if len(word) > 3:
+ if word[0:2] in self.inits:
+ self.inits[word[0:2]].append(word[2:3])
+ else:
+ self.inits[word[0:2]] = [word[2:3]]
+ pos = 0
+ while pos < len(word)-2:
+ if word[pos:pos+2] in self.pairs:
+ self.pairs[word[pos:pos+2]].append(word[pos+2])
+ else:
+ self.pairs[word[pos:pos+2]] = [word[pos+2]]
+ pos = pos + 1
+ self.parsed = True
+
+ def dump(self):
+ """Print the current parsed data; use pickle for inflatable dumps"""
+ self.parse()
+ print('name = """', self.name, '"""')
+ print("dump = { 'inits': ", self.inits, ",")
+ print("'pairs': ", self.pairs, " }")
+
+ def generate(self, number, min, max):
+ """Generate list of words of min and max lengths"""
+ self.parse()
+ wordlist = []
+ while len(wordlist) < number:
+ word = random.choice(list(self.inits.keys()))
+ while word.find(' ') == -1:
+ word += random.choice(self.pairs[word[-2:]])
+ word = word.strip()
+ if len(word) >= min and len(word) <= max:
+ wordlist.append(word)
+ return wordlist
+
+if __name__ == '__main__':
+
+ usage = "usage: %prog [options] datafile1 [datafile2 ...]"
+ parser = OptionParser(usage=usage, version="%prog 1.0")
+ parser.add_option("-d", "--dump", action="store_true",
+ dest="dump", default=False,
+ help="Dump internal representation of the pseudolanguage")
+ parser.add_option("-g", "--generate", type="int", dest="num",
+ help="Generate specified number of words")
+ parser.add_option("--min", type="int", dest="min", default=3,
+ help="Set the minimum length of each word")
+ parser.add_option("--max", type="int", dest="max", default=9,
+ help="Set the maximum length of each word")
+ parser.add_option("--name", dest="name", default=' ',
+ help="Set the name of the pseudolanguage")
+ (options, args) = parser.parse_args()
+
+ aLanguage = Pseudolanguage(name=options.name)
+ aLanguage.incorporate(args)
+ if options.dump:
+ aLanguage.dump()
+ else:
+ results = aLanguage.generate(options.num, options.min, options.max)
+ for word in results:
+ print(word)
diff --git a/config/chroot_local-includes/usr/local/bin/pidgin b/config/chroot_local-includes/usr/local/bin/pidgin
new file mode 100755
index 0000000000000000000000000000000000000000..580632a130756e7e22dfcedbbde2ca9ffe7a975f
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/pidgin
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+# Start Pidgin with the GNOME integration disabled, so that the
+# "Global proxy configuration" is used, which we set to use Tor
+exec env GNOME_DESKTOP_SESSION_ID="" /usr/bin/pidgin "${@}"
diff --git a/config/chroot_local-includes/usr/local/bin/replace-su-with-sudo b/config/chroot_local-includes/usr/local/bin/replace-su-with-sudo
new file mode 100755
index 0000000000000000000000000000000000000000..33e7142cccbe0af1d7d91dec0412b96f2e90ecee
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/replace-su-with-sudo
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+
+# In Tails, the administration password doesn't work with 'su'. New
+# users in particular may be puzzled by the authentication failures
+# while trying to 'su' using administration password.
+#
+# This script is called by '/etc/bash.bashrc.d/replace-su-with-sudo.sh'.
+# It checks if the current user's password is set or not using
+# 'is_password_set()' function from 'adminpassword.py' library. If the
+# password is set, the user is asked to use 'sudo' instead of 'su'.
+# Otherwise, the user is asked to first set the administration password.
+#
+# https://redmine.tails.boum.org/code/issues/15583
+
+import gettext
+import sys
+from tailslib.adminpassword import is_password_set
+
+def main() -> None:
+ if is_password_set():
+ print(_('su is disabled. Please use sudo instead.'))
+ sys.exit(0)
+ else:
+ print(open("/usr/share/tails-greeter/no-password-lecture.txt").read())
+ sys.exit(1)
+
+if __name__ == "__main__":
+ # Initialize translations
+ translation = gettext.translation("tails", fallback=True)
+ _ = translation.gettext
+
+ main()
diff --git a/config/chroot_local-includes/usr/local/bin/tails-about b/config/chroot_local-includes/usr/local/bin/tails-about
new file mode 100755
index 0000000000000000000000000000000000000000..173b0f47b89c0613c944e7e49e4a6c0f02469684
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-about
@@ -0,0 +1,57 @@
+#!/usr/bin/python3
+# -*- encoding: UTF-8
+"""This scripts show a dialog with basic information about the
+running Tails installation.
+"""
+
+import gettext
+import subprocess
+
+from gi import require_version
+require_version('Gtk', '3.0')
+from gi.repository import GLib, Gtk, GdkPixbuf
+
+
+class AboutTails(Gtk.AboutDialog):
+ """A simple class showing the dialog"""
+ def __init__(self):
+ gettext.install("tails")
+ self.get_tails_version()
+ Gtk.AboutDialog.__init__(self)
+
+ self.set_program_name(_("Tails"))
+
+ headerbar = Gtk.HeaderBar()
+ headerbar.set_title(_("About Tails"))
+ headerbar.set_show_close_button(True)
+ self.set_titlebar(headerbar)
+
+ try:
+ self.set_logo(GdkPixbuf.Pixbuf.new_from_file_at_size(
+ '/usr/share/tails/tails-logo-flat-inverted.svg', 400, 200))
+ except GLib.GError:
+ pass
+
+ self.set_comments(_("The Amnesic Incognito Live System") + "\n\n" +
+ _("Build information:\n%s") % self.tails_version)
+ self.set_copyright("Tails developers")
+ self.set_version(self.tails_main_version)
+ self.set_website("https://tails.boum.org/")
+
+ self.connect("delete-event", Gtk.main_quit)
+ self.connect("response", Gtk.main_quit)
+ self.show_all()
+ Gtk.main()
+
+ def get_tails_version(self):
+ """Find out the tails_version and the tails_main_version"""
+ try:
+ self.tails_version = subprocess.Popen(
+ ["tails-version"], stdout=subprocess.PIPE).communicate()[0]
+ self.tails_version = self.tails_version.decode('utf-8')
+ self.tails_main_version = self.tails_version.split("-")[0]
+ except OSError:
+ self.tails_version = _("not available")
+ self.tails_main_version = ""
+
+AboutTails()
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 0000000000000000000000000000000000000000..41e003167379b4cb43669f7144df4db67cd46809
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-additional-software-config
@@ -0,0 +1,273 @@
+#!/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 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,
+ # Translators: Don't translate {package}, it's a placeholder and will be replaced.
+ _("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(
+ # Translators: Don't translate {pkg}, it's a placeholder and will be replaced.
+ _("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("{}".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(
+ # Translators: Don't translate {package}, it's a placeholder and will be replaced.
+ _("Stop installing {package} "
+ "automatically").format(package=package_name))
+ remove_button.get_accessible().set_name(_("Remove"))
+ 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 '
+ 'Synaptic Package Manager '
+ 'or APT on the '
+ 'command line.'))
+ else:
+ self.stack.set_visible_child(self.no_package_page)
+ self.install_label.set_markup(
+ _('To do so, install some software using '
+ 'Synaptic Package Manager '
+ 'or APT on the '
+ 'command line.'))
+ 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 '
+ 'Synaptic Package '
+ 'Manager or '
+ 'APT on the '
+ 'command line.'))
+ 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 '
+ 'Synaptic Package '
+ 'Manager or '
+ 'APT on the '
+ 'command line.'))
+ else: # It's impossible to have a persistent storage
+ self.install_label.set_markup(
+ _('To do so, install Tails on a USB stick using '
+ 'Tails Installer '
+ '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-delete-persistent-volume b/config/chroot_local-includes/usr/local/bin/tails-delete-persistent-volume
new file mode 100755
index 0000000000000000000000000000000000000000..1da2f5992d699146c0e20fb9f57800fdbdc84a8e
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-delete-persistent-volume
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -e
+
+RUN_AS_USER=tails-persistence-setup
+
+cd /
+xhost +SI:localuser:"$RUN_AS_USER"
+sudo -u "$RUN_AS_USER" /usr/bin/tails-persistence-setup --step delete $@
+xhost -SI:localuser:"$RUN_AS_USER"
diff --git a/config/chroot_local-includes/usr/local/bin/tails-documentation b/config/chroot_local-includes/usr/local/bin/tails-documentation
new file mode 100755
index 0000000000000000000000000000000000000000..c65c6100af131f5e3b52a9346421aa0b471a7862
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-documentation
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+
+import os
+import os.path
+import sys
+
+# Main
+
+try:
+ page = sys.argv[1]
+except IndexError:
+ page = 'doc'
+
+try:
+ anchor = sys.argv[2]
+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]
+
+# If possible, let's hand-off to our website, which should be the most
+# up-to-date option.
+if os.system('/usr/local/sbin/tor-has-bootstrapped') == 0:
+ if os.path.isfile(os.path.join(
+ wiki_path, page + '.' + lang_code + ".html")):
+ uri = tails_homepage + '/' + page + '/index.' + lang_code + '.html'
+ else:
+ 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')
+
+if anchor is not None:
+ uri = uri + '#' + anchor
+
+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/tails-get-bootinfo b/config/chroot_local-includes/usr/local/bin/tails-get-bootinfo
new file mode 100755
index 0000000000000000000000000000000000000000..a104b85a92b5b4198ee33a0ed8586c9d74b9b702
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-get-bootinfo
@@ -0,0 +1,57 @@
+#! /usr/bin/env python3
+"""
+Get Tails boot info.
+
+Test with "python3 tails-get-bootinfo.py doctest".
+
+goodcrypto.com converted from bash to python and added basic tests.
+
+>>> import sh
+>>> sh.Command(sys.argv[0])('kernel')
+/lib/live/mount/medium/live/vmlinuz
+>>> sh.Command(sys.argv[0])('initrd')
+/lib/live/mount/medium/live/initrd.img
+>>> sh.Command(sys.argv[0])(_ok_code=(1))
+Usage: tails-get-bootinfo kernel|initrd
+
+"""
+
+import sys
+
+LIVE_IMAGE_MOUNTPOINT = '/lib/live/mount/medium'
+
+
+def main(*args):
+ kernel = None
+ initrd = None
+
+ with open('/proc/cmdline') as f:
+ kernel_params = f.read().split()
+ for param in kernel_params:
+ if param.startswith('BOOT_IMAGE='):
+ kernel = param[len('BOOT_IMAGE='):]
+ elif param.startswith('initrd='):
+ initrd = param[len('initrd='):]
+
+ if not kernel or not initrd:
+ print('Failed to parse /proc/cmdline', file=sys.stderr)
+ sys.exit(1)
+
+ if 'kernel' in args:
+ print(LIVE_IMAGE_MOUNTPOINT + kernel, end="")
+ elif 'initrd' in args:
+ print(LIVE_IMAGE_MOUNTPOINT + initrd, end="")
+ else:
+ print('Usage: tails-get-bootinfo kernel|initrd', file=sys.stderr)
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ if len(sys.argv) > 1 and sys.argv[1] == 'doctest':
+ import doctest
+ doctest.testmod()
+ elif len(sys.argv) == 2:
+ main(*sys.argv[1:])
+ else:
+ print('Usage: tails-get-bootinfo kernel|initrd', file=sys.stderr)
+ sys.exit(1)
diff --git a/config/chroot_local-includes/usr/local/bin/tails-persistence-setup b/config/chroot_local-includes/usr/local/bin/tails-persistence-setup
new file mode 100755
index 0000000000000000000000000000000000000000..22777cada09cc44c5bc8c878b1a60d005ca2163b
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-persistence-setup
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -e
+
+RUN_AS_USER=tails-persistence-setup
+
+cd /
+xhost +SI:localuser:"$RUN_AS_USER"
+sudo -u "$RUN_AS_USER" /usr/bin/tails-persistence-setup $@
+xhost -SI:localuser:"$RUN_AS_USER"
diff --git a/config/chroot_local-includes/usr/local/bin/tails-screen-locker b/config/chroot_local-includes/usr/local/bin/tails-screen-locker
new file mode 100755
index 0000000000000000000000000000000000000000..42f7aadaa003d014cc451ee42591d660f25cb900
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-screen-locker
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+
+import logging
+import socket
+import sys
+import gettext
+import subprocess
+from pydbus import SessionBus, SystemBus
+import os
+from pam import pam
+import time
+import pwd
+
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk, GLib
+gi.require_version('Gdk', '3.0')
+from gi.repository import Gdk
+
+from tailslib.adminpassword import is_password_set
+
+_ = gettext.gettext
+gettext.textdomain("tails")
+
+logging.basicConfig(level=logging.DEBUG)
+
+mainloop = GLib.MainLoop()
+
+
+def lock_screen():
+ # org.gnome.ScreenSaver.Lock() sometimes does not return, so we set a timeout of 5 seconds
+ try:
+ SessionBus().get("org.gnome.ScreenSaver").Lock(timeout=5)
+ except Exception as e:
+ logging.exception(e)
+ finally:
+ sys.exit()
+
+
+class PasswordDialog(object):
+
+ def on_cancel_clicked(self, button, data=None):
+ sys.exit(1)
+
+ def on_entry_changed(self, entry, data=None):
+ if not self.entry1.get_text() or not self.entry2.get_text():
+ self.ok_button.set_sensitive(False)
+ elif self.entry1.get_text() == self.entry2.get_text():
+ # Passwords match
+ self.ok_button.set_sensitive(True)
+ self.entry2.set_icon_from_icon_name(1, None)
+ else:
+ # Passwords don't match
+ self.ok_button.set_sensitive(False)
+ self.entry2.set_icon_from_stock(1, "gtk-dialog-warning")
+
+ def on_ok_clicked(self, button, data=None):
+ pw1 = self.entry1.get_text()
+ pw2 = self.entry2.get_text()
+ if not pw1 == pw2:
+ return
+
+ self.pw = pw1.encode('utf8')
+
+ bus = SystemBus()
+ object_path = bus.get("org.freedesktop.Accounts").FindUserById(os.getuid())
+ user_object = bus.get("org.freedesktop.Accounts", object_path)
+ # lock the screen once the 'Changed' signal was received
+ user_object.Changed.connect(self.wait_until_password_set_and_lock_screen)
+
+ p = subprocess.Popen("passwd", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ p.stdin.write(self.pw + b"\n")
+ p.stdin.write(self.pw)
+ p.stdin.flush()
+ out, err = p.communicate()
+ if p.returncode != 0:
+ print("passwd stdout: %s" % out)
+ print("passwd stderr: %s" % err)
+ raise RuntimeError("passwd returned %r" % p.returncode)
+
+ # We close the window here for the case that lock_screen does not return immediately,
+ # otherwise it would look like the app is unresponsive
+ self.window.close()
+
+ def on_key_pressed(self, widget, event):
+ if Gdk.keyval_name(event.keyval) == "Escape":
+ sys.exit(1)
+
+ if self.ok_button.get_sensitive() and Gdk.keyval_name(event.keyval) == "Return":
+ self.ok_button.clicked()
+
+ def wait_until_password_set_and_lock_screen(self):
+ # TODO: Remove this once this is fixed: https://bugzilla.gnome.org/show_bug.cgi?id=761969
+ p = pam()
+ while not p.authenticate(pwd.getpwuid(os.getuid()).pw_name, self.pw):
+ logging.debug("PAM not updated yet...")
+ time.sleep(0.01)
+ # We close the window here for the case that lock_screen does not return immediately,
+ # otherwise it would look like the app is unresponsive
+ self.window.close()
+ GLib.idle_add(lock_screen)
+
+ def run(self):
+ self.window.show()
+
+ def __init__(self):
+ self.pw = None
+
+ self.ok_button = Gtk.Button(
+ label=_("Lock Screen"),
+ receives_default=True,
+ sensitive=False,
+ width_request=86
+ )
+ self.ok_button.connect("clicked", self.on_ok_clicked)
+ self.ok_button.get_style_context().add_class('suggested-action')
+
+ cancel_button = Gtk.Button(
+ label=_("Cancel"),
+ width_request=86
+ )
+ cancel_button.connect("clicked", self.on_cancel_clicked)
+
+ headerbar = Gtk.HeaderBar(
+ title=_("Screen Locker"),
+ )
+ headerbar.pack_start(cancel_button)
+ headerbar.pack_end(self.ok_button)
+
+ label_subtitle = Gtk.Label(
+ label=_("Set up a password to unlock the screen."),
+ )
+
+ self.entry1 = Gtk.Entry(
+ can_focus=True,
+ visibility=False,
+ width_request=200
+ )
+ self.entry1.connect("changed", self.on_entry_changed)
+
+ self.entry2 = Gtk.Entry(
+ can_focus=True,
+ visibility=False,
+ width_request=200
+ )
+ self.entry2.connect("changed", self.on_entry_changed)
+
+ grid = Gtk.Grid(row_spacing=2, column_spacing=10)
+ grid.attach(Gtk.Label(label=_("Password"), xalign=1), 0, 0, 1, 1)
+ grid.attach(Gtk.Label(label=_("Confirm"), xalign=1), 0, 1, 1, 1)
+ grid.attach(self.entry1, 1, 0, 1, 1)
+ grid.attach(self.entry2, 1, 1, 1, 1)
+
+ content_box = Gtk.Box(Gtk.Orientation.HORIZONTAL)
+ content_box.pack_start(Gtk.Box(hexpand=True), False, False, 0)
+ content_box.pack_end(Gtk.Box(hexpand=True), False, False, 0)
+ content_box.add(grid)
+
+ box = Gtk.Box(
+ orientation=Gtk.Orientation.VERTICAL,
+ margin_top=18,
+ margin_bottom=18,
+ margin_left=18,
+ margin_right=18,
+ spacing=18
+ )
+ box.add(label_subtitle)
+ box.add(content_box)
+
+ self.window = Gtk.Window(
+ type_hint=Gdk.WindowTypeHint.DIALOG,
+ )
+ self.window.connect("key-press-event", self.on_key_pressed)
+ self.window.set_titlebar(headerbar)
+ self.window.add(box)
+ self.window.show_all()
+
+
+def get_lock():
+ # Source: https://stackoverflow.com/a/7758075
+ # Original author: https://stackoverflow.com/users/639295/aychedee
+ # Without holding a reference to our socket somewhere it gets garbage
+ # collected when the function exits
+ get_lock._lock_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+ get_lock._lock_socket.bind('\0' + 'tails-screen-locker')
+
+
+def main():
+ get_lock()
+ if is_password_set():
+ lock_screen()
+ return
+
+ dialog = PasswordDialog()
+ GLib.idle_add(dialog.run)
+ mainloop.run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/config/chroot_local-includes/usr/local/bin/tails-security-check b/config/chroot_local-includes/usr/local/bin/tails-security-check
new file mode 100755
index 0000000000000000000000000000000000000000..ce86ee17e4628b950e2a61a4e7df737d71c3780b
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-security-check
@@ -0,0 +1,202 @@
+#! /usr/bin/perl
+
+use strict;
+use warnings FATAL => 'all';
+use 5.10.1;
+
+#man{{{
+
+=head1 NAME
+
+tails-security-check
+
+=cut
+
+
+=head1 DESCRIPTION
+
+=head1 SYNOPSIS
+
+tails-security-check [ ATOM_FEED_BASE_URL ]
+
+ ATOM_FEED_BASE_URL will be appended /index.XX.atom,
+ for XX in (current locale's language code, 'en'),
+ until success is reported by the HTTP layer.
+
+=head1 AUTHOR
+
+Tails developers
+See https://tails.boum.org/.
+
+=cut
+
+#}}}
+
+use Carp;
+use Carp::Assert::More;
+use Fatal qw{open close};
+use Locale::gettext;
+use POSIX;
+use Tails::Download::HTTPS;
+use Try::Tiny;
+use XML::Atom;
+use XML::Atom::Feed;
+
+setlocale(LC_MESSAGES, "");
+textdomain("tails");
+
+### configuration
+
+my $default_base_url = 'https://tails.boum.org/security/';
+
+=head1 FUNCTIONS
+
+=head2 current_lang
+
+Returns the two-letters language code of the current session.
+
+=cut
+sub current_lang {
+ my ($code) = ($ENV{LANG} =~ m/([a-z]{2}).*/);
+
+ return $code;
+}
+
+=head2 atom_str
+
+Argument: an Atom feed URL
+
+Returns the Atom's feed content on success, undef on failure.
+
+=cut
+sub atom_str {
+ my $url = shift;
+ assert_defined($url);
+
+ my $downloader = Tails::Download::HTTPS->new(
+ max_download_size => 256 * 2**10,
+ );
+ my $content;
+ try { $content = $downloader->get_url($url); };
+ defined $content ? return $content : return undef;
+}
+
+=head2 get_entries
+
+Arguments: the Atom feed URL.
+
+Returns the list of XML::Atom::Entry objects from the feed.
+
+We use this manual Accept-Language algorithm as the website
+layout does not allow us to use content negotiation.
+
+=cut
+sub get_entries {
+ my $base_url = shift;
+ assert_defined($base_url);
+ assert_nonblank($base_url);
+
+ my $separator = '';
+ $separator = '/' unless $base_url =~ m{/\z}xms;
+
+ my @try_urls = (
+ $base_url . $separator . 'index.' . current_lang() . '.atom',
+ $base_url . $separator . 'index.en.atom',
+ );
+
+ my $feed_str;
+ foreach my $url (@try_urls) {
+ last if ($feed_str = atom_str($url));
+ }
+ assert_defined($feed_str);
+
+ return XML::Atom::Feed->new(\$feed_str)->entries();
+}
+
+=head2 notify_user
+
+Notify the user about the Atom entries passed as arguments.
+
+=cut
+sub notify_user {
+ my @entries = @_;
+
+ my $body = gettext('This version of Tails has known security issues:') . "\n";
+
+ for (@entries) {
+ $body .= '• ' . '' . $_->title . '' . "\n";
+ }
+
+ say $body;
+
+ exec(
+ qw{/usr/bin/zenity --warning},
+ q{--title}, gettext('Known security issues'),
+ q{--text}, $body,
+ );
+}
+
+=head2 categories
+
+Return the list of categories of the input XML::Atom::Entry object.
+
+=cut
+sub categories {
+ my $entry = shift;
+ my $ns = XML::Atom::Namespace->new(
+ dc => 'http://purl.org/dc/elements/1.1/'
+ );
+ my @category = ($entry->can('categories'))
+ ? $entry->categories
+ : $entry->category;
+ @category
+ ? (map { $_->label || $_->term } @category)
+ : $entry->getlist($ns, 'subject');
+}
+
+=head2 is_not_fixed
+
+Returns true iff. the input XML::Atom::Entry object hasn't the
+security/fixed tag.
+
+=cut
+sub is_not_fixed {
+ my $entry = shift;
+ assert_isa($entry, 'XML::Atom::Entry');
+
+ ! grep { $_ eq 'security/fixed' } categories($entry);
+}
+
+=head2 unfixed_entries
+
+Filter the input list of XML::Atom::Entry objects to only keep entries
+that are not marked as fixed yet.
+
+=cut
+sub unfixed_entries {
+ my @entries = @_;
+
+ grep { is_not_fixed($_) } @entries;
+}
+
+
+=head1 MAIN
+
+=head2 parse command line args
+
+=cut
+my $base_url = shift || $default_base_url;
+my $opt_since = shift;
+
+
+=head2 do the work
+
+=cut
+my @unfixed_entries = unfixed_entries(get_entries($base_url));
+
+if (! @unfixed_entries) {
+ exit 0;
+}
+else {
+ notify_user(@unfixed_entries);
+}
diff --git a/config/chroot_local-includes/usr/local/bin/tails-upgrade-frontend-wrapper b/config/chroot_local-includes/usr/local/bin/tails-upgrade-frontend-wrapper
new file mode 100755
index 0000000000000000000000000000000000000000..e85bf6f5699a993d0bae3fd09712fe3d9bd85402
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-upgrade-frontend-wrapper
@@ -0,0 +1,106 @@
+#! /usr/bin/env python3
+"""
+Tails upgrade frontend wrapper.
+
+Test with "python3 tails-upgrade-frontend-wrapper.py doctest".
+The tests will start the upgrade process which could pop up a dialog box
+so you probably want to use a tester that handles user interaction or
+run the tests from the command line and answer prompts as needed.
+
+goodcrypto.com converted from bash to python and added basic tests.
+
+>>> # run this script (without waiting 30 seconds)
+>>> sh.Command(sys.argv[0])("--no-wait")
+
+"""
+
+import os
+import sys
+import time
+from gettext import gettext
+
+import sh
+import psutil
+
+os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
+os.environ['TEXTDOMAIN'] = 'tails'
+
+CMD = os.path.basename(sys.argv[0])
+TORDATE_DIR = '/run/tordate'
+TORDATE_DONE_FILE = '{}/done'.format(TORDATE_DIR)
+INOTIFY_TIMEOUT = 60
+MIN_AVAILABLE_MEMORY = (300 * 1024 * 1024) # In Bytes
+RUN_AS_USER = 'tails-upgrade-frontend'
+
+ERROR_MESSAGE = gettext('''\"Not enough memory available to check for upgrades.
+
+Make sure this system satisfies the requirements for running Tails.
+See file:///usr/share/doc/tails/website/doc/about/requirements.en.html
+
+Try to restart Tails to check for upgrades again.
+
+Or do a manual upgrade.
+See https://tails.boum.org/doc/first_steps/upgrade#manual\"''')
+
+
+def main(*args):
+ if "--no-wait" not in args:
+ time.sleep(30)
+ else:
+ args = (arg for arg in args if arg != "--no-wait")
+
+ check_free_memory(MIN_AVAILABLE_MEMORY)
+
+ # Go to a place where everyone, especially Archive::Tar::Wrapper called by
+ # tails-install-iuk, can chdir back after it has chdir'd elsewhere to do
+ # its job.
+ os.chdir('/')
+
+ os.execv(
+ "/bin/sh",
+ (
+ "/bin/sh", "-c",
+ "xhost +SI:localuser:{user};"
+ "sudo -u {user} /usr/bin/tails-upgrade-frontend {args};"
+ "xhost -SI:localuser:{user}".format(user=RUN_AS_USER, args=" ".join(args))
+ )
+ )
+
+
+def error(msg):
+ """Show error and exit."""
+ cli_text = '{}: {} {}'.format(CMD, gettext('error:'), msg)
+ dialog_text = '''{}\n\n{}'''.format(gettext('Error'), msg)
+ print(cli_text, file=sys.stderr)
+
+ sh.zenity('--error', '--title', "", '--text', dialog_text, _ok_code=[0,1,5])
+ sys.exit(1)
+
+
+def check_free_memory(min_available_memory):
+ """Check for enough free memory.
+
+ # 1 KiB should be available when running the doctest
+ >>> check_free_memory(1024)
+ # 1 TiB should not be available, an error prompt should be displayed
+ >>> try:
+ ... check_free_memory(1024*1024*1024*1024)
+ ... fail()
+ ... except SystemExit:
+ ... pass
+ """
+
+ available_memory = psutil.virtual_memory().available
+
+ if available_memory < min_available_memory:
+ print('Only {} Bytes memory available, while {} is needed'.format(
+ available_memory, min_available_memory), file=sys.stderr)
+ error(ERROR_MESSAGE)
+
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1 and sys.argv[1] == 'doctest':
+ import doctest
+ doctest.testmod()
+ else:
+ main(*sys.argv[1:])
diff --git a/config/chroot_local-includes/usr/local/bin/tails-version b/config/chroot_local-includes/usr/local/bin/tails-version
new file mode 100755
index 0000000000000000000000000000000000000000..1c4efd05d0856a123cefa4f069df779cc4e027de
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tails-version
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+cat /etc/amnesia/version
diff --git a/config/chroot_local-includes/usr/local/bin/thunderbird b/config/chroot_local-includes/usr/local/bin/thunderbird
new file mode 100755
index 0000000000000000000000000000000000000000..454ef58644763bcb4cfaec020cda5e368979424d
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/thunderbird
@@ -0,0 +1,89 @@
+#!/bin/sh
+
+set -e
+set -u
+set -x
+
+# Import set_mozilla_pref()
+. /usr/local/lib/tails-shell-library/tor-browser.sh
+
+THUNDERBIRD_CONFIG_DIR="${HOME}/.thunderbird"
+PROFILE="${THUNDERBIRD_CONFIG_DIR}/profile.default"
+
+thunderbird_config_is_persistent() {
+ [ "$(findmnt --noheadings --output SOURCE --target "${THUNDERBIRD_CONFIG_DIR}")" = "/dev/mapper/TailsData_unlocked[/thunderbird]" ]
+}
+
+configure_locale() {
+ # Thunderbird will set the locale based on the environment when
+ # this pref is empty, but will then save the result to this pref
+ # disabling this "guess" for the next time. We want Thunderbird to
+ # always match the locale of the Tails session.
+ set_mozilla_pref "${PROFILE}/prefs.js" \
+ "intl.locale.requested" \
+ '""' \
+ user_pref
+}
+
+disable_autocrypt() {
+ # Disable Autocrypt since it is not safe (#15923).
+ set_mozilla_pref "${PROFILE}/prefs.js" \
+ "mail.server.default.enableAutocrypt" \
+ "false" \
+ user_pref
+}
+
+configure_default_incoming_protocol() {
+ # For extensions.torbirdy.defaultprotocol, POP = 0, IMAP = 1
+ local default_protocol
+ if thunderbird_config_is_persistent; then
+ default_protocol=0
+ else
+ default_protocol=1
+ fi
+ set_mozilla_pref "${PROFILE}/prefs.js" \
+ "extensions.torbirdy.defaultprotocol" \
+ "${default_protocol}" \
+ user_pref
+}
+
+reconfigure_profile() {
+ mkdir -p "${PROFILE}"
+ configure_locale
+ disable_autocrypt
+ 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
+}
+
+thunderbird_profile_is_new() {
+ [ ! -f "${PROFILE}/extensions.json" ]
+}
+
+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() {
+ export GNOME_ACCESSIBILITY=1
+ unset SESSION_MANAGER
+ reconfigure_profile
+ exec /usr/bin/thunderbird --class "Thunderbird" -profile "${PROFILE}" "${@}"
+}
+
+start_thunderbird "${@}"
diff --git a/config/chroot_local-includes/usr/local/bin/tor-browser b/config/chroot_local-includes/usr/local/bin/tor-browser
new file mode 100755
index 0000000000000000000000000000000000000000..14a624530ee2873944f453919b9aafed25fbdd3a
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tor-browser
@@ -0,0 +1,77 @@
+#!/bin/sh
+
+# AppArmor Ux rules don't sanitize $PATH, which can lead to an
+# exploited application (that's allowed to run this script unconfined)
+# having this script run arbitrary code, violating that application's
+# confinement. Let's prevent that by setting PATH to a list of
+# directories where only root can write.
+export PATH='/usr/local/bin:/usr/bin:/bin'
+
+set -e
+set -u
+
+. gettext.sh
+TEXTDOMAIN="tails"
+export TEXTDOMAIN
+
+PROFILE="${HOME}/.tor-browser/profile.default"
+
+# Import exec_firefox() and configure_best_tor_browser_locale()
+. /usr/local/lib/tails-shell-library/tor-browser.sh
+
+# Get LIVE_USERNAME
+. /etc/live/config.d/username.conf
+
+# Allow Torbutton access to the control port filter (for new identity).
+# Setting a password is required, otherwise Torbutton attempts to
+# read the authentication cookie file instead, which fails.
+export TOR_CONTROL_HOST='127.0.0.1'
+export TOR_CONTROL_PORT='9051'
+export TOR_CONTROL_PASSWD='passwd'
+# Hide Torbutton's "Tor Network Settings..." context menu entry since
+# it doesn't work in Tails, and we deal with those configurations
+# strictly through Tor Launcher.
+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.real"; then
+ return
+ fi
+
+ local dialog_title="`gettext \"Tor is not ready\"`"
+ local dialog_text="`gettext \"Tor is not ready. Start Tor Browser anyway?\"`"
+ local dialog_start="`gettext \"Start Tor Browser\"`"
+ local dialog_cancel="`gettext \"Cancel\"`"
+ zenity --question --title "$dialog_title" --text="$dialog_text" \
+ --default-cancel --ok-label "$dialog_start" --cancel-label "$dialog_cancel"
+}
+
+start_browser() {
+ if [ ! -d "${PROFILE}" ]; then
+ /usr/local/lib/generate-tor-browser-profile
+ fi
+
+ TMPDIR="${PROFILE}/tmp"
+ 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.
+ configure_best_tor_browser_locale "${PROFILE}"
+
+ exec_firefox -allow-remote --class "Tor Browser" -profile "${PROFILE}" "${@}"
+}
+
+
+if /usr/local/sbin/tor-has-bootstrapped || ask_for_confirmation; then
+ # Torbutton 1.5.1+ uses those environment variables
+ export TOR_SOCKS_HOST='127.0.0.1'
+ export TOR_SOCKS_PORT='9150'
+
+ start_browser "${@}"
+fi
diff --git a/config/chroot_local-includes/usr/local/bin/tor-launcher b/config/chroot_local-includes/usr/local/bin/tor-launcher
new file mode 100755
index 0000000000000000000000000000000000000000..f2b01b3e0f97b9e38e1b232e0f049ca49e3c91a0
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/tor-launcher
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+set -e
+
+# Import:
+# - the TBB_EXT and TOR_LAUNCHER_INSTALL variables;
+# - the exec_unconfined_firefox() and configure_best_tor_launcher_locale()
+# functions.
+. /usr/local/lib/tails-shell-library/tor-browser.sh
+
+unset TOR_CONTROL_PASSWD
+unset TOR_FORCE_NET_CONFIG
+export TOR_CONFIGURE_ONLY=1
+export TOR_CONTROL_PORT=9051
+export TOR_CONTROL_COOKIE_AUTH_FILE=/run/tor/control.authcookie
+export TOR_HIDE_BROWSER_LOGO=1
+if echo "$@" | grep -qw -- --force-net-config; then
+ export TOR_FORCE_NET_CONFIG=1
+fi
+
+PROFILE="${HOME}/.tor-launcher/profile.default"
+if [ ! -d "${PROFILE}" ]; then
+ mkdir -p "${PROFILE}/extensions"
+ for ext in "${TBB_EXT}"/langpack-*.xpi; do
+ ln -s "${ext}" "${PROFILE}/extensions/"
+ done
+ configure_best_tor_launcher_locale "${PROFILE}"
+fi
+
+exec_unconfined_firefox \
+ -app "${TOR_LAUNCHER_INSTALL}/application.ini" \
+ -profile "${PROFILE}"
diff --git a/config/chroot_local-includes/usr/local/bin/totem b/config/chroot_local-includes/usr/local/bin/totem
new file mode 100755
index 0000000000000000000000000000000000000000..cb72b214f7c55618161ed6491bc8b6060d0fa0a5
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/totem
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec torsocks /usr/bin/totem "$@"
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 0000000000000000000000000000000000000000..c7299fff106964ac66393ccb0b60cab2270a2269
--- /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/bin/whois b/config/chroot_local-includes/usr/local/bin/whois
new file mode 100755
index 0000000000000000000000000000000000000000..0bfe673eed918c3f6fb82bebea183e3b348b688a
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/bin/whois
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec torsocks /usr/bin/whois "$@"
diff --git a/config/chroot_local-includes/usr/local/lib/add-GNOME-bookmarks b/config/chroot_local-includes/usr/local/lib/add-GNOME-bookmarks
new file mode 100755
index 0000000000000000000000000000000000000000..400820b4febf9af11bd6858ebd40e25d795f17c0
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/add-GNOME-bookmarks
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+set -e
+set -u
+
+# We're a no-op unless running as the default desktop user
+[ "$(/usr/bin/id -u)" = 1000 ] || exit 0
+
+. /usr/local/lib/tails-shell-library/tails-greeter.sh
+
+add_gtk_bookmark_for() {
+ local target
+ target=$(echo "$1" | sed 's, ,%20,g')
+
+ if [ $# -ge 2 ]; then
+ title="$2"
+ echo "file://$target $title" >> "${HOME}/.gtk-bookmarks"
+ else
+ echo "file://$target" >> "${HOME}/.gtk-bookmarks"
+ fi
+}
+
+add_gtk_bookmark_for "${HOME}/Tor Browser"
+
+if persistence_is_enabled_for "${HOME}/Persistent" ; then
+ add_gtk_bookmark_for "${HOME}/Persistent"
+
+ if persistence_is_enabled_read_write ; then
+ add_gtk_bookmark_for "${HOME}/Persistent/Tor Browser" \
+ "Tor Browser (persistent)"
+ fi
+fi
+
+for launcher in Report_an_error tails-documentation ; do
+ gio set "${HOME}/Desktop/${launcher}.desktop" metadata::trusted yes
+done
diff --git a/config/chroot_local-includes/usr/local/lib/boot-profile b/config/chroot_local-includes/usr/local/lib/boot-profile
new file mode 100755
index 0000000000000000000000000000000000000000..456873c156c18455c08d19ae682ea3c11dee61c2
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/boot-profile
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+
+from pyinotify import WatchManager, Notifier, \
+ ThreadedNotifier, ProcessEvent, IN_OPEN, IN_ACCESS, IN_CREATE, IN_MOVED_TO
+import re
+import sys
+import atexit
+from signal import signal, SIGTERM
+import os.path
+
+# Ignore files matching this regular expression
+IGNORE_RE = "^/(tmp|sys|proc|dev|live/cow)"
+
+# Remove the following prefix (except the last /) from all paths
+IGNORE_PREFIX="/lib/live/mount/rootfs/filesystem.squashfs/"
+
+class ProfileProcessor(ProcessEvent):
+ def __init__(self, profile_path):
+ self.priority = 32767
+ self.files = {}
+ self.ignored_files = {}
+ self.ignore_re = re.compile(IGNORE_RE)
+ self.profile_path = profile_path
+
+ def add_file(self, path):
+ if ' ' in path:
+ # Skip path with white spaces: mksquashfs -sort does not
+ # handle them! fscanf(fd, "%s %d", ...)
+ return
+ if path.startswith(IGNORE_PREFIX):
+ path = path[len(IGNORE_PREFIX)-1:]
+ if path not in self.files:
+ self.files[path] = self.priority
+ self.priority -= 1
+
+ def ignore_file(self, path):
+ if path.startswith(IGNORE_PREFIX):
+ path = path[len(IGNORE_PREFIX)-1:]
+ self.ignored_files[path] = None
+
+ def process_IN_OPEN(self, event):
+ if not event.dir:
+ self.add_file(event.pathname)
+
+ def process_IN_ACCESS(self, event):
+ if not event.dir:
+ self.add_file(event.pathname)
+
+ def process_IN_CREATE(self, event):
+ self.ignore_file(event.pathname)
+
+ def process_IN_MOVED_TO(self, event):
+ self.ignore_file(event.pathname)
+
+ def is_excluded(self, path):
+ if path.startswith(IGNORE_PREFIX):
+ path = path[len(IGNORE_PREFIX)-1:]
+ return self.ignore_re.match(path)
+
+ def end_profiling(self):
+ profile = open(self.profile_path, 'w')
+ priorities = {}
+ for path, priority in self.files.items():
+ if path not in self.ignored_files:
+ priorities[priority] = path
+ keys = list(priorities.keys())
+ keys.sort(reverse=True)
+ for key in keys:
+ profile.write("%-68s %s\n" % (priorities[key][1:], key))
+ profile.close()
+
+def main():
+ if len(sys.argv) < 2:
+ print("usage: %s " % sys.argv[0], file=sys.stderr)
+ sys.exit(0)
+
+ wm = WatchManager()
+ profiler = ProfileProcessor(sys.argv[1])
+
+ atexit.register(profiler.end_profiling)
+ signal(SIGTERM, lambda signum, stack_frame: sys.exit(0))
+
+ notifier = Notifier(wm, profiler)
+ wm.add_watch('/', IN_OPEN | IN_ACCESS | IN_CREATE | IN_MOVED_TO, rec=True, exclude_filter=profiler.is_excluded)
+ notifier.loop(daemonize=True, pid_file='/boot-profile.pid')
+
+if __name__ == '__main__':
+ main()
diff --git a/config/chroot_local-includes/usr/local/lib/create-tor-browser-directories b/config/chroot_local-includes/usr/local/lib/create-tor-browser-directories
new file mode 100755
index 0000000000000000000000000000000000000000..3e486a539dd5fdbd2937d16ed1e62f8a28d3ae7c
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/create-tor-browser-directories
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+set -e
+set -u
+
+# We're a no-op unless running as the default desktop user
+[ "$(/usr/bin/id -u)" = 1000 ] || exit 0
+
+TOR_BROWSER_AMNESIAC_DIR='/home/amnesia/Tor Browser'
+TOR_BROWSER_PERSISTENT_DIR='/home/amnesia/Persistent/Tor Browser'
+
+. /usr/local/lib/tails-shell-library/tails-greeter.sh
+
+install -d -o amnesia -g amnesia -m 0700 "$TOR_BROWSER_AMNESIAC_DIR"
+
+if persistence_is_enabled_for "${HOME}/Persistent" && \
+ persistence_is_enabled_read_write ; then
+ install -d -o amnesia -g amnesia -m 0700 "$TOR_BROWSER_PERSISTENT_DIR"
+fi
diff --git a/config/chroot_local-includes/usr/local/lib/do_not_ever_run_me b/config/chroot_local-includes/usr/local/lib/do_not_ever_run_me
new file mode 100755
index 0000000000000000000000000000000000000000..60c2d16a5febd2e9030f25624a6620291783115a
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/do_not_ever_run_me
@@ -0,0 +1,39 @@
+#!/bin/sh
+#
+# This script fully disables the iptables firewall, and thus the
+# transparent forwarding thru Tor of all non-local network
+# connections... which defeats the whole purpose of this OS, hence
+# this script's name.
+
+IPT=/sbin/iptables
+IP6T=/sbin/ip6tables
+
+[ -x "$IPT" ] || exit 67
+[ -x "$IP6T" ] || exit 68
+
+$IPT -P INPUT ACCEPT
+$IPT -P FORWARD ACCEPT
+$IPT -P OUTPUT ACCEPT
+
+$IPT -t nat -P PREROUTING ACCEPT
+$IPT -t nat -P POSTROUTING ACCEPT
+$IPT -t nat -P OUTPUT ACCEPT
+
+$IPT -t mangle -P PREROUTING ACCEPT
+$IPT -t mangle -P INPUT ACCEPT
+$IPT -t mangle -P FORWARD ACCEPT
+$IPT -t mangle -P OUTPUT ACCEPT
+$IPT -t mangle -P POSTROUTING ACCEPT
+
+$IPT -F
+$IPT -t nat -F
+$IPT -t mangle -F
+
+$IPT -X
+$IPT -t nat -X
+$IPT -t mangle -X
+
+$IP6T -F
+$IP6T -P INPUT ACCEPT
+$IP6T -P FORWARD ACCEPT
+$IP6T -P OUTPUT ACCEPT
diff --git a/config/chroot_local-includes/usr/local/lib/end-profile b/config/chroot_local-includes/usr/local/lib/end-profile
new file mode 100755
index 0000000000000000000000000000000000000000..4aee0c7c77bdcde2ad4728db08e2a475eecef791
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/end-profile
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -e
+
+test -e /boot-profile.pid || exit 0
+
+# Wait some time hoping Tor has bootstrapped and Tor Browser is started
+sleep 180
+
+sudo -n /usr/local/lib/kill-boot-profile
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
new file mode 100755
index 0000000000000000000000000000000000000000..1ded3836d97748dbb1aa447ff71ca09757444cf4
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/generate-tor-browser-profile
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+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
+ echo "A tor-browser profile already exists at: ${USER_PROFILE}" >&2
+ exit 1
+fi
+
+mkdir -p "${USER_PROFILE}"
+cp -a "${TBB_PROFILE}" "${USER_PROFILE}"/profile.default
diff --git a/config/chroot_local-includes/usr/local/lib/initramfs-pre-shutdown-hook b/config/chroot_local-includes/usr/local/lib/initramfs-pre-shutdown-hook
new file mode 100755
index 0000000000000000000000000000000000000000..607c257978488be4819218e36b13c32a9d838e08
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/initramfs-pre-shutdown-hook
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+# This script is installed by /usr/share/initramfs-tools/hooks/shutdown
+# into the initramfs, as /lib/systemd/system-shutdown/tails. It's run by the
+# copy of systemd-shutdown that runs (as /shutdown) from inside
+# the unpacked initramfs, immediately before executing the requested action
+# (halt/poweroff/reboot).
+
+set -x
+
+### Unmount relevant filesystems
+
+# Debugging
+mount
+
+# Otherwise aufs pseudo-links can't be cleaned and we cannot drop caches.
+# This may also help for tracking remaining mounts.
+mount -o remount,rw /proc
+
+# Otherwise we can't create new mountpoints in /mnt
+mount -o remount,rw /
+
+# Otherwise aufs pseudo-links can't be removed while unmounting /oldroot,
+# and we can't clean up the content of /mnt/live/overlay.
+mount -o remount,rw /oldroot/lib/live/mount/overlay
+
+# Move /oldroot/* mountpoints out of the way
+mkdir -p /mnt/live/overlay
+mount --move \
+ /oldroot/lib/live/mount/overlay \
+ /mnt/live/overlay
+mkdir -p /mnt/live/squashfs
+mount --move \
+ /oldroot/lib/live/mount/rootfs/filesystem.squashfs \
+ /mnt/live/squashfs
+mkdir -p /mnt/live/medium
+mount --move \
+ /oldroot/lib/live/mount/medium \
+ /mnt/live/medium
+
+# Finally, really unmount relevant filesystems
+umount /oldroot
+rm -rf /mnt/live/overlay/.w* /mnt/live/overlay/*
+umount /mnt/live/overlay
+
+# Debugging
+mount
+
+### Ensure any remaining disk cache is erased by Linux' memory poisoning
+echo 3 > /proc/sys/vm/drop_caches
+
+### Pause if the test suite wants us to
+if [ -e /tails_shutdown_debugging ] ; then
+ echo "Going to sleep 2 minutes. Happy dumping!"
+ sleep 120
+fi
diff --git a/config/chroot_local-includes/usr/local/lib/initramfs-restore b/config/chroot_local-includes/usr/local/lib/initramfs-restore
new file mode 100755
index 0000000000000000000000000000000000000000..7f006a9c2b9ee04e9e7ab0230d4d07dd92b6f31f
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/initramfs-restore
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+set -e
+set -u
+
+WORKDIR=$(/bin/mktemp -d)
+
+/usr/bin/unmkinitramfs \
+ "$(/usr/local/bin/tails-get-bootinfo initrd)" \
+ "$WORKDIR"
+
+# We should not need any kernel modules in there at shutdown time,
+# and they take 66% of the entire uncompressed initramfs size, so
+# let's save some RAM.
+/bin/rm -rf "$WORKDIR"/main/lib/modules
+
+/bin/mv "$WORKDIR"/main/* /run/initramfs/
+
+/bin/rm -rf "$WORKDIR"
diff --git a/config/chroot_local-includes/usr/local/lib/kill-boot-profile b/config/chroot_local-includes/usr/local/lib/kill-boot-profile
new file mode 100755
index 0000000000000000000000000000000000000000..3828f609e2b27149a16a0426916e59cdad7c1549
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/kill-boot-profile
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+sudo kill $(cat /boot-profile.pid)
diff --git a/config/chroot_local-includes/usr/local/lib/onion-grater b/config/chroot_local-includes/usr/local/lib/onion-grater
new file mode 100755
index 0000000000000000000000000000000000000000..b8369b91bc20dd9abbd5ec48a24ca284402e0de9
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/onion-grater
@@ -0,0 +1,731 @@
+#!/usr/bin/python3 -u
+
+# This filter proxy allows fine-grained access whitelists of commands
+# (and their argunents) and events on a per-application basis, stored
+# in:
+#
+# /etc/onion-grater.d/
+#
+# that are pretty self-explanatory as long as you understand the Tor
+# ControlPort language. The format is expressed in YAML where the
+# top-level is supposed to be a list, where each element is a
+# dictionary looking something like this:
+#
+# - name: blabla
+# apparmor-profiles:
+# - path_to_executable_if_that_is_the_name_of_the_apparmor_profile
+# # or
+# - explicit_apparmor_profile_name
+# ...
+# users:
+# - user
+# ...
+# hosts:
+# - host
+# ...
+# commands:
+# command:
+# - command_arg_rule
+# ...
+# ...
+# confs:
+# conf:
+# - conf_arg_rule
+# ...
+# ...
+# events:
+# event:
+# event_option: event_option_value
+# ...
+# ...
+#
+# `name` (optional) is a string which gives an internal name, useful
+# for debugging. When not given, filters will default to the name of
+# the file (excluding extension) they were read from (so there can be
+# duplicates!). It is advisable to define one filter per file, and
+# give helpful filenames instead of using this field.
+#
+# A filter is matched if for each of the relevant qualifiers at
+# least one of the elements match the client. For local (loopback)
+# clients the following qualifiers are relevant:
+#
+# * `apparmor-profiles`: a list of strings, each being the name
+# of the AppArmor profile applied to the binary or script of the client,
+# with `*` matching anything. While this matcher always works for binaries,
+# it only works for scripts with an enabled AppArmor profile (not
+# necessarily enforced, complain mode is good enough).
+#
+# * `users`: a list of strings, each describing the user of the
+# client with `*` matching anything.
+#
+# For remote (non-local) clients, the following qualifiers are
+# relevant:
+#
+# * hosts: a list of strings, each describing the IPv4 address
+# of the client with `*` matching anything.
+#
+# A filter can serve both local and remote clients by having
+# qualifiers of both types.
+#
+# `commands` (optional) is a list where each item is a dictionary with
+# the obligatory `pattern` key, which is a regular expression that is
+# matched against the full argument part of the command. The default
+# behavior is to just proxy the line through if matched, but it can be
+# altered with these keys:
+#
+# * `replacement`: this rewrites the arguments. The value is a Python
+# format string (str.format()) which will be given the match groups
+# from the match of `pattern`. The rewritten command is then proxied
+# without the need to match any rule. There are also some special
+# patterns that will be replaced as follows:
+#
+# - {client-address}: the client's IP address
+# - {client-port}: the client's port
+# - {server-address}: the server's IP address
+# - {server-port}: the server's (listening) port
+#
+# * `response`: a list of dictionaries, where the `pattern` and
+# `replacement` keys work exactly as for commands arguments, but now
+# for the response. Note that this means that the response is left
+# intact if `pattern` doesn't match it, and if many `pattern`:s
+# match, only the first one (in the order listed) will trigger a
+# replacement.
+#
+# If a simple regex (as string) is given, it is assumed to be the
+# `pattern` which allows a short-hand for this common type of rule.
+#
+# Note that to allow a command to be run without arguments, the empty
+# string must be explicitly given as a `pattern`. Hence, an empty
+# argument list does not allow any use of the command.
+#
+# `confs` (optional) is a dictionary, and it's just syntactic sugar to
+# generate GETCONF/SETCONF rules. If a key exists, GETCONF of the
+# keyname is allowed, and if it has a non-empty list as value, those
+# values are allowed to be set. The empty string means that resetting
+# it is allowed. This is very useful for applications that like to
+# SETCONF on multiple configurations at the same time.
+#
+# `events` (optional) is a dictionary where the key represents the
+# event. If a key exists the event is allowed. The value is another
+# dictionary of options:
+#
+# * `suppress`: a boolean determining whether we should just fool the
+# client that it has subscribed to the event (i.e. the client
+# request is not filtered) while we suppress them.
+#
+# * `response`: a dictionary, where the `pattern` and `replacement`
+# keys work exactly as for `response` for commands, but now for the
+# events.
+#
+# `restrict-stream-events` (optional) is a boolean, and if set any
+# STREAM events sent to the client (after it has subscribed to them)
+# will be restricted to those belonging to the client itself. This
+# option only works for local clients and will be unset for remote
+# clients.
+
+import argparse
+import fcntl
+import glob
+import ipaddress
+import os.path
+import psutil
+import re
+import socket
+import socketserver
+import stem
+import stem.control
+import stem.connection
+import struct
+import sys
+import textwrap
+import yaml
+
+DEFAULT_LISTEN_ADDRESS = 'localhost'
+DEFAULT_LISTEN_PORT = 9051
+DEFAULT_COOKIE_PATH = '/run/tor/control.authcookie'
+DEFAULT_CONTROL_SOCKET_PATH = '/run/tor/control'
+
+
+class NoRewriteMatch(RuntimeError):
+ """
+ Error when no matching rewrite rule was found but one was expected.
+ """
+ pass
+
+
+def log(msg):
+ print(msg, file=sys.stderr)
+ sys.stderr.flush()
+
+
+def pid_of_laddr(address):
+ try:
+ return next(conn for conn in psutil.net_connections()
+ if conn.laddr == address).pid
+ except StopIteration:
+ return None
+
+
+def apparmor_profile_of_pid(pid):
+ # Here we leverage AppArmor's in-kernel solution for determining
+ # the exact executable invoked. Looking at /proc/pid/exe when an
+ # interpreted script is running will just point to the
+ # interpreter's binary, which is not fine-grained enough, but
+ # AppArmor will be aware of which script is running for processes
+ # using one of its profiles. However, we fallback to /proc/pid/exe
+ # in case there is no AppArmor profile, so the only unsupported
+ # mode here is unconfined scripts.
+ enabled_aa_profile_re = r'^(.+) \((?:complain|enforce)\)$'
+ with open('/proc/{}/attr/current'.format(str(pid)), "rb") as fh:
+ aa_profile_status = str(fh.read().strip(), 'UTF-8')
+ apparmor_profile_match = re.match(enabled_aa_profile_re, aa_profile_status)
+ if apparmor_profile_match:
+ return apparmor_profile_match.group(1)
+ else:
+ return psutil.Process(pid).exe()
+
+
+def get_ip_address(ifname):
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ return socket.inet_ntoa(fcntl.ioctl(
+ s.fileno(),
+ 0x8915, # SIOCGIFADDR
+ struct.pack('256s', bytes(ifname[:15], 'utf-8'))
+ )[20:24])
+
+
+class FilteredControlPortProxySession:
+ """
+ Class used to deal with a single session, delegated from the handler
+ (FilteredControlPortProxyHandler). Its main job is proxying the traffic
+ between the client and the real control port, blocking or rewriting
+ it as dictated by the filter rule set.
+ """
+
+ # Limit the length of a line, to prevent DoS attacks trying to
+ # crash this filter proxy by sending infinitely long lines.
+ MAX_LINESIZE = 10*1024
+
+ def __init__(self, handler):
+ self.allowed_commands = handler.allowed_commands
+ self.allowed_events = handler.allowed_events
+ self.client_address = handler.client_address
+ self.client_pid = handler.client_pid
+ self.controller = handler.controller
+ self.debug_log = handler.debug_log
+ self.filter_name = handler.filter_name
+ self.restrict_stream_events = handler.restrict_stream_events
+ self.rfile = handler.rfile
+ self.server_address = handler.server_address
+ self.wfile = handler.wfile
+ self.client_streams = set()
+ self.subscribed_event_listeners = []
+
+ def debug_log_send(self, line):
+ if global_args.print_responses:
+ self.debug_log(line, format_multiline=True, sep=': <- ')
+
+ def debug_log_recv(self, line):
+ if global_args.print_requests:
+ self.debug_log(line, format_multiline=True, sep=': -> ')
+
+ def debug_log_rewrite(self, kind, old, new):
+ if kind not in ['command', 'received event', 'response'] or \
+ (kind == 'command' and not global_args.print_responses) or \
+ (kind in ['received event', 'response']
+ and not global_args.print_requests):
+ return
+ if new != old:
+ old = textwrap.indent(old.strip(), ' '*4)
+ new = textwrap.indent(new.strip(), ' '*4)
+ self.debug_log("rewrote {}:\n{}\nto:\n{}".format(kind, old, new),
+ format_multiline=False)
+
+ def respond(self, line, raw=False):
+ if line.isspace():
+ return
+ self.debug_log_send(line)
+ self.wfile.write(bytes(line, 'ascii'))
+ if not raw:
+ self.wfile.write(bytes("\r\n", 'ascii'))
+ self.wfile.flush()
+
+ def get_rule(self, cmd, arg_str):
+ allowed_args = self.allowed_commands.get(cmd, [])
+ return next((rule for rule in allowed_args
+ if re.match(rule['pattern'] + "$", arg_str)), None)
+
+ def proxy_line(self, line, args_rewriter=None, response_rewriter=None):
+ if args_rewriter:
+ new_line = args_rewriter(line)
+ self.debug_log_rewrite('command', line, new_line)
+ line = new_line
+ response = self.controller.msg(line.strip()).raw_content()
+ if response_rewriter:
+ new_response = response_rewriter(response)
+ self.debug_log_rewrite('response', response, new_response)
+ response = new_response
+ self.respond(response, raw=True)
+
+ def filter_line(self, line):
+ self.debug_log("command filtered: {}".format(line))
+ self.respond("510 Command filtered")
+
+ def rewrite_line(self, replacers, line):
+ builtin_replacers = {
+ 'client-address': self.client_address[0],
+ 'client-port': str(self.client_address[1]),
+ 'server-address': self.server_address[0],
+ 'server-port': str(self.server_address[1]),
+ }
+ terminator = ''
+ if line[-2:] == "\r\n":
+ terminator = "\r\n"
+ line = line[:-2]
+ for r in replacers:
+ match = re.match(r['pattern'] + "$", line)
+ if match:
+ return r['replacement'].format(
+ *match.groups(), **builtin_replacers
+ ) + terminator
+ raise NoRewriteMatch()
+
+ def rewrite_matched_line(self, replacers, line):
+ try:
+ return self.rewrite_line(replacers, line)
+ except NoRewriteMatch:
+ return line
+
+ def rewrite_matched_lines(self, replacers, lines):
+ split_lines = lines.strip().split("\r\n")
+ return "\r\n".join([self.rewrite_matched_line(replacers, line)
+ for line in split_lines]) + "\r\n"
+
+ def event_cb(self, event, event_rewriter=None):
+ if self.restrict_stream_events and \
+ isinstance(event, stem.response.events.StreamEvent) and \
+ not global_args.disable_filtering:
+ if event.id not in self.client_streams:
+ if event.status in [stem.StreamStatus.NEW,
+ stem.StreamStatus.NEWRESOLVE] and \
+ self.client_pid == pid_of_laddr((event.source_address,
+ event.source_port)):
+ self.client_streams.add(event.id)
+ else:
+ return
+ elif event.status in [stem.StreamStatus.FAILED,
+ stem.StreamStatus.CLOSED]:
+ self.client_streams.remove(event.id)
+ raw_event_content = event.raw_content()
+ if event_rewriter:
+ new_raw_event_content = event_rewriter(raw_event_content)
+ self.debug_log_rewrite(
+ 'received event', raw_event_content, new_raw_event_content
+ )
+ raw_event_content = new_raw_event_content
+ if raw_event_content.strip() == '':
+ return
+ self.respond(raw_event_content, raw=True)
+
+ def update_event_subscriptions(self, events):
+ for listener, event in self.subscribed_event_listeners:
+ if event not in events:
+ self.controller.remove_event_listener(listener)
+ self.subscribed_event_listeners.remove((listener, event))
+ if global_args.print_responses:
+ self.debug_log("unsubscribed from event '{}'".format(event))
+ for event in events:
+ if any(event == event_ for _, event_ in self.subscribed_event_listeners):
+ if global_args.print_responses:
+ self.debug_log("already subscribed to event '{}'"
+ .format(event))
+ continue
+ rule = self.allowed_events.get(event, {}) or {}
+ if not rule.get('suppress', False) or \
+ global_args.disable_filtering:
+ event_rewriter = None
+ if 'response' in rule:
+ replacers = rule['response']
+ def _event_rewriter(line):
+ return self.rewrite_matched_line(replacers, line)
+ event_rewriter = _event_rewriter
+ def _event_cb(event):
+ self.event_cb(event, event_rewriter=event_rewriter)
+ self.controller.add_event_listener(
+ _event_cb, getattr(stem.control.EventType, event)
+ )
+ self.subscribed_event_listeners.append((_event_cb, event))
+ if global_args.print_responses:
+ self.debug_log("subscribed to event '{}'".format(event))
+ else:
+ if global_args.print_responses:
+ self.debug_log("suppressed subscription to event '{}'"
+ .format(event))
+ self.respond("250 OK")
+
+ def handle(self):
+ while True:
+ binary_line = self.rfile.readline(self.MAX_LINESIZE)
+ if binary_line == b'':
+ # Deal with clients that close the socket without a QUIT.
+ break
+ line = str(binary_line, 'ascii')
+ if line.isspace():
+ self.debug_log('ignoring received empty (or whitespace-only) '
+ + 'line')
+ continue
+ match = re.match(
+ r'(?P\S+)(?P\s*)(?P[^\r\n]*)\r?\n$',
+ line
+ )
+ if not match:
+ self.debug_log("received bad line (escapes made explicit): " +
+ repr(line))
+ # Hopefully the next line is ok...
+ continue
+ self.debug_log_recv(line)
+ cmd = match.group('cmd')
+ cmd_arg_sep = match.group('cmd_arg_sep')
+ arg_str = match.group('arg_str')
+ args = arg_str.split()
+ cmd = cmd.upper()
+
+ if cmd == "PROTOCOLINFO":
+ # Stem calls PROTOCOLINFO before authenticating. Tell the
+ # client that there is no authentication.
+ self.respond("250-PROTOCOLINFO 1")
+ self.respond("250-AUTH METHODS=NULL")
+ self.respond("250-VERSION Tor=\"{}\""
+ .format(self.controller.get_version()))
+ self.respond("250 OK")
+
+ elif cmd == "AUTHENTICATE":
+ # We have already authenticated, and the filtered port is
+ # access-restricted according to our filter instead.
+ self.respond("250 OK")
+
+ elif cmd == "QUIT":
+ self.respond("250 closing connection")
+ break
+
+ elif cmd == "SETEVENTS":
+ # The control language doesn't care about case for
+ # the event type.
+ events = [event.upper() for event in args]
+ if not global_args.disable_filtering and \
+ any(event not in self.allowed_events for event in events):
+ self.filter_line(line)
+ else:
+ self.update_event_subscriptions(events)
+
+ else:
+ rule = self.get_rule(cmd, arg_str)
+ if rule is None and global_args.disable_filtering:
+ rule = {}
+ if rule is not None:
+ args_rewriter = None
+ response_rewriter = None
+
+ if 'response' in rule:
+ def _response_rewriter(lines):
+ return self.rewrite_matched_lines(rule['response'],
+ lines)
+ response_rewriter = _response_rewriter
+
+ if 'replacement' in rule:
+ def _args_rewriter(line):
+ # We also want to match the command in `line`
+ # and add it back to the replacement string.
+ # We make sure to keep the exact white spaces
+ # separating the command and arguments, to not
+ # rewrite the line unnecessarily.
+ prefix = cmd + cmd_arg_sep
+ replacer = {
+ 'pattern': prefix + rule['pattern'],
+ 'replacement': prefix + rule['replacement']
+ }
+ return self.rewrite_line([replacer], line)
+ args_rewriter = _args_rewriter
+
+ self.proxy_line(line, args_rewriter=args_rewriter,
+ response_rewriter=response_rewriter)
+ else:
+ self.filter_line(line)
+
+
+class FilteredControlPortProxyHandler(socketserver.StreamRequestHandler):
+ """
+ Class handing each control port connection and collecting information
+ about the origin and using it to find a matching filter rule set. It
+ then delegates the session handling (the actual filtering) to a
+ FilteredControlPortProxySession object.
+ """
+
+ def debug_log(self, line, format_multiline=False, sep=': '):
+ line = line.strip()
+ if format_multiline and "\n" in line:
+ sep += "(multi-line)\n"
+ line = textwrap.indent(line, ' '*4)
+ log(self.client_desc + sep + line)
+
+ def setup(self):
+ super(type(self), self).setup()
+ self.allowed_commands = {}
+ self.allowed_events = {}
+ self.client_desc = None
+ self.client_pid = None
+ self.client_streams = set()
+ self.controller = None
+ self.filter_name = None
+ self.filters = []
+ self.restrict_stream_events = False
+ self.server_address = self.server.server_address
+ self.subscribed_event_listeners = []
+ for filter_file in glob.glob('/etc/onion-grater.d/*.yml'):
+ try:
+ with open(filter_file, "rb") as fh:
+ filters = yaml.safe_load(fh.read())
+ name = re.sub(r'\.yml$', '', os.path.basename(filter_file))
+ for filter_ in filters:
+ if name not in filter_:
+ filter_['name'] = name
+ self.filters += filters
+ except (yaml.parser.ParserError, yaml.scanner.ScannerError) as err:
+ log("filter '{}' has bad YAML and was not loaded: {}"
+ .format(filter_file, str(err)))
+
+ def add_allowed_commands(self, commands):
+ for cmd in commands:
+ allowed_args = commands[cmd]
+ # An empty argument list allows nothing, but will
+ # make some code below easier than if it can be
+ # None as well.
+ if allowed_args is None:
+ allowed_args = []
+ for i in range(len(allowed_args)):
+ if isinstance(allowed_args[i], str):
+ allowed_args[i] = {'pattern': allowed_args[i]}
+ self.allowed_commands[cmd.upper()] = allowed_args
+
+ def add_allowed_confs_commands(self, confs):
+ combined_getconf_rule = {'pattern': "(" + "|".join([
+ key for key in confs]) + ")"}
+ setconf_reset_part = "\s*|\s*".join([
+ key for key in confs
+ if isinstance(confs[key], list) and '' in confs[key]]
+ )
+ setconf_assignment_part = "\s*|\s*".join([
+ "{}=({})".format(
+ key, "|".join(confs[key])
+ )
+ for key in confs
+ if isinstance(confs[key], list) and len(confs[key]) > 0])
+ setconf_parts = []
+ for part in [setconf_reset_part, setconf_assignment_part]:
+ if part and part != '':
+ setconf_parts.append(part)
+ combined_setconf_rule = {
+ 'pattern': "({})+".format("\s*|\s*".join(setconf_parts))
+ }
+ for cmd, rule in [('GETCONF', combined_getconf_rule),
+ ('SETCONF', combined_setconf_rule)]:
+ if rule['pattern'] != "()+":
+ if cmd not in self.allowed_commands:
+ self.allowed_commands[cmd] = []
+ self.allowed_commands[cmd].append(rule)
+
+ def add_allowed_events(self, events):
+ for event in events:
+ opts = events[event]
+ # Same as for the `commands` argument list, let's
+ # add an empty dict to simplify later code.
+ if opts is None:
+ opts = {}
+ self.allowed_events[event.upper()] = opts
+
+ def match_and_parse_filter(self, matchers):
+ matched_filters = [filter_ for filter_ in self.filters
+ if all(any(val == expected_val or val == '*'
+ for val in filter_.get(key, []))
+ for key, expected_val in matchers)]
+ if len(matched_filters) == 0:
+ return
+ elif len(matched_filters) > 1:
+ raise RuntimeError('multiple filters matched: ' +
+ ', '.join(matched_filters))
+ matched_filter = matched_filters[0]
+ self.filter_name = matched_filter['name']
+ commands = matched_filter.get('commands', {}) or {}
+ self.add_allowed_commands(commands)
+ confs = matched_filter.get('confs', {}) or {}
+ self.add_allowed_confs_commands(confs)
+ events = matched_filter.get('events', {}) or {}
+ self.add_allowed_events(events)
+ self.restrict_stream_events = bool(matched_filter.get(
+ 'restrict-stream-events', False
+ ))
+
+ def connect_to_real_control_port(self):
+ controller = stem.connection.connect(control_socket=global_args.control_socket_path)
+ stem.connection.authenticate_cookie(controller, cookie_path=global_args.control_cookie_path)
+ return controller
+
+ def handle(self):
+ client_host = self.client_address[0]
+ local_connection = ipaddress.ip_address(client_host).is_loopback
+ if local_connection:
+ self.client_pid = pid_of_laddr(self.client_address)
+ # Deal with the race between looking up the PID, and the
+ # client being killed before we find the PID.
+ if not self.client_pid:
+ return
+ client_apparmor_profile = apparmor_profile_of_pid(self.client_pid)
+ client_user = psutil.Process(self.client_pid).username()
+ matchers = [
+ ('apparmor-profiles', client_apparmor_profile),
+ ('users', client_user),
+ ]
+ else:
+ self.client_pid = None
+ matchers = [
+ ('hosts', client_host),
+ ]
+ self.match_and_parse_filter(matchers)
+ if local_connection:
+ self.client_desc = '{aa_profile} (pid: {pid}, user: {user}, ' \
+ 'port: {port}, filter: {filter_name})'.format(
+ aa_profile=client_apparmor_profile,
+ pid=self.client_pid,
+ user=client_user,
+ port=self.client_address[1],
+ filter_name=self.filter_name
+ )
+ else:
+ self.client_desc = '{1}:{2} (filter: {0})'.format(
+ self.filter_name, *self.client_address
+ )
+ if self.restrict_stream_events and not local_connection:
+ self.debug_log(
+ "filter '{}' has `restrict-stream-events` set "
+ "and we are remote so the option was disabled"
+ .format(self.filter_name)
+ )
+ self.restrict_stream_events = False
+
+ if len(self.filters) == 0:
+ status = 'no matching filter found, using an empty one'
+ else:
+ status = 'loaded filter: {}'.format(self.filter_name)
+ log('{} connected: {}'.format(self.client_desc, status))
+ if global_args.debug:
+ log('Final rules:')
+ log(yaml.dump({
+ 'commands': self.allowed_commands,
+ 'events': self.allowed_events,
+ 'restrict-stream-events': self.restrict_stream_events,
+ }))
+ disconnect_reason = "client quit"
+ try:
+ self.controller = self.connect_to_real_control_port()
+ session = FilteredControlPortProxySession(self)
+ session.handle()
+ except (ConnectionResetError, BrokenPipeError) as err:
+ # Handle clients disconnecting abruptly
+ disconnect_reason = str(err)
+ except stem.SocketError:
+ # Handle client closing its socket abruptly
+ disconnect_reason = "Client closed its socket"
+ except stem.SocketClosed:
+ # Handle Tor closing its socket abruptly
+ disconnect_reason = "Tor closed its socket"
+ finally:
+ if self.controller:
+ self.controller.close()
+ log('{} disconnected: {}'.format(self.client_desc,
+ disconnect_reason))
+
+
+class FilteredControlPortProxy(socketserver.ThreadingTCPServer):
+ """
+ Simple subclass just setting some defaults differently.
+ """
+
+ # So we can restart when the listening port if in TIME_WAIT state
+ # after an abrupt shutdown.
+ allow_reuse_address = True
+ # So all server threads immediately quit when the main thread
+ # quits.
+ daemon_threads = True
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--listen-address",
+ type=str, metavar='ADDR', default=DEFAULT_LISTEN_ADDRESS,
+ help="specifies the address on which the server listens " +
+ "(default: {})".format(DEFAULT_LISTEN_ADDRESS)
+ )
+ parser.add_argument(
+ "--listen-port",
+ type=int, metavar='PORT', default=DEFAULT_LISTEN_PORT,
+ help="specifies the port on which the server listens " +
+ "(default: {})".format(DEFAULT_LISTEN_PORT)
+ )
+ parser.add_argument(
+ "--listen-interface",
+ type=str, metavar='INTERFACE',
+ help="specifies the interface on which the server listens " +
+ "(default: NULL)"
+ )
+ parser.add_argument(
+ "--control-cookie-path",
+ type=str, metavar='PATH', default=DEFAULT_COOKIE_PATH,
+ help="specifies the path to Tor's control authentication cookie " +
+ "(default: {})".format(DEFAULT_COOKIE_PATH)
+ )
+ parser.add_argument(
+ "--control-socket-path",
+ type=str, metavar='PATH', default=DEFAULT_CONTROL_SOCKET_PATH,
+ help="specifies the path to Tor's control socket " +
+ "(default: {})".format(DEFAULT_CONTROL_SOCKET_PATH)
+ )
+ parser.add_argument(
+ "--complain",
+ action='store_true', default=False,
+ help="disables all filtering and just prints the commands sent " +
+ "by the client"
+ )
+ parser.add_argument(
+ "--debug",
+ action='store_true', default=False,
+ help="prints all requests and responses"
+ )
+ # We put the argparse results in the global scope since it's
+ # awkward to extend socketserver so additional data can be sent to
+ # the request handler, where we need access to the arguments.
+ global global_args
+ global_args = parser.parse_args()
+ # Deal with overlapping functionality between arguments
+ global_args.__dict__['disable_filtering'] = global_args.complain
+ global_args.__dict__['print_requests'] = global_args.complain or \
+ global_args.debug
+ global_args.__dict__['print_responses'] = global_args.debug
+ if global_args.listen_interface:
+ ip_address = get_ip_address(global_args.listen_interface)
+ if global_args.debug:
+ log("IP address for interface {} : {}".format(
+ global_args.listen_interface,ip_address))
+ else:
+ ip_address = global_args.listen_address
+ address = (ip_address, global_args.listen_port)
+ server = FilteredControlPortProxy(address, FilteredControlPortProxyHandler)
+ log("Tor control port filter started, listening on {}:{}".format(*address))
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ pass
+
+
+if __name__ == "__main__":
+ main()
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 0000000000000000000000000000000000000000..50f48c271b3703b6209421989099b53efbba19bc
--- /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 0000000000000000000000000000000000000000..6d025c3c840b3064db5c97b730c2b63e972fe510
--- /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/tails/%s/" % APP_NAME
+MAIN_UI_FILE = path.join(DATA_DIR, "main.ui")
+VOLUME_UI_FILE = path.join(DATA_DIR, "volume.ui")
+TRANSLATION_DOMAIN = "tails"
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 0000000000000000000000000000000000000000..01a728d8a00c894eec7297497be925ebbb487344
--- /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 0000000000000000000000000000000000000000..417bc79faaad63ee041514342743bfb32c387f5b
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume.py
@@ -0,0 +1,370 @@
+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 TRANSLATION_DOMAIN, VOLUME_UI_FILE
+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(TRANSLATION_DOMAIN)
+ 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:
+ # Translators: Don't translate {volume_label} or {volume_size},
+ # they are placeholders and will be replaced.
+ return _("{volume_label} ({volume_size})").format(volume_label=block_label,
+ volume_size=self.size_for_display)
+ elif partition and partition.props.name:
+ # Translators: Don't translate {partition_name} or {partition_size},
+ # they are placeholders and will be replaced.
+ return _("{partition_name} ({partition_size})").format(partition_name=partition.props.name,
+ partition_size=self.size_for_display)
+ else:
+ # Translators: Don't translate {volume_size}, it's a placeholder
+ # and will be replaced.
+ 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")
+
+ # Translators: Don't translate {volume_name} or {error_message},
+ # they are placeholder and will be replaced.
+ body = _("Couldn't unlock volume {volume_name}:\n{error_message}".format(volume_name=self.name, error_message=e.message))
+ self.manager.show_warning(title, body)
+ return
+ finally:
+ self.manager.mount_op_lock.release()
+
+ if open_after_unlock:
+ # The GVolume now changed from the loop device to the dm device, so
+ # by also updating the udisks object we change this volume from the
+ # crypto backing loop device to the unlocked device-mapper device,
+ # which we can then open
+ self.udisks_object = self._find_udisks_object()
+ 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)
+ unmounted_at_least_once = False
+ 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
+ unmounted_at_least_once = True
+ except GLib.Error as e:
+ # Ignore "not mounted" error if the volume was already unmounted
+ if "org.freedesktop.UDisks2.Error.NotMounted" in e.message and unmounted_at_least_once:
+ 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
+ try:
+ self.unmount()
+ self.backing_volume.lock()
+ except GLib.Error as e:
+ # Translators: Don't translate {volume_name} or {error_message},
+ # they are placeholder and will be replaced.
+ body = _("Couldn't lock volume {volume_name}:\n{error_message}".format(volume_name=self.name,
+ error_message=e.message))
+ self.manager.show_warning(_("Error locking volume"), body)
+ return
+
+ 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 0000000000000000000000000000000000000000..12404d505bb9c4dd1abb21d46e603dacf1cb7af2
--- /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 0000000000000000000000000000000000000000..5a1e3e39e8a4768618ff61ef5cc32873850f56e7
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/python3/dist-packages/unlock_veracrypt_volumes/volume_manager.py
@@ -0,0 +1,268 @@
+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 MAIN_UI_FILE, TRANSLATION_DOMAIN
+
+
+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(TRANSLATION_DOMAIN)
+ 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")
+ # Translators: Don't translate {path}, it's a placeholder and will be replaced.
+ 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.\n"
+ "Please try using the Disks 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)
+ # Make the body selectable to allow users to easily copy/paste the error message
+ dialog.get_message_area().get_children()[-1].set_selectable(True)
+
+ 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/start-systemd-desktop-target b/config/chroot_local-includes/usr/local/lib/start-systemd-desktop-target
new file mode 100755
index 0000000000000000000000000000000000000000..f4905b461401246a0475cbeb546666a28e1eb2bf
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/start-systemd-desktop-target
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+set -u
+
+# XXX: check if we still need that in Stretch
+
+# Import (almost all) XDG_*, locale-related and DBUS_SESSION_BUS_ADDRESS variables
+# into the systemd user instance's environment. We're filtering some
+# XDG_* out in order not to pretend that processes run via `systemd --user`
+# are part of the desktop session.
+/usr/bin/env \
+ | /bin/grep '^XDG_' \
+ | /bin/grep -E -v '^XDG_(SEAT=|SESSION_)' \
+ | /usr/bin/xargs /bin/systemctl --user set-environment
+/usr/bin/locale | /usr/bin/xargs /bin/systemctl --user set-environment
+/bin/systemctl --user import-environment \
+ DBUS_SESSION_BUS_ADDRESS \
+ DISPLAY \
+ XAUTHORITY
+
+# Start desktop.target
+/bin/systemctl --user start desktop.target
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 0000000000000000000000000000000000000000..90b36b9317293946b0ac3d90ca9a649ce59e870e
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-additional-software-notify
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+
+import gettext
+import os
+import os.path
+import subprocess
+import sys
+
+from tailslib.gnome import gnome_env_vars
+
+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(
+ ["env", *gnome_env_vars(), "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 [ [ "
+ "[documentation_target []]]]\n" % program_name)
+ sys.stderr.write(
+ "Shows a notification with , and optional "
+ "buttons.\n"
+ "\n"
+ "Returns: 0 if the button with is selected\n"
+ " 2 if the arguments are wrong\n"
+ " 3 if the button with 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-autotest-remote-shell b/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell
new file mode 100755
index 0000000000000000000000000000000000000000..8660cd6905c4ffcfc66eb651f1bafbbbf70ba049
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-autotest-remote-shell
@@ -0,0 +1,191 @@
+#!/usr/bin/python3
+
+# ATTENTION: Yes, this can be used as a backdoor, but only for an
+# adversary with access to you *physical* serial port, which means
+# that you are screwed any way.
+
+import base64
+import fcntl
+import io
+import json
+import os
+import pwd
+import serial
+import signal
+import subprocess
+import sys
+import systemd.daemon
+import textwrap
+import traceback
+
+REMOTE_SHELL_DEV = '/dev/ttyS0'
+
+
+def mk_switch_user_fn(user):
+ pwd_user = pwd.getpwnam(user)
+ def switch_user():
+ os.initgroups(user, pwd_user.pw_gid)
+ os.setgid(pwd_user.pw_gid)
+ os.setuid(pwd_user.pw_uid)
+ return switch_user
+
+
+def get_user_env(user):
+ # We try to create an environment identical to what's expected
+ # inside Tails for the user by logging in (via `su`) as the user,
+ # setting up the GNOME shell environment, and extracting the
+ # environment via `env`; not that we will run `env` unconditionally
+ # since the former command could fail, e.g. if GNOME is not running.
+ env_cmd = '. /usr/local/lib/tails-shell-library/gnome.sh && ' + \
+ 'export_gnome_env ; ' + \
+ 'env'
+ wrapped_env_cmd = "su -c '{}' {}".format(env_cmd, user)
+ pipe = subprocess.Popen(wrapped_env_cmd, stdout=subprocess.PIPE, shell=True)
+ env_data = pipe.communicate()[0].decode('utf-8')
+ return dict((line.split('=', 1) for line in env_data.splitlines()))
+
+
+# Dogtail does not seem to support the root user interacting with
+# other users' applications, and it does not support Python 3 (which
+# this script is written in) so let's wrap around an interactive
+# Python shell started as a subprocess.
+class PythonSession:
+ def __init__(self, user = None):
+ interactive_shell_code = '; '.join([
+ "import sys",
+ "import code",
+ "sys.ps1 = ''",
+ "sys.ps2 = ''",
+ "code.interact(banner='')",
+ ])
+ if not user:
+ user = pwd.getpwuid(os.getuid()).pw_name
+ env = get_user_env(user)
+ cwd = env['HOME']
+ self.process = subprocess.Popen(
+ ["python2", "-u", "-c", interactive_shell_code],
+ bufsize = 0,
+ shell=False,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=env,
+ cwd=cwd,
+ preexec_fn=mk_switch_user_fn(user)
+ )
+ init_code = """
+ import cStringIO
+ import json
+ import sys
+ orig_stdout = sys.stdout
+ orig_stderr = sys.stderr
+ """.replace(' ', '').lstrip()
+ self.process.stdin.write(init_code.encode())
+ self.process.stdin.flush()
+
+ def execute(self, code):
+ # This wrapping ensures that we can run almost any reasonable
+ # code and capture what it does.
+ wrapper = """
+ fake_stdout = cStringIO.StringIO()
+ fake_stderr = cStringIO.StringIO()
+ sys.stdout = fake_stdout
+ sys.stderr = fake_stderr
+ exc = None
+ try:
+ {code}
+ except Exception as e:
+ exc = '%s: %s' % (type(e).__name__, str(e))
+ # This newline is needed to end the `try` statement.
+
+ sys.stdout = orig_stdout
+ sys.stderr = orig_stderr
+ out_data = fake_stdout.getvalue()
+ err_data = fake_stderr.getvalue()
+ fake_stdout.close()
+ fake_stderr.close()
+ print(json.dumps([exc, out_data, err_data]))
+ """.replace(' ', '').lstrip()
+ indented_code = textwrap.indent(code, prefix=' '*4)
+ wrapped_code = wrapper.format(code=indented_code)
+ self.process.stdin.write(wrapped_code.encode())
+ self.process.stdin.flush()
+ return str(self.process.stdout.readline().strip(), 'utf-8')
+
+
+def run_cmd_as_user(cmd, user):
+ switch_user_fn = mk_switch_user_fn(user)
+ env = get_user_env(user)
+ cwd = env['HOME']
+ return subprocess.Popen(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ shell=True, env=env, cwd=cwd, preexec_fn=switch_user_fn
+ )
+
+
+def main():
+ port = serial.Serial(port = REMOTE_SHELL_DEV, baudrate = 4000000)
+ python_sessions = dict()
+
+ # Notify systemd that we're ready
+ systemd.daemon.notify('READY=1')
+ systemd.daemon.notify('STATUS=Processing requests...\n')
+
+ while True:
+ line = port.readline().decode('utf-8')
+ try:
+ id, cmd_type, *rest = json.loads(line)
+ ret = ""
+ if cmd_type in ['sh_call', 'sh_spawn']:
+ user, cmd = rest
+ p = run_cmd_as_user(cmd, user)
+ if cmd_type == "sh_spawn":
+ returncode, stdout, stderr = 0, "", ""
+ else:
+ stdout_b, stderr_b = p.communicate()
+ stdout = stdout_b.decode('utf-8')
+ stderr = stderr_b.decode('utf-8')
+ returncode = p.returncode
+ ret = json.dumps([id, 'success', returncode, stdout, stderr])
+ elif cmd_type == 'python_execute':
+ user, code = rest
+ if user not in python_sessions:
+ python_sessions[user] = PythonSession(user)
+ session = python_sessions[user]
+ result_str = session.execute(code)
+ result = json.loads(result_str)
+ ret = json.dumps([id, 'success'] + result)
+ elif cmd_type in ['file_read', 'file_write', 'file_append']:
+ path, *rest = rest
+ open_mode = cmd_type[5] + 'b'
+ with open(path, open_mode) as f:
+ if cmd_type == 'file_read':
+ assert(rest == [])
+ ret = str(base64.b64encode(f.read()), 'utf-8')
+ elif cmd_type in ['file_write', 'file_append']:
+ assert(len(rest) == 1)
+ data = base64.b64decode(rest[0])
+ ret = f.write(data)
+ if ret != len(data):
+ raise IOError("we only wrote {} bytes out of {}"
+ .format(ret, len(data)))
+ ret = json.dumps([id, 'success'] + [ret])
+ else:
+ raise ValueError("unknown command type")
+ response = (ret + "\n").encode('utf-8')
+ port.write(response)
+ port.flush()
+ except Exception as e:
+ print("Error caught while processing line:", file=sys.stderr)
+ print(" " + line, file=sys.stderr)
+ print("The error was:", file=sys.stderr)
+ traceback.print_exc(file=sys.stdout)
+ print("-----", file=sys.stderr)
+ sys.stderr.flush()
+ exc_str = '{}: {}'.format(type(e).__name__, str(e))
+ port.write(json.dumps([id, 'error', exc_str]).encode('utf-8') + b"\n")
+ port.flush()
+ continue
+
+if __name__ == "__main__":
+ main()
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 0000000000000000000000000000000000000000..4ff759f99584340e867928579d223c489df84eb5
--- /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-configure-keyboard b/config/chroot_local-includes/usr/local/lib/tails-configure-keyboard
new file mode 100755
index 0000000000000000000000000000000000000000..c25119c42b93c807604f8292762499c2ae75b79d
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-configure-keyboard
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+set -e
+set -u
+
+# Get $XKBMODEL, $XKBLAYOUT, $XKBVARIANT and $XKBOPTIONS
+. /var/lib/tails-user-session/keyboard
+
+if [ -z "${XKBVARIANT:-}" ] ; then
+ XKBCONF="$XKBLAYOUT"
+else
+ XKBCONF="$XKBLAYOUT+$XKBVARIANT"
+fi
+
+# Choose the keyboard layout we'll use regardless of the IBus input methods
+if [ "$XKBLAYOUT" = 'us' ] ; then
+ SOURCES="('xkb', '$XKBCONF')"
+else
+ SOURCES="('xkb', '$XKBCONF'), ('xkb', 'us')"
+fi
+
+# Choose preferred IBus input methods
+LANGPREFIX=`echo "$LANG" | sed 's/_.*//'`
+case "$LANGPREFIX" in
+ ja)
+ PRELOAD="['anthy', 'libpinyin', 'hangul', 'Unikey', 'chewing']"
+ SOURCES="[$SOURCES, ('ibus', 'anthy'), ('ibus', 'libpinyin'), ('ibus', 'chewing'), ('ibus', 'hangul'), ('ibus', 'Unikey')]"
+ ;;
+ ko)
+ PRELOAD="['hangul', 'libpinyin', 'anthy', 'Unikey', 'chewing']"
+ SOURCES="[$SOURCES, ('ibus', 'hangul'), ('ibus', 'libpinyin'), ('ibus', 'chewing'), ('ibus', 'anthy'), ('ibus', 'Unikey')]"
+ ;;
+ vi)
+ PRELOAD="['Unikey', 'hangul', 'libpinyin', 'anthy', 'chewing']"
+ SOURCES="[$SOURCES, ('ibus', 'Unikey'), ('ibus', 'hangul'), ('ibus', 'libpinyin'), ('ibus', 'chewing'), ('ibus', 'anthy')]"
+ ;;
+ zh)
+ PRELOAD="['libpinyin', 'chewing', 'anthy', 'hangul', 'Unikey']"
+ SOURCES="[$SOURCES, ('ibus', 'libpinyin'), ('ibus', 'chewing'), ('ibus', 'anthy'), ('ibus', 'hangul'), ('ibus', 'Unikey')]"
+ ;;
+ *)
+ PRELOAD="['libpinyin', 'anthy', 'hangul', 'Unikey', 'chewing']"
+ SOURCES="[$SOURCES, ('ibus', 'libpinyin'), ('ibus', 'anthy'), ('ibus', 'hangul'), ('ibus', 'Unikey'), ('ibus', 'chewing')]"
+ ;;
+esac
+
+# Configure enabled input methods and their preferred order
+dconf write /desktop/ibus/general/preload-engines "$PRELOAD"
+dconf write /org/gnome/desktop/input-sources/sources "$SOURCES"
+if [ -n "${XKBOPTIONS:-}" ] ; then
+ dconf write /org/gnome/desktop/input-sources/xkb-options "$XKBOPTIONS"
+fi
+
+# Export environment variables to enable use of IBus
+export GTK_IM_MODULE='ibus'
+export QT_IM_MODULE='ibus'
+export XMODIFIERS='@im=ibus'
diff --git a/config/chroot_local-includes/usr/local/lib/tails-htp-notify-user b/config/chroot_local-includes/usr/local/lib/tails-htp-notify-user
new file mode 100755
index 0000000000000000000000000000000000000000..afa90d6c2f65fbbc79db8e8ed05ba556b0509f35
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-htp-notify-user
@@ -0,0 +1,93 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use 5.10.1;
+
+#man{{{
+
+=head1 NAME
+
+tails-htp-notify-user
+
+=head1 VERSION
+
+Version X.XX
+
+=head1 AUTHOR
+
+Tails dev team
+See https://tails.boum.org/.
+
+=cut
+
+#}}}
+
+use Data::Dumper;
+use Desktop::Notify;
+use English '-no_match_vars';
+use Locale::gettext;
+use POSIX;
+
+### initialization
+setlocale(LC_MESSAGES, "");
+textdomain("tails");
+my $htp_done_file = '/run/htpdate/done';
+my $htp_success_file = '/run/htpdate/success';
+my $htp_log_file = '/var/log/htpdate.log';
+my $debug;
+
+### subroutines
+
+sub debug { say STDERR $_[0] if $debug; }
+
+### main
+
+exit 0 if -e $htp_success_file;
+
+my $notify = Desktop::Notify->new()
+ or die "Failed creating Desktop::Notify object.";
+debug('$notify:' . "\n" . Dumper($notify));
+
+my $summary = gettext("Synchronizing the system's clock");
+my $body = gettext("Tor needs an accurate clock to work properly, especially for Hidden Services. Please wait...");
+
+my $notification = $notify->create(summary => $summary,
+ body => $body,
+ timeout => 0)
+ or die "Failed to create notification object";
+debug('$notification:' . "\n" . Dumper($notification));
+
+# Wait until notifications can be shown
+until (system("pidof", "ibus-daemon") == 0) {
+ sleep 1
+}
+
+$notification->show() or warn "Failed showing notification.";
+
+# Wait until htpdate is done
+until ( -e $htp_done_file ) {
+ sleep 1;
+}
+
+$notification->close();
+
+# in case htpdate failed, notify the user with the corresponding logs
+unless (-e $htp_success_file) {
+ open(my $htp_log, '<', $htp_log_file)
+ or die "Can not open file '$htp_log_file': $OS_ERROR";
+ my $last_log;
+ while (<$htp_log>) {
+ if ($_ =~ /^Running htpdate\./) {
+ $last_log = '';
+ next;
+ }
+ $last_log .= $_;
+ }
+ my $failure_summary = gettext("Failed to synchronize the clock!");
+ my $failure_body = $last_log;
+ my $failure_notification = $notify->create(summary => $failure_summary,
+ body => $failure_body,
+ timeout => 0);
+ $failure_notification->show();
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-set-wireless-devices-state b/config/chroot_local-includes/usr/local/lib/tails-set-wireless-devices-state
new file mode 100755
index 0000000000000000000000000000000000000000..bf23d2e3ebaf6c06e036b46b8b77a71b6713681a
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-set-wireless-devices-state
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+set -e
+
+[ -e /dev/rfkill ] || exit 0
+
+rfkill block all
+
+for devtype in wifi wwan wimax ; do
+ rfkill unblock "$devtype"
+done
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/build.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/build.sh
new file mode 100644
index 0000000000000000000000000000000000000000..baaf5468a2709c343ed0c8375325f44ba9627482
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/build.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+
+# Import is_package_installed
+. /usr/local/lib/tails-shell-library/common.sh
+
+strip_nondeterminism_wrapper() {
+ apt-get --yes install strip-nondeterminism
+ strip-nondeterminism "${@}"
+ apt-get --yes purge strip-nondeterminism '^libfile-stripnondeterminism-perl'
+}
+
+# Ensure that the packages whose names are passed as arguments are
+# installed. If they are installed now, they will be marked as
+# "automatically installed" so the next `apt-get autoremove` action
+# *unless* they are later explicitly installed (or other packages
+# depends on them).
+ensure_hook_dependency_is_installed() {
+ # Filter out already installed packages from $@.
+ for p in "${@}"; do
+ shift
+ if ! echo "${p}" | grep -q --extended-regexp '^[a-z0-9.+-]+$'; then
+ echo "ensure_hook_dependency_is_installed():" \
+ "doesn't look like a package name: ${p}" >&2
+ exit 1
+ fi
+ if is_package_installed "${p}"; then
+ continue
+ fi
+ set -- "${@}" "${p}"
+ done
+ if [ -z "${*}" ]; then
+ return
+ fi
+ apt-get install --yes "${@}"
+ apt-mark auto "${@}"
+}
+
+install_fake_package() {
+ local name version section provides tmp control_file
+ name="${1}"
+ version="${2}"
+ section="${3:-misc}"
+ provides="${4:-}"
+ ensure_hook_dependency_is_installed equivs
+ tmp="$(mktemp -d)"
+ control_file="${tmp}/${name}_${version}.control"
+ cat > "${control_file}" << EOF
+Section: ${section}
+Priority: optional
+Homepage: https://tails.boum.org/
+Standards-Version: 3.9.6
+
+Package: ${name}
+Version: ${version}
+Maintainer: Tails developers
+Architecture: all
+Provides: ${provides}
+Description: (Fake) ${name}
+ Dummy packaged used to meet some dependency without installing the
+ real ${name} package.
+EOF
+ (
+ cd "${tmp}"
+ equivs-build "${control_file}"
+ dpkg -i "${tmp}/${name}_${version}_all.deb"
+ )
+ rm -R "${tmp}"
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..3deed22908a7eaadee739b5abd7224671cc34c0e
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/chroot-browser.sh
@@ -0,0 +1,277 @@
+#!/bin/sh
+
+# This shell library is meant to be used with `set -e`.
+
+if [ "$(whoami)" != "root" ]; then
+ echo "This library is useless for non-root users. Exiting..." >&2
+ exit 1
+fi
+
+# Import the TBB_INSTALL, TBB_PROFILE and TBB_EXT variables, and
+# configure_xulrunner_app_locale().
+. /usr/local/lib/tails-shell-library/tor-browser.sh
+
+# Import try_for().
+. /usr/local/lib/tails-shell-library/common.sh
+
+# Break down the chroot and kill all of its processes
+try_cleanup_browser_chroot () {
+ local chroot="${1}"
+ local cow="${2}"
+ local user="${3}"
+ try_for 10 "pkill -u ${user} 1>/dev/null 2>&1" 0.1 || \
+ pkill -9 -u "${user}" || :
+ # findmnt sorts submounts so we just have to revert the list to
+ # have the proper umount order. We use `tail` to suppress the
+ # "TARGET" column header.
+ local chroot_mounts="$(
+ findmnt --output TARGET --list --submounts "${chroot}" | tail -n+2 | tac
+ )"
+ for mnt in ${chroot_mounts} "${cow}"; do
+ try_for 10 "umount ${mnt} 2>/dev/null" 0.1
+ done
+ rmdir "${cow}" "${chroot}"
+}
+
+# Setup a chroot on a clean aufs "fork" of the root filesystem.
+setup_chroot_for_browser () {
+ local chroot="${1}"
+ local cow="${2}"
+ local user="${3}"
+
+ # FIXME: When LXC matures to the point where it becomes a viable option
+ # for creating isolated jails, the chroot can be used as its rootfs.
+
+ local cleanup_cmd="try_cleanup_browser_chroot \"${chroot}\" \"${cow}\" \"${user}\""
+ trap "${cleanup_cmd}" INT EXIT
+
+ local rootfs_dir
+ local rootfs_dirs_path="/lib/live/mount/rootfs"
+ local tails_module_path="/lib/live/mount/medium/live/Tails.module"
+ local aufs_dirs=
+
+ # We have to pay attention to the order we stack the filesystems;
+ # newest must be first, and remember that the .module file lists
+ # oldest first, newest last.
+ while read rootfs_dir; do
+ rootfs_dir="${rootfs_dirs_path}/${rootfs_dir}"
+ mountpoint -q "${rootfs_dir}" && \
+ aufs_dirs="${rootfs_dir}=rr+wh:${aufs_dirs}"
+ done < "${tails_module_path}"
+ # But our copy-on-write dir must be at the very top.
+ aufs_dirs="${cow}=rw:${aufs_dirs}"
+
+ mkdir -p "${cow}" "${chroot}" && \
+ 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 -t tmpfs -o rw,nosuid,nodev tmpfs "${chroot}/dev/shm" || \
+ return 1
+
+ # Workaround for #6110
+ chmod -t "${cow}"
+}
+
+browser_conf_dir () {
+ local browser_name="${1}"
+ local browser_user="${2}"
+ echo "/home/${browser_user}/.${browser_name}"
+}
+
+browser_profile_dir () {
+ local conf_dir="$(browser_conf_dir "${@}")"
+ echo "${conf_dir}/profile.default"
+}
+
+chroot_browser_conf_dir () {
+ local chroot="${1}"; shift
+ echo "${chroot}/$(browser_conf_dir "${@}")"
+}
+
+chroot_browser_profile_dir () {
+ local conf_dir="$(chroot_browser_conf_dir "${@}")"
+ echo "${conf_dir}/profile.default"
+}
+
+set_chroot_browser_permissions () {
+ local chroot="${1}"
+ local browser_name="${2}"
+ local browser_user="${3}"
+ local browser_conf="$(chroot_browser_conf_dir "${chroot}" "${browser_name}" "${browser_user}")"
+ chown -R "${browser_user}:${browser_user}" "${browser_conf}"
+}
+
+configure_chroot_browser_profile () {
+ local chroot="${1}" ; shift
+ local browser_name="${1}" ; shift
+ local browser_user="${1}" ; shift
+ local home_page="${1}" ; shift
+ # Now $@ is a list of paths (that must be valid after chrooting)
+ # to extensions to enable.
+
+ # Prevent sudo from complaining about failing to resolve the 'amnesia' host
+ echo "127.0.0.1 localhost amnesia" > "${chroot}/etc/hosts"
+
+ # Create a fresh browser profile for the clearnet user
+ local browser_profile="$(chroot_browser_profile_dir "${chroot}" "${browser_name}" "${browser_user}")"
+ local browser_ext="${browser_profile}/extensions"
+ mkdir -p "${browser_profile}" "${browser_ext}"
+
+ # Select extensions to enable
+ local extension
+ while [ -n "${*:-}" ]; do
+ extension="${1}" ; shift
+ ln -s "${extension}" "${browser_ext}"
+ done
+
+ # Set preferences
+ local browser_prefs="${browser_profile}/user.js"
+ local chroot_browser_config="/usr/share/tails/chroot-browsers"
+ cat "${chroot_browser_config}/common/prefs.js" \
+ "${chroot_browser_config}/${browser_name}/prefs.js" > "${browser_prefs}"
+
+ # Set browser home page to something that explains what's going on
+ if [ -n "${home_page:-}" ]; then
+ echo 'user_pref("browser.startup.homepage", "'"${home_page}"'");' >> \
+ "${browser_prefs}"
+ fi
+
+ # Set an appropriate theme
+ cat "${chroot_browser_config}/${browser_name}/theme.js" >> "${browser_prefs}"
+
+ # Customize the GUI.
+ local browser_chrome="${browser_profile}/chrome/userChrome.css"
+ mkdir -p "$(dirname "${browser_chrome}")"
+ cat "${chroot_browser_config}/common/userChrome.css" \
+ "${chroot_browser_config}/${browser_name}/userChrome.css" >> \
+ "${browser_chrome}"
+
+ set_chroot_browser_permissions "${chroot}" "${browser_name}" "${browser_user}"
+}
+
+set_chroot_browser_locale () {
+ local chroot="${1}"
+ local browser_name="${2}"
+ local browser_user="${3}"
+ local locale="${4}"
+ local browser_profile="$(chroot_browser_profile_dir "${chroot}" "${browser_name}" "${browser_user}")"
+ configure_xulrunner_app_locale "${browser_profile}" "${locale}"
+}
+
+# Must be called after configure_chroot_browser_profile(), since it
+# depends on which extensions are installed in the profile.
+set_chroot_browser_name () {
+ local chroot="${1}"
+ local human_readable_name="${2}"
+ local browser_name="${3}"
+ local browser_user="${4}"
+ local locale="${5}"
+ local ext_dir="${chroot}/${TBB_EXT}"
+ local browser_profile_ext_dir="$(chroot_browser_profile_dir "${chroot}" "${browser_name}" "${browser_user}")/extensions"
+
+ # If Torbutton is installed in the browser profile, it will decide
+ # the browser name.
+ if [ -e "${browser_profile_ext_dir}/torbutton@torproject.org" ]; then
+ local torbutton_locale_dir="${ext_dir}/torbutton/chrome/locale/${locale}"
+ if [ ! -d "${torbutton_locale_dir}" ]; then
+ # 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\|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.
+ return
+ fi
+
+ local pack top rest
+ 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 tmp="$(mktemp -d)"
+ 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
+ local browser_name="${1}" ; shift
+ local human_readable_name="${1}" ; shift
+ local home_page="${1}" ; shift
+ # Now $@ is a list of paths (that must be valid after chrooting)
+ # to extensions to enable.
+ local best_locale="$(guess_best_tor_browser_locale)"
+
+ configure_chroot_browser_profile "${chroot}" "${browser_name}" \
+ "${browser_user}" "${home_page}" "${@}"
+ set_chroot_browser_locale "${chroot}" "${browser_name}" "${browser_user}" \
+ "${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}"
+}
+
+# Start the browser in the chroot
+run_browser_in_chroot () {
+ local chroot="${1}"
+ 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/common.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/common.sh
new file mode 100644
index 0000000000000000000000000000000000000000..b90504f18c024880279a48212621f358e8ef477e
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/common.sh
@@ -0,0 +1,77 @@
+#!/bin/sh
+
+# Get monotonic time in seconds. See clock_gettime(2) for details.
+# Note: we limit ourselves to seconds simply because floating point
+# arithmetic is a PITA in the shell.
+clock_gettime_monotonic() {
+ perl -w -MTime::HiRes=clock_gettime,CLOCK_MONOTONIC \
+ -E 'say int(clock_gettime(CLOCK_MONOTONIC))'
+}
+
+# Run `check_expr` until `timeout` seconds has passed, and sleep
+# `delay` (optional, defaults to 1) seconds in between the calls.
+# Note that execution isn't aborted exactly after `timeout`
+# seconds. In the worst case (the timeout happens right after we check
+# if the timeout has happened) we'll wait in total: `timeout` seconds +
+# `delay` seconds + the time needed for `check_expr`.
+wait_until() {
+ local timeout check_expr delay timeout_at
+ timeout="${1}"
+ check_expr="${2}"
+ delay="${3:-1}"
+ timeout_at=$(expr $(clock_gettime_monotonic) + ${timeout})
+ until eval "${check_expr}"; do
+ if [ "$(clock_gettime_monotonic)" -ge "${timeout_at}" ]; then
+ return 1
+ fi
+ sleep ${delay}
+ done
+ return 0
+}
+
+# Just an alias. The second argument (wait_until()'s check_expr) is
+# the "try code block". Just like in `wait_until()`, the timeout isn't
+# very accurate.
+try_for() {
+ wait_until "${@}"
+}
+
+# Runs the wrapped command while temporarily disabling `set -e`, if
+# enabled. It will always return 0 to not make scripts with `set -e`
+# enabled abort but will instead store the wrapped command's return
+# value into the global variable _NO_ABORT_RET.
+no_abort() {
+ local set_e_was_enabled
+ if echo "${-}" | grep -q 'e'; then
+ set +e
+ set_e_was_enabled=true
+ else
+ set_e_was_enabled=false
+ fi
+ "${@}"
+ _NO_ABORT_RET=${?}
+ if [ "${set_e_was_enabled}" = true ]; then
+ set -e
+ fi
+ return 0
+}
+
+is_package_installed() {
+ local package_name package_status
+ package_name="${1}"
+ package_status="$(no_abort dpkg-query --show \
+ --showformat='${db:Status-Status}' "${package_name}" \
+ 2>/dev/null)"
+ [ "${package_status}" = "installed" ]
+}
+
+extract_from_file_between_markers () {
+ local file start stop
+ file="${1}"
+ start="${2}"
+ stop="${3}"
+ awk "/${start}/ { between=1; next; }
+ /${stop}/ { between=0; }
+ { if (between) { print; } }" \
+ "${file}"
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/gnome.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/gnome.sh
new file mode 100644
index 0000000000000000000000000000000000000000..da390d3b0ca7480b7a3f2663c992b5d6c6abe57a
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/gnome.sh
@@ -0,0 +1,18 @@
+GNOME_ENV_VARS="
+DBUS_SESSION_BUS_ADDRESS
+DISPLAY
+XAUTHORITY
+XDG_RUNTIME_DIR
+"
+
+export_gnome_env() {
+ # Get LIVE_USERNAME
+ . /etc/live/config.d/username.conf
+ local gnome_shell_pid="$(pgrep --newest --euid ${LIVE_USERNAME} gnome-shell)"
+ local tmp_env_file="$(tempfile)"
+ local vars="($(echo ${GNOME_ENV_VARS} | tr ' ' '|'))"
+ tr '\0' '\n' < "/proc/${gnome_shell_pid}/environ" | \
+ grep -E "^${vars}=" > "${tmp_env_file}"
+ while read line; do export "${line}"; done < "${tmp_env_file}"
+ rm "${tmp_env_file}"
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/hardware.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/hardware.sh
new file mode 100644
index 0000000000000000000000000000000000000000..821ec4777daa4f3dd3a1a04928b9b85a1f8a70f1
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/hardware.sh
@@ -0,0 +1,86 @@
+#!/bin/sh
+
+get_all_ethernet_nics() {
+ for i in /sys/class/net/*; do
+ # type = 1 means ethernet (ARPHDR_ETHER, see Linux' sources,
+ # beginning of include/linux/if_arp.h)
+ if [ "$(cat "${i}"/type)" = 1 ]; then
+ basename "${i}"
+ fi
+ done
+}
+
+nic_exists() {
+ [ -e /sys/class/net/"${1}" ]
+}
+
+nic_is_up() {
+ [ "$(cat /sys/class/net/"${1}"/operstate)" = "up" ]
+}
+
+# The following "nic"-related functions require that the argument is a
+# NIC that exists
+
+nic_ipv4_addr() {
+ ip addr show "${1}" | sed -n 's,^\s*inet \([0-9\.]\+\)/.*$,\1,p'
+}
+
+nic_ipv6_addr() {
+ ip addr show "${1}" | sed -n 's,^\s*inet6 \([0-9a-fA-F:]\+\)/.*$,\1,p'
+}
+
+# Will just output nothing on failure
+get_current_mac_of_nic() {
+ local mac
+ mac="$(macchanger "${1}" | sed -n "s/^Current\s*MAC:\s*\([0-9a-f:]\+\)\s.*$/\1/p" || :)"
+ if echo "${mac}:" | grep -q "^\([0-9a-fA-F]\{2\}:\)\{6\}$"; then
+ echo "${mac}"
+ fi
+}
+
+get_module_used_by_nic() {
+ basename "$(readlink "/sys/class/net/${1}/device/driver/module")"
+}
+
+get_name_of_nic() {
+ vendor=$(sed 's/^0x\(.*\)$/\1/' "/sys/class/net/${1}/device/vendor")
+ device=$(sed 's/^0x\(.*\)$/\1/' "/sys/class/net/${1}/device/device")
+ lspci -nn | sed -n "s/^\S\+\s\+[^:]\+:\s\+\(.*\)\s\+\[$vendor:$device\].*$/\1/p"
+}
+
+# Auxillary function for mod_rev_dep(). It recurses over the graph of
+# kernel module depencies of $@ (note that it only works for loaded
+# modules). To deal with circular dependencies a global variable
+# MOD_REV_DEP_VISITED keeps track of already visited nodes, and it
+# should be unset before the first call of this function.
+mod_rev_dep_aux() {
+ local mod
+ local rev_deps
+ for mod in ${@}; do
+ if echo ${MOD_REV_DEP_VISITED} | grep -qw ${mod}; then
+ continue
+ fi
+ MOD_REV_DEP_VISITED="${MOD_REV_DEP_VISITED} ${mod}"
+ # extract the "Used by" column for $mod from lsmod
+ rev_deps=$(lsmod | \
+ sed -n "s/^${mod}\s\+\S\+\s\+\S\+\s\+\(\S\+\)/\1/p" | \
+ tr ',' ' ')
+ mod_rev_dep_aux ${rev_deps}
+ echo ${mod}
+ done
+}
+
+# Prints a list of all loaded modules depending on $1, including $1. It's
+# ordered by descending "maximum dependency distance" from $1, so the
+# output is ideal if we want to unload $1 and (by necessity) all
+# modules that uses $1.
+mod_rev_dep() {
+ MOD_REV_DEP_VISITED=""
+ mod_rev_dep_aux ${1}
+}
+
+# Unloads module $1, and all modules that (transatively) depends on
+# $1 (i.e. its reverse dependencies).
+unload_module_and_rev_deps() {
+ /sbin/modprobe -r $(mod_rev_dep ${1})
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/localization.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/localization.sh
new file mode 100644
index 0000000000000000000000000000000000000000..df7dd7c26d5f30bf70118990f2137a7cd262c313
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/localization.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+# Extracts the language part of a given locale, e.g. "en_US.UTF-8"
+# yields "en". Often $LANG will be passed as the argument.
+language_code_from_locale () {
+ echo "${1}" | sed "s,\(_\|\.\).*$,,"
+}
+
+# Prints the path to the localized (according to the environment's
+# LANG) version of `page` in the local copy of Tails' website. `page`
+# should specify only the name of the page, not the language code (of
+# course!) or the ".html" extension. If a localized page doesn't exist
+# the default is the English version.
+localized_tails_doc_page () {
+ local page="${1}"
+ local lang_code="$(language_code_from_locale "${LANG}")"
+ local try_page
+ for locale in "${lang_code}" "en"; do
+ try_page="${page}.${locale}.html"
+ if [ -r "${try_page}" ]; then
+ echo "${try_page}"
+ return 0
+ fi
+ done
+ return 1
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/log.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/log.sh
new file mode 100644
index 0000000000000000000000000000000000000000..6d677371dcadc6ace23f565591ca1aef58b8d47e
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/log.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+warn() {
+ echo "$*" >&2
+}
+
+die() {
+ warn "$*"
+ exit 1
+}
+
+# Shouldn't be used in shell libraries; a script including such a
+# library would overwrite the library's log tag.
+set_log_tag() {
+ _LOG_TAG=$1
+}
+
+log() {
+ if [ "${_LOG_TAG}" ]; then
+ logger -t "${_LOG_TAG}" "$*" || :
+ else
+ logger "$*" || :
+ fi
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/po.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/po.sh
new file mode 100644
index 0000000000000000000000000000000000000000..bb041f5706e4b4942f6524ecd25ee1c50562aae5
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/po.sh
@@ -0,0 +1,40 @@
+# This shell library is meant to be used with `set -e` and `set -u`.
+
+po_languages () {
+ for po in po/*.po ; do
+ rel="${po%.po}"
+ echo "${rel#po/}"
+ done
+}
+
+diff_without_pot_creation_date () {
+ diff --ignore-matching-lines '^"POT-Creation-Date:' "${@}"
+}
+
+diff_without_pot_creation_date_and_comments () {
+ diff --ignore-matching-lines '^"POT-Creation-Date:' \
+ --ignore-matching-lines '^#: .*:[0-9]\+$' "${@}"
+}
+
+intltool_update_po () {
+ (
+ cd po
+ for locale in "$@" ; do
+ intltool-update --dist --gettext-package=tails $locale -o ${locale}.po.new
+
+ [ -f ${locale}.po ] || continue
+ [ -f ${locale}.po.new ] || continue
+
+ if [ "${FORCE:-}" = yes ]; then
+ echo "Force-updating '${locale}.po'."
+ mv ${locale}.po.new ${locale}.po
+ elif diff_without_pot_creation_date -q "${locale}.po" "${locale}.po.new"; then
+ echo "${locale}: Only header changes in PO file: keeping the old one"
+ rm ${locale}.po.new
+ else
+ echo "${locale}: Real changes in PO file: switching to the updated one"
+ mv ${locale}.po.new ${locale}.po
+ fi
+ done
+ )
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-shell-library/tails-greeter.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/tails-greeter.sh
new file mode 100644
index 0000000000000000000000000000000000000000..dc6cc26ce0cc5f676c6d89f77c161e85ac55d037
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/tails-greeter.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+PERSISTENCE_STATE='/var/lib/live/config/tails.persistence'
+PHYSICAL_SECURITY_SETTINGS='/var/lib/live/config/tails.physical_security'
+
+_get_tg_setting() {
+ if [ -r "${1}" ]; then
+ . "${1}"
+ eval "echo \${${2}:-}"
+ fi
+}
+
+persistence_is_enabled() {
+ [ "$(_get_tg_setting "${PERSISTENCE_STATE}" TAILS_PERSISTENCE_ENABLED)" = true ]
+}
+
+persistence_is_enabled_for() {
+ persistence_is_enabled && mountpoint -q "$1" 2>/dev/null
+}
+
+persistence_is_enabled_read_write() {
+ persistence_is_enabled && \
+ [ "$(_get_tg_setting "${PERSISTENCE_STATE}" TAILS_PERSISTENCE_READONLY)" != true ]
+}
+
+mac_spoof_is_enabled() {
+ # Only return false when explicitly told so to increase failure
+ # safety.
+ [ "$(_get_tg_setting "${PHYSICAL_SECURITY_SETTINGS}" TAILS_MACSPOOF_ENABLED)" != false ]
+}
+
+tails_netconf() {
+ _get_tg_setting "${PHYSICAL_SECURITY_SETTINGS}" TAILS_NETCONF
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..71cdfe5e7607febdd017d823a964eadf438647a9
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/tor-browser.sh
@@ -0,0 +1,136 @@
+#!/bin/sh
+
+TBB_INSTALL=/usr/local/lib/tor-browser
+TBB_PROFILE=/etc/tor-browser/profile
+TBB_EXT=/usr/local/share/tor-browser-extensions
+TOR_LAUNCHER_INSTALL=/usr/local/lib/tor-launcher-standalone
+TOR_LAUNCHER_LOCALES_DIR="${TOR_LAUNCHER_INSTALL}/chrome/locale"
+
+# For strings it's up to the caller to add double-quotes ("") around
+# the value.
+set_mozilla_pref() {
+ local file name value prefix
+ file="${1}"
+ name="${2}"
+ value="${3}"
+ # Sometimes we might want to do e.g. user_pref
+ prefix="${4:-pref}"
+ [ -e "${file}" ] && sed -i "/^${prefix}(\"${name}\",/d" "${file}"
+ echo "${prefix}(\"${name}\", ${value});" >> "${file}"
+}
+
+exec_firefox_helper() {
+ local binary="${1}"; shift
+
+ export LD_LIBRARY_PATH="${TBB_INSTALL}"
+ export FONTCONFIG_PATH="${TBB_INSTALL}/TorBrowser/Data/fontconfig"
+ export FONTCONFIG_FILE="fonts.conf"
+ export GNOME_ACCESSIBILITY=1
+
+ # The Tor Browser often assumes that the current directory is
+ # where the browser lives, e.g. for the fixed set of fonts set by
+ # fontconfig above.
+ cd "${TBB_INSTALL}"
+
+ # From start-tor-browser:
+ unset SESSION_MANAGER
+
+ exec "${TBB_INSTALL}"/"${binary}" "${@}"
+}
+
+exec_firefox() {
+ exec_firefox_helper firefox.real "${@}"
+}
+
+exec_unconfined_firefox() {
+ exec_firefox_helper firefox-unconfined "${@}"
+}
+
+guess_best_tor_browser_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 "${TBB_EXT}/langpack-${long_locale}@firefox.mozilla.org.xpi" ]; then
+ echo "${long_locale}"
+ return
+ elif [ -e "${TBB_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 "${TBB_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'
+}
+
+guess_best_tor_launcher_locale() {
+ local long_locale short_locale
+ long_locale="$(echo ${LANG} | sed -e 's/\..*$//' -e 's/_/-/')"
+ short_locale="$(echo ${long_locale} | cut -d"-" -f1)"
+ if [ -e "${TOR_LAUNCHER_LOCALES_DIR}/${long_locale}" ]; then
+ echo ${long_locale}
+ elif ls -1 "${TOR_LAUNCHER_LOCALES_DIR}" | grep -q "^${short_locale}\(-[A-Z]\+\)\?$"; then
+ # See comment in guess_best_firefox_locale()
+ echo ${short_locale}
+ else
+ echo en-US
+ fi
+}
+
+configure_xulrunner_app_locale() {
+ local profile locale
+ profile="${1}"
+ locale="${2}"
+ mkdir -p "${profile}"/preferences
+ set_mozilla_pref "${profile}"/prefs.js \
+ "intl.locale.requested" "\"${locale}\"" \
+ "user_pref"
+}
+
+configure_best_tor_browser_locale() {
+ local profile best_locale
+ profile="${1}"
+ best_locale="$(guess_best_tor_browser_locale)"
+ configure_xulrunner_app_locale "${profile}" "${best_locale}"
+ cat "/etc/tor-browser/locale-profiles/${best_locale}.js" \
+ >> "${profile}/prefs.js"
+}
+
+configure_best_tor_launcher_locale() {
+ configure_xulrunner_app_locale "${1}" "$(guess_best_tor_launcher_locale)"
+}
+
+supported_tor_browser_locales() {
+ # The default is always supported
+ echo en-US
+ for langpack in "${TBB_EXT}"/langpack-*@firefox.mozilla.org.xpi; do
+ 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/lib/tails-shell-library/tor.sh b/config/chroot_local-includes/usr/local/lib/tails-shell-library/tor.sh
new file mode 100755
index 0000000000000000000000000000000000000000..e2c0f5afe02e0175cfaba732e9caeebd068b44ed
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-shell-library/tor.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+
+TOR_RC_DEFAULTS=/usr/share/tor/tor-service-defaults-torrc
+TOR_RC=/etc/tor/torrc
+TOR_LOG=/var/log/tor/log
+TOR_DIR=/var/lib/tor
+TOR_DESCRIPTORS=${TOR_DIR}/cached-microdescs
+NEW_TOR_DESCRIPTORS=${TOR_DESCRIPTORS}.new
+
+tor_rc_lookup() {
+ grep --no-filename "^${1}\s" "${TOR_RC_DEFAULTS}" "${TOR_RC}" | \
+ sed --regexp-extended "s/^${1}\s+(.+)$/\1/" | tail -n1
+}
+
+tor_control_cookie_path() {
+ local path
+ path="$(tor_rc_lookup CookieAuthFile)"
+ [ -e "${path}" ] && echo "${path}"
+}
+
+tor_control_send() {
+ local control_port cookie_path hexcookie
+ control_port="$(tor_rc_lookup ControlPort)"
+ cookie_path="$(tor_control_cookie_path)"
+ if [ -e "${cookie_path}" ] && [ -n "${control_port}" ]; then
+ hexcookie=$(xxd -c 32 -g 0 "${cookie_path}" | cut -d' ' -f2)
+ /bin/echo -ne "AUTHENTICATE ${hexcookie}\r\n${1}\r\nQUIT\r\n" | \
+ /bin/nc 127.0.0.1 "${control_port}" | tr -d "\r"
+ else
+ return 1
+ fi
+}
+
+# Only handles GETINFO keys with single-line answers
+tor_control_getinfo() {
+ tor_control_send "GETINFO ${1}" | \
+ sed --regexp-extended -n "s|^250-${1}=(.*)$|\1|p"
+}
+
+tor_control_getconf() {
+ tor_control_send "GETCONF ${1}" | \
+ sed --regexp-extended -n "s|^250 ${1}=(.*)$|\1|p"
+}
+
+tor_control_setconf() {
+ tor_control_send "SETCONF ${1}" >/dev/null
+}
+
+tor_bootstrap_progress() {
+ local res
+ res=$(tor_control_getinfo status/bootstrap-phase | \
+ sed --regexp-extended 's/^.* BOOTSTRAP PROGRESS=([[:digit:]]+) .*$/\1/')
+ echo ${res:-0}
+}
+
+tor_is_working() {
+ [ "$(tor_bootstrap_progress)" -eq 100 ]
+}
+
+tor_append_to_torrc () {
+ echo "${@}" >> "${TOR_RC}"
+}
+
+# Set a (possibly existing) option $1 to $2 in torrc. Shouldn't be
+# used for options that can be set multiple times (e.g. the listener
+# options). Does not support configuration entries split into multiple
+# lines (with the backslash character).
+tor_set_in_torrc () {
+ sed -i "/^${1}\s/d" "${TOR_RC}"
+ tor_append_to_torrc "${1} ${2}"
+}
diff --git a/config/chroot_local-includes/usr/local/lib/tails-spoof-mac b/config/chroot_local-includes/usr/local/lib/tails-spoof-mac
new file mode 100755
index 0000000000000000000000000000000000000000..b43811cd33400cb9ce471f936913afcf2246f466
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-spoof-mac
@@ -0,0 +1,155 @@
+#!/bin/sh
+
+set -e
+set -u
+
+# This script spoofs or resets the MAC address of all NICs given as
+# arguments according to the setting in Tails Greeter. The default (i.e
+# before Tails Greeter has been run) is to enable MAC spoofing.
+
+. /usr/local/lib/tails-shell-library/hardware.sh
+. /usr/local/lib/tails-shell-library/log.sh
+. /usr/local/lib/tails-shell-library/tails-greeter.sh
+
+# Get LIVE_USERNAME
+. /etc/live/config.d/username.conf
+
+. /usr/bin/gettext.sh
+TEXTDOMAIN="tails"
+export TEXTDOMAIN
+
+stop_and_disable_NM() {
+ for s in NetworkManager-dispatcher.service \
+ NetworkManager-wait-online.service \
+ NetworkManager.service; do
+ systemctl stop "${s}"
+ systemctl disable "${s}"
+ systemctl mask "${s}"
+ done
+ log "Networking disabled"
+}
+
+show_notification() {
+ # We must wait until all the facilities necessary for showing the
+ # notification to the Live user is available to prevent it from
+ # getting lost.
+ until pgrep -u "${LIVE_USERNAME}" '^ibus-daemon' >/dev/null ; do
+ sleep 1
+ done
+ # The above doesn't seem to be enough. The best we can do seems to
+ # be to statically wait a bit longer. The amount chosen is just
+ # arbitrarily picked, and may not work on slow hardware or even
+ # DVD boot, but should at least work in our automated test suite.
+ sleep 10
+ /usr/local/sbin/tails-notify-user "${1}" "${2}" 0
+}
+
+notify_panic_success() {
+ local nic
+ local nic_name
+ nic="${1}"
+ nic_name="${2}"
+ show_notification "`gettext \"Network card \\\${nic} disabled\"`" \
+"`eval_gettext \"MAC spoofing failed for network card \\\${nic_name} (\\\${nic}) so it is temporarily disabled.
+You might prefer to restart Tails and disable MAC spoofing.\"`"
+}
+
+notify_panic_failure() {
+ local nic
+ local nic_name
+ nic="${1}"
+ nic_name="${2}"
+ show_notification "`gettext \"All networking disabled\"`" \
+"`eval_gettext \"MAC spoofing failed for network card \\\${nic_name} (\\\${nic}). The error recovery also failed so all networking is disabled.
+You might prefer to restart Tails and disable MAC spoofing.\"`"
+}
+
+mac_spoof_panic() {
+ local nic
+ local module
+ local nic_name
+ local unload_success
+ nic=${1}
+ if ! /sbin/ip link set dev "${nic}" down; then
+ log "Failed to down NIC ${nic} in panic mode."
+ fi
+ module=$(get_module_used_by_nic "${nic}")
+ nic_name="$(get_name_of_nic ${nic})"
+ echo "install ${module} /bin/true" >> \
+ /etc/modprobe.d/"${module}"-blacklist.conf
+ unload_module_and_rev_deps "${module}" || :
+ if nic_exists "${nic}"; then
+ log "Failed to unload module ${module} of NIC ${nic}."
+ stop_and_disable_NM
+ notify_panic_failure "${nic}" "${nic_name}" &
+ else
+ log "Successfully unloaded module ${module} of NIC ${nic}."
+ notify_panic_success "${nic}" "${nic_name}" &
+ fi
+}
+
+spoof_mac() {
+ local msg
+ set +e
+ msg="$(macchanger -e "${1}" 2>&1)"
+ ret="${?}"
+ set -e
+ if [ "${ret}" != 0 ]; then
+ log "macchanger failed for NIC ${1}, returned ${ret} and said: ${msg}"
+ return 1
+ fi
+}
+
+set_log_tag spoof-mac
+
+NIC="${1}"
+
+if ! mac_spoof_is_enabled; then
+ exit 0
+fi
+
+log "Trying to spoof MAC address of NIC ${NIC}..."
+
+if ! nic_exists "${NIC}"; then
+ log "NIC ${NIC} doesn't exist, skipping"
+ exit 1
+fi
+
+OLD_MAC="$(get_current_mac_of_nic "${NIC}")"
+
+# There is a 1/2^24 chance macchanger will randomly pick the real MAC
+# address. We try to making it really unlikely repeating it up to
+# three times. Theoretically speaking this leaks information about the
+# real MAC address at each occasion but actually leaking the real MAC
+# address will be more serious in practice.
+for i in 1 2 3; do
+ if ! spoof_mac "${NIC}"; then
+ # If our MAC spoofing primitive fails, we fail safe by forcing
+ # us to enter into panic mode.
+ unset NEW_MAC
+ break
+ fi
+ NEW_MAC="$(get_current_mac_of_nic "${NIC}")"
+ if [ "${OLD_MAC}" != "${NEW_MAC}" ]; then
+ break
+ fi
+done
+
+# MAC spoofing fail-safe: if $NIC's MAC address isn't spoofed at this
+# point we have to take some drastic measures in order to prevent
+# potential leaks.
+if [ -z "${OLD_MAC:-}" ] || [ -z "${NEW_MAC:-}" ] || \
+ [ "${OLD_MAC:-}" = "${NEW_MAC:-}" ]
+then
+ log "Failed to spoof MAC address of NIC ${NIC}. Going into panic mode."
+ if ! mac_spoof_panic "${NIC}"; then
+ # If mac_spoof_panic() fails we're quite screwed, so we kill
+ # NetworkManager without notification to do our best to
+ # prevent a MAC address leak.
+ log "Panic mode failed for NIC ${NIC}."
+ stop_and_disable_NM
+ fi
+ exit 1
+fi
+
+log "Successfully spoofed MAC address of NIC ${NIC}"
diff --git a/config/chroot_local-includes/usr/local/lib/tails-unblock-network b/config/chroot_local-includes/usr/local/lib/tails-unblock-network
new file mode 100755
index 0000000000000000000000000000000000000000..f2c07d3f7dddddc6e9e11ecd4a26b9e66d21d665
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-unblock-network
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+set -e
+set -u
+set -x
+
+systemctl start tails-unblock-network.service
+
+# Without this, network is sometimes not unblocked, probably due to some
+# race condition between tails-unblock-network.service (that deletes the
+# blacklist file) and udevadm trigger, possibly caused by yet another aufs
+# weirdness (#9012)...
+echo "Sleeping..." >&2
+sleep 5
+echo "slept." >&2
+
+# XXX: add debugging information for #9012 to the Journal
+ls /etc/modprobe.d
+/sbin/lsmod
+ls -l /sys/class/net
+
+# Now we'll load any present network device previously blocked by
+# the blacklist. In particular, the MAC spoofing udev rule should trigger
+# for each network device added.
+echo "Restarting systemd-udev-trigger.service..." >&2
+systemctl restart systemd-udev-trigger.service
+echo "systemd-udev-trigger.service restarted." >&2
+
+# Block until all triggers have been run. NetworkManager is started immediately
+# after, and without the blocking behaviour there's a race between NM
+# and the MAC spoof udev triggers. When NM takes control of some network device,
+# some operations are not possible on the device, like MAC spoofing. Hence,
+# if NM wins, the udev-triggered run of tails-spoof-mac will fail.
+echo "Restarting systemd-udev-settle.service..." >&2
+systemctl restart systemd-udev-settle.service
+echo "systemd-udev-settle.service restarted." >&2
+
+# XXX: add debugging information for #9012 to the Journal
+/sbin/lsmod
+ls -l /sys/class/net
+
+# Enable and start NetworkManager services
+# No need to manually enable NetworkManager-dispatcher.service,
+# as NetworkManager.service has "Also=NetworkManager-dispatcher.service"
+# in its [Install] section.
+# If tails-spoof-mac goes into panic mode but fails to disable the
+# problematic device, the NetworkManager services will be masked.
+systemctl enable NetworkManager.service NetworkManager-wait-online.service || :
+systemctl start NetworkManager.service NetworkManager-dispatcher.service || :
+systemctl --no-block start NetworkManager-wait-online.service || :
diff --git a/config/chroot_local-includes/usr/local/lib/tails-virt-notify-user b/config/chroot_local-includes/usr/local/lib/tails-virt-notify-user
new file mode 100755
index 0000000000000000000000000000000000000000..f9678ef060cec24401237211d81fd9166e4e801a
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/tails-virt-notify-user
@@ -0,0 +1,85 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+#man{{{
+
+=head1 NAME
+
+tails-virt-notify-user
+
+=head1 VERSION
+
+Version X.XX
+
+=head1 AUTHOR
+
+Tails dev team
+See https://tails.boum.org/.
+
+=cut
+
+#}}}
+
+use Desktop::Notify;
+use English '-no_match_vars';
+use IPC::System::Simple qw{capturex $EXITVAL};
+use Locale::gettext;
+use Net::DBus::Reactor;
+use POSIX;
+
+### initialization
+setlocale(LC_MESSAGES, "");
+textdomain("tails");
+
+### callbacks
+
+sub action_cb {
+ my $reactor = shift;
+ unless (fork) {
+ exec(
+ '/usr/local/bin/tails-documentation',
+ 'doc/advanced_topics/virtualization',
+ 'security'
+ );
+ }
+ $reactor->shutdown;
+}
+
+### main
+
+# both 0 and 1 are acceptable exit values:
+# - 0 means that we're running in a virtualized environment
+# - 1 means that we're not running in a virtualized environment
+# - anything else means there is a problem, and capturex will throw an exception
+my $vm_name = capturex([0, 1], qw{/usr/bin/systemd-detect-virt --vm});
+exit 0 if $EXITVAL == 1;
+
+my @whitelist = qw(bochs kvm qemu uml virtualbox xen);
+
+my $reactor = Net::DBus::Reactor->main;
+
+my $notify = Desktop::Notify->new();
+$notify->action_callback(sub { action_cb($reactor, @_) });
+$notify->close_callback(sub { $reactor->shutdown; });
+
+my ($body, $summary);
+
+chomp($vm_name);
+if (grep {$_ eq $vm_name} @whitelist) {
+ $summary = gettext("Warning: virtual machine detected!");
+}
+else {
+ $summary = gettext("Warning: non-free virtual machine detected!");
+}
+
+$body = gettext("Both the host operating system and the virtualization software are able to monitor what you are doing in Tails. Only free software can be considered trustworthy, for both the host operating system and the virtualization software.");
+
+$notify->create(summary => $summary,
+ body => $body,
+ actions => { "moreinfo_$PID" => gettext('Learn more'), },
+ hints => { 'transient' => 1, },
+ timeout => 0)->show();
+
+$reactor->run;
diff --git a/config/chroot_local-includes/usr/local/lib/udev-watchdog-wrapper b/config/chroot_local-includes/usr/local/lib/udev-watchdog-wrapper
new file mode 100755
index 0000000000000000000000000000000000000000..8d7e1dd7bb1a2049d35dab7d37e6b331d0566d9f
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/lib/udev-watchdog-wrapper
@@ -0,0 +1,89 @@
+#!/bin/sh
+
+set -e
+
+### Helper functions
+
+# For whatever reason, the initscript that calls us sets a pretty scarse $PATH
+PATH="/usr/bin:${PATH}"
+
+### Helper functions
+
+using_fromiso() {
+ grep -qs -w -E '(fromiso|isofrom)=' /proc/cmdline
+}
+
+# Returns the boot device's path in a form that can be passed to the
+# eject command, e.g. /dev/scd0 or /dev/block/NN:MM.
+boot_device() {
+ if using_fromiso ; then
+ # When booting with e.g. fromiso=/dev/sdx3/tails-XXX.iso, a loop device
+ # is mounted onto /lib/live/mount/medium => we cannot get the boot device from there.
+ # This loop device's backing file is seen by the system as
+ # /isofrom/XXX.iso, which is not available presumably because pivotroot
+ # was run => we cannot get the boot device from there either.
+ # Instead, we parse fromiso='s argument the same way live-boot does
+ # in order to extract the device path (/dev/sdx3)
+ for ARGUMENT in $(cat /proc/cmdline) ; do
+ case "${ARGUMENT}" in
+ isofrom=*|fromiso=*)
+ FROMISO="${ARGUMENT#*=}"
+ ;;
+ esac
+ done
+ echo $(dirname "$FROMISO")
+ else
+ # Refactorer, beware: the rest of this script depends on the fact that
+ # the path returned in this case is suitable to be passed as an argument
+ # to --path in "udevadm info --query" commands... which is not the case
+ # of paths in the /dev/sdxN form.
+ DEV_NUMBER="$(udevadm info --device-id-of-file=/lib/live/mount/medium)"
+ echo "/dev/block/$DEV_NUMBER"
+ fi
+}
+
+# First clean the screen, then brutally shutdown the machine.
+do_stop() {
+ # Kill everything run by amnesia or Debian-gdm, otherwise emergency
+ # shutdown fails for some reason. Incidentally, this also allows
+ # the test suite to look for a known message ("Happy dumping!")
+ # on the screen.
+ /bin/loginctl --signal=9 kill-user amnesia || true
+ /bin/systemctl stop gdm.service || true
+ /bin/systemctl --signal=9 kill gdm.service || true
+ /bin/loginctl --signal=9 kill-user Debian-gdm || true
+
+ # Finally, return to the initramfs and poweroff the system
+ /bin/systemctl --force poweroff
+}
+
+
+### Main
+
+BOOT_DEVICE=$(boot_device)
+
+# Assign to QUERY_SELECTOR an option that can be passed as a query selector
+# to udevadm info --query commands.
+if using_fromiso ; then
+ DEV_NAME=$(basename "$BOOT_DEVICE")
+ QUERY_SELECTOR="--name $DEV_NAME"
+else
+ QUERY_SELECTOR="--path $BOOT_DEVICE"
+fi
+
+DEV_UDEV_PATH=$(udevadm info --query path $QUERY_SELECTOR)
+# SD in SDIO has no ID_TYPE, let's pretend it's a disk just like USB sticks
+DEV_TYPE_LINE=$(udevadm info --query property $QUERY_SELECTOR | grep -w '^ID_TYPE') \
+ || DEV_TYPE_LINE='ID_TYPE=disk'
+DEV_TYPE="${DEV_TYPE_LINE#*=}"
+
+# If the world was sane we'd want to *disable* the eject lock, but it turns out
+# that blocks the block events so udev-watchdog never receives the "change"
+# event. See [[bugs/sdmem_on_eject_broken_for_CD]].
+# FIXME: we might be able to do the more sane "-i off" with future kernel/udev
+if [ "$DEV_TYPE" = "cd" ]; then
+ eject -i on "${BOOT_DEVICE}" >/dev/null
+fi
+
+# Start udev-watchdog and stop on clean exit.
+/usr/local/sbin/udev-watchdog "$DEV_UDEV_PATH" "$DEV_TYPE" && do_stop
diff --git a/config/chroot_local-includes/usr/local/sbin/fillram b/config/chroot_local-includes/usr/local/sbin/fillram
new file mode 100755
index 0000000000000000000000000000000000000000..cf3808666b4dfdb785212e39ec4e8c684286c0ed
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/fillram
@@ -0,0 +1,7 @@
+#!/usr/bin/python3
+
+# NB: this program is subject to the system's per-process memory limits.
+
+string = ""
+while True:
+ string = string + "wipe_didnt_work\n"
diff --git a/config/chroot_local-includes/usr/local/sbin/htpdate b/config/chroot_local-includes/usr/local/sbin/htpdate
new file mode 100755
index 0000000000000000000000000000000000000000..2aaf83f1bdaaad891f1a03a2441348f833e0e5af
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/htpdate
@@ -0,0 +1,317 @@
+#!/usr/bin/perl
+#
+# htpdate time poller version 0.9.3
+# Copyright (C) 2005 Eddy Vervest
+# Copyright (C) 2010-2011 Tails developers
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# http://www.gnu.org/copyleft/gpl.html
+
+use strict;
+use warnings;
+
+use version; our $VERSION = qv('0.9.3');
+
+use Carp;
+use Cwd;
+use Data::Dumper;
+use DateTime;
+use DateTime::Format::DateParse;
+use English qw( -no_match_vars );
+use File::Path qw(rmtree);
+use File::Spec::Functions;
+use File::Temp qw/tempdir/;
+use Getopt::Long::Descriptive;
+use IPC::System::Simple qw(capturex);
+use List::Util qw( shuffle );
+use open qw{:utf8 :std};
+use POSIX qw( WIFEXITED );
+use threads;
+use Try::Tiny;
+
+my $datecommand = '/bin/date'; # "date" command to set time
+my $dateparam = '-s'; # "date" parameter to set time
+my $maxadjust = 0; # maximum time step in seconds (0 means no max.)
+my $minadjust = 1; # minimum time step in seconds
+my (
+ $debug, $useragent, $log, $quiet, $set_date,
+ $done_file, $res_file, $usage, $opt, $runas,
+ $allowed_per_pool_failure_ratio, $proxy, @pools,
+);
+
+sub done {
+ if (defined $done_file) {
+ $> = 0 if $runas;
+ open my $f, '>', $done_file or
+ print STDERR "Couldn't write done file: $done_file\n";
+ close $f;
+ $> = getpwnam($runas) if $runas;
+ }
+}
+
+$SIG{__DIE__} = sub {
+ # Avoid the "done" file to be created by an catched exception.
+ # When a eval block is being run, e.g. for exception catching, $^S is true.
+ # It is false otherwise.
+ done unless $^S;
+ die(@_);
+};
+
+sub message {
+ my @msg = @_;
+
+ if ($log) {
+ open my $h, '>>', $log or die "Cannot open log file $log: $!";
+ print $h "@msg\n";
+ close $h;
+ }
+ else {
+ print "@msg\n" unless $quiet;
+ }
+}
+
+sub debug {
+ message(@_) if $debug;
+}
+
+sub error {
+ debug(@_);
+ croak @_;
+}
+
+sub parseCommandLine () {
+ # specify valid switches
+ ($opt, $usage) = describe_options(
+ 'htpdate %o',
+ [ 'debug|d', "debug", { default => 0 } ],
+ [ 'help', "print usage message and exit" ],
+ [ 'quiet|q', "quiet", { default => 0 } ],
+ [ 'user|u:s', "userid to run as" ],
+ [ 'dont_set_date|x', "do not set the time (only show)", { default => 0 } ],
+ [ 'user_agent|a:s', "http user agent to use", { default => "htpdate/$VERSION" } ],
+ [ 'log_file|l:s', "log to this file rather than to STDOUT" ],
+ [ 'done_file|D:s', "create this file after quitting in any way" ],
+ [ 'success_file|T:s', "create this file after setting time successfully" ],
+ [ 'pool1=s@', "first pool of hostnames" ],
+ [ 'pool2=s@', "second pool of hostnames" ],
+ [ 'pool3=s@', "third pool of hostnames" ],
+ [ 'allowed_per_pool_failure_ratio:f', "ratio (0.0-1.0) of allowed per-pool failure", { default => 1.0 } ],
+ [ 'proxy|p:s', "what to pass to curl's --socks5-hostname (if unset, environment variables may affect curl's behavior -- see curl(1) for details)" ],
+ );
+
+ usage() if $opt->help;
+ usage() unless $opt->pool1 && $opt->pool2 && $opt->pool3;
+
+ $runas = $opt->user if $opt->user;
+ $> = getpwnam($runas) if $runas;
+ $useragent = $opt->user_agent;
+ $debug = $opt->debug;
+ $log = $opt->log_file if $opt->log_file;
+ $quiet = $opt->quiet;
+ $set_date = ! $opt->dont_set_date;
+ $done_file = $opt->done_file if $opt->done_file;
+ $res_file = $opt->success_file if $opt->success_file;
+ $allowed_per_pool_failure_ratio = $opt->allowed_per_pool_failure_ratio;
+ $proxy = $opt->proxy if $opt->proxy;
+ @pools = map {
+ [
+ map {
+ $_ = 'https://'.$_ unless $_ =~ /^http/i;
+ } split(/,/, join(',', @{$_}))
+ ]
+ } ($opt->pool1, $opt->pool2, $opt->pool3);
+}
+
+sub usage () {
+ print STDERR $usage->text;
+ exit;
+}
+
+sub newestDateHeader {
+ my ($dir) = @_;
+
+ my @files = grep { ! ( $_ =~ m|/?\.{1,2}$| ) } glob("$dir/.* $dir/*");
+ @files or error "No downloaded files can be found";
+
+ my $newestdt;
+
+ foreach my $file (@files) {
+ next if -l $file || -d _;
+ my $date;
+ open(my $file_h, '<', $file) or die "Can not read file $file: $!";
+ while (my $line = <$file_h>) {
+ chomp $line;
+ # empty line == we leave the headers to go into the content
+ last if $line eq '';
+ last if ($date) = ($line =~ m/^\s*[Dd]ate:\s+(.*)$/m);
+ }
+ close $file_h;
+ if (defined $date) {
+ # RFC 2616 (3.3.1) says Date headers MUST be represented in GMT
+ my $dt = DateTime::Format::DateParse->parse_datetime( $date, 'GMT' );
+ if (! defined $newestdt || DateTime->compare($dt, $newestdt) > 0) {
+ $newestdt = $dt;
+ }
+ }
+ }
+
+ return $newestdt;
+}
+
+=head2 random_first_with_allowed_failure_ratio
+
+Returns the result of the first successful application of
+$args->{code} on a random element of $args->{list}.
+Success is tested using the $args->{is_success} predicate,
+called on the value returned by $args->{code}.
+
+$args->{allowed_failure_ratio} caps the maximum failure ratio before
+giving up.
+
+$args->{code} is called with two arguments: the currently (randomly
+picked) considered element, and $args->{args}.
+
+Any exceptions thrown by $args->{code} is catched.
+
+=cut
+sub random_first_with_allowed_failure_ratio {
+ my $args = shift;
+
+ my %tried;
+ $tried{$_} = 0 for (@{$args->{list}});
+ my $failures = 0;
+ my $total = keys %tried;
+
+ while ( $failures / $total <= $args->{allowed_failure_ratio} ) {
+ my @randomized_left = shuffle grep { ! $tried{$_} } keys(%tried);
+ my $picked = $randomized_left[0];
+ $tried{$picked}++;
+ my $res;
+ try {
+ $res = $args->{code}->($picked, $args->{args})
+ };
+ return $res if $args->{is_success}->($res);
+ $failures++;
+ }
+
+ return;
+}
+
+sub getPoolDateDiff {
+ my $args = shift;
+
+ random_first_with_allowed_failure_ratio({
+ list => $args->{urls},
+ code => \&getUrlDateDiff,
+ is_success => sub { defined shift },
+ allowed_failure_ratio => $allowed_per_pool_failure_ratio,
+ });
+}
+
+sub getUrlDateDiff {
+ my $url = shift;
+ my $args = shift;
+
+ defined $url or error "getUrlDateDiff must be passed an URL";
+ debug("getUrlDateDiff: $url");
+
+ my $tmpdir = tempdir("XXXXXXXXXX", TMPDIR => 1);
+
+ my @curl_options = (
+ '--user-agent', $useragent, '--silent',
+ '--proto', '=https', '--tlsv1',
+ '--max-time', '30',
+ '--head', '--output', catfile($tmpdir, 'headers'),
+ );
+ push @curl_options, ('--socks5-hostname', $proxy) if defined $proxy;
+
+ my @cmdline = ('curl', @curl_options, $url);
+
+ # fetch (the page and) referenced resources:
+ # images, stylesheets, scripts, etc.
+ my $before = DateTime->now->epoch();
+ WIFEXITED(system(@cmdline)) or error "Failed to fetch content from $url: $!";
+ my $local = DateTime->now->epoch();
+ my $newestdt;
+ eval { $newestdt = newestDateHeader($tmpdir) };
+ if ($EVAL_ERROR =~ m/No downloaded files can be found/) {
+ rmtree($tmpdir);
+ error "No file could be downloaded from $url.";
+ }
+
+ rmtree($tmpdir);
+
+ defined $newestdt or error "Could not get any Date header from $url";
+ my $newest_epoch = $newestdt->epoch();
+
+ my $diff = $newest_epoch - $local;
+ my $took = $local - $before;
+
+ debug("$url (took ${took}s) => diff = $diff second(s)");
+
+ return $diff;
+}
+
+sub adjustDate {
+ my ($diff) = @_;
+
+ defined $diff or error "adjustDate was passed an undefined diff";
+
+ my $local = DateTime->now->epoch();
+ my $absdiff = abs($diff);
+
+ debug("Median diff: $diff second(s)");
+
+ if ( $maxadjust && $absdiff gt $maxadjust ) {
+ message("Not setting clock as diff ($diff seconds) is too large.");
+ }
+ elsif ( $absdiff lt $minadjust) {
+ message("Not setting clock as diff ($diff seconds) is too small.");
+ }
+ else {
+ my $newtime = DateTime->now->epoch + $diff;
+ message("Setting time to $newtime...");
+ if ($set_date) {
+ $> = 0 if $runas;
+ my $output;
+ try {
+ $output = capturex($datecommand, $dateparam, '@' . $newtime);
+ } catch {
+ error "An error occured setting the time\n$output";
+ };
+ $> = getpwnam($runas) if $runas;
+ }
+ }
+ if (defined $res_file) {
+ $> = 0 if $runas;
+ open my $res_h, '>>', $res_file or die "Cannot open res file $res_file: $!";
+ print $res_h "$diff\n";
+ close $res_h;
+ $> = getpwnam($runas) if $runas;
+ }
+}
+
+sub median {
+ my @a = sort {$a <=> $b} @_;
+ return ($a[$#a/2] + $a[@a/2]) / 2;
+}
+
+parseCommandLine();
+message("Running htpdate.");
+my @diffs = grep {
+ defined $_
+} map {
+ my $diff = $_->join();
+ if (! defined $diff) {
+ error('Aborting as one pool could not be reached');
+ }
+ $diff;
+} map {
+ threads->create(\&getPoolDateDiff, { urls => $_ })
+} @pools
+ or error "No Date header could be received.";
+adjustDate(median(@diffs));
+done;
diff --git a/config/chroot_local-includes/usr/local/sbin/live-persist b/config/chroot_local-includes/usr/local/sbin/live-persist
new file mode 100755
index 0000000000000000000000000000000000000000..8bdba4f76c1e24c57fbad27c022fb55f33f63b5c
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/live-persist
@@ -0,0 +1,604 @@
+#!/bin/bash
+
+error ()
+{
+ echo "error: ${@}" >&2
+ exit 1
+}
+
+warning ()
+{
+ echo "warning: ${@}" >&2
+}
+
+# import Cmdline_old()
+. /lib/live/boot/9990-cmdline-old \
+ || error 'Could not source /lib/live/boot/9990-cmdline-old'
+
+# Set variable names needed by get_custom_mounts(),
+# and now initialized by live-boot in a file that we certainly
+# don't want to source.
+export persistence_list="persistence.conf"
+export old_persistence_list="nonexistent"
+
+# This will import the following functions and variables used below:
+# activate_custom_mounts()
+# get_custom_mounts()
+# open_luks_device()
+# probe_for_gpt_name()
+# removable_dev()
+# removable_usb_dev()
+# storage_devices()
+# where_is_mounted()
+# $custom_overlay_label
+. /lib/live/boot/9990-misc-helpers.sh \
+ || error 'Could not source /lib/live/boot/9990-misc-helpers.sh'
+
+usage ()
+{
+ local cmd=${0##*/}
+ echo "Usage: ${cmd} [OPTION]... list [LABEL]...
+List (on stdout) all GPT partitions with names among LABEL(s) that are
+compatible with live-boot's overlay persistence, and that are adhering to
+live-boot's persistence filters (e.g. persistent-media). If no LABEL is given
+the default in live-boot is used ('${custom_overlay_label}').
+ or: ${cmd} [OPTION]... activate VOLUME...
+Activates persistence on the given VOLUME(s). Successes and failures are
+written to stdout. There are no checks for whether the given volumes adhere
+to live-boot's options.
+
+Kernel command-line options are parsed just like in live-boot and have the same
+effect (see live-boot(7) for more information)
+
+Arguments to options must be passed using an equality sign. LISTs are coma
+separated. Most options correspond to the persistent-* options of live-boot,
+and will override the corresponging options parsed from the kernel command-line.
+
+General options:
+ --help display this help and exit
+ --log-file=FILE log the bash execution trace to FILE
+
+Options affecting the 'list' action:
+ --encryption=LIST override 'persistent-encryption'
+ --media=VALUE override 'persistent-media'
+
+Options affecting the 'activate' action:
+ --read-only enable 'persistent-read-only'
+ --read-write disable 'persistent-read-only'
+ --union=VALUE override 'union'
+"
+}
+
+escape() {
+ printf "%s\n" "${2}" | sed --regexp-extended "s/[${1}]/\\\&/g"
+}
+
+escape_dots() {
+ escape . "${@}"
+}
+
+add_persistence_preset()
+{
+ local PRESET="${1}"
+ local PRESET_SOURCE="${2}"
+ local CONFIG="${3}"
+ if [ "${PERSISTENCE_READONLY}" = true ]
+ then
+ warning "We are trying to add a persistence preset while" \
+ "persistence is in read-only mode"
+ else
+ echo "${PRESET} source=${PRESET_SOURCE}" \
+ >> "${CONFIG}" || error "Failed to make ${PRESET}: $?"
+ warning "Successfully made ${PRESET} persistent"
+ fi
+}
+
+remove_persistence_preset()
+{
+ local PRESET="${1}"
+ local CONFIG="${2}"
+ # The intent here is to simply remove the line starting with
+ # $PRESET, but since it is a path, and sed uses / as delimiter
+ # for patterns we can use together with d, we have to escape /
+ # (and . as usual).
+ sed -i "/^$(escape './' "${PRESET}")\s/d" "${CONFIG}"
+}
+
+is_preset_enabled() {
+ local PRESET="${1}"
+ local PRESET_SOURCE="${2}"
+ local CONFIG="${3}"
+ grep --extended-regexp --line-regex --quiet --no-messages \
+ -e "$(escape_dots ${PRESET})\s+source=${PRESET_SOURCE}" "$CONFIG"
+}
+
+migrate_persistence_preset()
+{
+ local OLD="${1}"
+ local OLD_SOURCE="${2}"
+ local NEW="${3}"
+ local NEW_SOURCE="${4}"
+ local CONFIG="${5}"
+ if is_preset_enabled "${OLD}" "${OLD_SOURCE}" "${CONFIG}" && \
+ ! is_preset_enabled "${NEW}" "${NEW_SOURCE}" "${CONFIG}"
+ then
+ warning "Need to make ${NEW} persistent"
+ add_persistence_preset "${NEW}" "${NEW_SOURCE}" \
+ "${CONFIG}"
+ fi
+}
+
+# We override live-boot's logging facilities to get more useful error messages
+log_warning_msg ()
+{
+ warning ${@}
+}
+
+# We override live-boot's panic() since it does a lot of crazy stuff
+panic ()
+{
+ error ${@}
+}
+
+list_gpt_volumes ()
+{
+ local labels=${@}
+
+ local whitelistdev=""
+ case "${PERSISTENCE_MEDIA}" in
+ removable)
+ whitelistdev="$(removable_dev)"
+ [ -z "${whitelistdev}" ] && return
+ ;;
+ removable-usb)
+ whitelistdev="$(removable_usb_dev)"
+ [ -z "${whitelistdev}" ] && return
+ ;;
+ *)
+ if grep -qs -w -E '(live-media|bootfrom)=removable-usb' /proc/cmdline ; then
+ whitelistdev="$(removable_usb_dev)"
+ [ -z "${whitelistdev}" ] && return
+ elif grep -qs -w -E '(live-media|bootfrom)=removable' /proc/cmdline ; then
+ whitelistdev="$(removable_dev)"
+ [ -z "${whitelistdev}" ] && return
+ else
+ whitelistdev=""
+ fi
+ ;;
+ esac
+
+ for dev in $(storage_devices "" "${whitelistdev}")
+ do
+ if ( is_luks_partition ${dev} >/dev/null 2>&1 && \
+ echo ${PERSISTENCE_ENCRYPTION} | grep -qve "\" ) || \
+
+ ( ! is_luks_partition ${dev} >/dev/null 2>&1 && \
+ echo ${PERSISTENCE_ENCRYPTION} | grep -qve "\" )
+ then
+ continue
+ fi
+ local result="$(probe_for_gpt_name "${labels}" ${dev})"
+ if [ -n "${result}" ]
+ then
+ echo ${result#*=}
+ fi
+ done
+
+ exit 0
+}
+
+mountpoint_has_correct_access_rights ()
+{
+ local mountpoint="$1"
+ local expected_user=root
+ local expected_group=root
+ local expected_perms=775
+ local expected_acl="user::rwx
+user:tails-persistence-setup:rwx
+group::rwx
+mask::rwx
+other::r-x"
+
+ if [ $(stat -c %U "$mountpoint") != "$expected_user" ]
+ then
+ warning "'$mountpoint' is not owned by the '$expected_user' user"
+ return 1
+ fi
+ if [ $(stat -c %G "$mountpoint") != "$expected_group" ]
+ then
+ warning "'$mountpoint' is not owned by the '$expected_group' group"
+ return 2
+ fi
+ if [ $(stat -c %a "$mountpoint") != "$expected_perms" ]
+ then
+ warning "'$mountpoint' permissions are not $expected_perms"
+ return 4
+ fi
+ if [ "$(getfacl --omit-header --skip-base "$mountpoint" 2>/dev/null | grep -v '^\s*$')" \
+ != "$expected_acl" ]
+ then
+ warning "'$mountpoint' has incorrect ACL"
+ return 8
+ fi
+ return 0
+}
+
+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_acl=""
+
+ if [ $(stat -c %U "$conf") != "$expected_user" ]
+ then
+ warning "'$conf' is not owned by the '$expected_user' user"
+ return 1
+ fi
+ if [ $(stat -c %G "$conf") != "$expected_group" ]
+ then
+ warning "'$conf' is not owned by the '$expected_group' group"
+ return 2
+ fi
+ if [ $(stat -c %a "$conf") != "$expected_perms" ]
+ then
+ warning "'$conf' permissions are not $expected_perms"
+ return 4
+ fi
+ if [ "$(getfacl --omit-header --skip-base "$conf" 2>/dev/null | grep -v '^\s*$')" \
+ != "$expected_acl" ]
+ then
+ warning "'$conf' has incorrect ACL"
+ return 8
+ fi
+ return 0
+}
+
+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" "$mode"
+}
+
+create_empty_persistence_conf_file ()
+{
+ local conf="$1"
+ local mode="$2"
+
+ install --owner tails-persistence-setup \
+ --group tails-persistence-setup --mode "$mode" \
+ /dev/null "$conf" \
+ || error "Failed to create empty '$conf': $?"
+}
+
+activate_volumes ()
+{
+ local volumes=${@}
+ local ret=0
+ local open_volumes=""
+ local successes=""
+ local failures=""
+
+ # required by open_luks_device()
+ exec 6>&1
+
+ for vol in ${volumes}
+ do
+ if [ ! -b "${vol}" ]
+ then
+ warning "${vol} is not a block device"
+ failures="${failures} ${vol}"
+ ret=1
+ continue
+ fi
+ if [ -n "$(what_is_mounted_on ${dev})" ]
+ then
+ warning "${vol} is already mounted"
+ failures="${failures} ${vol}"
+ ret=1
+ continue
+ fi
+ local luks_vol=""
+ if /sbin/cryptsetup isLuks ${vol} >/dev/null 2>&1
+ then
+ if luks_vol=$(open_luks_device "${vol}")
+ then
+ open_volumes="${open_volumes} ${luks_vol}"
+ else
+ failures="${failures} ${vol}"
+ fi
+ else
+ open_volumes="${open_volumes} ${vol}"
+ fi
+ done
+
+ custom_mounts="$(mktemp /tmp/custom_mounts-XXXXXX.list)"
+ get_custom_mounts ${custom_mounts} ${open_volumes}
+ # ... and now the persistent volumes should be mounted.
+
+ # Enable the acl mount option on all persistent filesystems.
+ for mountpoint in $(ls -d /live/persistence/*_unlocked || true)
+ do
+ mount -o remount,acl "$mountpoint"
+ done
+
+ # Detect if we have incorrect ownership, permissions and ACL.
+ ACCESS_RIGHTS_ARE_CORRECT=true
+ for mountpoint in $(ls -d /live/persistence/*_unlocked || true)
+ do
+ if ! mountpoint_has_correct_access_rights "$mountpoint"
+ then
+ ACCESS_RIGHTS_ARE_CORRECT=false
+ break
+ fi
+ done
+
+ # Create live-additional-software.conf if there is none
+ for mountpoint in $(ls -d /live/persistence/*_unlocked || true)
+ do
+ if test ! -f "$mountpoint/live-additional-software.conf"
+ then
+ create_empty_persistence_conf_file "$mountpoint/live-additional-software.conf" "0644"
+ fi
+ done
+
+ # Disable all persistence configuration files if the mountpoint
+ # has wrong access rights.
+ if [ "$ACCESS_RIGHTS_ARE_CORRECT" != true ]
+ then
+ 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 || true)
+ do
+ 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.
+ for persistent_fs in $(ls -d /live/persistence/*_unlocked || true)
+ do
+ [ -d "$persistent_fs" ] || continue
+ for child in $(ls "$persistent_fs" || true)
+ do
+ subdir="$persistent_fs/$child"
+ [ -d "$subdir" ] || continue
+ # Note: we chmod even custom persistent directories.
+ # This may break things by changing otherwise correct
+ # permissions copied from the directory that was made
+ # persistent, so we only do that if the persistent directory
+ # is owned by amnesia:amnesia, and thus unlikely to be
+ # a system directory. This e.g. avoids setting wrong
+ # permissions on the APT, CUPS and NetworkManager
+ # persistent directories.
+ [ $(stat -c '%U' "$subdir") = 'amnesia' ] || continue
+ [ $(stat -c '%G' "$subdir") = 'amnesia' ] || continue
+ if [ "$PERSISTENCE_READONLY" = true ]
+ then
+ warning "Permissions of '$subdir' may need to be fixed, but read only was selected; please retry in read-write mode"
+ else
+ chmod go= "$subdir"
+ fi
+ done
+ done
+
+ # Load the new persistence.conf.
+ custom_mounts="$(mktemp /tmp/custom_mounts-XXXXXX.list)"
+ get_custom_mounts ${custom_mounts} ${open_volumes}
+
+ if [ -s "${custom_mounts}" ]
+ then
+ OLD_UMASK="$(umask)"
+ # Have activate_custom_mounts create new directories
+ # with safe permissions (#7443)
+ umask 0077
+ activate_custom_mounts ${custom_mounts} &> /dev/null
+ umask "$OLD_UMASK"
+ fi
+ rm -f ${custom_mounts} 2> /dev/null
+
+ # Update persistent GnuPG configuration for Stretch
+ if mountpoint --quiet /home/amnesia/.gnupg ; then
+ # Install current dirmngr.conf if there is no persistent one
+ if [ ! -e /home/amnesia/.gnupg/dirmngr.conf ]
+ then
+ install --owner amnesia --group amnesia --mode 0600 \
+ /etc/skel/.gnupg/dirmngr.conf \
+ /home/amnesia/.gnupg/dirmngr.conf \
+ || warning "Could not install dirmngr.conf"
+ fi
+ # Disable gpg.conf settings that either are obsolete,
+ # or would break communication with keyservers
+ if [ -e /home/amnesia/.gnupg/gpg.conf ]
+ then
+ obsolete_keyserver_options_str='http-proxy|ca-cert-file'
+ obsolete_keyserver_options_bool='no-try-dns-srv|no-honor-keyserver-url'
+ sed -i --regexp-extended \
+ "s/^(keyserver\s+)/#\1/ ; \
+ s/^(keyserver-options\s+($obsolete_keyserver_options_str)=)/\#\\1/ ; \
+ s/^(keyserver-options\s+($obsolete_keyserver_options_bool))\$/\#\\1/" \
+ /home/amnesia/.gnupg/gpg.conf \
+ || warning "Could not update gpg.conf"
+ 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
+ then
+ successes="${successes} ${vol}"
+ else
+ failures="${failures} ${vol}"
+ ret=1
+ fi
+ done
+
+ if [ -n "${successes}" ]
+ then
+ echo "Successes:"
+ for vol in ${successes}
+ do
+ echo " - ${vol}"
+ done
+ fi
+
+ if [ -n "${failures}" ]
+ then
+ echo "Failures:"
+ for vol in ${failures}
+ do
+ echo " - ${vol}"
+ done
+ fi
+ exit ${ret}
+}
+
+close_volumes ()
+{
+ local volumes=${@}
+ local custom_mounts="$(mktemp /tmp/custom_mounts-XXXXXX.list)"
+ get_custom_mounts ${custom_mounts} ${volumes}
+ while read device source dest options # < ${custom_mounts}
+ do
+ if [ "${options}" != linkfiles ]
+ then
+ umount ${dest} 2> /dev/null
+ fi
+ done < ${custom_mounts}
+ rm -f ${custom_mounts} 2> /dev/null
+ for vol in ${volumes}
+ do
+ local backing=$(where_is_mounted ${vol})
+ umount ${backing}
+ done
+}
+
+main ()
+{
+ # tracing get's activated by Arguments() if "debug" is in /proc/cmdline
+ # which may be something we don't want to flood stderr
+ exec 3<>"/dev/null"
+ BASH_XTRACEFD="3"
+
+ # parse the kernel cmdline for live-boot's configuration as defaults
+ Cmdline_old
+
+ # note that this is not enough for disabling tracing. we need to do the
+ # $BASH_XTRACEFD stuff above to avoid tracing until this point.
+ set +x
+
+ export PERSISTENCE="true"
+ export NOPERSISTENCE=""
+
+ # Should be set empty since live-boot already changed root for us
+ export rootmnt=""
+
+ while echo "${1}" | grep -qe "^--[^ ]\+\>"
+ do
+ case "${1}" in
+ --encryption=*)
+ export PERSISTENCE_ENCRYPTION="${1#*=}"
+ ;;
+ --help)
+ usage
+ exit 0
+ ;;
+ --log-file=*)
+ local log_file="${1#*=}"
+ [ -e "${log_file}" ] && rm -f "${log_file}"
+ exec 3<>"${log_file}"
+ set -x
+ ;;
+ --media=*)
+ export PERSISTENCE_MEDIA="${1#*=}"
+ ;;
+ --read-only)
+ export PERSISTENCE_READONLY="true"
+ ;;
+ --read-write)
+ export PERSISTENCE_READONLY=""
+ ;;
+ --union=*)
+ export UNIONTYPE="${1#*=}"
+ ;;
+ *)
+ error "unrecognized option: ${1}"
+ ;;
+ esac
+ shift
+ done
+
+ local action="${1}"
+ shift
+ case "${action}" in
+ list)
+ local labels=${@}
+ if ! echo ${labels} | grep -qe "[^[:space:]]"
+ then
+ # use default from live-helpers
+ labels=${custom_overlay_label}
+ fi
+ list_gpt_volumes ${labels}
+ ;;
+ activate|close)
+ if ! echo ${@} | grep -qe "[^[:space:]]"
+ then
+ error "you must specify at least one volume"
+ fi
+ ${action}_volumes "${@}"
+ ;;
+ "")
+ error "no action specified"
+ ;;
+ *)
+ error "unrecognized action: ${action}"
+ ;;
+ esac
+}
+
+main ${@}
diff --git a/config/chroot_local-includes/usr/local/sbin/nautilus b/config/chroot_local-includes/usr/local/sbin/nautilus
new file mode 100755
index 0000000000000000000000000000000000000000..682b9e026e51758d59199217765ff8f6e29c23f3
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/nautilus
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Without this wrapper, after closing Nautilus, one gets the prompt back
+# only after 5-15 seconds, which confuses users and makes our doc more
+# complicated than it should (#12034, #10391). So let's start Nautilus
+# silently in the background instead.
+
+set -eu
+
+exec /usr/bin/nautilus "$@" 2>/dev/null &
diff --git a/config/chroot_local-includes/usr/local/sbin/restart-tor b/config/chroot_local-includes/usr/local/sbin/restart-tor
new file mode 100755
index 0000000000000000000000000000000000000000..895b12c2676e670529b68134ec105d808bd5ff20
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/restart-tor
@@ -0,0 +1,43 @@
+#! /usr/bin/env python3
+
+import logging
+import time
+import sys
+
+import sh
+
+from tailslib.tor import tor_has_bootstrapped
+from tailslib.exceptions import TorFailedToBoostrapError
+
+
+logger = logging.getLogger(__name__)
+
+TIMEOUT = 270
+
+
+def main():
+ restart_tor()
+
+
+def restart_tor():
+ """ Restart the Tor systemd service
+
+ >>> restart_tor()
+ """
+ sh.systemctl('restart', 'tor@default.service')
+
+ for i in range(TIMEOUT):
+ if tor_has_bootstrapped():
+ logger.info("Tor has successfully bootstrapped")
+ return
+ time.sleep(1)
+
+ raise TorFailedToBoostrapError("Tor failed to bootstrap within %s seconds" % TIMEOUT)
+
+
+if __name__ == "__main__":
+ if len(sys.argv) > 1 and sys.argv[1] == 'doctest':
+ import doctest
+ doctest.testmod()
+ else:
+ main()
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-additional-software b/config/chroot_local-includes/usr/local/sbin/tails-additional-software
new file mode 100755
index 0000000000000000000000000000000000000000..4c3379e92ae43f0f34ce9e7ca1a4a4b0bd1c2230
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/tails-additional-software
@@ -0,0 +1,679 @@
+#!/usr/bin/env python3
+
+import gettext
+import json
+import logging
+import logging.handlers
+import os
+import os.path
+import pwd
+import shutil
+import subprocess
+import sys
+
+import apt.cache
+
+from tailslib import LIVE_USERNAME
+
+from tailslib.additionalsoftware import (
+ ASPDataError,
+ 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
+
+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.
+
+ Launch apt-get with given arguments list, log its standard and error output
+ and return its returncode."""
+ apt_get_env = os.environ.copy()
+ # The environnment provided in GDM PostLogin hooks doesn't contain /sbin/
+ # which is required by dpkg. Let's use the default path for root in Tails.
+ apt_get_env['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:" \
+ "/usr/bin:/sbin:/bin"
+ # We will log the output and want it in English when included in bug
+ # reports
+ apt_get_env['LANG'] = "C"
+ apt_get_env['DEBIAN_PRIORITY'] = "critical"
+ args = ["apt-get", "--quiet", "--yes"]
+ args.extend(specific_args)
+ apt_get = subprocess.Popen(args,
+ env=apt_get_env,
+ universal_newlines=True,
+ stderr=subprocess.STDOUT,
+ stdout=subprocess.PIPE)
+ for line in iter(apt_get.stdout.readline, ''):
+ if not line.startswith('('):
+ logging.info(line.rstrip())
+ apt_get.wait()
+ if apt_get.returncode:
+ logging.warning("apt-get exited with returncode %i"
+ % apt_get.returncode)
+ return apt_get.returncode
+
+
+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:
+ 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:
+ logging.warning("Warning: unable to notify the user. %s" % e)
+ logging.warning("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 _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 or to view the system
+ log.
+ """
+ if details:
+ # Translators: Don't translate {details}, it's a placeholder and will
+ # be replaced.
+ details = _("{details} Please check your list of additional "
+ "software or read the system log to "
+ "understand the problem.").format(details=details)
+
+ else:
+ details = _("Please check your list of additional "
+ "software or read the system log to "
+ "understand the problem.")
+
+ 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:
+ # Translators: Don't translate {beginning} or {last}, they are
+ # placeholders and will be replaced.
+ 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.warning("Warning: {}".format(e))
+ return False
+ if os.path.isfile(packages_list_path):
+ logging.info("Found additional packages list.")
+ return True
+ else:
+ logging.warning("Warning: no configuration file found.")
+ return False
+
+
+def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
+ """Delete the copy of the old APT lists, if any."""
+ shutil.rmtree(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):
+ logging.warning("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)
+
+
+# Note: we can't do nicer delete + move operations because the directory
+# we want to replace is bind-mounted. So we have to delete the content
+# we want to replace, and then move the content we want to restore.
+def restore_old_apt_lists(srcdir=OLD_APT_LISTS_DIR, dstdir=APT_LISTS_DIR):
+ """Restore the copy of the old APT lists."""
+ # Empty dstdir
+ for basename in os.listdir(dstdir):
+ path = os.path.join(dstdir, basename)
+ if os.path.isfile(path):
+ os.remove(path)
+ elif os.path.isdir(path):
+ shutil.rmtree(path)
+ # Move the content of srcdir to dstdir
+ for basename in os.listdir(srcdir):
+ path = os.path.join(srcdir, basename)
+ shutil.move(path, dstdir)
+
+
+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):
+ # Translators: Don't translate {packages}, it's a placeholder and will
+ # be replaced.
+ 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.warning("Warning: persistence storage is locked, can't add "
+ "additional software.")
+ elif is_tails_media_writable():
+ # Translators: Don't translate {packages}, it's a placeholder and will
+ # be replaced.
+ 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 "
+ "Additional Software 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.warning("Cannot create persistent storage on this media.")
+ if not os.path.isfile(ASP_STATE_INSTALLER_ASKED):
+ open(ASP_STATE_INSTALLER_ASKED, 'a').close()
+ # Translators: Don't translate {packages}, it's a placeholder and
+ # will be replaced.
+ _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 Tails Installer."),
+ 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)
+ # Translators: Don't translate {packages}, it's a placeholder and will be
+ # replaced.
+ if _notify(_("Remove {packages} from your additional software?").format(
+ packages=_format_iterable(packages)),
+ # Translators: Don't translate {packages}, it's a placeholder
+ # and will be replaced.
+ _("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()
+ if not line.startswith("VERSION 3"):
+ raise ASPDataError("APT data is not 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 = {
+ filter_package_details(pkg) for pkg in
+ 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 = {
+ pkg for pkg in packages["installed"] if (
+ not apt_cache[pkg].is_auto_installed and
+ pkg not in additional_packages_names)
+ }
+
+ 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
+ # attempt has not completed successfully (it may have failed e.g.
+ # due to network problems, or it may have been interrupted).
+ # In many of these cases, the APT package cache lacks some
+ # packages the new APT lists reference, so the (offline)
+ # 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 upgrade_mode:
+ logging.warning("Found a copy of old APT lists, restoring it.")
+ try:
+ restore_old_apt_lists()
+ except Exception as e:
+ logging.warning("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
+ # restored, chances are restoration will fail next time
+ # as well.
+ delete_old_apt_lists()
+
+ packages = get_additional_packages()
+ if not packages:
+ logging.warning("Warning: no packages to install, exiting")
+ return True
+ 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"] + list(packages))
+ if apt_get_returncode:
+ logging.warning("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:
+ 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.
+ logging.info("Saving old APT lists...")
+ save_old_apt_lists()
+
+ apt_get_returncode = _launch_apt_get(["update"])
+ if apt_get_returncode:
+ logging.warning("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(upgrade_mode=True):
+ logging.info("The upgrade was successful.")
+ else:
+ _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,
+ # so we can delete the copy of the old lists
+ delete_old_apt_lists()
+
+ # Remove outdated packages from the local package cache. This is needed as
+ # we disable apt-daily.timer, which would else take care of this cleanup.
+ # We do this after the upgrade has succeeded so that the old packages
+ # remain available in the cache in case we have to restore the old lists.
+ # In the past we did this before upgrading in order to remove the
+ # i386 packages from the cache before downloading amd64 ones, but
+ # this does not matter anymore now that all persistent volumes
+ # must have been upgraded already.
+ apt_get_returncode = _launch_apt_get(["autoclean"])
+ if apt_get_returncode:
+ logging.warning("Warning: autoclean failed.")
+ return True
+
+
+def print_help():
+ """Subcommand which displays help."""
+ sys.stderr.write("Usage: %s \n" % program_name)
+ sys.stderr.write("""Subcommands:
+ install: install additional software
+ upgrade: upgrade additional software\n""")
+
+
+if __name__ == "__main__":
+ program_name = os.path.basename(sys.argv[0])
+
+ # 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"
+ stderr_handler = logging.StreamHandler()
+ file_handler = logging.FileHandler(ASP_LOG_FILE)
+ logging.basicConfig(format=log_format,
+ handlers=[stderr_handler, file_handler],
+ level=log_level)
+
+ gettext.install("tails")
+
+ if len(sys.argv) < 2:
+ print_help()
+ sys.exit(2)
+
+ if sys.argv[1] == "install":
+ if not install_additional_packages():
+ sys.exit(150)
+ 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 0000000000000000000000000000000000000000..4ee6a2b4e728b344a1abedf52ac11192ab1c3358
--- /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 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
new file mode 100755
index 0000000000000000000000000000000000000000..a0babdef1a50ae4029b867105877293ed14fceb9
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/tails-debugging-info
@@ -0,0 +1,160 @@
+#! /usr/bin/env python3
+"""
+Debug Tails.
+
+Test with "python3 tails-debugging-info.py doctest" as root.
+
+goodcrypto.com converted from bash to python and added basic tests.
+
+*** WARNING about debug_file and debug_directory *********************
+
+Great attention must be given to the ownership situation of these
+files and their parent directories in order to avoid a symlink-based
+attack that could read the contents of any file and make it
+accessible to the user running this script (typically the live
+user). Therefore, when adding a new file, give as the first argument
+'root' only if the complete path to it (including the file itself)
+is owned by root and already exists before the system is connected to
+the network (that is, before GDM's PostLogin script is run).
+If not, the following rules must be followed strictly:
+
+* only one non-root user is involved in the ownership situation (the
+ file, its dir and the parent dirs). From now on let's assume it is
+ the case and call it $USER.
+
+* if any non-root group has write access, it must not have any
+ members.
+
+If any of these rules does not apply, the file cannot be added here
+safely and something is probably quite wrong and should be
+investigated carefully.
+
+>>> # run script
+>>> import sh
+>>> this_command = sh.Command(sys.argv[0])
+>>> this_command()
+
+...
+"""
+
+import json
+import os
+import sys
+import subprocess
+from pwd import getpwuid
+
+
+# AppArmor Ux rules don't sanitize PATH, which can lead to an
+# exploited application (that's allowed to run this script unconfined)
+# having this script run arbitrary code, violating that application's
+# confinement. Let's prevent that by setting PATH to a list of
+# directories where only root can write.
+os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
+
+
+def main():
+ """Print debug information serialized as json.
+
+ >>> main()
+
+ ...
+ """
+
+ 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):
+ """Return the command and it's standard output as dict.
+
+ >>> debug_command('echo', 'foo')
+ {...'key': 'echo foo'...}
+ """
+ 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):
+ """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)
+ {...'content': ['foo', 'bar']...}
+ """
+ if not os.path.isfile(filename):
+ 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:
+ return {'key': filename, 'content': '''WARNING: not opening file {}, because it is '''
+ '''owned by {} instead of {}'''.format(filename, owner, user)}
+
+ file_content = []
+ with open(filename) as f:
+ for l in f:
+ file_content.append(l.replace('\n', ''))
+ return {'key': filename, 'content': file_content}
+
+
+def debug_directory(user, dir_name):
+ """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 {'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:
+ 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)
+
+ listing = []
+
+ for f in files:
+ listing.append(debug_file(user, os.path.join(dir_name, f)))
+ return {'key': dir_name, 'content': listing}
+
+
+if __name__ == '__main__':
+ if sys.argv and len(sys.argv) > 1:
+ if sys.argv[1] == 'doctest':
+ import doctest
+ doctest.testmod(optionflags=doctest.ELLIPSIS)
+ else:
+ main()
+ else:
+ main()
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-notify-user b/config/chroot_local-includes/usr/local/sbin/tails-notify-user
new file mode 100755
index 0000000000000000000000000000000000000000..6c89d67ba8b10c964f927e52e5657814c9d77d79
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/tails-notify-user
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+set -e
+
+# Import export_gnome_env().
+. /usr/local/lib/tails-shell-library/gnome.sh
+
+# Get LIVE_USERNAME
+. /etc/live/config.d/username.conf
+
+if [ $# -ne 2 ] && [ $# -ne 3 ]; then
+ echo "Usage: $0 SUMMARY BODY [TIMEOUT]" >&2
+ exit 16
+fi
+
+# Notify the desktop user
+summary="$1"
+body="$2"
+
+if [ -n "$3" ]; then
+ timeout_args="--expire-time=$3"
+fi
+(
+ export_gnome_env
+ exec /bin/su -c "notify-send ${timeout_args} \"${summary}\" \"${body}\"" "${LIVE_USERNAME}" &
+)
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-tor-launcher b/config/chroot_local-includes/usr/local/sbin/tails-tor-launcher
new file mode 100755
index 0000000000000000000000000000000000000000..761270b4666a5df72488d030b77506c1cc84aa4e
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/tails-tor-launcher
@@ -0,0 +1,33 @@
+#!/bin/sh
+
+set -e
+
+# Import export_gnome_env().
+. /usr/local/lib/tails-shell-library/gnome.sh
+
+# Get LIVE_USERNAME
+. /etc/live/config.d/username.conf
+
+# Get LANG
+. /etc/default/locale
+
+# The Tor Browser hardcodes the default profile dir to inside
+# ../TorBrowser/Data/Browser/ from the folder storing the
+# application.ini file supplied via -app. We can use -profile to load
+# it from a different place, but then the Caches directory
+# must still exist and be accessible in the above folder.
+mkdir -p /usr/local/lib/TorBrowser/Data/Browser/Caches
+chmod -R a+rX /usr/local/lib/TorBrowser
+
+until pgrep -u "${LIVE_USERNAME}" '^ibus-daemon' >/dev/null ; do
+ sleep 5
+done
+
+export LANG
+export_gnome_env
+sudo -u ${LIVE_USERNAME} xhost +SI:localuser:tor-launcher
+pkexec -u tor-launcher /usr/local/bin/tor-launcher -- "$@"
+RET=${?}
+sudo -u ${LIVE_USERNAME} xhost -SI:localuser:tor-launcher
+
+exit ${RET}
diff --git a/config/chroot_local-includes/usr/local/sbin/tor-has-bootstrapped b/config/chroot_local-includes/usr/local/sbin/tor-has-bootstrapped
new file mode 100755
index 0000000000000000000000000000000000000000..326e09a6d8e4e67db711ff1e68a15ba8d5493485
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/tor-has-bootstrapped
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+/bin/systemctl --quiet is-active tor@default.service || exit 1
+/bin/systemctl --quiet is-active tails-tor-has-bootstrapped.target
diff --git a/config/chroot_local-includes/usr/local/sbin/unsafe-browser b/config/chroot_local-includes/usr/local/sbin/unsafe-browser
new file mode 100755
index 0000000000000000000000000000000000000000..24eb30e0ab9ae8efb6be812c59cb5e7fa615571c
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/unsafe-browser
@@ -0,0 +1,127 @@
+#!/bin/sh
+
+set -e
+set -u
+
+. gettext.sh
+TEXTDOMAIN="tails"
+export TEXTDOMAIN
+
+# Import tor_is_working()
+. /usr/local/lib/tails-shell-library/tor.sh
+
+# Import the TBB_EXT variable, and guess_best_tor_browser_locale().
+. /usr/local/lib/tails-shell-library/tor-browser.sh
+
+# Import localized_tails_doc_page().
+. /usr/local/lib/tails-shell-library/localization.sh
+
+# Import try_cleanup_browser_chroot(), setup_browser_chroot(),
+# configure_chroot_dns_servers(), configure_chroot_browser(),
+# configure_chroot_browser(), set_chroot_browser_locale()
+# set_chroot_browser_name(), set_chroot_browser_permissions()
+# and run_browser_in_chroot().
+. /usr/local/lib/tails-shell-library/chroot-browser.sh
+
+error () {
+ local cli_text="${CMD}: `gettext \"error:\"` ${@}"
+ local dialog_text="`gettext \"Error\"`
+
+${@}"
+ echo "${cli_text}" >&2
+ sudo -u "${SUDO_USER}" zenity --error --title "" --text "${dialog_text}"
+ exit 1
+}
+
+verify_start () {
+ # Make sure the user really wants to start the browser
+ local dialog_msg="`gettext \"Do you really want to launch the Unsafe Browser?\"`
+
+`gettext \"Network activity within the Unsafe Browser is not anonymous.\\nOnly use the Unsafe Browser if necessary, for example\\nif you have to login or register to activate your Internet connection.\"`"
+ local launch="`gettext \"_Launch\"`"
+ local exit="`gettext \"_Exit\"`"
+ if ! sudo -u "${SUDO_USER}" \
+ zenity --question --title "" --text "${dialog_msg}" --default-cancel \
+ --ok-label "${launch}" --cancel-label "${exit}"; then
+ exit 0
+ fi
+}
+
+show_start_notification () {
+ local title="`gettext \"Starting the Unsafe Browser...\"`"
+ local body="`gettext \"This may take a while, so please be patient.\"`"
+ tails-notify-user "${title}" "${body}" 10000
+}
+
+show_shutdown_notification () {
+ local title="`gettext \"Shutting down the Unsafe Browser...\"`"
+ local body="`gettext \"This may take a while, and you may not restart the Unsafe Browser until it is properly shut down.\"`"
+ tails-notify-user "${title}" "${body}" 10000
+}
+
+maybe_restart_tor () {
+ # Restart Tor if it's not working (a captive portal may have prevented
+ # Tor from bootstrapping, and a restart is the fastest way to get
+ # wheels turning)
+ if ! tor_is_working; then
+ echo "* Restarting Tor"
+ restart-tor
+ if ! systemctl --quiet is-active tor@default.service; then
+ error "`gettext \"Failed to restart Tor.\"`"
+ fi
+ fi
+}
+
+# Main script:
+
+CMD="$(basename "${0}")"
+LOCK="/var/lock/${CMD}"
+CONF_DIR="/var/lib/unsafe-browser"
+COW="${CONF_DIR}/cow"
+CHROOT="${CONF_DIR}/chroot"
+BROWSER_NAME="unsafe-browser"
+BROWSER_USER="clearnet"
+HUMAN_READABLE_NAME="`gettext \"Unsafe Browser\"`"
+WARNING_PAGE='/usr/share/doc/tails/website/misc/unsafe_browser_warning'
+HOME_PAGE="$(localized_tails_doc_page "${WARNING_PAGE}")"
+
+# Prevent multiple instances of the script.
+exec 9>"${LOCK}"
+if ! flock -x -n 9; then
+ error "`gettext \"Another Unsafe Browser is currently running, or being cleaned up. Please retry in a while.\"`"
+fi
+
+verify_start
+show_start_notification
+
+echo "* Setting up chroot"
+setup_chroot_for_browser "${CHROOT}" "${COW}" "${BROWSER_USER}" || \
+ error "`gettext \"Failed to setup chroot.\"`"
+
+echo "* Configuring chroot"
+configure_chroot_browser "${CHROOT}" "${BROWSER_USER}" "${BROWSER_NAME}" \
+ "${HUMAN_READABLE_NAME}" "${HOME_PAGE}" "${TBB_EXT}"/langpack-*.xpi || \
+ error "`gettext \"Failed to configure browser.\"`"
+# If /etc/resolv-over-clearnet.conf file is empty or doesn't exist, we
+# have no clearnet DNS server.
+if [ "$(stat --format=%s /etc/resolv-over-clearnet.conf || echo 0)" -gt 0 ]; then
+ mount --bind /etc/resolv-over-clearnet.conf "${CHROOT}"/etc/resolv.conf
+else
+ error "`gettext \"No DNS server was obtained through DHCP or manually configured in NetworkManager.\"`"
+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}" \
+ 'Unsafe Browser' || \
+ error "`gettext \"Failed to run browser.\"`"
+
+echo "* Exiting the Unsafe Browser"
+show_shutdown_notification
+maybe_restart_tor
+
+exit 0
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 0000000000000000000000000000000000000000..58f26152922d33fbd80a323b44001948448b2641
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/share/mime/packages/unlock-veracrypt-volumes.xml.in
@@ -0,0 +1,9 @@
+
+
+
+ <_comment>TrueCrypt/VeraCrypt container
+
+
+
+
+
diff --git a/config/chroot_local-includes/usr/share/X11/xorg.conf.d/90-tails.conf b/config/chroot_local-includes/usr/share/X11/xorg.conf.d/90-tails.conf
new file mode 100644
index 0000000000000000000000000000000000000000..f2e6c46afae19c6077fb40eaf43950f7d609802a
--- /dev/null
+++ b/config/chroot_local-includes/usr/share/X11/xorg.conf.d/90-tails.conf
@@ -0,0 +1,6 @@
+Section "InputClass"
+ Identifier "Tails-touchpad-configuration"
+ MatchIsTouchpad "on"
+ Option "TapButton1" "1"
+ Option "VertTwoFingerScroll" "1"
+EndSection
diff --git a/config/chroot_local-includes/usr/share/applications/org.boum.tails.additional-software-config.desktop.in b/config/chroot_local-includes/usr/share/applications/org.boum.tails.additional-software-config.desktop.in
new file mode 100644
index 0000000000000000000000000000000000000000..b043305301412401c3bb06ac8f5a28355b8b17ac
--- /dev/null
+++ b/config/chroot_local-includes/usr/share/applications/org.boum.tails.additional-software-config.desktop.in
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Application
+_Name=Additional Software
+_Comment=Configure the additional software installed from your persistent storage when starting Tails
+Exec=tails-additional-software-config
+Icon=package-x-generic
+Terminal=false
+Categories=System;Tails;
+StartupNotify=true
+StartupWMClass=tails-additional-software-config
diff --git a/config/chroot_local-includes/usr/share/applications/tails-about.desktop.in b/config/chroot_local-includes/usr/share/applications/tails-about.desktop.in
new file mode 100644
index 0000000000000000000000000000000000000000..078514871a040e49c4833c1813c17c6d3c44d994
--- /dev/null
+++ b/config/chroot_local-includes/usr/share/applications/tails-about.desktop.in
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+_Name=About Tails
+_Comment=Learn more about Tails
+Exec=tails-about
+Icon=gtk-about
+Terminal=false
+Categories=Utilities;Tails;
+StartupNotify=true
diff --git a/config/chroot_local-includes/usr/share/applications/tails-documentation.desktop.in b/config/chroot_local-includes/usr/share/applications/tails-documentation.desktop.in
new file mode 100644
index 0000000000000000000000000000000000000000..d7bd7732b7a78f02732889933cc491660674e14e
--- /dev/null
+++ b/config/chroot_local-includes/usr/share/applications/tails-documentation.desktop.in
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Version=1.0
+Encoding=UTF-8
+_Name=Tails documentation
+_Comment=Learn how to use Tails
+Categories=Documentation;Tails;
+Type=Application
+Terminal=false
+Exec=/usr/local/bin/tails-documentation doc
+Icon=/usr/share/icons/gnome/48x48/categories/system-help.png
+StartupNotify=true
diff --git a/config/chroot_local-includes/usr/share/applications/tor-browser.desktop.in b/config/chroot_local-includes/usr/share/applications/tor-browser.desktop.in
new file mode 100644
index 0000000000000000000000000000000000000000..c73e587780646835a899e4f6d85ce97b6ffaaf27
--- /dev/null
+++ b/config/chroot_local-includes/usr/share/applications/tor-browser.desktop.in
@@ -0,0 +1,11 @@
+[Desktop Entry]
+_Name=Tor Browser
+_Comment=Anonymous Web Browser
+_GenericName=Anonymous Web Browser
+Categories=Network;
+Icon=/usr/local/lib/tor-browser/browser/chrome/icons/default/default128.png
+Terminal=false
+Type=Application
+Exec=/usr/local/bin/tor-browser %u
+StartupNotify=true
+StartupWMClass=Tor Browser
diff --git a/config/chroot_local-includes/usr/share/applications/unlock-veracrypt-volumes.desktop.in b/config/chroot_local-includes/usr/share/applications/unlock-veracrypt-volumes.desktop.in
new file mode 100644
index 0000000000000000000000000000000000000000..68727ebb491aef0588d6f559e84dcd8bb60fe9ac
--- /dev/null
+++ b/config/chroot_local-includes/usr/share/applications/unlock-veracrypt-volumes.desktop.in
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Type=Application
+_Name=Unlock VeraCrypt Volumes
+_Comment=Mount VeraCrypt encrypted file containers and devices
+Icon=unlock-veracrypt-volumes.png
+Exec=unlock-veracrypt-volumes %U
+MimeType=application/x-tcrypt-container
+Terminal=false
+Categories=GTK;Encryption;Utility;X-GNOME-Utilities;
+Keywords=VeraCrypt;TrueCrypt;Encryption;Volume;Container;Device;Mount;Unlock;Decrypt
+StartupNotify=true
+StartupWMClass=unlock-veracrypt-volumes
diff --git a/config/chroot_local-includes/usr/share/applications/unsafe-browser.desktop.in b/config/chroot_local-includes/usr/share/applications/unsafe-browser.desktop.in
new file mode 100644
index 0000000000000000000000000000000000000000..7abce9e9d77d6654d5e236c3c8cf112e1d35d63d
--- /dev/null
+++ b/config/chroot_local-includes/usr/share/applications/unsafe-browser.desktop.in
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Encoding=UTF-8
+_Name=Unsafe Browser
+_Comment=Browse the World Wide Web without anonymity
+_GenericName=Uns