Merge remote-tracking branch 'origin/stable' into devel
[tails.git] / features / step_definitions / usb.rb
blob63b70736596daa3f2bd5cf00d2268c2fede6ff4f
1 require 'securerandom'
2 require 'json'
4 def tps_is_created
5   $vm.execute('/usr/local/lib/tpscli is-created').success?
6 end
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
10 # Tails is aware of.
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>
14   script = [
15     'from tps.configuration import features',
16     'for feature in features.get_classes():',
17     '    for binding in feature.Bindings:',
18     '        print(binding)',
19   ]
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'
26   bindings_mapping = {}
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
38              else
39                source_str.split('=')[1]
40              end
41     bindings_mapping[source] = destination
42   end
43   bindings_mapping
44 end
46 def tps_bindings
47   get_tps_bindings
48 end
50 def tps_bind_mounts
51   get_tps_bindings(skip_links: true)
52 end
54 def tps_features
55   c = $vm.execute_successfully('/usr/local/lib/tpscli get-features')
56   JSON.parse(c.stdout.chomp)
57 end
59 def tps_feature_is_enabled(feature, reload: true)
60   tps_reload if reload
61   c = $vm.execute("/usr/local/lib/tpscli is-enabled #{feature}")
62   c.success?
63 end
65 def tps_feature_is_active(feature, reload: true)
66   tps_reload if reload
67   c = $vm.execute("/usr/local/lib/tpscli is-active #{feature}")
68   c.success?
69 end
71 def tps_reload
72   $vm.execute_successfully('systemctl reload tails-persistent-storage.service')
73 end
75 def persistent_volumes_mountpoints
76   $vm.execute('ls -1 -d /live/persistence/*_unlocked/').stdout.chomp.split
77 end
79 def persistent_storage_frontend(**opts)
80   Dogtail::Application.new('tps-frontend', **opts)
81 end
83 def persistent_storage_main_frame
84   persistent_storage_frontend.child('Persistent Storage', roleName: 'frame')
85 end
87 def persistent_directory_is_active(**opts)
88   opts[:user] = 'root'
89   opts[:use_system_bus] = true
90   dbus_send(
91     'org.boum.tails.PersistentStorage',
92     '/org/boum/tails/PersistentStorage/Features/PersistentDirectory',
93     'org.freedesktop.DBus.Properties.Get',
94     'org.boum.tails.PersistentStorage.Feature',
95     'IsActive',
96     **opts
97   )
98 end
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)
107 def greeter
108   Dogtail::Application.new('Welcome to Tails!',
109                            user:               'Debian-gdm',
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) }
117   end
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')
126   @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
154   # wait a bit extra.
155   sleep 3
156   @screen.wait('TailsClonerWindow.png', 10).click
159 When /^I am told by Tails Installer that.*"([^"]+)".*$/ do |status|
160   try_for(10) do
161     tails_installer_match_status(status)
162   end
165 Then /^a suitable USB device is (?:still )?not found$/ do
166   @installer.child(
167     'No device suitable to install Tails could be found', roleName: 'label'
168   )
171 Then /^(no|the "([^"]+)") USB drive is selected$/ do |mode, name|
172   try_for(30) do
173     if mode == 'no'
174       tails_installer_selected_device == ''
175     else
176       tails_installer_is_device_selected?(name)
177     end
178   end
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
191   # Storage.
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)".
195   begin
196     clone_persistence_button = @installer
197                                .child('Clone the current Persistent Storage.*',
198                                       roleName: 'check box',
199                                       retry:    false)
200     sensitive = clone_persistence_button.sensitive?
201   rescue Dogtail::Failure
202     sensitive = false
203   end
204   if tps_is_created
205     assert(sensitive,
206            "Couldn't find clone Persistent Storage check button " \
207            '(even though a Persistent Storage exists)')
208   else
209     assert(!sensitive,
210            'Found clone Persistent Storage check button ' \
211            '(even though no Persistent Storage exists)')
212   end
214   if with_persistence
215     assert(sensitive,
216            "Can't clone with Persistent Storage: Clone button is not sensitive")
217     clone_persistence_button.click
218   end
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) }
223   begin
224     label = if action == 'reinstall'
225               'Reinstall (delete all data)'
226             else
227               action.capitalize
228             end
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'
239                            else
240                              'Delete All Data and Install'
241                            end
242       @installer.child('Question',
243                        roleName: 'alert').button(confirmation_label).click
245       if with_persistence
246         # Enter the passphrase in the passphrase dialog
247         passphrase_entry = @installer.child('Choose Passphrase',
248                                             roleName: 'dialog')
249                                      .child('Passphrase:', roleName: 'label')
250                                      .labelee
251         confirm_entry = @installer.child('Choose Passphrase',
252                                          roleName: 'dialog')
253                                   .child('Confirm:', roleName: 'label')
254                                   .labelee
255         passphrase_entry.text = @persistence_password
256         confirm_entry.text = @persistence_password
257         confirm_entry.activate
258       end
259     end
261     try_for(15 * 60, delay: 10) do
262       @installer
263         .child('Information', roleName: 'alert')
264         .child('Installation complete!', roleName: 'label')
265       true
266     end
267   rescue StandardError => e
268     debug_log("Tails Installer debug log:\n#{$vm.file_content(@installer_log_path)}")
269     raise e
270   end
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|
282     if switch.checked?
283       debug_log("#{switch.name} is already enabled, skipping")
284     else
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.
288       switch.toggle
289       try_for(10) { switch.checked? }
290     end
291   end
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'
299   )
300   if mode == 'enable'
301     assert !persistent_folder_switch.checked?
302   else
303     assert persistent_folder_switch.checked?
304   end
306   persistent_folder_switch.toggle
307   try_for(10) do
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.
311     if mode == 'enable'
312       assert persistent_folder_switch.checked?
313       persistent_directory_is_active
314     else
315       assert !persistent_folder_switch.checked?
316       !persistent_directory_is_active
317     end
318   end
319   @screen.press('alt', 'F4')
322 Given(/^I enable persistence creation in Tails Greeter$/) do
323   greeter.child('Create Persistent Storage', roleName: 'toggle button')
324          .toggle
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"
337   # label.
338   try_for(300) do
339     persistent_storage_main_frame.child('Personal Documents', roleName: 'label')
340   end
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
348   end
349   persistent_storage_main_frame.button('Co_ntinue').click
350   persistent_storage_main_frame
351     .child('Passphrase:', roleName: 'label')
352     .labelee
353     .text = @persistence_password
354   persistent_storage_main_frame
355     .child('Confirm:', roleName: 'label')
356     .labelee
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")
385     next
386   end
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}"
391   )
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
398     # memory threshold.
399     low_mem_kib - 100 * 1024 <= mem_available_kib &&
400       mem_available_kib <= low_mem_kib + 100 * 1024
401   end
404 Given /^I free up some memory$/ do
405   # This assumes that the step 'the system is very low on memory' was
406   # run before.
407   $vm.execute_successfully('rm /fill')
408   step 'the system is low on memory'
411 Given /^I close the Persistent Storage app$/ do
412   # Close any alerts
413   alert = persistent_storage_frontend.child(roleName: 'alert', retry: false)
414   while alert
415     alert.button('Close').click
416     begin
417       alert = persistent_storage_frontend.child(roleName: 'alert', retry: false)
418     rescue StandardError
419       alert = nil
420     end
421   end
423   # Close the main window
424   persistent_storage_main_frame.button('Close').click
426   # Wait for the app to close
427   try_for(10) do
428     persistent_storage_frontend(retry: false)
429     false
430   rescue StandardError
431     true
432   end
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|
440   if change_back
441     current_passphrase = @changed_persistence_password
442     new_passphrase = @persistence_password
443   else
444     current_passphrase = @persistence_password
445     new_passphrase = @changed_persistence_password
446   end
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')
460     .labelee
461     .text = current_passphrase
462   change_passphrase_dialog
463     .child('New Passphrase', roleName: 'label')
464     .labelee
465     .text = new_passphrase
466   change_passphrase_dialog
467     .child('Confirm New Passphrase', roleName: 'label')
468     .labelee
469     .text = new_passphrase
470   change_passphrase_dialog.button('Change').click
471   # Wait for the dialog to close
472   try_for(60) do
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
477     true
478   else
479     false
480   end
483 def check_disk_integrity(name, dev, scheme)
484   info = $vm.execute_successfully(
485     "udisksctl info --block-device '#{dev}'"
486   ).stdout
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,
493   # the scheme is gpt.
494   return unless scheme == 'gpt'
496   c = $vm.execute("sgdisk --verify #{dev}")
497   assert(
498     # Note that sgdisk --verify exits with 0 even if it finds problems,
499     # so we also need to check the output.
500     c.success? &&
501     c.to_s.include?('No problems found.') && \
502     # The output of sgdisk --verify includes "ERROR" if any of the
503     # following are corrupt:
504     # * The GPT header
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
510     # MBR is corrupt.
511     !c.to_s.include?('corrupt'),
512     "sgdisk --verify #{dev} failed.\n#{c}"
513   )
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}'"
520   ).stdout
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}'")
528   if part_label
529     assert_match(/^    Name: +#{part_label}$/, part_info,
530                  "Unexpected partition label on drive '#{name}', '#{dev}'")
531   end
532   if part_type
533     assert_match(/^    Type: +#{part_type}$/, part_info,
534                  "Unexpected partition type on drive '#{name}', '#{dev}'")
535   end
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'")
550   assert(
551     c.success?,
552     "USB drive '#{name}' has differences in /live:\n#{c.stdout}\n#{c.stderr}"
553   )
555   syslinux_files = $vm.execute("ls -1 #{target_root}/syslinux")
556                       .stdout.chomp.split
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}'"
564     )
565   end
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'"
572   )
574   $vm.execute("umount #{target_root}")
575   $vm.execute('sync')
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
616   # is argon2id.
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)
632   if luks_dev.nil?
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}'"
637     )
638     luks_dev = "/dev/mapper/#{name}"
639   end
641   if luks1.nil?
642     assert_luks2_with_argon2id(name, dev)
643   else
644     assert_luks1(dev)
645   end
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}'"
650   ).stdout
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}")
663   $vm.execute('sync')
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
672              else
673                @persistence_password
674              end
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.
683   try_for(120) do
684     tails_persistence_active?
685   end
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.
691   try_for(30) do
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.',
695                   roleName: 'label')
696   end
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
707   # header upgrade.
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'
717 def greeter_language
718   english_label = 'English - United States'
719   german_label = 'Deutsch - Deutschland (German - Germany)'
720   try_for(30) do
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.
724     return '', 'en'
725   rescue Dogtail::Failure
726     greeter.child(german_label, roleName: 'label', retry: false)
727     return 'German', 'de'
728   end
731 def tails_persistence_unlocked?
732   $vm.execute('tps_is_unlocked', libs: 'libtps').success?
735 def tails_persistence_active?
736   tails_persistence_unlocked? &&
737     tps_features
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?
748   end
750   tps_reload
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"
756     else
757       assert is_active, "Feature '#{feature}' is not active"
758     end
759   end
762 Then /^all tps features(| but the first one) are enabled$/ do |except_first_str|
763   except_first = !except_first_str.empty?
764   tps_reload
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"
769     else
770       assert is_enabled,  "Feature '#{feature}' is not enabled"
771     end
772   end
775 Then /^all tps features(| but the first one) are enabled and active$/ do |except_first_str|
776   except_first = !except_first_str.empty?
777   if except_first
778     step 'all tps features but the first one are enabled'
779     step 'all tps features but the first one are active'
780   else
781     step 'all tps features are enabled'
782     step 'all tps features are active'
783   end
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)
789   if check_not_enabled
790     assert !is_enabled, "Feature '#{feature}' is enabled"
791   else
792     assert is_enabled, "Feature '#{feature}' is not enabled"
793   end
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)
799   if check_not_active
800     assert !is_active, "Feature '#{feature}' is active"
801   else
802     assert is_active, "Feature '#{feature}' is not active"
803   end
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')
819 def boot_device
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'
824   ).stdout.chomp
825   $vm.execute("readlink -f /dev/block/'#{boot_dev_id}'").stdout.chomp
828 def device_info(dev)
829   # Approach borrowed from
830   # config/chroot_local_includes/lib/live/config/998-permissions
831   info = $vm.execute("udevadm info --query=property --name='#{dev}'")
832             .stdout.chomp
833   info.split("\n").map { |e| e.split('=') }.to_h
836 def boot_device_type
837   device_info(boot_device)['ID_BUS']
840 # Turn udisksctl info output into something more manipulable:
841 def parse_udisksctl_info(input)
842   tree = {}
843   section = nil
844   key = nil
845   input.chomp.split("\n").each do |line|
846     case line
847     when %r{^/org/freedesktop/UDisks2/block_devices/}
848       true
849     when /^  (org\.freedesktop\.UDisks2\..+):$/
850       section = Regexp.last_match(1)
851       tree[section] = {}
852     when /^\s+(.+?):\s+(.+)$/
853       key = Regexp.last_match(1)
854       value = Regexp.last_match(2)
855       tree[section][key] = value
856     else
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
861     end
862   end
863   fs_section = tree['org.freedesktop.UDisks2.Filesystem']
864   if fs_section && fs_section['MountPoints']
865     fs_section['MountPoints'] = fs_section['MountPoints'].split
866   end
867   tree
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/")
873   if c.success?
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}"
878       end
879     end
880   end
881   nil
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
889   )
890   if info['org.freedesktop.UDisks2.Block']['IdType'] == 'crypto_LUKS'
891     luks_device = luks_mapping(device)
892     mountpoint(luks_device) if luks_device
893   else
894     info['org.freedesktop.UDisks2.Filesystem']['MountPoints']
895       .find { |p| !p.match?(Regexp.new('^/run/nosymfollow/')) }
896   end
899 Then /^Tails is running from (.*) drive "([^"]+)"$/ do |bus, name|
900   bus = bus.downcase
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
907   # (#10504).
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')
919                  .split("\n")
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} : /,
923                                                             '').split(' ')
924     [user, groups]
925   end
926   devs.each do |dev|
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 " \
933            "'disk' or 'root'.")
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}'")
941     end
942   end
944   info = $vm.execute_successfully(
945     "udisksctl info --block-device '#{super_boot_dev}'"
946   ).stdout
947   assert_match(/^    HintSystem: +true$/, info,
948                "Boot device '#{super_boot_dev}' is not system internal " \
949                'for udisks')
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)
971   end
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"
979     )
980     assert_vmcommand_success(
981       $vm.execute("test ! -e #{mountpoint}/live-persistence.conf"),
982       "#{mountpoint}/live-persistence.conf does exist, while it should not"
983     )
984     $vm.file_glob(
985       "#{mountpoint}/persistence.conf* #{mountpoint}/live-*.conf"
986     ).each do |f|
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)
992       case f
993       when %r{.*/live-additional-software.conf$}
994         assert_equal('644', file_perms)
995       else
996         assert_equal('600', file_perms)
997       end
998     end
999   end
1002 Then /^all persistent directories(| from the old Tails version) have safe access rights$/ do |old_tails|
1003   if old_tails.empty?
1004     expected_bindings = tps_bindings
1005   else
1006     assert_not_nil($remembered_tps_bindings)
1007     expected_bindings = $remembered_tps_bindings
1008   end
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}'")
1014                      .stdout.chomp
1015       dir_owner = $vm.execute_successfully("stat -c %U '#{full_src}'")
1016                      .stdout.chomp
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'
1026       else
1027         expected_perms = '755'
1028         expected_owner = 'root'
1029       end
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}")
1036     end
1037   end
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}"
1046     )
1047   end
1050 When /^I write some dotfile expected to persist$/ do
1051   assert_vmcommand_success(
1052     $vm.execute(
1053       'touch /live/persistence/TailsData_unlocked/dotfiles/.XXX_persist',
1054       user: LIVE_USER
1055     ),
1056     'Could not create a file in the dotfiles persistence.'
1057   )
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}"
1066     )
1067   end
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}"
1076     )
1077   end
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|
1087   if old_tails.empty?
1088     expected_mounts = tps_bind_mounts
1089   else
1090     assert_not_nil($remembered_tps_bind_mounts)
1091     expected_mounts = $remembered_tps_bind_mounts
1092   end
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}"
1097     )
1098     assert(
1099       $vm.execute("test -e #{dir}/XXX_gone").failure?,
1100       "Found file that should not have persisted in persistent directory #{dir}"
1101     )
1102   end
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.'
1110   )
1111   assert_vmcommand_success(
1112     $vm.execute(
1113       "test -e $(readlink -f #{expected_bindings['dotfiles']}/.XXX_persist)"
1114     ),
1115     'Could not find expected persistent dotfile link target.'
1116   )
1119 Then /^only the expected files are present on the persistence partition on USB drive "([^"]+)"$/ do |name|
1120   assert(!$vm.running?)
1121   disk = {
1122     path: $vm.storage.disk_path(name),
1123     opts: {
1124       format:   $vm.storage.disk_format(name),
1125       readonly: true,
1126     },
1127   }
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
1131     end
1132     partition = partitions.find do |part|
1133       g.blkid(part)['PART_ENTRY_NAME'] == 'TailsData'
1134     end
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}"
1140     mount_point = '/'
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}")
1149       assert(
1150         g.exists("/#{dir}/XXX_gone") != 1,
1151         "Found file that should not have persisted in persistent directory #{dir}"
1152       )
1153     end
1154     g.umount(mount_point)
1155     g.cryptsetup_close(luks_dev)
1156   end
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.',
1174     roleName: 'label'
1175   )
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
1192   changes = [
1193     {
1194       filesystem:  :rootfs,
1195       path:        'some_new_file',
1196       status:      :added,
1197       new_content: <<~CONTENT,
1198         Some content
1199       CONTENT
1200     },
1201     {
1202       filesystem:  :rootfs,
1203       path:        'etc/os-release',
1204       status:      :modified,
1205       new_content: <<~CONTENT,
1206         NAME="Tails"
1207         VERSION="#{version}"
1208       CONTENT
1209     },
1210     {
1211       filesystem: :rootfs,
1212       path:       'usr/share/common-licenses/BSD',
1213       status:     :removed,
1214     },
1215     {
1216       filesystem: :rootfs,
1217       path:       'usr/share/doc/tor',
1218       status:     :removed,
1219     },
1220     {
1221       filesystem: :medium,
1222       path:       'utils/linux/syslinux',
1223       status:     :removed,
1224     },
1225   ]
1227   case version
1228   when '6.2~testoverlayfs'
1229     changes
1230   when '6.3~testoverlayfs'
1231     changes + [
1232       {
1233         filesystem:  :rootfs,
1234         path:        'some_new_file_6.3',
1235         status:      :added,
1236         new_content: <<~CONTENT,
1237           Some content 6.3
1238         CONTENT
1239       },
1240       {
1241         filesystem: :rootfs,
1242         path:       'usr/share/common-licenses/MPL-1.1',
1243         status:     :removed,
1244       },
1245       {
1246         filesystem: :medium,
1247         path:       'utils/mbr/mbr.bin',
1248         status:     :removed,
1249       },
1250     ]
1251   else
1252     raise "Test suite implementation error: unsupported version #{version}"
1253   end
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]
1263     when :rootfs
1264       path = '/'
1265       path += "var/lib/#{chroot_browser}/chroot/" if chroot_browser
1266       path += change[:path]
1267     when :medium
1268       path = "/lib/live/mount/medium/#{change[:path]}"
1269     else
1270       raise "Unknown filesystem '#{change[:filesystem]}'"
1271     end
1272     case change[:status]
1273     when :removed
1274       assert_equal(!upgrade_applied, $vm.file_exist?(path))
1275     when :added
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))
1279       end
1280     when :modified
1281       assert($vm.file_exist?(path))
1282       if upgrade_applied
1283         assert_not_nil(change[:new_content])
1284         assert_equal(change[:new_content], $vm.file_content(path))
1285       end
1286     else
1287       raise "Unknown status '#{change[:status]}'"
1288     end
1289   end
1292 Then /^I am proposed to install an incremental upgrade to version (.+)$/ do |version|
1293   recovery_proc = proc do
1294     recover_from_upgrader_failure
1295   end
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)
1301   end
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'
1307   )
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'
1317   end
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)
1323   end
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
1328   $vm.file_overwrite(
1329     '/lib/live/mount/medium/syslinux/syslinux.cfg',
1330     @orig_syslinux_cfg
1331   )
1334 def default_squash
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")
1341   assert_equal(
1342     default_squash,
1343     listed_squashes.first,
1344     "Tails.module does not list #{default_squash} on the first line"
1345   )
1346   present_squashes = $vm.file_glob("#{live}/*.squashfs").map do |f|
1347     f.sub('/lib/live/mount/medium/live/', '')
1348   end
1349   # Sanity check
1350   assert_equal(
1351     listed_squashes.sort,
1352     present_squashes.sort,
1353     'Tails.module does not match the present .squashfs files'
1354   )
1355   listed_squashes
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}")
1367   assert_equal(
1368     old_squashes + [new_squash],
1369     installed_squashes,
1370     'Implementation error, alert the test suite maintainer!'
1371   )
1372   $vm.execute_successfully(
1373     "sed -i 's/^VERSION=.*/VERSION=\"#{version}\"/' " \
1374     '/etc/os-release'
1375   )
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',
1382       user: LIVE_USER
1383     )
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}'"
1388     )
1389   end
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}!'"
1406     )
1407   end
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.
1414   true
1417 Then /^(?:no|only the (.+)) SquashFS delta is installed$/ do |version|
1418   expected_squashes = [default_squash]
1419   expected_squashes << "#{version}.squashfs" if version
1420   assert_equal(
1421     expected_squashes,
1422     installed_squashes,
1423     'Unexpected .squashfs files encountered'
1424   )
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}"
1453   ).stdout
1454   partition_size = parse_udisksctl_info(udisks_info)[
1455     'org.freedesktop.UDisks2.Partition'
1456   ]['Size'].to_i
1458   # Partition size:
1459   assert(
1460     partition_size >= wanted_size,
1461     "FAT partition is too small: #{partition_size} is less than #{wanted_size}"
1462   )
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}"
1479   ).stdout
1480   fs_uuid = parse_udisksctl_info(
1481     udisks_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}"
1496   ).stdout
1497   fs_label = parse_udisksctl_info(
1498     udisks_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}"
1513   ).stdout
1514   flags = parse_udisksctl_info(
1515     udisks_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'),
1530     'GB'
1531   ).round(1).to_s
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) }
1538        .grabFocus
1539   disks.child(description: 'Drive Options', roleName: 'toggle button')
1540        .click
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
1554   text_entry.activate
1555   text_entry.activate
1557   try_for(10) do
1558     !select_disk_image_dialog.showing?
1559   end
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')
1568        .grabFocus
1569   @screen.press('Return')
1570   # Wait until the restoration job is finished
1571   job = disks.child('Job', roleName: 'label')
1572   try_for(180) do
1573     !job.showing?
1574   end
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
1581   # miss it.
1582   step 'I disable the Unsafe Browser'
1583   sleep 2
1584   step 'I disable networking in Tails Greeter'
1585   sleep 2
1586   step 'I disable MAC spoofing in Tails Greeter'
1587   sleep 2
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'
1591   sleep 2
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'
1605   ).stdout
1606   if non_default
1607     expected = <<~EXPECTED
1608       TAILS_FORMATS=de_DE
1609       TAILS_LOCALE_NAME=de_DE
1610       TAILS_MACSPOOF_ENABLED=false
1611       TAILS_NETWORK=false
1612       TAILS_UNSAFE_BROWSER_ENABLED=false
1613       TAILS_XKBLAYOUT=de
1614       TAILS_XKBMODEL=pc105
1615       TAILS_XKBVARIANT=
1616     EXPECTED
1617     $vm.execute_successfully(
1618       'grep "^TAILS_USER_PASSWORD=\'.\+\'$" ' \
1619       '/var/lib/gdm3/settings/persistent/tails.password'
1620     )
1621     $vm.execute_successfully(
1622       'grep "^TAILS_PASSWORD_HASH_FUNCTION=SHA512$" ' \
1623       '/var/lib/gdm3/settings/persistent/tails.password'
1624     )
1625   else
1626     expected = <<~EXPECTED
1627       TAILS_FORMATS=en_US
1628       TAILS_LOCALE_NAME=en_US
1629       TAILS_MACSPOOF_ENABLED=true
1630       TAILS_NETWORK=true
1631       TAILS_UNSAFE_BROWSER_ENABLED=true
1632       TAILS_XKBLAYOUT=us
1633       TAILS_XKBMODEL=pc105
1634       TAILS_XKBVARIANT=
1635     EXPECTED
1636     assert(!$vm.file_exist?('/var/lib/gdm3/settings/persistent/tails.password'))
1637   end
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.',
1646                               roleName: 'label')
1647   if no
1648     assert(!settings_restored)
1649   else
1650     assert(settings_restored)
1651   end
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'
1657   )
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"'
1670   end
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
1702     )
1703   end
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'
1713   )
1714   confirm_deletion_dialog.button('Delete Data').click
1716   # Wait for the delete data button to disappear
1717   try_for(10) do
1718     persistent_folder_delete_button(retry: false)
1719   rescue Dogtail::Failure
1720     # The button couldn't be found, which is what we want
1721     true
1722   else
1723     false
1724   end
1725   @screen.press('alt', 'F4')
1728 Then /^the Welcome Screen tells me that the Persistent Folder feature couldn't be activated$/ do
1729   try_for(60) do
1730     greeter.child?('Failed to activate some features of the Persistent Storage: ' \
1731                    'Persistent Folder.\n.*',
1732                    roleName: 'label')
1733   end
1736 Then(/^the Welcome Screen tells me that filesystem errors were found on the Persistent Storage$/) do
1737   try_for(60) do
1738     greeter.child?('File System Errors', roleName: 'label') && \
1739       greeter.child?('Repair File System', roleName: 'push button')
1740   end
1743 Then /^the Welcome Screen tells me that it failed to repair the Persistent Storage$/ do
1744   greeter.child(
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.',
1747     roleName: 'label'
1748   )
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
1766   # LUKS version 1.
1767   $vm.execute_successfully(
1768     "echo -n #{@persistence_password} | " \
1769     "cryptsetup luksConvertKey --batch-mode --pbkdf pbkdf2 --key-file=- #{dev}"
1770   )
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"
1784   )
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'
1790     )
1791   else
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')
1799   end
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
1807     #!/bin/sh
1808     exit 4
1809   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"'
1818   )
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
1826   try_for(60) do
1827     greeter.child?('File System Repaired Successfully', roleName: 'label')
1828   end
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
1836   pending
1839 Then(/^the Welcome Screen tells me that my hardware is probably failing$/) do
1840   try_for(60) do
1841     greeter.child?('Error reading data from your Persistent Storage. ' \
1842                      'The hardware of your USB stick is probably failing.\n\n.*',
1843                    roleName: 'label')
1844   end