5 $vm.execute('/usr/local/lib/tpscli is-created').success?
8 # Returns a mapping from the source of a binding to its destination
9 # for all bindings of all pre-configured tps features that the running
11 def get_tps_bindings(skip_links: false)
12 # Python script that prints all persistence configuration lines (one per
13 # line) in the form: <mount_point>\t<comma-separated-list-of-options>
15 'from tps.configuration import features',
16 'for feature in features.get_classes():',
17 ' for binding in feature.Bindings:',
20 c = RemoteShell::PythonCommand.new($vm, script.join("\n"))
21 assert(c.success?, 'Python script for get_tps_bindings failed')
22 binding_configs = c.stdout.chomp.split("\n")
23 assert binding_configs.size >= 10,
24 "Got #{binding_configs.size} binding configuration " \
25 'lines, which is too few'
27 binding_configs.each do |line|
28 destination, options_str = line.split("\t")
29 options = options_str.split(',')
30 is_link = options.include? 'link'
31 next if is_link && skip_links
33 source_str = options.find { |option| /^source=/.match(option) }
34 # If no source is given as an option, live-boot's persistence
35 # feature defaults to the destination minus the initial "/".
36 source = if source_str.nil?
37 destination.partition('/').last
39 source_str.split('=')[1]
41 bindings_mapping[source] = destination
51 get_tps_bindings(skip_links: true)
55 c = $vm.execute_successfully('/usr/local/lib/tpscli get-features')
56 JSON.parse(c.stdout.chomp)
59 def tps_feature_is_enabled(feature, reload: true)
61 c = $vm.execute("/usr/local/lib/tpscli is-enabled #{feature}")
65 def tps_feature_is_active(feature, reload: true)
67 c = $vm.execute("/usr/local/lib/tpscli is-active #{feature}")
72 $vm.execute_successfully('systemctl reload tails-persistent-storage.service')
75 def persistent_volumes_mountpoints
76 $vm.execute('ls -1 -d /live/persistence/*_unlocked/').stdout.chomp.split
79 def persistent_storage_frontend(**opts)
80 Dogtail::Application.new('tps-frontend', **opts)
83 def persistent_storage_main_frame
84 persistent_storage_frontend.child('Persistent Storage', roleName: 'frame')
87 def persistent_directory_is_active(**opts)
89 opts[:use_system_bus] = true
91 'org.boum.tails.PersistentStorage',
92 '/org/boum/tails/PersistentStorage/Features/PersistentDirectory',
93 'org.freedesktop.DBus.Properties.Get',
94 'org.boum.tails.PersistentStorage.Feature',
100 def recover_from_upgrader_failure
101 $vm.execute('pkill --full tails-upgrade-frontend-wrapper')
102 $vm.execute('killall tails-upgrade-frontend zenity')
103 # Do not sleep when retrying
104 $vm.spawn('tails-upgrade-frontend-wrapper --no-wait', user: LIVE_USER)
108 Dogtail::Application.new('Welcome to Tails!',
110 translation_domain: 'tails')
113 Given /^I clone USB drive "([^"]+)" to a (new|temporary) USB drive "([^"]+)"$/ do |from, mode, to|
114 $vm.storage.clone_to_new_disk(from, to)
115 if mode == 'temporary'
116 add_after_scenario_hook { $vm.storage.delete_volume(to) }
120 Given /^I unplug USB drive "([^"]+)"$/ do |name|
121 $vm.unplug_drive(name)
124 Given /^the computer is set to boot in UEFI mode$/ do
125 $vm.set_os_loader('UEFI')
129 def tails_installer_selected_device
130 @installer.child('Target USB stick:', roleName: 'label').parent
131 .child('', roleName: 'combo box', recursive: false).name
134 def tails_installer_is_device_selected?(name)
135 device = $vm.disk_dev(name)
136 tails_installer_selected_device[/\(#{device}\d*\)$/]
139 def tails_installer_match_status(pattern)
140 @installer.child('', roleName: 'text').text[pattern]
143 When /^I start Tails Installer$/ do
144 @installer_log_path = '/tmp/tails-installer.log'
145 command = '/usr/local/bin/tails-installer --verbose 2>&1 ' \
146 "| tee #{@installer_log_path} | logger -t tails-installer"
147 step "I run \"#{command}\" in GNOME Terminal"
148 @installer = Dogtail::Application.new('tails-installer')
149 @installer.child('Tails Cloner', roleName: 'frame')
150 # Sometimes Dogtail will find the Installer and click its window
151 # before it is shown (searchShowingOnly is not perfect) which
152 # generally means clicking somewhere on the Terminal => the click is
153 # lost *and* the installer does not go to the foreground. So let's
156 @screen.wait('TailsClonerWindow.png', 10).click
159 When /^I am told by Tails Installer that.*"([^"]+)".*$/ do |status|
161 tails_installer_match_status(status)
165 Then /^a suitable USB device is (?:still )?not found$/ do
167 'No device suitable to install Tails could be found', roleName: 'label'
171 Then /^(no|the "([^"]+)") USB drive is selected$/ do |mode, name|
174 tails_installer_selected_device == ''
176 tails_installer_is_device_selected?(name)
181 def persistence_exists?(name)
182 data_part_dev = $vm.persistent_storage_dev_on_disk(name)
183 $vm.execute("test -b #{data_part_dev}").success?
186 When /^I (install|reinstall|upgrade) Tails( with Persistent Storage)? (?:to|on) USB drive "([^"]+)" by cloning$/ do |action, with_persistence, name|
187 step 'I start Tails Installer'
189 # Check that the "Clone the current Persistent Storage" check button
190 # is visible if and only if the current Tails device has a Persistent
192 # We use a wildcard in the label because in case that the target device
193 # already contains a Tails installation, the check button label is
194 # "Clone the current Persistent Storage (requires reinstall)".
196 clone_persistence_button = @installer
197 .child('Clone the current Persistent Storage.*',
198 roleName: 'check box',
200 sensitive = clone_persistence_button.sensitive?
201 rescue Dogtail::Failure
206 "Couldn't find clone Persistent Storage check button " \
207 '(even though a Persistent Storage exists)')
210 'Found clone Persistent Storage check button ' \
211 '(even though no Persistent Storage exists)')
216 "Can't clone with Persistent Storage: Clone button is not sensitive")
217 clone_persistence_button.click
220 # If the device was plugged *just* before this step, it might not be
221 # completely ready (so it's shown) at this stage.
222 try_for(10) { tails_installer_is_device_selected?(name) }
224 label = if action == 'reinstall'
225 'Reinstall (delete all data)'
229 # We can't use the click action here because this button causes a
230 # modal dialog to be run via gtk_dialog_run() which causes the
231 # application to hang when triggered via a ATSPI action. See
232 # https://gitlab.gnome.org/GNOME/gtk/-/issues/1281
233 @installer.button(label).grabFocus
234 @screen.press('Enter')
236 unless action == 'upgrade'
237 confirmation_label = if persistence_exists?(name)
238 'Delete Persistent Storage and Reinstall'
240 'Delete All Data and Install'
242 @installer.child('Question',
243 roleName: 'alert').button(confirmation_label).click
246 # Enter the passphrase in the passphrase dialog
247 passphrase_entry = @installer.child('Choose Passphrase',
249 .child('Passphrase:', roleName: 'label')
251 confirm_entry = @installer.child('Choose Passphrase',
253 .child('Confirm:', roleName: 'label')
255 passphrase_entry.text = @persistence_password
256 confirm_entry.text = @persistence_password
257 confirm_entry.activate
261 try_for(15 * 60, delay: 10) do
263 .child('Information', roleName: 'alert')
264 .child('Installation complete!', roleName: 'label')
267 rescue StandardError => e
268 debug_log("Tails Installer debug log:\n#{$vm.file_content(@installer_log_path)}")
273 Given(/^I plug and mount a USB drive containing a Tails USB image$/) do
274 usb_image_dir = share_host_files(TAILS_IMG)
275 @usb_image_path = "#{usb_image_dir}/#{File.basename(TAILS_IMG)}"
278 def enable_all_tps_features
279 assert persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
280 switches = persistent_storage_main_frame.children(roleName: 'toggle button')
281 switches.each do |switch|
283 debug_log("#{switch.name} is already enabled, skipping")
285 debug_log("enabling #{switch.name}")
286 # To avoid having to bother with scrolling the window we just
287 # send an AT-SPI action instead of clicking.
289 try_for(10) { switch.checked? }
294 When /^I (enable|disable) the first tps feature$/ do |mode|
295 launch_persistent_storage
296 persistent_folder_switch = persistent_storage_main_frame.child(
297 'Activate Persistent Folder',
298 roleName: 'toggle button'
301 assert !persistent_folder_switch.checked?
303 assert persistent_folder_switch.checked?
306 persistent_folder_switch.toggle
308 # GtkSwitch does not expose its underlying state via AT-SPI (the
309 # accessible has the "check" state when the switch is on but the
310 # underlying state is false) so we check the state via D-Bus.
312 assert persistent_folder_switch.checked?
313 persistent_directory_is_active
315 assert !persistent_folder_switch.checked?
316 !persistent_directory_is_active
319 @screen.press('alt', 'F4')
322 Given(/^I enable persistence creation in Tails Greeter$/) do
323 greeter.child('Create Persistent Storage', roleName: 'toggle button')
327 Given /^I create a persistent partition( with the default settings)?( for Additional Software)?( using the wizard that was already open)?$/ do |default_settings, asp, dontrun|
328 # When creating a persistent partition for Additional Software, we
329 # want to use the default settings.
330 default_settings = true if asp
332 mode = asp ? ' for Additional Software' : ''
333 step "I try to create a persistent partition#{mode}#{dontrun}"
335 # Check that the Persistent Storage was created by checking that the
336 # tps frontend shows the features view with the "Personal Documents"
339 persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
342 enable_all_tps_features unless default_settings
345 Given /^I try to create a persistent partition( for Additional Software)?( using the wizard that was already open)?$/ do |asp, dontrun|
346 unless asp || dontrun
347 launch_persistent_storage
349 persistent_storage_main_frame.button('Co_ntinue').click
350 persistent_storage_main_frame
351 .child('Passphrase:', roleName: 'label')
353 .text = @persistence_password
354 persistent_storage_main_frame
355 .child('Confirm:', roleName: 'label')
357 .text = @persistence_password
358 persistent_storage_main_frame.button('_Create Persistent Storage').click
361 def available_memory_kib
362 meminfo = $vm.file_content('/proc/meminfo')
363 meminfo =~ /^MemAvailable:\s+(\d+) kB$/
364 Regexp.last_match(1).to_i
367 Given /^the system is( very)? low on memory$/ do |very_low|
368 # If we're asked to make the system very low on memory, then
369 # we leave only 200 MiB of memory available, otherwise we leave 550
370 # MiB (550 MiB is enough to create a Persistent Storage with the
371 # lowest PBKDF memory cost).
372 low_mem_kib = very_low ? 200 * 1024 : 550 * 1024
374 # Ensure that the zram swap is disabled, to avoid that the memory
375 # pressure is relieved by swapping.
376 $vm.execute_successfully('zramswap stop')
378 # Get the amount of available memory
379 mem_available_kib = available_memory_kib
381 # Calculate how much memory we need to fill up
382 mem_to_fill_kib = mem_available_kib - low_mem_kib
383 if mem_to_fill_kib <= 0
384 debug_log("Available memory is already low enough: #{mem_available_kib} KiB")
388 # Write a file that will fill up the memory
389 $vm.execute_successfully(
390 "dd if=/dev/zero of=/fill bs=1M count=#{mem_to_fill_kib / 1024}"
393 # Wait for the memory to be filled up
394 try_for(20, msg: 'The system did not become low on memory') do
395 mem_available_kib = available_memory_kib
396 debug_log("Available memory after filling up: #{mem_available_kib} KiB")
397 # The memory is considered low if it's within 100 MiB of the low
399 low_mem_kib - 100 * 1024 <= mem_available_kib &&
400 mem_available_kib <= low_mem_kib + 100 * 1024
404 Given /^I free up some memory$/ do
405 # This assumes that the step 'the system is very low on memory' was
407 $vm.execute_successfully('rm /fill')
408 step 'the system is low on memory'
411 Given /^I close the Persistent Storage app$/ do
413 alert = persistent_storage_frontend.child(roleName: 'alert', retry: false)
415 alert.button('Close').click
417 alert = persistent_storage_frontend.child(roleName: 'alert', retry: false)
423 # Close the main window
424 persistent_storage_main_frame.button('Close').click
426 # Wait for the app to close
428 persistent_storage_frontend(retry: false)
435 Then /^The Persistent Storage app shows the error message "([^"]*)"$/ do |message|
436 persistent_storage_frontend.child(message, roleName: 'label')
439 Given /^I change the passphrase of the Persistent Storage( back to the original)?$/ do |change_back|
441 current_passphrase = @changed_persistence_password
442 new_passphrase = @persistence_password
444 current_passphrase = @persistence_password
445 new_passphrase = @changed_persistence_password
448 launch_persistent_storage
450 # We can't use the click action here because this button causes a
451 # modal dialog to be run via gtk_dialog_run() which causes the
452 # application to hang when triggered via a ATSPI action. See
453 # https://gitlab.gnome.org/GNOME/gtk/-/issues/1281
454 persistent_storage_main_frame.button('Change Passphrase').grabFocus
455 @screen.press('Return')
456 change_passphrase_dialog = persistent_storage_frontend
457 .child('Change Passphrase', roleName: 'dialog')
458 change_passphrase_dialog
459 .child('Current Passphrase', roleName: 'label')
461 .text = current_passphrase
462 change_passphrase_dialog
463 .child('New Passphrase', roleName: 'label')
465 .text = new_passphrase
466 change_passphrase_dialog
467 .child('Confirm New Passphrase', roleName: 'label')
469 .text = new_passphrase
470 change_passphrase_dialog.button('Change').click
471 # Wait for the dialog to close
473 persistent_storage_frontend
474 .child('Change Passphrase', roleName: 'dialog')
475 rescue Dogtail::Failure
476 # The dialog couldn't be found, which is what we want
483 def check_disk_integrity(name, dev, scheme)
484 info = $vm.execute_successfully(
485 "udisksctl info --block-device '#{dev}'"
487 info_split = info.split("\n org\.freedesktop\.UDisks2\.PartitionTable:\n")
488 part_table_info = info_split[1]
489 assert_match(/^ Type: +#{scheme}/, part_table_info,
490 "Unexpected partition scheme on USB drive '#{name}', '#{dev}'")
492 # Now we will additionally verify the partition table if, and only if,
494 return unless scheme == 'gpt'
496 c = $vm.execute("sgdisk --verify #{dev}")
498 # Note that sgdisk --verify exits with 0 even if it finds problems,
499 # so we also need to check the output.
501 c.to_s.include?('No problems found.') && \
502 # The output of sgdisk --verify includes "ERROR" if any of the
503 # following are corrupt:
505 # * The GPT partition table
506 # * The GPT backup header
507 # * The GPT backup partition table
508 !c.to_s.include?('ERROR') &&
509 # The output of sgdisk --verify includes "corrupt" if the protective
511 !c.to_s.include?('corrupt'),
512 "sgdisk --verify #{dev} failed.\n#{c}"
516 def check_part_integrity(name, dev, usage, fs_type,
517 part_label: nil, part_type: nil)
518 info = $vm.execute_successfully(
519 "udisksctl info --block-device '#{dev}'"
521 info_split = info.split("\n org\.freedesktop\.UDisks2\.Partition:\n")
522 dev_info = info_split[0]
523 part_info = info_split[1]
524 assert_match(/^ IdUsage: +#{usage}$/, dev_info,
525 "Unexpected device field 'usage' on drive '#{name}', '#{dev}'")
526 assert_match(/^ IdType: +#{fs_type}$/, dev_info,
527 "Unexpected device field 'IdType' on drive '#{name}', '#{dev}'")
529 assert_match(/^ Name: +#{part_label}$/, part_info,
530 "Unexpected partition label on drive '#{name}', '#{dev}'")
533 assert_match(/^ Type: +#{part_type}$/, part_info,
534 "Unexpected partition type on drive '#{name}', '#{dev}'")
538 def tails_is_installed_helper(name, tails_root, loader)
539 disk_dev = $vm.disk_dev(name)
540 part_dev = "#{disk_dev}1"
541 check_disk_integrity(name, disk_dev, 'gpt')
542 check_part_integrity(name, part_dev, 'filesystem', 'vfat',
543 part_label: 'Tails', part_type: ESP_GUID)
545 target_root = '/mnt/new'
546 $vm.execute("mkdir -p #{target_root}")
547 $vm.execute("mount #{part_dev} #{target_root}")
549 c = $vm.execute("diff -qr '#{tails_root}/live' '#{target_root}/live'")
552 "USB drive '#{name}' has differences in /live:\n#{c.stdout}\n#{c.stderr}"
555 syslinux_files = $vm.execute("ls -1 #{target_root}/syslinux")
557 # We deal with these files separately
558 ignores = ['syslinux.cfg', 'exithelp.cfg', 'ldlinux.c32', 'ldlinux.sys']
559 (syslinux_files - ignores).each do |f|
560 assert_vmcommand_success(
561 $vm.execute("diff -q '#{tails_root}/#{loader}/#{f}' " \
562 "'#{target_root}/syslinux/#{f}'"),
563 "USB drive '#{name}' has differences in '/syslinux/#{f}'"
567 # The main .cfg is named differently vs isolinux
568 assert_vmcommand_success(
569 $vm.execute("diff -q '#{tails_root}/#{loader}/#{loader}.cfg' " \
570 "'#{target_root}/syslinux/syslinux.cfg'"),
571 "USB drive '#{name}' has differences in '/syslinux/syslinux.cfg'"
574 $vm.execute("umount #{target_root}")
578 Then /^the running Tails is installed on USB drive "([^"]+)"$/ do |target_name|
579 loader = boot_device_type == 'usb' ? 'syslinux' : 'isolinux'
580 tails_is_installed_helper(target_name, '/lib/live/mount/medium', loader)
583 Then /^there is no persistence partition on USB drive "([^"]+)"$/ do |name|
584 data_part_dev = $vm.persistent_storage_dev_on_disk(name)
585 assert($vm.execute("test -b #{data_part_dev}").failure?,
586 "USB drive #{name} has a partition '#{data_part_dev}'")
589 Then /^there is a persistence partition on USB drive "([^"]+)"$/ do |name|
590 data_part_dev = $vm.persistent_storage_dev_on_disk(name)
591 assert($vm.execute("test -b #{data_part_dev}").success?,
592 "USB drive #{name} has no partition '#{data_part_dev}'")
595 def assert_luks2_with_argon2id(name, device)
596 # Tails 5.12 and older used LUKS1 by default
597 return if name == 'old' && !$old_version.nil? \
598 && system("dpkg --compare-versions '#{$old_version}' le 5.12")
600 luks_info = $vm.execute("cryptsetup luksDump #{device}").stdout
601 assert_match(/^^Version:\s*2$/, luks_info,
602 "Device #{device} is not LUKS2")
603 assert_match(/^\s*PBKDF:\s*argon2id$/, luks_info,
604 "Device #{device} does not use argon2id")
607 def assert_luks1(device)
608 luks_info = $vm.execute("cryptsetup luksDump #{device}").stdout
609 assert_match(/^^Version:\s*1$/, luks_info,
610 "Device #{device} is not LUKS1")
613 Then /^a Tails persistence partition with LUKS version 2 and argon2id exists on USB drive "([^"]+)"$/ do |name|
614 # Step "a Tails persistence partition exists on USB drive" checks by
615 # default that the LUKS version is 2 and the key derivation function
617 step "a Tails persistence partition exists on USB drive \"#{name}\""
620 Then /^the Tails persistence partition on USB drive "([^"]+)" still has LUKS version 1$/ do |name|
621 step 'a Tails persistence partition exists with LUKS version 1 ' \
622 "on USB drive \"#{name}\""
625 Then /^a Tails persistence partition exists( with LUKS version 1)? on USB drive "([^"]+)"$/ do |luks1, name|
626 dev = $vm.persistent_storage_dev_on_disk(name)
627 check_part_integrity(name, dev, 'crypto', 'crypto_LUKS',
628 part_label: 'TailsData')
629 # The LUKS container may already be opened, e.g. by udisks after
630 # we've created the Persistent Storage.
631 luks_dev = luks_mapping(dev)
633 assert_vmcommand_success(
634 $vm.execute("echo #{@persistence_password} | " \
635 "cryptsetup luksOpen #{dev} #{name}"),
636 "Couldn't open LUKS device '#{dev}' on drive '#{name}'"
638 luks_dev = "/dev/mapper/#{name}"
642 assert_luks2_with_argon2id(name, dev)
647 # Adapting check_part_integrity() seems like a bad idea so here goes
648 info = $vm.execute_successfully(
649 "udisksctl info --block-device '#{luks_dev}'"
651 assert_match(%r{^ CryptoBackingDevice: +'/[a-zA-Z0-9_/]+'$}, info)
652 assert_match(/^ IdUsage: +filesystem$/, info)
653 assert_match(/^ IdType: +ext[34]$/, info)
654 assert_match(/^ IdLabel: +TailsData$/, info)
656 mount_dir = "/mnt/#{name}"
657 $vm.execute("mkdir -p #{mount_dir}")
658 assert_vmcommand_success($vm.execute("mount '#{luks_dev}' #{mount_dir}"),
659 "Couldn't mount opened LUKS device '#{dev}' " \
660 "on drive '#{name}'")
662 $vm.execute("umount #{mount_dir}")
664 $vm.execute("cryptsetup luksClose #{name}")
667 Given /^I try to enable persistence( with the changed passphrase)?$/ do |with_changed_passphrase|
668 passphrase_entry = greeter.child(roleName: 'password text')
669 passphrase_entry.grabFocus
670 password = if with_changed_passphrase
671 @changed_persistence_password
673 @persistence_password
675 passphrase_entry.text = password
676 @screen.press('Return')
679 Then /^persistence is successfully enabled$/ do
680 # Wait until the Persistent Storage is fully activated. We don't know which
681 # language is set in the Welcome Screen after the Persistent Storage
682 # was unlocked, so we check the backend directly.
684 tails_persistence_active?
687 # If the Persistent Welcome Screen options feature is enabled the
688 # GUI's language might change around this time, and we have to set
689 # the language accordingly in the test suite so Dogtail will use the
690 # translated strings.
692 $language, $lang_code = greeter_language
693 greeter.child('Your Persistent Storage is unlocked. ' \
694 'Its content will be available until you shut down Tails.',
699 Given /^I enable persistence( with the changed passphrase)?$/ do |with_changed_passphrase|
700 step "I try to enable persistence#{with_changed_passphrase}"
701 step 'persistence is successfully enabled'
704 Given /^I enable persistence but something goes wrong during the LUKS header upgrade$/ do
705 # Copy a cryptsetup wrapper to the VM which will call `cryptsetup luksErase`
706 # instead of `cryptsetup luksConvertKey` to simulate a failure during the LUKS
708 $vm.file_copy_local("#{GIT_DIR}/features/scripts/cryptsetup-wrapper",
709 '/usr/local/sbin/cryptsetup')
711 step 'I enable persistence'
713 # Check that the LUKS header was erased by our wrapper script.
714 assert $vm.file_exist?('/tmp/luks-header-erased'), 'LUKS header was not erased'
718 english_label = 'English - United States'
719 german_label = 'Deutsch - Deutschland (German - Germany)'
721 greeter.child(english_label, roleName: 'label', retry: false)
722 # We have to set the language to '' for English, setting it to
723 # 'English' doesn't work.
725 rescue Dogtail::Failure
726 greeter.child(german_label, roleName: 'label', retry: false)
727 return 'German', 'de'
731 def tails_persistence_unlocked?
732 $vm.execute('tps_is_unlocked', libs: 'libtps').success?
735 def tails_persistence_active?
736 tails_persistence_unlocked? &&
738 .select { |f| tps_feature_is_enabled(f, reload: false) }
739 .all? { |f| tps_feature_is_active(f, reload: false) }
742 Then /^all tps features(| from the old Tails version)(| but the first one) are active$/ do |old_tails_str, except_first_str|
743 old_tails = !old_tails_str.empty?
744 except_first = !except_first_str.empty?
745 assert(!old_tails || !except_first, 'Unsupported case.')
746 try_for(120, msg: 'Persistence is disabled') do
747 tails_persistence_unlocked?
751 features = old_tails ? $remembered_tps_features : tps_features
752 features.each do |feature|
753 is_active = tps_feature_is_active(feature, reload: false)
754 if except_first && feature == 'PersistentDirectory'
755 assert !is_active, "Feature '#{feature}' is active"
757 assert is_active, "Feature '#{feature}' is not active"
762 Then /^all tps features(| but the first one) are enabled$/ do |except_first_str|
763 except_first = !except_first_str.empty?
765 tps_features.each do |feature|
766 is_enabled = tps_feature_is_enabled(feature, reload: false)
767 if except_first && feature == 'PersistentDirectory'
768 assert !is_enabled, "Feature '#{feature}' is enabled"
770 assert is_enabled, "Feature '#{feature}' is not enabled"
775 Then /^all tps features(| but the first one) are enabled and active$/ do |except_first_str|
776 except_first = !except_first_str.empty?
778 step 'all tps features but the first one are enabled'
779 step 'all tps features but the first one are active'
781 step 'all tps features are enabled'
782 step 'all tps features are active'
786 Then /^the "(\S+)" tps feature is(| not) enabled$/ do |feature, not_str|
787 check_not_enabled = !not_str.empty?
788 is_enabled = tps_feature_is_enabled(feature)
790 assert !is_enabled, "Feature '#{feature}' is enabled"
792 assert is_enabled, "Feature '#{feature}' is not enabled"
796 Then /^the "(\S+)" tps feature is(| not) active$/ do |feature, not_str|
797 check_not_active = !not_str.empty?
798 is_active = tps_feature_is_active(feature)
800 assert !is_active, "Feature '#{feature}' is active"
802 assert is_active, "Feature '#{feature}' is not active"
806 Then /^the "(\S+)" tps feature is(| not) enabled and(| not) active$/ do |feature, not_enabled_str, not_active_str|
807 step "the \"#{feature}\" tps feature is#{not_enabled_str} enabled"
808 step "the \"#{feature}\" tps feature is#{not_active_str} active"
811 Then /^persistence is disabled$/ do
812 assert(!tails_persistence_unlocked?, 'Persistence is enabled')
815 Then /^persistence is enabled$/ do
816 assert(tails_persistence_active?, 'Persistence is disabled or not active yet')
820 # Approach borrowed from
821 # config/chroot_local_includes/lib/live/config/998-permissions
822 boot_dev_id = $vm.execute(
823 'udevadm info --device-id-of-file=/lib/live/mount/medium'
825 $vm.execute("readlink -f /dev/block/'#{boot_dev_id}'").stdout.chomp
829 # Approach borrowed from
830 # config/chroot_local_includes/lib/live/config/998-permissions
831 info = $vm.execute("udevadm info --query=property --name='#{dev}'")
833 info.split("\n").map { |e| e.split('=') }.to_h
837 device_info(boot_device)['ID_BUS']
840 # Turn udisksctl info output into something more manipulable:
841 def parse_udisksctl_info(input)
845 input.chomp.split("\n").each do |line|
847 when %r{^/org/freedesktop/UDisks2/block_devices/}
849 when /^ (org\.freedesktop\.UDisks2\..+):$/
850 section = Regexp.last_match(1)
852 when /^\s+(.+?):\s+(.+)$/
853 key = Regexp.last_match(1)
854 value = Regexp.last_match(2)
855 tree[section][key] = value
857 # XXX: Best effort = consider this a continuation from previous
858 # line (e.g. Symlinks), and add the whole line, without
859 # stripping anything (e.g. leading whitespaces)
860 tree[section][key] += line
863 fs_section = tree['org.freedesktop.UDisks2.Filesystem']
864 if fs_section && fs_section['MountPoints']
865 fs_section['MountPoints'] = fs_section['MountPoints'].split
870 # Get the LUKS mapping of device, or nil if there is none
871 def luks_mapping(device)
872 c = $vm.execute("ls -1 --hide 'control' /dev/mapper/")
874 c.stdout.split("\n").each do |candidate|
875 luks_info = $vm.execute("cryptsetup status '#{candidate}'")
876 if luks_info.success? && luks_info.stdout.match("^\s+device:\s+#{device}$")
877 return "/dev/mapper/#{candidate}"
884 # Returns the first non-nosymfollow mountpoint of device. If the
885 # device has a LUKS mapping we instead return where it is mounted.
886 def mountpoint(device)
887 info = parse_udisksctl_info(
888 $vm.execute_successfully("udisksctl info -b #{device}").stdout
890 if info['org.freedesktop.UDisks2.Block']['IdType'] == 'crypto_LUKS'
891 luks_device = luks_mapping(device)
892 mountpoint(luks_device) if luks_device
894 info['org.freedesktop.UDisks2.Filesystem']['MountPoints']
895 .find { |p| !p.match?(Regexp.new('^/run/nosymfollow/')) }
899 Then /^Tails is running from (.*) drive "([^"]+)"$/ do |bus, name|
901 expected_bus = bus == 'sata' ? 'ata' : bus
902 assert_equal(expected_bus, boot_device_type)
903 actual_dev = boot_device
904 # The boot partition differs between an using Tails installer and
905 # isohybrids. There's also a strange case isohybrids are thought to
906 # be booting from the "raw" device, and not a partition of it
908 expected_devs = ['', '1', '4'].map { |e| $vm.disk_dev(name) + e }
909 assert(expected_devs.include?(actual_dev),
910 "We are running from device #{actual_dev}, but for #{bus} drive " \
911 "'#{name}' we expected to run from one of #{expected_devs}")
914 Then /^the boot device has safe access rights$/ do
915 super_boot_dev = boot_device.sub(/[[:digit:]]+$/, '')
916 devs = $vm.file_glob("#{super_boot_dev}*")
917 assert(!devs.empty?, 'Could not determine boot device')
918 all_users = $vm.file_content('/etc/passwd')
920 .map { |line| line.split(':')[0] }
921 all_users_with_groups = all_users.map do |user|
922 groups = $vm.execute("groups #{user}").stdout.chomp.sub(/^#{user} : /,
927 dev_owner = $vm.execute("stat -c %U #{dev}").stdout.chomp
928 dev_group = $vm.execute("stat -c %G #{dev}").stdout.chomp
929 dev_perms = $vm.execute("stat -c %a #{dev}").stdout.chomp
930 assert_equal('root', dev_owner)
931 assert(['disk', 'root'].include?(dev_group),
932 "Boot device '#{dev}' owned by group '#{dev_group}', expected " \
934 assert_equal('660', dev_perms)
935 all_users_with_groups.each do |user, groups|
936 next if user == 'root'
938 assert(!groups.include?(dev_group),
939 "Unprivileged user '#{user}' is in group '#{dev_group}' which " \
940 "owns boot device '#{dev}'")
944 info = $vm.execute_successfully(
945 "udisksctl info --block-device '#{super_boot_dev}'"
947 assert_match(/^ HintSystem: +true$/, info,
948 "Boot device '#{super_boot_dev}' is not system internal " \
952 Then /^the USB drive "([^"]+)" has a valid partition table$/ do |name|
953 disk_dev = $vm.disk_dev(name)
954 check_disk_integrity(name, disk_dev, 'gpt')
957 Then /^all persistent filesystems have safe access rights$/ do
958 persistent_volumes_mountpoints.each do |mountpoint|
959 fs_owner = $vm.execute("stat -c %U #{mountpoint}").stdout.chomp
960 fs_group = $vm.execute("stat -c %G #{mountpoint}").stdout.chomp
961 fs_perms = $vm.execute("stat -c %a #{mountpoint}").stdout.chomp
962 assert_equal('root', fs_owner)
963 assert_equal('root', fs_group)
964 # This ensures the amnesia user cannot write to the root of the
965 # persistent storage, which in turns ensures this user cannot
966 # create a .Trash-1000 folder in there, which is our current best
967 # workaround for the lack of proper trash support in Persistent
968 # Storage: then the user is not offered to send files to the
969 # trash, and they can only delete files permanently (#18118).
970 assert_equal('770', fs_perms)
974 Then /^all persistence configuration files have safe access rights$/ do
975 persistent_volumes_mountpoints.each do |mountpoint|
976 assert_vmcommand_success(
977 $vm.execute("test -e #{mountpoint}/persistence.conf"),
978 "#{mountpoint}/persistence.conf does not exist, while it should"
980 assert_vmcommand_success(
981 $vm.execute("test ! -e #{mountpoint}/live-persistence.conf"),
982 "#{mountpoint}/live-persistence.conf does exist, while it should not"
985 "#{mountpoint}/persistence.conf* #{mountpoint}/live-*.conf"
987 file_owner = $vm.execute("stat -c %U '#{f}'").stdout.chomp
988 file_group = $vm.execute("stat -c %G '#{f}'").stdout.chomp
989 file_perms = $vm.execute("stat -c %a '#{f}'").stdout.chomp
990 assert_equal('tails-persistent-storage', file_owner)
991 assert_equal('tails-persistent-storage', file_group)
993 when %r{.*/live-additional-software.conf$}
994 assert_equal('644', file_perms)
996 assert_equal('600', file_perms)
1002 Then /^all persistent directories(| from the old Tails version) have safe access rights$/ do |old_tails|
1004 expected_bindings = tps_bindings
1006 assert_not_nil($remembered_tps_bindings)
1007 expected_bindings = $remembered_tps_bindings
1009 persistent_volumes_mountpoints.each do |mountpoint|
1010 expected_bindings.each do |src, dest|
1011 full_src = "#{mountpoint}/#{src}"
1012 assert_vmcommand_success $vm.execute("test -d #{full_src}")
1013 dir_perms = $vm.execute_successfully("stat -c %a '#{full_src}'")
1015 dir_owner = $vm.execute_successfully("stat -c %U '#{full_src}'")
1017 if dest.start_with?("/home/#{LIVE_USER}")
1018 expected_perms = '700'
1019 expected_owner = LIVE_USER
1020 elsif File.basename(src) == 'greeter-settings'
1021 expected_perms = '700'
1022 expected_owner = 'Debian-gdm'
1023 elsif File.basename(src) == 'tca'
1024 expected_perms = '700'
1025 expected_owner = 'root'
1027 expected_perms = '755'
1028 expected_owner = 'root'
1030 assert_equal(expected_perms, dir_perms,
1031 "Persistent source #{full_src} has permission " \
1032 "#{dir_perms}, expected #{expected_perms}")
1033 assert_equal(expected_owner, dir_owner,
1034 "Persistent source #{full_src} has owner " \
1035 "#{dir_owner}, expected #{expected_owner}")
1040 When /^I write some files expected to persist$/ do
1041 tps_bind_mounts.each do |_, dir|
1042 owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
1043 assert_vmcommand_success(
1044 $vm.execute("touch #{dir}/XXX_persist", user: owner),
1045 "Could not create file in persistent directory #{dir}"
1050 When /^I write some dotfile expected to persist$/ do
1051 assert_vmcommand_success(
1053 'touch /live/persistence/TailsData_unlocked/dotfiles/.XXX_persist',
1056 'Could not create a file in the dotfiles persistence.'
1060 When /^I remove some files expected to persist$/ do
1061 tps_bind_mounts.each do |_, dir|
1062 owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
1063 assert_vmcommand_success(
1064 $vm.execute("rm #{dir}/XXX_persist", user: owner),
1065 "Could not remove file in persistent directory #{dir}"
1070 When /^I write some files not expected to persist$/ do
1071 tps_bind_mounts.each do |_, dir|
1072 owner = $vm.execute("stat -c %U #{dir}").stdout.chomp
1073 assert_vmcommand_success(
1074 $vm.execute("touch #{dir}/XXX_gone", user: owner),
1075 "Could not create file in persistent directory #{dir}"
1080 When /^I take note of which tps features are available$/ do
1081 $remembered_tps_features = tps_features
1082 $remembered_tps_bind_mounts = tps_bind_mounts
1083 $remembered_tps_bindings = tps_bindings
1086 Then /^the expected persistent files(| created with the old Tails version) are present in the filesystem$/ do |old_tails|
1088 expected_mounts = tps_bind_mounts
1090 assert_not_nil($remembered_tps_bind_mounts)
1091 expected_mounts = $remembered_tps_bind_mounts
1093 expected_mounts.each do |_, dir|
1094 assert_vmcommand_success(
1095 $vm.execute("test -e #{dir}/XXX_persist"),
1096 "Could not find expected file in persistent directory #{dir}"
1099 $vm.execute("test -e #{dir}/XXX_gone").failure?,
1100 "Found file that should not have persisted in persistent directory #{dir}"
1105 Then /^the expected persistent dotfile is present in the filesystem$/ do
1106 expected_bindings = tps_bindings
1107 assert_vmcommand_success(
1108 $vm.execute("test -L #{expected_bindings['dotfiles']}/.XXX_persist"),
1109 'Could not find expected persistent dotfile link.'
1111 assert_vmcommand_success(
1113 "test -e $(readlink -f #{expected_bindings['dotfiles']}/.XXX_persist)"
1115 'Could not find expected persistent dotfile link target.'
1119 Then /^only the expected files are present on the persistence partition on USB drive "([^"]+)"$/ do |name|
1120 assert(!$vm.running?)
1122 path: $vm.storage.disk_path(name),
1124 format: $vm.storage.disk_format(name),
1128 $vm.storage.guestfs_disk_helper(disk) do |g, disk_handle|
1129 partitions = g.part_list(disk_handle).map do |part_desc|
1130 disk_handle + part_desc['part_num'].to_s
1132 partition = partitions.find do |part|
1133 g.blkid(part)['PART_ENTRY_NAME'] == 'TailsData'
1135 assert_not_nil(partition, "Could not find the 'TailsData' partition " \
1136 "on disk '#{disk_handle}'")
1137 luks_mapping = "#{File.basename(partition)}_unlocked"
1138 g.cryptsetup_open(partition, @persistence_password, luks_mapping)
1139 luks_dev = "/dev/mapper/#{luks_mapping}"
1141 g.mount(luks_dev, mount_point)
1142 assert_not_nil($remembered_tps_bind_mounts)
1143 $remembered_tps_bind_mounts.each do |dir, _|
1144 # Guestfs::exists may have a bug; if the file exists, 1 is
1145 # returned, but if it doesn't exist false is returned. It seems
1146 # the translation of C types into Ruby types is glitchy.
1147 assert(g.exists("/#{dir}/XXX_persist") == 1,
1148 "Could not find expected file in persistent directory #{dir}")
1150 g.exists("/#{dir}/XXX_gone") != 1,
1151 "Found file that should not have persisted in persistent directory #{dir}"
1154 g.umount(mount_point)
1155 g.cryptsetup_close(luks_dev)
1159 When /^I delete the persistent partition$/ do
1160 launch_persistent_storage
1162 # If we just do delete_btn.click, then dogtail won't find tps-frontend anymore.
1163 # Related to https://gitlab.gnome.org/GNOME/gtk/-/issues/1281 mentioned
1164 # elsewhere in this file?
1165 # That's probably a bug somewhere, and this is a simple workaround
1166 persistent_storage_main_frame.button('Delete Persistent Storage').grabFocus
1167 @screen.press('Return')
1169 persistent_storage_frontend
1170 .child('Warning', roleName: 'alert')
1171 .button('Delete Persistent Storage').click
1172 assert persistent_storage_main_frame.child(
1173 'The Persistent Storage was successfully deleted.',
1178 Then /^Tails has started in UEFI mode$/ do
1179 assert_vmcommand_success($vm.execute('test -d /sys/firmware/efi'),
1180 '/sys/firmware/efi does not exist')
1183 Given /^I create a ([[:alpha:]]+) label on disk "([^"]+)"$/ do |type, name|
1184 $vm.storage.disk_mklabel(name, type)
1187 # The (crude) bin/create-test-iuks script can be used to generate the IUKs,
1188 # meant to apply these exact changes, that are used by the test suite.
1189 # It's nice to keep that script updated when updating the list of expected
1190 # changes here and uploading new test IUKs.
1191 def iuk_changes(version) # rubocop:disable Metrics/MethodLength
1194 filesystem: :rootfs,
1195 path: 'some_new_file',
1197 new_content: <<~CONTENT,
1202 filesystem: :rootfs,
1203 path: 'etc/os-release',
1205 new_content: <<~CONTENT,
1207 VERSION="#{version}"
1211 filesystem: :rootfs,
1212 path: 'usr/share/common-licenses/BSD',
1216 filesystem: :rootfs,
1217 path: 'usr/share/doc/tor',
1221 filesystem: :medium,
1222 path: 'utils/linux/syslinux',
1228 when '6.2~testoverlayfs'
1230 when '6.3~testoverlayfs'
1233 filesystem: :rootfs,
1234 path: 'some_new_file_6.3',
1236 new_content: <<~CONTENT,
1241 filesystem: :rootfs,
1242 path: 'usr/share/common-licenses/MPL-1.1',
1246 filesystem: :medium,
1247 path: 'utils/mbr/mbr.bin',
1252 raise "Test suite implementation error: unsupported version #{version}"
1256 Given /^the file system changes introduced in version (.+) are (not )?present(?: in the (\S+) Browser's chroot)?$/ do |version, not_present, chroot_browser|
1257 assert(['6.2~testoverlayfs', '6.3~testoverlayfs'].include?(version))
1258 upgrade_applied = not_present.nil?
1259 chroot_browser = "#{chroot_browser.downcase}-browser" if chroot_browser
1260 changes = iuk_changes(version)
1261 changes.each do |change|
1262 case change[:filesystem]
1265 path += "var/lib/#{chroot_browser}/chroot/" if chroot_browser
1266 path += change[:path]
1268 path = "/lib/live/mount/medium/#{change[:path]}"
1270 raise "Unknown filesystem '#{change[:filesystem]}'"
1272 case change[:status]
1274 assert_equal(!upgrade_applied, $vm.file_exist?(path))
1276 assert_equal(upgrade_applied, $vm.file_exist?(path))
1277 if upgrade_applied && change[:new_content]
1278 assert_equal(change[:new_content], $vm.file_content(path))
1281 assert($vm.file_exist?(path))
1283 assert_not_nil(change[:new_content])
1284 assert_equal(change[:new_content], $vm.file_content(path))
1287 raise "Unknown status '#{change[:status]}'"
1292 Then /^I am proposed to install an incremental upgrade to version (.+)$/ do |version|
1293 recovery_proc = proc do
1294 recover_from_upgrader_failure
1296 failure_pic = 'TailsUpgraderFailure.png'
1297 success_pic = "TailsUpgraderUpgradeTo#{version}.png"
1298 retry_tor(recovery_proc) do
1299 found_pic = @screen.wait_any([success_pic, failure_pic], 2 * 60).image
1300 assert_equal(success_pic, found_pic)
1304 When /^I agree to install the incremental upgrade$/ do
1305 @orig_syslinux_cfg = $vm.file_content(
1306 '/lib/live/mount/medium/syslinux/syslinux.cfg'
1308 @screen.click('TailsUpgraderUpgradeNowButton.png')
1311 Then /^I can successfully install the incremental upgrade to version (.+)$/ do |version|
1312 step 'I agree to install the incremental upgrade'
1313 recovery_proc = proc do
1314 recover_from_upgrader_failure
1315 step "I am proposed to install an incremental upgrade to version #{version}"
1316 step 'I agree to install the incremental upgrade'
1318 failure_pic = 'TailsUpgraderFailure.png'
1319 success_pic = 'TailsUpgraderDownloadComplete.png'
1320 retry_tor(recovery_proc) do
1321 found_pic = @screen.wait_any([success_pic, failure_pic], 2 * 60).image
1322 assert_equal(success_pic, found_pic)
1324 @screen.wait('TailsUpgraderApplyUpgradeButton.png', 5).click
1325 @screen.wait('TailsUpgraderDone.png', 60)
1326 # Restore syslinux.cfg: our test IUKs replace it with something
1327 # that would break the next boot
1329 '/lib/live/mount/medium/syslinux/syslinux.cfg',
1335 'filesystem.squashfs'
1338 def installed_squashes
1339 live = '/lib/live/mount/medium/live'
1340 listed_squashes = $vm.file_content("#{live}/Tails.module").chomp.split("\n")
1343 listed_squashes.first,
1344 "Tails.module does not list #{default_squash} on the first line"
1346 present_squashes = $vm.file_glob("#{live}/*.squashfs").map do |f|
1347 f.sub('/lib/live/mount/medium/live/', '')
1351 listed_squashes.sort,
1352 present_squashes.sort,
1353 'Tails.module does not match the present .squashfs files'
1358 Given /^Tails is fooled to think a (.+) SquashFS delta is installed$/ do |version|
1359 old_squashes = installed_squashes
1360 medium = '/lib/live/mount/medium'
1361 live = "#{medium}/live"
1362 new_squash = "#{version}.squashfs"
1363 $vm.execute_successfully("mount -o remount,rw #{medium}")
1364 $vm.execute_successfully("touch #{live}/#{new_squash}")
1365 $vm.file_append("#{live}/Tails.module", "#{new_squash}\n")
1366 $vm.execute_successfully("mount -o remount,ro #{medium}")
1368 old_squashes + [new_squash],
1370 'Implementation error, alert the test suite maintainer!'
1372 $vm.execute_successfully(
1373 "sed -i 's/^VERSION=.*/VERSION=\"#{version}\"/' " \
1378 Then /^the Upgrader considers the system as up-to-date$/ do
1379 try_for(120, delay: 10) do
1380 $vm.execute_successfully(
1381 'systemctl --user status tails-upgrade-frontend.service',
1384 up_to_date_regexp = 'tails-upgrade-frontend-wrapper\[[0-9]+\]: ' \
1385 'The system is up-to-date'
1386 $vm.execute_successfully(
1387 "journalctl | grep -q -E '#{up_to_date_regexp}'"
1392 def upgrader_trusted_signing_subkeys
1393 $vm.execute_successfully(
1394 'sudo -u tails-upgrade-frontend ' \
1395 'gpg --batch --list-keys --with-colons ' + TAILS_SIGNING_KEY
1396 ).stdout.split("\n")
1397 .select { |line| /^sub:/.match(line) }
1398 .map { |line| line[/^sub:.:\d+:\d+:(?<subkeyid>[A-F0-9]+):/, 'subkeyid'] }
1401 Given /^the signing key used by the Upgrader is outdated$/ do
1402 upgrader_trusted_signing_subkeys.each do |subkeyid|
1403 $vm.execute_successfully(
1404 'sudo -u tails-upgrade-frontend ' \
1405 "gpg --batch --yes --delete-keys '#{subkeyid}!'"
1408 assert_equal(0, upgrader_trusted_signing_subkeys.length)
1411 Given /^a current signing key is available on our website$/ do
1412 # We already check this via features/keys.feature so let's not bother here
1413 # ā this step is only here to improve the Gherkin scenario.
1417 Then /^(?:no|only the (.+)) SquashFS delta is installed$/ do |version|
1418 expected_squashes = [default_squash]
1419 expected_squashes << "#{version}.squashfs" if version
1423 'Unexpected .squashfs files encountered'
1427 Then /^the label of the system partition on "([^"]+)" is "([^"]+)"$/ do |name, label|
1428 assert($vm.running?)
1429 disk_dev = $vm.disk_dev(name)
1430 part_dev = "#{disk_dev}1"
1431 check_disk_integrity(name, disk_dev, 'gpt')
1432 check_part_integrity(name, part_dev, 'filesystem', 'vfat', part_label: label)
1435 Then /^the system partition on "([^"]+)" is an EFI system partition$/ do |name|
1436 assert($vm.running?)
1437 disk_dev = $vm.disk_dev(name)
1438 part_dev = "#{disk_dev}1"
1439 check_disk_integrity(name, disk_dev, 'gpt')
1440 check_part_integrity(name, part_dev, 'filesystem', 'vfat',
1441 part_type: ESP_GUID)
1444 Then /^the FAT filesystem on the system partition on "([^"]+)" is at least (\d+)(.+) large$/ do |name, size, unit|
1445 # Let's use bytes all the way:
1446 wanted_size = convert_to_bytes(size.to_i, unit)
1448 disk_dev = $vm.disk_dev(name)
1449 part_dev = "#{disk_dev}1"
1451 udisks_info = $vm.execute_successfully(
1452 "udisksctl info --block-device #{part_dev}"
1454 partition_size = parse_udisksctl_info(udisks_info)[
1455 'org.freedesktop.UDisks2.Partition'
1460 partition_size >= wanted_size,
1461 "FAT partition is too small: #{partition_size} is less than #{wanted_size}"
1464 # -B 1 forces size to be expressed in bytes rather than (1K) blocks:
1465 fs_size = $vm.execute_successfully(
1466 "df --output=size -B 1 '/lib/live/mount/medium'"
1467 ).stdout.split("\n")[1].to_i
1468 assert(fs_size >= wanted_size,
1469 "FAT filesystem is too small: #{fs_size} is less than #{wanted_size}")
1472 Then /^the UUID of the FAT filesystem on the system partition on "([^"]+)" was randomized$/ do |name|
1473 disk_dev = $vm.disk_dev(name)
1474 part_dev = "#{disk_dev}1"
1476 # Get the UUID from the block area:
1477 udisks_info = $vm.execute_successfully(
1478 "udisksctl info --block-device #{part_dev}"
1480 fs_uuid = parse_udisksctl_info(
1482 )['org.freedesktop.UDisks2.Block']['IdUUID']
1484 static_uuid = 'A690-20D2'
1485 assert(fs_uuid != static_uuid,
1486 "FS UUID on #{name} wasn't randomized, it's still: #{fs_uuid}")
1489 Then /^the label of the FAT filesystem on the system partition on "([^"]+)" is "([^"]+)"$/ do |name, label|
1490 disk_dev = $vm.disk_dev(name)
1491 part_dev = "#{disk_dev}1"
1493 # Get FS label from the block area:
1494 udisks_info = $vm.execute_successfully(
1495 "udisksctl info --block-device #{part_dev}"
1497 fs_label = parse_udisksctl_info(
1499 )['org.freedesktop.UDisks2.Block']['IdLabel']
1501 assert(label == fs_label,
1502 "FS label on #{part_dev} is #{fs_label} " \
1503 "instead of the expected #{label}")
1506 Then /^the system partition on "([^"]+)" has the expected flags$/ do |name|
1507 disk_dev = $vm.disk_dev(name)
1508 part_dev = "#{disk_dev}1"
1510 # Look at the flags from the partition area:
1511 udisks_info = $vm.execute_successfully(
1512 "udisksctl info --block-device #{part_dev}"
1514 flags = parse_udisksctl_info(
1516 )['org.freedesktop.UDisks2.Partition']['Flags']
1518 # See SYSTEM_PARTITION_FLAGS in create-usb-image-from-iso: 0xd000000000000005,
1519 # displayed in decimal (14987979559889010693) in udisksctl's output:
1520 expected_flags = 0xd000000000000005
1521 assert(flags == expected_flags.to_s,
1522 "Got #{flags} as partition flags on #{part_dev} (for #{name}), " \
1523 "instead of the expected #{expected_flags}")
1526 Given /^I install a Tails USB image to the (\d+) MiB disk with GNOME Disks$/ do |size_in_MiB_of_destination_disk|
1527 # GNOME Disks displays devices sizes in GB, with 1 decimal digit precision
1528 size_in_GB_of_destination_disk = convert_from_bytes(
1529 convert_to_bytes(size_in_MiB_of_destination_disk.to_i, 'MiB'),
1532 debug_log("Expected size of destination disk: #{size_in_GB_of_destination_disk}")
1534 disks = launch_gnome_disks
1535 destination_disk_label_regexp = /^#{size_in_GB_of_destination_disk} GB Drive/
1536 disks.children(roleName: 'table cell')
1537 .find { |row| destination_disk_label_regexp.match(row.name) }
1539 disks.child(description: 'Drive Options', roleName: 'toggle button')
1541 disks.child('Restore Disk Imageā¦', roleName: 'push button').click
1542 restore_dialog = disks.child('Restore Disk Image', roleName: 'dialog')
1543 # Open the file chooser
1544 @screen.press('Enter')
1545 select_disk_image_dialog = disks.child('Select Disk Image to Restore',
1546 roleName: 'file chooser')
1547 select_disk_image_dialog.child('File Chooser Widget',
1548 roleName: 'file chooser')
1549 .doActionNamed('show_location')
1550 text_entry = select_disk_image_dialog.child('Location Layer')
1551 .child(roleName: 'text')
1552 text_entry.text = @usb_image_path
1553 # For some reason two activate calls are necessary to close the dialog
1558 !select_disk_image_dialog.showing?
1560 # We can't use the click action here because this button causes a
1561 # modal dialog to be run via gtk_dialog_run() which causes the
1562 # application to hang when triggered via a ATSPI action. See
1563 # https://gitlab.gnome.org/GNOME/gtk/-/issues/1281
1564 restore_dialog.child('Start Restoringā¦', roleName: 'push button').grabFocus
1565 @screen.press('Return')
1566 disks.child('Information', roleName: 'alert')
1567 .child('Restore', roleName: 'push button')
1569 @screen.press('Return')
1570 # Wait until the restoration job is finished
1571 job = disks.child('Job', roleName: 'label')
1577 Given /^I set all Greeter options to non-default values$/ do
1578 # We sleep between each option to give the UI time to update,
1579 # otherwise we might detect the + button or language entry before it
1580 # has been readjusted, so while we try to click it, it moves so we
1582 step 'I disable the Unsafe Browser'
1584 step 'I disable networking in Tails Greeter'
1586 step 'I disable MAC spoofing in Tails Greeter'
1588 # Administration password needs to be done last because its image
1589 # has blue background (selected) while the others have no such background.
1590 step 'I set an administration password'
1593 # We should change language, too, but we won't: in fact, changing
1594 # the language would change labels in the UI, so we would need to
1595 # keep images (see #19420) in both languages, making the test suite
1596 # harder to maintain. The "I log in to a new session" step can
1597 # change language at the very last moment, which is a good
1598 # workaround to the problem.
1601 Then /^all Greeter options are set to (non-)?default values$/ do |non_default|
1602 settings = $vm.execute_successfully(
1603 'grep -h "^TAILS_" /var/lib/gdm3/settings/persistent/tails.* | ' \
1604 'grep -v "^TAILS_.*PASSWORD" | LC_ALL=C sort'
1607 expected = <<~EXPECTED
1609 TAILS_LOCALE_NAME=de_DE
1610 TAILS_MACSPOOF_ENABLED=false
1612 TAILS_UNSAFE_BROWSER_ENABLED=false
1614 TAILS_XKBMODEL=pc105
1617 $vm.execute_successfully(
1618 'grep "^TAILS_USER_PASSWORD=\'.\+\'$" ' \
1619 '/var/lib/gdm3/settings/persistent/tails.password'
1621 $vm.execute_successfully(
1622 'grep "^TAILS_PASSWORD_HASH_FUNCTION=SHA512$" ' \
1623 '/var/lib/gdm3/settings/persistent/tails.password'
1626 expected = <<~EXPECTED
1628 TAILS_LOCALE_NAME=en_US
1629 TAILS_MACSPOOF_ENABLED=true
1631 TAILS_UNSAFE_BROWSER_ENABLED=true
1633 TAILS_XKBMODEL=pc105
1636 assert(!$vm.file_exist?('/var/lib/gdm3/settings/persistent/tails.password'))
1638 assert_equal(expected, settings)
1641 Then /^(no )?persistent Greeter options were restored$/ do |no|
1642 $language, $lang_code = greeter_language
1643 # Our Dogtail wrapper code automatically translates strings to $language
1644 settings_restored = greeter
1645 .child?('Settings were loaded from the Persistent Storage.',
1648 assert(!settings_restored)
1650 assert(settings_restored)
1654 Then /^the Tails Persistent Storage behave tests pass$/ do
1655 $vm.execute_successfully(
1656 '/usr/lib/python3/dist-packages/tps/configuration/behave-tests/run-tests.sh'
1660 When /^I give the Persistent Storage on drive "([^"]+)" its own UUID$/ do |name|
1661 # Rationale: udisks cannot unlock 2 devices with the same UUID.
1662 dev = $vm.persistent_storage_dev_on_disk(name)
1663 uuid = SecureRandom.uuid
1664 $vm.execute_successfully("cryptsetup luksUUID --uuid #{uuid} #{dev}")
1667 When /^I create a file in the Persistent directory$/ do
1668 unless $vm.file_exist?('/home/amnesia/Persistent')
1669 step 'I create a directory "/home/amnesia/Persistent"'
1671 step 'I write a file "/home/amnesia/Persistent/foo" with contents "foo"'
1674 Then /^the file I created was copied to the Persistent Storage$/ do
1675 file = '/live/persistence/TailsData_unlocked/Persistent/foo'
1676 step "the file \"#{file}\" exists"
1677 step "the file \"#{file}\" has the content \"foo\""
1680 Then /^the file I created does not exist on the Persistent Storage$/ do
1681 file = '/live/persistence/TailsData_unlocked/Persistent/foo'
1682 step "the file \"#{file}\" does not exist"
1685 Then /^the file I created in the Persistent directory exists$/ do
1686 file = '/home/amnesia/Persistent/foo'
1687 step "the file \"#{file}\" exists"
1688 step "the file \"#{file}\" has the content \"foo\""
1691 Then /^the Persistent directory does not exist$/ do
1692 step 'the directory "/home/amnesia/Persistent" does not exist'
1695 When /^I delete the data of the Persistent Folder feature$/ do
1696 launch_persistent_storage
1698 def persistent_folder_delete_button(**opts)
1699 persistent_storage_main_frame.child(
1700 'Delete Persistent Folder data',
1701 roleName: 'push button', **opts
1705 # We can't use the click action here because this button causes a
1706 # modal dialog to be run via gtk_dialog_run() which causes the
1707 # application to hang when triggered via a ATSPI action. See
1708 # https://gitlab.gnome.org/GNOME/gtk/-/issues/1281
1709 persistent_folder_delete_button.grabFocus
1710 @screen.press('Return')
1711 confirm_deletion_dialog = persistent_storage_frontend.child(
1712 'Warning', roleName: 'alert'
1714 confirm_deletion_dialog.button('Delete Data').click
1716 # Wait for the delete data button to disappear
1718 persistent_folder_delete_button(retry: false)
1719 rescue Dogtail::Failure
1720 # The button couldn't be found, which is what we want
1725 @screen.press('alt', 'F4')
1728 Then /^the Welcome Screen tells me that the Persistent Folder feature couldn't be activated$/ do
1730 greeter.child?('Failed to activate some features of the Persistent Storage: ' \
1731 'Persistent Folder.\n.*',
1736 Then(/^the Welcome Screen tells me that filesystem errors were found on the Persistent Storage$/) do
1738 greeter.child?('File System Errors', roleName: 'label') && \
1739 greeter.child?('Repair File System', roleName: 'push button')
1743 Then /^the Welcome Screen tells me that it failed to repair the Persistent Storage$/ do
1745 "Failed to repair the file system of your Persistent Storage.\n\n" \
1746 'Start Tails to send an error report and learn how to recover your data.',
1751 Then /^the Persistent Storage settings tell me that the Persistent Folder feature couldn't be activated$/ do
1752 launch_persistent_storage
1754 persistent_folder_row = persistent_storage_frontend
1755 .child('Activate Persistent Folder').parent
1756 assert persistent_folder_row
1757 .child(description: 'Activation failed')
1760 Given /^the persistence partition on USB drive "([^"]+)" uses LUKS version 1$/ do |name|
1761 # NOTE: This step requires that the persistence partition is locked,
1762 # else the `cryptsetup convert` command will fail.
1764 dev = $vm.persistent_storage_dev_on_disk(name)
1765 # First we need to configure a key derivation function which is supported by
1767 $vm.execute_successfully(
1768 "echo -n #{@persistence_password} | " \
1769 "cryptsetup luksConvertKey --batch-mode --pbkdf pbkdf2 --key-file=- #{dev}"
1771 $vm.execute_successfully("cryptsetup convert --batch-mode --type luks1 #{dev}")
1774 Given /^I reload tails-persistent-storage.service$/ do
1775 $vm.execute_successfully('systemctl reload tails-persistent-storage.service')
1778 Given(/^I corrupt the Persistent Storage filesystem on USB drive "([^"]*)"( in a way which can't be automatically repaired)?$/) do |name, requires_manual_repair|
1779 # Unlock the Persistent Storage
1780 $vm.execute_successfully(
1781 "echo -n #{@persistence_password} | " \
1782 'cryptsetup luksOpen --batch-mode --key-file=- ' \
1783 "#{$vm.persistent_storage_dev_on_disk(name)} TailsData_unlocked"
1786 if requires_manual_repair
1787 # Corrupt the filesystem
1788 $vm.execute_successfully(
1789 'dd if=/dev/zero of=/dev/mapper/TailsData_unlocked bs=1k count=4k seek=10'
1792 # Mount the filesystem
1793 $vm.execute_successfully('mkdir -p /tmp/persistence')
1794 $vm.execute_successfully('mount /dev/mapper/TailsData_unlocked /tmp/persistence')
1795 # Corrupt the filesystem
1796 $vm.execute_successfully('rm -rf /tmp/persistence/lost+found')
1797 # Unmount the filesystem
1798 $vm.execute_successfully('umount /tmp/persistence')
1801 # Lock the Persistent Storage
1802 $vm.execute_successfully('cryptsetup luksClose TailsData_unlocked')
1805 Given(/^the Persistent Storage filesystem is corrupted beyond what e2fsck can repair$/) do
1806 fsck_fail_script = <<~SCRIPT
1810 $vm.file_overwrite('/usr/sbin/e2fsck', fsck_fail_script)
1811 $vm.execute_successfully('chmod a+rx /usr/sbin/e2fsck')
1814 Then(/^the filesystem of the Persistent Storage was repaired$/) do
1815 $vm.execute_successfully(
1816 'journalctl -u tails-persistent-storage.service | ' \
1817 'grep -q "e2fsck corrected file system errors"'
1821 When(/^I repair the filesystem of the Persistent Storage$/) do
1822 greeter.child('Repair File System', roleName: 'push button').click
1825 Then(/^the Welcome Screen tells me that the filesystem was repaired successfully$/) do
1827 greeter.child?('File System Repaired Successfully', roleName: 'label')
1831 When(/^I close the filesystem repair dialog$/) do
1832 greeter.child('Close', roleName: 'push button').click
1835 Then(/^the Persistent Storage is successfully unlocked$/) do
1839 Then(/^the Welcome Screen tells me that my hardware is probably failing$/) do
1841 greeter.child?('Error reading data from your Persistent Storage. ' \
1842 'The hardware of your USB stick is probably failing.\n\n.*',