1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
|
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 /`;
die 'Programming error in cd handling'
if $current_root_part =~ /(?:loop|rootfs)/;
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 my $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 )
{
$really_wants_pass = 1;
my $hash_complete = $self->_create_or_find_grub_hash;
say $fh <<"EOF";
set superusers="@{[join ',', @{$self->user_list}]}"
@{[
join "\n\n", map { "password_pbkdf2 $_ grub.pbkdf2.sha512.$hash_complete" } @{$self->user_list}
]}
EOF
}
my %devices;
if ( $self->search_root ) {
%devices = %{ $self->_devices };
die "Live booting and root requested at once"
if $self->search_live_cd_rootfs;
die "No AlgaOSRoot in that device" if !$devices{AlgaOSRoot};
my $sort_kernel = sub($a, $b) {
my ($kernel_version_a) = $a =~ m{/kernel-(\d+(?:\.\d+)*)};
my ($kernel_version_b) = $b =~ m{/kernel-(\d+(?:\.\d+)*)};
my @version_a = split /\./, $kernel_version_a;
my @version_b = split /\./, $kernel_version_b;
my $component_count = @version_a > @version_b
? @version_a
: @version_b;
for my $component_index (0 .. $component_count - 1) {
my $comparison =
($version_a[$component_index] // 0)
<=>
($version_b[$component_index] // 0);
return -$comparison if $comparison;
}
return 0;
}
for my $kver ( sort { $sort_kernel->($a, $b) } 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 ) {
%devices = %{ $self->_devices };
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 ( $self->root_dir eq '/' && system qw{mount /recovery} ) {
die 'Unable to mount /recovery';
}
my @rootfs = glob $self->root_dir . '/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', $tmp_dir, $rootfs, 'boot/kernel-*',
'boot/initramfs-*', ) == 0
or die "unsquashfs failed for $rootfs: $?";
my ($kernel) = glob "$tmp_dir/boot/kernel-*";
my $kver = $kernel =~ s{^.*kernel-}{}r;
my $initramfs = "$tmp_dir/boot/initramfs-$kver.img";
my $rootfs_ver = $rootfs =~ s/\.squashfs$//r;
$rootfs_ver = $rootfs_ver =~ s/^.*\///r;
system qw{mkdir -pv}, $self->root_dir . '/boot/recovery/';
system "rm -rf " . $self->root_dir . '/boot/recovery/*';
if ( system qw{cp},
$kernel, $self->root_dir . "/boot/recovery/kernel-$rootfs_ver" )
{
die 'Failed kernel copy';
}
if ( system qw{cp},
$initramfs,
$self->root_dir . "/boot/recovery/initramfs-$rootfs_ver.img" )
{
die 'Failed initramfs copy';
}
$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 = $kernel =~ 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($self) {
if ( !$self->change_to_pass ) {
open my $fh, '<', $self->root_dir . '/grub_hash'
or die 'No grub hash and no pass sent';
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 );
my $hash_complete = "$iterations.$salt_hex.$hash";
open my $fh, '>', $self->root_dir . '/grub_hash';
print $fh $hash_complete;
close $fh;
return $hash_complete;
}
1;
|