3 Tails::Persistence::Setup - main application class
7 package Tails
::Persistence
::Setup
;
9 use MooseX
::Method
::Signatures
;
10 use MooseX
::Types
::Moose
qw( :all );
11 use MooseX
::Types
::Path
::Class
;
12 use MooseX
::Has
::Sugar
::Saccharin
;
16 with
'Tails::Role::DisplayError::Gtk3';
17 with
'Tails::Role::HasEncoding';
18 with
'Tails::Role::HasDBus::System';
19 with
'MooseX::Getopt::Dashes';
22 use namespace
::autoclean
;
25 use Carp
::Assert
::More
;
27 use English
qw{-no_match_vars
};
28 use Glib
qw{TRUE FALSE
};
30 use Net
::DBus
qw(:typing);
31 use Net
::DBus
::Annotation
qw(:call);
32 use List
::Util
qw{first max
};
33 use Number
::Format
qw(:subs);
37 use Tails
::RunningSystem
;
40 use Tails
::Persistence
::Configuration
;
41 use Tails
::Persistence
::Constants
;
43 use Tails
::Persistence
::Step
::Bootstrap
;
44 use Tails
::Persistence
::Step
::Configure
;
45 use Tails
::Persistence
::Step
::Delete
;
46 use Tails
::Persistence
::Utils
qw{align_up_at_2MiB align_down_at_2MiB step_name_to_class_name get_variable_from_file check_config_file_permissions
};
50 setlocale
(LC_MESSAGES
, "");
51 textdomain
("tails-persistence-setup");
60 documentation
=> q{Get more output.},
62 exists $ENV{DEBUG
} && defined $ENV{DEBUG
} && $ENV{DEBUG
}
67 documentation
=> q{Make some sanity checks non-fatal.};
70 lazy_build ro
'Tails::UDisks',
71 metaclass
=> 'NoGetopt',
73 qw{bytes_array_to_string device_has_partition_with_label
74 drive_is_optical drive_is_connected_via_a_supported_interface
75 device_partition_with_label get_block_device_property
76 get_filesystem_property get_partition_property luks_holder
77 mountpoints partitions udisks_service
}
80 has
'running_system' =>
81 lazy_build ro
'Tails::RunningSystem',
82 metaclass
=> 'NoGetopt',
84 qw{boot_drive boot_block_device boot_device_file boot_drive_model boot_drive_vendor
86 started_from_device_installed_with_tails_installer
}
89 has
'persistence_constants' =>
90 lazy_build ro
'Tails::Persistence::Constants',
91 metaclass
=> 'NoGetopt',
95 } qw{partition_label partition_guid filesystem_type filesystem_label
96 minimum_size filesystem_options state_file
}
100 lazy_build ro
'Gtk3::Window',
101 metaclass
=> 'NoGetopt';
103 has
"$_" => lazy_build ro Str
104 for (qw{override_liveos_mountpoint override_boot_drive
105 override_system_partition
});
107 has
'persistence_partition_device_file'=> lazy_build ro Str
, metaclass
=> 'NoGetopt';
108 has
'persistence_partition_size' => lazy_build ro Int
, metaclass
=> 'NoGetopt';
109 has
'persistence_is_enabled' => lazy_build ro Bool
, metaclass
=> 'NoGetopt';
110 has
'persistence_is_read_write' => lazy_build ro Bool
, metaclass
=> 'NoGetopt';
112 has
'persistence_partition_mountpoint' => (
113 isa
=> 'Path::Class::Dir',
117 metaclass
=> 'NoGetopt',
120 foreach (qw{beginning_of_free_space size_of_free_space
}) {
121 has
$_ => lazy_build ro Int
, metaclass
=> 'NoGetopt';
124 has
'current_step' =>
126 predicate
'has_current_step',
127 metaclass
=> 'NoGetopt';
130 lazy_build required ro
'ArrayRef[Str]',
133 all_steps
=> 'elements',
134 number_of_steps
=> 'count',
135 append_to_steps
=> 'push',
136 shift_steps
=> 'shift',
137 next_step
=> 'first',
138 grep_steps
=> 'grep',
140 documentation
=> q{Specify once per wizard step to run. Supported steps are: bootstrap, configure, delete.};
146 grep_orig_steps
=> 'grep',
149 has
'passphrase' => rw Str
, documentation
=> q{Unsupported. Developers only.};
151 has
'configuration' =>
152 lazy_build rw
'Tails::Persistence::Configuration',
153 handles
=> { save_configuration
=> 'save' },
154 metaclass
=> 'NoGetopt';
156 has
'+codeset' => ( metaclass
=> 'NoGetopt' );
157 has
'+encoding' => ( metaclass
=> 'NoGetopt' );
160 =head1 CONSTRUCTORS AND BUILDERS
165 my @orig_steps = $self->all_steps;
166 $self->orig_steps(\
@orig_steps);
174 sub _build_persistence_constants
{ my $self = shift; Tails
::Persistence
::Constants
->new(); }
175 sub _build_udisks
{ my $self = shift; Tails
::UDisks
->new(); }
177 sub _build_running_system
{
181 for (qw{liveos_mountpoint boot_drive system_partition
}) {
182 my $attribute = "override_$_";
183 my $predicate = "has_$attribute";
184 if ($self->$predicate) {
185 push @args, ($_ => $self->$attribute)
189 Tails
::RunningSystem
->new(main_window
=> $self->main_window, @args);
192 sub _build_persistence_is_enabled
{
195 -e
$self->persistence_state_file || return 0;
196 -r
$self->persistence_state_file || return 0;
198 my $value = $self->get_variable_from_persistence_state_file(
199 'TAILS_PERSISTENCE_ENABLED'
201 defined($value) && $value eq 'true';
204 sub _build_persistence_is_read_write
{
207 -e
$self->persistence_state_file || return 0;
208 -r
$self->persistence_state_file || return 0;
210 my $value = $self->get_variable_from_persistence_state_file(
211 'TAILS_PERSISTENCE_READONLY'
213 ! (defined($value) && $value eq 'true');
219 if ($self->device_has_persistent_volume) {
220 return [ qw{configure
} ];
223 return [ qw{bootstrap configure
} ]
227 sub _build_main_window
{
229 my $win = Gtk3
::Window
->new('toplevel');
230 $win->set_title($self->encoding->decode(gettext
('Setup Tails persistent volume')));
232 $win->set_border_width(10);
234 $win->add($self->current_step->main_box) if $self->has_current_step;
235 $win->signal_connect('destroy' => sub { Gtk3
->main_quit; });
236 $win->signal_connect('key-press-event' => sub {
239 $win->destroy if $event->key->{keyval
} == Gtk3
::Gdk
::keyval_from_name
('Escape');
241 $win->set_default($self->current_step->go_button) if $self->has_current_step;
246 sub _build_persistence_partition_mountpoint
{
250 $_ eq '/live/persistence/TailsData_unlocked'
251 or $_ eq '/media/'.getpwuid($UID).'/TailsData'
252 } $self->mountpoints($self->persistence_partition);
255 sub _build_beginning_of_free_space
{
261 $self->get_partition_property($_, 'Offset')
262 + $self->get_partition_property($_, 'Size')
263 } $self->partitions($self->boot_block_device)
268 sub _build_size_of_free_space
{
272 $self->get_block_device_property($self->boot_block_device, 'Size')
273 - $self->beginning_of_free_space
277 sub _build_persistence_partition_device_file
{
280 return $self->bytes_array_to_string($self->get_block_device_property(
281 $self->persistence_partition, 'PreferredDevice'
285 sub _build_persistence_partition_size
{
288 $self->get_block_device_property($self->persistence_partition, 'Size');
291 sub _build_configuration
{
294 my $config_file_path = file
($self->persistence_partition_mountpoint, 'persistence.conf');
295 if (-e
$config_file_path) {
296 my $expected_uid = getpwnam('tails-persistence-setup');
297 my $expected_gid = getgrnam('tails-persistence-setup');
299 check_config_file_permissions
(
302 uid
=> $expected_uid,
303 gid
=> $expected_gid,
310 $self->display_error(
312 $self->encoding->decode(gettext
('Error')),
313 $self->encoding->decode(gettext
(
319 Tails
::Persistence
::Configuration
->new(
320 config_file_path
=> $config_file_path
332 say STDERR
$self->encoding->encode($mesg) if $self->verbose;
337 my $step_name = shift;
342 method
=> 'device_has_persistent_volume',
343 message
=> $self->encoding->decode(gettext
(
344 "Device %s already has a persistent volume.")),
347 needs_device_arg
=> 1,
350 method
=> 'device_has_enough_free_space',
351 message
=> $self->encoding->decode(gettext
(
352 "Device %s has not enough unallocated space.")),
353 needs_device_arg
=> 1,
358 method
=> 'device_has_persistent_volume',
359 message
=> $self->encoding->decode(gettext
(
360 "Device %s has no persistent volume.")),
361 needs_device_arg
=> 1,
364 method
=> 'persistence_is_enabled',
365 message
=> $self->encoding->decode(gettext
(
366 "Cannot delete the persistent volume while in use. You should restart Tails without persistence.")),
372 method
=> 'device_has_persistent_volume',
373 message
=> $self->encoding->decode(gettext
(
374 "Device %s has no persistent volume.")),
375 needs_device_arg
=> 1,
380 if (! $self->grep_orig_steps(sub { $_ eq 'bootstrap' })) {
381 push @
{$step_checks{configure
}}, (
383 method
=> 'persistence_partition_is_unlocked',
384 message
=> $self->encoding->decode(gettext
(
385 "Persistence volume is not unlocked.")),
388 method
=> 'persistence_filesystem_is_mounted',
389 message
=> $self->encoding->decode(gettext
(
390 "Persistence volume is not mounted.")),
393 method
=> 'persistence_filesystem_is_readable',
394 message
=> $self->encoding->decode(gettext
(
395 "Persistence volume is not readable. Permissions or ownership problems?")),
398 method
=> 'persistence_filesystem_is_writable',
399 message
=> $self->encoding->decode(gettext
(
400 "Persistence volume is not writable. Maybe it was mounted read-only?")),
407 method
=> 'drive_is_connected_via_a_supported_interface',
408 message
=> $self->encoding->decode(gettext
(
409 "Tails is running from non-USB / non-SDIO device %s.")),
410 needs_drive_arg
=> 1,
413 method
=> 'drive_is_optical',
414 message
=> $self->encoding->decode(gettext
(
415 "Device %s is optical.")),
417 needs_drive_arg
=> 1,
420 method
=> 'started_from_device_installed_with_tails_installer',
421 message
=> $self->encoding->decode(gettext
(
422 "Device %s was not created using Tails Installer.")),
427 && exists $step_checks{$step_name}
428 && defined $step_checks{$step_name}
430 push @checks, @
{$step_checks{$step_name}};
433 foreach my $check (@checks) {
434 my $check_method = $self->meta->get_method($check->{method
});
435 assert_defined
($check_method);
438 if (exists($check->{needs_device_arg
}) && $check->{needs_device_arg
}) {
439 push @args, $self->boot_block_device;
441 elsif (exists($check->{needs_drive_arg
}) && $check->{needs_drive_arg
}) {
442 push @args, $self->boot_drive;
444 $res = $check_method->execute(@args);
445 if (exists($check->{must_be_false
}) && $check->{must_be_false
}) {
449 my $message = $self->encoding->decode(sprintf(
450 gettext
($check->{message
}),
451 $self->boot_device_file));
452 if ($self->force && exists($check->{can_be_forced
}) && $check->{can_be_forced
}) {
454 "... but --force is enabled, ignoring results of this sanity check.";
457 $self->display_error(
459 $self->encoding->decode(gettext
('Error')),
473 $self->debug("Entering Tails::Persistence::Setup::run");
474 $self->debug(sprintf("Working on device %s", $self->boot_device_file));
476 $self->main_window->set_visible(FALSE
);
477 $self->goto_next_step;
478 $self->debug("Entering main Gtk3 loop.");
482 sub device_has_persistent_volume
{
485 $device ||= $self->boot_block_device;
487 $self->debug("Entering device_has_persistent_volume");
488 return $self->device_has_partition_with_label($device, $self->persistence_partition_label);
491 sub device_has_enough_free_space
{
495 $self->size_of_free_space >= $self->persistence_minimum_size;
498 sub persistence_partition
{
501 $self->debug("Entering persistence_partition");
502 $self->device_partition_with_label(
503 $self->boot_block_device,
504 $self->persistence_partition_label
508 sub create_persistence_partition
{
511 $opts->{end_cb
} ||= sub { say STDERR
"finished." };
513 $self->debug("Entering create_persistence_partition");
515 my $offset = $self->beginning_of_free_space;
516 my $size = $self->size_of_free_space;
517 my $type = $self->persistence_partition_guid;
518 my $label = $self->persistence_partition_label;
521 $self->debug(sprintf(
522 "Creating partition of size %s at offset %s on device %s",
523 format_bytes
($size, mode
=> "iec"), $offset, $self->boot_device_file
526 $self->udisks_service->get_object($self->boot_block_device)
527 ->as_interface('org.freedesktop.UDisks2.PartitionTable')
528 ->CreatePartition(dbus_call_async
, $offset, $size, $type, $label, $options)
530 $self->create_persistent_encrypted_filesystem($opts, @_);
533 $self->debug("waiting...");
536 sub create_persistent_encrypted_filesystem
{
539 $opts->{end_cb
} ||= sub { say STDERR
"finished." };
540 my $create_partition_reply = shift;
541 my ($created_device, $create_partition_error);
543 $self->debug("Entering create_persistent_encrypted_filesystem");
545 # For some reason, we cannot get the exception when Try::Tiny is used,
546 # so let's do it by hand.
549 eval { $created_device = $create_partition_reply->get_result };
550 $create_partition_error = $@
;
552 if ($create_partition_error) {
553 return $opts->{end_cb
}->({
554 create_partition_error
=> $create_partition_error,
558 my $fstype = $self->persistence_filesystem_type;
560 %{$self->persistence_filesystem_options},
561 'encrypt.passphrase' => $opts->{passphrase
},
564 $self->udisks_service->get_object($self->persistence_partition)
565 ->as_interface('org.freedesktop.UDisks2.Block')
567 dbus_call_async
, dbus_call_timeout
, 3600 * 1000,
569 ->set_notify(sub { $opts->{end_cb
}->({
570 created_device
=> $created_device,
574 $self->debug("waiting...");
577 sub delete_persistence_partition
{
579 (my $opts = shift) ||= {};
580 $opts->{end_cb
} ||= sub { say STDERR
"finished." };
582 $self->debug(sprintf("Deleting partition %s", $self->persistence_partition_device_file));
584 my $obj = $self->udisks_service->get_object($self->persistence_partition);
586 # lock the device if it is unlocked
587 my $luksholder = $self->luks_holder($self->persistence_partition);
589 if ($self->persistence_filesystem_is_mounted) {
590 $self->udisks_service
591 ->get_object($luksholder)
592 ->as_interface("org.freedesktop.UDisks2.Filesystem")
595 $obj->as_interface('org.freedesktop.UDisks2.Encrypted')->Lock({});
598 # TODO: wipe the LUKS header (#8436)
600 my $iface = $obj->as_interface("org.freedesktop.UDisks2.Partition");
601 $iface->Delete(dbus_call_async
, {})->set_notify($opts->{end_cb
});
602 $self->debug("waiting...");
605 sub mount_persistence_partition
{
609 $self->debug(sprintf("Mounting partition %s", $self->persistence_partition_device_file));
611 my $luks_holder = $self->luks_holder($self->persistence_partition);
613 return $self->udisks_service
614 ->get_object($luks_holder)
615 ->as_interface("org.freedesktop.UDisks2.Filesystem")
616 ->Mount(dbus_call_sync
, {});
619 sub empty_main_window
{
622 my $child = $self->main_window->get_child;
623 $self->main_window->remove($child) if defined($child);
626 sub run_current_step
{
628 my ($width, $height) = $self->main_window->get_size();
630 $self->debug("Running step " . $self->current_step->name);
632 $self->current_step->working(0);
633 $self->empty_main_window;
634 $self->main_window->add($self->current_step->main_box);
635 $self->main_window->set_default($self->current_step->go_button);
636 $self->main_window->show_all;
637 $self->current_step->working(0);
638 $self->main_window->set_visible(TRUE
);
640 if($self->current_step->name eq 'configure') {
641 $self->main_window->resize($width, $self->main_window->get_screen()->get_height());
644 $self->main_window->resize($width, $height);
650 (my $opts = shift) ||= {};
654 if ($next_step = $self->shift_steps) {
655 if ($self->check_sanity($next_step)) {
656 $self->current_step($self->step_object_from_name($next_step));
657 $self->run_current_step;
660 # check_sanity already has displayed an error dialog,
661 # that the user already closed.
666 $self->debug("No more steps.");
667 $self->current_step->title->set_text($self->encoding->decode(gettext
(
668 q{Persistence wizard - Finished}
670 $self->current_step->subtitle->set_text($self->encoding->decode(gettext
(
671 q{Any changes you have made will only take effect after restarting Tails.
673 You may now close this application.}
675 $self->current_step->description->set_text(' ');
676 $self->current_step->go_button->hide;
677 $self->current_step->status_area->hide;
681 sub step_object_from_name
{
685 my $class_name = step_name_to_class_name
($name);
689 if ($name eq 'bootstrap') {
692 $self->create_persistence_partition({ @_ })
694 size_of_free_space
=> $self->size_of_free_space,
695 should_mount_persistence_partition
=>
696 0 < $self->grep_steps(sub { $_ eq 'configure' }),
697 mount_persistence_partition_cb
=> sub {
698 $self->mount_persistence_partition({ @_ })
702 elsif ($name eq 'delete') {
705 $self->delete_persistence_partition({ @_ })
707 persistence_partition
=> $self->persistence_partition,
708 persistence_partition_device_file
=> $self->persistence_partition_device_file,
709 persistence_partition_size
=> $self->persistence_partition_size,
712 elsif ($name eq 'configure') {
715 $self->save_configuration({ @_ })
717 configuration
=> $self->configuration,
718 persistence_partition
=> $self->persistence_partition,
719 persistence_partition_device_file
=> $self->persistence_partition_device_file,
720 persistence_partition_size
=> $self->persistence_partition_size,
724 return $class_name->new(
726 encoding
=> $self->encoding,
727 success_callback
=> sub { $self->goto_next_step({ @_ }) },
728 drive_vendor
=> $self->boot_drive_vendor,
729 drive_model
=> $self->boot_drive_model,
735 method get_variable_from_persistence_state_file
(Str
$variable) {
736 get_variable_from_file
($self->persistence_state_file, $variable);
739 method persistence_filesystem_is_mounted
() {
740 return scalar($self->mountpoints($self->persistence_partition));
743 method persistence_partition_is_unlocked
() {
744 my $luks_holder = $self->luks_holder($self->persistence_partition) || return;
749 method persistence_filesystem_is_readable
() {
750 return unless my $mountpoint = $self->persistence_partition_mountpoint;
753 use filetest
'access'; # take ACLs into account
754 $ret = -r
$self->persistence_partition_mountpoint;
759 method persistence_filesystem_is_writable
() {
760 return unless my $mountpoint = $self->persistence_partition_mountpoint;
763 use filetest
'access'; # take ACLs into account
764 $ret = -w
$self->persistence_partition_mountpoint;