3 # Returns a hash that for each persistence preset the running Tails is aware of,
4 # for each of the corresponding configuration lines,
5 # maps the source to the destination.
6 def get_persistence_presets_config(skip_links = false)
7 # Python script that prints all persistence configuration lines (one per
8 # line) in the form: <mount_point>\t<comma-separated-list-of-options>
10 'from tps.configuration import features',
11 'for feature in features.get_classes():',
12 ' for mount in feature.Mounts:',
15 c = RemoteShell::PythonCommand.new($vm, script.join("\n"))
16 assert(c.success?, 'Python script for get_persistence_presets_config failed')
17 presets_configs = c.stdout.chomp.split("\n")
18 assert presets_configs.size >= 10,
19 "Got #{presets_configs.size} persistence preset configuration " \
20 'lines, which is too few'
21 persistence_mapping = {}
22 presets_configs.each do |line|
23 destination, options_str = line.split("\t")
24 options = options_str.split(',')
25 is_link = options.include? 'link'
26 next if is_link && skip_links
28 source_str = options.find { |option| /^source=/.match(option) }
29 # If no source is given as an option, live-boot's persistence
30 # feature defaults to the destination minus the initial "/".
31 source = if source_str.nil?
32 destination.partition('/').last
34 source_str.split('=')[1]
36 persistence_mapping[source] = destination
42 get_persistence_presets_config
46 get_persistence_presets_config(true)
49 def persistent_volumes_mountpoints
50 $vm.execute('ls -1 -d /live/persistence/*_unlocked/').stdout.chomp.split
53 # Returns the list of mountpoints which are configured in persistence.conf
54 def configured_persistent_mountpoints
56 '/live/persistence/TailsData_unlocked/persistence.conf'
57 ).split("\n").map do |line|
62 def persistent_storage_frontend
63 Dogtail::Application.new('tps-frontend')
66 def persistent_storage_main_frame
67 persistent_storage_frontend.child('Persistent Storage', roleName: 'frame')
70 def persistent_directory_is_active(**opts)
72 opts[:use_system_bus] = true
74 'org.boum.tails.PersistentStorage',
75 '/org/boum/tails/PersistentStorage/Features/PersistentDirectory',
76 "org.freedesktop.DBus.Properties.Get",
77 "org.boum.tails.PersistentStorage.Feature",
83 def recover_from_upgrader_failure
84 $vm.execute('pkill --full tails-upgrade-frontend-wrapper')
85 $vm.execute('killall tails-upgrade-frontend zenity')
86 # Do not sleep when retrying
87 $vm.spawn('tails-upgrade-frontend-wrapper --no-wait', user: LIVE_USER)
90 Given /^I clone USB drive "([^"]+)" to a (new|temporary) USB drive "([^"]+)"$/ do |from, mode, to|
91 $vm.storage.clone_to_new_disk(from, to)
92 if mode == 'temporary'
93 add_after_scenario_hook { $vm.storage.delete_volume(to) }
97 Given /^I unplug USB drive "([^"]+)"$/ do |name|
98 $vm.unplug_drive(name)
101 Given /^the computer is set to boot in UEFI mode$/ do
102 $vm.set_os_loader('UEFI')
106 def tails_installer_selected_device
107 @installer.child('Target USB stick:', roleName: 'label').parent
108 .child('', roleName: 'combo box', recursive: false).name
111 def tails_installer_is_device_selected?(name)
112 device = $vm.disk_dev(name)
113 tails_installer_selected_device[/\(#{device}\d*\)$/]
116 def tails_installer_match_status(pattern)
117 @installer.child('', roleName: 'text').text[pattern]
120 When /^I start Tails Installer$/ do
121 @installer_log_path = '/tmp/tails-installer.log'
122 command = "/usr/local/bin/tails-installer --verbose 2>&1 | tee #{@installer_log_path} | logger -t tails-installer"
123 step "I run \"#{command}\" in GNOME Terminal"
124 @installer = Dogtail::Application.new('tails-installer')
125 @installer.child('Tails Installer', roleName: 'frame')
126 # Sometimes Dogtail will find the Installer and click its window
127 # before it is shown (searchShowingOnly is not perfect) which
128 # generally means clicking somewhere on the Terminal => the click is
129 # lost *and* the installer does not go to the foreground. So let's
132 @screen.wait('TailsInstallerWindow.png', 10).click
135 When /^I am told by Tails Installer that.*"([^"]+)".*$/ do |status|
137 tails_installer_match_status(status)
141 Then /^a suitable USB device is (?:still )?not found$/ do
143 'No device suitable to install Tails could be found', roleName: 'label'
147 Then /^(no|the "([^"]+)") USB drive is selected$/ do |mode, name|
150 tails_installer_selected_device == ''
152 tails_installer_is_device_selected?(name)
157 def persistence_exists?(name)
158 data_part_dev = $vm.persistent_storage_dev_on_disk(name)
159 $vm.execute("test -b #{data_part_dev}").success?
162 When /^I (install|reinstall|upgrade) Tails (?:to|on) USB drive "([^"]+)" by cloning$/ do |action, name|
163 step 'I start Tails Installer'
164 # If the device was plugged *just* before this step, it might not be
165 # completely ready (so it's shown) at this stage.
166 try_for(10) { tails_installer_is_device_selected?(name) }
168 label = if action == 'reinstall'
169 'Reinstall (delete all data)'
173 # Despite being a normal "push button" this button doesn't respond
174 # to the "press" action. It has a "click" action, which works, but
175 # after that the installer is inaccessible for Dogtail.
176 @installer.button(label).grabFocus
177 @screen.press('Enter')
178 unless action == 'upgrade'
179 confirmation_label = if persistence_exists?(name)
180 'Delete Persistent Storage and Reinstall'
182 'Delete All Data and Install'
184 @installer.child('Question',
185 roleName: 'alert').button(confirmation_label).click
187 try_for(15 * 60, delay: 10) do
189 .child('Information', roleName: 'alert')
190 .child('Installation complete!', roleName: 'label')
193 rescue StandardError => e
194 debug_log("Tails Installer debug log:\n" +
195 $vm.file_content(@installer_log_path))
200 Given(/^I plug and mount a USB drive containing a Tails USB image$/) do
201 usb_image_dir = share_host_files(TAILS_IMG)
202 @usb_image_path = "#{usb_image_dir}/#{File.basename(TAILS_IMG)}"
205 def enable_all_persistence_presets
206 assert persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
207 switches = persistent_storage_main_frame.children(roleName: 'toggle button')
208 switches.each do |switch|
210 debug_log("#{switch.name} is already enabled, skipping")
212 debug_log("enabling #{switch.name}")
213 # To avoid having to bother with scrolling the window we just
214 # send an AT-SPI action instead of clicking.
216 try_for(10) { switch.checked }
221 When /^I disable the first persistence preset$/ do
222 step 'I start "Persistent Storage" via GNOME Activities Overview'
223 assert persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
224 persistent_folder_switch = persistent_storage_main_frame.child(
225 'Activate Persistent Folder',
226 roleName: 'toggle button'
228 assert persistent_folder_switch.checked
229 persistent_folder_switch.toggle
231 assert !persistent_folder_switch.checked
232 # GtkSwitch does not expose its underlying state via AT-SPI (the
233 # accessible has the "check" state when the switch is on but the
234 # underlying state is false) so we check via D-Bus that the
235 # Persistent Directory feature is inactive.
236 !persistent_directory_is_active
238 @screen.press('alt', 'F4')
241 Given(/^I enable persistence creation in Tails Greeter$/) do
242 @screen.wait('TailsGreeterPersistenceCreate.png', 10).click
245 Given /^I create a persistent partition( with the default settings| for Additional Software)?( using the wizard that was already open)?$/ do |mode, dontrun|
246 # XXX: the wording here could be misleading. Pay attention when reading it! (or, please improve it)
247 default_settings = mode
248 asp = mode == ' for Additional Software'
249 unless asp || dontrun
250 step 'I start "Persistent Storage" via GNOME Activities Overview'
252 persistent_storage_main_frame.button('Co_ntinue').click
253 persistent_storage_main_frame
254 .child('Passphrase:', roleName: 'label')
257 @screen.type(@persistence_password)
258 persistent_storage_main_frame
259 .child('Confirm:', roleName: 'label')
262 @screen.type(@persistence_password)
263 persistent_storage_main_frame.button('_Create Persistent Storage').click
265 persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
267 enable_all_persistence_presets unless default_settings
270 def check_disk_integrity(name, dev, scheme)
271 info = $vm.execute("udisksctl info --block-device '#{dev}'").stdout
272 info_split = info.split("\n org\.freedesktop\.UDisks2\.PartitionTable:\n")
273 part_table_info = info_split[1]
274 assert_match(/^ Type: +#{scheme}/, part_table_info,
275 "Unexpected partition scheme on USB drive '#{name}', '#{dev}'")
278 def check_part_integrity(name, dev, usage, fs_type,
279 part_label: nil, part_type: nil)
280 info = $vm.execute("udisksctl info --block-device '#{dev}'").stdout
281 info_split = info.split("\n org\.freedesktop\.UDisks2\.Partition:\n")
282 dev_info = info_split[0]
283 part_info = info_split[1]
284 assert_match(/^ IdUsage: +#{usage}$/, dev_info,
285 "Unexpected device field 'usage' on drive '#{name}', '#{dev}'")
286 assert_match(/^ IdType: +#{fs_type}$/, dev_info,
287 "Unexpected device field 'IdType' on drive '#{name}', '#{dev}'")
289 assert_match(/^ Name: +#{part_label}$/, part_info,
290 "Unexpected partition label on drive '#{name}', '#{dev}'")
293 assert_match(/^ Type: +#{part_type}$/, part_info,
294 "Unexpected partition type on drive '#{name}', '#{dev}'")
298 def tails_is_installed_helper(name, tails_root, loader)
299 disk_dev = $vm.disk_dev(name)
300 part_dev = disk_dev + '1'
301 check_disk_integrity(name, disk_dev, 'gpt')
302 check_part_integrity(name, part_dev, 'filesystem', 'vfat',
303 part_label: 'Tails', part_type: ESP_GUID)
305 target_root = '/mnt/new'
306 $vm.execute("mkdir -p #{target_root}")
307 $vm.execute("mount #{part_dev} #{target_root}")
309 c = $vm.execute("diff -qr '#{tails_root}/live' '#{target_root}/live'")
312 "USB drive '#{name}' has differences in /live:\n#{c.stdout}\n#{c.stderr}"
315 syslinux_files = $vm.execute("ls -1 #{target_root}/syslinux")
317 # We deal with these files separately
318 ignores = ['syslinux.cfg', 'exithelp.cfg', 'ldlinux.c32', 'ldlinux.sys']
319 (syslinux_files - ignores).each do |f|
320 assert_vmcommand_success(
321 $vm.execute("diff -q '#{tails_root}/#{loader}/#{f}' " \
322 "'#{target_root}/syslinux/#{f}'"),
323 "USB drive '#{name}' has differences in '/syslinux/#{f}'"
327 # The main .cfg is named differently vs isolinux
328 assert_vmcommand_success(
329 $vm.execute("diff -q '#{tails_root}/#{loader}/#{loader}.cfg' " \
330 "'#{target_root}/syslinux/syslinux.cfg'"),
331 "USB drive '#{name}' has differences in '/syslinux/syslinux.cfg'"
334 $vm.execute("umount #{target_root}")
338 Then /^the running Tails is installed on USB drive "([^"]+)"$/ do |target_name|
339 loader = boot_device_type == 'usb' ? 'syslinux' : 'isolinux'
340 tails_is_installed_helper(target_name, '/lib/live/mount/medium', loader)
343 Then /^there is no persistence partition on USB drive "([^"]+)"$/ do |name|
344 data_part_dev = $vm.persistent_storage_dev_on_disk(name)
345 assert($vm.execute("test -b #{data_part_dev}").failure?,
346 "USB drive #{name} has a partition '#{data_part_dev}'")
349 Then /^a Tails persistence partition exists on USB drive "([^"]+)"$/ do |name|
350 dev = $vm.persistent_storage_dev_on_disk(name)
351 check_part_integrity(name, dev, 'crypto', 'crypto_LUKS',
352 part_label: 'TailsData')
355 # The LUKS container may already be opened, e.g. by udisks after
356 # we've created the Persistent Storage.
357 c = $vm.execute("ls -1 --hide 'control' /dev/mapper/")
359 c.stdout.split("\n").each do |candidate|
360 luks_info = $vm.execute("cryptsetup status '#{candidate}'")
361 if luks_info.success? && luks_info.stdout.match("^\s+device:\s+#{dev}$")
362 luks_dev = "/dev/mapper/#{candidate}"
368 assert_vmcommand_success(
369 $vm.execute("echo #{@persistence_password} | " \
370 "cryptsetup luksOpen #{dev} #{name}"),
371 "Couldn't open LUKS device '#{dev}' on drive '#{name}'"
373 luks_dev = "/dev/mapper/#{name}"
376 # Adapting check_part_integrity() seems like a bad idea so here goes
377 info = $vm.execute("udisksctl info --block-device '#{luks_dev}'").stdout
378 assert_match(%r{^ CryptoBackingDevice: +'/[a-zA-Z0-9_/]+'$}, info)
379 assert_match(/^ IdUsage: +filesystem$/, info)
380 assert_match(/^ IdType: +ext[34]$/, info)
381 assert_match(/^ IdLabel: +TailsData$/, info)
383 mount_dir = "/mnt/#{name}"
384 $vm.execute("mkdir -p #{mount_dir}")
385 assert_vmcommand_success($vm.execute("mount '#{luks_dev}' #{mount_dir}"),
386 "Couldn't mount opened LUKS device '#{dev}' " \
387 "on drive '#{name}'")
389 $vm.execute("umount #{mount_dir}")
391 $vm.execute("cryptsetup luksClose #{name}")
394 Given /^I enable persistence$/ do
395 @screen.wait('TailsGreeterPersistencePassphrase.png', 60).click
397 @screen.type(@persistence_password, ['Return'])
398 @screen.wait_any(['TailsGreeterPersistenceUnlocked.png', 'TailsGreeterPersistenceUnlockedGerman.png'], 30)
401 def tails_persistence_enabled?
402 libtps_file = '/usr/local/lib/tails-shell-library/libtps.sh'
403 $vm.execute(". '#{libtps_file}' && " \
404 'tps_is_unlocked').success?
407 Given /^all persistence presets(| from the old Tails version)(| but the first one) are enabled$/ do |old_tails, except_first|
408 assert(old_tails.empty? || except_first.empty?, 'Unsupported case.')
409 try_for(120, msg: 'Persistence is disabled') do
410 tails_persistence_enabled?
412 unexpected_mounts = []
413 # Check that all persistent directories are mounted
415 expected_mounts = persistent_mounts
416 unless except_first.empty?
417 first_expected_mount_source = expected_mounts.keys[0]
418 first_expected_mount_destination = expected_mounts[
419 first_expected_mount_source
421 expected_mounts.delete(first_expected_mount_source)
422 unexpected_mounts = [first_expected_mount_destination]
425 assert_not_nil($remembered_persistence_mounts)
426 expected_mounts = $remembered_persistence_mounts
428 mount = $vm.execute('mount').stdout.chomp
429 expected_mounts.each do |_, dir|
430 assert(mount.include?("on #{dir} "),
431 "Persistent directory '#{dir}' is not mounted")
433 unexpected_mounts.each do |dir|
434 assert(!mount.include?("on #{dir} "),
435 "Persistent directory '#{dir}' is mounted")
439 Given /^persistence is disabled$/ do
440 assert(!tails_persistence_enabled?, 'Persistence is enabled')
444 # Approach borrowed from
445 # config/chroot_local_includes/lib/live/config/998-permissions
446 boot_dev_id = $vm.execute(
447 'udevadm info --device-id-of-file=/lib/live/mount/medium'
449 $vm.execute("readlink -f /dev/block/'#{boot_dev_id}'").stdout.chomp
453 # Approach borrowed from
454 # config/chroot_local_includes/lib/live/config/998-permissions
455 info = $vm.execute("udevadm info --query=property --name='#{dev}'")
457 info.split("\n").map { |e| e.split('=') }.to_h
461 device_info(boot_device)['ID_BUS']
464 # Turn udisksctl info output into something more manipulable:
465 def parse_udisksctl_info(input)
469 input.chomp.split("\n").each do |line|
471 when %r{^/org/freedesktop/UDisks2/block_devices/}
473 when /^ (org\.freedesktop\.UDisks2\..+):$/
474 section = Regexp.last_match(1)
476 when /^\s+(.+?):\s+(.+)$/
477 key = Regexp.last_match(1)
478 value = Regexp.last_match(2)
479 tree[section][key] = value
481 # XXX: Best effort = consider this a continuation from previous
482 # line (e.g. Symlinks), and add the whole line, without
483 # stripping anything (e.g. leading whitespaces)
484 tree[section][key] += line
490 Then /^Tails is running from (.*) drive "([^"]+)"$/ do |bus, name|
492 expected_bus = bus == 'sata' ? 'ata' : bus
493 assert_equal(expected_bus, boot_device_type)
494 actual_dev = boot_device
495 # The boot partition differs between an using Tails installer and
496 # isohybrids. There's also a strange case isohybrids are thought to
497 # be booting from the "raw" device, and not a partition of it
499 expected_devs = ['', '1', '4'].map { |e| $vm.disk_dev(name) + e }
500 assert(expected_devs.include?(actual_dev),
501 "We are running from device #{actual_dev}, but for #{bus} drive " \
502 "'#{name}' we expected to run from one of #{expected_devs}")
505 Then /^the boot device has safe access rights$/ do
506 super_boot_dev = boot_device.sub(/[[:digit:]]+$/, '')
507 devs = $vm.file_glob("#{super_boot_dev}*")
508 assert(!devs.empty?, 'Could not determine boot device')
509 all_users = $vm.file_content('/etc/passwd')
511 .map { |line| line.split(':')[0] }
512 all_users_with_groups = all_users.map do |user|
513 groups = $vm.execute("groups #{user}").stdout.chomp.sub(/^#{user} : /,
518 dev_owner = $vm.execute("stat -c %U #{dev}").stdout.chomp
519 dev_group = $vm.execute("stat -c %G #{dev}").stdout.chomp
520 dev_perms = $vm.execute("stat -c %a #{dev}").stdout.chomp
521 assert_equal('root', dev_owner)
522 assert(['disk', 'root'].include?(dev_group),
523 "Boot device '#{dev}' owned by group '#{dev_group}', expected " \
525 assert_equal('660', dev_perms)
526 all_users_with_groups.each do |user, groups|
527 next if user == 'root'
529 assert(!groups.include?(dev_group),
530 "Unprivileged user '#{user}' is in group '#{dev_group}' which " \
531 "owns boot device '#{dev}'")
535 info = $vm.execute("udisksctl info --block-device '#{super_boot_dev}'").stdout
536 assert_match(/^ HintSystem: +true$/, info,
537 "Boot device '#{super_boot_dev}' is not system internal " \
541 Then /^all persistent filesystems have safe access rights$/ do
542 persistent_volumes_mountpoints.each do |mountpoint|
543 fs_owner = $vm.execute("stat -c %U #{mountpoint}").stdout.chomp
544 fs_group = $vm.execute("stat -c %G #{mountpoint}").stdout.chomp
545 fs_perms = $vm.execute("stat -c %a #{mountpoint}").stdout.chomp
546 assert_equal('root', fs_owner)
547 assert_equal('root', fs_group)
548 # This ensures the amnesia user cannot write to the root of the
549 # persistent storage, which in turns ensures this user cannot
550 # create a .Trash-1000 folder in there, which is our current best
551 # workaround for the lack of proper trash support in Persistent
552 # Storage: then the user is not offered to send files to the
553 # trash, and they can only delete files permanently (#18118).
554 assert_equal('770', fs_perms)
558 Then /^all persistence configuration files have safe access rights$/ do
559 persistent_volumes_mountpoints.each do |mountpoint|
560 assert_vmcommand_success(
561 $vm.execute("test -e #{mountpoint}/persistence.conf"),
562 "#{mountpoint}/persistence.conf does not exist, while it should"
564 assert_vmcommand_success(
565 $vm.execute("test ! -e #{mountpoint}/live-persistence.conf"),
566 "#{mountpoint}/live-persistence.conf does exist, while it should not"
569 "#{mountpoint}/persistence.conf* #{mountpoint}/live-*.conf"
571 file_owner = $vm.execute("stat -c %U '#{f}'").stdout.chomp
572 file_group = $vm.execute("stat -c %G '#{f}'").stdout.chomp
573 file_perms = $vm.execute("stat -c %a '#{f}'").stdout.chomp
574 assert_equal('tails-persistent-storage', file_owner)
575 assert_equal('tails-persistent-storage', file_group)
577 when %r{.*/live-additional-software.conf$}
578 assert_equal('644', file_perms)
580 assert_equal('600', file_perms)
586 Then /^all persistent directories(| from the old Tails version) have safe access rights$/ do |old_tails|
588 expected_dirs = persistent_dirs
590 assert_not_nil($remembered_persistence_dirs)
591 expected_dirs = $remembered_persistence_dirs
593 persistent_volumes_mountpoints.each do |mountpoint|
594 expected_dirs.each do |src, dest|
595 full_src = "#{mountpoint}/#{src}"
596 assert_vmcommand_success $vm.execute("test -d #{full_src}")
597 dir_perms = $vm.execute_successfully("stat -c %a '#{full_src}'")
599 dir_owner = $vm.execute_successfully("stat -c %U '#{full_src}'")
601 if dest.start_with?("/home/#{LIVE_USER}")
602 expected_perms = '700'
603 expected_owner = LIVE_USER
604 elsif File.basename(src) == 'greeter-settings'
605 expected_perms = '700'
606 expected_owner = 'Debian-gdm'
607 elsif File.basename(src) == 'tca'
608 expected_perms = '700'
609 expected_owner = 'root'
611 expected_perms = '755'
612 expected_owner = 'root'
614 assert_equal(expected_perms, dir_perms,
615 "Persistent source #{full_src} has permission " \
616 "#{dir_perms}, expected #{expected_perms}")
617 assert_equal(expected_owner, dir_owner,
618 "Persistent source #{full_src} has owner " \
619 "#{dir_owner}, expected #{expected_owner}")
624 When /^I write some files expected to persist$/ do
625 persistent_mounts.each do |_, dir|
626 owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
627 assert_vmcommand_success(
628 $vm.execute("touch #{dir}/XXX_persist", user: owner),
629 "Could not create file in persistent directory #{dir}"
634 When /^I write some dotfile expected to persist$/ do
635 assert_vmcommand_success(
637 'touch /live/persistence/TailsData_unlocked/dotfiles/.XXX_persist',
640 'Could not create a file in the dotfiles persistence.'
644 When /^I remove some files expected to persist$/ do
645 persistent_mounts.each do |_, dir|
646 owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
647 assert_vmcommand_success(
648 $vm.execute("rm #{dir}/XXX_persist", user: owner),
649 "Could not remove file in persistent directory #{dir}"
654 When /^I write some files not expected to persist$/ do
655 persistent_mounts.each do |_, dir|
656 owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
657 assert_vmcommand_success(
658 $vm.execute("touch #{dir}/XXX_gone", user: owner),
659 "Could not create file in persistent directory #{dir}"
664 When /^I take note of which persistence presets are available$/ do
665 $remembered_persistence_mounts = persistent_mounts
666 $remembered_persistence_dirs = persistent_dirs
669 Then /^the expected persistent files(| created with the old Tails version) are present in the filesystem$/ do |old_tails|
671 expected_mounts = persistent_mounts
673 assert_not_nil($remembered_persistence_mounts)
674 expected_mounts = $remembered_persistence_mounts
676 expected_mounts.each do |_, dir|
677 assert_vmcommand_success(
678 $vm.execute("test -e #{dir}/XXX_persist"),
679 "Could not find expected file in persistent directory #{dir}"
682 $vm.execute("test -e #{dir}/XXX_gone").failure?,
683 "Found file that should not have persisted in persistent directory #{dir}"
688 Then /^the expected persistent dotfile is present in the filesystem$/ do
689 expected_dirs = persistent_dirs
690 assert_vmcommand_success(
691 $vm.execute("test -L #{expected_dirs['dotfiles']}/.XXX_persist"),
692 'Could not find expected persistent dotfile link.'
694 assert_vmcommand_success(
696 "test -e $(readlink -f #{expected_dirs['dotfiles']}/.XXX_persist)"
698 'Could not find expected persistent dotfile link target.'
702 Then /^only the expected files are present on the persistence partition on USB drive "([^"]+)"$/ do |name|
703 assert(!$vm.running?)
705 path: $vm.storage.disk_path(name),
707 format: $vm.storage.disk_format(name),
711 $vm.storage.guestfs_disk_helper(disk) do |g, disk_handle|
712 partitions = g.part_list(disk_handle).map do |part_desc|
713 disk_handle + part_desc['part_num'].to_s
715 partition = partitions.find do |part|
716 g.blkid(part)['PART_ENTRY_NAME'] == 'TailsData'
718 assert_not_nil(partition, "Could not find the 'TailsData' partition " \
719 "on disk '#{disk_handle}'")
720 luks_mapping = File.basename(partition) + '_unlocked'
721 g.cryptsetup_open(partition, @persistence_password, luks_mapping)
722 luks_dev = "/dev/mapper/#{luks_mapping}"
724 g.mount(luks_dev, mount_point)
725 assert_not_nil($remembered_persistence_mounts)
726 $remembered_persistence_mounts.each do |dir, _|
727 # Guestfs::exists may have a bug; if the file exists, 1 is
728 # returned, but if it doesn't exist false is returned. It seems
729 # the translation of C types into Ruby types is glitchy.
730 assert(g.exists("/#{dir}/XXX_persist") == 1,
731 "Could not find expected file in persistent directory #{dir}")
733 g.exists("/#{dir}/XXX_gone") != 1,
734 'Found file that should not have persisted in persistent directory ' +
738 g.umount(mount_point)
739 g.cryptsetup_close(luks_dev)
743 When /^I delete the persistent partition$/ do
744 step 'I start "Persistent Storage" via GNOME Activities Overview'
746 delete_btn = persistent_storage_main_frame.button('Delete Persistent Storage')
749 # If we just do delete_btn.click, then dogtail won't find tps-frontend anymore.
750 # That's probably a bug somewhere, and this is a simple workaround
752 @screen.press('Return')
754 persistent_storage_frontend
755 .child('Warning', roleName: 'alert')
756 .button('Delete Persistent Storage').click
757 assert persistent_storage_main_frame.child(
758 'The Persistent Storage was successfully deleted.',
763 Then /^Tails has started in UEFI mode$/ do
764 assert_vmcommand_success($vm.execute('test -d /sys/firmware/efi'),
765 '/sys/firmware/efi does not exist')
768 Given /^I create a ([[:alpha:]]+) label on disk "([^"]+)"$/ do |type, name|
769 $vm.storage.disk_mklabel(name, type)
772 # The (crude) bin/create-test-iuks script can be used to generate the IUKs,
773 # meant to apply these exact changes, that are used by the test suite.
774 # It's nice to keep that script updated when updating the list of expected
775 # changes here and uploading new test IUKs.
776 def iuk_changes(version) # rubocop:disable Metrics/MethodLength
780 path: 'some_new_file',
782 new_content: <<~CONTENT,
788 path: 'etc/amnesia/version',
790 new_content: <<~CONTENT,
791 #{version} - 20380119
792 ffffffffffffffffffffffffffffffffffffffff
793 live-build: 3.0.5+really+is+2.0.12-0.tails2
800 path: 'etc/os-release',
802 new_content: <<~CONTENT,
803 TAILS_PRODUCT_NAME="Tails"
804 TAILS_VERSION_ID="#{version}"
809 path: 'usr/share/common-licenses/BSD',
814 path: 'usr/share/doc/tor',
819 path: 'utils/linux/syslinux',
825 when '2.2~testoverlayfsng'
827 when '2.3~testoverlayfsng'
831 path: 'some_new_file_2.3',
833 new_content: <<~CONTENT,
839 path: 'usr/share/common-licenses/MPL-1.1',
844 path: 'utils/mbr/mbr.bin',
849 raise "Test suite implementation error: unsupported version #{version}"
853 Given /^the file system changes introduced in version (.+) are (not )?present(?: in the (\S+) Browser's chroot)?$/ do |version, not_present, chroot_browser|
854 assert(['2.2~testoverlayfsng', '2.3~testoverlayfsng'].include?(version))
855 upgrade_applied = not_present.nil?
856 chroot_browser = "#{chroot_browser.downcase}-browser" if chroot_browser
857 changes = iuk_changes(version)
858 changes.each do |change|
859 case change[:filesystem]
862 path += "var/lib/#{chroot_browser}/chroot/" if chroot_browser
863 path += change[:path]
865 path = '/lib/live/mount/medium/' + change[:path]
867 raise "Unknown filesystem '#{change[:filesystem]}'"
871 assert_equal(!upgrade_applied, $vm.file_exist?(path))
873 assert_equal(upgrade_applied, $vm.file_exist?(path))
874 if upgrade_applied && change[:new_content]
875 assert_equal(change[:new_content], $vm.file_content(path))
878 assert($vm.file_exist?(path))
880 assert_not_nil(change[:new_content])
881 assert_equal(change[:new_content], $vm.file_content(path))
884 raise "Unknown status '#{change[:status]}'"
889 Then /^I am proposed to install an incremental upgrade to version (.+)$/ do |version|
890 recovery_proc = proc do
891 recover_from_upgrader_failure
893 failure_pic = 'TailsUpgraderFailure.png'
894 success_pic = "TailsUpgraderUpgradeTo#{version}.png"
895 retry_tor(recovery_proc) do
896 found_pic = @screen.wait_any([success_pic, failure_pic], 2 * 60)[:found_pattern]
897 assert_equal(success_pic, found_pic)
901 When /^I agree to install the incremental upgrade$/ do
902 @orig_syslinux_cfg = $vm.file_content(
903 '/lib/live/mount/medium/syslinux/syslinux.cfg'
905 @screen.click('TailsUpgraderUpgradeNowButton.png')
908 Then /^I can successfully install the incremental upgrade to version (.+)$/ do |version|
909 step 'I agree to install the incremental upgrade'
910 recovery_proc = proc do
911 recover_from_upgrader_failure
912 step "I am proposed to install an incremental upgrade to version #{version}"
913 step 'I agree to install the incremental upgrade'
915 failure_pic = 'TailsUpgraderFailure.png'
916 success_pic = 'TailsUpgraderDownloadComplete.png'
917 retry_tor(recovery_proc) do
918 found_pic = @screen.wait_any([success_pic, failure_pic], 2 * 60)[:found_pattern]
919 assert_equal(success_pic, found_pic)
921 @screen.wait('TailsUpgraderApplyUpgradeButton.png', 5).click
922 @screen.wait('TailsUpgraderDone.png', 60)
923 # Restore syslinux.cfg: our test IUKs replace it with something
924 # that would break the next boot
926 '/lib/live/mount/medium/syslinux/syslinux.cfg',
932 'filesystem.squashfs'
935 def installed_squashes
936 live = '/lib/live/mount/medium/live'
937 listed_squashes = $vm.file_content("#{live}/Tails.module").chomp.split("\n")
940 listed_squashes.first,
941 "Tails.module does not list #{default_squash} on the first line"
943 present_squashes = $vm.file_glob("#{live}/*.squashfs").map do |f|
944 f.sub('/lib/live/mount/medium/live/', '')
948 listed_squashes.sort,
949 present_squashes.sort,
950 'Tails.module does not match the present .squashfs files'
955 Given /^Tails is fooled to think a (.+) SquashFS delta is installed$/ do |version|
956 old_squashes = installed_squashes
957 medium = '/lib/live/mount/medium'
958 live = "#{medium}/live"
959 new_squash = "#{version}.squashfs"
960 $vm.execute_successfully("mount -o remount,rw #{medium}")
961 $vm.execute_successfully("touch #{live}/#{new_squash}")
962 $vm.file_append("#{live}/Tails.module", new_squash + "\n")
963 $vm.execute_successfully("mount -o remount,ro #{medium}")
965 old_squashes + [new_squash],
967 'Implementation error, alert the test suite maintainer!'
969 $vm.execute_successfully(
970 "sed --regexp-extended -i '1s/^\S+ /#{version}/' /etc/amnesia/version"
972 $vm.execute_successfully(
973 "sed -i 's/^TAILS_VERSION_ID=.*/TAILS_VERSION_ID=#{version}/' " \
974 '/etc/amnesia/version'
978 Then /^the Upgrader considers the system as up-to-date$/ do
979 try_for(120, delay: 10) do
980 $vm.execute_successfully(
981 'systemctl --user status tails-upgrade-frontend.service',
984 up_to_date_regexp = 'tails-upgrade-frontend-wrapper\[[0-9]+\]: ' \
985 'The system is up-to-date'
986 $vm.execute_successfully(
987 "journalctl | grep -q -E '#{up_to_date_regexp}'"
992 def upgrader_trusted_signing_subkeys
993 $vm.execute_successfully(
994 'sudo -u tails-upgrade-frontend ' \
995 'gpg --batch --list-keys --with-colons ' + TAILS_SIGNING_KEY
997 .select { |line| /^sub:/.match(line) }
998 .map { |line| line[/^sub:.:\d+:\d+:(?<subkeyid>[A-F0-9]+):/, 'subkeyid'] }
1001 Given /^the signing key used by the Upgrader is outdated$/ do
1002 upgrader_trusted_signing_subkeys.each do |subkeyid|
1003 $vm.execute_successfully(
1004 'sudo -u tails-upgrade-frontend ' \
1005 "gpg --batch --yes --delete-keys '#{subkeyid}!'"
1008 assert_equal(0, upgrader_trusted_signing_subkeys.length)
1011 Given /^a current signing key is available on our website$/ do
1012 # We already check this via features/keys.feature so let's not bother here
1013 # ⇒ this step is only here to improve the Gherkin scenario.
1017 Then /^(?:no|only the (.+)) SquashFS delta is installed$/ do |version|
1018 expected_squashes = [default_squash]
1019 expected_squashes << "#{version}.squashfs" if version
1023 'Unexpected .squashfs files encountered'
1027 Then /^the label of the system partition on "([^"]+)" is "([^"]+)"$/ do |name, label|
1028 assert($vm.running?)
1029 disk_dev = $vm.disk_dev(name)
1030 part_dev = disk_dev + '1'
1031 check_disk_integrity(name, disk_dev, 'gpt')
1032 check_part_integrity(name, part_dev, 'filesystem', 'vfat', part_label: label)
1035 Then /^the system partition on "([^"]+)" is an EFI system partition$/ do |name|
1036 assert($vm.running?)
1037 disk_dev = $vm.disk_dev(name)
1038 part_dev = disk_dev + '1'
1039 check_disk_integrity(name, disk_dev, 'gpt')
1040 check_part_integrity(name, part_dev, 'filesystem', 'vfat',
1041 part_type: ESP_GUID)
1044 Then /^the FAT filesystem on the system partition on "([^"]+)" is at least (\d+)(.+) large$/ do |name, size, unit|
1045 # Let's use bytes all the way:
1046 wanted_size = convert_to_bytes(size.to_i, unit)
1048 disk_dev = $vm.disk_dev(name)
1049 part_dev = disk_dev + '1'
1051 udisks_info = $vm.execute_successfully(
1052 "udisksctl info --block-device #{part_dev}"
1054 partition_size = parse_udisksctl_info(udisks_info)[
1055 'org.freedesktop.UDisks2.Partition'
1060 partition_size >= wanted_size,
1061 "FAT partition is too small: #{partition_size} is less than #{wanted_size}"
1064 # -B 1 forces size to be expressed in bytes rather than (1K) blocks:
1065 fs_size = $vm.execute_successfully(
1066 "df --output=size -B 1 '/lib/live/mount/medium'"
1067 ).stdout.split("\n")[1].to_i
1068 assert(fs_size >= wanted_size,
1069 "FAT filesystem is too small: #{fs_size} is less than #{wanted_size}")
1072 Then /^the UUID of the FAT filesystem on the system partition on "([^"]+)" was randomized$/ do |name|
1073 disk_dev = $vm.disk_dev(name)
1074 part_dev = disk_dev + '1'
1076 # Get the UUID from the block area:
1077 udisks_info = $vm.execute_successfully(
1078 "udisksctl info --block-device #{part_dev}"
1080 fs_uuid = parse_udisksctl_info(
1082 )['org.freedesktop.UDisks2.Block']['IdUUID']
1084 static_uuid = 'A690-20D2'
1085 assert(fs_uuid != static_uuid,
1086 "FS UUID on #{name} wasn't randomized, it's still: #{fs_uuid}")
1089 Then /^the label of the FAT filesystem on the system partition on "([^"]+)" is "([^"]+)"$/ do |name, label|
1090 disk_dev = $vm.disk_dev(name)
1091 part_dev = disk_dev + '1'
1093 # Get FS label from the block area:
1094 udisks_info = $vm.execute_successfully(
1095 "udisksctl info --block-device #{part_dev}"
1097 fs_label = parse_udisksctl_info(
1099 )['org.freedesktop.UDisks2.Block']['IdLabel']
1101 assert(label == fs_label,
1102 "FS label on #{part_dev} is #{fs_label} " \
1103 "instead of the expected #{label}")
1106 Then /^the system partition on "([^"]+)" has the expected flags$/ do |name|
1107 disk_dev = $vm.disk_dev(name)
1108 part_dev = disk_dev + '1'
1110 # Look at the flags from the partition area:
1111 udisks_info = $vm.execute_successfully(
1112 "udisksctl info --block-device #{part_dev}"
1114 flags = parse_udisksctl_info(
1116 )['org.freedesktop.UDisks2.Partition']['Flags']
1118 # See SYSTEM_PARTITION_FLAGS in create-usb-image-from-iso: 0xd000000000000005,
1119 # displayed in decimal (14987979559889010693) in udisksctl's output:
1120 expected_flags = 0xd000000000000005
1121 assert(flags == expected_flags.to_s,
1122 "Got #{flags} as partition flags on #{part_dev} (for #{name}), " \
1123 "instead of the expected #{expected_flags}")
1126 Given /^I install a Tails USB image to the (\d+) MiB disk with GNOME Disks$/ do |size_in_MiB_of_destination_disk|
1127 # GNOME Disks displays devices sizes in GB, with 1 decimal digit precision
1128 size_in_GB_of_destination_disk = convert_from_bytes(
1129 convert_to_bytes(size_in_MiB_of_destination_disk.to_i, 'MiB'),
1132 debug_log('Expected size of destination disk: ' +
1133 size_in_GB_of_destination_disk)
1135 step 'I start "Disks" via GNOME Activities Overview'
1136 disks = gnome_disks_app
1137 destination_disk_label_regexp = /^#{size_in_GB_of_destination_disk} GB Drive/
1138 disks.children(roleName: 'table cell')
1139 .find { |row| destination_disk_label_regexp.match(row.name) }
1141 @screen.wait('GnomeDisksDriveMenuButton.png', 5).click
1142 disks.child('Restore Disk Image…',
1143 roleName: 'push button',
1146 restore_dialog = disks.child('Restore Disk Image',
1149 # Open the file chooser
1150 @screen.press('Enter')
1151 select_disk_image_dialog = disks.child('Select Disk Image to Restore',
1152 roleName: 'file chooser',
1156 app: :gtk_file_chooser
1158 sleep 2 # avoid ENTER being eaten by the auto-completion system
1159 @screen.press('Enter')
1161 !select_disk_image_dialog.showing
1163 # Clicking this button using Dogtail works, but afterwards GNOME
1164 # Disks becomes inaccessible.
1165 restore_dialog.child('Start Restoring…',
1166 roleName: 'push button',
1167 showingOnly: true).grabFocus
1168 @screen.press('Return')
1169 disks.child('Information', roleName: 'alert', showingOnly: true)
1170 .child('Restore', roleName: 'push button', showingOnly: true)
1172 @screen.press('Return')
1173 # Wait until the restoration job is finished
1174 job = disks.child('Job', roleName: 'label', showingOnly: true)
1180 Given /^I set all Greeter options to non-default values$/ do
1181 # We sleep between each option to give the UI time to update,
1182 # otherwise we might detect the + button or language entry before it
1183 # has been readjusted, so while we try to click it, it moves so we
1185 step 'I disable the Unsafe Browser'
1187 step 'I disable networking in Tails Greeter'
1189 step 'I disable MAC spoofing in Tails Greeter'
1191 # Administration password needs to be done last because its image has blue background (selected)
1192 # while the others have no such background.
1193 step 'I set an administration password'
1196 # We should change language, too, but we'll not: in fact, changing the language would change labels in the
1197 # UI, so we would need to keep images (see #19420) in both languages, making the test suite harder to
1199 # The "I log in to a new session" step can change language at the very last moment, which is a good
1200 # workaround to the problem.
1203 Then /^all Greeter options are set to (non-)?default values$/ do |non_default|
1204 settings = $vm.execute_successfully(
1205 'grep -h "^TAILS_" /var/lib/gdm3/settings/persistent/tails.* | ' \
1206 'grep -v "^TAILS_.*PASSWORD" | LC_ALL=C sort'
1209 expected = <<~EXPECTED
1211 TAILS_LOCALE_NAME=de_DE
1212 TAILS_MACSPOOF_ENABLED=false
1214 TAILS_UNSAFE_BROWSER_ENABLED=false
1216 TAILS_XKBMODEL=pc105
1219 $vm.execute_successfully(
1220 'grep "^TAILS_USER_PASSWORD=\'.\+\'$" ' \
1221 '/var/lib/gdm3/settings/persistent/tails.password'
1223 $vm.execute_successfully(
1224 'grep "^TAILS_PASSWORD_HASH_FUNCTION=SHA512$" ' \
1225 '/var/lib/gdm3/settings/persistent/tails.password'
1228 expected = <<~EXPECTED
1230 TAILS_LOCALE_NAME=en_US
1231 TAILS_MACSPOOF_ENABLED=true
1233 TAILS_UNSAFE_BROWSER_ENABLED=true
1235 TAILS_XKBMODEL=pc105
1238 assert(!$vm.file_exist?('/var/lib/gdm3/settings/persistent/tails.password'))
1240 assert_equal(expected, settings)
1243 Then /^(no )?persistent Greeter options were restored$/ do |no|
1245 assert(!@screen.exists('TailsGreeterToast.png'))
1247 $language = 'German'
1248 @screen.wait('TailsGreeterPersistentSettingsRestoredGerman.png', 10)
1252 Then /^(.*) is (?:still )?configured to persist$/ do |dir|
1253 assert(configured_persistent_mountpoints.include?(dir))
1256 Then /^(.*) is not configured to persist$/ do |dir|
1257 assert(!configured_persistent_mountpoints.include?(dir))
1260 Then /^the Tails Persistent Storage behave tests pass$/ do
1261 $vm.execute_successfully('/usr/lib/python3/dist-packages/tps/configuration/behave-tests/run-tests.sh')
1264 When /^I give the Persistent Storage on drive "([^"]+)" its own UUID$/ do |name|
1265 # Rationale: udisks cannot unlock 2 devices with the same UUID.
1266 dev = $vm.persistent_storage_dev_on_disk(name)
1267 uuid = SecureRandom.uuid
1268 $vm.execute_successfully("cryptsetup luksUUID --uuid #{uuid} #{dev}")