linux-image-manager 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {linux_image_manager-2.2.0/linux_image_manager.egg-info → linux_image_manager-2.3.0}/PKG-INFO +1 -1
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/catalog.py +4 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/cli.py +24 -22
- linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_bottom +19 -0
- linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_hook +37 -0
- linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_premount +55 -0
- linux_image_manager-2.3.0/lim/configuration/initramfs-tools/torrc +16 -0
- linux_image_manager-2.3.0/lim/configuration/packages/server/luks-debian.txt +5 -0
- linux_image_manager-2.3.0/lim/configuration/packages/server/tor-debian.txt +3 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/data/sync.py +4 -13
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/device.py +1 -3
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/distributions.yml +13 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/fsutil.py +17 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/choosers.py +34 -19
- linux_image_manager-2.3.0/lim/image/crossarch.py +71 -0
- linux_image_manager-2.3.0/lim/image/encryption.py +42 -0
- linux_image_manager-2.3.0/lim/image/initramfs/__init__.py +21 -0
- linux_image_manager-2.3.0/lim/image/initramfs/base.py +42 -0
- linux_image_manager-2.3.0/lim/image/initramfs/initramfs_tools.py +117 -0
- linux_image_manager-2.3.0/lim/image/initramfs/keygen.py +54 -0
- linux_image_manager-2.3.0/lim/image/initramfs/mkinitcpio.py +122 -0
- linux_image_manager-2.3.0/lim/image/loopimg.py +28 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/plan.py +6 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/raspberry.py +3 -7
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/session.py +35 -19
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/setup.py +2 -5
- linux_image_manager-2.3.0/lim/image/transfer.py +236 -0
- linux_image_manager-2.3.0/lim/image/unlock.py +100 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/verify.py +2 -4
- linux_image_manager-2.3.0/lim/image/wizard.py +206 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/luks.py +3 -9
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/runner.py +2 -4
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/storage/raid1.py +1 -3
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/storage/single_drive.py +2 -6
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0/linux_image_manager.egg-info}/PKG-INFO +1 -1
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/SOURCES.txt +15 -1
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/pyproject.toml +2 -1
- linux_image_manager-2.2.0/lim/image/encryption.py +0 -129
- linux_image_manager-2.2.0/lim/image/tor.py +0 -79
- linux_image_manager-2.2.0/lim/image/transfer.py +0 -140
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/LICENSE.txt +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/README.md +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/__init__.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/config.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/configuration/initcpio/tor_hook +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/configuration/initcpio/tor_install +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/configuration/initcpio/torrc +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/configuration/packages/general.txt +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/configuration/packages/server/luks.txt +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/configuration/packages/server/tor.txt +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/data/__init__.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/data/crypt.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/errors.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/__init__.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/backup.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/image/chroot.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/packages.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/storage/__init__.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/storage/common.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/system.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/lim/ui.py +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/dependency_links.txt +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/entry_points.txt +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/requires.txt +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/top_level.txt +0 -0
- {linux_image_manager-2.2.0 → linux_image_manager-2.3.0}/setup.cfg +0 -0
{linux_image_manager-2.2.0/linux_image_manager.egg-info → linux_image_manager-2.3.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linux-image-manager
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: CLI (`lim`) for downloading, verifying and flashing Linux images, LUKS/Btrfs encrypted storage (single drive and RAID1), image backups and chroot maintenance
|
|
5
5
|
Author-email: Kevin Veen-Birkenbach <kevin@veen.world>
|
|
6
6
|
Maintainer-email: Kevin Veen-Birkenbach <kevin@veen.world>
|
|
@@ -27,5 +27,9 @@ def retropie_images() -> dict[str, dict[str, str]]:
|
|
|
27
27
|
return _load()["retropie_images"]
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
def raspios_images() -> dict[str, dict[str, str]]:
|
|
31
|
+
return _load()["raspios_images"]
|
|
32
|
+
|
|
33
|
+
|
|
30
34
|
def mkinitcpio_modules_by_rpi() -> dict[str, str]:
|
|
31
35
|
return _load()["mkinitcpio_modules_by_rpi"]
|
|
@@ -10,6 +10,8 @@ from lim.data import crypt, sync
|
|
|
10
10
|
from lim.errors import LimError
|
|
11
11
|
from lim.image import backup, chroot
|
|
12
12
|
from lim.image import setup as image_setup
|
|
13
|
+
from lim.image import unlock as image_unlock
|
|
14
|
+
from lim.image import wizard as image_wizard
|
|
13
15
|
from lim.storage import raid1, single_drive
|
|
14
16
|
|
|
15
17
|
|
|
@@ -29,6 +31,20 @@ COMMANDS: dict[str, Command] = {
|
|
|
29
31
|
" - Configures boot and root partitions.",
|
|
30
32
|
needs_root=True,
|
|
31
33
|
),
|
|
34
|
+
"guided": Command(
|
|
35
|
+
image_wizard.run_guided,
|
|
36
|
+
"Guided Encrypted Linux Image Setup (default command):\n"
|
|
37
|
+
" - Walks through flashing a Linux image (Arch, Raspberry Pi OS, ...) onto a device.\n"
|
|
38
|
+
" - Encrypts the root with LUKS and enables remote unlock over Tor.",
|
|
39
|
+
needs_root=True,
|
|
40
|
+
),
|
|
41
|
+
"remote-unlock": Command(
|
|
42
|
+
image_unlock.remote_unlock,
|
|
43
|
+
"Remote LUKS Boot Unlock:\n"
|
|
44
|
+
" - Reaches a waiting initramfs over Tor (onion) or plain SSH.\n"
|
|
45
|
+
" - Runs cryptroot-unlock or presents the passphrase prompt.",
|
|
46
|
+
needs_root=False,
|
|
47
|
+
),
|
|
32
48
|
"single": Command(
|
|
33
49
|
single_drive.setup,
|
|
34
50
|
"Single Drive Encryption Setup:\n"
|
|
@@ -59,14 +75,12 @@ COMMANDS: dict[str, Command] = {
|
|
|
59
75
|
),
|
|
60
76
|
"mount": Command(
|
|
61
77
|
single_drive.mount,
|
|
62
|
-
"Mount Encrypted Storage:\n"
|
|
63
|
-
" - Unlocks a LUKS partition and mounts it.",
|
|
78
|
+
"Mount Encrypted Storage:\n - Unlocks a LUKS partition and mounts it.",
|
|
64
79
|
needs_root=True,
|
|
65
80
|
),
|
|
66
81
|
"umount": Command(
|
|
67
82
|
single_drive.umount,
|
|
68
|
-
"Unmount Encrypted Storage:\n"
|
|
69
|
-
" - Unmounts a LUKS partition and closes the mapper.",
|
|
83
|
+
"Unmount Encrypted Storage:\n - Unmounts a LUKS partition and closes the mapper.",
|
|
70
84
|
needs_root=True,
|
|
71
85
|
),
|
|
72
86
|
"single-boot": Command(
|
|
@@ -83,26 +97,22 @@ COMMANDS: dict[str, Command] = {
|
|
|
83
97
|
),
|
|
84
98
|
"unlock": Command(
|
|
85
99
|
crypt.unlock,
|
|
86
|
-
"Unlock Data:\n"
|
|
87
|
-
" - Decrypts the encfs data store into the decrypted folder.",
|
|
100
|
+
"Unlock Data:\n - Decrypts the encfs data store into the decrypted folder.",
|
|
88
101
|
needs_root=False,
|
|
89
102
|
),
|
|
90
103
|
"lock": Command(
|
|
91
104
|
crypt.lock,
|
|
92
|
-
"Lock Data:\n"
|
|
93
|
-
" - Unmounts the decrypted encfs folder.",
|
|
105
|
+
"Lock Data:\n - Unmounts the decrypted encfs folder.",
|
|
94
106
|
needs_root=False,
|
|
95
107
|
),
|
|
96
108
|
"import": Command(
|
|
97
109
|
sync.import_from_system,
|
|
98
|
-
"Import Data:\n"
|
|
99
|
-
" - Copies personal data from the system into the encrypted store.",
|
|
110
|
+
"Import Data:\n - Copies personal data from the system into the encrypted store.",
|
|
100
111
|
needs_root=False,
|
|
101
112
|
),
|
|
102
113
|
"export": Command(
|
|
103
114
|
sync.export_to_system,
|
|
104
|
-
"Export Data:\n"
|
|
105
|
-
" - Copies personal data from the encrypted store back to the system.",
|
|
115
|
+
"Export Data:\n - Copies personal data from the encrypted store back to the system.",
|
|
106
116
|
needs_root=False,
|
|
107
117
|
),
|
|
108
118
|
}
|
|
@@ -122,21 +132,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
122
132
|
)
|
|
123
133
|
parser.add_argument(
|
|
124
134
|
"--type",
|
|
125
|
-
|
|
135
|
+
default="guided",
|
|
126
136
|
choices=list(COMMANDS.keys()),
|
|
127
|
-
help="
|
|
137
|
+
help="Command to execute (default: guided).",
|
|
128
138
|
)
|
|
129
139
|
parser.add_argument(
|
|
130
140
|
"--auto-confirm",
|
|
131
141
|
action="store_true",
|
|
132
142
|
help="Automatically confirm execution without prompting the user.",
|
|
133
143
|
)
|
|
134
|
-
parser.add_argument(
|
|
135
|
-
"--extra",
|
|
136
|
-
nargs=argparse.REMAINDER,
|
|
137
|
-
default=[],
|
|
138
|
-
help="Deprecated, ignored. Former extra parameters for the shell scripts.",
|
|
139
|
-
)
|
|
140
144
|
return parser
|
|
141
145
|
|
|
142
146
|
|
|
@@ -144,8 +148,6 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
144
148
|
args = build_parser().parse_args(argv)
|
|
145
149
|
command = COMMANDS[args.type]
|
|
146
150
|
|
|
147
|
-
if args.extra:
|
|
148
|
-
ui.warning("--extra is deprecated and ignored.")
|
|
149
151
|
if command.needs_root:
|
|
150
152
|
system.ensure_root()
|
|
151
153
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# initramfs-tools cleanup hook: stop Tor before the pivot to the real root.
|
|
3
|
+
# Installed to /etc/initramfs-tools/scripts/init-bottom/tor by lim.
|
|
4
|
+
# Nothing from the initramfs may keep running once run-init replaces it.
|
|
5
|
+
PREREQ=""
|
|
6
|
+
prereqs() { echo "$PREREQ"; }
|
|
7
|
+
case "$1" in
|
|
8
|
+
prereqs) prereqs; exit 0 ;;
|
|
9
|
+
esac
|
|
10
|
+
|
|
11
|
+
# pidof/killall may be absent; scan /proc for the tor binary (no extra tools).
|
|
12
|
+
for pid in $(ls /proc 2>/dev/null); do
|
|
13
|
+
case "$pid" in
|
|
14
|
+
*[!0-9]*) continue ;;
|
|
15
|
+
esac
|
|
16
|
+
[ "$(readlink "/proc/$pid/exe" 2>/dev/null)" = "/usr/bin/tor" ] \
|
|
17
|
+
&& kill "$pid" 2>/dev/null
|
|
18
|
+
done
|
|
19
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# initramfs-tools build hook: bake the Tor onion service into the initramfs.
|
|
3
|
+
# Installed to /etc/initramfs-tools/hooks/tor by linux-image-manager.
|
|
4
|
+
PREREQ="dropbear"
|
|
5
|
+
prereqs() { echo "$PREREQ"; }
|
|
6
|
+
case "$1" in
|
|
7
|
+
prereqs) prereqs; exit 0 ;;
|
|
8
|
+
esac
|
|
9
|
+
|
|
10
|
+
. /usr/share/initramfs-tools/hook-functions
|
|
11
|
+
|
|
12
|
+
copy_exec /usr/bin/tor /usr/bin
|
|
13
|
+
|
|
14
|
+
# glibc resolves the NTP server hostname via getaddrinfo(), which dlopen()s
|
|
15
|
+
# these NSS modules at runtime; without them DNS silently fails and the clock
|
|
16
|
+
# (RTC-less boards) stays at 1970, so Tor rejects the consensus.
|
|
17
|
+
for nss in /usr/lib/*/libnss_dns.so.2 /usr/lib/*/libnss_files.so.2 \
|
|
18
|
+
/lib/*/libnss_dns.so.2 /lib/*/libnss_files.so.2; do
|
|
19
|
+
[ -e "$nss" ] && copy_exec "$nss"
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
# Full busybox for the ntpd applet (the initramfs busybox may lack it); a
|
|
23
|
+
# distinct path keeps the initramfs's own busybox untouched.
|
|
24
|
+
for bb in /bin/busybox /usr/bin/busybox; do
|
|
25
|
+
[ -x "$bb" ] && { copy_exec "$bb" /usr/local/bin/busybox; break; }
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
date -u +%s > "${DESTDIR}/etc/tor-clock-floor"
|
|
29
|
+
|
|
30
|
+
# Onion keys + torrc, staged on the system by lim.
|
|
31
|
+
mkdir -p "${DESTDIR}/etc/tor/onion"
|
|
32
|
+
for key in hostname hs_ed25519_public_key hs_ed25519_secret_key; do
|
|
33
|
+
cp -a "/etc/tor/initramfs-onion/${key}" "${DESTDIR}/etc/tor/onion/${key}"
|
|
34
|
+
done
|
|
35
|
+
chmod 0700 "${DESTDIR}/etc/tor/onion"
|
|
36
|
+
chmod 0600 "${DESTDIR}/etc/tor/onion/hs_ed25519_secret_key"
|
|
37
|
+
cp -a /etc/tor/initramfs-torrc "${DESTDIR}/etc/tor/torrc"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# initramfs-tools runtime hook: bring up networking, sync the clock and start
|
|
3
|
+
# the Tor onion service so the dropbear unlock shell is reachable while
|
|
4
|
+
# cryptsetup waits. Installed to /etc/initramfs-tools/scripts/init-premount/tor.
|
|
5
|
+
PREREQ=""
|
|
6
|
+
prereqs() { echo "$PREREQ"; }
|
|
7
|
+
case "$1" in
|
|
8
|
+
prereqs) prereqs; exit 0 ;;
|
|
9
|
+
esac
|
|
10
|
+
|
|
11
|
+
. /scripts/functions
|
|
12
|
+
|
|
13
|
+
log_begin_msg "tor: bringing up networking and starting the onion service"
|
|
14
|
+
|
|
15
|
+
# init-premount runs BEFORE the cryptroot/dropbear networking, so Tor would
|
|
16
|
+
# start with no network and never publish the onion. Bring the interface up
|
|
17
|
+
# ourselves from the ip= cmdline (idempotent; the later dropbear setup reuses
|
|
18
|
+
# the lease in /run/net-*.conf).
|
|
19
|
+
configure_networking
|
|
20
|
+
|
|
21
|
+
# initramfs-tools does not export arbitrary cmdline params as shell vars.
|
|
22
|
+
tor_ntp=$(sed -n 's/.*\btor_ntp=\([^ ]*\).*/\1/p' /proc/cmdline)
|
|
23
|
+
|
|
24
|
+
# Best-effort DNS from the DHCP lease; extract ONLY the DNS fields with sed,
|
|
25
|
+
# never source the files (attacker-controllable DHCP option strings would run
|
|
26
|
+
# as root pre-boot).
|
|
27
|
+
if [ ! -s /etc/resolv.conf ]; then
|
|
28
|
+
for conf in /run/net-*.conf /tmp/net-*.conf; do
|
|
29
|
+
[ -f "$conf" ] || continue
|
|
30
|
+
for dns in $(sed -n 's/^IPV4DNS[01]=//p' "$conf"); do
|
|
31
|
+
[ -n "$dns" ] && [ "$dns" != "0.0.0.0" ] \
|
|
32
|
+
&& echo "nameserver $dns" >> /etc/resolv.conf
|
|
33
|
+
done
|
|
34
|
+
done
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
floor=$(cat /etc/tor-clock-floor 2>/dev/null || echo 0)
|
|
38
|
+
if [ "$(date +%s)" -lt "$floor" ]; then
|
|
39
|
+
/usr/local/bin/busybox date -s "@$floor" >/dev/null 2>&1 \
|
|
40
|
+
|| date -s "@$floor" >/dev/null 2>&1 || true
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [ -x /usr/local/bin/busybox ]; then
|
|
44
|
+
/usr/local/bin/busybox timeout 30 \
|
|
45
|
+
/usr/local/bin/busybox ntpd -n -q -p "${tor_ntp:-pool.ntp.org}" \
|
|
46
|
+
|| log_warning_msg "tor: NTP sync failed, keeping current clock"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
mkdir -p /var/lib/tor
|
|
50
|
+
chmod 0700 /var/lib/tor /etc/tor/onion
|
|
51
|
+
chmod 0600 /etc/tor/onion/hs_ed25519_secret_key
|
|
52
|
+
tor -f /etc/tor/torrc --RunAsDaemon 1 --Log "notice file /run/tor.log" \
|
|
53
|
+
|| log_warning_msg "tor: failed to start, unlock stays reachable via direct IP"
|
|
54
|
+
|
|
55
|
+
log_end_msg
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Tor configuration for the initramfs onion unlock service (initramfs-tools).
|
|
2
|
+
# Baked into the initramfs as /etc/tor/torrc by the "tor" hook; the onion keys
|
|
3
|
+
# come from /etc/tor/initramfs-onion on the system.
|
|
4
|
+
DataDirectory /var/lib/tor
|
|
5
|
+
HiddenServiceDir /etc/tor/onion
|
|
6
|
+
HiddenServicePort 22 127.0.0.1:22
|
|
7
|
+
SocksPort 0
|
|
8
|
+
# Single-hop, non-anonymous onion service. A boot-unlock onion needs
|
|
9
|
+
# reachability (behind NAT / a dynamic IP), NOT server-location anonymity, so
|
|
10
|
+
# it can skip the multi-hop, vanguards-restricted circuits that a fresh Tor
|
|
11
|
+
# with a limited relay view fails to build ("Giving up on launching a
|
|
12
|
+
# rendezvous circuit"). Single-hop builds trivial 1-hop circuits that publish
|
|
13
|
+
# and rendezvous reliably. Entry guards are likewise unnecessary here.
|
|
14
|
+
HiddenServiceNonAnonymousMode 1
|
|
15
|
+
HiddenServiceSingleHopMode 1
|
|
16
|
+
UseEntryGuards 0
|
|
@@ -90,31 +90,22 @@ def execute_sync_plan(operations: list[SyncOperation]) -> None:
|
|
|
90
90
|
if not operation.is_directory and Path(operation.destination).is_file():
|
|
91
91
|
ui.info("The destination file already exists!")
|
|
92
92
|
ui.info("Difference:")
|
|
93
|
-
runner.run(
|
|
94
|
-
["diff", operation.destination, operation.source], check=False
|
|
95
|
-
)
|
|
93
|
+
runner.run(["diff", operation.destination, operation.source], check=False)
|
|
96
94
|
runner.run(rsync_command(operation))
|
|
97
95
|
|
|
98
96
|
|
|
99
97
|
def ensure_unlocked() -> None:
|
|
100
98
|
mounts = runner.output(["mount"], check=False)
|
|
101
99
|
if str(config.DECRYPTED_PATH) not in mounts:
|
|
102
|
-
ui.info(
|
|
103
|
-
f"The decrypted folder {config.DECRYPTED_PATH} is locked. "
|
|
104
|
-
"You need to unlock it!"
|
|
105
|
-
)
|
|
100
|
+
ui.info(f"The decrypted folder {config.DECRYPTED_PATH} is locked. You need to unlock it!")
|
|
106
101
|
crypt.unlock()
|
|
107
102
|
|
|
108
103
|
|
|
109
104
|
def import_from_system(mode: str = "import") -> None:
|
|
110
105
|
ensure_unlocked()
|
|
111
|
-
backup_folder = (
|
|
112
|
-
config.BACKUP_PATH / mode / time.strftime("%Y%m%d%H%M%S")
|
|
113
|
-
)
|
|
106
|
+
backup_folder = config.BACKUP_PATH / mode / time.strftime("%Y%m%d%H%M%S")
|
|
114
107
|
backup_folder.mkdir(parents=True, exist_ok=True)
|
|
115
|
-
operations = build_sync_plan(
|
|
116
|
-
mode, system.real_home(), config.DATA_PATH, backup_folder
|
|
117
|
-
)
|
|
108
|
+
operations = build_sync_plan(mode, system.real_home(), config.DATA_PATH, backup_folder)
|
|
118
109
|
execute_sync_plan(operations)
|
|
119
110
|
|
|
120
111
|
|
|
@@ -119,9 +119,7 @@ def overwrite_device(device: Device) -> None:
|
|
|
119
119
|
|
|
120
120
|
def blkid_value(path: str, tag: str) -> str:
|
|
121
121
|
"""Return a blkid tag value ("TYPE", "UUID", ...) or "" when unavailable."""
|
|
122
|
-
return runner.output(
|
|
123
|
-
["blkid", path, "-s", tag, "-o", "value"], sudo=True, check=False
|
|
124
|
-
)
|
|
122
|
+
return runner.output(["blkid", path, "-s", tag, "-o", "value"], sudo=True, check=False)
|
|
125
123
|
|
|
126
124
|
|
|
127
125
|
def is_mounted(path_fragment: str) -> bool:
|
|
@@ -57,6 +57,19 @@ retropie_images:
|
|
|
57
57
|
checksum: b5daa6e7660a99c246966f3f09b4014b
|
|
58
58
|
image: retropie-buster-4.8-rpi4_400.img.gz
|
|
59
59
|
|
|
60
|
+
# Raspberry Pi OS. The "_latest" endpoints 302-redirect to the current release;
|
|
61
|
+
# image is only the local filename (must end .img.xz for decompress_command).
|
|
62
|
+
raspios_images:
|
|
63
|
+
lite64:
|
|
64
|
+
source_url: https://downloads.raspberrypi.com/raspios_lite_arm64_latest
|
|
65
|
+
image: raspios_lite_arm64_latest.img.xz
|
|
66
|
+
desktop64:
|
|
67
|
+
source_url: https://downloads.raspberrypi.com/raspios_arm64_latest
|
|
68
|
+
image: raspios_arm64_latest.img.xz
|
|
69
|
+
lite32:
|
|
70
|
+
source_url: https://downloads.raspberrypi.com/raspios_lite_armhf_latest
|
|
71
|
+
image: raspios_lite_armhf_latest.img.xz
|
|
72
|
+
|
|
60
73
|
# Extra kernel modules the initramfs needs for early network access,
|
|
61
74
|
# see https://raspberrypi.stackexchange.com/questions/67051
|
|
62
75
|
mkinitcpio_modules_by_rpi:
|
|
@@ -16,6 +16,23 @@ def replace_in_file(search: str, replace: str, path: str | Path) -> None:
|
|
|
16
16
|
path.write_text(new_text)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def drop_fstab_mount(mount_point: str, path: str | Path) -> None:
|
|
20
|
+
"""Remove any (non-comment) fstab line whose mount point field equals mount_point.
|
|
21
|
+
|
|
22
|
+
Lets a fresh mapper/boot entry replace a stock image's existing one instead
|
|
23
|
+
of colliding with it. No-op when the file or a matching line is absent.
|
|
24
|
+
"""
|
|
25
|
+
path = Path(path)
|
|
26
|
+
if not path.exists():
|
|
27
|
+
return
|
|
28
|
+
kept = [
|
|
29
|
+
line
|
|
30
|
+
for line in path.read_text().splitlines()
|
|
31
|
+
if line.lstrip().startswith("#") or line.split()[1:2] != [mount_point]
|
|
32
|
+
]
|
|
33
|
+
path.write_text("".join(f"{line}\n" for line in kept))
|
|
34
|
+
|
|
35
|
+
|
|
19
36
|
def ensure_line_in_file(line: str, path: str | Path) -> bool:
|
|
20
37
|
"""Append ``line`` unless already present. Returns True when appended."""
|
|
21
38
|
path = Path(path)
|
|
@@ -5,8 +5,21 @@ from lim.errors import LimError
|
|
|
5
5
|
from lim.image.plan import ImagePlan
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def _choose(prompt: str, options: list[str]) -> str:
|
|
9
|
+
"""List options numbered; accept either a number or the name itself."""
|
|
10
|
+
ui.info(f"{prompt}:")
|
|
11
|
+
for index, name in enumerate(options, 1):
|
|
12
|
+
ui.info(f" {index}) {name}")
|
|
13
|
+
answer = ui.ask("Pick a number or name:").strip()
|
|
14
|
+
if answer in options: # name wins, so numeric names (e.g. arch "4") still work
|
|
15
|
+
return answer
|
|
16
|
+
if answer.isdigit() and 1 <= int(answer) <= len(options):
|
|
17
|
+
return options[int(answer) - 1]
|
|
18
|
+
return answer
|
|
19
|
+
|
|
20
|
+
|
|
8
21
|
def choose_arch(plan: ImagePlan) -> None:
|
|
9
|
-
version =
|
|
22
|
+
version = _choose("Which Raspberry Pi version", list(catalog.arch_rpi_images()))
|
|
10
23
|
entry = catalog.arch_rpi_images().get(version)
|
|
11
24
|
if entry is None:
|
|
12
25
|
raise LimError(f"Version {version} isn't supported.")
|
|
@@ -19,7 +32,7 @@ def choose_arch(plan: ImagePlan) -> None:
|
|
|
19
32
|
|
|
20
33
|
def choose_manjaro(plan: ImagePlan) -> None:
|
|
21
34
|
plan.boot_size = "+500M"
|
|
22
|
-
flavour =
|
|
35
|
+
flavour = _choose("Which Manjaro version", ["architect", "gnome"])
|
|
23
36
|
if flavour == "architect":
|
|
24
37
|
plan.image_checksum = "6b1c2fce12f244c1e32212767a9d3af2cf8263b2"
|
|
25
38
|
plan.base_download_url = (
|
|
@@ -28,7 +41,7 @@ def choose_manjaro(plan: ImagePlan) -> None:
|
|
|
28
41
|
)
|
|
29
42
|
plan.image_name = "manjaro-architect-20.0-200426-linux56.iso"
|
|
30
43
|
elif flavour == "gnome":
|
|
31
|
-
release =
|
|
44
|
+
release = _choose("Which Gnome release", list(catalog.manjaro_gnome_releases()))
|
|
32
45
|
entry = catalog.manjaro_gnome_releases().get(release)
|
|
33
46
|
if entry is None:
|
|
34
47
|
raise LimError(f"Gnome release {release} isn't supported.")
|
|
@@ -44,32 +57,37 @@ def choose_manjaro(plan: ImagePlan) -> None:
|
|
|
44
57
|
def choose_moode(plan: ImagePlan) -> None:
|
|
45
58
|
plan.boot_size = "+200M"
|
|
46
59
|
plan.image_checksum = "185cbc9a4994534bb7a4bc2744c78197"
|
|
47
|
-
plan.base_download_url =
|
|
48
|
-
"https://github.com/moode-player/moode/releases/download/r651prod/"
|
|
49
|
-
)
|
|
60
|
+
plan.base_download_url = "https://github.com/moode-player/moode/releases/download/r651prod/"
|
|
50
61
|
plan.image_name = "moode-r651-iso.zip"
|
|
51
62
|
|
|
52
63
|
|
|
53
64
|
def choose_retropie(plan: ImagePlan) -> None:
|
|
54
65
|
plan.boot_size = "+500M"
|
|
55
|
-
version =
|
|
66
|
+
version = _choose("Which RetroPie version", list(catalog.retropie_images()))
|
|
56
67
|
entry = catalog.retropie_images().get(version)
|
|
57
68
|
if entry is None:
|
|
58
69
|
raise LimError(f"Version {version} isn't supported.")
|
|
59
70
|
plan.raspberry_pi_version = version
|
|
60
|
-
plan.base_download_url =
|
|
61
|
-
"https://github.com/RetroPie/RetroPie-Setup/releases/download/4.8/"
|
|
62
|
-
)
|
|
71
|
+
plan.base_download_url = "https://github.com/RetroPie/RetroPie-Setup/releases/download/4.8/"
|
|
63
72
|
plan.image_checksum = entry["checksum"]
|
|
64
73
|
plan.image_name = entry["image"]
|
|
65
74
|
|
|
66
75
|
|
|
76
|
+
def choose_raspios(plan: ImagePlan) -> None:
|
|
77
|
+
plan.boot_size = "+512M"
|
|
78
|
+
plan.root_filesystem = "ext4"
|
|
79
|
+
variant = _choose("Which Raspberry Pi OS image", list(catalog.raspios_images()))
|
|
80
|
+
entry = catalog.raspios_images().get(variant)
|
|
81
|
+
if entry is None:
|
|
82
|
+
raise LimError(f"Raspberry Pi OS variant {variant} isn't supported.")
|
|
83
|
+
plan.source_url = entry["source_url"]
|
|
84
|
+
plan.image_name = entry["image"]
|
|
85
|
+
|
|
86
|
+
|
|
67
87
|
def choose_torbox(plan: ImagePlan) -> None:
|
|
68
88
|
plan.base_download_url = "https://www.torbox.ch/data/"
|
|
69
89
|
plan.image_name = "torbox-20220102-v050.gz"
|
|
70
|
-
plan.image_checksum =
|
|
71
|
-
"0E1BA7FFD14AAAE5F0462C8293D95B62C3BF1D9E726E26977BD04772C55680D3"
|
|
72
|
-
)
|
|
90
|
+
plan.image_checksum = "0E1BA7FFD14AAAE5F0462C8293D95B62C3BF1D9E726E26977BD04772C55680D3"
|
|
73
91
|
plan.boot_size = "+200M"
|
|
74
92
|
|
|
75
93
|
|
|
@@ -78,9 +96,7 @@ def choose_android_x86(plan: ImagePlan) -> None:
|
|
|
78
96
|
"https://www.fosshub.com/Android-x86.html?dwl=android-x86_64-9.0-r2.iso"
|
|
79
97
|
)
|
|
80
98
|
plan.image_name = "android-x86_64-9.0-r2.iso"
|
|
81
|
-
plan.image_checksum =
|
|
82
|
-
"f7eb8fc56f29ad5432335dc054183acf086c539f3990f0b6e9ff58bd6df4604e"
|
|
83
|
-
)
|
|
99
|
+
plan.image_checksum = "f7eb8fc56f29ad5432335dc054183acf086c539f3990f0b6e9ff58bd6df4604e"
|
|
84
100
|
plan.boot_size = "+500M"
|
|
85
101
|
|
|
86
102
|
|
|
@@ -91,13 +107,12 @@ DISTRIBUTION_CHOOSERS = {
|
|
|
91
107
|
"manjaro": choose_manjaro,
|
|
92
108
|
"moode": choose_moode,
|
|
93
109
|
"retropie": choose_retropie,
|
|
110
|
+
"raspios": choose_raspios,
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
|
|
97
114
|
def choose_linux_image(plan: ImagePlan) -> None:
|
|
98
|
-
distribution =
|
|
99
|
-
"Which distribution should be used [arch,moode,retropie,manjaro,torbox...]?"
|
|
100
|
-
)
|
|
115
|
+
distribution = _choose("Which distribution", list(DISTRIBUTION_CHOOSERS))
|
|
101
116
|
chooser = DISTRIBUTION_CHOOSERS.get(distribution)
|
|
102
117
|
if chooser is None:
|
|
103
118
|
raise LimError(f"Distribution {distribution} isn't supported.")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Ensure the host can chroot into a foreign-architecture image (qemu binfmt)."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from lim import runner, ui
|
|
8
|
+
from lim.errors import LimError
|
|
9
|
+
|
|
10
|
+
# Building natively needs no qemu; on these the chroot runs directly.
|
|
11
|
+
_NATIVE_ARM = ("aarch64", "armv7l", "armv6l", "arm64")
|
|
12
|
+
|
|
13
|
+
# Repo-based installs only; AUR helpers refuse to run from this root process.
|
|
14
|
+
_BINFMT_INSTALLERS = {
|
|
15
|
+
"apt-get": ["apt-get", "install", "-y", "qemu-user-static", "binfmt-support"],
|
|
16
|
+
"dnf": ["dnf", "install", "-y", "qemu-user-static"],
|
|
17
|
+
"zypper": ["zypper", "--non-interactive", "install", "qemu-linux-user"],
|
|
18
|
+
"pacman": ["pacman", "-S", "--needed", "--noconfirm", "qemu-user-static-binfmt"],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _entry_enabled(entry: Path) -> bool:
|
|
23
|
+
try:
|
|
24
|
+
return "enabled" in entry.read_text()
|
|
25
|
+
except OSError:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def qemu_binfmt_enabled() -> bool:
|
|
30
|
+
binfmt = Path("/proc/sys/fs/binfmt_misc")
|
|
31
|
+
return binfmt.is_dir() and any(_entry_enabled(e) for e in binfmt.glob("qemu-*"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def auto_enable() -> bool:
|
|
35
|
+
"""Install qemu-user-static via the host package manager and register binfmt."""
|
|
36
|
+
command = next((cmd for tool, cmd in _BINFMT_INSTALLERS.items() if shutil.which(tool)), None)
|
|
37
|
+
if command is None:
|
|
38
|
+
ui.warning("No supported host package manager found (apt-get/dnf/zypper/pacman).")
|
|
39
|
+
return False
|
|
40
|
+
runner.run(command, sudo=True, check=False)
|
|
41
|
+
runner.run(["systemctl", "restart", "systemd-binfmt"], sudo=True, check=False)
|
|
42
|
+
return qemu_binfmt_enabled()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ensure_ready() -> None:
|
|
46
|
+
"""Make foreign-arch chroot work, or fail with actionable guidance.
|
|
47
|
+
|
|
48
|
+
Called before the target is erased, so a host that cannot run the image's
|
|
49
|
+
binaries aborts early instead of mid-build with a cryptic "Exec format error".
|
|
50
|
+
"""
|
|
51
|
+
if platform.machine() in _NATIVE_ARM:
|
|
52
|
+
return
|
|
53
|
+
if qemu_binfmt_enabled():
|
|
54
|
+
ui.info("Cross-architecture build: qemu binfmt handlers are registered.")
|
|
55
|
+
return
|
|
56
|
+
ui.warning(
|
|
57
|
+
f"Cross-architecture build: this host ({platform.machine()}) cannot run the "
|
|
58
|
+
"image's ARM binaries in chroot yet."
|
|
59
|
+
)
|
|
60
|
+
if ui.confirm("Install qemu-user-static and register binfmt now?"):
|
|
61
|
+
if auto_enable():
|
|
62
|
+
ui.success("qemu binfmt handlers registered.")
|
|
63
|
+
return
|
|
64
|
+
ui.warning("Automatic setup did not register a handler.")
|
|
65
|
+
raise LimError(
|
|
66
|
+
"Cross-architecture chroot is not available. Install qemu-user-static + binfmt "
|
|
67
|
+
"manually, or build on the target itself (e.g. the Pi booted from USB).\n"
|
|
68
|
+
" Arch/Manjaro : qemu-user-static + qemu-user-static-binfmt (AUR), "
|
|
69
|
+
"then: sudo systemctl restart systemd-binfmt\n"
|
|
70
|
+
" Debian/Ubuntu: sudo apt install qemu-user-static binfmt-support"
|
|
71
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Remote-unlockable LUKS boot configuration.
|
|
2
|
+
|
|
3
|
+
Distro-agnostic orchestration; the init-system-specific steps live in the
|
|
4
|
+
initramfs backends (mkinitcpio for Arch/Manjaro, initramfs-tools for
|
|
5
|
+
Debian/Raspberry Pi OS). See lim/image/initramfs/.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from lim import packages, ui
|
|
11
|
+
from lim.image.initramfs import get_backend
|
|
12
|
+
from lim.image.plan import ImagePlan
|
|
13
|
+
from lim.image.session import ImageSession, install_packages
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def configure_encryption(
|
|
17
|
+
plan: ImagePlan, session: ImageSession, authorized_keys: Path
|
|
18
|
+
) -> str | None:
|
|
19
|
+
"""Configure the remote-unlock LUKS stack; return the onion address, if any."""
|
|
20
|
+
root = session.root_mount_path
|
|
21
|
+
backend = get_backend(plan.distribution)
|
|
22
|
+
ui.info("Setup encryption...")
|
|
23
|
+
ui.info("Installing neccessary software...")
|
|
24
|
+
install_packages(
|
|
25
|
+
plan.distribution,
|
|
26
|
+
root,
|
|
27
|
+
" ".join(packages.get_packages(backend.luks_package_collection())),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
backend.install_authorized_key(root, authorized_keys)
|
|
31
|
+
|
|
32
|
+
onion_address = None
|
|
33
|
+
if plan.tor_unlock:
|
|
34
|
+
# Hook files and onion keys must exist before the initramfs is baked.
|
|
35
|
+
onion_address = backend.install_tor_unlock(plan, root)
|
|
36
|
+
|
|
37
|
+
# crypttab must be written before the initramfs is (re)generated so the
|
|
38
|
+
# Debian cryptsetup hook can bake the mapping in.
|
|
39
|
+
backend.register_encrypted_root(plan, session, root)
|
|
40
|
+
backend.configure_initramfs(plan, root)
|
|
41
|
+
backend.configure_bootloader(plan, session, root)
|
|
42
|
+
return onion_address
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Distro-specific initramfs backends for encrypted remote unlock."""
|
|
2
|
+
|
|
3
|
+
from lim.errors import LimError
|
|
4
|
+
from lim.image.initramfs.base import InitramfsBackend
|
|
5
|
+
from lim.image.initramfs.initramfs_tools import InitramfsToolsBackend
|
|
6
|
+
from lim.image.initramfs.mkinitcpio import MkinitcpioBackend
|
|
7
|
+
|
|
8
|
+
_MKINITCPIO = frozenset({"arch", "manjaro"})
|
|
9
|
+
_INITRAMFS_TOOLS = frozenset({"raspios", "moode", "retropie"})
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_backend(distribution: str | None) -> InitramfsBackend:
|
|
13
|
+
"""Pick the initramfs backend for a distribution's init system."""
|
|
14
|
+
if distribution in _MKINITCPIO:
|
|
15
|
+
return MkinitcpioBackend()
|
|
16
|
+
if distribution in _INITRAMFS_TOOLS:
|
|
17
|
+
return InitramfsToolsBackend()
|
|
18
|
+
raise LimError(f"No initramfs backend for distribution {distribution!r}.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["InitramfsBackend", "get_backend"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The initramfs backend interface.
|
|
2
|
+
|
|
3
|
+
Encrypted remote-unlock images differ by init system: Arch/Manjaro use
|
|
4
|
+
mkinitcpio, Debian/Raspberry Pi OS use initramfs-tools. Each backend owns the
|
|
5
|
+
distro-specific bits (package set, crypttab syntax, initramfs generation,
|
|
6
|
+
bootloader cmdline, and how the Tor hook is baked in); the encryption flow
|
|
7
|
+
orchestrates them the same way for every distro.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from lim.image.plan import ImagePlan
|
|
14
|
+
from lim.image.session import ImageSession
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InitramfsBackend(ABC):
|
|
18
|
+
"""Distro-specific steps to build an encrypted, remote-unlockable image."""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def luks_package_collection(self) -> str:
|
|
22
|
+
"""Name of the package collection providing the LUKS/unlock stack."""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def install_authorized_key(self, root: Path, authorized_keys: Path) -> None:
|
|
26
|
+
"""Place the SSH public key where the initramfs dropbear reads it."""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def install_tor_unlock(self, plan: ImagePlan, root: Path) -> str:
|
|
30
|
+
"""Bake the Tor onion service into the initramfs; return the address."""
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def register_encrypted_root(self, plan: ImagePlan, session: ImageSession, root: Path) -> None:
|
|
34
|
+
"""Write fstab and crypttab in the distro's syntax."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def configure_initramfs(self, plan: ImagePlan, root: Path) -> None:
|
|
38
|
+
"""Configure the initramfs modules/hooks and (re)generate it."""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def configure_bootloader(self, plan: ImagePlan, session: ImageSession, root: Path) -> None:
|
|
42
|
+
"""Point the bootloader at the encrypted root with early networking."""
|