linux-image-manager 2.1.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.
Files changed (65) hide show
  1. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/PKG-INFO +74 -3
  2. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/README.md +73 -2
  3. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/catalog.py +4 -0
  4. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/cli.py +24 -22
  5. linux_image_manager-2.3.0/lim/configuration/initcpio/tor_hook +47 -0
  6. linux_image_manager-2.3.0/lim/configuration/initcpio/tor_install +37 -0
  7. linux_image_manager-2.3.0/lim/configuration/initcpio/torrc +7 -0
  8. linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_bottom +19 -0
  9. linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_hook +37 -0
  10. linux_image_manager-2.3.0/lim/configuration/initramfs-tools/tor_premount +55 -0
  11. linux_image_manager-2.3.0/lim/configuration/initramfs-tools/torrc +16 -0
  12. linux_image_manager-2.3.0/lim/configuration/packages/server/luks-debian.txt +5 -0
  13. linux_image_manager-2.3.0/lim/configuration/packages/server/tor-debian.txt +3 -0
  14. linux_image_manager-2.3.0/lim/configuration/packages/server/tor.txt +3 -0
  15. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/data/sync.py +4 -13
  16. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/device.py +1 -3
  17. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/distributions.yml +13 -0
  18. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/fsutil.py +17 -0
  19. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/choosers.py +34 -19
  20. linux_image_manager-2.3.0/lim/image/crossarch.py +71 -0
  21. linux_image_manager-2.3.0/lim/image/encryption.py +42 -0
  22. linux_image_manager-2.3.0/lim/image/initramfs/__init__.py +21 -0
  23. linux_image_manager-2.3.0/lim/image/initramfs/base.py +42 -0
  24. linux_image_manager-2.3.0/lim/image/initramfs/initramfs_tools.py +117 -0
  25. linux_image_manager-2.3.0/lim/image/initramfs/keygen.py +54 -0
  26. linux_image_manager-2.3.0/lim/image/initramfs/mkinitcpio.py +122 -0
  27. linux_image_manager-2.3.0/lim/image/loopimg.py +28 -0
  28. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/plan.py +7 -0
  29. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/raspberry.py +3 -7
  30. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/session.py +35 -19
  31. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/setup.py +6 -5
  32. linux_image_manager-2.3.0/lim/image/transfer.py +236 -0
  33. linux_image_manager-2.3.0/lim/image/unlock.py +100 -0
  34. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/verify.py +2 -4
  35. linux_image_manager-2.3.0/lim/image/wizard.py +206 -0
  36. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/luks.py +3 -9
  37. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/runner.py +2 -4
  38. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/storage/raid1.py +1 -3
  39. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/storage/single_drive.py +2 -6
  40. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/PKG-INFO +74 -3
  41. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/SOURCES.txt +19 -0
  42. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/pyproject.toml +7 -2
  43. linux_image_manager-2.1.0/lim/image/encryption.py +0 -117
  44. linux_image_manager-2.1.0/lim/image/transfer.py +0 -140
  45. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/LICENSE.txt +0 -0
  46. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/__init__.py +0 -0
  47. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/config.py +0 -0
  48. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/configuration/packages/general.txt +0 -0
  49. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/configuration/packages/server/luks.txt +0 -0
  50. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/data/__init__.py +0 -0
  51. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/data/crypt.py +0 -0
  52. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/errors.py +0 -0
  53. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/__init__.py +0 -0
  54. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/backup.py +0 -0
  55. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/image/chroot.py +0 -0
  56. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/packages.py +0 -0
  57. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/storage/__init__.py +0 -0
  58. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/storage/common.py +0 -0
  59. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/system.py +0 -0
  60. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/lim/ui.py +0 -0
  61. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/dependency_links.txt +0 -0
  62. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/entry_points.txt +0 -0
  63. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/requires.txt +0 -0
  64. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/linux_image_manager.egg-info/top_level.txt +0 -0
  65. {linux_image_manager-2.1.0 → linux_image_manager-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linux-image-manager
3
- Version: 2.1.0
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>
@@ -28,6 +28,7 @@ Linux Image Manager (lim) is a Python tool for downloading, configuring, and man
28
28
 
29
29
  - **Image Download & Setup:** Automatically download, verify (checksum + GPG signature) and prepare Linux distributions.
30
30
  - **Encrypted Storage:** Configure LUKS encryption for secure image management.
31
+ - **Tor Onion Unlock:** Optionally bake a Tor onion service into the initramfs so the dropbear LUKS unlock shell stays reachable behind NAT or dynamic IPs.
31
32
  - **Virtual RAID1:** Easily set up virtual Btrfs RAID1 for data redundancy.
32
33
  - **Backup & Restore:** Create image backups from devices using dd.
33
34
  - **Chroot Environment:** Easily enter a chroot shell to maintain or modify Linux images.
@@ -44,7 +45,7 @@ package-manager install lim
44
45
 
45
46
  This command makes Linux Image Manager globally available as `lim` in your terminal. The `lim` alias points to the **main.py** entry point.
46
47
 
47
- There are no Python dependencies beyond the standard library. The commands call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
48
+ The only Python dependency is **PyYAML** (`>=6`), used to read the image catalog. The pip/wheel install pulls it in automatically; on the `package-manager`/symlink install path make sure it is present (`python-yaml` on Arch). The commands also call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
48
49
 
49
50
  ## Usage ⚙️
50
51
 
@@ -112,6 +113,8 @@ lim/
112
113
  distributions.yml # single point of truth for the image catalog
113
114
  configuration/ # package collections used during image setup
114
115
  tests/unit/ # unit tests (all external commands mocked)
116
+ tests/e2e/ # Tor onion unlock end-to-end tests (rootless + QEMU, opt-in)
117
+ tests/e2e/qemu/ # full virtualized build+boot+unlock harness
115
118
  tests/lint/ # architecture guards (e.g. max file length)
116
119
  ```
117
120
 
@@ -120,15 +123,83 @@ tests/lint/ # architecture guards (e.g. max file length)
120
123
  Customize your environment in the `lim/configuration/` folder:
121
124
  - **General Packages:** Contains common packages for all setup scripts.
122
125
  - **Server LUKS Packages:** Contains packages needed for setting up LUKS encryption on servers.
126
+ - **initcpio Hooks:** The mkinitcpio install/runtime hooks and torrc baked into images for the Tor onion unlock.
127
+
128
+ ## Remote LUKS Unlock via Tor 🧅
129
+
130
+ When you answer yes to *"Should the system be remotely unlockable via a Tor
131
+ onion service?"* during an encrypted `lim --type image` setup, the image gets:
132
+
133
+ - `tor` and `busybox` installed, plus a custom `tor` mkinitcpio hook ordered
134
+ between `netconf` and `dropbear`.
135
+ - A v3 onion service key generated **offline** inside the image chroot; the
136
+ resulting `.onion` address is printed at the end of the setup and stays
137
+ stable across reboots and IP changes.
138
+ - An NTP clock sync in early userspace (boards like the Raspberry Pi have no
139
+ RTC and would otherwise boot with a clock Tor rejects). Override the server
140
+ with the kernel parameter `tor_ntp=<host>`.
141
+
142
+ After booting the device, unlock it from any machine with a running Tor
143
+ client:
144
+
145
+ ```bash
146
+ torsocks ssh root@<onion-address>
147
+ ```
148
+
149
+ Typing the LUKS passphrase into that shell resumes the boot. The direct
150
+ `ssh root@<ip>` unlock keeps working as before; Tor is an additional path,
151
+ useful behind NAT/CGNAT where no port forwarding is possible.
152
+
153
+ **Security note:** the onion private key is stored in the initramfs on the
154
+ unencrypted boot partition. Anyone with physical access to the SD card can
155
+ read it and impersonate the onion service — treat the address as
156
+ non-secret. Access control remains the SSH public key
157
+ (`/etc/dropbear/root_key`), exactly as with the direct unlock.
123
158
 
124
159
  ## Development & Tests 🧪
125
160
 
126
- The test suite mocks all external commands, so it runs safely on any machine:
161
+ The unit suite mocks all external commands, so it runs safely on any machine:
127
162
 
128
163
  ```bash
129
164
  pytest
130
165
  ```
131
166
 
167
+ ### Tor unlock end-to-end test
168
+
169
+ `tests/e2e/test_tor_unlock_e2e.py` reproduces the operator-visible half of the
170
+ Tor decryption process **rootless** — no block devices, no `cryptsetup`, no
171
+ `chroot`. It generates the v3 onion keys offline (exactly as `lim.image.tor`
172
+ does inside the image), starts a real Tor onion service from a torrc mirroring
173
+ the baked-in one, then connects to the `.onion` address through Tor and
174
+ delivers a passphrase to a dropbear stand-in, asserting it arrives and the
175
+ endpoint "unlocks".
176
+
177
+ It needs the real `tor` binary and live Tor network access (onion bootstrap is
178
+ slow), so it is opt-in and skipped otherwise:
179
+
180
+ ```bash
181
+ LIM_E2E_TOR=1 pytest tests/e2e/test_tor_unlock_e2e.py -v
182
+ ```
183
+
184
+ The offline-only checks in that file (onion keygen, production-flag guard) run
185
+ without network. The physical flashing half (`dd`, `cryptsetup luksFormat`,
186
+ `mount`, `chroot`, `mkinitcpio`) requires root and a matching CPU/qemu setup and
187
+ is not covered by this rootless test.
188
+
189
+ ### Full virtualized unlock (QEMU)
190
+
191
+ For the *whole* process — build an encrypted image, boot it, run the real
192
+ initramfs Tor+dropbear chain, and unlock it — `tests/e2e/qemu/` drives a QEMU
193
+ virtual machine end to end and asserts the real system booted only after the
194
+ passphrase was delivered over Tor. It is opt-in (`LIM_E2E_QEMU=1`) and needs
195
+ QEMU, `arch-install-scripts`, `cryptsetup`, `tor` and `ncat`; the build stage
196
+ needs root (ideally inside a throwaway VM to keep the host rootless). A private,
197
+ offline Tor network via [chutney](https://gitlab.torproject.org/tpo/core/chutney)
198
+ is supported. See [tests/e2e/qemu/README.md](tests/e2e/qemu/README.md).
199
+
200
+ The harness's pure logic and drift guards run in the normal suite
201
+ (`test_qemu_harness_unit.py`) — no QEMU, root, or network needed.
202
+
132
203
  ## License 📜
133
204
 
134
205
  This project is licensed under the GNU General Public License Version 3. See the [LICENSE.txt](./LICENSE.txt) file for details.
@@ -10,6 +10,7 @@ Linux Image Manager (lim) is a Python tool for downloading, configuring, and man
10
10
 
11
11
  - **Image Download & Setup:** Automatically download, verify (checksum + GPG signature) and prepare Linux distributions.
12
12
  - **Encrypted Storage:** Configure LUKS encryption for secure image management.
13
+ - **Tor Onion Unlock:** Optionally bake a Tor onion service into the initramfs so the dropbear LUKS unlock shell stays reachable behind NAT or dynamic IPs.
13
14
  - **Virtual RAID1:** Easily set up virtual Btrfs RAID1 for data redundancy.
14
15
  - **Backup & Restore:** Create image backups from devices using dd.
15
16
  - **Chroot Environment:** Easily enter a chroot shell to maintain or modify Linux images.
@@ -26,7 +27,7 @@ package-manager install lim
26
27
 
27
28
  This command makes Linux Image Manager globally available as `lim` in your terminal. The `lim` alias points to the **main.py** entry point.
28
29
 
29
- There are no Python dependencies beyond the standard library. The commands call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
30
+ The only Python dependency is **PyYAML** (`>=6`), used to read the image catalog. The pip/wheel install pulls it in automatically; on the `package-manager`/symlink install path make sure it is present (`python-yaml` on Arch). The commands also call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
30
31
 
31
32
  ## Usage ⚙️
32
33
 
@@ -94,6 +95,8 @@ lim/
94
95
  distributions.yml # single point of truth for the image catalog
95
96
  configuration/ # package collections used during image setup
96
97
  tests/unit/ # unit tests (all external commands mocked)
98
+ tests/e2e/ # Tor onion unlock end-to-end tests (rootless + QEMU, opt-in)
99
+ tests/e2e/qemu/ # full virtualized build+boot+unlock harness
97
100
  tests/lint/ # architecture guards (e.g. max file length)
98
101
  ```
99
102
 
@@ -102,15 +105,83 @@ tests/lint/ # architecture guards (e.g. max file length)
102
105
  Customize your environment in the `lim/configuration/` folder:
103
106
  - **General Packages:** Contains common packages for all setup scripts.
104
107
  - **Server LUKS Packages:** Contains packages needed for setting up LUKS encryption on servers.
108
+ - **initcpio Hooks:** The mkinitcpio install/runtime hooks and torrc baked into images for the Tor onion unlock.
109
+
110
+ ## Remote LUKS Unlock via Tor 🧅
111
+
112
+ When you answer yes to *"Should the system be remotely unlockable via a Tor
113
+ onion service?"* during an encrypted `lim --type image` setup, the image gets:
114
+
115
+ - `tor` and `busybox` installed, plus a custom `tor` mkinitcpio hook ordered
116
+ between `netconf` and `dropbear`.
117
+ - A v3 onion service key generated **offline** inside the image chroot; the
118
+ resulting `.onion` address is printed at the end of the setup and stays
119
+ stable across reboots and IP changes.
120
+ - An NTP clock sync in early userspace (boards like the Raspberry Pi have no
121
+ RTC and would otherwise boot with a clock Tor rejects). Override the server
122
+ with the kernel parameter `tor_ntp=<host>`.
123
+
124
+ After booting the device, unlock it from any machine with a running Tor
125
+ client:
126
+
127
+ ```bash
128
+ torsocks ssh root@<onion-address>
129
+ ```
130
+
131
+ Typing the LUKS passphrase into that shell resumes the boot. The direct
132
+ `ssh root@<ip>` unlock keeps working as before; Tor is an additional path,
133
+ useful behind NAT/CGNAT where no port forwarding is possible.
134
+
135
+ **Security note:** the onion private key is stored in the initramfs on the
136
+ unencrypted boot partition. Anyone with physical access to the SD card can
137
+ read it and impersonate the onion service — treat the address as
138
+ non-secret. Access control remains the SSH public key
139
+ (`/etc/dropbear/root_key`), exactly as with the direct unlock.
105
140
 
106
141
  ## Development & Tests 🧪
107
142
 
108
- The test suite mocks all external commands, so it runs safely on any machine:
143
+ The unit suite mocks all external commands, so it runs safely on any machine:
109
144
 
110
145
  ```bash
111
146
  pytest
112
147
  ```
113
148
 
149
+ ### Tor unlock end-to-end test
150
+
151
+ `tests/e2e/test_tor_unlock_e2e.py` reproduces the operator-visible half of the
152
+ Tor decryption process **rootless** — no block devices, no `cryptsetup`, no
153
+ `chroot`. It generates the v3 onion keys offline (exactly as `lim.image.tor`
154
+ does inside the image), starts a real Tor onion service from a torrc mirroring
155
+ the baked-in one, then connects to the `.onion` address through Tor and
156
+ delivers a passphrase to a dropbear stand-in, asserting it arrives and the
157
+ endpoint "unlocks".
158
+
159
+ It needs the real `tor` binary and live Tor network access (onion bootstrap is
160
+ slow), so it is opt-in and skipped otherwise:
161
+
162
+ ```bash
163
+ LIM_E2E_TOR=1 pytest tests/e2e/test_tor_unlock_e2e.py -v
164
+ ```
165
+
166
+ The offline-only checks in that file (onion keygen, production-flag guard) run
167
+ without network. The physical flashing half (`dd`, `cryptsetup luksFormat`,
168
+ `mount`, `chroot`, `mkinitcpio`) requires root and a matching CPU/qemu setup and
169
+ is not covered by this rootless test.
170
+
171
+ ### Full virtualized unlock (QEMU)
172
+
173
+ For the *whole* process — build an encrypted image, boot it, run the real
174
+ initramfs Tor+dropbear chain, and unlock it — `tests/e2e/qemu/` drives a QEMU
175
+ virtual machine end to end and asserts the real system booted only after the
176
+ passphrase was delivered over Tor. It is opt-in (`LIM_E2E_QEMU=1`) and needs
177
+ QEMU, `arch-install-scripts`, `cryptsetup`, `tor` and `ncat`; the build stage
178
+ needs root (ideally inside a throwaway VM to keep the host rootless). A private,
179
+ offline Tor network via [chutney](https://gitlab.torproject.org/tpo/core/chutney)
180
+ is supported. See [tests/e2e/qemu/README.md](tests/e2e/qemu/README.md).
181
+
182
+ The harness's pure logic and drift guards run in the normal suite
183
+ (`test_qemu_harness_unit.py`) — no QEMU, root, or network needed.
184
+
114
185
  ## License 📜
115
186
 
116
187
  This project is licensed under the GNU General Public License Version 3. See the [LICENSE.txt](./LICENSE.txt) file for details.
@@ -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
- required=True,
135
+ default="guided",
126
136
  choices=list(COMMANDS.keys()),
127
- help="Select the command to execute.",
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,47 @@
1
+ #!/usr/bin/ash
2
+ # mkinitcpio runtime hook: start Tor so the dropbear unlock shell is
3
+ # reachable as an onion service while encryptssh waits for the passphrase.
4
+ # Installed to /etc/initcpio/hooks/tor by linux-image-manager.
5
+
6
+ # netconf's ipconfig drops its DHCP lease data (incl. DNS) into
7
+ # /tmp/net-*.conf; busybox's resolver only reads /etc/resolv.conf.
8
+ _tor_write_resolv_conf() {
9
+ [ -s /etc/resolv.conf ] && return 0
10
+ local conf dns
11
+ for conf in /tmp/net-*.conf; do
12
+ [ -f "$conf" ] || continue
13
+ # Extract ONLY the DNS fields with sed; never source these files —
14
+ # they hold attacker-controllable DHCP option strings (hostname,
15
+ # domain, rootpath), and sourcing would run them as root pre-boot.
16
+ for dns in $(sed -n 's/^IPV4DNS[01]=//p' "$conf"); do
17
+ [ -n "$dns" ] && [ "$dns" != "0.0.0.0" ] \
18
+ && echo "nameserver $dns" >> /etc/resolv.conf
19
+ done
20
+ done
21
+ [ -s /etc/resolv.conf ]
22
+ }
23
+
24
+ run_hook() {
25
+ # Tor rejects consensus documents when the clock is far off; boards
26
+ # without an RTC boot in 1970, so sync before starting Tor. Bounded:
27
+ # a failed sync must never block the boot.
28
+ msg "tor: syncing clock via NTP..."
29
+ # Best-effort DNS; a tor_ntp IP literal needs none, so never gate on it.
30
+ _tor_write_resolv_conf || msg "tor: no DNS from DHCP (fine if tor_ntp is an IP)"
31
+ /usr/local/bin/busybox timeout 30 \
32
+ /usr/local/bin/busybox ntpd -n -q -p "${tor_ntp:-pool.ntp.org}" \
33
+ || msg "tor: NTP sync failed, keeping current clock"
34
+
35
+ msg "tor: starting onion service for remote unlock..."
36
+ mkdir -p /var/lib/tor
37
+ chmod 0700 /var/lib/tor /etc/tor/onion
38
+ chmod 0600 /etc/tor/onion/hs_ed25519_secret_key
39
+ tor -f /etc/tor/torrc --RunAsDaemon 1 --Log "notice file /tmp/tor.log" \
40
+ || msg "tor: failed to start, unlock stays reachable via direct IP"
41
+ }
42
+
43
+ run_cleanuphook() {
44
+ # Nothing from the initramfs may keep running after the pivot.
45
+ /usr/local/bin/busybox killall tor 2>/dev/null
46
+ return 0
47
+ }
@@ -0,0 +1,37 @@
1
+ #!/bin/bash
2
+ # mkinitcpio install hook: Tor onion service for remote LUKS unlock.
3
+ # Installed to /etc/initcpio/install/tor by linux-image-manager.
4
+
5
+ build() {
6
+ add_binary /usr/bin/tor
7
+ # Full busybox for the ntpd applet: boards without an RTC (e.g. most
8
+ # Raspberry Pis) wake up in 1970, which Tor's consensus checks reject.
9
+ # A different target path keeps mkinitcpio's own busybox untouched.
10
+ add_binary /usr/bin/busybox /usr/local/bin/busybox
11
+ # glibc resolves the NTP server hostname via getaddrinfo(), which dlopen()s
12
+ # these NSS modules at runtime — add_binary only follows NEEDED libs, so
13
+ # without them DNS silently fails, ntpd never syncs, and the clock stays
14
+ # at 1970. glibc's built-in default (no nsswitch.conf) is "dns ... files".
15
+ add_binary /usr/lib/libnss_dns.so.2
16
+ add_binary /usr/lib/libnss_files.so.2
17
+ add_file /etc/tor/initramfs-torrc /etc/tor/torrc
18
+ add_file /etc/tor/initramfs-onion/hostname /etc/tor/onion/hostname
19
+ add_file /etc/tor/initramfs-onion/hs_ed25519_public_key /etc/tor/onion/hs_ed25519_public_key
20
+ add_file /etc/tor/initramfs-onion/hs_ed25519_secret_key /etc/tor/onion/hs_ed25519_secret_key
21
+ add_runscript
22
+ }
23
+
24
+ help() {
25
+ cat <<HELPEOF
26
+ Starts a Tor onion service in early userspace so the dropbear unlock
27
+ shell stays reachable under the .onion address baked into the image,
28
+ even behind NAT or a dynamic IP.
29
+
30
+ Place it between netconf and dropbear:
31
+ HOOKS=(... netconf tor dropbear encryptssh ...)
32
+
33
+ Optional kernel parameter:
34
+ tor_ntp=<server> NTP server used to set the clock before Tor starts
35
+ (default: pool.ntp.org)
36
+ HELPEOF
37
+ }
@@ -0,0 +1,7 @@
1
+ # Tor configuration for the initramfs onion unlock service.
2
+ # Baked into the initramfs as /etc/tor/torrc by the "tor" mkinitcpio hook;
3
+ # the onion keys 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
@@ -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
@@ -0,0 +1,5 @@
1
+ # Packages for LUKS remote unlock on Debian / Raspberry Pi OS (initramfs-tools)
2
+ cryptsetup
3
+ cryptsetup-initramfs
4
+ dropbear-initramfs
5
+ busybox
@@ -0,0 +1,3 @@
1
+ # Packages for the Tor onion unlock service in the initramfs (Debian)
2
+ tor
3
+ busybox # full applet set: ntpd for clock sync on RTC-less boards
@@ -0,0 +1,3 @@
1
+ # Packages for the Tor onion unlock service in the initramfs
2
+ tor
3
+ busybox # full applet set: ntpd for clock sync on RTC-less boards
@@ -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: