Don't hardcode user name, to ease development.
[tails-persistence-setup.git] / lib / Tails / Persistence / Setup.pm
blobbc448360f06b813130f129afaa826662c3b0cd6d
1 =head1 NAME
3 Tails::Persistence::Setup - main application class
5 =cut
7 package Tails::Persistence::Setup;
8 use Moose;
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';
21 use 5.10.0;
22 use namespace::autoclean;
24 use autodie qw(:all);
25 use Carp::Assert::More;
26 use Data::Dumper;
27 use English qw{-no_match_vars};
28 use Glib qw{TRUE FALSE};
29 use Gtk3 qw{-init};
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);
34 use Path::Class;
35 use Try::Tiny;
37 use Tails::RunningSystem;
38 use Tails::UDisks;
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};
48 use Locale::gettext;
49 use POSIX;
50 setlocale(LC_MESSAGES, "");
51 textdomain("tails-persistence-setup");
54 =head1 ATTRIBUTES
56 =cut
58 has 'verbose' =>
59 ro Bool,
60 documentation => q{Get more output.},
61 default => sub {
62 exists $ENV{DEBUG} && defined $ENV{DEBUG} && $ENV{DEBUG}
65 has 'force' =>
66 lazy_build ro Bool,
67 documentation => q{Make some sanity checks non-fatal.};
69 has 'udisks' =>
70 lazy_build ro 'Tails::UDisks',
71 metaclass => 'NoGetopt',
72 handles => [
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',
83 handles => [
84 qw{boot_drive boot_block_device boot_device_file boot_drive_model boot_drive_vendor
85 boot_drive_size
86 started_from_device_installed_with_tails_installer}
89 has 'persistence_constants' =>
90 lazy_build ro 'Tails::Persistence::Constants',
91 metaclass => 'NoGetopt',
92 handles => [
93 map {
94 "persistence_$_"
95 } qw{partition_label partition_guid filesystem_type filesystem_label
96 minimum_size filesystem_options state_file}
99 has 'main_window' =>
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',
114 is => 'rw',
115 lazy_build => 1,
116 coerce => 1,
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' =>
125 rw Object,
126 predicate 'has_current_step',
127 metaclass => 'NoGetopt';
129 has 'steps' =>
130 lazy_build required ro 'ArrayRef[Str]',
131 traits => ['Array'],
132 handles => {
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.};
142 has 'orig_steps' =>
143 rw 'ArrayRef[Str]',
144 traits => ['Array'],
145 handles => {
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
162 =cut
164 method BUILD {
165 my @orig_steps = $self->all_steps;
166 $self->orig_steps(\@orig_steps);
169 sub _build_force {
170 my $self = shift;
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 {
178 my $self = shift;
180 my @args;
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 {
193 my $self = shift;
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 {
205 my $self = shift;
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');
216 sub _build_steps {
217 my $self = shift;
219 if ($self->device_has_persistent_volume) {
220 return [ qw{configure} ];
222 else {
223 return [ qw{bootstrap configure} ]
227 sub _build_main_window {
228 my $self = shift;
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 {
237 my $twin = shift;
238 my $event = shift;
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;
243 return $win;
246 sub _build_persistence_partition_mountpoint {
247 my $self = shift;
249 first {
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 {
256 my $self = shift;
258 align_up_at_2MiB(
259 max(
260 map {
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 {
269 my $self = shift;
271 align_down_at_2MiB(
272 $self->get_block_device_property($self->boot_block_device, 'Size')
273 - $self->beginning_of_free_space
277 sub _build_persistence_partition_device_file {
278 my $self = shift;
280 return $self->bytes_array_to_string($self->get_block_device_property(
281 $self->persistence_partition, 'PreferredDevice'
285 sub _build_persistence_partition_size {
286 my $self = shift;
288 $self->get_block_device_property($self->persistence_partition, 'Size');
291 sub _build_configuration {
292 my $self = shift;
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');
298 try {
299 check_config_file_permissions(
300 $config_file_path,
302 uid => $expected_uid,
303 gid => $expected_gid,
304 mode => oct(600),
305 acl => '',
309 catch {
310 $self->display_error(
311 $self->main_window,
312 $self->encoding->decode(gettext('Error')),
313 $self->encoding->decode(gettext(
315 )));
319 Tails::Persistence::Configuration->new(
320 config_file_path => $config_file_path
325 =head1 METHODS
327 =cut
329 sub debug {
330 my $self = shift;
331 my $mesg = shift;
332 say STDERR $self->encoding->encode($mesg) if $self->verbose;
335 sub check_sanity {
336 my $self = shift;
337 my $step_name = shift;
339 my %step_checks = (
340 'bootstrap' => [
342 method => 'device_has_persistent_volume',
343 message => $self->encoding->decode(gettext(
344 "Device %s already has a persistent volume.")),
345 must_be_false => 1,
346 can_be_forced => 1,
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,
356 'delete' => [
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.")),
367 must_be_false => 1,
370 'configure' => [
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?")),
405 my @checks = (
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.")),
416 must_be_false => 1,
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.")),
423 must_be_false => 0,
426 if ($step_name
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);
436 my $res;
437 my @args = ($self);
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}) {
446 $res = ! $res;
448 if (! $res) {
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}) {
453 warn "$message",
454 "... but --force is enabled, ignoring results of this sanity check.";
456 else {
457 $self->display_error(
458 $self->main_window,
459 $self->encoding->decode(gettext('Error')),
460 $message
462 return;
467 return 1;
470 sub run {
471 my $self = shift;
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.");
479 Gtk3->main;
482 sub device_has_persistent_volume {
483 my $self = shift;
484 my $device = shift;
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 {
492 my $self = shift;
493 my $device = shift;
495 $self->size_of_free_space >= $self->persistence_minimum_size;
498 sub persistence_partition {
499 my $self = shift;
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 {
509 my $self = shift;
510 my $opts = shift;
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;
519 my $options = {};
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)
529 ->set_notify(sub {
530 $self->create_persistent_encrypted_filesystem($opts, @_);
533 $self->debug("waiting...");
536 sub create_persistent_encrypted_filesystem {
537 my $self = shift;
538 my $opts = shift;
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.
548 local $@;
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;
559 my $fsoptions = {
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')
566 ->Format(
567 dbus_call_async, dbus_call_timeout, 3600 * 1000,
568 $fstype, $fsoptions)
569 ->set_notify(sub { $opts->{end_cb}->({
570 created_device => $created_device,
571 format_reply => @_,
572 })});
574 $self->debug("waiting...");
577 sub delete_persistence_partition {
578 my $self = shift;
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);
588 if ($luksholder) {
589 if ($self->persistence_filesystem_is_mounted) {
590 $self->udisks_service
591 ->get_object($luksholder)
592 ->as_interface("org.freedesktop.UDisks2.Filesystem")
593 ->Unmount({})
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 {
606 my $self = shift;
607 my $opts = shift;
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 {
620 my $self = shift;
622 my $child = $self->main_window->get_child;
623 $self->main_window->remove($child) if defined($child);
626 sub run_current_step {
627 my $self = shift;
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());
643 else {
644 $self->main_window->resize($width, $height);
648 sub goto_next_step {
649 my $self = shift;
650 (my $opts = shift) ||= {};
652 my $next_step;
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;
659 else {
660 # check_sanity already has displayed an error dialog,
661 # that the user already closed.
662 exit 2;
665 else {
666 $self->debug("No more steps.");
667 $self->current_step->title->set_text($self->encoding->decode(gettext(
668 q{Persistence wizard - Finished}
669 )));
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.}
674 )));
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 {
682 my $self = shift;
683 my $name = shift;
685 my $class_name = step_name_to_class_name($name);
687 my %init_args;
689 if ($name eq 'bootstrap') {
690 %init_args = (
691 go_callback => sub {
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') {
703 %init_args = (
704 go_callback => sub {
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') {
713 %init_args = (
714 go_callback => sub {
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(
725 name => $name,
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,
730 %init_args
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;
746 return 1;
749 method persistence_filesystem_is_readable () {
750 return unless my $mountpoint = $self->persistence_partition_mountpoint;
751 my $ret;
753 use filetest 'access'; # take ACLs into account
754 $ret = -r $self->persistence_partition_mountpoint;
756 return $ret;
759 method persistence_filesystem_is_writable () {
760 return unless my $mountpoint = $self->persistence_partition_mountpoint;
761 my $ret;
763 use filetest 'access'; # take ACLs into account
764 $ret = -w $self->persistence_partition_mountpoint;
766 return $ret;
769 no Moose;