diff options
| -rw-r--r-- | Build.PL | 18 | ||||
| -rw-r--r-- | lib/AlgaOS/UpdateGrub.pm | 177 | ||||
| -rw-r--r-- | scripts/update-grub | 92 |
3 files changed, 287 insertions, 0 deletions
diff --git a/Build.PL b/Build.PL new file mode 100644 index 0000000..d2fb346 --- /dev/null +++ b/Build.PL @@ -0,0 +1,18 @@ +use v5.38.0; +use Module::Build; + +my $build = Module::Build->new( + module_name => 'AlgaOS::UpdateGrub', + license => 'perl', + requires => { + 'perl' => '5.40.0', + 'Crypt::URandom' => 0, + 'Moo' => 0, + 'PBKDF2::Tiny' => 0, + }, + script_files => [ + 'scripts/update-grub', + ], +); + +$build->create_build_script; diff --git a/lib/AlgaOS/UpdateGrub.pm b/lib/AlgaOS/UpdateGrub.pm new file mode 100644 index 0000000..32ca1ef --- /dev/null +++ b/lib/AlgaOS/UpdateGrub.pm @@ -0,0 +1,177 @@ +package AlgaOS::UpdateGrub; + +use v5.40.0; + +use strict; +use warnings; + +use Moo; +use Crypt::URandom qw/urandom/; +use PBKDF2::Tiny; + +# Boolean +has search_recovery => (is => 'ro'); +# Boolean +has search_root => (is => 'ro'); +# Boolean +has search_live_cd_rootfs => (is => 'ro'); +# Boolean +has wants_pass_in_sensitive_options => (is => 'lazy'); +# New pass if wanted will fail if not cached +has change_to_pass => (is => 'ro'); +# List of users +has user_list => (is => 'lazy'); +# Target storage device (/dev/sda for example) +has target_device => (is => 'lazy'); + +# What path to use as root +has root_dir => (is => 'lazy'); + +sub _build_target_device { + my $current_root_part = `findmnt -n -o SOURCE /`; + return '/dev/'.`lsblk -no PKNAME $current_root_part`; +} + +sub _build_root_dir { + return '/'; +} + +has _devices => (is => 'lazy'); + +sub _build__devices($self) { + my $target_device = $self->target_device; + my $devices = `lsblk -o PARTLABEL,PARTUUID $target_device`; + + my @devices = split /\n/, $devices; + shift @devices; + + @devices = grep { !/^\s*$/ } @devices; + + return { ( map { ( split /\s+/, $_ ) } @devices ) }; +} + + +sub _build_wants_pass_in_sensitive_options { + return 1; +} + +sub _build_user_list { + return [qw/admin/]; +} + +sub run($self) { + my $grub_dir = $self->root_dir.'/boot/grub'; + system qw{mkdir -pv}, $grub_dir; + open $fh, '>', "$grub_dir/grub.cfg"; + say $fh <<"EOF"; +set timeout=5 +set default=0 +EOF + + my $really_wants_pass = 0; + if ( $self->wants_pass_in_sensitive_options || !$self->search_live_cd_rootfs ) { + my $really_wants_pass = 1; + my $hash_complete = $self->_create_or_find_grub_hash; + say $fh <<"EOF"; +set superusers="admin" +password_pbkdf2 admin grub.pbkdf2.sha512.$hash_complete +EOF + } + my %devices = %{$self->_devices}; + if ($self->search_root) { + die "Live booting and root requested at once" if $self->search_live_cd_rootfs; + die "No AlgaOSRoot in that device" if !$devices{AlgaOSRoot}; + for my $kver ( glob($self->root_dir."/boot/kernel-*") ) { + die "No kernel found in /boot\n" unless $kver; + + $kver =~ s{.*/kernel-}{}; + my $security_string = $really_wants_pass ? '--unrestricted' : ''; + say $fh <<"EOF"; +menuentry "AlgaOS" $security_string { + linux /boot/kernel-$kver root=PARTUUID=$devices{AlgaOSRoot} splash quiet + initrd /boot/initramfs-$kver.img +}; +EOF + } + } + if ($self->search_recovery) { + die "Live booting and recovery requested at once" if $self->search_live_cd_rootfs; + die "No AlgaOSRecovery in that device" if !$devices{AlgaOSRecovery}; + my $recovery_title = $self->search_root ? 'AlgaOS Recovery' : 'Install AlgaOS now'; + if (system qw{mount /recovery}) { + die 'Unable to mount /recovery'; + } + my @rootfs = glob '/recovery/*rootfs*.squashfs'; + for my $rootfs (@rootfs) { + my $tmp_dir = '/tmp/rootfs-uncompression'; + system qw{rm -rf}, $tmp_dir; + system qw{mkdir -pv}, $tmp_dir; + system( + 'unsquashfs', + '-d', $dir, + $rootfs, + 'boot/kernel-*', + 'boot/initramfs-*', + ) == 0 or die "unsquashfs failed for $rootfs: $?"; + my ($kernel) = glob "$tmp_dir/kernel-*"; + my $kver = $kernel =~ s{.*/kernel-}{}r; + my $intramfs = "$tmp_dir/initramfs-$kver.img"; + if (system qw{cp}, $kernel, "/boot/recovery/kernel-$rootfs_ver") { + die 'Failed kernel copy'; + } + if (system qw{cp}, $initramfs, "/boot/recovery/initramfs-$rootfs_ver.img") { + die 'Failed initramfs copy'; + } + + my $rootfs_ver = $rootfs =~ s/\.squashfs$//r; + $rootfs_ver = $rootfs_ver =~ s{^.*\/}{}r; + my $security_string = $really_wants_pass ? '--users '.(join ',', @{$self->user_list}) : ''; + say $fh <<"EOF"; +menuentry "AlgaOS Recovery" $security_string { + linux /boot/recovery/kernel-$rootfs_ver root=live:PARTUUID=$devices{AlgaOSRecovery} rd.live.dir=/ rd.live.squashimg=$rootfs_ver.squashfs rd.live.overlay.overlayfs=1 rd.live.debug=1 rd.systemd.show_status=1 rd.systemd.log_level=debug splash quiet + initrd /boot/recovery/initramfs-$rootfs_ver.img +}; +EOF + } + + if ($self->search_live_cd_rootfs) { + my $boot_dir = $self->root_dir . '/boot'; + my ($kernel) = glob "$boot_dir/kernel-*"; + my $kver = s/^.*kernel-//; +say $fh <<"EOF"; +menuentry "AlgaOS" { + linux /boot/kernel-$kver root=live:LABEL=ALGAOS rd.live.dir=/ rd.live.squashimg=rootfs.squashfs rd.live.overlay.overlayfs=1 rd.live.debug=1 rd.systemd.show_status=1 rd.systemd.log_level=debug quiet splash + initrd /boot/initramfs-$kver.img +}; +EOF + } + + } +} + +sub _create_or_find_grub_hash { + if ( !$self->change_to_pass ) { + open $fh, '<', '/grub_hash'; + local $/ = undef; + my $hash_complete = <$fh>; + close $fh; + return $hash_complete if $hash_complete; + } + my $salt = urandom(64); + my $salt_hex = unpack( 'H*', $salt ); + my $iterations = 1000; + my $password = $self->change_to_pass; + + die 'No pass sent, no cached one, send pass' if !$password; + + my $hash = + PBKDF2::Tiny::derive_hex( 'SHA-512', $password, $salt, $iterations, 64 ); + + $hash_complete = "$iterations.$salt_hex.$hash"; + + open my $fh, '>', '/grub_hash'; + print $fh $hash_complete; + close $fh; + return $hash_complete; +} +1; diff --git a/scripts/update-grub b/scripts/update-grub new file mode 100644 index 0000000..b26ad17 --- /dev/null +++ b/scripts/update-grub @@ -0,0 +1,92 @@ +#!/usr/bin/env perl +use v5.40.0; +use strict; +use warnings; + +use Getopt::Long; + +use AlgaOS::UpdateGrub; + +my $help; +my $recovery = 1; +my $root = 1; +my $live = 0; +my $protect = 1; +my $user_list = ['admin']; +my $target_device; +my $chroot; + +Getopt::Long::Configure( "bundling", "no_ignore_case" ); +GetOptions( + 'help|h' => \$help, + 'recovery!' => \$recovery, + 'root!' => \$root, + 'live' => \$live, + 'protect!' => \$protect, + 'new-pass' => \$new_pass, + 'user=s@' => $user_list, + 'target-device=s' => \$target_device, + 'chroot=s' => \$chroot, +); + +if ($help) { + show_help(); + exit 0; +} + +if ($live && !$chroot) { + say STDERR '--live without --chroot is non sense, do not continue if you do not know what you are doing'; + show_help(); + exit -1; +} + +if ($root && !$protect && $recovery) { + say STDERR 'Not having password in grub and having recovery and root makes your data very easily accesible without even using an external device, giving you 10 seconds to think about it, press ctrl+c to interrupt if unsure'; + sleep 10; +} + +my $probable_root = `findmnt -n -o SOURCE /`; +if (!$target_device && $probable_root =~ /loop/) { + say STDERR 'You are not in the destination install, you need --target-device, also if you do not know what you are doing do not continue'; + show_help(); + exit -1; +} + +AlgaOS::UpdateGrub->new( + search_recovery => $recovery, + search_root => $root, + search_live_cd_rootfs => $livecd, + wants_pass_in_sensitive_options => $protect, + user_list => $user_list, + ( + defined $new_pass + ? ( change_to_pass => $new_pass ) + : () + ), + ( + defined $chroot + ? (root_dir => $chroot) + : () + ), + ( + defined $target_device + ? ( target_device => $target_device ) + : () + ), +)->run; + +sub show_help { + say STDERR "IF YOU DO NOT KNOW WHAT YOU ARE DOING DO NOT DIRECTLY RUN THIS COMMAND AND ASK FOR HELP INSTEAD. + +update-grub [--help|-h] [--norecovery] [--noroot] [--user foo --user bar] [--live] [--noprotect] [--new-pass <new password for grub sensitive fields>] [--target-device <path to disk>] [--chroot <New root to put grub on>] + +--help: Shows this help +--norecovery: Disables creation of grub entry for recovery (Voids your warranty) +--noroot: Disables creation of grub entry for root (Voids your warranty) +--user: Can be specified multiple times to send the list of allowable users to log in grub, \"admin\" will always be among them. +--live: Creates a grub that tries to boot a live system. (Not recommended for final users, voids your warranty) +--new-pass: Changes your grub password, be sure to remember it. +--target-device: Selects the harddisk to use to search for AlgaOS, update-grub will make its best guess if not sent. (Example: /dev/sda) +--chroot: Writes the grub to other root. (Not covered by warranty, used for livecd) +"; +} |
